fix: 手机号认证按俱乐部 wx_appid 换 token,修复星之界 40013
This commit is contained in:
@@ -610,7 +610,7 @@ class UserInfoUpdateView(APIView):
|
|||||||
# 手机号 —— 极简可靠版
|
# 手机号 —— 极简可靠版
|
||||||
code = request.data.get('shoujihao_code')
|
code = request.data.get('shoujihao_code')
|
||||||
if code:
|
if code:
|
||||||
phone = self._get_phone_safe(code)
|
phone = self._get_phone_safe(code, request)
|
||||||
if phone is None:
|
if phone is None:
|
||||||
return Response({'code': 6, 'msg': '手机号获取失败,请重新授权', 'data': None}, status=400)
|
return Response({'code': 6, 'msg': '手机号获取失败,请重新授权', 'data': None}, status=400)
|
||||||
user.Phone = phone
|
user.Phone = phone
|
||||||
@@ -630,35 +630,47 @@ class UserInfoUpdateView(APIView):
|
|||||||
return Response({'code': 99, 'msg': str(e), 'data': None}, status=500)
|
return Response({'code': 99, 'msg': str(e), 'data': None}, status=500)
|
||||||
|
|
||||||
# ---------- 安全手机号(内存去重) ----------
|
# ---------- 安全手机号(内存去重) ----------
|
||||||
def _get_phone_safe(self, code):
|
def _get_phone_safe(self, code, request):
|
||||||
# 如果已经处理过这个code,直接返回结果
|
cache_key = f'{code}:{self._resolve_wx_club_id(request, request.user)}'
|
||||||
if code in self._phone_cache:
|
if cache_key in self._phone_cache:
|
||||||
cached = self._phone_cache[code]
|
cached = self._phone_cache[cache_key]
|
||||||
return cached if cached != '__FAIL__' else None
|
return cached if cached != '__FAIL__' else None
|
||||||
|
|
||||||
# 标记处理中(简单防重入)
|
self._phone_cache[cache_key] = '__PROCESSING__'
|
||||||
self._phone_cache[code] = '__PROCESSING__'
|
|
||||||
try:
|
try:
|
||||||
phone = self._call_wechat(code)
|
phone = self._call_wechat(code, request)
|
||||||
self._phone_cache[code] = phone
|
self._phone_cache[cache_key] = phone
|
||||||
return phone
|
return phone
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"微信调用失败: {str(e)}")
|
logger.error(f"微信调用失败: {str(e)}")
|
||||||
self._phone_cache[code] = '__FAIL__'
|
self._phone_cache[cache_key] = '__FAIL__'
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ---------- 微信接口调用(每次全新token) ----------
|
def _resolve_wx_club_id(self, request, user):
|
||||||
def _call_wechat(self, code):
|
"""手机号 code 与小程序 appid 绑定,须用对应俱乐部的 wx 配置换 token。"""
|
||||||
token = self._get_new_token()
|
from jituan.constants import CLUB_ID_DEFAULT
|
||||||
|
from jituan.services.club_context import resolve_club_id_from_request
|
||||||
|
from jituan.services.club_user import get_user_club_id
|
||||||
|
|
||||||
|
club_id = resolve_club_id_from_request(request)
|
||||||
|
if club_id == CLUB_ID_DEFAULT:
|
||||||
|
user_club = get_user_club_id(user)
|
||||||
|
if user_club:
|
||||||
|
return user_club
|
||||||
|
return club_id
|
||||||
|
|
||||||
|
# ---------- 微信接口调用(按俱乐部 appid 换 token) ----------
|
||||||
|
def _call_wechat(self, code, request):
|
||||||
|
club_id = self._resolve_wx_club_id(request, request.user)
|
||||||
|
token = self._get_new_token(club_id, force_refresh=True)
|
||||||
url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
|
url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
|
||||||
resp = requests.post(url, json={'code': code}, timeout=10)
|
resp = requests.post(url, json={'code': code}, timeout=10)
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
errcode = data.get('errcode')
|
errcode = data.get('errcode')
|
||||||
if errcode == 0:
|
if errcode == 0:
|
||||||
return data['phone_info']['purePhoneNumber']
|
return data['phone_info']['purePhoneNumber']
|
||||||
# 即使刚拿的token也可能意外失效,重试一次
|
|
||||||
if errcode == 40001:
|
if errcode == 40001:
|
||||||
token = self._get_new_token()
|
token = self._get_new_token(club_id, force_refresh=True)
|
||||||
url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
|
url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}'
|
||||||
resp = requests.post(url, json={'code': code}, timeout=10)
|
resp = requests.post(url, json={'code': code}, timeout=10)
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
@@ -666,22 +678,13 @@ class UserInfoUpdateView(APIView):
|
|||||||
return data['phone_info']['purePhoneNumber']
|
return data['phone_info']['purePhoneNumber']
|
||||||
raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}")
|
raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}")
|
||||||
|
|
||||||
# ---------- 获取全新 access_token(不缓存,每次调微信) ----------
|
def _get_new_token(self, club_id, force_refresh=False):
|
||||||
def _get_new_token(self):
|
from utils.weixin_token import get_club_mini_access_token
|
||||||
appid = getattr(settings, 'WEIXIN_APPID', '')
|
|
||||||
secret = getattr(settings, 'WEIXIN_SECRET', '')
|
token = get_club_mini_access_token(club_id, force_refresh=force_refresh)
|
||||||
if not appid or not secret:
|
|
||||||
raise Exception('缺少微信配置')
|
|
||||||
resp = requests.get('https://api.weixin.qq.com/cgi-bin/token', params={
|
|
||||||
'grant_type': 'client_credential',
|
|
||||||
'appid': appid,
|
|
||||||
'secret': secret
|
|
||||||
})
|
|
||||||
data = resp.json()
|
|
||||||
token = data.get('access_token')
|
|
||||||
if token:
|
if token:
|
||||||
return token
|
return token
|
||||||
raise Exception(f"获取access_token失败: {data}")
|
raise Exception(f'俱乐部 {club_id} 缺少微信配置或获取 access_token 失败')
|
||||||
|
|
||||||
# ---------- 工具 ----------
|
# ---------- 工具 ----------
|
||||||
def _get_or_create_boss(self, user):
|
def _get_or_create_boss(self, user):
|
||||||
|
|||||||
@@ -9,10 +9,47 @@ logger = logging.getLogger(__name__)
|
|||||||
WX_TOKEN_CACHE_KEY = 'wx_mini_access_token'
|
WX_TOKEN_CACHE_KEY = 'wx_mini_access_token'
|
||||||
|
|
||||||
|
|
||||||
def invalidate_weixin_mini_access_token():
|
def invalidate_weixin_mini_access_token(club_id=None):
|
||||||
|
if club_id:
|
||||||
|
cache.delete(f'{WX_TOKEN_CACHE_KEY}:{club_id}')
|
||||||
|
else:
|
||||||
cache.delete(WX_TOKEN_CACHE_KEY)
|
cache.delete(WX_TOKEN_CACHE_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
def get_club_mini_access_token(club_id=None, *, force_refresh=False):
|
||||||
|
"""
|
||||||
|
按俱乐部读取 wx_appid/wx_secret 获取 access_token(多小程序上线必需)。
|
||||||
|
xq 缺配置时回退 settings;其它俱乐部读 club 表。
|
||||||
|
"""
|
||||||
|
from jituan.constants import CLUB_ID_DEFAULT
|
||||||
|
from jituan.services.club_resolver import get_club_wx_credentials
|
||||||
|
|
||||||
|
club_id = club_id or CLUB_ID_DEFAULT
|
||||||
|
cache_key = f'{WX_TOKEN_CACHE_KEY}:{club_id}'
|
||||||
|
|
||||||
|
if not force_refresh:
|
||||||
|
token = cache.get(cache_key)
|
||||||
|
if token:
|
||||||
|
return token
|
||||||
|
|
||||||
|
if force_refresh:
|
||||||
|
cache.delete(cache_key)
|
||||||
|
|
||||||
|
appid, secret, _ = get_club_wx_credentials(club_id)
|
||||||
|
if not appid or not secret:
|
||||||
|
logger.error('俱乐部 %s 微信配置缺失: wx_appid/wx_secret 未设置', club_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
token, expires_in = _fetch_stable_token(appid, secret, force_refresh)
|
||||||
|
if not token:
|
||||||
|
token, expires_in = _fetch_legacy_token(appid, secret)
|
||||||
|
|
||||||
|
if token:
|
||||||
|
ttl = max(60, int(expires_in or 7200) - 200)
|
||||||
|
cache.set(cache_key, token, ttl)
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
def get_weixin_mini_access_token(*, force_refresh=False):
|
def get_weixin_mini_access_token(*, force_refresh=False):
|
||||||
"""
|
"""
|
||||||
获取小程序 access_token。
|
获取小程序 access_token。
|
||||||
|
|||||||
Reference in New Issue
Block a user