功能:新增看板统计接口(趋势、设备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:
@@ -290,6 +290,55 @@ async def get_statistics(
|
|||||||
return YudaoResponse.success(stats)
|
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")
|
@router.get("/device-summary/page")
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
处理 alarm_event / alarm_event_ext / alarm_llm_analysis 的 CRUD
|
处理 alarm_event / alarm_event_ext / alarm_llm_analysis 的 CRUD
|
||||||
"""
|
"""
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone, timedelta
|
||||||
from typing import Optional, List, Dict, Any, Tuple
|
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.models import AlarmEvent, AlarmEventExt, AlarmLlmAnalysis, get_session
|
||||||
from app.services.oss_storage import get_oss_storage
|
from app.services.oss_storage import get_oss_storage
|
||||||
@@ -492,11 +492,50 @@ class AlarmEventService:
|
|||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
def get_statistics(self) -> Dict:
|
def get_statistics(self) -> Dict:
|
||||||
"""获取告警统计"""
|
"""获取告警统计(扩展版:含今日、昨日、待处理、平均响应时间)"""
|
||||||
db = get_session()
|
db = get_session()
|
||||||
try:
|
try:
|
||||||
total = db.query(AlarmEvent).count()
|
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 计数
|
# 按 alarm_status 计数
|
||||||
by_status = {}
|
by_status = {}
|
||||||
for row in db.query(
|
for row in db.query(
|
||||||
@@ -520,6 +559,11 @@ class AlarmEventService:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"total": total,
|
"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,
|
"byStatus": by_status,
|
||||||
"byType": by_type,
|
"byType": by_type,
|
||||||
"byLevel": by_level,
|
"byLevel": by_level,
|
||||||
@@ -527,6 +571,90 @@ class AlarmEventService:
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
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(
|
def get_device_summary(
|
||||||
self,
|
self,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
|
|||||||
Reference in New Issue
Block a user