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.alarm_event_service import get_alarm_event_service, AlarmEventService
|
||||||
from app.services.notification_service import get_notification_service
|
from app.services.notification_service import get_notification_service
|
||||||
from app.services.oss_storage import get_oss_storage
|
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
|
from app.utils.logger import logger
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin-api/aiot/alarm", tags=["AIoT-告警"])
|
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"
|
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="算法编码")
|
algorithm_code: Optional[str] = Field(None, max_length=64, description="算法编码")
|
||||||
confidence_score: Optional[float] = Field(None, ge=0, le=1, description="置信度 0-1")
|
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 等)")
|
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_type = data.get("alarm_type", "unknown")
|
||||||
alarm_level = data.get("alarm_level")
|
alarm_level = data.get("alarm_level")
|
||||||
|
ext_data = data.get("ext_data") or {}
|
||||||
if alarm_level is None:
|
if alarm_level is None:
|
||||||
# 从 ext_data 取 duration_ms
|
# 从 ext_data 取 duration_ms
|
||||||
ext_data = data.get("ext_data") or {}
|
|
||||||
duration_ms = ext_data.get("duration_ms")
|
duration_ms = ext_data.get("duration_ms")
|
||||||
alarm_level = _determine_alarm_level(alarm_type, confidence or 0, 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 = AlarmEvent(
|
||||||
alarm_id=alarm_id,
|
alarm_id=alarm_id,
|
||||||
alarm_type=alarm_type,
|
alarm_type=alarm_type,
|
||||||
@@ -184,12 +193,14 @@ class AlarmEventService:
|
|||||||
device_id=data.get("device_id", "unknown"),
|
device_id=data.get("device_id", "unknown"),
|
||||||
scene_id=data.get("scene_id"),
|
scene_id=data.get("scene_id"),
|
||||||
event_time=event_time,
|
event_time=event_time,
|
||||||
|
first_frame_time=first_frame_time,
|
||||||
|
duration_ms=ext_data.get("duration_ms"),
|
||||||
alarm_level=alarm_level,
|
alarm_level=alarm_level,
|
||||||
confidence_score=confidence,
|
confidence_score=confidence,
|
||||||
alarm_status="NEW",
|
alarm_status="NEW",
|
||||||
handle_status="UNHANDLED",
|
handle_status="UNHANDLED",
|
||||||
snapshot_url=data.get("snapshot_url"), # COS object_key
|
snapshot_url=data.get("snapshot_url"),
|
||||||
edge_node_id=data.get("ext_data", {}).get("edge_node_id") if data.get("ext_data") else None,
|
edge_node_id=ext_data.get("edge_node_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
db.add(alarm)
|
db.add(alarm)
|
||||||
@@ -520,6 +531,46 @@ class AlarmEventService:
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
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(
|
def save_llm_analysis(
|
||||||
self,
|
self,
|
||||||
alarm_id: str,
|
alarm_id: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user