feat(aiot): 边缘告警HTTP上报 + 移除配置中转层

- 新增 edge/report 端点接收边缘端HTTP告警上报
- alarm_event_service 新增 create_from_edge_report 幂等创建
- schemas 新增 EdgeAlarmReport 模型
- 移除 config_service/redis_service/yudao_aiot_config 配置中转
- MQTT 服务标记废弃,告警上报改为HTTP+COS
- config 新增 COS/Redis 配置项
- requirements 新增 redis 依赖

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 15:22:01 +08:00
parent 6cf1524013
commit 9f4cea0810
8 changed files with 207 additions and 307 deletions

View File

@@ -1,265 +1,53 @@
"""
MQTT 订阅服务
订阅边缘端告警和心跳消息
MQTT 服务 - 已废弃
告警上报已改为 HTTP + COS 方案(边缘端直传)。
此文件保留为空壳,避免其他模块 import 报错。
后续版本将彻底删除此文件。
"""
import json
import threading
import uuid
from datetime import datetime, timezone
from typing import Callable, Dict, Any, Optional, List
import paho.mqtt.client as mqtt
from app.config import settings
from app.utils.logger import logger
from typing import Dict, Any, Optional
class MQTTService:
"""MQTT 订阅服务"""
"""MQTT 服务 (已废弃,保留空壳兼容旧代码)"""
def __init__(self):
self._client: Optional[mqtt.Client] = None
self._connected = False
self._running = False
self._use_v2_callback = False # paho-mqtt 版本标记
self._lock = threading.Lock()
pass
# 回调函数
self._alert_handlers: List[Callable[[Dict[str, Any]], None]] = []
self._heartbeat_handlers: List[Callable[[Dict[str, Any]], None]] = []
@property
def is_connected(self) -> bool:
return False
# 统计
self._stats = {
def register_alert_handler(self, handler):
pass
def register_heartbeat_handler(self, handler):
pass
def start(self):
pass
def stop(self):
pass
def get_statistics(self) -> Dict[str, Any]:
return {
"messages_received": 0,
"alerts_received": 0,
"heartbeats_received": 0,
"errors": 0,
"connected": False,
"running": False,
"deprecated": True,
}
@property
def is_connected(self) -> bool:
return self._connected
def register_alert_handler(self, handler: Callable[[Dict[str, Any]], None]):
"""注册告警处理回调"""
self._alert_handlers.append(handler)
logger.info(f"已注册告警处理器: {handler.__name__}")
def register_heartbeat_handler(self, handler: Callable[[Dict[str, Any]], None]):
"""注册心跳处理回调"""
self._heartbeat_handlers.append(handler)
logger.info(f"已注册心跳处理器: {handler.__name__}")
def start(self):
"""启动 MQTT 服务"""
if not settings.mqtt.enabled:
logger.info("MQTT 服务已禁用")
return
if self._running:
return
try:
# 给 client_id 添加随机后缀,防止多实例 client_id 冲突导致反复踢连接
unique_client_id = f"{settings.mqtt.client_id}_{uuid.uuid4().hex[:8]}"
# 兼容 paho-mqtt 1.x 和 2.x 版本
try:
# paho-mqtt 2.0+ 新 API
self._client = mqtt.Client(
client_id=unique_client_id,
protocol=mqtt.MQTTv311,
callback_api_version=mqtt.CallbackAPIVersion.VERSION2
)
self._use_v2_callback = True
except AttributeError:
# paho-mqtt 1.x 旧 API
self._client = mqtt.Client(
client_id=unique_client_id,
protocol=mqtt.MQTTv311
)
self._use_v2_callback = False
logger.info("使用 paho-mqtt 1.x 兼容模式")
# 设置回调
self._client.on_connect = self._on_connect
self._client.on_disconnect = self._on_disconnect
self._client.on_message = self._on_message
# 设置认证(如果有)
if settings.mqtt.username:
self._client.username_pw_set(
settings.mqtt.username,
settings.mqtt.password
)
# 连接
self._client.connect(
settings.mqtt.broker_host,
settings.mqtt.broker_port,
60
)
# 启动循环
self._client.loop_start()
self._running = True
logger.info(
f"MQTT 服务启动: {settings.mqtt.broker_host}:{settings.mqtt.broker_port}"
)
except Exception as e:
logger.error(f"MQTT 服务启动失败: {e}")
self._running = False
def stop(self):
"""停止 MQTT 服务"""
if not self._running:
return
self._running = False
if self._client:
self._client.loop_stop()
self._client.disconnect()
logger.info("MQTT 服务已停止")
def _on_connect(self, client, userdata, *args):
"""连接回调 (兼容 1.x 和 2.x)"""
# 1.x: (client, userdata, flags, rc)
# 2.x: (client, userdata, connect_flags, reason_code, properties)
rc = 0
if args:
# 取倒数第二个参数1.x 的 rc 或 2.x 的 reason_code
if len(args) >= 2:
reason_code = args[-2]
else:
reason_code = args[0]
rc = reason_code if isinstance(reason_code, int) else getattr(reason_code, 'value', 0)
if rc == 0:
self._connected = True
logger.info("MQTT 连接成功")
# 订阅告警主题
client.subscribe(settings.mqtt.alert_topic, settings.mqtt.qos)
logger.info(f"已订阅告警主题: {settings.mqtt.alert_topic}")
# 心跳主题通常是 edge/alert/heartbeat/# 但它已经被 edge/alert/# 包含
# 所以不需要单独订阅
else:
self._connected = False
logger.error(f"MQTT 连接失败: {reason_code}")
def _on_disconnect(self, client, userdata, *args):
"""断开连接回调 (兼容 1.x 和 2.x)"""
# 1.x: (client, userdata, rc)
# 2.x: (client, userdata, disconnect_flags, reason_code, properties)
self._connected = False
rc = 0
if args:
if len(args) >= 2:
reason_code = args[-2]
else:
reason_code = args[0]
rc = reason_code if isinstance(reason_code, int) else getattr(reason_code, 'value', 0)
if rc != 0:
logger.warning(f"MQTT 连接异常断开: rc={rc}")
else:
logger.info("MQTT 连接断开")
def _on_message(self, client, userdata, msg: mqtt.MQTTMessage):
"""消息回调"""
with self._lock:
self._stats["messages_received"] += 1
try:
topic = msg.topic
payload = json.loads(msg.payload.decode("utf-8"))
logger.debug(f"收到 MQTT 消息: topic={topic}")
# 判断消息类型
if "/heartbeat/" in topic:
self._handle_heartbeat(topic, payload)
else:
self._handle_alert(topic, payload)
except json.JSONDecodeError as e:
logger.error(f"MQTT 消息解析失败: {e}")
with self._lock:
self._stats["errors"] += 1
except Exception as e:
logger.error(f"MQTT 消息处理失败: {e}")
with self._lock:
self._stats["errors"] += 1
def _handle_alert(self, topic: str, payload: Dict[str, Any]):
"""处理告警消息"""
with self._lock:
self._stats["alerts_received"] += 1
# 从 topic 解析 camera_id 和 roi_id
# topic 格式: edge/alert/{camera_id}/{roi_id}
parts = topic.split("/")
if len(parts) >= 4:
camera_id = parts[2]
roi_id = parts[3] if len(parts) > 3 else None
# 确保 payload 中有这些字段
payload.setdefault("camera_id", camera_id)
payload.setdefault("roi_id", roi_id)
logger.info(
f"收到告警: camera={payload.get('camera_id')}, "
f"type={payload.get('alert_type')}, "
f"confidence={payload.get('confidence')}"
)
# 调用所有注册的处理器
for handler in self._alert_handlers:
try:
handler(payload)
except Exception as e:
logger.error(f"告警处理器执行失败: {e}")
def _handle_heartbeat(self, topic: str, payload: Dict[str, Any]):
"""处理心跳消息"""
with self._lock:
self._stats["heartbeats_received"] += 1
# 从 topic 解析 device_id
# topic 格式: edge/alert/heartbeat/{device_id}
parts = topic.split("/")
if len(parts) >= 4:
device_id = parts[3]
payload.setdefault("device_id", device_id)
logger.debug(f"收到心跳: device={payload.get('device_id')}")
# 调用所有注册的处理器
for handler in self._heartbeat_handlers:
try:
handler(payload)
except Exception as e:
logger.error(f"心跳处理器执行失败: {e}")
def get_statistics(self) -> Dict[str, Any]:
"""获取统计信息"""
with self._lock:
return {
**self._stats,
"connected": self._connected,
"running": self._running,
}
# 全局单例
_mqtt_service: Optional[MQTTService] = None
def get_mqtt_service() -> MQTTService:
"""获取 MQTT 服务单例"""
"""获取 MQTT 服务单例 (已废弃)"""
global _mqtt_service
if _mqtt_service is None:
_mqtt_service = MQTTService()