fix: 店铺二维码生成与下载接口

使用 stable access_token 并重试;规范化 OSS Key;下载接口返回 JSON 错误便于前端识别。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-09 23:51:00 +08:00
parent 90e187b65c
commit df49631401

View File

@@ -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_tokenstable_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