# ==================== 标准库 ==================== 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.cache import cache 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 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 HttpResponse('店铺暂无二维码', status=404) # 3. 拼接 OSS 完整访问地址(使用 settings 中的域名) oss_domain = getattr(settings, 'COS_DOMAIN', '') if not oss_domain: logger.error("COS_DOMAIN 未配置") return HttpResponse('OSS域名未配置', status=500) image_url = oss_domain.rstrip('/') + '/' + erweima_relative_url.lstrip('/') # 4. 从腾讯云 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) # 5. 构造下载响应,强制浏览器保存为文件 filename = f"店铺二维码_{dianpu.dianpu_mingcheng}.png" response = HttpResponse(image_data, content_type='image/png') response['Content-Disposition'] = f'attachment; filename="{filename}"' response['Content-Length'] = str(len(image_data)) 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 access_token = self._get_wx_access_token() if not access_token: return Response( {'code': 500, 'msg': '获取微信 access_token 失败'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) # 构造小程序码请求参数 # 注意:scene 最大32个字符,我们使用店铺ID scene = str(dianpu.id) page_path = "pages/index/index" # 前端小程序首页路径 wx_url = f'https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token={access_token}' post_data = { 'scene': scene, 'page': page_path, 'width': 430, 'auto_color': False, 'line_color': {'r': 0, 'g': 0, 'b': 0}, } # 调用微信接口生成小程序码 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 ) # 判断返回是否为图片 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}") 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 ) # 提取相对路径(根据您的 upload_to_oss 返回值格式调整) cos_domain = getattr(settings, 'COS_DOMAIN', '') if cos_domain and full_url.startswith(cos_domain): relative_path = full_url[len(cos_domain):].lstrip('/') 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} }) 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 class ShopProductConfigView(APIView): """ 获取商品发布所需配置:公共类型、店铺商品类型、专区及各自商品数量 URL: /shangdian/dphqspxx 权限: JWT认证 + 店铺归属验证 """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 身份验证,获取店铺对象 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 # ---------- 2. 公共商品类型(需审核正常) ---------- try: public_types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values( 'id', 'jieshao', 'tupian_url' ) public_types = [{ 'id': pt['id'], 'jieshao': pt['jieshao'] or '', 'tupian_url': pt['tupian_url'] or '', } for pt in public_types_qs] except Exception as e: logger.exception("查询公共商品类型失败") return Response({'code': 500, 'msg': '服务器错误'}) # ---------- 3. 店铺商品类型 ---------- try: shop_types = ShangpinLeixingDianpu.query.filter( dianpu_id=dianpu.id ).select_related('gonggong_leixing').order_by('-paixu') except Exception as e: logger.exception("查询店铺商品类型失败") return Response({'code': 500, 'msg': '服务器错误'}) type_ids = [] shop_product_types = [] for t in shop_types: shop_product_types.append({ 'id': t.id, 'jieshao': t.jieshao or '', 'tupian_url': t.tupian_url or '', 'paixu': t.paixu, 'shangjia_zhuangtai': t.shangjia_zhuangtai, 'fengjin_zhuangtai': t.fengjin_zhuangtai, 'shenhe_zhuangtai': t.shenhe_zhuangtai, 'gonggong_leixing_id': t.gonggong_leixing_id, }) type_ids.append(t.id) # ---------- 4. 各类型商品数量统计 ---------- type_product_counts = {} if type_ids: counts = Shangpin.query.filter( dianpu_id=dianpu.id, dianpu_leixing_id__in=type_ids ).values('dianpu_leixing_id').annotate(cnt=Count('id')) type_product_counts = {item['dianpu_leixing_id']: item['cnt'] for item in counts} # ---------- 5. 专区列表 ---------- try: zhuanqu_qs = ShangpinZhuanqu.query.filter( dianpu_id=dianpu.id ).order_by('-paixu').values( 'id', 'mingzi', 'paixu', 'shangjia_zhuangtai', 'fengjin_zhuangtai', 'dianpu_leixing_id' ) zhuanqu_list = [] zhuanqu_ids = [] for zq in zhuanqu_qs: zhuanqu_list.append({ 'id': zq['id'], 'mingzi': zq['mingzi'] or '', 'paixu': zq['paixu'], 'shangjia_zhuangtai': zq['shangjia_zhuangtai'], 'fengjin_zhuangtai': zq['fengjin_zhuangtai'], 'dianpu_leixing_id': zq['dianpu_leixing_id'], }) zhuanqu_ids.append(zq['id']) except Exception as e: logger.exception("查询专区失败") return Response({'code': 500, 'msg': '服务器错误'}) # ---------- 6. 各专区商品数量统计 ---------- zhuanqu_product_counts = {} if zhuanqu_ids: counts = Shangpin.query.filter( dianpu_id=dianpu.id, zhuanqu_id__in=zhuanqu_ids ).values('zhuanqu_id').annotate(cnt=Count('id')) zhuanqu_product_counts = {item['zhuanqu_id']: item['cnt'] for item in counts} return Response({ 'code': 0, 'msg': 'success', 'data': { 'public_types': public_types, 'shop_product_types': shop_product_types, 'zhuanqu_list': zhuanqu_list, 'type_product_counts': type_product_counts, 'zhuanqu_product_counts': zhuanqu_product_counts, } }) class ShopProductModifyView(APIView): """店铺商品类型与专区管理(增、改、删)""" permission_classes = [IsAuthenticated] def post(self, request): try: frontend_user_id = request.data.get('userId') or request.POST.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 action = request.data.get('action') or request.POST.get('action') if not action: return Response({'code': 400, 'msg': '缺少操作类型 action'}) handlers = { 'add_type': self._add_type, 'update_type': self._update_type, 'delete_type': self._delete_type, 'add_zhuanqu': self._add_zhuanqu, 'update_zhuanqu': self._update_zhuanqu, 'delete_zhuanqu': self._delete_zhuanqu, } handler = handlers.get(action) if not handler: return Response({'code': 400, 'msg': '不支持的操作类型'}) return handler(dianpu, request) except Exception as e: logger.exception("ShopProductModifyView 全局异常") return Response({'code': 500, 'msg': '服务器内部错误'}) # ---------- 商品类型操作 ---------- @transaction.atomic def _add_type(self, dianpu, request): """添加店铺商品类型""" try: jieshao = request.data.get('jieshao') or request.POST.get('jieshao') gonggong_leixing_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id') paixu = request.data.get('paixu', 0) or request.POST.get('paixu', 0) if not jieshao or not gonggong_leixing_id: return Response({'code': 400, 'msg': '缺少介绍或公共类型ID'}) # 验证公共类型存在且正常 if not ShangpinLeixing.query.filter(id=gonggong_leixing_id, shenhezhuangtai=1).exists(): return Response({'code': 400, 'msg': '所选公共商品类型无效'}) image_file = request.FILES.get('image') if not image_file: return Response({'code': 400, 'msg': '必须上传类型图片'}) # 上传图片 file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png" full_url = upload_to_oss(image_file, file_path) if not full_url: return Response({'code': 500, 'msg': '图片上传失败'}) # 提取相对路径 tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) # 审核状态 audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() shenhe_status = not (audit_config and audit_config.kaiqi_shenhe) ShangpinLeixingDianpu.query.create( dianpu_id=dianpu.id, gonggong_leixing_id=gonggong_leixing_id, jieshao=jieshao, tupian_url=tupian_url, paixu=paixu, shangjia_zhuangtai=True, fengjin_zhuangtai=False, shenhe_zhuangtai=shenhe_status, ) return Response({'code': 0, 'msg': '商品类型添加成功'}) except Exception as e: logger.exception("添加商品类型失败") return Response({'code': 500, 'msg': '添加失败,请稍后重试'}) @transaction.atomic def _update_type(self, dianpu, request): """修改店铺商品类型,支持更换图片(先删旧图)""" type_id = request.data.get('type_id') or request.POST.get('type_id') if not type_id: return Response({'code': 400, 'msg': '缺少 type_id'}) try: obj = ShangpinLeixingDianpu.objects.select_for_update().get( id=type_id, dianpu_id=dianpu.id ) except ShangpinLeixingDianpu.DoesNotExist: return Response({'code': 404, 'msg': '商品类型不存在'}) except Exception as e: logger.exception("查询商品类型异常") return Response({'code': 500, 'msg': '服务器错误'}) try: update_fields = [] content_changed = False # 简介 if 'jieshao' in request.data or 'jieshao' in request.POST: obj.jieshao = request.data.get('jieshao') or request.POST.get('jieshao') update_fields.append('jieshao') content_changed = True # 映射公共类型 if 'gonggong_leixing_id' in request.data or 'gonggong_leixing_id' in request.POST: new_id = request.data.get('gonggong_leixing_id') or request.POST.get('gonggong_leixing_id') if not ShangpinLeixing.query.filter(id=new_id, shenhezhuangtai=1).exists(): return Response({'code': 400, 'msg': '公共商品类型无效'}) obj.gonggong_leixing_id = new_id update_fields.append('gonggong_leixing_id') content_changed = True # 排序 if 'paixu' in request.data or 'paixu' in request.POST: obj.paixu = request.data.get('paixu') or request.POST.get('paixu') update_fields.append('paixu') # 上架状态 if 'shangjia_zhuangtai' in request.data or 'shangjia_zhuangtai' in request.POST: val = request.data.get('shangjia_zhuangtai') or request.POST.get('shangjia_zhuangtai') if isinstance(val, str): val = val.lower() in ('true', '1', 'yes') obj.shangjia_zhuangtai = bool(val) update_fields.append('shangjia_zhuangtai') # 图片更换:先删除旧图,再上传新图 image_file = request.FILES.get('image') if image_file: if obj.tupian_url: delete_from_oss(obj.tupian_url) # 删除旧图片 file_path = f"dianpu/shangpin/shangpinleixing/{dianpu.id}_{int(time.time())}.png" full_url = upload_to_oss(image_file, file_path) if not full_url: return Response({'code': 500, 'msg': '图片上传失败'}) obj.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) update_fields.append('tupian_url') content_changed = True # 审核重置 if content_changed: audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() if audit_config and audit_config.kaiqi_shenhe: obj.shenhe_zhuangtai = False update_fields.append('shenhe_zhuangtai') if update_fields: obj.save(update_fields=update_fields) return Response({'code': 0, 'msg': '商品类型更新成功'}) except Exception as e: logger.exception("修改商品类型失败") return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) @transaction.atomic def _delete_type(self, dianpu, request): """删除店铺商品类型,可选是否同时删除关联商品""" type_id = request.data.get('id') or request.POST.get('id') if not type_id: return Response({'code': 400, 'msg': '缺少类型ID'}) try: obj = ShangpinLeixingDianpu.objects.select_for_update().get( id=type_id, dianpu_id=dianpu.id ) except ShangpinLeixingDianpu.DoesNotExist: return Response({'code': 404, 'msg': '商品类型不存在'}) except Exception as e: logger.exception("查询商品类型异常") return Response({'code': 500, 'msg': '服务器错误'}) try: delete_products = request.data.get('delete_products') or request.POST.get('delete_products') delete_products = str(delete_products).lower() in ('true', '1', 'yes') if delete_products: # 删除该类型下所有关联商品(同时清理图片) products = Shangpin.query.filter( dianpu_leixing_id=obj.id, dianpu_id=dianpu.id ) for p in products: if p.tupian_url: delete_from_oss(p.tupian_url) if p.guize_tupian: delete_from_oss(p.guize_tupian) p.delete() else: # 仅解除关联,不删商品 Shangpin.query.filter( dianpu_leixing_id=obj.id, dianpu_id=dianpu.id ).update(dianpu_leixing=None) # 删除类型自身图片 if obj.tupian_url: delete_from_oss(obj.tupian_url) obj.delete() return Response({'code': 0, 'msg': '类型删除成功'}) except Exception as e: logger.exception("删除商品类型失败") return Response({'code': 500, 'msg': '删除失败,请稍后重试'}) # ---------- 专区操作 ---------- @transaction.atomic def _add_zhuanqu(self, dianpu, request): """添加专区,自动从店铺商品类型获取 leixing_id(公共商品类型ID)""" try: mingzi = request.data.get('mingzi') dianpu_leixing_id = request.data.get('dianpu_leixing_id') paixu = request.data.get('paixu', 0) if not mingzi or not dianpu_leixing_id: return Response({'code': 400, 'msg': '缺少名称或所属商品类型'}) # 获取店铺商品类型,并拿到映射的公共类型ID try: shop_type = ShangpinLeixingDianpu.query.get( id=dianpu_leixing_id, dianpu_id=dianpu.id ) leixing_id = shop_type.gonggong_leixing_id # 这就是专区需要的 leixing_id except ShangpinLeixingDianpu.DoesNotExist: return Response({'code': 400, 'msg': '所选商品类型无效'}) # 验证公共类型有效 if not ShangpinLeixing.query.filter(id=leixing_id, shenhezhuangtai=1).exists(): return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) ShangpinZhuanqu.query.create( dianpu_id=dianpu.id, mingzi=mingzi, dianpu_leixing_id=dianpu_leixing_id, leixing_id=leixing_id, # 自动填充 paixu=paixu, shangjia_zhuangtai=True, fengjin_zhuangtai=False, ) return Response({'code': 0, 'msg': '专区添加成功'}) except Exception as e: logger.exception("添加专区失败") return Response({'code': 500, 'msg': '添加失败,请稍后重试'}) @transaction.atomic def _update_zhuanqu(self, dianpu, request): """修改专区,若更换所属商品类型则同步更新 leixing_id""" zhuanqu_id = request.data.get('zhuanqu_id') if not zhuanqu_id: return Response({'code': 400, 'msg': '缺少 zhuanqu_id'}) try: obj = ShangpinZhuanqu.objects.select_for_update().get( id=zhuanqu_id, dianpu_id=dianpu.id ) except ShangpinZhuanqu.DoesNotExist: return Response({'code': 404, 'msg': '专区不存在'}) except Exception as e: logger.exception("查询专区异常") return Response({'code': 500, 'msg': '服务器错误'}) try: update_fields = [] if 'mingzi' in request.data: obj.mingzi = request.data.get('mingzi') update_fields.append('mingzi') if 'dianpu_leixing_id' in request.data: new_id = request.data.get('dianpu_leixing_id') try: shop_type = ShangpinLeixingDianpu.query.get( id=new_id, dianpu_id=dianpu.id ) obj.dianpu_leixing_id = new_id obj.leixing_id = shop_type.gonggong_leixing_id # 同步更新公共类型ID update_fields.extend(['dianpu_leixing_id', 'leixing_id']) except ShangpinLeixingDianpu.DoesNotExist: return Response({'code': 400, 'msg': '所选商品类型无效'}) if 'paixu' in request.data: obj.paixu = request.data.get('paixu') update_fields.append('paixu') if 'shangjia_zhuangtai' in request.data: val = request.data.get('shangjia_zhuangtai') if isinstance(val, str): val = val.lower() in ('true', '1', 'yes') obj.shangjia_zhuangtai = bool(val) update_fields.append('shangjia_zhuangtai') if update_fields: obj.save(update_fields=update_fields) return Response({'code': 0, 'msg': '专区更新成功'}) except Exception as e: logger.exception("修改专区失败") return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) @transaction.atomic def _delete_zhuanqu(self, dianpu, request): """删除专区,可选是否同时删除关联商品""" zhuanqu_id = request.data.get('id') if not zhuanqu_id: return Response({'code': 400, 'msg': '缺少专区ID'}) try: obj = ShangpinZhuanqu.objects.select_for_update().get( id=zhuanqu_id, dianpu_id=dianpu.id ) except ShangpinZhuanqu.DoesNotExist: return Response({'code': 404, 'msg': '专区不存在'}) except Exception as e: logger.exception("查询专区异常") return Response({'code': 500, 'msg': '服务器错误'}) try: delete_products = request.data.get('delete_products') delete_products = str(delete_products).lower() in ('true', '1', 'yes') if delete_products: products = Shangpin.query.filter( zhuanqu_id=obj.id, dianpu_id=dianpu.id ) for p in products: if p.tupian_url: delete_from_oss(p.tupian_url) if p.guize_tupian: delete_from_oss(p.guize_tupian) p.delete() else: Shangpin.query.filter( zhuanqu_id=obj.id, dianpu_id=dianpu.id ).update(zhuanqu_id=None) obj.delete() return Response({'code': 0, 'msg': '专区删除成功'}) except Exception as e: logger.exception("删除专区失败") return Response({'code': 500, 'msg': '删除失败,请稍后重试'}) class ShopProductListView(APIView): """ 商品列表接口,支持筛选、分页。 URL: /shangdian/sdhqspsj 权限: JWT认证 + 店铺验证 """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 身份验证,获取店铺对象 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 # 2. 安全解析分页与筛选参数 try: page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 20)) except (TypeError, ValueError): return Response({'code': 400, 'msg': '分页参数格式错误'}) keyword = str(request.data.get('keyword', '')).strip() shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型ID zhuanqu_id = request.data.get('zhuanqu_id') # 专区ID # 布尔型筛选字段 def parse_bool(val): if val is None or val == '': return None if isinstance(val, bool): return val if isinstance(val, str): return val.lower() in ('true', '1', 'yes') return bool(val) shenhe = parse_bool(request.data.get('shenhe_zhuangtai')) shangjia = parse_bool(request.data.get('shangjia_zhuangtai')) fengjin = parse_bool(request.data.get('fengjin_zhuangtai')) # 3. 构建查询(只查当前店铺的商品) products = Shangpin.query.filter(dianpu_id=dianpu.id).select_related( 'dianpu_leixing' ).only( 'id', 'biaoqian', 'jiage', 'kucun', 'leixing_id', 'zhuanqu_id', 'zhenshi_xiaoliang', 'duiwai_xiaoliang', 'jieshao', 'xiadan_xuzhi', 'guize_tupian', 'yaoqiuleixing', 'huiyuan_id', 'yongjin', 'tupian_url', 'ewai_dashou_fencheng', 'paixu', 'dianpu_leixing_id', 'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai', 'CreateTime', 'UpdateTime' ) # 筛选 if shop_product_type_id is not None: try: products = products.filter(dianpu_leixing_id=int(shop_product_type_id)) except (ValueError, TypeError): return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'}) if zhuanqu_id is not None: try: products = products.filter(zhuanqu_id=int(zhuanqu_id)) except (ValueError, TypeError): return Response({'code': 400, 'msg': 'zhuanqu_id 格式错误'}) if shenhe is not None: products = products.filter(shenhe_zhuangtai=shenhe) if shangjia is not None: products = products.filter(shangjia_zhuangtai=shangjia) if fengjin is not None: products = products.filter(fengjin_zhuangtai=fengjin) # 关键词搜索(商品ID、标题、介绍) if keyword: products = products.filter( models.Q(id__icontains=keyword) | models.Q(biaoqian__icontains=keyword) | models.Q(jieshao__icontains=keyword) ) # 4. 分页 total_count = products.count() paginator = Paginator(products, page_size) page_obj = paginator.get_page(page) # 5. 构造返回数据 product_list = [] for p in page_obj: product_list.append({ 'id': p.id, 'biaoqian': p.biaoqian or '', 'jiage': p.jiage, 'kucun': p.kucun, 'leixing_id': p.leixing_id, 'zhuanqu_id': p.zhuanqu_id, 'zhenshi_xiaoliang': p.zhenshi_xiaoliang, 'duiwai_xiaoliang': p.duiwai_xiaoliang, 'jieshao': p.jieshao or '', 'xiadan_xuzhi': p.xiadan_xuzhi or '', 'guize_tupian': p.guize_tupian or '', 'yaoqiuleixing': p.yaoqiuleixing, 'huiyuan_id': p.huiyuan_id or '', 'yongjin': p.yongjin, 'tupian_url': p.tupian_url or '', 'ewai_dashou_fencheng': p.ewai_dashou_fencheng, 'paixu': p.paixu, 'dianpu_leixing_id': p.dianpu_leixing_id, 'shangjia_zhuangtai': p.shangjia_zhuangtai, 'fengjin_zhuangtai': p.fengjin_zhuangtai, 'shenhe_zhuangtai': p.shenhe_zhuangtai, 'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '', 'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '', }) return Response({ 'code': 0, 'data': { 'list': product_list, 'total': total_count, } }) class ShopProductModifyView1(APIView): """ 商品统一操作:修改、添加、删除 URL: /shangdian/xgdpsplx1 """ 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 action = request.data.get('action') if not action: return Response({'code': 400, 'msg': '缺少操作类型 action'}) if action == 'update_product': return self._update_product(dianpu, request.data, request.FILES) elif action == 'add_product': return self._add_product(dianpu, request.data, request.FILES) elif action == 'delete_product': return self._delete_product(dianpu, request.data) else: return Response({'code': 400, 'msg': '不支持的操作类型'}) # ---------- 修改商品 ---------- @transaction.atomic def _update_product(self, dianpu, data, files): product_id = data.get('product_id') if not product_id: return Response({'code': 400, 'msg': '缺少 product_id'}) try: product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id) except Shangpin.DoesNotExist: return Response({'code': 404, 'msg': '商品不存在'}) update_fields = [] content_changed = False # 辅助函数:安全转换为整数,失败则返回原值(让其报错) def to_int(val): try: return int(val) except (TypeError, ValueError): return val def to_float(val): try: return float(val) except (TypeError, ValueError): return val # 标题 if 'biaoqian' in data: product.biaoqian = data['biaoqian'] update_fields.append('biaoqian') content_changed = True # 价格 if 'jiage' in data: product.jiage = to_float(data['jiage']) update_fields.append('jiage') content_changed = True # 库存 if 'kucun' in data: product.kucun = to_int(data['kucun']) update_fields.append('kucun') content_changed = True # 店铺商品类型 if 'dianpu_leixing_id' in data: new_type_id = to_int(data['dianpu_leixing_id']) if not ShangpinLeixingDianpu.query.filter(id=new_type_id, dianpu_id=dianpu.id).exists(): return Response({'code': 400, 'msg': '无效的店铺商品类型'}) product.dianpu_leixing_id = new_type_id update_fields.append('dianpu_leixing_id') content_changed = True # 专区 if 'zhuanqu_id' in data: new_zq_id = data['zhuanqu_id'] if new_zq_id is not None and new_zq_id != '': new_zq_id = to_int(new_zq_id) if not ShangpinZhuanqu.query.filter(id=new_zq_id, dianpu_id=dianpu.id).exists(): return Response({'code': 400, 'msg': '无效的专区'}) product.zhuanqu_id = new_zq_id else: product.zhuanqu_id = None update_fields.append('zhuanqu_id') content_changed = True # 介绍 if 'jieshao' in data: product.jieshao = data['jieshao'] update_fields.append('jieshao') content_changed = True # 下单须知 if 'xiadan_xuzhi' in data: product.xiadan_xuzhi = data['xiadan_xuzhi'] update_fields.append('xiadan_xuzhi') # ★ 抢单要求类型及会员/佣金自动处理 if 'yaoqiuleixing' in data: yqlx = to_int(data['yaoqiuleixing']) if yqlx not in [1, 2]: return Response({'code': 400, 'msg': '抢单要求类型无效'}) product.yaoqiuleixing = yqlx update_fields.append('yaoqiuleixing') content_changed = True if yqlx == 1: # 会员抢单:优先使用前端显式传递的非空会员ID if 'huiyuan_id' in data and data.get('huiyuan_id'): product.huiyuan_id = data['huiyuan_id'] update_fields.append('huiyuan_id') product.yongjin = None if 'yongjin' not in update_fields: update_fields.append('yongjin') else: # 自动从公共类型获取会员ID,或转为佣金 try: shop_type = ShangpinLeixingDianpu.query.get( id=product.dianpu_leixing_id, dianpu_id=dianpu.id ) public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id) auto_huiyuan_id = public_type.huiyuan_id if auto_huiyuan_id: product.huiyuan_id = auto_huiyuan_id update_fields.append('huiyuan_id') product.yongjin = None if 'yongjin' not in update_fields: update_fields.append('yongjin') elif public_type.yaoqiuleixing == 2 and public_type.yongjin: # 无会员,强制转为佣金 product.yaoqiuleixing = 2 product.yongjin = public_type.yongjin product.huiyuan_id = None # yaoqiuleixing 已在 update_fields 中,需要添加 huiyuan_id 和 yongjin update_fields.append('huiyuan_id') update_fields.append('yongjin') else: return Response({'code': 400, 'msg': '会员抢单缺少会员ID,公共类型也无会员ID或佣金'}) except (ShangpinLeixingDianpu.DoesNotExist, ShangpinLeixing.DoesNotExist): return Response({'code': 400, 'msg': '店铺商品类型无效'}) elif yqlx == 2: # 佣金抢单,清除会员ID,设置佣金 product.huiyuan_id = None update_fields.append('huiyuan_id') if 'yongjin' in data: product.yongjin = to_float(data['yongjin']) or 0 else: product.yongjin = 0 update_fields.append('yongjin') else: # 未修改抢单类型,保留原有独立修改逻辑 if 'huiyuan_id' in data: product.huiyuan_id = data['huiyuan_id'] or None update_fields.append('huiyuan_id') content_changed = True if 'yongjin' in data: product.yongjin = to_float(data['yongjin']) or 0 update_fields.append('yongjin') content_changed = True # 打手额外分成金额 if 'ewai_dashou_fencheng' in data: fencheng = to_float(data['ewai_dashou_fencheng']) or 0 if fencheng > (product.jiage or 0): return Response({'code': 400, 'msg': '分成金额不能超过商品价格'}) product.ewai_dashou_fencheng = fencheng update_fields.append('ewai_dashou_fencheng') content_changed = True # 排序 if 'paixu' in data: product.paixu = to_int(data['paixu']) update_fields.append('paixu') # 上架状态(保持原逻辑,兼容字符串) if 'shangjia_zhuangtai' in data: val = data['shangjia_zhuangtai'] if isinstance(val, str): val = val.lower() in ('true', '1', 'yes') product.shangjia_zhuangtai = bool(val) update_fields.append('shangjia_zhuangtai') # ★ 商品图片真上传 image_file = files.get('image') if image_file: if product.tupian_url: delete_from_oss(product.tupian_url) file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png" full_url = upload_to_oss(image_file, file_path) if not full_url: return Response({'code': 500, 'msg': '商品图片上传失败'}) product.tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) update_fields.append('tupian_url') content_changed = True # ★ 规则图片真上传 rule_file = files.get('rule_image') remove_rule = data.get('remove_rule_image', '0') if rule_file: if product.guize_tupian: delete_from_oss(product.guize_tupian) file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png" full_url = upload_to_oss(rule_file, file_path) if full_url: product.guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) update_fields.append('guize_tupian') content_changed = True elif remove_rule == '1' and product.guize_tupian: delete_from_oss(product.guize_tupian) product.guize_tupian = None update_fields.append('guize_tupian') content_changed = True if content_changed: audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() if audit_config and audit_config.kaiqi_shenhe: product.shenhe_zhuangtai = False update_fields.append('shenhe_zhuangtai') if update_fields: product.save(update_fields=update_fields + ['UpdateTime']) return Response({'code': 0, 'msg': '商品修改成功'}) # ---------- 添加商品(增强会员/佣金默认逻辑) ---------- @transaction.atomic def _add_product(self, dianpu, data, files): biaoqian = data.get('biaoqian') jiage = data.get('jiage') kucun = data.get('kucun') dianpu_leixing_id = data.get('dianpu_leixing_id') zhuanqu_id = data.get('zhuanqu_id') jieshao = data.get('jieshao') if not all([biaoqian, jiage is not None, kucun is not None, dianpu_leixing_id, zhuanqu_id, jieshao]): return Response({'code': 400, 'msg': '必填项缺失'}) try: shop_type = ShangpinLeixingDianpu.query.get(id=dianpu_leixing_id, dianpu_id=dianpu.id) except ShangpinLeixingDianpu.DoesNotExist: return Response({'code': 400, 'msg': '无效的店铺商品类型'}) try: public_type = ShangpinLeixing.query.get(id=shop_type.gonggong_leixing_id, shenhezhuangtai=1) except ShangpinLeixing.DoesNotExist: return Response({'code': 400, 'msg': '关联的公共商品类型无效'}) # 商品图片上传 image_file = files.get('image') if not image_file: return Response({'code': 400, 'msg': '必须上传商品图片'}) file_path = f"dianpu/shangpin/shangpin/{dianpu.id}_{int(time.time())}.png" full_url = upload_to_oss(image_file, file_path) if not full_url: return Response({'code': 500, 'msg': '商品图片上传失败'}) tupian_url = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) # 规则图片 guize_tupian = None rule_file = files.get('rule_image') if rule_file: file_path = f"dianpu/shangpin/shangpinguize/{dianpu.id}_{int(time.time())}.png" full_url = upload_to_oss(rule_file, file_path) if full_url: guize_tupian = full_url.replace(settings.COS_DOMAIN.rstrip('/') + '/', '', 1) yaoqiuleixing = int(data.get('yaoqiuleixing', 1)) if yaoqiuleixing == 1: huiyuan_id = data.get('huiyuan_id') # 如果前端没传有效的会员ID,尝试从公共类型获取 if not huiyuan_id: huiyuan_id = public_type.huiyuan_id # 仍然没有会员ID,检查公共类型是否可转为佣金 if not huiyuan_id: if public_type.yaoqiuleixing == 2 and public_type.yongjin: # 自动转为佣金抢单 yaoqiuleixing = 2 yongjin = public_type.yongjin huiyuan_id = None else: return Response({'code': 400, 'msg': '会员抢单缺少会员ID,且无法自动获取'}) else: yongjin = None else: huiyuan_id = None yongjin = data.get('yongjin') or public_type.yongjin or 0 ewai_fencheng = data.get('ewai_dashou_fencheng') or 0 try: ewai_val = float(ewai_fencheng) jiage_val = float(jiage) if ewai_val > jiage_val: return Response({'code': 400, 'msg': '分成金额不能超过商品价格'}) except ValueError: # 处理转换失败的情况,比如记录日志或设为默认值 print("无法转换为数字") '''if ewai_fencheng > jiage: return Response({'code': 400, 'msg': '分成金额不能超过商品价格'})''' shenhe_status = True audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first() if audit_config and audit_config.kaiqi_shenhe: shenhe_status = False product = Shangpin.query.create( biaoqian=biaoqian, jiage=jiage, kucun=kucun, dianpu_id=dianpu.id, dianpu_leixing_id=dianpu_leixing_id, zhuanqu_id=zhuanqu_id, leixing_id=public_type.id, jieshao=jieshao, xiadan_xuzhi=data.get('xiadan_xuzhi', ''), guize_tupian=guize_tupian, tupian_url=tupian_url, yaoqiuleixing=yaoqiuleixing, huiyuan_id=huiyuan_id, yongjin=yongjin, ewai_dashou_fencheng=ewai_fencheng, kaioi_ewai_dashou_fencheng=True, paixu=data.get('paixu', 0), shangjia_zhuangtai=data.get('shangjia_zhuangtai', True), fengjin_zhuangtai=False, shenhe_zhuangtai=shenhe_status, shenhezhuangtai=1, ) return Response({'code': 0, 'msg': '商品添加成功', 'product_id': product.id}) # ---------- 删除商品(完全不变) ---------- @transaction.atomic def _delete_product(self, dianpu, data): product_id = data.get('product_id') if not product_id: return Response({'code': 400, 'msg': '缺少 product_id'}) try: product = Shangpin.objects.select_for_update().get(id=product_id, dianpu_id=dianpu.id) except Shangpin.DoesNotExist: return Response({'code': 404, 'msg': '商品不存在'}) if product.tupian_url: delete_from_oss(product.tupian_url) if product.guize_tupian: delete_from_oss(product.guize_tupian) product.delete() return Response({'code': 0, 'msg': '商品已删除'}) class MemberListView(APIView): """ 获取所有会员列表(供商品发布时选择) URL: /shangdian/hqhy 权限:JWT认证 + 店铺归属验证 说明:前端需传递 userId,接口会进行身份及店铺状态校验 """ permission_classes = [IsAuthenticated] def post(self, request): # ---------- 1. 获取前端传递的用户ID(必须) ---------- frontend_user_id = request.data.get('userId') if not frontend_user_id: return Response({'code': 400, 'msg': '缺少必要参数 userId'}) # ---------- 2. 调用公共验证方法(会校验用户身份、店铺状态) ---------- dianpu, error_response = verify_shop_permission(request, frontend_user_id) if error_response: return error_response # ---------- 3. 查询所有会员(只返回前端需要的字段) ---------- try: members = Huiyuan.query.all().values('huiyuan_id', 'jieshao') member_list = [ { 'huiyuan_id': m['huiyuan_id'], 'jieshao': m['jieshao'] or '' } for m in members ] except Exception as e: logger.exception("获取会员列表失败") return Response({'code': 500, 'msg': '服务器错误'}) # ---------- 4. 返回成功响应 ---------- return Response({ 'code': 0, 'msg': 'success', 'data': member_list }) class SubShopListView(APIView): """ 获取下级店铺列表,同时返回上级分红汇总 URL: /shangdian/wdxjdp 方法: POST 权限: JWT认证 + 上级店铺身份验证 """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 身份验证 - 获取上级店铺对象 user_id = request.data.get('userId') if not user_id: return Response({'code': 400, 'msg': '缺少 userId'}) parent_dianpu, error = verify_shop_permission(request, user_id) if error: return error # 此时 parent_dianpu 即为当前上级店铺 # 2. 读取筛选参数 dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() zhuangtai = request.data.get('zhuangtai') bind_min = request.data.get('bind_min') bind_max = request.data.get('bind_max') page = request.data.get('page', 1) page_size = request.data.get('page_size', 10) # 3. 查询下级店铺:yaoqing_dianpu_id 等于当前上级店铺ID queryset = Dianpu.query.filter(yaoqing_dianpu_id=parent_dianpu.id) # 4. 筛选条件 if dianpu_mingcheng: queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng) if zhuangtai is not None: try: zhuangtai = int(zhuangtai) queryset = queryset.filter(zhuangtai=zhuangtai) except (ValueError, TypeError): pass if bind_min is not None: try: bind_min = int(bind_min) queryset = queryset.filter(bangding_yonghushu__gte=bind_min) except (ValueError, TypeError): pass if bind_max is not None: try: bind_max = int(bind_max) queryset = queryset.filter(bangding_yonghushu__lte=bind_max) except (ValueError, TypeError): pass # 5. 排序:按创建时间倒序 queryset = queryset.order_by('-CreateTime') # 6. 分页 try: paginator = Paginator(queryset, page_size) page_obj = paginator.page(page) except EmptyPage: page_obj = paginator.page(1) # 7. 构建列表数据 shop_list = [] for shop in page_obj: shop_list.append({ 'id': shop.id, 'dianpu_mingcheng': shop.dianpu_mingcheng, 'dianpu_touxiang': shop.dianpu_touxiang or '', 'lianxi_dianhua': shop.lianxi_dianhua or '', 'zhuangtai': shop.zhuangtai, 'bangding_yonghushu': shop.bangding_yonghushu, 'zhengqu_zonge': str(shop.zhengqu_zonge), 'ketixian_yue': str(shop.ketixian_yue), 'CreateTime': shop.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if shop.CreateTime else '', 'UpdateTime': shop.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if shop.UpdateTime else '', 'erweima_url': shop.erweima_url or '', 'fenhong_choucheng_feilv': str(shop.fenhong_choucheng_feilv) if shop.fenhong_choucheng_feilv else '0.0000', }) # 8. 上级分红汇总(从当前店铺主表读取) shangji_leiji = str(parent_dianpu.shangji_fenhong_leiji) if parent_dianpu.shangji_fenhong_leiji else '0.00' shangji_ketixian = str(parent_dianpu.shangji_ketixian_fenhong) if parent_dianpu.shangji_ketixian_fenhong else '0.00' return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'list': shop_list, 'total': paginator.count, 'page': page_obj.number, 'page_size': page_size, 'shangji_fenhong_leiji': shangji_leiji, 'shangji_ketixian_fenhong': shangji_ketixian, } }) class SubShopModifyView(APIView): """ 添加/修改下级店铺 URL: /shangdian/xjdpbj 方法: POST 权限: JWT认证 + 上级店铺身份验证 action字段: - 'add' : 添加新的子店铺 - 'update': 修改已有子店铺的状态/分红费率 """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 身份验证 user_id = request.data.get('userId') if not user_id: return Response({'code': 400, 'msg': '缺少 userId'}) parent_dianpu, error = verify_shop_permission(request, user_id) if error: return error action = request.data.get('action', '').strip().lower() if action == 'add': return self._add_subshop(request, parent_dianpu) elif action == 'update': return self._update_subshop(request, parent_dianpu) else: return Response({'code': 400, 'msg': '未知操作类型,action 应为 add 或 update'}) def _add_subshop(self, request, parent_dianpu): """添加子店铺,需提供用户UID、账号、密码、店铺名称等""" user_uid = request.data.get('user_uid', '').strip() zhanghao = request.data.get('zhanghao', '').strip() mima = request.data.get('mima', '') dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip() lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip() or None fenhong_feilv = request.data.get('fenhong_choucheng_feilv', None) # 必填校验 if not all([user_uid, zhanghao, mima, dianpu_mingcheng]): return Response({'code': 400, 'msg': '必填字段缺失(user_uid, zhanghao, mima, dianpu_mingcheng)'}) # 检查用户是否存在 if not User.query.filter(UserUID=user_uid).exists(): return Response({'code': 404, 'msg': '对应的小程序用户不存在'}) # 检查账号是否已占用 if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists(): return Response({'code': 400, 'msg': '登录账号已存在'}) if YonghuPingzheng.query.filter(yonghu=user_uid).exists(): return Response({'code': 400, 'msg': '该用户 ID 已有店铺凭证'}) # 费率处理:前端可选,后端默认 0.0500 if fenhong_feilv is not None: try: fenhong_feilv = float(fenhong_feilv) if fenhong_feilv < 0 or fenhong_feilv > 1: return Response({'code': 400, 'msg': '分红费率需在 0~1 之间'}) except (ValueError, TypeError): return Response({'code': 400, 'msg': '分红费率格式错误'}) else: fenhong_feilv = 0.0500 try: with transaction.atomic(): # 创建凭证 pingzheng = YonghuPingzheng.query.create( yonghu=user_uid, zhanghao=zhanghao, mima=mima, is_active=True ) # 创建店铺,关联上级 dianpu = Dianpu.query.create( pingzheng=pingzheng, dianpu_mingcheng=dianpu_mingcheng, lianxi_dianhua=lianxi_dianhua or '', yaoqing_dianpu_id=parent_dianpu.id, # 关键:绑定上级 fenhong_choucheng_feilv=fenhong_feilv, # 其他字段使用默认值 ) return Response({ 'code': 200, 'msg': '子店铺添加成功', 'dianpu_id': dianpu.id }) except IntegrityError as e: logger.exception("创建子店铺违反唯一约束") return Response({'code': 400, 'msg': '数据冲突,请检查账号或用户ID'}) except Exception as e: logger.exception("创建子店铺失败") return Response({'code': 500, 'msg': '创建失败,请稍后重试'}) def _update_subshop(self, request, parent_dianpu): """修改子店铺状态和上级分红费率""" dianpu_id = request.data.get('dianpu_id') if not dianpu_id: return Response({'code': 400, 'msg': '缺少 dianpu_id'}) try: dianpu_id = int(dianpu_id) except (ValueError, TypeError): return Response({'code': 400, 'msg': 'dianpu_id 需为数字'}) try: with transaction.atomic(): sub_dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id) # 权限校验:确保该店铺的上级就是当前店铺 if sub_dianpu.yaoqing_dianpu_id != parent_dianpu.id: return Response({'code': 403, 'msg': '无权修改该店铺,非您的下级'}) # 更新状态(如果有传) if 'zhuangtai' in request.data: zhuangtai = request.data['zhuangtai'] if zhuangtai not in (0, 1, '0', '1'): return Response({'code': 400, 'msg': '店铺状态只能为 0(封禁) 或 1(正常)'}) sub_dianpu.zhuangtai = int(zhuangtai) # 更新上级分红费率(如果有传) if 'fenhong_choucheng_feilv' in request.data: feilv = request.data['fenhong_choucheng_feilv'] try: feilv = float(feilv) if feilv < 0 or feilv > 1: return Response({'code': 400, 'msg': '分红费率需在 0~1 之间'}) sub_dianpu.fenhong_choucheng_feilv = feilv except (ValueError, TypeError): return Response({'code': 400, 'msg': '分红费率格式错误'}) sub_dianpu.save(update_fields=['zhuangtai', 'fenhong_choucheng_feilv', 'UpdateTime']) return Response({'code': 200, 'msg': '修改成功'}) except Dianpu.DoesNotExist: return Response({'code': 404, 'msg': '店铺不存在'}) except Exception as e: logger.exception("修改下级店铺失败") return Response({'code': 500, 'msg': '修改失败,请稍后重试'}) # shangdian/views/order_manage_views.py """ 店铺订单管理接口 包含: 1. 统计数据:商品类型及订单总数、各状态订单数 2. 订单列表:支持筛选、分页、返回订单详情及打手图片(含跨平台图片拉取) """ # ---------- 图片域名 ---------- """ 店铺订单管理接口 ================= 功能1:获取统计数据(action='statistics') - 每个商品类型的订单总数(order_count) - 每个商品类型下各状态的订单数量(status_counts) - 全局各状态的订单总数(status_counts) - 店铺订单总量(total_orders) 功能2:获取订单列表(默认,不带action参数时执行) - 支持商品类型、订单状态、订单ID、打手ID、游戏昵称、老板ID、订单介绍筛选 - 支持分页、自定义每页大小 - 返回打手提交图片(绝对URL)、老板信息、店铺收益等 接口地址:POST /shangdian/ddlbhq 权限:JWT认证 + verify_shop_permission 验证店铺身份 """ class ShopOrderStatisticsView(APIView): """ 店铺订单统计接口 URL: POST /shangdian/ddltj 权限: JWT认证 + 店铺身份验证 请求参数: userId (必填): 店铺所属用户的ID 返回结构: { "code": 200, "msg": "统计成功", "data": { "types": [ # 所有有效商品类型 { "id": 类型ID, "jieshao": "类型介绍", "tupian_url": "类型图片相对路径", "order_count": 该类型订单总数, "status_counts": { # 该类型下各状态订单数量 "1": 数量, "7": 数量, "2": 数量, "8": 数量, "3": 数量, "4": 数量, "5": 数量, "6": 数量 } }, ... ], "status_counts": { # 全局各状态订单总数 "1": 总数, "7": 总数, "2": 总数, "8": 总数, "3": 总数, "4": 总数, "5": 总数, "6": 总数 }, "total_orders": 订单总量 } } """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 身份验证 user_id = request.data.get('userId', '').strip() if not user_id: return Response({'code': 400, 'msg': '缺少 userId'}) dianpu, error = verify_shop_permission(request, user_id) if error: return error shop_id = dianpu.id try: # 2. 获取所有正常状态(审核通过)的商品类型 types_qs = ShangpinLeixing.query.filter( shenhezhuangtai=1 ).order_by('-paixu', 'id') type_list = [] type_id_map = {} for idx, t in enumerate(types_qs): item = { 'id': t.id, 'jieshao': t.jieshao or '', 'tupian_url': t.tupian_url or '', 'order_count': 0, 'status_counts': { '1': 0, '7': 0, '2': 0, '8': 0, '3': 0, '4': 0, '5': 0, '6': 0 } } type_list.append(item) type_id_map[t.id] = idx # 3. 获取本店铺所有订单的主键ID列表(通过平台扩展表) order_pks = list( PlatformOrderExt.query.filter(dianpu_id=shop_id) .values_list('Order_id', flat=True) # 这里取的是订单主表的主键id ) total_orders = 0 global_counts = defaultdict(int) if order_pks: # 4. 用主键列表进行聚合统计,按商品类型和订单状态分组 aggregation = ( Order.objects .filter(id__in=order_pks) # 修正点:使用id__in,而不是dingdan_id__in .values('ProductTypeID', 'Status') .annotate(cnt=Count('id')) ) for row in aggregation: lid = row['ProductTypeID'] # 商品类型ID st = row['Status'] # 订单状态码 cnt = row['cnt'] # 转为字符串键,方便前端展示 st_key = str(st) # 累加总量 total_orders += cnt global_counts[st_key] += cnt # 填充到对应商品类型 if lid in type_id_map: idx = type_id_map[lid] type_list[idx]['order_count'] += cnt if st_key in type_list[idx]['status_counts']: type_list[idx]['status_counts'][st_key] += cnt # 5. 确保全局状态返回所有8个键,即使值为0 all_status_keys = ['1', '7', '2', '8', '3', '4', '5', '6'] final_global_counts = { k: global_counts.get(k, 0) for k in all_status_keys } # 6. 返回统计结果 return Response({ 'code': 200, 'msg': '统计成功', 'data': { 'types': type_list, 'status_counts': final_global_counts, 'total_orders': total_orders } }) except Exception as e: logger.exception("订单统计接口异常") return Response({ 'code': 500, 'msg': '统计失败,服务器内部错误' }) # ================================================================ # 辅助函数:获取OSS域名(用于拼接本地图片绝对URL) # ================================================================ def get_oss_domain(): """ 获取对象存储的域名,末尾包含 '/'。 返回:字符串,如 'https://example.com/' """ return getattr(settings, 'COS_DOMAIN', 'https://xingque999qygwuyq-1404472910.cos.ap-shanghai.myqcloud.com/').rstrip('/') + '/' # ================================================================ # 辅助函数:获取单个订单的打手提交图片(绝对URL) # ================================================================ def _fetch_images(order, oss_domain): """ 查询指定订单的打手提交图片记录(Dashoutupian 表), 将相对路径拼接为完整的绝对 URL 并返回。 参数: order: Dingdan 订单模型实例 oss_domain: OSS 域名,末尾含 '/' 返回: (images_list, is_punished, punishment_info) 三元组 - images_list: 图片绝对 URL 列表 - is_punished: 暂未使用,默认 False - punishment_info: 暂未使用,默认空字典 """ images = [] is_punished = False punishment_info = {} # 仅当订单有接单打手ID时才查询打手图片 if order.PlayerID: # 从打手图片表中查询该订单、该打手的所有图片记录 image_urls = PlayerDeliveryImage.query.filter( OrderID=order.OrderID, # 订单ID匹配 PlayerID=order.PlayerID # 接单打手ID匹配 ).values_list('ImageURL', flat=True) # 只取图片URL字段 # 遍历所有图片路径 for img_path in image_urls: if img_path: if img_path.startswith('http'): # 已经是完整URL,直接添加 images.append(img_path) else: # 相对路径,拼接 OSS 域名 images.append(oss_domain + img_path.lstrip('/')) # 跨平台图片拉取逻辑(根据实际业务需求补充,此处保留结构) # 如果有跨平台订单,需要请求对方平台接口获取打手图片 return images, is_punished, punishment_info # ================================================================ # 辅助函数:并发获取多个订单的打手图片 # ================================================================ def _batch_images(orders, oss_domain): """ 并发调用 _fetch_images,提升批量获取图片的速度。 参数: orders: Dingdan 订单对象列表 oss_domain: OSS 域名 返回: 字典,键为订单ID (dingdan_id),值为 (images_list, is_punished, punishment_info) """ result = {} # 存储结果 # 使用线程池并发执行,max_workers 可根据服务器性能调整 with ThreadPoolExecutor(max_workers=10) as executor: # 建立 future 到订单ID的映射 future_to_oid = { executor.submit(_fetch_images, order, oss_domain): order.OrderID for order in orders } # 等待所有任务完成 for future in as_completed(future_to_oid): oid = future_to_oid[future] # 对应的订单ID try: # 获取任务返回值 imgs, pun, pinfo = future.result() result[oid] = (imgs, pun, pinfo) except Exception as e: # 某个订单获取失败,记录日志并赋予空值,保证整体流程不中断 logger.error(f"获取订单 {oid} 图片失败: {e}") result[oid] = ([], False, {}) return result # ================================================================ # 核心视图类 # ================================================================ class ShopOrderManageView(APIView): permission_classes = [IsAuthenticated] def post(self, request): """ 统一入口,根据 action 参数分发到统计或列表模块。 """ # 1. 从请求中提取店铺用户ID user_id = request.data.get('userId', '').strip() if not user_id: return Response({'code': 400, 'msg': '缺少 userId'}) # 2. 验证店铺身份,获取店铺对象 dianpu, error_response = verify_shop_permission(request, user_id) if error_response: return error_response # 3. 根据 action 决定调用哪个方法 action = request.data.get('action', '').strip().lower() if action == 'statistics': return self._statistics(dianpu) # 统计 else: return self._order_list(request, dianpu) # 列表 # ----------------------------------------------------------------- # 统计实现 (这就是你要求的100个统计字段的详细实现) # ----------------------------------------------------------------- def _statistics(self, dianpu): """ 统计本店铺的订单数据,输出以下信息: - types: 商品类型数组,每个元素包含: id : 类型ID jieshao : 类型介绍 tupian_url : 类型图片 order_count : 【该类型的订单总数】 status_counts: 该类型下各个状态的订单数量 - status_counts: 全局各状态订单总数(键为状态码字符串) - total_orders : 店铺订单总量 所有字段名均与前端保持一致。 """ # 获取店铺ID shop_id = dianpu.id # ------------------------------------------------------------ # 步骤1:获取所有正常的商品类型(审核状态为1) # ------------------------------------------------------------ types_query = ShangpinLeixing.query.filter( shenhezhuangtai=1 # 审核状态为1(正常) ).order_by('-paixu', 'id') # 按排序权重和ID排序 # 构建返回的类型列表,并预初始化各状态计数 type_list = [] # 最终要返回的类型数组 type_ids = [] # 所有类型的ID列表,用于后续快速索引 # 定义可能出现的所有状态码(1/7已付款,2进行中,8待结算,3已完成,4退款审核,5已退款,6退款失败) status_keys = ['1', '7', '2', '8', '3', '4', '5', '6'] for t in types_query: # 构造每个类型的基础数据 type_item = { 'id': t.id, 'jieshao': t.jieshao or '', 'tupian_url': t.tupian_url or '', 'order_count': 0, # 该类型订单总数,稍后填充 'status_counts': {k: 0 for k in status_keys} # 各状态计数初始为0 } type_list.append(type_item) type_ids.append(t.id) # 记录ID # ------------------------------------------------------------ # 步骤2:获取本店铺的所有订单ID(通过平台扩展表) # ------------------------------------------------------------ # 从 DingdanPingtai 中过滤出属于当前店铺的订单扩展记录,并提取订单主表ID order_ids = list( PlatformOrderExt.query.filter( dianpu_id=shop_id # 店铺ID匹配 ).values_list('Order_id', flat=True) # 只取订单ID字段 ) # 统计变量 total_orders = 0 # 订单总量 global_status_counts = defaultdict(int) # 全局各状态计数,键为状态码字符串 # ------------------------------------------------------------ # 步骤3:如果店铺有订单,进行聚合统计 # ------------------------------------------------------------ if order_ids: # 使用 Django 聚合查询:按 leixing_id(商品类型)和 zhuangtai(订单状态)分组计数 aggregation = Order.query.filter( OrderID__in=order_ids # 从该店铺的订单中查询 ).values( 'ProductTypeID', # 按类型ID分组 'Status' # 按状态分组 ).annotate( cnt=Count('id') # 统计每个分组的记录数 ) # 遍历每个聚合结果,累加到对应的类型和全局计数中 for row in aggregation: # 提取分组字段 lid = row['ProductTypeID'] # 商品类型ID st = row['Status'] # 订单状态码 cnt = row['cnt'] # 该分组下的订单数量 # 累加到全局总数 total_orders += cnt global_status_counts[str(st)] += cnt # 累加到对应商品类型的状态计数中 if lid in type_ids: # 在 type_list 中找到对应的类型条目 for tp in type_list: if tp['id'] == lid: # 更新该类型的订单总数 tp['order_count'] += cnt # 更新该类型下对应状态的计数(键为字符串) tp['status_counts'][str(st)] += cnt break # 找到后跳出循环,因为ID唯一 # 将全局状态计数转换为按固定顺序的字典,确保前端展示顺序稳定 final_global_counts = {k: global_status_counts.get(k, 0) for k in status_keys} # ------------------------------------------------------------ # 步骤4:返回统计结果 # ------------------------------------------------------------ return Response({ 'code': 200, 'msg': '统计成功', 'data': { 'types': type_list, # 商品类型列表(含各类型统计) 'status_counts': final_global_counts, # 全局各状态总数 'total_orders': total_orders # 订单总量 } }) # ----------------------------------------------------------------- # 订单列表实现 # ----------------------------------------------------------------- def _order_list(self, request, dianpu): """处理订单列表请求,支持筛选、分页、并发获取图片。""" shop_id = dianpu.id # 解析筛选参数 leixing_id = request.data.get('leixing_id') zhuangtai = request.data.get('zhuangtai') dingdan_id_key = request.data.get('dingdan_id', '').strip() jiedan_dashou_id_key = request.data.get('jiedan_dashou_id', '').strip() nicheng_key = request.data.get('nicheng', '').strip() laoban_id_key = request.data.get('laoban_id', '').strip() jieshao_key = request.data.get('jieshao', '').strip() page = request.data.get('page', 1) page_size = request.data.get('page_size', 20) # 基础查询:从平台扩展表关联订单主表,并按创建时间倒序 qs = PlatformOrderExt.query.filter( dianpu_id=shop_id ).select_related('Order').order_by('-Order__CreateTime') # 应用筛选条件 if leixing_id is not None: qs = qs.filter(Order__ProductTypeID=leixing_id) if zhuangtai is not None: qs = qs.filter(Order__Status=zhuangtai) if dingdan_id_key: qs = qs.filter(Order__OrderID__icontains=dingdan_id_key) if jiedan_dashou_id_key: qs = qs.filter(Order__PlayerID=jiedan_dashou_id_key) if nicheng_key: qs = qs.filter(Order__Nickname__icontains=nicheng_key) if laoban_id_key: qs = qs.filter(BossID=laoban_id_key) if jieshao_key: qs = qs.filter(Order__Description__icontains=jieshao_key) # 分页 paginator = Paginator(qs, page_size) try: page_obj = paginator.page(page) except EmptyPage: page_obj = paginator.page(1) # 获取当前页的所有订单和扩展对象 orders_with_ext = [(item.Order, item) for item in page_obj] # 提取订单对象列表,用于并发获取图片 order_objs = [order for order, _ in orders_with_ext] oss_domain = get_oss_domain() img_map = _batch_images(order_objs, oss_domain) # 构建返回数据 list_data = [] for order, ext in orders_with_ext: imgs, is_pun, _ = img_map.get(order.OrderID, ([], False, {})) list_data.append({ 'dingdan_id': order.OrderID, 'zhuangtai': order.Status, 'jine': float(order.Amount) if order.Amount else 0.00, 'dashou_fencheng': float(order.PlayerCommission) if order.PlayerCommission else 0.00, 'jiedan_dashou_id': order.PlayerID or '', 'dashou_liuyan': order.dashou_liuyan or '', 'zhiding_id': order.zhiding_id or '', 'shangpin_id': order.ProductID or '', 'tupian': order.ImageURL or '', 'jieshao': order.Description or '', 'beizhu': order.Remark or '', 'nicheng': order.Nickname or '', 'leixing_id': order.ProductTypeID, 'CreateTime': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '', 'UpdateTime': order.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if order.UpdateTime else '', 'pingtai_kuozhan': { 'laoban_id': ext.BossID or '', 'laoban_pingjia': ext.BossReview or '', 'dianpu_shouyi': float(ext.ShopIncome) if ext.ShopIncome else 0.00, }, 'dashou_images': imgs, 'is_punished': is_pun, 'is_fadaned': False, }) return Response({ 'code': 200, 'msg': '列表成功', 'data': { 'list': list_data, 'total': paginator.count, 'page': page_obj.number, 'page_size': page_size } }) # ================================================================ # shangdian/views/refund_view.py # 店铺退款接口 —— 基于跨平台退款接口二改,只处理平台订单,我方派单 # ================================================================ logger = logging.getLogger(__name__) class ShopRefundView(APIView): """ 店铺订单退款接口 URL: POST /shangdian/tk 权限: JWT认证 + 店铺身份验证(只处理平台订单,我方派单) 请求参数: userId (必填) : 店铺所属用户的ID dingdan_id (必填): 订单ID reason (可选) : 退款理由 """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 获取参数 user_id = request.data.get('userId', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() reason = request.data.get('reason', '').strip() # 退款理由 if not user_id or not dingdan_id: return Response({'code': 400, 'msg': '参数不完整'}) # 2. 店铺身份验证 dianpu, error = verify_shop_permission(request, user_id) if error: return error shop_id = dianpu.id # 3. 查询订单,必须属于本店铺(通过扩展表关联) try: # 通过扩展表确认归属,并 select_related 获取订单主表 pingtai_ext = PlatformOrderExt.query.select_related('Order').get( dianpu_id=shop_id, Order__OrderID=dingdan_id ) order = pingtai_ext.Order except PlatformOrderExt.DoesNotExist: logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) # 4. 订单状态校验(只有这些状态可退款) allowed_statuses = [1, 7, 2, 8, 4] if order.Status not in allowed_statuses: logger.warning(f"订单状态不允许退款: {order.Status}") return Response({'code': 400, 'msg': '订单状态不允许退款'}) # 5. 全部视为平台订单(我方派单),调用微信退款 refund_result = self._call_wechat_refund(order) if refund_result['code'] != 0: return Response({'code': 400, 'msg': refund_result['msg']}) # 6. 事务内更新订单、打手、记录、统计 dashou_id = order.PlayerID try: with transaction.atomic(): # 更新订单状态为已退款 order.Status = 5 order.AssignedCS = str(shop_id) # 处理人记录为店铺ID if reason: order.RefundReason = reason order.save() # 更新打手统计(退款量+1,状态置为空闲) if dashou_id: try: dashou_user = User.query.get(UserUID=dashou_id) dashou_profile = dashou_user.DashouProfile dashou_profile.tuikuanliang += 1 dashou_profile.zhuangtai = 1 dashou_profile.save() except (User.DoesNotExist, ObjectDoesNotExist): logger.warning(f"退款:打手 {dashou_id} 不存在,跳过状态更新") # 更新退款记录表 self._update_refund_record(order, dashou_id, str(shop_id), reason) # 打手每日统计(退款动作) try: update_dashou_daily_by_action( yonghuid=dashou_id, amount=order.PlayerCommission or Decimal('0.00'), action=3 # 3 表示退款 ) except Exception as e: logger.error(f"打手每日统计更新失败: {e}") # 更新平台支出记录 try: update_daily_payout(order.Amount) except Exception as e: logger.error(f"更新每日支出失败: {e}") # ========== 新增:商品和店铺每日统计 ========== try: # 商品每日统计(只要有商品ID就执行) if order.ProductID: # 从扩展表获取已计算好的收益值(下单时已存储) pingtai_shouyi = Decimal('0.00') dianpu_shouyi = Decimal('0.00') if hasattr(order, 'pingtai_kuozhan'): pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00') dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_shangpin_daily_stat( shangpin_id=order.ProductID, caozuo_leixing=3, # 3 = 退款 dingdan_jiage=order.Amount, pingtai_shouyi=pingtai_shouyi, dianpu_zongshouyi=dianpu_shouyi ) logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") # 店铺每日统计(仅当存在店铺ID时执行) dianpu_id = None if hasattr(order, 'pingtai_kuozhan'): dianpu_id = order.pingtai_kuozhan.dianpu_id if dianpu_id: dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_dianpu_daily_stat( dianpu_id=dianpu_id, caozuo_leixing=3, # 3 = 退款 dingdan_jiage=order.Amount, dianpu_shouyi=dianpu_shouyi ) logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") except Exception as e: logger.error(f"商品/店铺统计更新失败: {e}") logger.error(traceback.format_exc()) # 统计失败不影响主流程 # ============================================= except Exception as e: logger.exception("退款事务执行失败") return Response({'code': 500, 'msg': '退款处理失败,请稍后重试'}) return Response({'code': 200, 'msg': '退款成功'}) # ========== 微信退款 ========== def _call_wechat_refund(self, order): """调用微信支付退款接口""" appid = getattr(settings, 'WEIXIN_APPID', '') mch_id = getattr(settings, 'WEIXIN_MCHID', '') key = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') cert_path = getattr(settings, 'WEIXIN_CERT_PATH', '') key_path = getattr(settings, 'WEIXIN_KEY_PATH', '') if not all([appid, mch_id, key, cert_path, key_path]): return {'code': 500, 'msg': '微信支付配置不完整'} out_refund_no = self._generate_refund_no(order.OrderID) total_fee = int(float(order.Amount or 0) * 100) refund_fee = total_fee params = { 'appid': appid, 'mch_id': mch_id, 'nonce_str': self._generate_nonce_str(), 'out_refund_no': out_refund_no, 'total_fee': total_fee, 'refund_fee': refund_fee, 'refund_desc': '店铺退款', } if order.WechatTransactionID: params['transaction_id'] = order.WechatTransactionID else: params['out_trade_no'] = order.OrderID # 签名 sorted_keys = sorted(params.keys()) stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) stringSignTemp = f"{stringA}&key={key}" sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() params['sign'] = sign xml_data = self._dict_to_xml(params) url = 'https://api.mch.weixin.qq.com/secapi/pay/refund' try: response = requests.post(url, data=xml_data, cert=(cert_path, key_path), timeout=10) result = xmltodict.parse(response.content)['xml'] logger.info(f"微信退款响应: {result}") except Exception as e: logger.error(f"微信退款请求异常: {e}", exc_info=True) return {'code': 500, 'msg': '微信退款请求异常'} if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': return {'code': 0, 'msg': '退款成功'} else: err_msg = result.get('err_code_des', result.get('return_msg', '未知错误')) return {'code': 400, 'msg': err_msg} # ========== 退款记录 ========== def _update_refund_record(self, order, dashou_id, chuli_id, reason): """更新退款记录表""" try: refund_record, created = RefundRecord.query.get_or_create( OrderID=order.OrderID, defaults={ 'PlayerID': dashou_id or '', 'ApplicantID': '', 'ProcessorID': chuli_id, 'Reason': reason or '店铺退款', 'ApplyStatus': 1, 'Amount': order.Amount or 0, 'PlayerCommission': order.PlayerCommission or 0, 'Description': order.Description or '', 'Remark': '店铺退款', 'Nickname': order.Nickname or '', } ) if not created: refund_record.ApplyStatus = 1 refund_record.ProcessorID = chuli_id if reason: refund_record.Reason = reason refund_record.save() except Exception as e: logger.error(f"更新退款记录异常: {e}") # ========== 工具方法 ========== def _generate_nonce_str(self): return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) def _generate_refund_no(self, dingdan_id): timestamp = str(int(time.time())) rand = ''.join(random.choices('0123456789', k=4)) return f"{dingdan_id}REF{timestamp}{rand}" def _dict_to_xml(self, data): xml = [''] for k, v in data.items(): xml.append(f'<{k}>{v}') xml.append('') return ''.join(xml) class ShopForceCompleteView(APIView): """ 店铺强制结单接口 URL: POST /shangdian/qzjd 权限: JWT认证 + 店铺身份验证 请求参数: userId (必填) : 店铺所属用户的ID dingdan_id(必填) : 订单ID 说明: - 只处理平台订单(fadan_pingtai == 1) - 订单必须属于当前店铺 - 订单状态必须为 8(结算中)才能强制结单 - 默认视为我方派单,根据是否有对方订单ID判断是否跨平台 - 本地订单:给打手结算、更新打手余额和统计、更新店铺收益统计 - 跨平台订单:仅通知对方平台,不处理打手 """ permission_classes = [IsAuthenticated] def post(self, request): # ========== 1. 获取参数 ========== user_id = request.data.get('userId', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() if not user_id or not dingdan_id: return Response({'code': 400, 'msg': '参数不完整'}) # ========== 2. 店铺身份验证 ========== dianpu, error = verify_shop_permission(request, user_id) if error: return error shop_id = dianpu.id # ========== 3. 查询订单并验证归属 ========== try: # 通过平台扩展表确认订单属于本店铺 pingtai_ext = PlatformOrderExt.query.select_related('Order').get( dianpu_id=shop_id, Order__OrderID=dingdan_id ) order = pingtai_ext.Order except PlatformOrderExt.DoesNotExist: logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) # ========== 4. 校验订单状态必须为 8(结算中) ========== if order.Status != 8: logger.warning(f"订单 {dingdan_id} 当前状态为 {order.Status},不是结算中") return Response({'code': 400, 'msg': f'订单状态不是结算中,当前状态: {order.Status}'}) dashou_id = order.PlayerID dashou_fencheng = order.PlayerCommission or Decimal('0.00') jine = order.Amount or Decimal('0.00') try: with transaction.atomic(): # 锁定订单行,防止并发 order = Order.objects.select_for_update().get(OrderID=dingdan_id) if not dashou_id: return Response({'code': 400, 'msg': '订单无接单打手,无法结算'}) if dashou_fencheng < 0: return Response({'code': 400, 'msg': '打手分成金额无效'}) # ---------- 更新打手余额和统计 ---------- try: dashou_user = User.query.get(UserUID=dashou_id) dashou_profile = dashou_user.DashouProfile dashou_profile.chengjiaozongliang += 1 dashou_profile.yue += dashou_fencheng dashou_profile.zonge += dashou_fencheng dashou_profile.jinrishouyi += dashou_fencheng dashou_profile.jinyueshouyi += dashou_fencheng dashou_profile.zhuangtai = 1 # 空闲 dashou_profile.save() except (User.DoesNotExist, ObjectDoesNotExist): logger.warning(f"本地订单结算:打手 {dashou_id} 不存在,跳过打手更新") # ---------- 更新订单状态 ---------- order.Status = 3 order.AssignedCS = str(shop_id) order.save() # ---------- 打手每日统计(成交) ---------- try: update_dashou_daily_by_action( yonghuid=dashou_id, amount=dashou_fencheng, action=2 # 2 表示成交(结算) ) except Exception as e: logger.error(f"打手每日统计更新失败: {e}") # ---------- 店铺收益统计(结算) ---------- if pingtai_ext.ShopIncome: try: # 商品每日统计(只要有商品ID就执行) if order.ProductID: # 从扩展表获取已计算好的收益值(下单时已存储) pingtai_shouyi = Decimal('0.00') dianpu_shouyi = Decimal('0.00') if hasattr(order, 'pingtai_kuozhan'): pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00') dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_shangpin_daily_stat( shangpin_id=order.ProductID, caozuo_leixing=2, # 3 = 退款 dingdan_jiage=order.Amount, pingtai_shouyi=pingtai_shouyi, dianpu_zongshouyi=dianpu_shouyi ) logger.info(f"商品统计更新完成,商品ID: {order.shangpin_id}") update_dianpu_daily_stat( dianpu_id=shop_id, caozuo_leixing=2, # 结算操作 dingdan_jiage=jine, dianpu_shouyi=pingtai_ext.ShopIncome, ) except Exception as e: logger.error(f"店铺每日统计更新失败: {e}") # ========== 5. 记录操作日志 ========== logger.info(f"强制结单成功,订单 {dingdan_id},店铺 {shop_id}") return Response({'code': 200, 'msg': '强制结单成功'}) except Order.DoesNotExist: logger.warning(f"订单不存在: {dingdan_id}") return Response({'code': 404, 'msg': '订单不存在'}) except Exception as e: logger.error(f"强制结单接口异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误'}) # ================================================================ # shangdian/views/transfer_hall_view.py # 店铺转移大厅接口 —— 严格二改,不添加任何原接口没有的逻辑 # ================================================================ # ================================================================ # shangdian/views/transfer_hall_view.py # 店铺转移大厅接口 —— 添加本地打手退款统计 # ================================================================ class ShopTransferHallView(APIView): """ 店铺转移大厅接口 URL: POST /shangdian/zydt 权限: JWT认证 + 店铺身份验证 请求参数: userId (必填) : 店铺所属用户的ID dingdan_id(必填) : 订单ID 说明: - 只处理平台订单,我方派单 - 允许转移的状态:2,8,4 - 决定是否处理本地打手:有对方订单ID或俱乐部ID(跨平台)则不处理本地打手,只通知对方; 没有对方信息则处理本地打手(释放+退款统计) - 转移后清空对方平台信息,订单重回可接单状态 """ permission_classes = [IsAuthenticated] def post(self, request): # ---------- 1. 获取参数 ---------- user_id = request.data.get('userId', '').strip() dingdan_id = request.data.get('dingdan_id', '').strip() if not user_id or not dingdan_id: return Response({'code': 400, 'msg': '参数不完整'}) # ---------- 2. 店铺身份验证 ---------- dianpu, error = verify_shop_permission(request, user_id) if error: return error shop_id = dianpu.id # ---------- 3. 验证订单归属 ---------- try: pingtai_ext = PlatformOrderExt.query.select_related('Order').get( dianpu_id=shop_id, Order__OrderID=dingdan_id ) order = pingtai_ext.Order except PlatformOrderExt.DoesNotExist: logger.warning(f"订单 {dingdan_id} 不属于店铺 {shop_id} 或不存在") return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) # ---------- 4. 状态校验 ---------- if order.Status not in [2, 8, 4]: return Response({'code': 400, 'msg': f'订单状态不允许转移大厅,当前状态: {order.Status}'}) # ---------- 5. 提取关键字段 ---------- current_dashou_id = order.PlayerID zhiding_id = order.zhiding_id dashou_fencheng = order.PlayerCommission or 0 # 用于退款统计 try: with transaction.atomic(): # 锁定订单行 order = Order.objects.select_for_update().get(OrderID=dingdan_id) # ---------- 6. 记录打手历史(原接口保留逻辑) ---------- if current_dashou_id: last_history = OrderPlayerHistory.query.filter( dingdan_id=dingdan_id ).order_by('-times').first() next_times = (last_history.times + 1) if last_history else 1 OrderPlayerHistory.query.create( dingdan_id=dingdan_id, dashou_id=current_dashou_id, times=next_times, operator=str(shop_id) ) # ---------- 7. 释放本地打手 + 退款统计 ---------- if current_dashou_id: try: dashou_profile = UserDashou.query.get(user__UserUID=current_dashou_id) dashou_profile.zhuangtai = 1 # 设为空闲 dashou_profile.save() except ObjectDoesNotExist: logger.warning(f"打手 {current_dashou_id} 不存在,跳过状态更新") else: # 打手释放成功后,记录每日退款统计(转移视为退款) try: update_dashou_daily_by_action( yonghuid=current_dashou_id, amount=dashou_fencheng, action=3 # 3 表示退款 ) logger.info(f"打手 {current_dashou_id} 转移大厅退款统计成功") except Exception as e: logger.error(f"打手每日统计失败: {e}") # ---------- 8. 重置订单 ---------- order.jiedan_dashou_id = None order.clkf = str(shop_id) # 根据是否有指定打手决定状态 order.zhuangtai = 7 if zhiding_id else 1 order.save() logger.info(f"转移大厅成功,订单 {dingdan_id}") return Response({'code': 200, 'msg': '转移大厅成功'}) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}) except Exception as e: logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误'}) # ================================================================ # shangdian/views/fine_apply_view.py # 店铺罚款申请接口 —— 二改自 FineApplyView(客服罚款) # ================================================================ class ShopFineApplyView(APIView): """ 店铺罚款申请接口 URL: POST /shangdian/fksq 权限: JWT认证 + 店铺身份验证 请求参数: userId (必填) : 店铺所属用户的ID(用于身份验证) dingdan_id (必填) : 订单ID reason (必填) : 罚款原因 amount (必填) : 罚款金额(元) yingxiang_qiangdan (可选) : 是否影响抢单,1=是,0=否,默认1 说明: - 仅处理平台订单,需属于当前店铺 - 本地/对方派单处理逻辑与客服接口一致: 无对方订单ID 或 对方派单 → 本地生成罚单 有对方订单ID且我方派单 → 跨平台通知对方,本地也记录(标记对方俱乐部ID) - 申请人:店铺ID,身份码固定为4(店铺管理员) - 被处罚人:接单打手,身份码固定为1 """ permission_classes = [IsAuthenticated] def post(self, request): # ---------- 1. 提取参数 ---------- user_id = request.data.get('userId', '').strip() # 店铺用户ID dingdan_id = request.data.get('dingdan_id', '').strip() chufaliyou = request.data.get('reason', '').strip() # 罚款原因(对应前端的reason) fakuanjine = request.data.get('amount', 0) # 罚款金额(对应前端的amount) yingxiang_qiangdan = request.data.get('yingxiang_qiangdan', 1) # 影响抢单 if not all([user_id, dingdan_id, chufaliyou, fakuanjine]): return Response({'code': 400, 'msg': '参数不完整'}) try: # ---------- 2. 店铺身份验证 ---------- dianpu, error = verify_shop_permission(request, user_id) if error: return error shop_id = dianpu.id # 店铺ID,作为罚款申请人 # ---------- 3. 验证订单归属 ---------- try: pingtai_ext = PlatformOrderExt.query.select_related('Order').get( ShopID=shop_id, Order__OrderID=dingdan_id ) order = pingtai_ext.Order except PlatformOrderExt.DoesNotExist: logger.warning(f"罚款申请:订单 {dingdan_id} 不属于店铺 {shop_id}") return Response({'code': 404, 'msg': '订单不存在或不属于本店铺'}) # ---------- 4. 获取打手ID ---------- dashou_id = order.PlayerID if not dashou_id: return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'}) # ---------- 5. 重复罚款检查 ---------- if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=dingdan_id).exists(): return Response({'code': 400, 'msg': '该打手已被罚款过'}) # ---------- 6. 创建罚单 ---------- with transaction.atomic(): Penalty.query.create( PenalizedUserID=dashou_id, ApplicantID=str(shop_id), # 申请人记为店铺ID Identity=1, # 被处罚人身份:1 打手 Reason=chufaliyou, FineAmount=fakuanjine, RelatedOrderID=dingdan_id, Status=1, # 待缴纳 AffectsGrabbing=yingxiang_qiangdan, ApplicantIdentity=4, # 申请人身份:4 店铺管理员 ) logger.info(f"本地罚款成功: 订单 {dingdan_id}, 打手 {dashou_id}, 金额 {fakuanjine}") return Response({'code': 200, 'msg': '罚款已生成'}) except Exception as e: logger.error(f"店铺罚款申请异常: {traceback.format_exc()}") return Response({'code': 500, 'msg': '服务器内部错误'})