dingdanquanliucheng/backend/app/api/wechat_callback.py

120 lines
4.0 KiB
Python
Raw Normal View History

"""微信服务号事件回调模块。
接收微信服务号推送的事件消息关注/取消关注等
将关注者的 openid 存入 pending_service_binding
等待用户在小程序中输入手机号完成绑定
需要在微信服务号后台配置
- 服务器地址 URL: https://dda.gaowenbu.cn/api/wechat/callback
- Token: .env WECHAT_SA_TOKEN 一致
"""
import hashlib
import logging
import xml.etree.ElementTree as ET
from fastapi import APIRouter, Query, Request, Response
from sqlalchemy import select
from sqlalchemy.orm import Session
from backend.app.core.config import get_settings
from backend.app.db import SessionLocal
from backend.app.models.system import User, PendingServiceBinding
logger = logging.getLogger(__name__)
router = APIRouter(tags=["wechat-callback"])
def _check_signature(signature: str, timestamp: str, nonce: str, token: str) -> bool:
"""验证微信签名。"""
items = sorted([token, timestamp, nonce])
s = "".join(items).encode("utf-8")
return hashlib.sha1(s).hexdigest() == signature
def _parse_xml_message(xml_str: str) -> dict:
"""解析微信推送的 XML 消息。"""
result = {}
try:
root = ET.fromstring(xml_str)
for child in root:
result[child.tag] = child.text or ""
except ET.ParseError:
pass
return result
@router.get("/api/wechat/callback")
def verify_callback(
signature: str = Query(...),
timestamp: str = Query(...),
nonce: str = Query(...),
echostr: str = Query(...),
):
"""微信服务器验证接口GET 请求)。"""
settings = get_settings()
if _check_signature(signature, timestamp, nonce, settings.wechat_sa_token):
return Response(content=echostr, media_type="text/plain")
return Response(content="invalid", media_type="text/plain", status_code=403)
@router.post("/api/wechat/callback")
async def handle_callback(request: Request):
"""微信事件推送接口POST 请求)。"""
xml_str = (await request.body()).decode("utf-8")
msg = _parse_xml_message(xml_str)
msg_type = msg.get("MsgType", "")
event = msg.get("Event", "").lower()
from_user = msg.get("FromUserName", "")
if msg_type == "event" and event in ("subscribe", "unsubscribe"):
session = SessionLocal()
try:
if event == "subscribe":
_handle_subscribe(session, from_user)
elif event == "unsubscribe":
_handle_unsubscribe(session, from_user)
session.commit()
except Exception:
session.rollback()
logger.exception("处理服务号事件失败: event=%s, openid=%s", event, from_user)
finally:
session.close()
return Response(content="success", media_type="text/plain")
def _handle_subscribe(session: Session, openid: str) -> None:
"""处理关注事件:存入 pending_service_binding 表。"""
existing = session.execute(
select(PendingServiceBinding).where(PendingServiceBinding.service_open_id == openid)
).scalar_one_or_none()
if existing:
existing.bound = False
session.add(existing)
logger.info("服务号重新关注: openid=%s, 更新 pending binding", openid)
else:
binding = PendingServiceBinding(service_open_id=openid, bound=False)
session.add(binding)
logger.info("服务号新关注: openid=%s, 创建 pending binding", openid)
def _handle_unsubscribe(session: Session, openid: str) -> None:
"""处理取消关注事件:清空用户 service_open_id删除 pending binding。"""
user = session.execute(
select(User).where(User.service_open_id == openid)
).scalar_one_or_none()
if user is not None:
user.service_open_id = None
session.add(user)
logger.info("用户 %s (id=%s) 取消关注服务号", user.real_name, user.id)
pending = session.execute(
select(PendingServiceBinding).where(PendingServiceBinding.service_open_id == openid)
).scalar_one_or_none()
if pending:
session.delete(pending)