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,