切换到 IoT 工单驱动模式:所有状态变更由 IoT 回调驱动

1. notify_dispatch: 工单优先于卡片发送,创建成功则跳过直发卡片(等IoT回调send-card)
2. wechat_callback: IoT API 成功后直接返回,等 sync-status 回调更新告警+卡片
3. edge_compat: 启用工单自动结单,成功后等 sync-status 回调
4. yudao_aiot_alarm: 前端操作优先调 IoT 工单 API,降级直接更新卡片
5. wechat_notify_api: 修复 confirmed 的 card_action 为 None 导致卡片不更新的 bug

所有路径均保留降级逻辑:IoT 失败或工单未启用时直接处理告警+更新卡片
This commit is contained in:
2026-03-25 15:38:52 +08:00
parent 5a64e2fe11
commit 3a9595de7c
5 changed files with 127 additions and 88 deletions

View File

@@ -176,34 +176,37 @@ async def edge_device_heartbeat(request: Request, _auth=Depends(_verify_edge_tok
async def _resolve_card_update(alarm_id: str, resolve_type: str):
"""边缘端 resolve 后异步处理:更新卡片(工单暂未上线)"""
"""边缘端 resolve 后异步处理:工单自动结单 + 更新卡片"""
try:
from app.services.work_order_client import get_work_order_client
from app.services.wechat_service import get_wechat_service
from app.models import get_session, AlarmEventExt
# 工单自动结单(暂未上线)
# from app.services.work_order_client import get_work_order_client
# from app.models import get_session, AlarmEventExt
# wo_client = get_work_order_client()
# if wo_client.enabled:
# db = get_session()
# try:
# ext = db.query(AlarmEventExt).filter(
# AlarmEventExt.alarm_id == alarm_id,
# AlarmEventExt.ext_type == "WORK_ORDER",
# ).first()
# order_id = ext.ext_data.get("order_id", "") if ext and ext.ext_data else ""
# finally:
# db.close()
# if order_id:
# remark_map = {
# "person_returned": "人员回岗自动关闭",
# "non_work_time": "非工作时间自动关闭",
# "intrusion_cleared": "入侵消失自动关闭",
# }
# remark = remark_map.get(resolve_type, f"边缘端自动结单: {resolve_type}")
# await wo_client.auto_complete_order(order_id, remark)
# 工单自动结单
wo_client = get_work_order_client()
if wo_client.enabled:
db = get_session()
try:
ext = db.query(AlarmEventExt).filter(
AlarmEventExt.alarm_id == alarm_id,
AlarmEventExt.ext_type == "WORK_ORDER",
).first()
order_id = ext.ext_data.get("order_id", "") if ext and ext.ext_data else ""
finally:
db.close()
if order_id:
remark_map = {
"person_returned": "人员回岗自动关闭",
"non_work_time": "非工作时间自动关闭",
"intrusion_cleared": "入侵消失自动关闭",
}
remark = remark_map.get(resolve_type, f"边缘端自动结单: {resolve_type}")
success = await wo_client.auto_complete_order(order_id, remark)
if success:
logger.info(f"IoT工单已自动结单等待sync-status回调: alarm={alarm_id}, order={order_id}")
return # IoT 回调 sync-status 会更新告警+卡片
# 更新企微卡片到终态(如果有 response_code
# 降级:直接更新企微卡片到终态
wechat = get_wechat_service()
if wechat.enabled:
response_code = wechat.get_response_code(alarm_id)

View File

@@ -181,20 +181,20 @@ async def _process_card_button_click(msg: dict):
if order_id and wo_client.enabled:
success = await wo_client.confirm_order(order_id)
if success:
logger.info(f"IoT工单已确认: alarm={alarm_id}, order={order_id}, by={user_id}")
else:
logger.warning(f"IoT工单确认失败: alarm={alarm_id}, order={order_id}")
# IoT 调用失败降级直接更新
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="CONFIRMED",
handle_status="HANDLING", handler=user_id, remark="企微确认(IoT降级)")
logger.info(f"IoT工单已确认等待sync-status回调: alarm={alarm_id}, order={order_id}, by={user_id}")
return # IoT 回调 sync-status 会更新告警+卡片
# IoT 调用失败时降级直接更新
logger.warning(f"IoT工单确认失败降级处理: alarm={alarm_id}, order={order_id}")
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="CONFIRMED",
handle_status="HANDLING", handler=user_id, remark="企微确认(IoT降级)")
else:
# 工单未启用时直接更新告警状态
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="CONFIRMED",
handle_status="HANDLING", handler=user_id, remark="企微确认接单")
# 更新卡片提示去H5处理
# 降级时才更新卡片
if response_code:
await wechat.update_alarm_card_step2(
response_code=response_code,
@@ -208,17 +208,18 @@ async def _process_card_button_click(msg: dict):
if order_id and wo_client.enabled:
success = await wo_client.false_alarm(order_id)
if success:
logger.info(f"IoT工单已标记误报: alarm={alarm_id}, order={order_id}, by={user_id}")
else:
logger.warning(f"IoT误报标记失败: alarm={alarm_id}, order={order_id}")
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="FALSE",
handle_status="IGNORED", handler=user_id, remark="企微误报(IoT降级)")
logger.info(f"IoT工单已标记误报等待sync-status回调: alarm={alarm_id}, order={order_id}, by={user_id}")
return # IoT 回调 sync-status 会更新告警+卡片
logger.warning(f"IoT误报标记失败,降级处理: alarm={alarm_id}, order={order_id}")
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="FALSE",
handle_status="IGNORED", handler=user_id, remark="企微误报(IoT降级)")
else:
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="FALSE",
handle_status="IGNORED", handler=user_id, remark="企微误报忽略")
# 降级时才更新卡片
if response_code:
await wechat.update_alarm_card_terminal(
response_code=response_code, user_ids=[user_id],
@@ -230,16 +231,17 @@ async def _process_card_button_click(msg: dict):
if order_id and wo_client.enabled:
success = await wo_client.submit_order(order_id, result=f"已处理 by {user_id}")
if success:
logger.info(f"IoT工单已提交: alarm={alarm_id}, order={order_id}")
else:
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="CLOSED",
handle_status="DONE", handler=user_id, remark="企微完成(IoT降级)")
logger.info(f"IoT工单已提交等待sync-status回调: alarm={alarm_id}, order={order_id}")
return # IoT 回调 sync-status 会更新告警+卡片
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="CLOSED",
handle_status="DONE", handler=user_id, remark="企微完成(IoT降级)")
else:
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="CLOSED",
handle_status="DONE", handler=user_id, remark="企微已处理")
# 降级时才更新卡片
if response_code:
await wechat.update_alarm_card_terminal(
response_code=response_code, user_ids=[user_id],
@@ -251,16 +253,17 @@ async def _process_card_button_click(msg: dict):
if order_id and wo_client.enabled:
success = await wo_client.false_alarm(order_id)
if success:
logger.info(f"IoT工单已标记误报: alarm={alarm_id}, order={order_id}")
else:
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="FALSE",
handle_status="IGNORED", handler=user_id, remark="企微误报(IoT降级)")
logger.info(f"IoT工单已标记误报等待sync-status回调: alarm={alarm_id}, order={order_id}")
return # IoT 回调 sync-status 会更新告警+卡片
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="FALSE",
handle_status="IGNORED", handler=user_id, remark="企微误报(IoT降级)")
else:
service = get_alarm_event_service()
service.handle_alarm(alarm_id=alarm_id, alarm_status="FALSE",
handle_status="IGNORED", handler=user_id, remark="企微标记误报")
# 降级时才更新卡片
if response_code:
await wechat.update_alarm_card_terminal(
response_code=response_code, user_ids=[user_id],

View File

@@ -83,7 +83,7 @@ async def sync_status(req: SyncStatusRequest):
# 状态映射
status_map = {
"confirmed": {"alarm_status": "CONFIRMED", "handle_status": "HANDLING", "card_action": None},
"confirmed": {"alarm_status": "CONFIRMED", "handle_status": "HANDLING", "card_action": "confirm"},
"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"},
@@ -103,11 +103,11 @@ async def sync_status(req: SyncStatusRequest):
)
logger.info(f"告警状态已同步: alarm={req.alarmId}, status={req.status}")
# 2. 更新企微卡片
# 2. 更新企微卡片(区分 confirm→step2 和其他→terminal
if mapping["card_action"] and wechat.enabled:
response_code = wechat.get_response_code(req.alarmId)
if response_code:
if req.status == "confirmed":
if mapping["card_action"] == "confirm":
# 确认接单重绘卡片提示去H5处理
await wechat.update_alarm_card_step2(
response_code=response_code,

View File

@@ -246,35 +246,64 @@ async def handle_alert(
if not alarm:
raise HTTPException(status_code=404, detail="告警不存在")
# 终态操作CLOSED/FALSE同步更新企微卡片
# 终态操作CLOSED/FALSE同步工单 + 更新卡片
if alarmStatus in ("CLOSED", "FALSE"):
action = "complete" if alarmStatus == "CLOSED" else "false"
asyncio.create_task(_sync_wechat_card_terminal(alarm_id, action, handler))
asyncio.create_task(_sync_work_order_and_card(alarm_id, alarmStatus, handler))
return YudaoResponse.success(True)
async def _sync_wechat_card_terminal(alarm_id: str, action: str, operator: str):
"""前端处理/误报后,异步同步企微卡片到终态"""
async def _sync_work_order_and_card(alarm_id: str, alarm_status: str, operator: str):
"""前端操作后:优先调 IoT 工单 API降级直接更新卡片"""
try:
from app.services.work_order_client import get_work_order_client
wo_client = get_work_order_client()
order_id = _get_order_id_for_alarm(alarm_id) if wo_client.enabled else ""
if order_id and wo_client.enabled:
if alarm_status == "CLOSED":
success = await wo_client.submit_order(order_id, result=f"管理员处理 by {operator}")
else: # FALSE
success = await wo_client.false_alarm(order_id)
if success:
logger.info(f"前端操作已同步IoT工单: alarm={alarm_id}, status={alarm_status}")
return # IoT 回调 sync-status 会更新卡片
# 降级:直接更新卡片
from app.services.wechat_service import get_wechat_service
wechat = get_wechat_service()
if not wechat.enabled:
return
response_code = wechat.get_response_code(alarm_id)
if not response_code:
logger.debug(f"告警 {alarm_id} 无企微卡片 response_code跳过卡片更新")
return
await wechat.update_alarm_card_terminal(
response_code=response_code,
user_ids=[],
alarm_id=alarm_id,
action=action,
operator_name=operator,
)
logger.info(f"前端操作同步企微卡片: alarm={alarm_id}, action={action}")
if wechat.enabled:
response_code = wechat.get_response_code(alarm_id)
if response_code:
action = "complete" if alarm_status == "CLOSED" else "false"
await wechat.update_alarm_card_terminal(
response_code=response_code, user_ids=[],
alarm_id=alarm_id, action=action, operator_name=operator,
)
logger.info(f"降级直接更新卡片: alarm={alarm_id}, action={action}")
except Exception as e:
logger.error(f"同步企微卡片失败: alarm={alarm_id}, error={e}", exc_info=True)
logger.error(f"同步工单/卡片失败: alarm={alarm_id}, error={e}", exc_info=True)
def _get_order_id_for_alarm(alarm_id: str) -> str:
"""从 alarm_event_ext 中获取关联的工单ID"""
from app.models import get_session, AlarmEventExt
db = get_session()
try:
ext = db.query(AlarmEventExt).filter(
AlarmEventExt.alarm_id == alarm_id,
AlarmEventExt.ext_type == "WORK_ORDER",
).first()
if ext and ext.ext_data:
return ext.ext_data.get("order_id", "")
return ""
except Exception as e:
logger.error(f"查询工单ID失败: alarm={alarm_id}, error={e}")
return ""
finally:
db.close()
@router.delete("/alert/delete")

View File

@@ -147,24 +147,9 @@ async def process_alarm_notification(alarm_data: Dict):
else:
logger.debug("未配置群聊ID跳过群聊推送")
# ---- 3b. 个人按钮交互卡片 ----
sent = await wechat_service.send_alarm_card(
user_ids=user_ids,
alarm_id=alarm_id,
alarm_type=alarm_type,
area_name=area_name,
camera_name=camera_name,
description=description,
event_time=event_time_str,
alarm_level=alarm_level,
)
if sent:
logger.info(f"告警通知完成: {alarm_id}")
else:
logger.warning(f"个人卡片发送失败: {alarm_id}")
# ---- 4. 创建安保工单 ----
# ---- 3b. 创建工单(优先于卡片发送) ----
wo_client = get_work_order_client()
order_created = False
if wo_client.enabled:
wo_area_id = _get_alarm_area_id(alarm_id) or area_id_int
if wo_area_id:
@@ -187,9 +172,28 @@ async def process_alarm_notification(alarm_data: Dict):
)
if order_id:
_save_order_id(alarm_id, order_id)
order_created = True
logger.info(f"工单已创建等待IoT回调发卡片: alarm={alarm_id}, order={order_id}")
else:
logger.warning(f"告警无 area_id跳过工单创建: {alarm_id}")
# ---- 3c. 个人卡片(仅在工单未创建时降级发送) ----
if not order_created:
sent = await wechat_service.send_alarm_card(
user_ids=user_ids,
alarm_id=alarm_id,
alarm_type=alarm_type,
area_name=area_name,
camera_name=camera_name,
description=description,
event_time=event_time_str,
alarm_level=alarm_level,
)
if sent:
logger.info(f"降级直发卡片完成: {alarm_id}")
else:
logger.warning(f"降级直发卡片失败: {alarm_id}")
except Exception as e:
logger.error(f"告警通知处理失败: {alarm_id}, error={e}", exc_info=True)