feat(aiot): 告警结束接口 + 持续时长更新 + first_frame_time存储
新增告警结束接口: - 新增EdgeAlarmResolve请求模型 - 新增POST /edge/resolve端点(无需认证,Edge设备调用) - 新增resolve_alarm服务方法:更新duration_ms、last_frame_time - 人员回岗/非工作时间自动设置alarm_status=CLOSED、handle_status=DONE 告警创建修复: - create_from_edge_report现在从ext_data读取first_frame_time写入数据库 - create_from_edge_report现在从ext_data读取duration_ms写入数据库 - 统一edge_node_id从ext_data提取
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user