feat: 更换邀请人、操作日志、组长海报token修复
复用 xiugaijilu 补全后台操作日志;新增更换打手/管事邀请人;修复组长海报 access_token 失效;修复组长列表 BossProfile 搜索 500。
This commit is contained in:
130
config/views.py
130
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
|
||||
|
||||
|
||||
|
||||
|
||||
94
utils/weixin_token.py
Normal file
94
utils/weixin_token.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user