告警-工单解耦:企微交互+Agent全面切换到工单驱动

Part A: 数据层
- 新增 WechatCardState 模型(order_id ↔ alarm_id 映射 + response_code)
- 新建 models_iot.py(IoT 工单只读 ORM:ops_order + security_ext + clean_ext)
- config.py 新增 IOT_DATABASE_URL 配置

Part B: 企微解耦(alarm_id → order_id)
- wechat_service: response_code 存储迁移到 wechat_card_state,集中 helper
- 卡片发送/更新方法改用 order_id,按钮 key: confirm_{order_id}
- wechat_callback: 按钮解析改 order_id,反查 alarm_id(可空)
- wechat_notify_api: send-card/sync-status 以 orderId 为主键
- yudao_aiot_alarm: 卡片操作改用 order_id,删重复 helper

Part C: Agent 工具全面改为工单驱动
- 新建 order_query.py(查 IoT ops_order,支持安保+保洁工单)
- 新建 order_action.py(操作工单状态 + 提交处理结果)
- 更新 prompts.py 为工单助手
- 更新工具注册(__init__.py)

Part D: 日报改为工单驱动
- daily_report_service 从查 alarm_event 改为查 IoT ops_order + 扩展表
- 支持安保+保洁工单统计
This commit is contained in:
2026-03-31 10:49:42 +08:00
parent 93148fe85b
commit 63a8d5a8f2
14 changed files with 1019 additions and 343 deletions

View File

@@ -18,7 +18,7 @@ router = APIRouter(prefix="/api/wechat/notify", tags=["企微通知-IoT回调"])
class SendCardRequest(BaseModel):
"""IoT 派单后发送企微卡片"""
alarmId: str
alarmId: Optional[str] = ""
orderId: str
userIds: List[str]
title: str
@@ -31,7 +31,7 @@ class SendCardRequest(BaseModel):
class SyncStatusRequest(BaseModel):
"""IoT 工单状态变更后同步"""
alarmId: str
alarmId: Optional[str] = ""
orderId: str
status: str # dispatched / confirmed / completed / false_alarm / auto_resolved
operator: Optional[str] = ""
@@ -107,28 +107,29 @@ async def send_card(request: Request):
camera_name = req.cameraName or ""
alarm_type_code = ""
alarm_snapshot_key = ""
from app.models import get_session, AlarmEvent
from app.services.camera_name_service import get_camera_name_service
db = get_session()
try:
alarm = db.query(AlarmEvent).filter(AlarmEvent.alarm_id == req.alarmId).first()
if alarm:
alarm_type_code = alarm.alarm_type or ""
alarm_snapshot_key = alarm.snapshot_url or ""
if not camera_name or camera_name == "未知":
if alarm.device_id:
camera_service = get_camera_name_service()
camera_info = await camera_service.get_camera_info(alarm.device_id)
camera_name = camera_service.format_display_name(alarm.device_id, camera_info)
finally:
db.close()
if req.alarmId:
from app.models import get_session, AlarmEvent
from app.services.camera_name_service import get_camera_name_service
db = get_session()
try:
alarm = db.query(AlarmEvent).filter(AlarmEvent.alarm_id == req.alarmId).first()
if alarm:
alarm_type_code = alarm.alarm_type or ""
alarm_snapshot_key = alarm.snapshot_url or ""
if not camera_name or camera_name == "未知":
if alarm.device_id:
camera_service = get_camera_name_service()
camera_info = await camera_service.get_camera_info(alarm.device_id)
camera_name = camera_service.format_display_name(alarm.device_id, camera_info)
finally:
db.close()
# 截图预签名:优先用告警表的 object key能正确签名
if not presigned_url or not snapshot_url:
if alarm_snapshot_key:
presigned_url = _get_presigned_url(alarm_snapshot_key)
logger.info(f"群聊截图诊断: alarm={req.alarmId}, "
logger.info(f"群聊截图诊断: order={req.orderId}, alarm={req.alarmId}, "
f"iot_snapshot={req.snapshotUrl!r}, "
f"db_snapshot={alarm_snapshot_key!r}, "
f"presigned_url={presigned_url[:80] if presigned_url else '(空)'}...")
@@ -138,7 +139,7 @@ async def send_card(request: Request):
await wechat.send_group_alarm_combo(
chat_id=group_chat_id,
alarm_id=req.alarmId,
alarm_id=req.alarmId or req.orderId,
alarm_type=actual_alarm_type,
area_name=req.areaName or "",
camera_name=camera_name,
@@ -149,9 +150,8 @@ async def send_card(request: Request):
mention_user_ids=req.userIds,
)
# 私发卡片
# 从告警表获取 alarm_type_code(避免重复"告警"
if not alarm_type_code:
# 私发卡片(以 order_id 为主键)
if not alarm_type_code and req.alarmId:
from app.models import get_session, AlarmEvent
db = get_session()
try:
@@ -162,17 +162,18 @@ async def send_card(request: Request):
sent = await wechat.send_alarm_card(
user_ids=req.userIds,
alarm_id=req.alarmId,
order_id=req.orderId,
alarm_type=alarm_type_code or req.title,
area_name=req.areaName or "",
camera_name=camera_name if 'camera_name' in dir() else (req.cameraName or ""),
description=f"工单编号:{req.orderId}",
event_time=req.eventTime or "",
alarm_level=req.level or 2,
alarm_id=req.alarmId or "",
)
if sent:
logger.info(f"IoT回调发卡片成功: alarm={req.alarmId}, order={req.orderId}, users={req.userIds}")
logger.info(f"IoT回调发卡片成功: order={req.orderId}, alarm={req.alarmId}, users={req.userIds}")
return {"code": 0, "msg": "success"}
else:
return {"code": -1, "msg": "发送失败"}
@@ -197,49 +198,56 @@ async def sync_status(request: Request):
service = get_alarm_event_service()
wechat = get_wechat_service()
# 用 order_id 查 response_code 和 alarm_id
order_id = req.orderId
alarm_id = req.alarmId or wechat.get_alarm_id_for_order(order_id)
if req.status == "dispatched":
# 派单:不更新告警,不发企微(企微由 send-card 接口发)
logger.info(f"dispatched 已记录: alarm={req.alarmId}, order={req.orderId}")
logger.info(f"dispatched 已记录: order={order_id}, alarm={alarm_id}")
return {"code": 0, "msg": "success"}
elif req.status == "confirmed":
# 确认接单:更新卡片到第二步(不更新告警)
if wechat.enabled:
response_code = wechat.get_response_code(req.alarmId)
response_code = wechat.get_response_code(order_id)
if response_code:
await wechat.update_alarm_card_step2(
response_code=response_code,
user_ids=[req.operator] if req.operator else [],
alarm_id=req.alarmId,
order_id=order_id,
operator_name=req.operator,
)
logger.info(f"卡片已更新到步骤2: alarm={req.alarmId}")
logger.info(f"卡片已更新到步骤2: order={order_id}")
return {"code": 0, "msg": "success"}
elif req.status in ("completed", "false_alarm", "auto_resolved"):
# 终态:更新告警状态 + 更新卡片
# 终态:更新告警状态(如果有关联告警)+ 更新卡片
terminal_map = {
"completed": {"alarm_status": "CLOSED", "handle_status": "DONE", "card_action": "complete"},
"false_alarm": {"alarm_status": "FALSE", "handle_status": "IGNORED", "card_action": "false"},
"auto_resolved": {"alarm_status": "CLOSED", "handle_status": "DONE", "card_action": "auto_resolve"},
}
mapping = terminal_map[req.status]
service.handle_alarm(
alarm_id=req.alarmId,
alarm_status=mapping["alarm_status"],
handle_status=mapping["handle_status"],
handler=req.operator or "",
remark=req.remark or f"IoT工单: {req.status}",
)
logger.info(f"告警终态已同步: alarm={req.alarmId}, status={req.status}")
# 仅在有关联告警时更新告警状态
if alarm_id:
service.handle_alarm(
alarm_id=alarm_id,
alarm_status=mapping["alarm_status"],
handle_status=mapping["handle_status"],
handler=req.operator or "",
remark=req.remark or f"IoT工单: {req.status}",
)
logger.info(f"告警终态已同步: alarm={alarm_id}, status={req.status}")
if wechat.enabled:
response_code = wechat.get_response_code(req.alarmId)
response_code = wechat.get_response_code(order_id)
if response_code:
await wechat.update_alarm_card_terminal(
response_code=response_code,
user_ids=[req.operator] if req.operator else [],
alarm_id=req.alarmId,
order_id=order_id,
action=mapping["card_action"],
operator_name=req.operator,
)