Files
iot-device-management-service/app/services/alert_service.py
16337 f81cc81ce6 refactor(service): 删除MQTT旧代码 + 修复边缘节点重复显示问题
**删除MQTT旧代码:**
- 删除 mqtt_service.py(已废弃的空壳)
- 从 config.py 删除 MQTTConfig 类和相关配置
- 从 schemas.py 删除 mqtt 字段
- 从 alert_service.py 删除 create_alert_from_mqtt 方法
- 告警上报已改为 HTTP + COS 方案,MQTT机制完全废弃

**修复边缘节点重复显示(方案A):**
- 清理 edge_devices 表历史数据(删除 edge_device_001、edge_inference_device)
- 禁用 DeviceService 的 handle_heartbeat 自动创建设备功能
- 边缘端未实现心跳机制,告警数从 alarm_event 表统计
- 运行时长、处理帧数字段设为 null(无心跳机制,不可用)
- 添加 count_alarms_by_edge_node 方法统计边缘节点告警数

**影响范围:**
- /admin-api/aiot/edge/device/page 接口返回数据调整
- /admin-api/aiot/edge/device/get 接口返回数据调整
- 确保不破坏现有功能(告警上报已改为HTTP)
2026-02-25 10:30:01 +08:00

334 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
告警服务
处理告警 CRUD旧版告警系统已迁移至 alarm_event_service
"""
import uuid
from datetime import datetime, timezone
from typing import Optional, List, Dict, Any
from app.models import Alert, AlertStatus, AlertLevel, get_session
from app.schemas import AlertCreate, AlertHandleRequest
from app.services.oss_storage import get_oss_storage
from app.utils.logger import logger
class AlertService:
"""告警服务"""
def __init__(self):
self.oss = get_oss_storage()
def generate_alert_no(self) -> str:
"""生成告警编号"""
timestamp = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
unique_id = uuid.uuid4().hex[:8].upper()
return f"ALT{timestamp}{unique_id}"
def create_alert(
self,
alert_data: AlertCreate,
snapshot_data: Optional[bytes] = None,
) -> Alert:
"""创建告警HTTP 方式)"""
db = get_session()
try:
alert = Alert(
alert_no=self.generate_alert_no(),
camera_id=alert_data.camera_id,
roi_id=alert_data.roi_id,
bind_id=getattr(alert_data, 'bind_id', None),
device_id=getattr(alert_data, 'device_id', None),
alert_type=alert_data.alert_type,
algorithm=alert_data.algorithm,
confidence=alert_data.confidence,
duration_minutes=alert_data.duration_minutes,
trigger_time=alert_data.trigger_time,
message=alert_data.message,
bbox=getattr(alert_data, 'bbox', None),
status=AlertStatus.PENDING,
level=AlertLevel.MEDIUM,
)
if snapshot_data:
snapshot_url = self.oss.upload_image(snapshot_data)
alert.snapshot_url = snapshot_url
db.add(alert)
db.commit()
db.refresh(alert)
logger.info(f"告警创建成功: {alert.alert_no}")
return alert
finally:
db.close()
def _determine_level(self, data: Dict[str, Any]) -> AlertLevel:
"""根据告警数据确定告警级别"""
alert_type = data.get("alert_type", "")
confidence = data.get("confidence", 0)
# 根据告警类型和置信度确定级别
if alert_type == "intrusion":
return AlertLevel.HIGH
elif alert_type == "leave_post":
duration = data.get("duration_minutes", 0)
if duration and duration > 30:
return AlertLevel.HIGH
elif duration and duration > 10:
return AlertLevel.MEDIUM
return AlertLevel.LOW
elif confidence and confidence > 0.9:
return AlertLevel.HIGH
elif confidence and confidence > 0.7:
return AlertLevel.MEDIUM
return AlertLevel.MEDIUM
def get_alert(self, alert_id: int) -> Optional[Alert]:
"""获取告警详情"""
db = get_session()
try:
return db.query(Alert).filter(Alert.id == alert_id).first()
finally:
db.close()
def get_alert_by_no(self, alert_no: str) -> Optional[Alert]:
"""根据告警编号获取"""
db = get_session()
try:
return db.query(Alert).filter(Alert.alert_no == alert_no).first()
finally:
db.close()
def get_alerts(
self,
camera_id: Optional[str] = None,
device_id: Optional[str] = None,
alert_type: Optional[str] = None,
status: Optional[str] = None,
level: Optional[str] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
page: int = 1,
page_size: int = 20,
) -> tuple[List[Alert], int]:
"""获取告警列表"""
db = get_session()
try:
query = db.query(Alert)
if camera_id:
query = query.filter(Alert.camera_id == camera_id)
if device_id:
query = query.filter(Alert.device_id == device_id)
if alert_type:
query = query.filter(Alert.alert_type == alert_type)
if status:
query = query.filter(Alert.status == status)
if level:
query = query.filter(Alert.level == level)
if start_time:
query = query.filter(Alert.trigger_time >= start_time)
if end_time:
query = query.filter(Alert.trigger_time <= end_time)
total = query.count()
alerts = (
query.order_by(Alert.created_at.desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
return alerts, total
finally:
db.close()
def handle_alert(
self,
alert_id: int,
handle_data: AlertHandleRequest,
handled_by: Optional[str] = None,
) -> Optional[Alert]:
"""处理告警"""
db = get_session()
try:
alert = db.query(Alert).filter(Alert.id == alert_id).first()
if not alert:
return None
alert.status = AlertStatus(handle_data.status)
alert.handle_remark = handle_data.remark
alert.handled_by = handled_by
alert.handled_at = datetime.now(timezone.utc)
alert.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(alert)
logger.info(f"告警处理完成: {alert.alert_no}, 状态: {handle_data.status}")
return alert
finally:
db.close()
def dispatch_alert(self, alert_id: int, work_order_id: int) -> Optional[Alert]:
"""派发告警(关联工单)"""
db = get_session()
try:
alert = db.query(Alert).filter(Alert.id == alert_id).first()
if not alert:
return None
alert.status = AlertStatus.DISPATCHED
alert.work_order_id = work_order_id
alert.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(alert)
logger.info(f"告警已派单: {alert.alert_no} -> 工单 {work_order_id}")
return alert
finally:
db.close()
def delete_alert(self, alert_id: int) -> bool:
"""删除告警"""
db = get_session()
try:
alert = db.query(Alert).filter(Alert.id == alert_id).first()
if not alert:
return False
db.delete(alert)
db.commit()
logger.info(f"告警已删除: {alert.alert_no}")
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(Alert).count()
pending = db.query(Alert).filter(Alert.status == AlertStatus.PENDING).count()
confirmed = db.query(Alert).filter(Alert.status == AlertStatus.CONFIRMED).count()
ignored = db.query(Alert).filter(Alert.status == AlertStatus.IGNORED).count()
resolved = db.query(Alert).filter(Alert.status == AlertStatus.RESOLVED).count()
dispatched = db.query(Alert).filter(Alert.status == AlertStatus.DISPATCHED).count()
by_type = {}
for alert in db.query(Alert.alert_type).distinct():
alert_type = alert[0]
by_type[alert_type] = db.query(Alert).filter(Alert.alert_type == alert_type).count()
by_level = {}
for level in AlertLevel:
by_level[level.value] = db.query(Alert).filter(Alert.level == level).count()
return {
"total": total,
"pending": pending,
"confirmed": confirmed,
"ignored": ignored,
"resolved": resolved,
"dispatched": dispatched,
"by_type": by_type,
"by_level": by_level,
}
finally:
db.close()
def update_ai_analysis(self, alert_id: int, analysis: dict) -> Optional[Alert]:
"""更新 AI 分析结果"""
db = get_session()
try:
alert = db.query(Alert).filter(Alert.id == alert_id).first()
if alert:
alert.ai_analysis = analysis
db.commit()
db.refresh(alert)
return alert
finally:
db.close()
def get_camera_alert_summary(
self,
page: int = 1,
page_size: int = 10,
) -> dict:
"""以摄像头为维度获取告警汇总"""
from sqlalchemy import func
db = get_session()
try:
# 按摄像头分组统计总数和最近时间
query = db.query(
Alert.camera_id,
func.count(Alert.id).label("total_count"),
func.max(Alert.created_at).label("last_alert_time"),
).group_by(Alert.camera_id)
# 总数
total = query.count()
# 分页
results = (
query.order_by(func.count(Alert.id).desc())
.offset((page - 1) * page_size)
.limit(page_size)
.all()
)
summary_list = []
for row in results:
# 获取该摄像头待处理告警数量
pending_count = (
db.query(Alert)
.filter(Alert.camera_id == row.camera_id)
.filter(Alert.status == AlertStatus.PENDING)
.count()
)
# 获取该摄像头最新的一条告警
latest_alert = (
db.query(Alert)
.filter(Alert.camera_id == row.camera_id)
.order_by(Alert.created_at.desc())
.first()
)
summary_list.append({
"cameraId": row.camera_id,
"cameraName": row.camera_id, # TODO: 从设备服务获取摄像头名称
"totalCount": row.total_count,
"pendingCount": pending_count,
"lastAlertTime": row.last_alert_time.isoformat() if row.last_alert_time else None,
"lastAlertType": latest_alert.alert_type if latest_alert else None,
"lastAlertTypeName": latest_alert.alert_type if latest_alert else None, # 前端会映射
})
return {
"list": summary_list,
"total": total,
}
finally:
db.close()
# 全局单例
alert_service = AlertService()
def get_alert_service() -> AlertService:
"""获取告警服务单例"""
return alert_service