diff --git a/config/views.py b/config/views.py index 14ddc1c..181cd56 100644 --- a/config/views.py +++ b/config/views.py @@ -26,6 +26,7 @@ from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image from utils.weixin_broadcast import WeixinBroadcastSender +from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid # from utils.ip_security import * from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode @@ -3002,8 +3003,8 @@ class ZuzhangHaibaoView(APIView): zuzhang.yaoqingma = invite_code zuzhang.save(update_fields=['yaoqingma']) - # 4. 获取微信 access_token - access_token = self.get_cached_access_token() + # 4. 获取微信 access_token(优先 stable_token,token 失效时自动刷新重试) + access_token = get_weixin_mini_access_token(force_refresh=False) if not access_token: return Response({ 'code': 500, @@ -3012,58 +3013,36 @@ class ZuzhangHaibaoView(APIView): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # ===== 修改点:使用 A 接口(getwxacode) ===== - # 对邀请码进行 URL 编码,防止特殊字符破坏路径 encoded_invite = urllib.parse.quote(invite_code, safe='') - # 构造带参数的完整路径(请根据实际页面调整) page_path = f'pages/guanshiduan/guanshiduan?inviteCode={encoded_invite}' - wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' - post_data = { - 'path': page_path, - 'width': 430, - 'auto_color': False, - 'line_color': {'r': 0, 'g': 0, 'b': 0} - } + wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token) + if wx_error and is_weixin_token_invalid(wx_error.get('errcode'), wx_error.get('errmsg', '')): + logger.warning('组长海报二维码 token 失效,强制刷新后重试') + access_token = get_weixin_mini_access_token(force_refresh=True) + if not access_token: + return Response({ + 'code': 500, + 'message': '获取微信access_token失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token) - try: - wx_resp = requests.post(wx_url, json=post_data, timeout=10) - except Exception as e: - logger.error(f"请求微信接口失败: {e}") - return Response({ - 'code': 500, - 'message': '调用微信服务失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - # 处理微信返回结果 - if wx_resp.status_code != 200: - error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {} - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}") + if wx_error: + errmsg = wx_error.get('errmsg', '未知错误') + logger.error(f"微信接口返回错误: {errmsg}") return Response({ 'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - content_type = wx_resp.headers.get('content-type', '') - if 'image' not in content_type: - try: - error_info = wx_resp.json() - errmsg = error_info.get('errmsg', '未知错误') - logger.error(f"微信接口返回非图片内容: {errmsg}") - return Response({ - 'code': 500, - 'message': f'生成二维码失败: {errmsg}', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) - except: - logger.error("微信接口返回未知内容") - return Response({ - 'code': 500, - 'message': '生成二维码失败', - 'data': None - }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + if wx_resp is None: + return Response({ + 'code': 500, + 'message': '调用微信服务失败', + 'data': None + }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 5. 上传到腾讯云 COS timestamp = int(time.time()) @@ -3115,31 +3094,48 @@ class ZuzhangHaibaoView(APIView): } }) - def get_cached_access_token(self): - """从缓存获取微信 access_token""" - cache_key = 'wx_mini_access_token' - token = cache.get(cache_key) - if token: - return token - - appid = getattr(settings, 'WEIXIN_APPID', '') - secret = getattr(settings, 'WEIXIN_SECRET', '') - if not appid or not secret: - logger.error("微信小程序配置缺失") - return None - - url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}' + def _request_wx_qrcode(self, page_path, access_token): + """ + 调用 getwxacode 生成小程序码。 + 返回 (response, error_dict);成功时 error_dict 为 None。 + """ + wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}' + post_data = { + 'path': page_path, + 'width': 430, + 'auto_color': False, + 'line_color': {'r': 0, 'g': 0, 'b': 0}, + } try: - resp = requests.get(url, timeout=5) - data = resp.json() - token = data.get('access_token') - expires_in = data.get('expires_in', 7200) - if token: - cache.set(cache_key, token, 7000) - return token + wx_resp = requests.post(wx_url, json=post_data, timeout=10) except Exception as e: - logger.error(f"获取微信access_token失败: {e}") - return None + logger.error(f'请求微信接口失败: {e}') + return None, {'errmsg': '调用微信服务失败'} + + if wx_resp.status_code != 200: + error_info = {} + if wx_resp.headers.get('content-type', '').startswith('application/json'): + try: + error_info = wx_resp.json() + except Exception: + pass + return wx_resp, { + 'errcode': error_info.get('errcode'), + 'errmsg': error_info.get('errmsg', f'HTTP {wx_resp.status_code}'), + } + + content_type = wx_resp.headers.get('content-type', '') + if 'image' not in content_type: + try: + error_info = wx_resp.json() + return wx_resp, { + 'errcode': error_info.get('errcode'), + 'errmsg': error_info.get('errmsg', '未知错误'), + } + except Exception: + return wx_resp, {'errmsg': '微信接口返回未知内容'} + + return wx_resp, None diff --git a/utils/weixin_token.py b/utils/weixin_token.py new file mode 100644 index 0000000..f15f12d --- /dev/null +++ b/utils/weixin_token.py @@ -0,0 +1,94 @@ +import logging + +import requests +from django.conf import settings +from django.core.cache import cache + +logger = logging.getLogger(__name__) + +WX_TOKEN_CACHE_KEY = 'wx_mini_access_token' + + +def invalidate_weixin_mini_access_token(): + cache.delete(WX_TOKEN_CACHE_KEY) + + +def get_weixin_mini_access_token(*, force_refresh=False): + """ + 获取小程序 access_token。 + force_refresh=True 时清除缓存并强制向微信重新获取,避免 token 非最新导致 40001。 + 优先调用 stable_token,失败时回退普通 token 接口。 + """ + if not force_refresh: + token = cache.get(WX_TOKEN_CACHE_KEY) + if token: + return token + + if force_refresh: + invalidate_weixin_mini_access_token() + + appid = getattr(settings, 'WEIXIN_APPID', '') + secret = getattr(settings, 'WEIXIN_SECRET', '') + if not appid or not secret: + logger.error('微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置') + 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(WX_TOKEN_CACHE_KEY, token, ttl) + return token + + +def is_weixin_token_invalid(errcode=None, errmsg=''): + if errcode in (40001, 40014, 42001): + return True + msg = (errmsg or '').lower() + return 'invalid credential' in msg or 'access_token is invalid' in msg + + +def _fetch_stable_token(appid, secret, force_refresh): + url = 'https://api.weixin.qq.com/cgi-bin/stable_token' + payload = { + 'grant_type': 'client_credential', + 'appid': appid, + 'secret': secret, + 'force_refresh': bool(force_refresh), + } + try: + resp = requests.post(url, json=payload, timeout=5) + data = resp.json() + token = data.get('access_token') + if token: + logger.info('成功获取微信 stable access_token') + return token, data.get('expires_in', 7200) + logger.warning( + 'stable_token 获取失败 errcode=%s errmsg=%s', + data.get('errcode'), data.get('errmsg'), + ) + except Exception as e: + logger.warning('请求 stable_token 异常: %s', e) + return None, None + + +def _fetch_legacy_token(appid, secret): + url = 'https://api.weixin.qq.com/cgi-bin/token' + params = { + 'grant_type': 'client_credential', + 'appid': appid, + 'secret': secret, + } + try: + resp = requests.get(url, params=params, timeout=5) + data = resp.json() + token = data.get('access_token') + if token: + logger.info('成功获取微信 access_token (legacy)') + return token, data.get('expires_in', 7200) + logger.error('legacy token 获取失败: %s', data) + except Exception as e: + logger.error('请求 legacy token 异常: %s', e) + return None, None