From df49631401178ee71555540008041688d1d05415 Mon Sep 17 00:00:00 2001 From: XingQue Date: Thu, 9 Jul 2026 23:51:00 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BA=97=E9=93=BA=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E4=B8=8E=E4=B8=8B=E8=BD=BD=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 使用 stable access_token 并重试;规范化 OSS Key;下载接口返回 JSON 错误便于前端识别。 Co-authored-by: Cursor --- shop/views/shop_base_views.py | 129 +++++++++++++++------------------- 1 file changed, 56 insertions(+), 73 deletions(-) diff --git a/shop/views/shop_base_views.py b/shop/views/shop_base_views.py index be7c677..8d458f3 100644 --- a/shop/views/shop_base_views.py +++ b/shop/views/shop_base_views.py @@ -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 -