"""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 : 用户ID(7位) 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) # 店铺二维码正常仅数十 KB,超过 2MB 视为 OSS 路径异常 if len(image_data) > 2 * 1024 * 1024: logger.error( '店铺二维码文件体积异常 dianpu=%s key=%s size=%s', dianpu.id, oss_key, len(image_data), ) return Response({'code': 500, 'msg': '二维码文件异常,请重新生成'}, status=500) # 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_token(stable_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} })