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

228 lines
8.3 KiB
Python
Raw Normal View History

"""
企微通知 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:
# 截图预签名URL
from app.services.notify_dispatch import _get_presigned_url
presigned_url = _get_presigned_url(req.snapshotUrl or "")
await wechat.send_group_alarm_combo(
chat_id=group_chat_id,
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,
snapshot_url=presigned_url,
mention_user_ids=req.userIds,
)
# 私发卡片
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()
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)}