功能:新增看板统计接口(趋势、设备Top、时段分布)

- GET /alert/trend?days=7 — 按天+按类型的告警趋势
- GET /alert/device-top?limit=10&days=7 — 告警最多设备排行
- GET /alert/hour-distribution?days=7 — 24小时告警分布
- 扩展 statistics 接口:增加 todayCount/yesterdayCount/pendingCount/
  handledCount/avgResponseMinutes 字段

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 17:06:08 +08:00
parent 3b64db7029
commit 67bd8881fa
2 changed files with 180 additions and 3 deletions

View File

@@ -290,6 +290,55 @@ async def get_statistics(
return YudaoResponse.success(stats)
@router.get("/alert/trend")
async def get_alert_trend(
days: int = Query(7, ge=1, le=90, description="统计天数"),
service: AlarmEventService = Depends(get_alarm_event_service),
current_user: dict = Depends(get_current_user)
):
"""获取告警趋势(按天+按类型分组)"""
data = service.get_trend(days=days)
return YudaoResponse.success(data)
@router.get("/alert/device-top")
async def get_alert_device_top(
limit: int = Query(10, ge=1, le=50, description="Top N"),
days: int = Query(7, ge=1, le=90, description="统计天数"),
service: AlarmEventService = Depends(get_alarm_event_service),
current_user: dict = Depends(get_current_user)
):
"""获取告警最多的设备 Top N"""
data = service.get_device_top(limit=limit, days=days)
# 批量查询摄像头名称
camera_service = get_camera_name_service()
device_ids = [d["deviceId"] for d in data]
camera_info_map = {}
try:
camera_info_map = await camera_service.get_camera_infos_batch(device_ids)
except Exception as e:
logger.warning(f"批量查询摄像头信息失败: {e}")
for item in data:
did = item["deviceId"]
info = camera_info_map.get(did)
item["deviceName"] = camera_service.format_display_name(did, info)
return YudaoResponse.success(data)
@router.get("/alert/hour-distribution")
async def get_alert_hour_distribution(
days: int = Query(7, ge=1, le=90, description="统计天数"),
service: AlarmEventService = Depends(get_alarm_event_service),
current_user: dict = Depends(get_current_user)
):
"""获取 24 小时告警分布"""
data = service.get_hour_distribution(days=days)
return YudaoResponse.success(data)
# ==================== 设备告警汇总 ====================
@router.get("/device-summary/page")

View File

@@ -3,10 +3,10 @@
处理 alarm_event / alarm_event_ext / alarm_llm_analysis 的 CRUD
"""
import uuid
from datetime import datetime, timezone
from datetime import datetime, timezone, timedelta
from typing import Optional, List, Dict, Any, Tuple
from sqlalchemy import func
from sqlalchemy import func, cast, Date, Integer, extract, text
from app.models import AlarmEvent, AlarmEventExt, AlarmLlmAnalysis, get_session
from app.services.oss_storage import get_oss_storage
@@ -492,11 +492,50 @@ class AlarmEventService:
db.close()
def get_statistics(self) -> Dict:
"""获取告警统计"""
"""获取告警统计(扩展版:含今日、昨日、待处理、平均响应时间)"""
db = get_session()
try:
total = db.query(AlarmEvent).count()
now = beijing_now()
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
yesterday_start = today_start - timedelta(days=1)
# 今日告警数
today_count = db.query(AlarmEvent).filter(
AlarmEvent.event_time >= today_start
).count()
# 昨日告警数
yesterday_count = db.query(AlarmEvent).filter(
AlarmEvent.event_time >= yesterday_start,
AlarmEvent.event_time < today_start,
).count()
# 待处理数
pending_count = db.query(AlarmEvent).filter(
AlarmEvent.handle_status == "UNHANDLED"
).count()
# 已处理数
handled_count = db.query(AlarmEvent).filter(
AlarmEvent.handle_status.in_(["DONE", "IGNORED"])
).count()
# 平均响应时间(从 event_time 到 handled_at只算已处理的
from sqlalchemy.sql.expression import literal_column
avg_response = db.query(
func.avg(
func.timestampdiff(
literal_column("MINUTE"),
AlarmEvent.event_time,
AlarmEvent.handled_at
)
)
).filter(
AlarmEvent.handled_at.isnot(None)
).scalar()
# 按 alarm_status 计数
by_status = {}
for row in db.query(
@@ -520,6 +559,11 @@ class AlarmEventService:
return {
"total": total,
"todayCount": today_count,
"yesterdayCount": yesterday_count,
"pendingCount": pending_count,
"handledCount": handled_count,
"avgResponseMinutes": round(float(avg_response), 1) if avg_response else None,
"byStatus": by_status,
"byType": by_type,
"byLevel": by_level,
@@ -527,6 +571,90 @@ class AlarmEventService:
finally:
db.close()
def get_trend(self, days: int = 7) -> List[Dict]:
"""获取告警趋势(按天+按类型分组)"""
db = get_session()
try:
now = beijing_now()
start = (now - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
rows = db.query(
func.date(AlarmEvent.event_time).label("date"),
AlarmEvent.alarm_type,
func.count(AlarmEvent.alarm_id).label("cnt"),
).filter(
AlarmEvent.event_time >= start
).group_by(
func.date(AlarmEvent.event_time),
AlarmEvent.alarm_type,
).all()
# 构造日期 → {type: count} 映射
date_map: Dict[str, Dict[str, int]] = {}
for r in rows:
d = str(r.date)
if d not in date_map:
date_map[d] = {}
date_map[d][r.alarm_type] = r.cnt
# 补全所有日期
result = []
for i in range(days):
d = (start + timedelta(days=i)).strftime("%Y-%m-%d")
type_counts = date_map.get(d, {})
total = sum(type_counts.values())
result.append({
"date": d,
"total": total,
**type_counts,
})
return result
finally:
db.close()
def get_device_top(self, limit: int = 10, days: int = 7) -> List[Dict]:
"""获取告警最多的设备 Top N"""
db = get_session()
try:
now = beijing_now()
start = (now - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
rows = db.query(
AlarmEvent.device_id,
func.count(AlarmEvent.alarm_id).label("cnt"),
).filter(
AlarmEvent.event_time >= start
).group_by(
AlarmEvent.device_id
).order_by(
func.count(AlarmEvent.alarm_id).desc()
).limit(limit).all()
return [{"deviceId": r.device_id, "count": r.cnt} for r in rows]
finally:
db.close()
def get_hour_distribution(self, days: int = 7) -> List[Dict]:
"""获取 24 小时告警分布"""
db = get_session()
try:
now = beijing_now()
start = (now - timedelta(days=days)).replace(hour=0, minute=0, second=0, microsecond=0)
rows = db.query(
func.hour(AlarmEvent.event_time).label("h"),
func.count(AlarmEvent.alarm_id).label("cnt"),
).filter(
AlarmEvent.event_time >= start
).group_by(
func.hour(AlarmEvent.event_time)
).all()
hour_map = {r.h: r.cnt for r in rows}
return [{"hour": h, "count": hour_map.get(h, 0)} for h in range(24)]
finally:
db.close()
def get_device_summary(
self,
page: int = 1,