Files
Django/shop/views/shop_base_views.py
XingQue df49631401 fix: 店铺二维码生成与下载接口
使用 stable access_token 并重试;规范化 OSS Key;下载接口返回 JSON 错误便于前端识别。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 23:51:00 +08:00

755 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""shop.views.shop_base_views - 店铺基础视图:绑定/登录/概览/二维码/收支统计."""
# ==================== 标准库 ====================
import io
import json
import time
import random
import string
import hmac
import hashlib
import requests
import logging
import traceback
import calendar
import threading
import xmltodict
from decimal import Decimal
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
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.paginator import Paginator, EmptyPage
from django.core.exceptions import ObjectDoesNotExist
from django.utils import timezone
from django.http import HttpResponse
from rest_framework.views import APIView
from rest_framework.response import Response
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, _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
from shop.utils import verify_shop_permission, update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from orders.utils import update_daily_payout
from backend.utils import update_dashou_daily_by_action
from ..models import YonghuPingzheng, Dianpu, DianpuShouzhiMeiriTongji
from users.models import UserDashou, UserShangjia, UserGuanshi, UserZuzhang
from users.business_models import User
from shop.models import YonghuDianpuBangding, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
from products.models import ShangpinLeixing, ShangpinZhuanqu, Shangpin, Huiyuan
from orders.models import (
Order, PlatformOrderExt, PlayerDeliveryImage, RefundRecord,
OrderPlayerHistory, Penalty
)
logger = logging.getLogger(__name__)
class BindDianpuView(APIView):
"""
店铺绑定接口
接收前端传入的店铺ID更新用户与店铺的绑定关系
"""
permission_classes = [IsAuthenticated]
def post(self, request):
dianpu_id = request.data.get('dianpu_id')
if not dianpu_id:
return Response({'code': 400, 'msg': '店铺ID不能为空'})
user = request.user
if not hasattr(user, 'UserUID'):
return Response({'code': 500, 'msg': '用户身份信息异常'})
# 先获取 User 对象(不加锁,仅用于校验存在性)
try:
user_main = User.query.get(UserUID=user.UserUID)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
# 校验店铺是否存在且状态正常(不加锁)
try:
dianpu = Dianpu.query.get(id=dianpu_id)
except Dianpu.DoesNotExist:
return Response({'code': 404, 'msg': '店铺不存在'})
if dianpu.zhuangtai != 1:
return Response({'code': 200, 'msg': '店铺已被封禁,无法绑定'})
# 在事务内部进行加锁操作
try:
with transaction.atomic():
# 重新获取加锁的用户对象(确保并发安全)
user_main_locked = User.objects.select_for_update().get(UserUID=user.UserUID)
dianpu_locked = Dianpu.objects.select_for_update().get(id=dianpu_id)
# 再次校验店铺状态(防止事务期间被修改)
if dianpu_locked.zhuangtai != 1:
return Response({'code': 200, 'msg': '店铺已被封禁,无法绑定'})
# 处理绑定关系
try:
existing_binding = YonghuDianpuBangding.objects.select_for_update().get(yonghu=user_main_locked)
old_dianpu = existing_binding.dianpu
if old_dianpu.id == dianpu_locked.id:
return Response({'code': 200, 'msg': '已绑定该店铺'})
# 旧店铺绑定人数减1
old_dianpu.bangding_yonghushu = F('bangding_yonghushu') - 1
old_dianpu.save(update_fields=['bangding_yonghushu'])
# 更新绑定关系
existing_binding.dianpu = dianpu_locked
existing_binding.save()
except YonghuDianpuBangding.DoesNotExist:
YonghuDianpuBangding.query.create(yonghu=user_main_locked, dianpu=dianpu_locked)
# 新店铺绑定人数加1
dianpu_locked.bangding_yonghushu = F('bangding_yonghushu') + 1
dianpu_locked.save(update_fields=['bangding_yonghushu'])
logger.info(f"用户 {user.UserUID} 绑定店铺 {dianpu_id} 成功")
return Response({'code': 200, 'msg': '绑定成功'})
except Exception as e:
logger.exception(f"绑定店铺失败: {e}")
return Response({'code': 500, 'msg': '绑定失败,服务器内部错误'})
class ShopGoodsView(APIView):
"""
获取店铺商品数据(类型、专区、商品列表)
根据用户绑定店铺状态及身份,返回店铺专属或公共商品
"""
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
try:
user_main = User.query.get(UserUUID=user.UserUUID)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
# 1. 查询用户店铺绑定关系
binding = YonghuDianpuBangding.query.filter(
yonghu=user_main
).select_related('dianpu').first()
# 2. 判断用户是否拥有特殊身份
has_special = self._has_special_identity(user_main)
# 3. 根据绑定关系及店铺状态决定数据获取策略
if binding:
dianpu = binding.dianpu
if dianpu.zhuangtai == 1:
# 店铺正常 -> 返回店铺专属商品(需通过平台审核)
data = self._get_shop_goods(dianpu)
else:
# 店铺被封 -> 返回公共商品有特殊身份则返回审核状态为1的否则返回2的
data = self._get_public_goods(True)
else:
# 无绑定店铺,根据身份决定返回公共商品的审核状态
data = self._get_public_goods(has_special)
return Response({'code': 200, 'data': data})
def _has_special_identity(self, user_main):
"""判断用户是否拥有打手/商家/管事/组长任一身份"""
return (UserDashou.query.filter(user=user_main).exists() or
UserShangjia.query.filter(user=user_main).exists() or
UserGuanshi.query.filter(user=user_main).exists() or
UserZuzhang.query.filter(user=user_main).exists())
def _get_shop_goods(self, dianpu):
"""
获取店铺专属商品
类型、专区、商品均从店铺相关表中获取且需通过平台审核shenhe_zhuangtai=True
"""
# 1. 店铺商品类型:未封禁、已上架、平台审核通过
leixing_queryset = ShangpinLeixingDianpu.query.filter(
dianpu=dianpu,
fengjin_zhuangtai=False,
shangjia_zhuangtai=True,
shenhe_zhuangtai=True # 🔥 必须通过平台审核
).select_related('gonggong_leixing').order_by('-paixu', 'id')
leixing_data = []
for lx in leixing_queryset:
leixing_data.append({
'id': lx.id, # 店铺类型ID前端归类用
'jieshao': lx.jieshao,
'tupian_url': lx.tupian_url,
'paixu': lx.paixu
})
leixing_ids = [lx['id'] for lx in leixing_data]
# 2. 专区根据店铺和店铺类型ID未封禁、已上架
if leixing_ids:
zhuanqu_queryset = ShangpinZhuanqu.query.filter(
dianpu=dianpu,
dianpu_leixing_id__in=leixing_ids,
fengjin_zhuangtai=False,
shangjia_zhuangtai=True
).order_by('-paixu', 'id')
else:
zhuanqu_queryset = ShangpinZhuanqu.query.none()
zhuanqu_data = []
for zq in zhuanqu_queryset:
zhuanqu_data.append({
'id': zq.id,
'mingzi': zq.mingzi,
'leixing_id': zq.dianpu_leixing_id, # 对应店铺类型ID
'paixu': zq.paixu
})
# 3. 商品:根据店铺、店铺类型、专区,未封禁、已上架、平台审核通过、原有审核状态为正常
zhuanqu_ids = [zq['id'] for zq in zhuanqu_data]
if zhuanqu_ids:
shangpin_queryset = Shangpin.query.filter(
dianpu=dianpu,
dianpu_leixing_id__in=leixing_ids,
zhuanqu_id__in=zhuanqu_ids,
fengjin_zhuangtai=False,
shangjia_zhuangtai=True,
shenhe_zhuangtai=True, # 🔥 平台审核通过
shenhezhuangtai=1 # 原有审核状态:正常
).values(
'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu',
'dianpu_leixing_id', 'zhuanqu_id'
).order_by('-paixu', 'id')
shangpin_data = list(shangpin_queryset)
# 字段映射:前端统一使用 leixing_id
for sp in shangpin_data:
sp['leixing_id'] = sp.pop('dianpu_leixing_id')
else:
shangpin_data = []
return {
'shangpinleixing': leixing_data,
'shangpinzhuanqu': zhuanqu_data,
'shangpinliebiao': shangpin_data
}
def _get_public_goods(self, use_audit_status_one=True):
"""
获取公共商品
类型、专区、商品均从公共表获取。
- 必须满足 dianpu 为空且 dianpu_leixing 为空(即纯公共数据)。
- 审核状态:若 use_audit_status_one=True则返回 shenhezhuangtai=1 的数据;
否则返回 shenhezhuangtai=2 的数据(审核专用)。
"""
target_shenhe = 1 if use_audit_status_one else 2
# 1. 公共商品类型:原有审核状态为 target_shenhe
leixing_queryset = ShangpinLeixing.query.filter(
shenhezhuangtai=target_shenhe
).order_by('-paixu', 'id')
leixing_data = []
for lx in leixing_queryset:
leixing_data.append({
'id': lx.id,
'jieshao': lx.jieshao,
'tupian_url': lx.tupian_url,
'paixu': lx.paixu
})
visible_leixing_ids = [lx['id'] for lx in leixing_data]
# 2. 专区dianpu 为空,且 dianpu_leixing 为空leixing_id 在可见范围内,原有审核状态为 target_shenhe
if visible_leixing_ids:
zhuanqu_queryset = ShangpinZhuanqu.query.filter(
dianpu__isnull=True,
dianpu_leixing__isnull=True, # 🔥 必须为空
leixing_id__in=visible_leixing_ids,
shenhezhuangtai=target_shenhe
).order_by('-paixu', 'id')
else:
zhuanqu_queryset = ShangpinZhuanqu.query.none()
zhuanqu_data = []
for zq in zhuanqu_queryset:
zhuanqu_data.append({
'id': zq.id,
'mingzi': zq.mingzi,
'leixing_id': zq.leixing_id,
'paixu': zq.paixu
})
# 3. 商品dianpu 为空dianpu_leixing 为空,专区在可见范围内,原有审核状态为 target_shenhe
zhuanqu_ids = [zq['id'] for zq in zhuanqu_data]
if zhuanqu_ids:
shangpin_queryset = Shangpin.query.filter(
dianpu__isnull=True,
dianpu_leixing__isnull=True, # 🔥 必须为空
leixing_id__in=visible_leixing_ids,
zhuanqu_id__in=zhuanqu_ids,
shenhezhuangtai=target_shenhe
).values(
'id', 'biaoqian', 'jiage', 'tupian_url', 'duiwai_xiaoliang', 'paixu',
'leixing_id', 'zhuanqu_id'
).order_by('-paixu', 'id')
shangpin_data = list(shangpin_queryset)
else:
shangpin_data = []
return {
'shangpinleixing': leixing_data,
'shangpinzhuanqu': zhuanqu_data,
'shangpinliebiao': shangpin_data
}
class ShangdianLoginView(APIView):
"""
商家后台登录接口(通过凭证直接获取店铺)
POST /shangdian/login
请求参数:
yonghuid : 用户ID7位
zhanghao : 登录账号
mima : 密码
返回数据:
code: 0, data: {
token: "jwt_token",
yonghuid: "1234567",
dianpu_mingcheng: "店铺名称",
dianpu_touxiang: "头像相对URL",
erweima_url: "二维码相对URL"
}
"""
permission_classes = [AllowAny]
def post(self, request):
# 1. 获取请求参数
yonghuid = request.data.get('yonghuid', '').strip()
zhanghao = request.data.get('zhanghao', '').strip()
mima = request.data.get('mima', '').strip()
# 2. 参数完整性校验
if not yonghuid or not zhanghao or not mima:
return Response({'code': 400, 'msg': '用户ID、账号、密码均不能为空'})
# 3. 验证用户主表是否存在
try:
user_main = User.query.get(UserUID=yonghuid)
except User.DoesNotExist:
logger.warning(f"登录失败用户ID不存在 - {yonghuid}")
return Response({'code': 404, 'msg': '账号或密码错误'})
# 4. 通过用户ID + 账号查询凭证表
try:
pingzheng = YonghuPingzheng.query.get(
yonghu=yonghuid,
zhanghao=zhanghao,
is_active=True
)
except YonghuPingzheng.DoesNotExist:
logger.warning(f"登录失败:账号或密码错误 - 用户ID:{yonghuid}, 账号:{zhanghao}")
return Response({'code': 401, 'msg': '账号或密码错误'})
# 5. 校验密码
if pingzheng.mima != mima:
logger.warning(f"登录失败:密码错误 - 用户ID:{yonghuid}, 账号:{zhanghao}")
return Response({'code': 401, 'msg': '账号或密码错误'})
# 6. 【修改点】通过凭证直接获取关联的店铺(利用 OneToOneField 反向关系)
try:
dianpu = pingzheng.dianpu
except Dianpu.DoesNotExist:
logger.error(f"登录异常:凭证 {zhanghao} 未关联任何店铺")
return Response({'code': 403, 'msg': '您尚未关联店铺,请联系管理员'})
# 7. 检查店铺状态
if dianpu.zhuangtai != 1:
logger.warning(f"登录失败:店铺已被封禁 - 店铺ID:{dianpu.id}, 用户ID:{yonghuid}")
return Response({'code': 403, 'msg': '店铺已被封禁,无法登录'})
# 8. 生成 JWT Token
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
# 9. 更新最后登录时间
user_main.UserLastLoginDate = timezone.now()
user_main.save(update_fields=['UserLastLoginDate'])
# 10. 构造返回数据
data = {
'token': token,
'yonghuid': user_main.UserUID,
'dianpu_mingcheng': dianpu.dianpu_mingcheng,
'dianpu_touxiang': dianpu.dianpu_touxiang or '',
'erweima_url': dianpu.erweima_url or ''
}
logger.info(f"商家登录成功用户ID {yonghuid} - 店铺 {dianpu.dianpu_mingcheng}")
return Response({'code': 0, 'msg': '登录成功', 'data': data})
class ShopOverviewView(APIView):
"""
店铺概览数据接口
URL: /shangdian/sjdpgl
Method: POST
权限: JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 从请求体中获取前端传递的 userId用于防越权
frontend_user_id = request.data.get('userId')
if not frontend_user_id:
return Response(
{'code': 400, 'msg': '缺少必要参数 userId'}
)
# 调用公共验证方法
dianpu, error_response = verify_shop_permission(request, frontend_user_id)
if error_response:
return error_response
# 验证通过,构造返回数据(新增字段,其余保持不变)
data = {
'id': dianpu.id,
'dianpu_mingcheng': dianpu.dianpu_mingcheng,
'dianpu_touxiang': dianpu.dianpu_touxiang or '',
'lianxi_dianhua': dianpu.lianxi_dianhua or '',
'weixinhao': dianpu.weixinhao or '',
'zhuangtai': dianpu.zhuangtai,
'bangding_yonghushu': dianpu.bangding_yonghushu,
'erweima_url': dianpu.erweima_url or '',
'CreateTime': dianpu.CreateTime.isoformat() if dianpu.CreateTime else '',
'UpdateTime': dianpu.UpdateTime.isoformat() if dianpu.UpdateTime else '',
# 新增字段
'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng,
'shouyi_choucheng_feilv': float(dianpu.shouyi_choucheng_feilv) if dianpu.shouyi_choucheng_feilv else 0.0,
'ketixian_yue': float(dianpu.ketixian_yue),
'zhengqu_zonge': float(dianpu.zhengqu_zonge),
'meiri_tixian_xiane': float(dianpu.meiri_tixian_xiane) if dianpu.meiri_tixian_xiane else 0.0,
'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane,
}
return Response({
'code': 0,
'msg': 'success',
'data': data
})
class QrcodeDownloadView(APIView):
"""
店铺二维码下载接口(通过后端代理下载,解决跨域限制)
URL: /shangdian/xzewm?dianpu_id=xx
权限: JWT认证 + 店铺归属验证
"""
permission_classes = [IsAuthenticated]
def get(self, request):
# 1. 身份验证与店铺校验
frontend_user_id = request.GET.get('userId') # 前端通过查询参数传递 userId
if not frontend_user_id:
return Response({'code': 400, 'msg': '缺少必要参数 userId'}, status=400)
dianpu, error_response = verify_shop_permission(request, frontend_user_id)
if error_response:
return error_response
# 2. 获取店铺的二维码相对路径
erweima_relative_url = dianpu.erweima_url
if not erweima_relative_url:
return Response({'code': 404, 'msg': '店铺暂无二维码'}, status=404)
oss_key = _url_to_oss_key(erweima_relative_url) or str(erweima_relative_url).lstrip('/')
# 3. 从腾讯云 COS 获取图片内容
try:
client = get_oss_client()
bucket = getattr(settings, 'COS_BUCKET', '')
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)
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'{disposition_type}; filename="{filename}"'
response['Content-Length'] = str(len(image_data))
response['Cache-Control'] = 'private, max-age=300'
return response
class DianpuShouzhiTongjiView(APIView):
"""
店铺每日收支统计接口(支持按日、月、年聚合)
URL: /dianpu/dpsztj
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 身份验证
frontend_user_id = request.data.get('userId')
if not frontend_user_id:
return Response({'code': 400, 'msg': '缺少 userId'})
dianpu, error = verify_shop_permission(request, frontend_user_id)
if error:
return error
granularity = request.data.get('granularity', 'day') # day / month / year
year = request.data.get('year')
month = request.data.get('month')
# 查询当前店铺的统计记录
qs = DianpuShouzhiMeiriTongji.query.filter(dianpu_id=dianpu.id)
if granularity == 'day':
if not year or not month:
return Response({'code': 400, 'msg': '日颗粒度需要提供年份和月份'})
try:
year = int(year)
month = int(month)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '年份或月份格式错误'})
# 取出该月每一天的数据,并按日排序
daily_data = qs.filter(nian=year, yue=month).values('ri').annotate(
xiadan_zongliang=Sum('xiadan_zongliang'),
xiadan_zonge=Sum('xiadan_zonge'),
chengjiao_zongliang=Sum('chengjiao_zongliang'),
chengjiao_zonge=Sum('chengjiao_zonge'),
tuikuan_zongliang=Sum('tuikuan_zongliang'),
tuikuan_zonge=Sum('tuikuan_zonge'),
shouyi_zonge=Sum('shouyi_zonge'),
).order_by('ri')
# 补全缺失的日期默认值为0
days_in_month = calendar.monthrange(year, month)[1]
result = []
for day in range(1, days_in_month + 1):
found = next((item for item in daily_data if item['ri'] == day), None)
if found:
result.append(found)
else:
result.append({
'ri': day,
'xiadan_zongliang': 0, 'xiadan_zonge': 0,
'chengjiao_zongliang': 0, 'chengjiao_zonge': 0,
'tuikuan_zongliang': 0, 'tuikuan_zonge': 0,
'shouyi_zonge': 0,
})
return Response({'code': 0, 'data': result, 'type': 'day'})
elif granularity == 'month':
if not year:
return Response({'code': 400, 'msg': '月颗粒度需要提供年份'})
try:
year = int(year)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '年份格式错误'})
# 按月份聚合
monthly_data = qs.filter(nian=year).values('yue').annotate(
xiadan_zongliang=Sum('xiadan_zongliang'),
xiadan_zonge=Sum('xiadan_zonge'),
chengjiao_zongliang=Sum('chengjiao_zongliang'),
chengjiao_zonge=Sum('chengjiao_zonge'),
tuikuan_zongliang=Sum('tuikuan_zongliang'),
tuikuan_zonge=Sum('tuikuan_zonge'),
shouyi_zonge=Sum('shouyi_zonge'),
).order_by('yue')
# 补全 1~12 月
result = []
for m in range(1, 13):
found = next((item for item in monthly_data if item['yue'] == m), None)
if found:
result.append(found)
else:
result.append({
'yue': m,
'xiadan_zongliang': 0, 'xiadan_zonge': 0,
'chengjiao_zongliang': 0, 'chengjiao_zonge': 0,
'tuikuan_zongliang': 0, 'tuikuan_zonge': 0,
'shouyi_zonge': 0,
})
return Response({'code': 0, 'data': result, 'type': 'month'})
elif granularity == 'year':
# 按年份聚合
yearly_data = qs.values('nian').annotate(
xiadan_zongliang=Sum('xiadan_zongliang'),
xiadan_zonge=Sum('xiadan_zonge'),
chengjiao_zongliang=Sum('chengjiao_zongliang'),
chengjiao_zonge=Sum('chengjiao_zonge'),
tuikuan_zongliang=Sum('tuikuan_zongliang'),
tuikuan_zonge=Sum('tuikuan_zonge'),
shouyi_zonge=Sum('shouyi_zonge'),
).order_by('nian')
return Response({'code': 0, 'data': list(yearly_data), 'type': 'year'})
else:
return Response({'code': 400, 'msg': '无效的颗粒度'})
class ShopQrcodeView(APIView):
"""
店铺专属二维码生成/更新接口
URL: /shangdian/sdscdpm
Method: POST
权限: JWT认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 获取前端传递的 userId
frontend_user_id = request.data.get('userId')
if not frontend_user_id:
return Response(
{'code': 400, 'msg': '缺少必要参数 userId'},
status=status.HTTP_400_BAD_REQUEST
)
# 公共验证
dianpu, error_response = verify_shop_permission(request, frontend_user_id)
if error_response:
return error_response
# 获取微信 access_tokenstable_token + 缓存)
access_token = get_weixin_mini_access_token()
if not access_token:
return Response(
{'code': 500, 'msg': '获取微信 access_token 失败,请检查小程序配置'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 构造小程序码请求参数scene 传店铺ID首页解析 scene 进入店铺)
scene = str(dianpu.id)
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,
}
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)
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
)
# 构造文件名和 COS 路径
timestamp = int(time.time())
# 三层文件夹结构: dianpu/shangjiadianpu/dianpuerweima/
filename = f"dianpu_{dianpu.id}_{timestamp}.png"
oss_path = f"dianpu/shangjiadianpu/dianpuerweima/{filename}"
file_obj = io.BytesIO(wx_resp.content)
# 上传到 COS
try:
full_url = upload_to_oss(file_obj, oss_path)
except Exception as e:
logger.error(f"上传二维码到 COS 失败: {e}")
return Response(
{'code': 500, 'msg': '图片上传失败'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if not full_url:
return Response(
{'code': 500, 'msg': '图片上传失败'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 提取相对路径(与商品图片上传逻辑一致)
cos_domain = getattr(settings, 'COS_DOMAIN', '').rstrip('/')
if cos_domain and full_url.startswith(cos_domain):
relative_path = full_url.replace(f'{cos_domain}/', '', 1)
else:
relative_path = oss_path
# 删除旧二维码文件(如果存在)
old_relative_path = dianpu.erweima_url
if old_relative_path:
try:
delete_from_oss(old_relative_path)
except Exception as e:
logger.warning(f"删除旧二维码文件失败: {old_relative_path}, 错误: {e}")
# 更新数据库
with transaction.atomic():
dianpu.erweima_url = relative_path
dianpu.save(update_fields=['erweima_url', 'UpdateTime'])
return Response({
'code': 0,
'msg': 'success',
'data': {'erweima_url': relative_path}
})