2026-02-06 16:39:39 +08:00
|
|
|
|
"""
|
2026-02-09 17:47:35 +08:00
|
|
|
|
AIoT 告警路由 - 芋道规范(新三表结构)
|
2026-02-06 16:39:39 +08:00
|
|
|
|
|
|
|
|
|
|
统一到 /admin-api/aiot/alarm 命名空间,与 aiot 平台架构对齐。
|
|
|
|
|
|
|
|
|
|
|
|
API 路径规范:
|
|
|
|
|
|
- /admin-api/aiot/alarm/alert/page - 分页查询
|
|
|
|
|
|
- /admin-api/aiot/alarm/alert/get - 获取详情
|
|
|
|
|
|
- /admin-api/aiot/alarm/alert/handle - 处理告警
|
|
|
|
|
|
- /admin-api/aiot/alarm/alert/delete - 删除告警
|
|
|
|
|
|
- /admin-api/aiot/alarm/alert/statistics - 获取统计
|
2026-02-09 17:47:35 +08:00
|
|
|
|
- /admin-api/aiot/alarm/device-summary/page - 设备告警汇总
|
2026-02-06 16:39:39 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Query, Depends, HTTPException
|
|
|
|
|
|
from typing import Optional
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
from app.yudao_compat import YudaoResponse, get_current_user
|
2026-02-09 17:47:35 +08:00
|
|
|
|
from app.services.alarm_event_service import get_alarm_event_service, AlarmEventService
|
|
|
|
|
|
from app.services.oss_storage import get_oss_storage
|
2026-02-06 16:39:39 +08:00
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/admin-api/aiot/alarm", tags=["AIoT-告警"])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-09 17:47:35 +08:00
|
|
|
|
def _alarm_to_camel(alarm_dict: dict) -> dict:
|
|
|
|
|
|
"""将 alarm_event 字典转换为前端 camelCase 格式"""
|
|
|
|
|
|
# snapshot_url: 如果是 COS object_key,转为预签名 URL
|
|
|
|
|
|
storage = get_oss_storage()
|
|
|
|
|
|
snapshot_url = alarm_dict.get("snapshot_url")
|
|
|
|
|
|
if snapshot_url:
|
|
|
|
|
|
snapshot_url = storage.get_url(snapshot_url)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"alarmId": alarm_dict.get("alarm_id"),
|
|
|
|
|
|
"alarmType": alarm_dict.get("alarm_type"),
|
|
|
|
|
|
"alarmTypeName": _get_alarm_type_name(alarm_dict.get("alarm_type")),
|
|
|
|
|
|
"algorithmCode": alarm_dict.get("algorithm_code"),
|
|
|
|
|
|
"deviceId": alarm_dict.get("device_id"),
|
|
|
|
|
|
"deviceName": alarm_dict.get("device_id"),
|
|
|
|
|
|
"sceneId": alarm_dict.get("scene_id"),
|
|
|
|
|
|
"eventTime": alarm_dict.get("event_time"),
|
|
|
|
|
|
"firstFrameTime": alarm_dict.get("first_frame_time"),
|
|
|
|
|
|
"lastFrameTime": alarm_dict.get("last_frame_time"),
|
|
|
|
|
|
"durationMs": alarm_dict.get("duration_ms"),
|
|
|
|
|
|
"alarmLevel": alarm_dict.get("alarm_level"),
|
|
|
|
|
|
"confidenceScore": alarm_dict.get("confidence_score"),
|
|
|
|
|
|
"alarmStatus": alarm_dict.get("alarm_status"),
|
|
|
|
|
|
"handleStatus": alarm_dict.get("handle_status"),
|
|
|
|
|
|
"snapshotUrl": snapshot_url,
|
|
|
|
|
|
"videoUrl": alarm_dict.get("video_url"),
|
|
|
|
|
|
"edgeNodeId": alarm_dict.get("edge_node_id"),
|
|
|
|
|
|
"handler": alarm_dict.get("handler"),
|
|
|
|
|
|
"handleRemark": alarm_dict.get("handle_remark"),
|
|
|
|
|
|
"handledAt": alarm_dict.get("handled_at"),
|
|
|
|
|
|
"createdAt": alarm_dict.get("created_at"),
|
|
|
|
|
|
"updatedAt": alarm_dict.get("updated_at"),
|
|
|
|
|
|
# 扩展数据(详情时可能包含)
|
|
|
|
|
|
"ext": alarm_dict.get("ext"),
|
|
|
|
|
|
"llmAnalyses": alarm_dict.get("llm_analyses"),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-06 16:39:39 +08:00
|
|
|
|
# ==================== 告警管理 ====================
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/alert/page")
|
|
|
|
|
|
async def get_alert_page(
|
|
|
|
|
|
pageNo: int = Query(1, ge=1, description="页码"),
|
|
|
|
|
|
pageSize: int = Query(20, ge=1, le=100, description="每页大小"),
|
2026-02-09 17:47:35 +08:00
|
|
|
|
deviceId: Optional[str] = Query(None, description="摄像头/设备ID"),
|
|
|
|
|
|
edgeNodeId: Optional[str] = Query(None, description="边缘节点ID"),
|
|
|
|
|
|
alarmType: Optional[str] = Query(None, description="告警类型"),
|
|
|
|
|
|
alarmStatus: Optional[str] = Query(None, description="告警状态: NEW/CONFIRMED/FALSE/CLOSED"),
|
|
|
|
|
|
alarmLevel: Optional[int] = Query(None, description="告警级别: 1提醒/2一般/3严重/4紧急"),
|
2026-02-06 16:39:39 +08:00
|
|
|
|
startTime: Optional[datetime] = Query(None, description="开始时间"),
|
|
|
|
|
|
endTime: Optional[datetime] = Query(None, description="结束时间"),
|
2026-02-09 17:47:35 +08:00
|
|
|
|
service: AlarmEventService = Depends(get_alarm_event_service),
|
2026-02-06 16:39:39 +08:00
|
|
|
|
current_user: dict = Depends(get_current_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""分页查询告警列表"""
|
2026-02-09 17:47:35 +08:00
|
|
|
|
alarms, total = service.get_alarms(
|
2026-02-06 16:39:39 +08:00
|
|
|
|
device_id=deviceId,
|
2026-02-09 17:47:35 +08:00
|
|
|
|
alarm_type=alarmType,
|
|
|
|
|
|
alarm_status=alarmStatus,
|
|
|
|
|
|
alarm_level=alarmLevel,
|
|
|
|
|
|
edge_node_id=edgeNodeId,
|
2026-02-06 16:39:39 +08:00
|
|
|
|
start_time=startTime,
|
|
|
|
|
|
end_time=endTime,
|
|
|
|
|
|
page=pageNo,
|
|
|
|
|
|
page_size=pageSize,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-09 17:47:35 +08:00
|
|
|
|
alarm_list = [_alarm_to_camel(a.to_dict()) for a in alarms]
|
2026-02-06 16:39:39 +08:00
|
|
|
|
|
|
|
|
|
|
return YudaoResponse.page(
|
2026-02-09 17:47:35 +08:00
|
|
|
|
list_data=alarm_list,
|
2026-02-06 16:39:39 +08:00
|
|
|
|
total=total,
|
|
|
|
|
|
page_no=pageNo,
|
|
|
|
|
|
page_size=pageSize
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/alert/get")
|
|
|
|
|
|
async def get_alert(
|
2026-02-09 17:47:35 +08:00
|
|
|
|
alarmId: str = Query(..., description="告警ID"),
|
|
|
|
|
|
service: AlarmEventService = Depends(get_alarm_event_service),
|
2026-02-06 16:39:39 +08:00
|
|
|
|
current_user: dict = Depends(get_current_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""获取告警详情"""
|
2026-02-09 17:47:35 +08:00
|
|
|
|
alarm_dict = service.get_alarm(alarmId)
|
|
|
|
|
|
if not alarm_dict:
|
2026-02-06 16:39:39 +08:00
|
|
|
|
raise HTTPException(status_code=404, detail="告警不存在")
|
|
|
|
|
|
|
2026-02-09 17:47:35 +08:00
|
|
|
|
return YudaoResponse.success(_alarm_to_camel(alarm_dict))
|
2026-02-06 16:39:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.put("/alert/handle")
|
|
|
|
|
|
async def handle_alert(
|
2026-02-09 17:47:35 +08:00
|
|
|
|
alarmId: str = Query(..., description="告警ID"),
|
|
|
|
|
|
alarmStatus: Optional[str] = Query(None, description="告警状态: CONFIRMED/FALSE/CLOSED"),
|
|
|
|
|
|
handleStatus: Optional[str] = Query(None, description="处理状态: HANDLING/DONE"),
|
2026-02-06 16:39:39 +08:00
|
|
|
|
remark: Optional[str] = Query(None, description="处理备注"),
|
2026-02-09 17:47:35 +08:00
|
|
|
|
service: AlarmEventService = Depends(get_alarm_event_service),
|
2026-02-06 16:39:39 +08:00
|
|
|
|
current_user: dict = Depends(get_current_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""处理告警"""
|
2026-02-09 17:47:35 +08:00
|
|
|
|
handler = current_user.get("username", "admin")
|
|
|
|
|
|
|
|
|
|
|
|
alarm = service.handle_alarm(
|
|
|
|
|
|
alarm_id=alarmId,
|
|
|
|
|
|
alarm_status=alarmStatus,
|
|
|
|
|
|
handle_status=handleStatus,
|
|
|
|
|
|
remark=remark,
|
|
|
|
|
|
handler=handler,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not alarm:
|
2026-02-06 16:39:39 +08:00
|
|
|
|
raise HTTPException(status_code=404, detail="告警不存在")
|
|
|
|
|
|
|
|
|
|
|
|
return YudaoResponse.success(True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/alert/delete")
|
|
|
|
|
|
async def delete_alert(
|
2026-02-09 17:47:35 +08:00
|
|
|
|
alarmId: str = Query(..., description="告警ID"),
|
|
|
|
|
|
service: AlarmEventService = Depends(get_alarm_event_service),
|
2026-02-06 16:39:39 +08:00
|
|
|
|
current_user: dict = Depends(get_current_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""删除告警"""
|
2026-02-09 17:47:35 +08:00
|
|
|
|
success = service.delete_alarm(alarmId)
|
2026-02-06 16:39:39 +08:00
|
|
|
|
if not success:
|
|
|
|
|
|
raise HTTPException(status_code=404, detail="告警不存在")
|
|
|
|
|
|
|
|
|
|
|
|
return YudaoResponse.success(True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/alert/statistics")
|
|
|
|
|
|
async def get_statistics(
|
2026-02-09 17:47:35 +08:00
|
|
|
|
service: AlarmEventService = Depends(get_alarm_event_service),
|
2026-02-06 16:39:39 +08:00
|
|
|
|
current_user: dict = Depends(get_current_user)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""获取告警统计"""
|
|
|
|
|
|
stats = service.get_statistics()
|
|
|
|
|
|
|
2026-02-09 17:47:35 +08:00
|
|
|
|
return YudaoResponse.success(stats)
|
2026-02-06 16:39:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-09 17:47:35 +08:00
|
|
|
|
# ==================== 设备告警汇总 ====================
|
2026-02-06 16:39:39 +08:00
|
|
|
|
|
2026-02-09 17:47:35 +08:00
|
|
|
|
@router.get("/device-summary/page")
|
|
|
|
|
|
async def get_device_summary_page(
|
2026-02-06 16:39:39 +08:00
|
|
|
|
pageNo: int = Query(1, ge=1, description="页码"),
|
|
|
|
|
|
pageSize: int = Query(20, ge=1, le=100, description="每页大小"),
|
2026-02-09 17:47:35 +08:00
|
|
|
|
service: AlarmEventService = Depends(get_alarm_event_service),
|
2026-02-06 16:39:39 +08:00
|
|
|
|
current_user: dict = Depends(get_current_user)
|
|
|
|
|
|
):
|
2026-02-09 17:47:35 +08:00
|
|
|
|
"""获取设备告警汇总(分页)"""
|
|
|
|
|
|
result = service.get_device_summary(page=pageNo, page_size=pageSize)
|
2026-02-06 16:39:39 +08:00
|
|
|
|
|
|
|
|
|
|
return YudaoResponse.page(
|
|
|
|
|
|
list_data=result.get("list", []),
|
|
|
|
|
|
total=result.get("total", 0),
|
|
|
|
|
|
page_no=pageNo,
|
|
|
|
|
|
page_size=pageSize
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ==================== 辅助函数 ====================
|
|
|
|
|
|
|
2026-02-09 17:47:35 +08:00
|
|
|
|
def _get_alarm_type_name(alarm_type: Optional[str]) -> str:
|
2026-02-06 16:39:39 +08:00
|
|
|
|
"""获取告警类型名称"""
|
|
|
|
|
|
type_names = {
|
|
|
|
|
|
"leave_post": "离岗检测",
|
|
|
|
|
|
"intrusion": "周界入侵",
|
|
|
|
|
|
"crowd": "人群聚集",
|
|
|
|
|
|
"fire": "火焰检测",
|
|
|
|
|
|
"smoke": "烟雾检测",
|
|
|
|
|
|
"fall": "跌倒检测",
|
|
|
|
|
|
"helmet": "安全帽检测",
|
|
|
|
|
|
"unknown": "未知类型",
|
|
|
|
|
|
}
|
2026-02-09 17:47:35 +08:00
|
|
|
|
return type_names.get(alarm_type, alarm_type or "未知类型")
|