Files
zkxw/backend/app/services/wechat_mini_auth.py
T

123 lines
3.7 KiB
Python
Raw 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.
"""微信小程序登录:code2session、手机号快速验证。"""
from __future__ import annotations
import logging
import re
from typing import Any, Dict, Optional
import httpx
from app.core.config import settings
logger = logging.getLogger(__name__)
_WX_CODE2SESSION = "https://api.weixin.qq.com/sns/jscode2session"
_WX_TOKEN = "https://api.weixin.qq.com/cgi-bin/token"
_WX_PHONE = "https://api.weixin.qq.com/wxa/business/getuserphonenumber"
class WechatMiniAuthError(Exception):
def __init__(self, message: str, *, wx_errcode: Optional[int] = None):
super().__init__(message)
self.wx_errcode = wx_errcode
def normalize_cn_mobile(raw: Optional[str]) -> str:
if raw is None:
return ""
s = re.sub(r"\s+", "", str(raw).strip())
if s.startswith("+86"):
s = s[3:]
elif s.startswith("86") and len(s) == 13:
s = s[2:]
return s
def _require_mini_credentials() -> tuple:
appid = (settings.WXPAY_MINI_APPID or "").strip()
secret = (settings.WXPAY_SECRET or "").strip()
if not appid or not secret:
raise WechatMiniAuthError("未配置小程序凭证(WXPAY_MINI_APPID / WXPAY_SECRET)")
return appid, secret
def code2session(login_code: str) -> Dict[str, Any]:
code = (login_code or "").strip()
if not code:
raise WechatMiniAuthError("loginCode 不能为空")
appid, secret = _require_mini_credentials()
with httpx.Client(timeout=15.0) as client:
r = client.get(
_WX_CODE2SESSION,
params={
"appid": appid,
"secret": secret,
"js_code": code,
"grant_type": "authorization_code",
},
)
r.raise_for_status()
data = r.json()
if data.get("errcode"):
logger.warning("code2session failed: %s", data)
raise WechatMiniAuthError(
"微信登录凭证无效或已过期", wx_errcode=data.get("errcode")
)
openid = data.get("openid")
if not openid:
raise WechatMiniAuthError("微信未返回 openid")
return {
"openid": openid,
"session_key": data.get("session_key"),
"unionid": data.get("unionid"),
}
def _get_access_token() -> str:
appid, secret = _require_mini_credentials()
with httpx.Client(timeout=15.0) as client:
r = client.get(
_WX_TOKEN,
params={
"grant_type": "client_credential",
"appid": appid,
"secret": secret,
},
)
r.raise_for_status()
data = r.json()
token = data.get("access_token")
if not token:
logger.warning("get access_token failed: %s", data)
raise WechatMiniAuthError(
"获取微信 access_token 失败", wx_errcode=data.get("errcode")
)
return token
def get_phone_number(phone_code: str) -> str:
code = (phone_code or "").strip()
if not code:
raise WechatMiniAuthError("phoneCode 不能为空")
token = _get_access_token()
with httpx.Client(timeout=15.0) as client:
r = client.post(
f"{_WX_PHONE}?access_token={token}",
json={"code": code},
)
r.raise_for_status()
data = r.json()
if data.get("errcode") not in (0, None):
logger.warning("getuserphonenumber failed: %s", data)
raise WechatMiniAuthError(
"获取手机号失败,请改用账号密码登录",
wx_errcode=data.get("errcode"),
)
info = data.get("phone_info") or {}
phone = normalize_cn_mobile(
info.get("purePhoneNumber") or info.get("phoneNumber")
)
if not phone:
raise WechatMiniAuthError("微信未返回手机号")
return phone