""" 企微通知 API — 供 IoT 平台调用 IoT 平台通过这些接口驱动企微消息: - /send-card: 工单派单后发送企微群聊+私发卡片 - /sync-status: 工单状态变更后同步(confirmed更新卡片、终态更新告警+卡片) """ import json from fastapi import APIRouter, Request from pydantic import BaseModel from typing import List, Optional from app.utils.logger import logger router = APIRouter(prefix="/api/wechat/notify", tags=["企微通知-IoT回调"]) class SendCardRequest(BaseModel): """IoT 派单后发送企微卡片""" alarmId: str orderId: str userIds: List[str] title: str areaName: Optional[str] = "" cameraName: Optional[str] = "" eventTime: Optional[str] = "" level: Optional[int] = 2 snapshotUrl: Optional[str] = "" class SyncStatusRequest(BaseModel): """IoT 工单状态变更后同步""" alarmId: str orderId: str status: str # dispatched / confirmed / completed / false_alarm / auto_resolved operator: Optional[str] = "" remark: Optional[str] = "" async def _parse_body(request: Request) -> dict: """兼容解析请求体:JSON / form / query params""" content_type = request.headers.get("content-type", "") logger.info(f"IoT请求调试: method={request.method}, content-type={content_type}, url={request.url}") # 1. 读取原始 body raw_body = await request.body() logger.info(f"IoT请求调试: raw_body_len={len(raw_body)}, raw_body={raw_body[:500]}") # 2. 尝试 JSON 解析 if raw_body: try: return json.loads(raw_body) except Exception as e: logger.warning(f"JSON解析失败: {e}") # 3. 尝试 form 解析(需要重新构造 body,因为上面已经读过了) if raw_body and "form" in content_type: try: from urllib.parse import parse_qs params = parse_qs(raw_body.decode("utf-8")) return {k: v[0] if len(v) == 1 else v for k, v in params.items()} except Exception as e: logger.warning(f"Form解析失败: {e}") # 4. 降级到 query params qp = dict(request.query_params) if qp: logger.info(f"使用query params: {qp}") return qp # 5. 尝试从 headers 中找数据(某些客户端可能这么做) logger.warning(f"所有解析方式均无数据,headers={dict(request.headers)}") return {} @router.post("/send-card") async def send_card(request: Request): """IoT 派单后调用,发送企微群聊+私发卡片""" try: data = await _parse_body(request) logger.info(f"IoT send-card 收到数据: {data}") req = SendCardRequest(**data) from app.services.wechat_service import get_wechat_service from app.config import settings wechat = get_wechat_service() if not wechat.enabled: return {"code": -1, "msg": "企微未启用"} # 群聊通知 group_chat_id = settings.wechat.group_chat_id if group_chat_id: # 截图:从告警表获取 object key 生成预签名 URL from app.services.notify_dispatch import _get_presigned_url snapshot_url = req.snapshotUrl or "" # 如果是完整 COS URL,从告警表取 object key 重新生成预签名 if not snapshot_url or "403" in snapshot_url: snapshot_url = "" presigned_url = "" if snapshot_url: presigned_url = _get_presigned_url(snapshot_url) # 从告警表补全摄像头名称和截图(IoT 可能没传或传了无效数据) 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() # 截图预签名:优先用告警表的 object key(能正确签名) if not presigned_url or not snapshot_url: if alarm_snapshot_key: presigned_url = _get_presigned_url(alarm_snapshot_key) # alarm_type: 用 alarm_type_code 避免"告警告警" actual_alarm_type = alarm_type_code or req.title await wechat.send_group_alarm_combo( chat_id=group_chat_id, alarm_id=req.alarmId, alarm_type=actual_alarm_type, area_name=req.areaName or "", camera_name=camera_name, description=req.title, event_time=req.eventTime or "", alarm_level=req.level or 2, snapshot_url=presigned_url, mention_user_ids=req.userIds, ) # 私发卡片 # 从告警表获取 alarm_type_code(避免重复"告警") if not alarm_type_code: from app.models import get_session, AlarmEvent db = get_session() try: alarm = db.query(AlarmEvent).filter(AlarmEvent.alarm_id == req.alarmId).first() alarm_type_code = alarm.alarm_type if alarm else "" finally: db.close() sent = await wechat.send_alarm_card( user_ids=req.userIds, alarm_id=req.alarmId, 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, ) if sent: logger.info(f"IoT回调发卡片成功: alarm={req.alarmId}, order={req.orderId}, users={req.userIds}") return {"code": 0, "msg": "success"} else: return {"code": -1, "msg": "发送失败"} except Exception as e: logger.error(f"IoT回调发卡片异常: {e}", exc_info=True) return {"code": -1, "msg": str(e)} @router.post("/sync-status") async def sync_status(request: Request): """IoT 工单状态变更后调用""" try: data = await _parse_body(request) logger.info(f"IoT sync-status 收到数据: {data}") req = SyncStatusRequest(**data) from app.services.alarm_event_service import get_alarm_event_service from app.services.wechat_service import get_wechat_service service = get_alarm_event_service() wechat = get_wechat_service() if req.status == "dispatched": # 派单:不更新告警,不发企微(企微由 send-card 接口发) logger.info(f"dispatched 已记录: alarm={req.alarmId}, order={req.orderId}") return {"code": 0, "msg": "success"} elif req.status == "confirmed": # 确认接单:更新卡片到第二步(不更新告警) if wechat.enabled: response_code = wechat.get_response_code(req.alarmId) 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, operator_name=req.operator, ) logger.info(f"卡片已更新到步骤2: alarm={req.alarmId}") 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 wechat.enabled: response_code = wechat.get_response_code(req.alarmId) 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, action=mapping["card_action"], operator_name=req.operator, ) return {"code": 0, "msg": "success"} else: logger.warning(f"未知状态,忽略: {req.status}") return {"code": 0, "msg": "ignored"} except Exception as e: logger.error(f"IoT回调同步状态异常: {e}", exc_info=True) return {"code": -1, "msg": str(e)} @router.post("/daily-report") async def trigger_daily_report(preview: bool = False): """手动触发每日告警日报 - preview=false(默认):生成并发送到企微群聊 - preview=true:仅生成内容预览,不发送 """ try: from app.services.daily_report_service import generate_daily_report, _send_daily_report if preview: content = await generate_daily_report() return {"code": 0, "data": {"content": content}, "msg": "预览生成成功(未发送)"} await _send_daily_report() return {"code": 0, "msg": "日报已发送"} except Exception as e: logger.error(f"手动触发日报异常: {e}", exc_info=True) return {"code": -1, "msg": str(e)}