Files
Neo-ZQYY/apps/backend/app/routers/wx_callback.py

105 lines
3.3 KiB
Python
Raw Permalink 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.
# 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")