Files
iot-device-management-service/app/services/alarm_event_service.py

618 lines
21 KiB
Python
Raw Normal View History

"""
告警事件服务新三表结构
处理 alarm_event / alarm_event_ext / alarm_llm_analysis CRUD
"""
import uuid
from datetime import datetime, timezone
from typing import Optional, List, Dict, Any, Tuple
from sqlalchemy import func
from app.models import AlarmEvent, AlarmEventExt, AlarmLlmAnalysis, get_session
from app.services.oss_storage import get_oss_storage
from app.utils.logger import logger
def generate_alarm_id() -> str:
"""生成告警ID: ALM + YYYYMMDDHHmmss + 8位uuid"""
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
unique_id = uuid.uuid4().hex[:8].upper()
return f"ALM{timestamp}{unique_id}"
def _determine_alarm_level(alarm_type: str, confidence: float, duration_ms: Optional[int] = None) -> int:
"""
根据告警类型置信度和持续时长确定告警级别
返回: 1提醒 2一般 3严重 4紧急
"""
if alarm_type == "intrusion":
return 3 # 严重
elif alarm_type == "leave_post":
if duration_ms and duration_ms > 30 * 60 * 1000:
return 3 # 严重
elif duration_ms and duration_ms > 10 * 60 * 1000:
return 2 # 一般
return 1 # 提醒
elif confidence and confidence > 0.9:
return 3 # 严重
elif confidence and confidence > 0.7:
return 2 # 一般
return 2 # 默认一般
class AlarmEventService:
"""告警事件服务"""
def __init__(self):
self.oss = get_oss_storage()
def create_from_mqtt(self, mqtt_data: Dict[str, Any]) -> Optional[AlarmEvent]:
"""从 MQTT 消息创建告警事件"""
db = get_session()
try:
alarm_id = generate_alarm_id()
# 解析时间
timestamp_str = mqtt_data.get("timestamp")
if timestamp_str:
try:
event_time = datetime.fromisoformat(timestamp_str.replace("Z", "+00:00"))
except ValueError:
event_time = datetime.now(timezone.utc)
else:
event_time = datetime.now(timezone.utc)
# 置信度保持 float 0-1
confidence = mqtt_data.get("confidence")
if confidence is not None:
confidence = float(confidence)
# 如果传入的是 0-100 范围的值,转为 0-1
if confidence > 1:
confidence = confidence / 100.0
# duration_minutes → duration_ms
duration_minutes = mqtt_data.get("duration_minutes")
duration_ms = None
if duration_minutes is not None:
duration_ms = int(float(duration_minutes) * 60 * 1000)
alarm_type = mqtt_data.get("alert_type", "unknown")
alarm_level = _determine_alarm_level(alarm_type, confidence or 0, duration_ms)
alarm = AlarmEvent(
alarm_id=alarm_id,
alarm_type=alarm_type,
algorithm_code=mqtt_data.get("algorithm", "YOLO"),
device_id=mqtt_data.get("camera_id", "unknown"),
scene_id=mqtt_data.get("roi_id"),
event_time=event_time,
duration_ms=duration_ms,
alarm_level=alarm_level,
confidence_score=confidence,
alarm_status="NEW",
handle_status="UNHANDLED",
edge_node_id=mqtt_data.get("device_id"),
)
db.add(alarm)
# 写入扩展表
ext_data = {}
for key in ("bind_id", "target_class", "bbox", "message", "alert_id"):
val = mqtt_data.get(key)
if val is not None:
ext_key = "edge_alert_id" if key == "alert_id" else key
ext_data[ext_key] = val
if ext_data:
ext = AlarmEventExt(
alarm_id=alarm_id,
ext_type="EDGE",
ext_data=ext_data,
)
db.add(ext)
db.commit()
db.refresh(alarm)
logger.info(f"新告警事件创建: {alarm_id}, type={alarm_type}")
return alarm
except Exception as e:
db.rollback()
logger.error(f"创建告警事件失败: {e}")
return None
finally:
db.close()
def create_from_edge_report(self, data: Dict[str, Any]) -> Optional[AlarmEvent]:
"""
从边缘端 HTTP 上报创建告警事件
边缘端通过 POST /admin-api/aiot/alarm/edge/report 上报告警
使用边缘端生成的 alarm_id支持幂等重复 alarm_id 跳过
Args:
data: 边缘端上报数据字段与 alarm_event 表对齐
Returns:
AlarmEvent None
"""
db = get_session()
try:
alarm_id = data.get("alarm_id")
if not alarm_id:
logger.error("边缘上报缺少 alarm_id")
return None
# 幂等校验alarm_id 已存在则跳过
existing = db.query(AlarmEvent).filter(AlarmEvent.alarm_id == alarm_id).first()
if existing:
logger.info(f"告警已存在,跳过: {alarm_id}")
return existing
# 解析时间
event_time_str = data.get("event_time")
if event_time_str:
try:
event_time = datetime.fromisoformat(event_time_str.replace("Z", "+00:00"))
except ValueError:
event_time = datetime.now(timezone.utc)
else:
event_time = datetime.now(timezone.utc)
# 置信度
confidence = data.get("confidence_score")
if confidence is not None:
confidence = float(confidence)
if confidence > 1:
confidence = confidence / 100.0
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
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,
algorithm_code=data.get("algorithm_code"),
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"),
edge_node_id=ext_data.get("edge_node_id"),
)
db.add(alarm)
# 写入扩展表
ext_data = data.get("ext_data")
if ext_data:
ext = AlarmEventExt(
alarm_id=alarm_id,
ext_type="EDGE_HTTP",
ext_data=ext_data,
)
db.add(ext)
db.commit()
db.refresh(alarm)
logger.info(f"边缘端告警创建成功: {alarm_id}, type={alarm_type}, device={data.get('device_id')}")
return alarm
except Exception as e:
db.rollback()
logger.error(f"创建边缘端告警失败: {e}")
return None
finally:
db.close()
def create_from_http(self, data: Dict[str, Any], snapshot_data: Optional[bytes] = None) -> Optional[AlarmEvent]:
"""从 HTTP 请求创建告警事件"""
db = get_session()
try:
alarm_id = generate_alarm_id()
# 解析时间
trigger_time = data.get("trigger_time") or data.get("timestamp")
if isinstance(trigger_time, str):
try:
event_time = datetime.fromisoformat(trigger_time.replace("Z", "+00:00"))
except ValueError:
event_time = datetime.now(timezone.utc)
elif isinstance(trigger_time, datetime):
event_time = trigger_time
else:
event_time = datetime.now(timezone.utc)
confidence = data.get("confidence")
if confidence is not None:
confidence = float(confidence)
if confidence > 1:
confidence = confidence / 100.0
duration_minutes = data.get("duration_minutes")
duration_ms = None
if duration_minutes is not None:
duration_ms = int(float(duration_minutes) * 60 * 1000)
alarm_type = data.get("alert_type", "unknown")
alarm_level = _determine_alarm_level(alarm_type, confidence or 0, duration_ms)
snapshot_url = None
if snapshot_data:
snapshot_url = self.oss.upload_image(snapshot_data)
alarm = AlarmEvent(
alarm_id=alarm_id,
alarm_type=alarm_type,
algorithm_code=data.get("algorithm"),
device_id=data.get("camera_id", "unknown"),
scene_id=data.get("roi_id"),
event_time=event_time,
duration_ms=duration_ms,
alarm_level=alarm_level,
confidence_score=confidence,
alarm_status="NEW",
handle_status="UNHANDLED",
snapshot_url=snapshot_url,
edge_node_id=data.get("device_id"),
)
db.add(alarm)
# 写入扩展表
ext_data = {}
for key in ("bind_id", "target_class", "bbox", "message"):
val = data.get(key)
if val is not None:
ext_data[key] = val
if ext_data:
ext = AlarmEventExt(
alarm_id=alarm_id,
ext_type="POST",
ext_data=ext_data,
)
db.add(ext)
db.commit()
db.refresh(alarm)
logger.info(f"HTTP告警事件创建: {alarm_id}")
return alarm
except Exception as e:
db.rollback()
logger.error(f"HTTP创建告警事件失败: {e}")
return None
finally:
db.close()
def get_alarm(self, alarm_id: str) -> Optional[Dict]:
"""获取告警详情(含扩展信息)"""
db = get_session()
try:
alarm = db.query(AlarmEvent).filter(AlarmEvent.alarm_id == alarm_id).first()
if not alarm:
return None
result = alarm.to_dict()
# 关联扩展
ext = db.query(AlarmEventExt).filter(AlarmEventExt.alarm_id == alarm_id).first()
if ext:
result["ext"] = ext.to_dict()
# 关联 LLM 分析
analyses = db.query(AlarmLlmAnalysis).filter(
AlarmLlmAnalysis.alarm_id == alarm_id
).order_by(AlarmLlmAnalysis.created_at.desc()).all()
if analyses:
result["llm_analyses"] = [a.to_dict() for a in analyses]
return result
finally:
db.close()
def get_alarms(
self,
device_id: Optional[str] = None,
alarm_type: Optional[str] = None,
alarm_status: Optional[str] = None,
alarm_level: Optional[int] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
edge_node_id: Optional[str] = None,
page: int = 1,
page_size: int = 20,
) -> Tuple[List[AlarmEvent], int]:
"""分页查询告警列表"""
db = get_session()
try:
query = db.query(AlarmEvent)
if device_id:
query = query.filter(AlarmEvent.device_id == device_id)
if alarm_type:
query = query.filter(AlarmEvent.alarm_type == alarm_type)
if alarm_status:
query = query.filter(AlarmEvent.alarm_status == alarm_status)
if alarm_level is not None:
query = query.filter(AlarmEvent.alarm_level == alarm_level)
if edge_node_id:
query = query.filter(AlarmEvent.edge_node_id == edge_node_id)
if start_time:
query = query.filter(AlarmEvent.event_time >= start_time)
if end_time:
query = query.filter(AlarmEvent.event_time <= end_time)
total = query.count()
alarms = (
query.order_by(AlarmEvent.event_time.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return alarms, total
finally:
db.close()
def handle_alarm(
self,
alarm_id: str,
alarm_status: Optional[str] = None,
handle_status: Optional[str] = None,
remark: Optional[str] = None,
handler: Optional[str] = None,
) -> Optional[AlarmEvent]:
"""处理告警"""
db = get_session()
try:
alarm = db.query(AlarmEvent).filter(AlarmEvent.alarm_id == alarm_id).first()
if not alarm:
return None
if alarm_status:
alarm.alarm_status = alarm_status
if handle_status:
alarm.handle_status = handle_status
if remark is not None:
alarm.handle_remark = remark
if handler:
alarm.handler = handler
alarm.handled_at = datetime.now(timezone.utc)
alarm.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(alarm)
logger.info(f"告警已处理: {alarm_id}, status={alarm_status}")
return alarm
finally:
db.close()
def delete_alarm(self, alarm_id: str) -> bool:
"""删除告警(含扩展和分析)"""
db = get_session()
try:
alarm = db.query(AlarmEvent).filter(AlarmEvent.alarm_id == alarm_id).first()
if not alarm:
return False
# 删除关联数据
db.query(AlarmEventExt).filter(AlarmEventExt.alarm_id == alarm_id).delete()
db.query(AlarmLlmAnalysis).filter(AlarmLlmAnalysis.alarm_id == alarm_id).delete()
db.delete(alarm)
db.commit()
logger.info(f"告警已删除: {alarm_id}")
return True
except Exception as e:
db.rollback()
logger.error(f"删除告警失败: {e}")
return False
finally:
db.close()
def get_statistics(self) -> Dict:
"""获取告警统计"""
db = get_session()
try:
total = db.query(AlarmEvent).count()
# 按 alarm_status 计数
by_status = {}
for row in db.query(
AlarmEvent.alarm_status, func.count(AlarmEvent.alarm_id)
).group_by(AlarmEvent.alarm_status).all():
by_status[row[0]] = row[1]
# 按 alarm_type 计数
by_type = {}
for row in db.query(
AlarmEvent.alarm_type, func.count(AlarmEvent.alarm_id)
).group_by(AlarmEvent.alarm_type).all():
by_type[row[0]] = row[1]
# 按 alarm_level 计数
by_level = {}
for row in db.query(
AlarmEvent.alarm_level, func.count(AlarmEvent.alarm_id)
).group_by(AlarmEvent.alarm_level).all():
by_level[row[0]] = row[1]
return {
"total": total,
"byStatus": by_status,
"byType": by_type,
"byLevel": by_level,
}
finally:
db.close()
def get_device_summary(
self,
page: int = 1,
page_size: int = 10,
) -> Dict:
"""按设备(摄像头)分组统计告警汇总"""
db = get_session()
try:
query = db.query(
AlarmEvent.device_id,
func.count(AlarmEvent.alarm_id).label("total_count"),
func.max(AlarmEvent.event_time).label("last_event_time"),
).group_by(AlarmEvent.device_id)
total = query.count()
results = (
query.order_by(func.count(AlarmEvent.alarm_id).desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
summary_list = []
for row in results:
# 该设备待处理数量
unhandled_count = (
db.query(AlarmEvent)
.filter(AlarmEvent.device_id == row.device_id)
.filter(AlarmEvent.handle_status == "UNHANDLED")
.count()
)
# 最新一条告警
latest = (
db.query(AlarmEvent)
.filter(AlarmEvent.device_id == row.device_id)
.order_by(AlarmEvent.event_time.desc())
.first()
)
summary_list.append({
"deviceId": row.device_id,
"deviceName": row.device_id,
"totalCount": row.total_count,
"unhandledCount": unhandled_count,
"lastEventTime": row.last_event_time.isoformat() if row.last_event_time else None,
"lastAlarmType": latest.alarm_type if latest else None,
"lastAlarmTypeName": latest.alarm_type if latest else None,
})
return {
"list": summary_list,
"total": total,
}
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,
llm_model: str,
analysis_type: str,
summary: Optional[str] = None,
is_false_alarm: Optional[bool] = None,
risk_score: Optional[int] = None,
confidence_score: Optional[float] = None,
suggestion: Optional[str] = None,
) -> Optional[AlarmLlmAnalysis]:
"""保存大模型分析结果"""
db = get_session()
try:
analysis = AlarmLlmAnalysis(
alarm_id=alarm_id,
llm_model=llm_model,
analysis_type=analysis_type,
summary=summary,
is_false_alarm=is_false_alarm,
risk_score=risk_score,
confidence_score=confidence_score,
suggestion=suggestion,
)
db.add(analysis)
db.commit()
db.refresh(analysis)
logger.info(f"LLM分析结果已保存: alarm={alarm_id}, model={llm_model}")
return analysis
except Exception as e:
db.rollback()
logger.error(f"保存LLM分析失败: {e}")
return None
finally:
db.close()
# 全局单例
alarm_event_service = AlarmEventService()
def get_alarm_event_service() -> AlarmEventService:
"""获取告警事件服务单例"""
return alarm_event_service