Files
iot-device-management-service/app/routers/wechat_notify_api.py

206 lines
7.5 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.

"""
企微通知 API — 供 IoT 平台调用
IoT 平台通过这两个接口驱动企微消息:
- /send-card: 工单派单后发送企微卡片
- /sync-status: 工单状态变更后同步告警状态+更新卡片
"""
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 # confirmed / completed / false_alarm / auto_resolved / dispatched
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, ALARM_LEVEL_NAMES
wechat = get_wechat_service()
if not wechat.enabled:
return {"code": -1, "msg": "企微未启用"}
level_name = ALARM_LEVEL_NAMES.get(req.level, "普通")
sent = await wechat.send_alarm_card(
user_ids=req.userIds,
alarm_id=req.alarmId,
alarm_type=req.title,
area_name=req.areaName or "",
camera_name=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()
# 状态映射
status_map = {
"dispatched": {"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"},
}
mapping = status_map.get(req.status)
if not mapping:
return {"code": 400, "msg": f"未知状态: {req.status}"}
# 1. 更新告警状态
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}")
# 2. 更新企微卡片(区分 confirm→step2 和其他→terminal
if mapping["card_action"] and wechat.enabled:
response_code = wechat.get_response_code(req.alarmId)
if response_code:
if mapping["card_action"] == "confirm":
# 确认接单重绘卡片提示去H5处理
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,
)
else:
# 终态:按钮变灰
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,
)
logger.info(f"卡片已更新: alarm={req.alarmId}, action={mapping['card_action']}")
else:
logger.warning(f"未找到 response_code跳过卡片更新: alarm={req.alarmId}")
return {"code": 0, "msg": "success"}
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)}