# AI_CHANGELOG # - 2026-02-19 | Prompt: 配置微信消息推送 | 新增微信消息推送回调接口,支持 GET 验签 + POST 消息接收 """ 微信消息推送回调接口 处理两类请求: 1. GET — 微信服务器验证(配置时触发一次) 2. POST — 接收微信推送的消息/事件 安全模式下需要解密消息体,当前先用明文模式跑通,后续切安全模式。 """ import hashlib import logging from fastapi import APIRouter, Query, Request, Response from app.config import get logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/wx", tags=["微信回调"]) # Token 从环境变量读取,与微信后台填写的一致 # 放在 apps/backend/.env.local 中:WX_CALLBACK_TOKEN=你自定义的token WX_CALLBACK_TOKEN: str = get("WX_CALLBACK_TOKEN", "") def _check_signature(signature: str, timestamp: str, nonce: str) -> bool: """ 验证请求是否来自微信服务器。 将 Token、timestamp、nonce 字典序排序后拼接,做 SHA1, 与 signature 比对。 """ if not WX_CALLBACK_TOKEN: logger.error("WX_CALLBACK_TOKEN 未配置") return False items = sorted([WX_CALLBACK_TOKEN, timestamp, nonce]) hash_str = hashlib.sha1("".join(items).encode("utf-8")).hexdigest() return hash_str == signature @router.get("/callback") async def verify( signature: str = Query(...), timestamp: str = Query(...), nonce: str = Query(...), echostr: str = Query(...), ): """ 微信服务器验证接口。 配置消息推送时微信会发 GET 请求,验签通过后原样返回 echostr。 """ if _check_signature(signature, timestamp, nonce): logger.info("微信回调验证通过") # 必须原样返回 echostr(纯文本,不能包裹 JSON) return Response(content=echostr, media_type="text/plain") else: logger.warning("微信回调验签失败: signature=%s", signature) return Response(content="signature mismatch", status_code=403) @router.post("/callback") async def receive_message( request: Request, signature: str = Query(""), timestamp: str = Query(""), nonce: str = Query(""), ): """ 接收微信推送的消息/事件。 当前为明文模式,直接解析 JSON 包体。 后续切安全模式时需增加 AES 解密逻辑。 """ # 验签(POST 也带 signature 参数) if not _check_signature(signature, timestamp, nonce): logger.warning("消息推送验签失败") return Response(content="signature mismatch", status_code=403) # 解析消息体 body = await request.body() content_type = request.headers.get("content-type", "") if "json" in content_type: import json try: data = json.loads(body) except json.JSONDecodeError: data = {"raw": body.decode("utf-8", errors="replace")} else: # XML 格式暂不解析,记录原文 data = {"raw_xml": body.decode("utf-8", errors="replace")} logger.info("收到微信推送: MsgType=%s, Event=%s", data.get("MsgType", "?"), data.get("Event", "?")) # TODO: 根据 MsgType/Event 分发处理(客服消息、订阅事件等) # 当前统一返回 success return Response(content="success", media_type="text/plain")