Files
Django/orders/views/merchant.py
TermiNexus f1c8633345 perf: P1 性能优化批量修复 + kefu 视图拆分
P1 性能优化(避免循环内 N+1 / N 次 DB 查询):
- shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组
- product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断
- roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取
- guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete
- admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update
- jituan 服务层多处合并统计查询、批量预取 map
- rank 多处循环 N+1 改批量预取
- backend 多处循环内 count/create 改批量
- config/orders/merchant_ops 多处循环 N+1 改批量预取

其他改动:
- users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件
- 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
2026-07-05 23:17:38 +08:00

1414 lines
59 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.
"""orders.views.merchant - auto-generated by split script."""
# ==================== 标准库 ====================
import hmac
import os
import json
import time
import random
import string
import traceback
import threading
import requests
import hashlib
import xml.etree.ElementTree as ET
import xmltodict
import logging
from decimal import Decimal
from django.conf import settings
from django.db import models, transaction
from gvsdsdk.fluent import db, func, FQ
from django.db.models import F, Q, OuterRef, Subquery
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.authentication import JWTAuthentication
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sts.v20180813 import sts_client, models
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.money import yuan_to_fen
from utils.chat_utils import (
_send_group_message, _subscribe_users_to_group, establish_order_chat,
prepare_order_group_chat, resolve_pair_group_id,
parse_pair_from_group_id, resolve_pair_from_order, check_pair_chat_permission,
query_pair_orders, order_to_chat_dict,
)
from utils.fadan_utils import check_fadan_qiangdan_eligible
from orders.utils import (
calc_shangjia_order_fencheng,
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from backend.utils import update_shangjia_daily, update_dashou_daily_by_action, pick_leixing_id, datetime_aliases, fmt_datetime
from rank.services import record_dashou_biaoxian
from rank.utils import check_dashou_biaoqian_required
from orders.notice_tasks import dingdan_guangbo
from ..models import (
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate,
CommissionRate, PlayerRating, RefundRecord,
)
from jituan.services.club_config import get_commission_rate, get_commission_rate_object
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_penalty_club_id, resolve_penalty_record_club_id
from users.fadan_fenhong_utils import lock_penalty_bonus
from users.fadan_fenhong_utils import lock_penalty_bonus
from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from users.models import UserDashou, UserShangjia, UserBoss
from users.business_models import User
from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
from config.models import (
ShangjiaLianjie
)
logger = logging.getLogger(__name__)
class ShangpinLeixingView(APIView):
"""
获取商家可用的商品类型列表(含关联板块的称号)
POST /dingdan/sjspleixing
"""
permission_classes = [IsAuthenticated]
@staticmethod
def _can_access(request):
"""商家老板、有效子客服(含代理调用)方可获取"""
if getattr(request, '_staff_member', None) is not None:
return True
from merchant_ops.services.authz import (
get_active_staff_member, is_merchant_owner, member_has_perm, staff_order_read_perm,
)
if is_merchant_owner(request.user):
return True
member = get_active_staff_member(request.user)
return bool(member and staff_order_read_perm(member))
def post(self, request):
if not self._can_access(request):
return Response({'code': 403, 'msg': '非商家或有效客服,无权获取订单类型', 'data': []})
try:
# 1. 查询审核通过且抢单要求类型为1的商品类型按排序权重升序
shangpin_queryset = ShangpinLeixing.query.filter(
yaoqiuleixing=1,
shenhezhuangtai=1
).select_related('bankuai').order_by('paixu')
# 2. 收集所有涉及到的板块ID
bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct()
# 3. 查询这些板块下的所有称号(每个称号绑定到一个板块)
chenghao_list = Chenghao.query.filter(
bankuai_id__in=bankuai_ids
).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id')
# 4. 构建 板块ID -> 称号列表 的映射(确保准确关联)
bankuai_chenghao_map = {}
for ch in chenghao_list:
bid = ch['bankuai_id']
if bid not in bankuai_chenghao_map:
bankuai_chenghao_map[bid] = []
bankuai_chenghao_map[bid].append({
'id': ch['id'],
'mingcheng': ch['mingcheng'],
'texiao_json': ch['texiao_miaoshu'] # 前端 chenghao-tag 组件需要
})
# 5. 组装最终数据
data_list = []
for sp in shangpin_queryset:
item = {
'id': sp.id,
'yaoqiuleixing': sp.yaoqiuleixing,
'huiyuan_id': sp.huiyuan_id,
'jieshao': sp.jieshao,
# 当前类型所属板块的称号列表(无板块则为空数组)
'chenghaoList': bankuai_chenghao_map.get(sp.bankuai_id, [])
}
data_list.append(item)
return Response({
'code': 200,
'msg': '获取成功',
'data': data_list
})
except Exception as e:
logger.error(f"获取商品类型失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500)
class ShangjiaPaifaView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
logger.info(f"商家派单请求: user_id={request.user.UserUID}, data={request.data}")
try:
data = request.data
# ----- 1. 获取原始参数(保持原类型)-----
shangpin_type_id = data.get('shangpinTypeId')
huiyuan_id = data.get('huiyuanId')
yaoqiuleixing = data.get('GrabRequirement')
dingdan_jieshao_raw = data.get('dingdanJieshao', '')
dingdan_beizhu_raw = data.get('dingdanBeizhu', '')
laoban_name_raw = data.get('laobanName', '')
zhiding_uid_raw = data.get('zhidingUid', '')
jiage_str = data.get('jiage')
label_id = data.get('labelId') # 整数或 None
commission_enabled = data.get('commissionEnabled', False)
commission_value_raw = data.get('commissionValue')
waibu_dingdan_id_raw = data.get('waibuDingdanId', '')
# ----- 2. 字符串字段安全处理 -----
dingdan_jieshao = str(dingdan_jieshao_raw).strip()
dingdan_beizhu = str(dingdan_beizhu_raw).strip()
laoban_name = str(laoban_name_raw).strip()
zhiding_uid = str(zhiding_uid_raw).strip()
commission_value_str = str(commission_value_raw).strip() if commission_value_raw is not None else ''
waibu_dingdan_id = str(waibu_dingdan_id_raw).strip()[:64] if waibu_dingdan_id_raw else ''
# ----- 3. 必填校验 -----
if not all([shangpin_type_id, dingdan_jieshao, laoban_name, jiage_str]):
logger.warning(f"参数缺失: {request.data}")
return Response({'code': 400, 'msg': '缺少必填参数'})
# ----- 4. 价格验证 -----
try:
jiage = float(jiage_str)
if jiage < 1 or jiage > 10000:
return Response({'code': 400, 'msg': '价格需在1-10000元之间'})
if '.' in str(jiage_str) and len(str(jiage_str).split('.')[1]) > 2:
return Response({'code': 400, 'msg': '最多支持两位小数'})
except ValueError:
return Response({'code': 400, 'msg': '价格格式错误'})
# ----- 5. 商品类型 -----
try:
shangpin_type = ShangpinLeixing.query.get(id=shangpin_type_id)
except ShangpinLeixing.DoesNotExist:
logger.error(f"商品类型不存在: {shangpin_type_id}")
return Response({'code': 400, 'msg': '商品类型不存在'})
# ----- 6. 押金 / 会员模式决定 -----
final_yongjin = None
if commission_enabled and commission_value_str:
try:
cv = float(commission_value_str)
if cv > 0:
final_yaoqiu_type = 2
final_yongjin = Decimal(str(cv))
final_huiyuan_id = ''
logger.info(f"押金模式: 佣金={cv}")
else:
final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
logger.warning("佣金为0降级为会员模式")
except ValueError:
final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
logger.warning("佣金格式错误,降级为会员模式")
else:
final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1
final_huiyuan_id = str(huiyuan_id) if huiyuan_id else ''
logger.info(f"会员模式: 类型={final_yaoqiu_type}")
# ----- 7. 称号(可选:不传/空值/无效/不存在均视为无标签)-----
chenghao_obj = None
label_id_str = str(label_id).strip() if label_id is not None else ''
if label_id_str and label_id_str not in ('0', 'null', 'undefined'):
try:
lid = int(label_id_str)
if lid > 0:
chenghao_obj = Chenghao.query.filter(id=lid).first()
if chenghao_obj:
logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}")
else:
logger.info(f"标签不存在,按无标签派单: {label_id}")
except (ValueError, TypeError):
logger.info(f"标签ID无效按无标签派单: {label_id}")
# ----- 8. 商家信息 -----
try:
shangjia = request.user.ShopProfile
except UserShangjia.DoesNotExist:
logger.warning(f"用户不是商家: {request.user.UserUID}")
return Response({'code': 403, 'msg': '您不是商家'})
if shangjia.zhuangtai != 1:
logger.warning(f"商家状态异常: {shangjia.zhuangtai}")
return Response({'code': 403, 'msg': '商家状态异常'})
if jiage > shangjia.yue:
logger.warning(f"余额不足: {shangjia.yue} < {jiage}")
return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}'})
# ----- 9. 指定打手 -----
zhiding_dashou = None
if zhiding_uid:
try:
zhiding_user = User.query.get(UserUID=zhiding_uid)
zhiding_dashou = zhiding_user.DashouProfile
if zhiding_dashou.zhanghaozhuangtai != 1:
return Response({'code': 400, 'msg': '指定打手状态异常'})
except User.DoesNotExist:
return Response({'code': 400, 'msg': '指定用户不存在'})
except UserDashou.DoesNotExist:
return Response({'code': 400, 'msg': '指定用户不是打手'})
# ----- 10. 利率(打手+管事分成,打手优先) -----
club_id = resolve_club_id_from_request(request)
dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage, club_id=club_id)
# ----- 11. 生成订单ID -----
def generate_dingdan_id():
ts = int(time.time() * 1000)
pid = os.getpid() % 1000
rnd = random.randint(1000, 9999)
return f"SJ{ts}{pid:03d}{rnd}"
# ----- 12. 事务:创建订单、更新商家统计 -----
dingdan = None
try:
with transaction.atomic():
dingdan = Order.query.create(
OrderID=generate_dingdan_id(),
Status=7 if zhiding_dashou else 1,
Platform=2,
Amount=jiage,
PlayerCommission=dashou_fencheng,
ManagerCommission=guanshi_fencheng,
AssignedID=zhiding_uid if zhiding_dashou else '',
ProductID=0,
ProductTypeID=int(shangpin_type_id),
GrabRequirement=final_yaoqiu_type,
CommissionReq=final_yongjin,
MembershipID=final_huiyuan_id,
Description=dingdan_jieshao[:2000],
Remark=dingdan_beizhu[:1000],
Nickname=laoban_name[:50],
ImageURL=request.user.Avatar,
User1ID=f"Sj{request.user.UserUID}",
ClubID=club_id,
ExternalOrderID=waibu_dingdan_id or None,
)
logger.info(f"订单主表创建: {dingdan.OrderID}")
MerchantOrderExt.query.create(
Order=dingdan,
MerchantID=request.user.UserUID,
MerchantNickname=shangjia.nicheng,
)
if chenghao_obj:
DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=chenghao_obj)
logger.info(f"订单关联称号: {chenghao_obj.mingcheng}")
# 更新商家统计
UserShangjia.query.filter(id=shangjia.id).update(
fabu=F('fabu') + 1,
yue=F('yue') - jiage,
jinridingdan=F('jinridingdan') + 1,
jinriliushui=F('jinriliushui') + jiage,
jinyuedingdan=F('jinyuedingdan') + 1,
jinyueliushui=F('jinyueliushui') + jiage,
)
update_shangjia_daily(
yonghuid=request.user.UserUID,
amount=Decimal(str(jiage)),
action=1,
order_id=dingdan.OrderID,
)
# 事务提交,订单已持久化
except Exception as e:
logger.error(f"订单创建事务失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '订单创建失败,请稍后重试'})
# ========== 以下操作均在事务外,避免异步任务查不到订单 ==========
# Celery 广播任务(异步)
try:
order_info = {
'OrderID': dingdan.OrderID,
'game_type': self._get_game_type_name(dingdan.ProductTypeID),
'amount': str(dingdan.Amount),
'order_desc': (dingdan.Description or '')[:50],
}
dingdan_guangbo.delay(order_info)
logger.info(f"已提交广播任务: {dingdan.OrderID}")
except Exception as e:
logger.error(f"广播任务提交失败: {e}", exc_info=True)
logger.info(f"商家派单成功: {dingdan.OrderID}, 金额={jiage}")
return Response({'code': 200, 'msg': '派单成功', 'data': {'jine': str(dingdan.Amount)}})
except Exception as e:
logger.error(f"商家派单接口未捕获异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '系统内部错误'})
def _get_game_type_name(self, leixing_id):
try:
gt = ShangpinLeixing.query.filter(id=leixing_id).first()
return gt.jieshao if gt else '游戏订单'
except Exception:
return '游戏订单'
# views.py 中 ShangjiaDingdanHuoquView 完整改造
class ShangjiaDingdanHuoquView(APIView):
"""
商家订单获取接口
POST /dingdan/sjdingdanhq
需要JWT认证
支持分页、状态筛选、商品类型筛选、模糊搜索订单ID/订单介绍/游戏昵称/打手ID
返回待结算订单数量pending_count
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 获取当前登录用户的yonghuid
try:
current_user = request.user
yonghuid = current_user.UserUID
except AttributeError:
return Response({
'code': 1,
'msg': '用户认证失败',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 解析参数
try:
data = request.data
zhuangtai_list = data.get('zhuangtai_list', [])
page = max(1, int(data.get('page', 1)))
page_size = min(100, max(1, int(data.get('page_size', 10))))
# 商品类型 / 关键字 / 时间段等见 apply_merchant_order_list_filters
except (ValueError, TypeError):
return Response({
'code': 2,
'msg': '参数格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 验证状态列表
if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0:
return Response({
'code': 3,
'msg': '状态列表不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
try:
from orders.services.merchant_order_list_filters import apply_merchant_order_list_filters
# 4. 构建基础查询(该商家的所有订单)
base_qs = MerchantOrderExt.query.filter(
MerchantID=yonghuid
).select_related('Order')
filter_payload = dict(data)
filter_payload['_merchant_id_filter'] = yonghuid
# 5. 状态过滤
base_qs = base_qs.filter(Order__Status__in=zhuangtai_list)
# 67. 类型 / 关键字 / 时间段 / 客服筛选
base_qs = apply_merchant_order_list_filters(base_qs, filter_payload)
# 8. 待结算统计(全量聚合,非分页求和)
from orders.services.merchant_order_stats import (
build_merchant_order_stats, merchant_ext_qs, filter_by_staff_member,
)
stats_qs = merchant_ext_qs(yonghuid)
stats_qs = apply_merchant_order_list_filters(stats_qs, filter_payload)
order_stats = build_merchant_order_stats(stats_qs)
# 9. 排序、分页
total_count = base_qs.count()
start_index = (page - 1) * page_size
end_index = start_index + page_size
current_page_orders = list(base_qs.order_by('-Order__CreateTime')[start_index:end_index])
from jituan.services.club_context import resolve_club_id_from_request
from orders.services.merchant_order_list_enrich import enrich_merchant_order_list_items
club_id = resolve_club_id_from_request(request)
order_list = enrich_merchant_order_list_items(current_page_orders, club_id=club_id)
# 11. 判断是否还有更多
current_page_count = len(current_page_orders)
has_more = start_index + current_page_count < total_count
return Response({
'code': 0,
'msg': '成功',
'data': {
'list': order_list,
'has_more': has_more,
'total': total_count,
'pending_count': order_stats['pending_count'],
'pending_amount': order_stats['pending_amount'],
}
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"商家订单获取失败: {str(e)}", exc_info=True)
return Response({
'code': 4,
'msg': '服务器内部错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ShangjiaShujuTongjiView(APIView):
"""
商家经营数据统计(订单表聚合)
POST /dingdan/sjshujutj
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
current_user = request.user
yonghuid = current_user.UserUID
except AttributeError:
return Response({'code': 1, 'msg': '用户认证失败', 'data': None}, status=401)
try:
shangjia = current_user.ShopProfile
if shangjia.zhuangtai != 1:
return Response({'code': 403, 'msg': '商家状态异常', 'data': None}, status=403)
except Exception:
return Response({'code': 403, 'msg': '非商家用户', 'data': None}, status=403)
data = request.data or {}
from orders.services.merchant_order_list_filters import apply_merchant_order_list_filters
from orders.services.merchant_order_stats import merchant_ext_qs, build_merchant_order_stats
filter_payload = dict(data)
filter_payload['_merchant_id_filter'] = yonghuid
qs = merchant_ext_qs(yonghuid)
qs = apply_merchant_order_list_filters(qs, filter_payload)
order_stats = build_merchant_order_stats(qs)
penalty_stats = None
try:
from orders.models import Penalty, PenaltyRecord
from django.db.models import Q, Sum, Count
_fakuan_agg = Penalty.query.filter(ApplicantID=yonghuid, Status__in=[1, 3]).aggregate(
count=Count('id'),
amt=Sum('FineAmount'),
)
fakuan_pending = _fakuan_agg['count']
fakuan_pending_amt = _fakuan_agg['amt']
jifen_pending = PenaltyRecord.query.filter(
ApplicantID=yonghuid,
).filter(Q(ApplyStatus=0) | Q(ApplyStatus=3)).count()
penalty_stats = {
'fakuan_pending': fakuan_pending,
'fakuan_pending_amount': str(fakuan_pending_amt or '0.00'),
'jifen_pending': jifen_pending,
}
except Exception:
penalty_stats = {
'fakuan_pending': 0,
'fakuan_pending_amount': '0.00',
'jifen_pending': 0,
}
return Response({
'code': 0,
'msg': '成功',
'data': {
'order': order_stats,
'penalty': penalty_stats,
'can_view_finance': True,
},
})
class ShangjiaDingdanXiangqingView(APIView):
"""
商家订单详情接口
返回订单基础信息、打手信息、交付图片、罚款数据、分红利率、更换打手产生的新订单ID等
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 获取当前登录用户的 yonghuid
try:
current_user = request.user
yonghuid = current_user.UserUID
except AttributeError:
return Response({
'code': 1,
'msg': '用户认证失败',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 获取前端参数
try:
data = request.data
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
except Exception:
return Response({
'code': 2,
'msg': '参数格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if not dingdan_id:
return Response({
'code': 3,
'msg': '订单ID不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
try:
# 3. 查询商家订单扩展表,验证权限并获取基础订单数据
shangjia_kuozhan = MerchantOrderExt.query.filter(
MerchantID=yonghuid,
Order__OrderID=dingdan_id
).select_related('Order').first()
if not shangjia_kuozhan:
return Response({
'code': 4,
'msg': '订单不存在或您无权限查看',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
order = shangjia_kuozhan.Order
# 4. 交付图片列表
dashou_tupian_list = PlayerDeliveryImage.query.filter(
OrderID=dingdan_id
).order_by('CreateTime').values_list('ImageURL', flat=True)
dashou_jiaofu = list(dashou_tupian_list)
# 5. 打手详细信息头像、昵称、用户ID
dashou_yonghuid = order.PlayerID or ''
dashou_nicheng = ''
dashou_avatar = ''
if dashou_yonghuid:
dashou_user = User.query.filter(
UserUID=dashou_yonghuid
).select_related('DashouProfile').first()
if dashou_user:
dashou_avatar = dashou_user.Avatar or ''
if hasattr(dashou_user, 'DashouProfile') and dashou_user.DashouProfile:
dashou_nicheng = dashou_user.DashouProfile.nicheng or ''
# 6. 罚款数据(如果存在)
fadan_obj = Penalty.query.filter(
RelatedOrderID=dingdan_id,
PenalizedUserID=dashou_yonghuid
).first()
fadan_dict = None
if fadan_obj:
ct = fmt_datetime(fadan_obj.CreateTime)
ut = fmt_datetime(fadan_obj.UpdateTime)
zhengju_urls = list(
PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=1).values_list('ImageURL', flat=True)
)
shensu_urls = list(
PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=2).values_list('ImageURL', flat=True)
)
zhengju_imgs = [
{'id': t.id, 'url': t.ImageURL}
for t in PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=1)
]
shensu_imgs = [
{'id': t.id, 'url': t.ImageURL}
for t in PenaltyAppealImage.query.filter(Penalty=fadan_obj, Purpose=2)
]
fadan_dict = {
'id': fadan_obj.id,
'chufaliyou': fadan_obj.Reason or '',
'fakuanjine': float(fadan_obj.FineAmount) if fadan_obj.FineAmount else 0.0,
'yingxiang_qiangdan': fadan_obj.AffectsGrabbing,
'zhuangtai': fadan_obj.Status,
'shensuliyou': fadan_obj.AppealReason or '',
'tongyi_chufaliyou': fadan_obj.ApproveReason or '',
'bohuiliyou': fadan_obj.RejectReason or '',
'yingde_fenhong': float(fadan_obj.ApplicantBonusAmount or 0),
'fenhong_lilv': float(fadan_obj.ApplicantBonusRate or 0),
'fenhong_yiruzhang': bool(fadan_obj.BonusCredited),
'zhengju_tupian': zhengju_imgs,
'shensu_tupian': shensu_imgs,
'CreateTime': ct,
'create_time': ct,
'UpdateTime': ut,
'update_time': ut,
}
# 7. 商家分红利率:已有罚单用锁定值;否则按商家所在俱乐部预览
fenhong_lilv = 0.0
try:
if fadan_obj and fadan_obj.ApplicantBonusRate:
fenhong_lilv = float(fadan_obj.ApplicantBonusRate)
else:
from users.fadan_fenhong_utils import get_merchant_fenhong_lilv
from jituan.services.club_write import resolve_club_id_for_write
merchant_club_id = resolve_club_id_for_write(request, current_user)
fenhong_lilv = get_merchant_fenhong_lilv(yonghuid, club_id=merchant_club_id)
except Exception as e:
logger.warning(f'获取分红利率失败: {e}')
# 8. 更换打手后产生的新订单ID如果当前订单被更换过
xin_dingdan_id = ''
try:
# 查找以当前订单作为“上一个订单ID”的新订单扩展记录
new_shangjia_order = MerchantOrderExt.query.filter(
PrevOrderID=dingdan_id,
MerchantID=yonghuid
).select_related('Order').first()
if new_shangjia_order:
xin_dingdan_id = new_shangjia_order.Order.OrderID
except Exception as e:
logger.warning(f'查找新订单ID失败: {e}')
# 9. 构建返回数据
response_data = {
# 基础订单字段
'dingdan_id': order.OrderID,
'zhuangtai': order.Status or 0,
'jine': float(order.Amount) if order.Amount else 0.0,
'jieshao': order.Description or '',
'beizhu': order.Remark or '',
'nicheng': order.Nickname or '',
**datetime_aliases(order.CreateTime),
# 打手信息
'dashou_yonghuid': dashou_yonghuid,
'dashou_nicheng': dashou_nicheng,
'dashou_avatar': dashou_avatar,
# 服务详情
'fuwudashou_id': dashou_yonghuid, # 保留旧字段兼容
'dashou_liuyan': order.PlayerRemark or '',
'dashou_jiaofu': dashou_jiaofu,
# 商家扩展字段(旧)
'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '',
'tkliyou': order.RefundReason or '',
'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 保留,但不影响新罚款
'sqzhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0,
'bhliyou': shangjia_kuozhan.RejectReason or '',
'chufa_jieguo': '', # 已弃用
# 新增:罚款数据
'fadan': fadan_dict,
# 新增:分红利率
'fenhong_lilv': fenhong_lilv,
# 新增更换打手后的新订单ID
'xin_dingdan_id': xin_dingdan_id,
# 拼多多订单号(商家/客服可见)
'waibu_dingdan_id': order.ExternalOrderID or '',
'laoban_shouji': getattr(shangjia_kuozhan, 'BossPhone', '') or '',
}
try:
from merchant_ops.services.order_enrich import enrich_order_detail_data
enrich_order_detail_data(response_data, dingdan_id)
except Exception as e:
logger.warning(f'派单客服信息附加失败: {e}')
return Response({
'code': 0,
'msg': '成功',
'data': response_data
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"商家订单详情查询失败: {str(e)}", exc_info=True)
return Response({
'code': 5,
'msg': '服务器内部错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ShangjiaJiesuanView(APIView):
"""商家结单接口"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
# 1. 获取当前登录用户
try:
current_user = request.user
yonghuid = current_user.UserUID
try:
shangjia = current_user.ShopProfile
except UserShangjia.DoesNotExist:
return Response({
'code': 1,
'msg': '您不是商家或商家信息未完善',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
except AttributeError:
return Response({
'code': 1,
'msg': '用户认证失败',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 获取前端参数
try:
data = request.data
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
pingfen = int(data.get('pingfen', 0))
liuyan = data.get('liuyan', '').strip()
except (ValueError, TypeError):
return Response({
'code': 2,
'msg': '参数格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 3. 参数验证
if not dingdan_id:
return Response({
'code': 3,
'msg': '订单ID不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if pingfen < 0 or pingfen > 5:
return Response({
'code': 4,
'msg': '评分必须在0-5分之间',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
try:
with transaction.atomic():
# 4. 查询订单并加锁(修复并发问题)
order = Order.query.select_for_update().get(OrderID=dingdan_id)
# 验证订单是否属于当前商家
shangjia_kuozhan = MerchantOrderExt.query.filter(
Order=order,
MerchantID=yonghuid
).first()
if not shangjia_kuozhan:
return Response({
'code': 5,
'msg': '订单不存在或您无权限操作',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 5. 验证订单状态
if order.Status != 8:
return Response({
'code': 6,
'msg': '订单不在结算状态,无法结单',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 获取订单字段
jine = order.Amount or Decimal('0')
dashou_fencheng = order.PlayerCommission or Decimal('0')
jiedan_dashou_id = order.PlayerID
if not jiedan_dashou_id:
return Response({
'code': 7,
'msg': '订单没有接单打手,无法结单',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# ========== 本地订单正常结算给打手 ==========
# 查询打手信息
try:
dashou_user = User.query.get(UserUID=jiedan_dashou_id)
dashou = dashou_user.DashouProfile
except (User.DoesNotExist, UserDashou.DoesNotExist):
return Response({
'code': 8,
'msg': '接单打手不存在或信息不完整',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 原子更新打手数据(使用 F 表达式)
UserDashou.query.filter(id=dashou.id).update(
chengjiaozongliang=F('chengjiaozongliang') + 1,
yue=F('yue') + dashou_fencheng,
zonge=F('zonge') + dashou_fencheng,
jinrishouyi=F('jinrishouyi') + dashou_fencheng,
jinyueshouyi=F('jinyueshouyi') + dashou_fencheng,
)
# 更新商家成交单量
UserShangjia.query.filter(id=shangjia.id).update(
chengjiao=F('chengjiao') + 1,
)
# 更新订单状态
order.Status = 3
order.save()
# 处理评分(所有情况都处理)
if pingfen > 0:
pingfen_obj, created = PlayerRating.query.get_or_create(
PlayerID=jiedan_dashou_id,
defaults={'TotalScore': pingfen, 'RatingCount': 1}
)
if not created:
PlayerRating.query.filter(id=pingfen_obj.id).update(
TotalScore=F('TotalScore') + pingfen,
RatingCount=F('RatingCount') + 1,
)
# 更新商家订单扩展表的评价
MerchantOrderExt.query.filter(id=shangjia_kuozhan.id).update(
MerchantReview=liuyan,
)
# 新增:调用打手每日统计(成交)
try:
update_dashou_daily_by_action(
yonghuid=jiedan_dashou_id,
amount=dashou_fencheng,
action=2 # 2 表示成交(结算)
)
logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 不影响主流程
# ✅ 新方法(行为驱动,更安全)
update_shangjia_daily(
yonghuid=request.user.UserUID,
amount=Decimal(str(jine)),
action=2, # 2 = 结算
order_id=dingdan_id,
)
# 商家订单管事分红(仅 Platform=2
if order.Platform == 2:
settle_shangjia_order_guanshi_fenhong(order, jiedan_dashou_id)
# 返回成功
return Response({
'code': 0,
'msg': '结单成功',
'data': {
'dingdan_id': dingdan_id,
'new_zhuangtai': 3,
'jine': float(jine),
'dashou_fencheng': float(dashou_fencheng),
'jiedan_dashou_id': jiedan_dashou_id,
'pingfen': pingfen,
'liuyan': liuyan
}
}, status=status.HTTP_200_OK)
except Order.DoesNotExist:
return Response({
'code': 5,
'msg': '订单不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"商家结单失败: {str(e)}", exc_info=True)
return Response({
'code': 9,
'msg': '服务器内部错误,结单失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ShangjiaChexiaoView(APIView):
"""商家撤销订单申请接口"""
permission_classes = [IsAuthenticated] # JWT认证
def post(self, request):
# 1. 获取当前登录用户并验证商家身份
try:
current_user = request.user
# 使用反向查询获取商家扩展信息(性能最佳)
shangjia = current_user.ShopProfile
except UserShangjia.DoesNotExist:
return Response({
'code': 1,
'msg': '您不是商家或商家信息未完善',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 2. 检查商家账号状态
if shangjia.zhuangtai != 1:
return Response({
'code': 2,
'msg': '商家账号状态异常,无法操作',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 3. 获取前端参数
try:
data = request.data
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
chexiao_liyou = data.get('chexiao_liyou', '').strip()
except Exception:
return Response({
'code': 3,
'msg': '参数格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 参数验证
if not dingdan_id or not chexiao_liyou:
return Response({
'code': 4,
'msg': '订单ID和撤销原因不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
try:
# 5. 使用事务确保数据一致性
with transaction.atomic():
# 查询订单主表
dingdan = Order.query.filter(OrderID=dingdan_id).first()
if not dingdan:
return Response({
'code': 5,
'msg': '订单不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 6. 验证订单状态13=链接待填单,可撤销释放额度)
if dingdan.Status not in [1, 7, 13]:
return Response({
'code': 6,
'msg': '当前订单状态不可撤销',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 7. 验证订单所属商家(使用反向查询)
shangjia_kuozhan = MerchantOrderExt.query.filter(
Order__OrderID=dingdan_id,
MerchantID=current_user.UserUID
).first()
if not shangjia_kuozhan:
return Response({
'code': 7,
'msg': '您无权撤销此订单',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 8. 创建退款记录注意申请状态直接设为1-成功)
RefundRecord.query.create(
OrderID=dingdan_id,
ApplicantID=current_user.UserUID,
Reason=chexiao_liyou,
ApplyStatus=1, # 直接成功
Amount=dingdan.Amount,
PlayerCommission=dingdan.PlayerCommission,
Description=dingdan.Description,
Remark=dingdan.Remark,
Nickname=dingdan.Nickname,
)
# 9. 更新商家扩展表(客服额度单不退商家余额,只释放额度)
from merchant_ops.services.wallet import is_staff_quota_order
staff_quota_order = is_staff_quota_order(dingdan_id)
shangjia_updates = {
'tuikuan': F('tuikuan') + 1,
'jinridingdan': F('jinridingdan') - 1,
'jinyuedingdan': F('jinyuedingdan') - 1,
'jinriliushui': F('jinriliushui') - dingdan.Amount,
'jinyueliushui': F('jinyueliushui') - dingdan.Amount,
}
if not staff_quota_order:
shangjia_updates['yue'] = F('yue') + dingdan.Amount
UserShangjia.query.filter(id=shangjia.id).update(**shangjia_updates)
# 10. 更新订单状态为5已退款
dingdan.Status = 5
dingdan.save()
# 行为驱动
update_shangjia_daily(
yonghuid=request.user.UserUID,
amount=Decimal(str(dingdan.Amount)),
action=3, # 3 = 退款
order_id=dingdan_id,
)
return Response({
'code': 0,
'msg': '撤销成功',
'data': {
'dingdan_id': dingdan_id,
'new_zhuangtai': 5,
'tuikuan_liyou': chexiao_liyou,
'jine': float(dingdan.Amount) if dingdan.Amount else 0.0
}
}, status=status.HTTP_200_OK)
except Exception as e:
# 记录异常日志
logger.error(f"商家撤销订单失败: {str(e)}", exc_info=True)
return Response({
'code': 8,
'msg': '服务器内部错误,撤销失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ShangjiaTuikuanShenqingView(APIView):
"""商家退款申请接口"""
permission_classes = [IsAuthenticated] # JWT认证
def post(self, request):
# 1. 获取当前登录用户并验证商家身份
try:
current_user = request.user
shangjia = current_user.ShopProfile
except UserShangjia.DoesNotExist:
return Response({
'code': 1,
'msg': '您不是商家或商家信息未完善',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 2. 检查商家账号状态
if shangjia.zhuangtai != 1:
return Response({
'code': 2,
'msg': '商家账号状态异常,无法操作',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 3. 获取前端参数
try:
data = request.data
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
tuikuan_liyou = data.get('tuikuan_liyou', '').strip()
except Exception:
return Response({
'code': 3,
'msg': '参数格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 参数验证
if not dingdan_id or not tuikuan_liyou:
return Response({
'code': 4,
'msg': '订单ID和退款原因不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
try:
# 5. 使用事务确保数据一致性
with transaction.atomic():
# 查询订单主表
dingdan = Order.query.filter(OrderID=dingdan_id).first()
if not dingdan:
return Response({
'code': 5,
'msg': '订单不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 6. 验证订单状态
if dingdan.Status not in [2, 8]:
return Response({
'code': 6,
'msg': '当前订单状态不可申请退款',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 7. 验证订单所属商家
shangjia_kuozhan = MerchantOrderExt.query.filter(
Order__OrderID=dingdan_id,
MerchantID=current_user.UserUID
).first()
if not shangjia_kuozhan:
return Response({
'code': 7,
'msg': '您无权为此订单申请退款',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# ========== 8. 处理退款记录(修改点:先查询是否存在) ==========
refund_record, created = RefundRecord.query.get_or_create(
OrderID=dingdan_id,
defaults={
'PlayerID': dingdan.PlayerID,
'ApplicantID': current_user.UserUID,
'Reason': tuikuan_liyou,
'ApplyStatus': 0, # 待处理
'Amount': dingdan.Amount,
'PlayerCommission': dingdan.PlayerCommission,
'Description': dingdan.Description,
'Remark': dingdan.Remark,
'Nickname': dingdan.Nickname,
}
)
if not created:
# 记录已存在,更新退款理由和申请时间
refund_record.Reason = tuikuan_liyou
refund_record.save(update_fields=['Reason', 'UpdateTime'])
logger.info(f"订单 {dingdan_id} 已有退款记录,已更新退款理由")
# 9. 更新订单状态为4退款中
dingdan.Status = 4
dingdan.RefundReason = tuikuan_liyou
dingdan.save()
# ====== 原有功能:更新打手状态 ======
if dingdan.PlayerID:
try:
dashou_main = User.query.filter(
UserUID=dingdan.PlayerID
).select_related('DashouProfile').first()
if dashou_main and hasattr(dashou_main, 'DashouProfile'):
dashou_profile = dashou_main.DashouProfile
dashou_profile.zhuangtai = 1
dashou_profile.save()
logger.info(f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.PlayerID}状态变更为1")
except Exception as e:
logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id}打手ID{dingdan.PlayerID}")
return Response({
'code': 0,
'msg': '退款申请提交成功',
'data': {
'dingdan_id': dingdan_id,
'new_zhuangtai': 4,
'tuikuan_liyou': tuikuan_liyou,
'jine': float(dingdan.Amount) if dingdan.Amount else 0.0,
'dashou_id': dingdan.PlayerID
}
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"商家退款申请失败: {str(e)}", exc_info=True)
return Response({
'code': 8,
'msg': '服务器内部错误,申请失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class ShangjiaChufaShenqingView(APIView):
"""商家处罚申请接口(已添加图片上传功能)"""
permission_classes = [IsAuthenticated] # JWT认证
def post(self, request):
# 1. 获取当前登录用户并验证商家身份
try:
current_user = request.user
shangjia = current_user.ShopProfile
except UserShangjia.DoesNotExist:
return Response({
'code': 1,
'msg': '您不是商家或商家信息未完善',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 2. 检查商家账号状态
if shangjia.zhuangtai != 1:
return Response({
'code': 2,
'msg': '商家账号状态异常,无法操作',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 3. 获取前端参数
try:
data = request.data
dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip()
dashou_id = data.get('dashou_id', '').strip()
chufa_liyou = data.get('chufa_liyou', '').strip()
chufa_tupian_urls = data.get('chufa_tupian_urls', []) # 🔴 新增图片URL数组
except Exception:
return Response({
'code': 3,
'msg': '参数格式错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 参数验证
if not dingdan_id or not dashou_id or not chufa_liyou:
return Response({
'code': 4,
'msg': '订单ID、打手ID和处罚原因不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
try:
# 5. 使用事务确保数据一致性
with transaction.atomic():
# 验证打手身份
try:
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou = dashou_user.DashouProfile
except User.DoesNotExist:
return Response({
'code': 5,
'msg': '打手用户不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
except UserDashou.DoesNotExist:
return Response({
'code': 5,
'msg': '该用户不是打手',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 查询订单主表
dingdan = Order.query.filter(OrderID=dingdan_id).first()
if not dingdan:
return Response({
'code': 6,
'msg': '订单不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 验证订单状态
if dingdan.Status not in [2, 8, 4, 5, 6, 3]:
return Response({
'code': 7,
'msg': '当前订单状态不可申请处罚',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 验证订单所属商家
shangjia_kuozhan = MerchantOrderExt.query.filter(
Order__OrderID=dingdan_id,
MerchantID=current_user.UserUID
).first()
if not shangjia_kuozhan:
return Response({
'code': 8,
'msg': '您无权为此订单申请处罚',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 验证订单的打手是否与提供的打手ID一致
if dingdan.PlayerID != dashou_id:
return Response({
'code': 9,
'msg': '该打手并非此订单的服务打手',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 创建处罚记录
chufajilu = PenaltyRecord.query.create(
PlayerID=dashou_id,
ApplicantID=current_user.UserUID,
PenaltyReason=chufa_liyou,
ApplyStatus=0, # 待处理
DeductedPoints=5, # 扣除5积分
OrderID=dingdan_id,
ClubID=resolve_penalty_record_club_id(
order_id=dingdan_id,
player_id=dashou_id,
request=request,
applicant_id=current_user.UserUID,
user=current_user,
),
)
# 🔴【新增】在创建处罚记录后立即扣除打手积分
# 注意这里使用前面已经获取的dashou对象打手扩展表
jifen_kouchu = chufajilu.DeductedPoints # 扣除的积分这里是5
if jifen_kouchu > 0:
# 检查打手积分是否足够UserDashou 字段为 jifen
if dashou.jifen < jifen_kouchu:
dashou.jifen = 0
else:
dashou.jifen -= jifen_kouchu
dashou.save()
# 🔴【新增】保存处罚图片到数据库
if chufa_tupian_urls and isinstance(chufa_tupian_urls, list):
_valid_urls = [u.strip() for u in chufa_tupian_urls if u and u.strip()]
if _valid_urls:
PenaltyEvidenceImage.objects.bulk_create([
PenaltyEvidenceImage(
OrderID=dingdan_id,
UserID=current_user.UserUID,
ImageURL=url,
)
for url in _valid_urls
])
# 更新商家订单扩展表的处罚相关字段
shangjia_kuozhan.PenaltyReason = chufa_liyou
shangjia_kuozhan.PenaltyApplyStatus = 0
shangjia_kuozhan.save()
return Response({
'code': 0,
'msg': '处罚申请提交成功',
'data': {
'dingdan_id': dingdan_id,
'dashou_id': dashou_id,
'chufa_liyou': chufa_liyou,
'sqzhuangtai': 0,
'jifen': 5,
'tupian_count': len(chufa_tupian_urls) if chufa_tupian_urls else 0 # 返回图片数量
}
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"商家处罚申请失败: {str(e)}", exc_info=True)
return Response({
'code': 10,
'msg': '服务器内部错误,申请失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)