进行了完整的视图拆分,同步了 merchant_ops 的修改
This commit is contained in:
3124
shop/views.py
3124
shop/views.py
File diff suppressed because it is too large
Load Diff
51
shop/views/__init__.py
Normal file
51
shop/views/__init__.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""shop.views package aggregate entry - re-export all submodule symbols.
|
||||
|
||||
Preserves external `from shop.views import X` for shop/urls.py.
|
||||
"""
|
||||
from .shop_base_views import (
|
||||
BindDianpuView,
|
||||
ShopGoodsView,
|
||||
ShangdianLoginView,
|
||||
ShopOverviewView,
|
||||
QrcodeDownloadView,
|
||||
DianpuShouzhiTongjiView,
|
||||
ShopQrcodeView,
|
||||
)
|
||||
from .shop_product_views import (
|
||||
ShopProductConfigView,
|
||||
ShopProductModifyView,
|
||||
ShopProductListView,
|
||||
ShopProductModifyView1,
|
||||
)
|
||||
from .shop_member_views import (
|
||||
MemberListView,
|
||||
SubShopListView,
|
||||
SubShopModifyView,
|
||||
)
|
||||
from .shop_order_views import (
|
||||
ShopOrderStatisticsView,
|
||||
ShopOrderManageView,
|
||||
)
|
||||
from .shop_refund_views import (
|
||||
ShopRefundView,
|
||||
ShopForceCompleteView,
|
||||
ShopTransferHallView,
|
||||
ShopFineApplyView,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# shop_base_views
|
||||
"BindDianpuView", "ShopGoodsView", "ShangdianLoginView",
|
||||
"ShopOverviewView", "QrcodeDownloadView", "DianpuShouzhiTongjiView",
|
||||
"ShopQrcodeView",
|
||||
# shop_product_views
|
||||
"ShopProductConfigView", "ShopProductModifyView",
|
||||
"ShopProductListView", "ShopProductModifyView1",
|
||||
# shop_member_views
|
||||
"MemberListView", "SubShopListView", "SubShopModifyView",
|
||||
# shop_order_views
|
||||
"ShopOrderStatisticsView", "ShopOrderManageView",
|
||||
# shop_refund_views
|
||||
"ShopRefundView", "ShopForceCompleteView",
|
||||
"ShopTransferHallView", "ShopFineApplyView",
|
||||
]
|
||||
771
shop/views/shop_base_views.py
Normal file
771
shop/views/shop_base_views.py
Normal file
@@ -0,0 +1,771 @@
|
||||
"""shop.views.shop_base_views - 店铺基础视图:绑定/登录/概览/二维码/收支统计."""
|
||||
# ==================== 标准库 ====================
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
import hmac
|
||||
import hashlib
|
||||
import requests
|
||||
import logging
|
||||
import traceback
|
||||
import calendar
|
||||
import threading
|
||||
import xmltodict
|
||||
from decimal import Decimal
|
||||
from collections import defaultdict
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import F, Sum, Count, Q, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.core.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 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
|
||||
|
||||
375
shop/views/shop_member_views.py
Normal file
375
shop/views/shop_member_views.py
Normal file
@@ -0,0 +1,375 @@
|
||||
"""shop.views.shop_member_views - 会员列表与下级店铺管理视图."""
|
||||
# ==================== 标准库 ====================
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
import hmac
|
||||
import hashlib
|
||||
import requests
|
||||
import logging
|
||||
import traceback
|
||||
import calendar
|
||||
import threading
|
||||
import xmltodict
|
||||
from decimal import Decimal
|
||||
from collections import defaultdict
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import F, Sum, Count, Q, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.core.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 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 验证店铺身份
|
||||
"""
|
||||
|
||||
531
shop/views/shop_order_views.py
Normal file
531
shop/views/shop_order_views.py
Normal file
@@ -0,0 +1,531 @@
|
||||
"""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
|
||||
}
|
||||
})
|
||||
|
||||
1012
shop/views/shop_product_views.py
Normal file
1012
shop/views/shop_product_views.py
Normal file
File diff suppressed because it is too large
Load Diff
660
shop/views/shop_refund_views.py
Normal file
660
shop/views/shop_refund_views.py
Normal file
@@ -0,0 +1,660 @@
|
||||
"""shop.views.shop_refund_views - 退款/强制结算/转移大厅/罚款申请视图."""
|
||||
# ==================== 标准库 ====================
|
||||
import io
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
import string
|
||||
import hmac
|
||||
import hashlib
|
||||
import requests
|
||||
import logging
|
||||
import traceback
|
||||
import calendar
|
||||
import threading
|
||||
import xmltodict
|
||||
from decimal import Decimal
|
||||
from collections import defaultdict
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models, transaction, IntegrityError
|
||||
from django.db.models import F, Sum, Count, Q, Prefetch
|
||||
from gvsdsdk.fluent import db, func, FQ
|
||||
from django.core.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 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(
|
||||
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. 订单状态校验(只有这些状态可退款)
|
||||
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, getattr(order, 'ClubID', None))
|
||||
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 = yuan_to_fen(order.Amount or 0)
|
||||
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 = ['<xml>']
|
||||
for k, v in data.items():
|
||||
xml.append(f'<{k}>{v}</{k}>')
|
||||
xml.append('</xml>')
|
||||
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(
|
||||
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. 校验订单状态必须为 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(
|
||||
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. 状态校验 ----------
|
||||
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. 创建罚单 ----------
|
||||
from jituan.services.club_penalty import resolve_penalty_club_id
|
||||
with transaction.atomic():
|
||||
penalty = 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 店铺管理员
|
||||
ClubID=resolve_penalty_club_id(
|
||||
order=order,
|
||||
request=request,
|
||||
user=request.user,
|
||||
applicant_id=str(shop_id),
|
||||
),
|
||||
)
|
||||
from users.fadan_fenhong_utils import lock_penalty_bonus
|
||||
lock_penalty_bonus(penalty)
|
||||
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': '服务器内部错误'})
|
||||
Reference in New Issue
Block a user