# -*- coding: utf-8 -*- """ 微信认证服务 —— 封装 code2Session API 调用。 通过 httpx.AsyncClient 异步调用微信 jscode2session 接口, 将小程序端的临时登录凭证 (code) 换取 openid / session_key。 """ from __future__ import annotations import logging import httpx from app.config import WX_APPID, WX_SECRET logger = logging.getLogger(__name__) CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session" # 微信 errcode → (HTTP 状态码, 用户可见提示) _WX_ERROR_MAP: dict[int, tuple[int, str]] = { 40029: (401, "登录凭证无效,请重新登录"), 45011: (429, "请求过于频繁"), 40226: (403, "账号存在风险"), } class WeChatAuthError(Exception): """微信认证错误,包含 errcode 和 errmsg。""" def __init__(self, errcode: int, errmsg: str) -> None: self.errcode = errcode self.errmsg = errmsg super().__init__(f"WeChatAuthError({errcode}): {errmsg}") @property def http_status(self) -> int: """根据 errcode 映射到建议的 HTTP 状态码。""" return _WX_ERROR_MAP.get(self.errcode, (401, ""))[0] @property def detail(self) -> str: """根据 errcode 映射到用户可见的错误提示。""" return _WX_ERROR_MAP.get(self.errcode, (401, "微信登录失败"))[1] async def code2session(code: str) -> dict: """ 调用微信 code2Session 接口。 参数: code: 小程序端 wx.login() 获取的临时登录凭证 返回: {"openid": str, "session_key": str, "unionid": str | None} 异常: WeChatAuthError: 微信接口返回非零 errcode 时抛出 RuntimeError: WX_APPID / WX_SECRET 环境变量缺失时抛出 """ appid = WX_APPID secret = WX_SECRET if not appid or not secret: raise RuntimeError("微信配置缺失:WX_APPID 或 WX_SECRET 未设置") params = { "appid": appid, "secret": secret, "js_code": code, "grant_type": "authorization_code", } async with httpx.AsyncClient(timeout=10.0) as client: resp = await client.get(CODE2SESSION_URL, params=params) resp.raise_for_status() data = resp.json() errcode = data.get("errcode", 0) if errcode != 0: errmsg = data.get("errmsg", "unknown error") logger.warning("微信 code2Session 失败: errcode=%s, errmsg=%s", errcode, errmsg) raise WeChatAuthError(errcode, errmsg) return { "openid": data["openid"], "session_key": data["session_key"], "unionid": data.get("unionid"), }