wechat_mp_auth.py 2.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. """微信服务号:access_token、oauth code 换 openid。"""
  2. from __future__ import annotations
  3. import logging
  4. from datetime import datetime, timedelta
  5. from typing import Any, Dict, Optional
  6. import httpx
  7. from app.core.config import settings
  8. logger = logging.getLogger(__name__)
  9. _token: Optional[str] = None
  10. _token_expire: Optional[datetime] = None
  11. class WechatMpAuthError(Exception):
  12. pass
  13. def _require_mp_credentials() -> tuple:
  14. appid = (settings.WX_MP_APPID or "").strip()
  15. secret = (settings.WX_MP_SECRET or "").strip()
  16. if not appid or not secret:
  17. raise WechatMpAuthError("未配置服务号凭证(WX_MP_APPID / WX_MP_SECRET)")
  18. return appid, secret
  19. def get_mp_access_token() -> str:
  20. global _token, _token_expire
  21. if _token and _token_expire and datetime.now() < _token_expire:
  22. return _token
  23. appid, secret = _require_mp_credentials()
  24. with httpx.Client(timeout=15.0) as client:
  25. r = client.get(
  26. "https://api.weixin.qq.com/cgi-bin/token",
  27. params={
  28. "grant_type": "client_credential",
  29. "appid": appid,
  30. "secret": secret,
  31. },
  32. )
  33. r.raise_for_status()
  34. data = r.json()
  35. token = data.get("access_token")
  36. if not token:
  37. logger.warning("mp access_token failed: %s", data)
  38. raise WechatMpAuthError("获取服务号 access_token 失败")
  39. _token = token
  40. _token_expire = datetime.now() + timedelta(seconds=int(data.get("expires_in", 7200)) - 300)
  41. return token
  42. def oauth_code_to_openid(code: str) -> Dict[str, Any]:
  43. """snsapi_base:code → openid / unionid。"""
  44. c = (code or "").strip()
  45. if not c:
  46. raise WechatMpAuthError("code 不能为空")
  47. appid, secret = _require_mp_credentials()
  48. with httpx.Client(timeout=15.0) as client:
  49. r = client.get(
  50. "https://api.weixin.qq.com/sns/oauth2/access_token",
  51. params={
  52. "appid": appid,
  53. "secret": secret,
  54. "code": c,
  55. "grant_type": "authorization_code",
  56. },
  57. )
  58. r.raise_for_status()
  59. data = r.json()
  60. if data.get("errcode"):
  61. raise WechatMpAuthError(data.get("errmsg") or "服务号授权失败")
  62. openid = data.get("openid")
  63. if not openid:
  64. raise WechatMpAuthError("服务号未返回 openid")
  65. return {"openid": openid, "unionid": data.get("unionid")}