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:
2026-02-11 17:56:02 +08:00
parent cf41db2983
commit f3af9cac22
3 changed files with 85 additions and 4 deletions

View File

@@ -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"

View File

@@ -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")

View File

@@ -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,