From 67bd8881fafd72966df15bb67cb95dc2b231d85a Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Wed, 18 Mar 2026 17:06:08 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E7=9C=8B=E6=9D=BF=E7=BB=9F=E8=AE=A1=E6=8E=A5=E5=8F=A3=EF=BC=88?= =?UTF-8?q?=E8=B6=8B=E5=8A=BF=E3=80=81=E8=AE=BE=E5=A4=87Top=E3=80=81?= =?UTF-8?q?=E6=97=B6=E6=AE=B5=E5=88=86=E5=B8=83=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- app/routers/yudao_aiot_alarm.py | 49 ++++++++++ app/services/alarm_event_service.py | 134 +++++++++++++++++++++++++++- 2 files changed, 180 insertions(+), 3 deletions(-) diff --git a/app/routers/yudao_aiot_alarm.py b/app/routers/yudao_aiot_alarm.py index 35cf7c4..dfb4ed4 100644 --- a/app/routers/yudao_aiot_alarm.py +++ b/app/routers/yudao_aiot_alarm.py @@ -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") diff --git a/app/services/alarm_event_service.py b/app/services/alarm_event_service.py index 6d684a9..0fdda4d 100644 --- a/app/services/alarm_event_service.py +++ b/app/services/alarm_event_service.py @@ -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,