Files
Django/shop/views/shop_order_views.py

532 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""shop.views.shop_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 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.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(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.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
}
})