fix: 店铺二维码生成与下载接口
使用 stable access_token 并重试;规范化 OSS Key;下载接口返回 JSON 错误便于前端识别。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,7 +21,6 @@ from django.conf import settings
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import F, Sum, Count, Q, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.core.cache import cache
|
||||
from django.core.paginator import Paginator, EmptyPage
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils import timezone
|
||||
@@ -33,7 +32,8 @@ from rest_framework import status
|
||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client
|
||||
from utils.oss_utils import upload_to_oss, delete_from_oss, get_oss_client, _url_to_oss_key
|
||||
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
|
||||
from utils.money import yuan_to_fen
|
||||
|
||||
from ..utils import verify_shop_permission
|
||||
@@ -482,31 +482,30 @@ class QrcodeDownloadView(APIView):
|
||||
# 2. 获取店铺的二维码相对路径
|
||||
erweima_relative_url = dianpu.erweima_url
|
||||
if not erweima_relative_url:
|
||||
return HttpResponse('店铺暂无二维码', status=404)
|
||||
return Response({'code': 404, 'msg': '店铺暂无二维码'}, status=404)
|
||||
|
||||
# 3. 拼接 OSS 完整访问地址(使用 settings 中的域名)
|
||||
oss_domain = getattr(settings, 'COS_DOMAIN', '')
|
||||
if not oss_domain:
|
||||
logger.error("COS_DOMAIN 未配置")
|
||||
return HttpResponse('OSS域名未配置', status=500)
|
||||
oss_key = _url_to_oss_key(erweima_relative_url) or str(erweima_relative_url).lstrip('/')
|
||||
|
||||
image_url = oss_domain.rstrip('/') + '/' + erweima_relative_url.lstrip('/')
|
||||
|
||||
# 4. 从腾讯云 COS 获取图片内容
|
||||
# 3. 从腾讯云 COS 获取图片内容
|
||||
try:
|
||||
client = get_oss_client()
|
||||
bucket = getattr(settings, 'COS_BUCKET', '')
|
||||
response = client.get_object(Bucket=bucket, Key=erweima_relative_url)
|
||||
image_data = response['Body'].get_raw_stream().read()
|
||||
except Exception as e:
|
||||
logger.exception("从COS获取二维码失败")
|
||||
return HttpResponse('下载失败,请稍后重试', status=502)
|
||||
cos_resp = client.get_object(Bucket=bucket, Key=oss_key)
|
||||
image_data = cos_resp['Body'].get_raw_stream().read()
|
||||
except Exception:
|
||||
logger.exception("从COS获取二维码失败 key=%s", oss_key)
|
||||
return Response({'code': 502, 'msg': '下载失败,请稍后重试'}, status=502)
|
||||
|
||||
# 5. 构造下载响应,强制浏览器保存为文件
|
||||
filename = f"店铺二维码_{dianpu.dianpu_mingcheng}.png"
|
||||
if not image_data:
|
||||
return Response({'code': 404, 'msg': '二维码文件不存在'}, status=404)
|
||||
|
||||
# 4. 返回图片(inline 供页面展示,attachment 供下载)
|
||||
filename = f"shop_qrcode_{dianpu.id}.png"
|
||||
disposition_type = 'inline' if request.GET.get('disposition') == 'inline' else 'attachment'
|
||||
response = HttpResponse(image_data, content_type='image/png')
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
response['Content-Disposition'] = f'{disposition_type}; filename="{filename}"'
|
||||
response['Content-Length'] = str(len(image_data))
|
||||
response['Cache-Control'] = 'private, max-age=300'
|
||||
return response
|
||||
|
||||
|
||||
@@ -649,42 +648,56 @@ class ShopQrcodeView(APIView):
|
||||
if error_response:
|
||||
return error_response
|
||||
|
||||
# 获取缓存的微信 access_token
|
||||
access_token = self._get_wx_access_token()
|
||||
# 获取微信 access_token(stable_token + 缓存)
|
||||
access_token = get_weixin_mini_access_token()
|
||||
if not access_token:
|
||||
return Response(
|
||||
{'code': 500, 'msg': '获取微信 access_token 失败'},
|
||||
{'code': 500, 'msg': '获取微信 access_token 失败,请检查小程序配置'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
# 构造小程序码请求参数
|
||||
# 注意:scene 最大32个字符,我们使用店铺ID
|
||||
# 构造小程序码请求参数(scene 传店铺ID,首页解析 scene 进入店铺)
|
||||
scene = str(dianpu.id)
|
||||
page_path = "pages/index/index" # 前端小程序首页路径
|
||||
wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}'
|
||||
page_path = getattr(settings, 'SHOP_QRCODE_PAGE', 'pages/index/index')
|
||||
post_data = {
|
||||
'scene': scene,
|
||||
'page': page_path,
|
||||
'width': 430,
|
||||
'auto_color': False,
|
||||
'line_color': {'r': 0, 'g': 0, 'b': 0},
|
||||
'check_path': False,
|
||||
}
|
||||
|
||||
# 调用微信接口生成小程序码
|
||||
try:
|
||||
wx_resp = requests.post(wx_url, json=post_data, timeout=10)
|
||||
except Exception as e:
|
||||
logger.error(f"请求微信生成二维码接口异常: {e}")
|
||||
return Response(
|
||||
{'code': 500, 'msg': '调用微信服务失败'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
def _request_wx_qrcode(token):
|
||||
wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={token}'
|
||||
return requests.post(wx_url, json=post_data, timeout=15)
|
||||
|
||||
# 判断返回是否为图片
|
||||
if wx_resp.status_code != 200 or 'image' not in wx_resp.headers.get('content-type', ''):
|
||||
error_info = wx_resp.json() if wx_resp.headers.get('content-type') == 'application/json' else {}
|
||||
errmsg = error_info.get('errmsg', '未知错误')
|
||||
logger.error(f"微信生成二维码失败: {errmsg}")
|
||||
wx_resp = _request_wx_qrcode(access_token)
|
||||
|
||||
# token 失效时强制刷新重试一次
|
||||
if wx_resp.headers.get('content-type', '').startswith('application/json'):
|
||||
try:
|
||||
err = wx_resp.json()
|
||||
if is_weixin_token_invalid(err.get('errcode'), err.get('errmsg', '')):
|
||||
access_token = get_weixin_mini_access_token(force_refresh=True)
|
||||
if access_token:
|
||||
wx_resp = _request_wx_qrcode(access_token)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 判断返回是否为图片(微信错误时可能返回 JSON)
|
||||
content_type = wx_resp.headers.get('content-type', '')
|
||||
if wx_resp.status_code != 200 or 'image' not in content_type:
|
||||
errmsg = '未知错误'
|
||||
try:
|
||||
error_info = wx_resp.json()
|
||||
errmsg = error_info.get('errmsg', errmsg)
|
||||
logger.error(
|
||||
'微信生成店铺二维码失败 dianpu=%s errcode=%s errmsg=%s',
|
||||
dianpu.id, error_info.get('errcode'), errmsg,
|
||||
)
|
||||
except Exception:
|
||||
logger.error('微信生成店铺二维码失败 dianpu=%s status=%s', dianpu.id, wx_resp.status_code)
|
||||
return Response(
|
||||
{'code': 500, 'msg': f'生成二维码失败: {errmsg}'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
@@ -713,10 +726,10 @@ class ShopQrcodeView(APIView):
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
# 提取相对路径(根据您的 upload_to_oss 返回值格式调整)
|
||||
cos_domain = getattr(settings, 'COS_DOMAIN', '')
|
||||
# 提取相对路径(与商品图片上传逻辑一致)
|
||||
cos_domain = getattr(settings, 'COS_DOMAIN', '').rstrip('/')
|
||||
if cos_domain and full_url.startswith(cos_domain):
|
||||
relative_path = full_url[len(cos_domain):].lstrip('/')
|
||||
relative_path = full_url.replace(f'{cos_domain}/', '', 1)
|
||||
else:
|
||||
relative_path = oss_path
|
||||
|
||||
@@ -739,33 +752,3 @@ class ShopQrcodeView(APIView):
|
||||
'data': {'erweima_url': relative_path}
|
||||
})
|
||||
|
||||
def _get_wx_access_token(self):
|
||||
"""
|
||||
获取微信小程序 access_token,带缓存
|
||||
缓存键: wx_mini_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("微信小程序 AppID 或 Secret 未配置")
|
||||
return None
|
||||
|
||||
url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}'
|
||||
try:
|
||||
resp = requests.get(url, timeout=5)
|
||||
data = resp.json()
|
||||
token = data.get('access_token')
|
||||
expires_in = data.get('expires_in', 7200)
|
||||
if token:
|
||||
# 提前200秒过期,防止边界情况
|
||||
cache.set(cache_key, token, expires_in - 200)
|
||||
return token
|
||||
except Exception as e:
|
||||
logger.error(f"获取微信 access_token 异常: {e}")
|
||||
return None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user