"""shop.views.shop_order_views - 店铺订单统计与管理视图(含 OSS 图片批量辅助函数).""" # ==================== 标准库 ==================== 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 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.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 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(ShopID=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 # ================================================================ # 辅助函数:批量获取多个订单的打手图片(单次 DB 查询) # ================================================================ def _batch_images(orders, oss_domain): """ 批量获取订单的打手提交图片,使用单次 DB 查询替代循环/线程池 N 次查询。 参数: orders: Dingdan 订单对象列表 oss_domain: OSS 域名 返回: 字典,键为订单ID (OrderID),值为 (images_list, is_punished, punishment_info) """ result = {} # 先为所有订单填充默认空值,保证调用方可拿到键 for order in orders: result[order.OrderID] = ([], False, {}) # 收集所有 (OrderID, PlayerID) 组合用于批量查询 _order_player_pairs = [ (order.OrderID, order.PlayerID) for order in orders if order.PlayerID ] if not _order_player_pairs: return result # 单次批量查询所有相关图片记录(避免循环 N 次或线程池掩盖 N 次查询) # PlayerDeliveryImage 表的主键是 OrderID + PlayerID,使用元组列表过滤 _order_ids = [pair[0] for pair in _order_player_pairs] _pair_set = set(_order_player_pairs) try: rows = list( PlayerDeliveryImage.query.filter( OrderID__in=_order_ids ).values_list('OrderID', 'PlayerID', 'ImageURL') ) except Exception as e: logger.error(f"_batch_images 批量查询图片失败: {e}") return result # 按 (OrderID, PlayerID) 分组 _img_map = {} for oid, pid, img_path in rows: if not img_path: continue # 仅保留属于本批 orders 且 PlayerID 匹配的记录(防止跨打手串号) if (oid, pid) not in _pair_set: continue if img_path.startswith('http'): url = img_path else: url = oss_domain + img_path.lstrip('/') _img_map.setdefault(oid, []).append(url) # 把分组结果写回 result for oid, imgs in _img_map.items(): if oid in result: result[oid] = (imgs, 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( ShopID=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( id__in=order_ids # Order_id 为订单主键 id ).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( ShopID=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.PlayerRemark or '', 'zhiding_id': order.AssignedID 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 } })