diff --git a/app/routers/yudao_aiot_alarm.py b/app/routers/yudao_aiot_alarm.py index eb1d60b..f8a46b9 100644 --- a/app/routers/yudao_aiot_alarm.py +++ b/app/routers/yudao_aiot_alarm.py @@ -22,7 +22,7 @@ from app.yudao_compat import YudaoResponse, get_current_user from app.services.alarm_event_service import get_alarm_event_service, AlarmEventService from app.services.notification_service import get_notification_service from app.services.oss_storage import get_oss_storage -from app.schemas import EdgeAlarmReport +from app.schemas import EdgeAlarmReport, EdgeAlarmResolve from app.utils.logger import logger router = APIRouter(prefix="/admin-api/aiot/alarm", tags=["AIoT-告警"]) @@ -304,6 +304,28 @@ async def edge_alarm_report( }) +@router.post("/edge/resolve") +async def edge_alarm_resolve( + resolve: EdgeAlarmResolve, + service: AlarmEventService = Depends(get_alarm_event_service), +): + """ + 边缘端告警结束通知 + + Edge 在人员回岗确认或非工作时间到达时调用此接口, + 更新告警的 duration_ms 和 last_frame_time。 + """ + success = service.resolve_alarm( + alarm_id=resolve.alarm_id, + duration_ms=resolve.duration_ms, + last_frame_time=resolve.last_frame_time, + resolve_type=resolve.resolve_type, + ) + if not success: + return YudaoResponse.error(404, "告警不存在") + return YudaoResponse.success(True) + + # ==================== 辅助函数 ==================== OPS_ALARM_URL = "http://192.168.0.104:48080/admin-api/ops/alarm/receive" diff --git a/app/schemas.py b/app/schemas.py index 6a1c3a6..d2e022f 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -126,3 +126,11 @@ class EdgeAlarmReport(BaseModel): algorithm_code: Optional[str] = Field(None, max_length=64, description="算法编码") confidence_score: Optional[float] = Field(None, ge=0, le=1, description="置信度 0-1") ext_data: Optional[Dict[str, Any]] = Field(None, description="扩展数据 (bbox/target_class 等)") + + +class EdgeAlarmResolve(BaseModel): + """边缘端告警结束事件""" + alarm_id: str = Field(..., description="告警ID") + duration_ms: int = Field(..., ge=0, description="持续时长(毫秒)") + last_frame_time: str = Field(..., description="结束时间 ISO8601") + resolve_type: str = Field(..., description="结束类型: person_returned/non_work_time") diff --git a/app/services/alarm_event_service.py b/app/services/alarm_event_service.py index c627a59..af83b48 100644 --- a/app/services/alarm_event_service.py +++ b/app/services/alarm_event_service.py @@ -171,12 +171,21 @@ class AlarmEventService: alarm_type = data.get("alarm_type", "unknown") alarm_level = data.get("alarm_level") + ext_data = data.get("ext_data") or {} if alarm_level is None: # 从 ext_data 取 duration_ms - ext_data = data.get("ext_data") or {} duration_ms = ext_data.get("duration_ms") alarm_level = _determine_alarm_level(alarm_type, confidence or 0, duration_ms) + # 解析 first_frame_time(离岗开始时间,由 Edge 在 ext_data 中传递) + first_frame_time = None + first_frame_time_str = ext_data.get("first_frame_time") + if first_frame_time_str: + try: + first_frame_time = datetime.fromisoformat(first_frame_time_str.replace("Z", "+00:00")) + except ValueError: + first_frame_time = None + alarm = AlarmEvent( alarm_id=alarm_id, alarm_type=alarm_type, @@ -184,12 +193,14 @@ class AlarmEventService: device_id=data.get("device_id", "unknown"), scene_id=data.get("scene_id"), event_time=event_time, + first_frame_time=first_frame_time, + duration_ms=ext_data.get("duration_ms"), alarm_level=alarm_level, confidence_score=confidence, alarm_status="NEW", handle_status="UNHANDLED", - snapshot_url=data.get("snapshot_url"), # COS object_key - edge_node_id=data.get("ext_data", {}).get("edge_node_id") if data.get("ext_data") else None, + snapshot_url=data.get("snapshot_url"), + edge_node_id=ext_data.get("edge_node_id"), ) db.add(alarm) @@ -520,6 +531,46 @@ class AlarmEventService: finally: db.close() + def resolve_alarm(self, alarm_id: str, duration_ms: int, last_frame_time: str, resolve_type: str) -> bool: + """更新告警的持续时长和结束时间""" + db = get_session() + try: + alarm = db.query(AlarmEvent).filter(AlarmEvent.alarm_id == alarm_id).first() + if not alarm: + return False + + alarm.duration_ms = duration_ms + + # 解析 last_frame_time + try: + alarm.last_frame_time = datetime.fromisoformat(last_frame_time.replace("Z", "+00:00")) + except Exception: + alarm.last_frame_time = datetime.now(timezone.utc) + + # 如果是人员回岗,标记为自动关闭 + if resolve_type == "person_returned": + alarm.alarm_status = "CLOSED" + alarm.handle_status = "DONE" + alarm.handle_remark = "人员回岗自动关闭" + alarm.handled_at = datetime.now(timezone.utc) + elif resolve_type == "non_work_time": + alarm.alarm_status = "CLOSED" + alarm.handle_status = "DONE" + alarm.handle_remark = "非工作时间自动关闭" + alarm.handled_at = datetime.now(timezone.utc) + + alarm.updated_at = datetime.now(timezone.utc) + db.commit() + + logger.info(f"告警已更新结束信息: {alarm_id}, duration={duration_ms}ms, type={resolve_type}") + return True + except Exception as e: + db.rollback() + logger.error(f"更新告警结束信息失败: {e}") + return False + finally: + db.close() + def save_llm_analysis( self, alarm_id: str,