feat(aiot): 本地截图回退访问 + 告警API前端兼容
- 挂载 Edge 截图目录为 /captures 静态文件(COS 不可用时回退) - 挂载 /uploads 静态文件目录 - _alarm_to_camel 支持 local: 前缀转 /captures/ URL - 告警分页/详情/处理/删除接口兼容前端旧字段名(id、cameraId、status 等) - 设备告警汇总添加前端兼容别名(cameraId、pendingCount 等) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
11
app/main.py
11
app/main.py
@@ -3,6 +3,7 @@ from contextlib import asynccontextmanager
|
|||||||
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -85,6 +86,16 @@ app.include_router(yudao_aiot_storage_router)
|
|||||||
# 注册芋道格式异常处理器
|
# 注册芋道格式异常处理器
|
||||||
app.add_exception_handler(HTTPException, yudao_exception_handler)
|
app.add_exception_handler(HTTPException, yudao_exception_handler)
|
||||||
|
|
||||||
|
# ==================== 静态文件(本地截图) ====================
|
||||||
|
_uploads_dir = Path("uploads")
|
||||||
|
_uploads_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
app.mount("/uploads", StaticFiles(directory=str(_uploads_dir)), name="uploads")
|
||||||
|
|
||||||
|
# Edge 本地截图目录(COS 未配置时的回退访问路径)
|
||||||
|
_edge_captures_dir = Path(r"C:\Users\16337\PycharmProjects\ai_edge\data\captures")
|
||||||
|
if _edge_captures_dir.exists():
|
||||||
|
app.mount("/captures", StaticFiles(directory=str(_edge_captures_dir)), name="captures")
|
||||||
|
|
||||||
|
|
||||||
def get_alert_svc():
|
def get_alert_svc():
|
||||||
return alert_service
|
return alert_service
|
||||||
|
|||||||
@@ -26,15 +26,40 @@ router = APIRouter(prefix="/admin-api/aiot/alarm", tags=["AIoT-告警"])
|
|||||||
|
|
||||||
|
|
||||||
def _alarm_to_camel(alarm_dict: dict) -> dict:
|
def _alarm_to_camel(alarm_dict: dict) -> dict:
|
||||||
"""将 alarm_event 字典转换为前端 camelCase 格式"""
|
"""将 alarm_event 字典转换为前端 camelCase 格式(兼容前端旧字段名)"""
|
||||||
# snapshot_url: 如果是 COS object_key,转为预签名 URL
|
# snapshot_url: 根据存储方式转为可访问 URL
|
||||||
storage = get_oss_storage()
|
storage = get_oss_storage()
|
||||||
snapshot_url = alarm_dict.get("snapshot_url")
|
snapshot_url = alarm_dict.get("snapshot_url")
|
||||||
if snapshot_url:
|
if snapshot_url:
|
||||||
|
if snapshot_url.startswith("local:"):
|
||||||
|
# 本地截图(COS 未配置时的回退路径)
|
||||||
|
snapshot_url = "/captures/" + snapshot_url[6:]
|
||||||
|
else:
|
||||||
snapshot_url = storage.get_url(snapshot_url)
|
snapshot_url = storage.get_url(snapshot_url)
|
||||||
|
|
||||||
|
# alarm_level int → 文本映射
|
||||||
|
alarm_level = alarm_dict.get("alarm_level")
|
||||||
|
level_map = {1: "low", 2: "medium", 3: "high", 4: "critical"}
|
||||||
|
level_str = level_map.get(alarm_level, "medium") if alarm_level else "medium"
|
||||||
|
|
||||||
|
# alarm_status → 前端 status 映射
|
||||||
|
alarm_status = alarm_dict.get("alarm_status", "NEW")
|
||||||
|
status_map = {"NEW": "pending", "CONFIRMED": "handled", "FALSE": "ignored", "CLOSED": "handled"}
|
||||||
|
status_str = status_map.get(alarm_status, "pending")
|
||||||
|
|
||||||
|
# confidence_score 0-1 → 百分比
|
||||||
|
confidence_score = alarm_dict.get("confidence_score")
|
||||||
|
confidence_pct = round(confidence_score * 100) if confidence_score is not None else None
|
||||||
|
|
||||||
|
# duration_ms → 分钟
|
||||||
|
duration_ms = alarm_dict.get("duration_ms")
|
||||||
|
duration_minutes = round(duration_ms / 60000, 1) if duration_ms else None
|
||||||
|
|
||||||
|
alarm_id = alarm_dict.get("alarm_id")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"alarmId": alarm_dict.get("alarm_id"),
|
# 新字段(三表结构)
|
||||||
|
"alarmId": alarm_id,
|
||||||
"alarmType": alarm_dict.get("alarm_type"),
|
"alarmType": alarm_dict.get("alarm_type"),
|
||||||
"alarmTypeName": _get_alarm_type_name(alarm_dict.get("alarm_type")),
|
"alarmTypeName": _get_alarm_type_name(alarm_dict.get("alarm_type")),
|
||||||
"algorithmCode": alarm_dict.get("algorithm_code"),
|
"algorithmCode": alarm_dict.get("algorithm_code"),
|
||||||
@@ -44,10 +69,10 @@ def _alarm_to_camel(alarm_dict: dict) -> dict:
|
|||||||
"eventTime": alarm_dict.get("event_time"),
|
"eventTime": alarm_dict.get("event_time"),
|
||||||
"firstFrameTime": alarm_dict.get("first_frame_time"),
|
"firstFrameTime": alarm_dict.get("first_frame_time"),
|
||||||
"lastFrameTime": alarm_dict.get("last_frame_time"),
|
"lastFrameTime": alarm_dict.get("last_frame_time"),
|
||||||
"durationMs": alarm_dict.get("duration_ms"),
|
"durationMs": duration_ms,
|
||||||
"alarmLevel": alarm_dict.get("alarm_level"),
|
"alarmLevel": alarm_level,
|
||||||
"confidenceScore": alarm_dict.get("confidence_score"),
|
"confidenceScore": confidence_score,
|
||||||
"alarmStatus": alarm_dict.get("alarm_status"),
|
"alarmStatus": alarm_status,
|
||||||
"handleStatus": alarm_dict.get("handle_status"),
|
"handleStatus": alarm_dict.get("handle_status"),
|
||||||
"snapshotUrl": snapshot_url,
|
"snapshotUrl": snapshot_url,
|
||||||
"videoUrl": alarm_dict.get("video_url"),
|
"videoUrl": alarm_dict.get("video_url"),
|
||||||
@@ -57,9 +82,22 @@ def _alarm_to_camel(alarm_dict: dict) -> dict:
|
|||||||
"handledAt": alarm_dict.get("handled_at"),
|
"handledAt": alarm_dict.get("handled_at"),
|
||||||
"createdAt": alarm_dict.get("created_at"),
|
"createdAt": alarm_dict.get("created_at"),
|
||||||
"updatedAt": alarm_dict.get("updated_at"),
|
"updatedAt": alarm_dict.get("updated_at"),
|
||||||
# 扩展数据(详情时可能包含)
|
|
||||||
"ext": alarm_dict.get("ext"),
|
"ext": alarm_dict.get("ext"),
|
||||||
"llmAnalyses": alarm_dict.get("llm_analyses"),
|
"llmAnalyses": alarm_dict.get("llm_analyses"),
|
||||||
|
# 兼容前端旧字段名
|
||||||
|
"id": alarm_id,
|
||||||
|
"alertNo": alarm_id,
|
||||||
|
"cameraId": alarm_dict.get("device_id"),
|
||||||
|
"alertType": alarm_dict.get("alarm_type"),
|
||||||
|
"alertTypeName": _get_alarm_type_name(alarm_dict.get("alarm_type")),
|
||||||
|
"confidence": confidence_pct,
|
||||||
|
"durationMinutes": duration_minutes,
|
||||||
|
"status": status_str,
|
||||||
|
"level": level_str,
|
||||||
|
"triggerTime": alarm_dict.get("event_time"),
|
||||||
|
"ossUrl": snapshot_url,
|
||||||
|
"message": None,
|
||||||
|
"bbox": (alarm_dict.get("ext") or {}).get("ext_data", {}).get("bbox") if isinstance(alarm_dict.get("ext"), dict) else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -70,9 +108,10 @@ async def get_alert_page(
|
|||||||
pageNo: int = Query(1, ge=1, description="页码"),
|
pageNo: int = Query(1, ge=1, description="页码"),
|
||||||
pageSize: int = Query(20, ge=1, le=100, description="每页大小"),
|
pageSize: int = Query(20, ge=1, le=100, description="每页大小"),
|
||||||
deviceId: Optional[str] = Query(None, description="摄像头/设备ID"),
|
deviceId: Optional[str] = Query(None, description="摄像头/设备ID"),
|
||||||
|
cameraId: Optional[str] = Query(None, description="摄像头ID(兼容旧接口)"),
|
||||||
edgeNodeId: Optional[str] = Query(None, description="边缘节点ID"),
|
edgeNodeId: Optional[str] = Query(None, description="边缘节点ID"),
|
||||||
alarmType: Optional[str] = Query(None, description="告警类型"),
|
alarmType: Optional[str] = Query(None, alias="alertType", description="告警类型"),
|
||||||
alarmStatus: Optional[str] = Query(None, description="告警状态: NEW/CONFIRMED/FALSE/CLOSED"),
|
alarmStatus: Optional[str] = Query(None, alias="status", description="告警状态"),
|
||||||
alarmLevel: Optional[int] = Query(None, description="告警级别: 1提醒/2一般/3严重/4紧急"),
|
alarmLevel: Optional[int] = Query(None, description="告警级别: 1提醒/2一般/3严重/4紧急"),
|
||||||
startTime: Optional[datetime] = Query(None, description="开始时间"),
|
startTime: Optional[datetime] = Query(None, description="开始时间"),
|
||||||
endTime: Optional[datetime] = Query(None, description="结束时间"),
|
endTime: Optional[datetime] = Query(None, description="结束时间"),
|
||||||
@@ -80,8 +119,13 @@ async def get_alert_page(
|
|||||||
current_user: dict = Depends(get_current_user)
|
current_user: dict = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""分页查询告警列表"""
|
"""分页查询告警列表"""
|
||||||
|
# 兼容旧接口的 status 文本转换
|
||||||
|
if alarmStatus and alarmStatus in ("pending", "handled", "ignored"):
|
||||||
|
status_convert = {"pending": "NEW", "handled": "CONFIRMED", "ignored": "FALSE"}
|
||||||
|
alarmStatus = status_convert.get(alarmStatus, alarmStatus)
|
||||||
|
|
||||||
alarms, total = service.get_alarms(
|
alarms, total = service.get_alarms(
|
||||||
device_id=deviceId,
|
device_id=deviceId or cameraId,
|
||||||
alarm_type=alarmType,
|
alarm_type=alarmType,
|
||||||
alarm_status=alarmStatus,
|
alarm_status=alarmStatus,
|
||||||
alarm_level=alarmLevel,
|
alarm_level=alarmLevel,
|
||||||
@@ -104,12 +148,16 @@ async def get_alert_page(
|
|||||||
|
|
||||||
@router.get("/alert/get")
|
@router.get("/alert/get")
|
||||||
async def get_alert(
|
async def get_alert(
|
||||||
alarmId: str = Query(..., description="告警ID"),
|
alarmId: Optional[str] = Query(None, description="告警ID"),
|
||||||
|
id: Optional[str] = Query(None, description="告警ID(兼容旧接口)"),
|
||||||
service: AlarmEventService = Depends(get_alarm_event_service),
|
service: AlarmEventService = Depends(get_alarm_event_service),
|
||||||
current_user: dict = Depends(get_current_user)
|
current_user: dict = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""获取告警详情"""
|
"""获取告警详情"""
|
||||||
alarm_dict = service.get_alarm(alarmId)
|
alarm_id = alarmId or id
|
||||||
|
if not alarm_id:
|
||||||
|
raise HTTPException(status_code=400, detail="缺少 alarmId 或 id 参数")
|
||||||
|
alarm_dict = service.get_alarm(alarm_id)
|
||||||
if not alarm_dict:
|
if not alarm_dict:
|
||||||
raise HTTPException(status_code=404, detail="告警不存在")
|
raise HTTPException(status_code=404, detail="告警不存在")
|
||||||
|
|
||||||
@@ -118,18 +166,28 @@ async def get_alert(
|
|||||||
|
|
||||||
@router.put("/alert/handle")
|
@router.put("/alert/handle")
|
||||||
async def handle_alert(
|
async def handle_alert(
|
||||||
alarmId: str = Query(..., description="告警ID"),
|
alarmId: Optional[str] = Query(None, description="告警ID"),
|
||||||
|
id: Optional[str] = Query(None, description="告警ID(兼容)"),
|
||||||
alarmStatus: Optional[str] = Query(None, description="告警状态: CONFIRMED/FALSE/CLOSED"),
|
alarmStatus: Optional[str] = Query(None, description="告警状态: CONFIRMED/FALSE/CLOSED"),
|
||||||
handleStatus: Optional[str] = Query(None, description="处理状态: HANDLING/DONE"),
|
handleStatus: Optional[str] = Query(None, description="处理状态: HANDLING/DONE"),
|
||||||
|
status: Optional[str] = Query(None, description="处理状态(兼容旧接口)"),
|
||||||
remark: Optional[str] = Query(None, description="处理备注"),
|
remark: Optional[str] = Query(None, description="处理备注"),
|
||||||
service: AlarmEventService = Depends(get_alarm_event_service),
|
service: AlarmEventService = Depends(get_alarm_event_service),
|
||||||
current_user: dict = Depends(get_current_user)
|
current_user: dict = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""处理告警"""
|
"""处理告警"""
|
||||||
|
alarm_id = alarmId or id
|
||||||
|
if not alarm_id:
|
||||||
|
raise HTTPException(status_code=400, detail="缺少 alarmId 或 id 参数")
|
||||||
handler = current_user.get("username", "admin")
|
handler = current_user.get("username", "admin")
|
||||||
|
|
||||||
|
# 兼容旧接口: status=handled → alarmStatus=CONFIRMED, status=ignored → alarmStatus=FALSE
|
||||||
|
if not alarmStatus and status:
|
||||||
|
status_convert = {"handled": "CONFIRMED", "ignored": "FALSE", "resolved": "CLOSED"}
|
||||||
|
alarmStatus = status_convert.get(status, status.upper())
|
||||||
|
|
||||||
alarm = service.handle_alarm(
|
alarm = service.handle_alarm(
|
||||||
alarm_id=alarmId,
|
alarm_id=alarm_id,
|
||||||
alarm_status=alarmStatus,
|
alarm_status=alarmStatus,
|
||||||
handle_status=handleStatus,
|
handle_status=handleStatus,
|
||||||
remark=remark,
|
remark=remark,
|
||||||
@@ -143,12 +201,16 @@ async def handle_alert(
|
|||||||
|
|
||||||
@router.delete("/alert/delete")
|
@router.delete("/alert/delete")
|
||||||
async def delete_alert(
|
async def delete_alert(
|
||||||
alarmId: str = Query(..., description="告警ID"),
|
alarmId: Optional[str] = Query(None, description="告警ID"),
|
||||||
|
id: Optional[str] = Query(None, description="告警ID(兼容)"),
|
||||||
service: AlarmEventService = Depends(get_alarm_event_service),
|
service: AlarmEventService = Depends(get_alarm_event_service),
|
||||||
current_user: dict = Depends(get_current_user)
|
current_user: dict = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""删除告警"""
|
"""删除告警"""
|
||||||
success = service.delete_alarm(alarmId)
|
alarm_id = alarmId or id
|
||||||
|
if not alarm_id:
|
||||||
|
raise HTTPException(status_code=400, detail="缺少 alarmId 或 id 参数")
|
||||||
|
success = service.delete_alarm(alarm_id)
|
||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=404, detail="告警不存在")
|
raise HTTPException(status_code=404, detail="告警不存在")
|
||||||
|
|
||||||
@@ -178,8 +240,19 @@ async def get_device_summary_page(
|
|||||||
"""获取设备告警汇总(分页)"""
|
"""获取设备告警汇总(分页)"""
|
||||||
result = service.get_device_summary(page=pageNo, page_size=pageSize)
|
result = service.get_device_summary(page=pageNo, page_size=pageSize)
|
||||||
|
|
||||||
|
# 添加前端兼容字段别名
|
||||||
|
compat_list = []
|
||||||
|
for item in result.get("list", []):
|
||||||
|
item["cameraId"] = item.get("deviceId")
|
||||||
|
item["cameraName"] = item.get("deviceName")
|
||||||
|
item["pendingCount"] = item.get("unhandledCount")
|
||||||
|
item["lastAlertTime"] = item.get("lastEventTime")
|
||||||
|
item["lastAlertType"] = item.get("lastAlarmType")
|
||||||
|
item["lastAlertTypeName"] = _get_alarm_type_name(item.get("lastAlarmType"))
|
||||||
|
compat_list.append(item)
|
||||||
|
|
||||||
return YudaoResponse.page(
|
return YudaoResponse.page(
|
||||||
list_data=result.get("list", []),
|
list_data=compat_list,
|
||||||
total=result.get("total", 0),
|
total=result.get("total", 0),
|
||||||
page_no=pageNo,
|
page_no=pageNo,
|
||||||
page_size=pageSize
|
page_size=pageSize
|
||||||
|
|||||||
Reference in New Issue
Block a user