在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View File

@@ -0,0 +1,104 @@
# 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")