Files
Django/orders/views/admin.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

1959 lines
85 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.admin - 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__)
ZHIDING_HF_TEXT = {1: '想接', 2: '不想接', 3: '等会接'}
class AdGetDingdanXiangQing(APIView):
"""
后台订单详情获取接口
权限: JWT Token认证 + 管理员身份验证
前端调用: POST /dingdan/adddxq
参数: {"zhanghao": "管理员手机号", "dingdan_id": "订单ID"}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
"""
获取订单详情数据
返回: 订单主表数据 + 扩展表数据 + 打手图片列表
"""
try:
# 1. 获取前端参数
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
if not qianzhanghao or not dingdan_id:
return Response(
{'code': 400, 'message': '参数不完整', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 2. JWT Token身份验证 (IsAuthenticated已处理)
dangqianyonghu = request.user
# 3. 验证手机号是否匹配
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 4. 验证用户类型是否为管理员
if dangqianyonghu.UserType != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.AdminProfile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询订单主表
try:
# 查询订单主表 - 使用select_related预加载关联表
dingdan_obj = Order.query.select_related(
'pingtai_kuozhan', 'shangjia_kuozhan'
).get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 7. 构建返回数据 - 主表字段
response_data = {
# 基础信息字段
'dingdan_id': dingdan_obj.OrderID or '',
'jieshao': dingdan_obj.Description or '',
'zhuangtai': dingdan_obj.Status or 0,
'leixing': dingdan_obj.ProductTypeID or 0, # 前端需要 leixing对应 leixing_id
'fadanpingtai': dingdan_obj.Platform or 0, # 前端需要 fadanpingtai对应 fadan_pingtai
'jiage': float(dingdan_obj.Amount) if dingdan_obj.Amount else 0.00, # 前端需要 jiage对应 jine
'tupian': dingdan_obj.ImageURL or '',
'beizhu': dingdan_obj.Remark or '',
# 服务信息字段
'jiedan_dashou_id': dingdan_obj.PlayerID or '',
'dashou_fencheng': float(dingdan_obj.PlayerCommission) if dingdan_obj.PlayerCommission else 0.00,
'zhiding_id': dingdan_obj.AssignedID or '',
'dashou_liuyan': dingdan_obj.PlayerRemark or '',
# 时间字段
'chuangjianshijian': fmt_datetime(dingdan_obj.CreateTime),
'create_time': fmt_datetime(dingdan_obj.CreateTime),
'genggaishijian': fmt_datetime(dingdan_obj.UpdateTime),
'update_time': fmt_datetime(dingdan_obj.UpdateTime),
}
# 8. 根据发单平台查询扩展表数据
fadan_pingtai = dingdan_obj.Platform
if fadan_pingtai == 1:
# 平台发单 - 查询平台订单扩展表
try:
# 使用反向关联 pingtai_kuozhan
pingtai_kuozhan = dingdan_obj.pingtai_kuozhan
response_data.update({
'laoban_id': pingtai_kuozhan.BossID or '',
'nicheng': dingdan_obj.Nickname or '', # 这个字段在订单主表
'laoban_pingjia': pingtai_kuozhan.BossReview or '',
})
except PlatformOrderExt.DoesNotExist:
# 扩展表不存在,返回空值
response_data.update({
'laoban_id': '',
'nicheng': dingdan_obj.Nickname or '',
'laoban_pingjia': '',
})
elif fadan_pingtai == 2:
# 商家发单 - 查询商家订单扩展表
try:
# 使用反向关联 shangjia_kuozhan
shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
response_data.update({
'shangjia_id': shangjia_kuozhan.MerchantID or '',
'shangjia_nicheng': shangjia_kuozhan.MerchantNickname or '',
'shangjia_pingjia': shangjia_kuozhan.MerchantReview or '',
'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 商家处罚理由
'bhliyou': shangjia_kuozhan.RejectReason or '', # 驳回理由
})
except MerchantOrderExt.DoesNotExist:
# 扩展表不存在,返回空值
response_data.update({
'shangjia_id': '',
'shangjia_nicheng': '',
'shangjia_pingjia': '',
'cfliyou': '',
'bhliyou': '',
})
# 9. 查询打手提交的图片
try:
# 查询该订单的所有打手提交图片
dashou_tupian_list = PlayerDeliveryImage.query.filter(
OrderID=dingdan_id
).values_list('ImageURL', flat=True)
# 转换为列表,过滤空值
dashou_images = [img for img in dashou_tupian_list if img]
response_data['dashou_images'] = dashou_images
except Exception as e:
logger.warning(f"查询打手图片失败: {str(e)}")
response_data['dashou_images'] = []
# 🔥 新增:查询退款记录表中的商家退款理由
try:
# 导入退款记录模型
# 查询该订单的退款记录(一个订单可能有多次退款申请,我们取最新的)
tuikuan_record = RefundRecord.query.filter(
OrderID=dingdan_id
).order_by('-CreateTime').first() # 按创建时间倒序,取最新的
if tuikuan_record and tuikuan_record.Reason:
# 如果有退款记录且理由不为空,添加到返回数据
response_data['tuikuan_liyou'] = tuikuan_record.Reason
else:
# 没有找到退款记录或理由为空
response_data['tuikuan_liyou'] = ''
except RefundRecord.DoesNotExist:
# 退款记录表不存在该订单的记录
response_data['tuikuan_liyou'] = ''
except Exception as e:
# 查询过程中出现其他异常
logger.warning(f"查询退款记录失败: {str(e)}")
response_data['tuikuan_liyou'] = ''
# 10. 返回成功响应
return Response({
'code': 0,
'message': '获取成功',
'data': response_data
})
except Exception as e:
logger.error(f"获取订单详情异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 获取日志器
logger = logging.getLogger(__name__)
class AdGengHuanDaShou(APIView):
"""
后台更换打手接口
权限: JWT Token认证 + 管理员身份验证
前端调用: POST /dingdan/adddgh
参数: {
"zhanghao": "管理员账号",
"dingdan_id": "订单ID",
"new_dashou_id": "新打手ID"
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
"""
更换订单的打手
核心逻辑:
1. 验证管理员身份
2. 验证订单存在且状态为进行中(2)或结算中(8)
3. 验证新打手ID存在且是打手身份
4. 验证新打手状态正常Status=1且zhanghaoStatus=1
5. 根据订单的抢单要求类型进行验证:
- 会员抢单(1):验证打手有对应会员且未过期
- 押金抢单(2):验证打手押金≥订单押金
6. 更新新打手状态和统计数据
7. 更新原打手状态和统计数据
8. 更新订单信息
"""
try:
# 1. 获取前端参数
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
new_dashou_id = request.data.get('new_dashou_id', '').strip()
if not qianzhanghao or not dingdan_id or not new_dashou_id:
return Response(
{'code': 400, 'message': '参数不完整', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 2. JWT Token身份验证
dangqianyonghu = request.user
# 3. 验证手机号是否匹配
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 4. 验证用户类型是否为管理员
if dangqianyonghu.UserType != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.AdminProfile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询订单主表
try:
dingdan_obj = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 7. 验证订单状态是否为进行中(2)或结算中(8)
if dingdan_obj.Status not in [2, 8]:
return Response(
{'code': 400, 'message': '订单状态不允许更换打手', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 8. 验证新打手ID是否存在并且是打手身份
try:
new_dashou_user = User.query.select_related('DashouProfile').get(
UserUID=new_dashou_id,
#user_type='PlayerID'
)
except User.DoesNotExist:
return Response(
{'code': 404, 'message': '打手不存在或不是打手身份', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 9. 获取新打手扩展表
try:
new_dashou_profile = new_dashou_user.DashouProfile
except AttributeError:
return Response(
{'code': 404, 'message': '打手信息不完整', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 10. 验证新打手状态Status=1且zhanghaoStatus=1
if new_dashou_profile.zhuangtai != 1 or new_dashou_profile.zhanghaozhuangtai != 1:
return Response(
{'code': 400, 'message': '打手状态不允许接单', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 11. 根据订单的抢单要求类型进行验证
yaoqiuleixing = dingdan_obj.GrabRequirement
if yaoqiuleixing == 1: # 会员抢单
# 获取订单的会员ID
huiyuan_id = dingdan_obj.MembershipID
if not huiyuan_id:
return Response(
{'code': 400, 'message': '订单会员ID为空', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 查询会员购买记录
try:
huiyuan_goumai = Huiyuangoumai.query.get(
huiyuan_id=huiyuan_id,
yonghu_id=new_dashou_id # 注意yonghu_id存储的是用户IDyonghuid
)
except Huiyuangoumai.DoesNotExist:
return Response(
{'code': 400, 'message': '打手未购买该订单要求的会员', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 检查会员是否过期
# ✅ 可以调用因为jiance_shifou_daoqi()是Huiyuangoumai模型的实例方法
is_expired = huiyuan_goumai.jiance_shifou_daoqi()
if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
return Response(
{'code': 400, 'message': '打手的会员已过期', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
elif yaoqiuleixing == 2: # 押金抢单
# 获取订单的押金yongjin字段
order_yajin = dingdan_obj.CommissionReq
if order_yajin is None:
return Response(
{'code': 400, 'message': '订单押金为空', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 验证打手押金是否大于等于订单押金
if new_dashou_profile.yajin < order_yajin:
return Response(
{'code': 400, 'message': '打手押金不足', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
else:
# 未知的抢单要求类型
return Response(
{'code': 400, 'message': '订单抢单要求类型错误', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 12. 所有验证通过,开始更换打手(使用事务保证一致性)
with transaction.atomic():
# 12.1 更新新打手扩展表
new_dashou_profile.zhuangtai = 0 # 接单中
new_dashou_profile.jiedanzongliang += 1
new_dashou_profile.jinrijiedan += 1
new_dashou_profile.jinyuejiedan += 1
new_dashou_profile.save()
# 12.2 如果原订单有接单打手,则更新原打手状态
old_dashou_id = dingdan_obj.PlayerID
if old_dashou_id:
try:
# 查询原打手主表
old_dashou_user = User.query.select_related('DashouProfile').get(
UserUID=old_dashou_id,
#user_type='PlayerID'
)
# 获取原打手扩展表
old_dashou_profile = old_dashou_user.DashouProfile
# 更新原打手状态
old_dashou_profile.zhuangtai = 1 # 空闲
old_dashou_profile.tuikuanliang += 1 # 退款订单总量+1
old_dashou_profile.save()
logger.info(f"成功更新原打手{old_dashou_id}状态")
except User.DoesNotExist:
# 原打手不存在,继续执行不影响更换
logger.warning(f"原打手{old_dashou_id}不存在,跳过更新")
except AttributeError:
# 原打手扩展表不存在,继续执行不影响更换
logger.warning(f"原打手{old_dashou_id}扩展信息不存在,跳过更新")
except Exception as e:
# 其他异常,记录日志但继续执行
logger.error(f"更新原打手状态异常: {str(e)}", exc_info=True)
# 12.3 更新订单表
dingdan_obj.PlayerID = new_dashou_id
dingdan_obj.Status = 2 # 进行中状态
dingdan_obj.save()
# 13. 返回成功响应
return Response({
'code': 0,
'message': '更换打手成功',
'data': None
})
except Exception as e:
logger.error(f"更换打手接口异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class AdQiangZhiJieDan(APIView):
"""
后台强制结单接口
权限: JWT Token认证 + 管理员身份验证
前端调用: POST /dingdan/adddqzjd
参数: {
"zhanghao": "管理员账号",
"dingdan_id": "订单ID"
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
"""
强制结单接口
核心逻辑:
1. 验证管理员身份
2. 验证订单状态为2(进行中)、8(结算中)或4(退款审核中)才能强制结单
3. 根据发单平台(1=平台发单, 2=商家发单)分别处理
4. 更新订单状态为3(已完成)
5. 给打手结算分成金额
6. 如果是商家发单,还需要更新商家成交订单数量
"""
try:
# 1. 获取前端参数
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
if not qianzhanghao or not dingdan_id:
return Response(
{'code': 400, 'message': '参数不完整', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 2. JWT Token身份验证
dangqianyonghu = request.user
# 3. 验证手机号是否匹配
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 4. 验证用户类型是否为管理员
if dangqianyonghu.UserType != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.AdminProfile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询订单主表
try:
dingdan_obj = Order.query.select_related('shangjia_kuozhan', 'pingtai_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 7. 验证订单状态是否为2、8或4
if dingdan_obj.Status not in [2, 8, 4]:
return Response(
{'code': 400, 'message': '订单状态不允许强制结单', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 8. 获取订单的重要字段
jiedan_dashou_id = dingdan_obj.PlayerID # 接单打手ID
dashou_fencheng = dingdan_obj.PlayerCommission # 打手分成金额
fadan_pingtai = dingdan_obj.Platform # 发单平台
# 9. 验证打手分成金额是否存在
if dashou_fencheng is None:
return Response(
{'code': 400, 'message': '订单缺少打手分成金额', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 10. 使用事务保证数据一致性
with transaction.atomic():
# 10.1 更新订单状态为3已完成
dingdan_obj.Status = 3
dingdan_obj.save()
# 10.2 处理打手结算
if jiedan_dashou_id:
try:
# 查询打手用户主表
dashou_user = User.query.select_related('DashouProfile').get(
UserUID=jiedan_dashou_id,
#user_type='PlayerID'
)
# 获取打手扩展表
dashou_profile = dashou_user.DashouProfile
# 更新打手扩展表数据
# ✅ 确认字段chengjiaozongliang 成交单总量
dashou_profile.chengjiaozongliang += 1
# ✅ 确认字段yue 可提现余额
dashou_profile.yue += dashou_fencheng
# ✅ 确认字段zonge 打单赚取总额
dashou_profile.zonge += dashou_fencheng
# ✅ 确认字段jinrishouyi 今日收益金额
dashou_profile.jinrishouyi += dashou_fencheng
# ✅ 确认字段jinyueshouyi 今月收益金额
dashou_profile.jinyueshouyi += dashou_fencheng
dashou_profile.save()
logger.info(f"强制结单成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}")
except User.DoesNotExist:
# 打手用户不存在,记录日志但不影响强制结单
logger.warning(f"强制结单:打手{jiedan_dashou_id}不存在,跳过打手结算")
except AttributeError:
# 打手扩展表不存在,记录日志但不影响强制结单
logger.warning(f"强制结单:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算")
# 10.3 如果是商家发单,还需要更新商家扩展表
if fadan_pingtai == 2:
try:
# 获取商家订单扩展表
shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
shangjia_id = shangjia_kuozhan.MerchantID
if shangjia_id:
# 查询商家用户主表
shangjia_user = User.query.select_related('ShopProfile').get(
UserUID=shangjia_id,
#user_type='shop'
)
# 获取商家扩展表
shangjia_profile = shangjia_user.ShopProfile
# ✅ 确认字段chengjiao 商家成交订单总量
shangjia_profile.chengjiao += 1
shangjia_profile.save()
logger.info(f"强制结单成功,商家{shangjia_id}成交订单+1")
except MerchantOrderExt.DoesNotExist:
# 商家订单扩展表不存在,跳过商家更新
logger.warning(f"强制结单:订单{dingdan_id}商家扩展表不存在,跳过商家更新")
except User.DoesNotExist:
# 商家用户不存在,跳过商家更新
logger.warning(f"强制结单:商家{shangjia_id}不存在,跳过商家更新")
except AttributeError:
# 商家扩展表不存在,跳过商家更新
logger.warning(f"强制结单:商家{shangjia_id}扩展信息不存在,跳过商家更新")
# 11. 返回成功响应
return Response({
'code': 0,
'message': '强制结单成功',
'data': None
})
except Exception as e:
logger.error(f"强制结单接口异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class AdJuJueTuiKuan(APIView):
"""
后台拒绝退款接口
权限: JWT Token认证 + 管理员身份验证
前端调用: POST /dingdan/adddjj
参数: {
"zhanghao": "管理员账号",
"dingdan_id": "订单ID",
"jujue_liyou": "拒绝退款理由(可选)"
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
"""
拒绝退款接口
核心逻辑:
1. 验证管理员身份
2. 拒绝退款后订单状态改为6退款失败
3. 给打手结算分成金额
4. 如果是商家发单,更新商家成交订单数量
5. 更新退款记录表状态改为2驳回
"""
try:
# 1. 获取前端参数
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
jujue_liyou = request.data.get('jujue_liyou', '').strip()
if not qianzhanghao or not dingdan_id:
return Response(
{'code': 400, 'message': '参数不完整', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 2. JWT Token身份验证
dangqianyonghu = request.user
# 3. 验证手机号是否匹配
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 4. 验证用户类型是否为管理员
if dangqianyonghu.UserType != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.AdminProfile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询订单主表
try:
dingdan_obj = Order.query.select_related('shangjia_kuozhan', 'pingtai_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 7. 获取订单的重要字段
jiedan_dashou_id = dingdan_obj.PlayerID # 接单打手ID
dashou_fencheng = dingdan_obj.PlayerCommission # 打手分成金额
fadan_pingtai = dingdan_obj.Platform # 发单平台
# 8. 验证打手分成金额是否存在
if dashou_fencheng is None:
return Response(
{'code': 400, 'message': '订单缺少打手分成金额', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 9. 使用事务保证数据一致性
with transaction.atomic():
# 9.1 更新订单状态为6退款失败
dingdan_obj.Status = 6
dingdan_obj.save()
# 9.2 处理打手结算
if jiedan_dashou_id:
try:
# 查询打手用户主表
dashou_user = User.query.select_related('DashouProfile').get(
UserUID=jiedan_dashou_id,
#user_type='PlayerID'
)
# 获取打手扩展表
dashou_profile = dashou_user.DashouProfile
# 更新打手扩展表数据
# ✅ 确认字段chengjiaozongliang 成交单总量
dashou_profile.chengjiaozongliang += 1
# ✅ 确认字段yue 可提现余额
dashou_profile.yue += dashou_fencheng
# ✅ 确认字段zonge 打单赚取总额
dashou_profile.zonge += dashou_fencheng
# ✅ 确认字段jinrishouyi 今日收益金额
dashou_profile.jinrishouyi += dashou_fencheng
# ✅ 确认字段jinyueshouyi 今月收益金额
dashou_profile.jinyueshouyi += dashou_fencheng
dashou_profile.save()
logger.info(f"拒绝退款成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}")
except User.DoesNotExist:
# 打手用户不存在,记录日志但不影响操作
logger.warning(f"拒绝退款:打手{jiedan_dashou_id}不存在,跳过打手结算")
except AttributeError:
# 打手扩展表不存在,记录日志但不影响操作
logger.warning(f"拒绝退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手结算")
# 9.3 如果是商家发单,还需要更新商家扩展表
if fadan_pingtai == 2:
try:
# 获取商家订单扩展表
shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
shangjia_id = shangjia_kuozhan.MerchantID
if shangjia_id:
# 查询商家用户主表
shangjia_user = User.query.select_related('ShopProfile').get(
UserUID=shangjia_id,
#user_type='shop'
)
# 获取商家扩展表
shangjia_profile = shangjia_user.ShopProfile
# ✅ 确认字段chengjiao 商家成交订单总量
shangjia_profile.chengjiao += 1
shangjia_profile.save()
logger.info(f"拒绝退款成功,商家{shangjia_id}成交订单+1")
except MerchantOrderExt.DoesNotExist:
# 商家订单扩展表不存在,跳过商家更新
logger.warning(f"拒绝退款:订单{dingdan_id}商家扩展表不存在,跳过商家更新")
except User.DoesNotExist:
# 商家用户不存在,跳过商家更新
logger.warning(f"拒绝退款:商家{shangjia_id}不存在,跳过商家更新")
except AttributeError:
# 商家扩展表不存在,跳过商家更新
logger.warning(f"拒绝退款:商家{shangjia_id}扩展信息不存在,跳过商家更新")
# 9.4 更新退款记录表
try:
# 查询退款记录表
tuikuan_jilu = RefundRecord.query.get(OrderID=dingdan_id)
# ✅ 确认字段sqzhuangtai 申请状态改为2驳回
tuikuan_jilu.ApplyStatus = 2
# ✅ 确认字段bhliyou 驳回理由
tuikuan_jilu.RejectReason = jujue_liyou
# ✅ 确认字段chuliid 处理人ID使用管理员账号即手机号
tuikuan_jilu.ProcessorID = qianzhanghao
tuikuan_jilu.save()
logger.info(f"拒绝退款成功,更新退款记录表,订单:{dingdan_id}")
except RefundRecord.DoesNotExist:
# 退款记录表不存在,记录日志但不影响主流程
logger.warning(f"拒绝退款:订单{dingdan_id}退款记录不存在,跳过更新")
# 10. 返回成功响应
return Response({
'code': 0,
'message': '拒绝退款成功',
'data': None
})
except Exception as e:
logger.error(f"拒绝退款接口异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class AdTongYiTuiKuanShangJia(APIView):
"""
后台同意退款接口(商家发单)
权限: JWT Token认证 + 管理员身份验证 + 密码验证
前端调用: POST /dingdan/adddsjtk
参数: {
"zhanghao": "管理员账号",
"dingdan_id": "订单ID",
"tuikuan_liyou": "退款理由(可选)",
"pay_password": "支付密码openid"
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
"""
同意退款接口(商家发单)
核心逻辑:
1. 验证管理员身份
2. 验证支付密码openid
3. 检查订单状态为4退款审核中
4. 更新订单状态为5已退款
5. 更新打手退款订单数量
6. 更新商家退款订单数量和余额(加上订单金额)
7. 更新退款记录表状态改为1成功
"""
try:
# 1. 获取前端参数
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
pay_password = request.data.get('pay_password', '').strip()
if not qianzhanghao or not dingdan_id or not pay_password:
return Response(
{'code': 400, 'message': '参数不完整', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 2. JWT Token身份验证
dangqianyonghu = request.user
# 3. 验证手机号是否匹配
if not hasattr(dangqianyonghu, 'Phone') or str(dangqianyonghu.Phone) != qianzhanghao:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 4. 验证用户类型是否为管理员
if dangqianyonghu.UserType != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.AdminProfile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 验证支付密码对比openid
if pay_password != dangqianyonghu.OpenID:
return Response(
{'code': 401, 'message': '支付密码错误', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 7. 查询订单主表
try:
dingdan_obj = Order.query.select_related('shangjia_kuozhan', 'pingtai_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 8. 验证订单状态为4退款审核中
if dingdan_obj.Status != 4:
return Response(
{'code': 400, 'message': '订单状态不是退款审核中', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 9. 验证订单为商家发单
if dingdan_obj.Platform != 2:
return Response(
{'code': 400, 'message': '此接口只处理商家发单的退款', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 10. 获取订单的重要字段
jiedan_dashou_id = dingdan_obj.PlayerID # 接单打手ID
jine = dingdan_obj.Amount # 订单金额(不是打手分成)
# 11. 验证订单金额是否存在
if jine is None:
return Response(
{'code': 400, 'message': '订单缺少订单金额', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 12. 获取商家ID
try:
shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
shangjia_id = shangjia_kuozhan.MerchantID
except MerchantOrderExt.DoesNotExist:
return Response(
{'code': 400, 'message': '订单商家信息不存在', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 13. 使用事务保证数据一致性
with transaction.atomic():
# 13.1 更新订单状态为5已退款
dingdan_obj.Status = 5
dingdan_obj.save()
# 13.2 更新打手扩展表(退款订单总量+1
if jiedan_dashou_id:
try:
# 查询打手用户主表
dashou_user = User.query.select_related('DashouProfile').get(
UserUID=jiedan_dashou_id,
#user_type='PlayerID'
)
# 获取打手扩展表
dashou_profile = dashou_user.DashouProfile
# ✅ 确认字段tuikuanliang 退款订单总量
dashou_profile.tuikuanliang += 1
dashou_profile.save()
logger.info(f"同意退款成功,打手{jiedan_dashou_id}退款订单+1")
except User.DoesNotExist:
# 打手用户不存在,记录日志但不影响操作
logger.warning(f"同意退款:打手{jiedan_dashou_id}不存在,跳过打手更新")
except AttributeError:
# 打手扩展表不存在,记录日志但不影响操作
logger.warning(f"同意退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手更新")
# 13.3 更新商家扩展表
if shangjia_id:
try:
# 查询商家用户主表
shangjia_user = User.query.select_related('ShopProfile').get(
UserUID=shangjia_id,
#user_type='shop'
)
# 获取商家扩展表
shangjia_profile = shangjia_user.ShopProfile
# ✅ 确认字段tuikuan 退款订单总量
shangjia_profile.tuikuan += 1
from merchant_ops.services.wallet import is_staff_quota_order
if not is_staff_quota_order(dingdan_id):
shangjia_profile.yue += jine
shangjia_profile.save()
logger.info(f"同意退款成功,商家{shangjia_id}退款订单+1余额增加{jine}")
update_shangjia_daily(
yonghuid=shangjia_id,
amount=Decimal(str(jine)),
action=3,
order_id=dingdan_id,
)
except User.DoesNotExist:
# 商家用户不存在,记录日志但不影响操作
logger.warning(f"同意退款:商家{shangjia_id}不存在,跳过商家更新")
except AttributeError:
# 商家扩展表不存在,记录日志但不影响操作
logger.warning(f"同意退款:商家{shangjia_id}扩展信息不存在,跳过商家更新")
# 13.4 更新退款记录表
try:
# 查询退款记录表
tuikuan_jilu = RefundRecord.query.get(OrderID=dingdan_id)
# ✅ 确认字段sqzhuangtai 申请状态改为1成功
tuikuan_jilu.PenaltyApplyStatus = 1
# 可以添加同意理由字段tongyi_liyou
# tuikuan_jilu.tongyi_liyou = tuikuan_liyou
# ✅ 确认字段chuliid 处理人ID使用管理员账号即手机号
tuikuan_jilu.ProcessorID = qianzhanghao
tuikuan_jilu.save()
logger.info(f"同意退款成功,更新退款记录表,订单:{dingdan_id}")
except RefundRecord.DoesNotExist:
# 退款记录表不存在,记录日志但不影响主流程
logger.warning(f"同意退款:订单{dingdan_id}退款记录不存在,跳过更新")
# 14. 返回成功响应
return Response({
'code': 0,
'message': '同意退款成功',
'data': None
})
except Exception as e:
logger.error(f"同意退款接口异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class AdTongYiTuiKuanPingTai(APIView):
"""
后台同意退款接口(平台发单)
权限: JWT Token认证 + 管理员身份验证 + 密码验证
前端调用: POST /dingdan/adddpttk
参数: {
"zhanghao": "管理员账号",
"dingdan_id": "订单ID",
"tuikuan_liyou": "退款理由(可选)",
"pay_password": "支付密码openid"
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
try:
# 1. 获取前端参数
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
tuikuan_liyou = request.data.get('tuikuan_liyou', '').strip()
pay_password = request.data.get('pay_password', '').strip()
if not qianzhanghao or not dingdan_id or not pay_password:
return Response(
{'code': 400, 'message': '参数不完整', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 2. JWT Token身份验证
dangqianyonghu = request.user
# 3. 验证手机号是否匹配
if str(getattr(dangqianyonghu, 'Phone', '')) != qianzhanghao:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 4. 验证用户类型是否为管理员
if dangqianyonghu.UserType != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.AdminProfile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 验证支付密码对比openid
if pay_password != dangqianyonghu.OpenID:
return Response(
{'code': 401, 'message': '支付密码错误', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 7. 查询订单主表(加锁)
with transaction.atomic():
try:
dingdan_obj = Order.query.select_related('pingtai_kuozhan').select_for_update().get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 8. 验证订单状态为1、7、2、8、4允许退款的状态
if dingdan_obj.Status not in [1, 7, 2, 8, 4]:
return Response(
{'code': 400, 'message': '订单状态不允许退款', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 9. 验证订单为平台发单
if dingdan_obj.Platform != 1:
return Response(
{'code': 400, 'message': '此接口只处理平台发单的退款', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 10. 获取订单的重要字段
jiedan_dashou_id = dingdan_obj.PlayerID
jine = dingdan_obj.Amount
if jine is None:
return Response(
{'code': 400, 'message': '订单缺少订单金额', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 11. 获取老板ID从平台订单扩展表
if not hasattr(dingdan_obj, 'pingtai_kuozhan') or not dingdan_obj.pingtai_kuozhan:
return Response(
{'code': 400, 'message': '订单老板信息不存在', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
laoban_id = dingdan_obj.pingtai_kuozhan.BossID
# 12. 调用微信退款接口(优先使用 wechat_transaction_id
transaction_id = getattr(dingdan_obj, 'WechatTransactionID', None)
weixin_refund_result = self.call_weixin_refund(
out_trade_no=dingdan_id,
Amount=jine,
transaction_id=transaction_id
)
if not weixin_refund_result.get('success'):
logger.error(f"微信退款失败,订单:{dingdan_id},错误:{weixin_refund_result.get('msg')}")
return Response(
{'code': 400, 'message': weixin_refund_result.get('msg', '退款失败')},
status=status.HTTP_400_BAD_REQUEST
)
# 13. 更新订单状态为5已退款
dingdan_obj.Status = 5
dingdan_obj.save()
# 14. 更新打手扩展表
if jiedan_dashou_id:
try:
dashou_user = User.query.select_related('DashouProfile').get(UserUID=jiedan_dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.zhuangtai = 1 # 打手状态设为空闲
dashou_profile.tuikuanliang += 1
dashou_profile.save()
except (User.DoesNotExist, AttributeError):
logger.warning(f"平台退款:打手{jiedan_dashou_id}不存在或扩展信息缺失,跳过")
# 15. 更新老板扩展表
if laoban_id:
try:
laoban_user = User.query.select_related('BossProfile').get(UserUID=laoban_id)
laoban_profile = laoban_user.BossProfile
laoban_profile.alltui += 1
laoban_profile.zonge -= jine
if laoban_profile.zonge < 0:
laoban_profile.zonge = 0
laoban_profile.save()
except (User.DoesNotExist, AttributeError):
logger.warning(f"平台退款:老板{laoban_id}不存在或扩展信息缺失,跳过")
# 16. 更新收支记录表(按俱乐部)
try:
from jituan.services.szjilu_accounting import apply_szjilu_expense
update_daily_payout(jine, getattr(dingdan_obj, 'ClubID', None))
apply_szjilu_expense(jine, getattr(dingdan_obj, 'ClubID', None))
except Exception as e:
logger.error(f"更新收支记录表异常: {str(e)}")
# 17. 更新退款记录表
try:
tuikuan_jilu, created = RefundRecord.query.get_or_create(
OrderID=dingdan_id,
defaults={
'PlayerID': jiedan_dashou_id if jiedan_dashou_id else '',
'ApplicantID': laoban_id if laoban_id else '',
'ProcessorID': qianzhanghao,
'Reason': tuikuan_liyou if tuikuan_liyou else '给老板退款',
'ApplyStatus': 1,
'Amount': jine,
'PlayerCommission': dingdan_obj.PlayerCommission or 0,
'Description': dingdan_obj.Description or '',
'Remark': '给老板退款',
'Nickname': dingdan_obj.Nickname or '',
}
)
if not created:
tuikuan_jilu.ApplyStatus = 1
if tuikuan_liyou:
tuikuan_jilu.Reason = tuikuan_liyou
tuikuan_jilu.ProcessorID = qianzhanghao
tuikuan_jilu.save()
except Exception as e:
logger.error(f"更新退款记录表异常: {str(e)}")
logger.info(f"平台退款成功,订单:{dingdan_id},退款单号:{weixin_refund_result.get('refund_id')}")
return Response({'code': 0, 'message': '退款处理成功', 'data': None})
except Exception as e:
logger.error(f"平台退款接口异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def call_weixin_refund(self, out_trade_no, jine, transaction_id=None):
"""
调用微信退款接口
:param out_trade_no: 商户订单号
:param jine: 退款金额
:param transaction_id: 微信支付订单号(优先使用)
:return: {'success': True/False, 'msg': '消息', 'refund_id': '退款单号'}
"""
try:
# 检查配置
required_settings = ['WEIXIN_APPID', 'WEIXIN_MCHID', 'WEIXIN_SHANGHUMIYAO', 'WEIXIN_CERT_PATH', 'WEIXIN_KEY_PATH']
for setting in required_settings:
if not hasattr(settings, setting) or not getattr(settings, setting):
return {'success': False, 'msg': f'缺少微信支付配置: {setting}'}
# 生成退款单号
refund_id = f"TK{int(time.time())}{random.randint(1000, 9999)}"
total_fee_in_fen = yuan_to_fen(jine)
# 构造请求参数
refund_data = {
'appid': settings.WEIXIN_APPID,
'mch_id': settings.WEIXIN_MCHID,
'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(),
'out_refund_no': refund_id,
'total_fee': total_fee_in_fen,
'refund_fee': total_fee_in_fen,
'op_user_id': settings.WEIXIN_MCHID,
'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS'
}
# 优先使用 transaction_id否则用 out_trade_no
if transaction_id:
refund_data['transaction_id'] = transaction_id
else:
refund_data['out_trade_no'] = out_trade_no
# 生成签名
sorted_params = sorted(refund_data.items())
sign_string = '&'.join([f"{k}={v}" for k, v in sorted_params if v])
sign_string += f"&key={settings.WEIXIN_SHANGHUMIYAO}"
sign = hashlib.md5(sign_string.encode()).hexdigest().upper()
refund_data['sign'] = sign
xml_data = self.dict_to_xml(refund_data)
url = "https://api.mch.weixin.qq.com/secapi/pay/refund"
response = requests.post(
url,
data=xml_data.encode('utf-8'),
headers={'Content-Type': 'application/xml'},
cert=(settings.WEIXIN_CERT_PATH, settings.WEIXIN_KEY_PATH),
timeout=30
)
if response.status_code == 200:
result = self.xml_to_dict(response.content)
if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS':
logger.info(f"微信退款成功,订单:{out_trade_no},退款单号:{refund_id}")
return {'success': True, 'msg': '退款申请提交成功', 'refund_id': refund_id}
else:
error_msg = result.get('err_code_des', result.get('return_msg', '退款申请失败'))
logger.error(f"微信退款失败,订单:{out_trade_no},错误:{error_msg}")
return {'success': False, 'msg': f'微信退款失败: {error_msg}'}
else:
logger.error(f"微信退款接口调用失败,状态码:{response.status_code}")
return {'success': False, 'msg': '微信退款接口调用失败'}
except requests.exceptions.Timeout:
logger.error(f"微信退款接口请求超时,订单:{out_trade_no}")
return {'success': False, 'msg': '微信退款接口请求超时'}
except Exception as e:
logger.error(f"调用微信退款接口异常: {str(e)}")
return {'success': False, 'msg': f'退款处理异常: {str(e)}'}
def dict_to_xml(self, dict_data):
xml = ["<xml>"]
for k, v in dict_data.items():
xml.append(f"<{k}>{v}</{k}>")
xml.append("</xml>")
return "".join(xml)
def xml_to_dict(self, xml_data):
try:
root = ET.fromstring(xml_data)
return {child.tag: child.text for child in root}
except ET.ParseError as e:
logger.error(f"XML解析失败: {str(e)}")
return {'return_code': 'FAIL', 'return_msg': 'XML解析失败'}
logger = logging.getLogger(__name__)
class AdJuJueJieSuan(APIView):
"""
后台拒绝结算接口(平台发单)
权限: JWT Token认证 + 管理员身份验证
前端调用: POST /dingdan/adddjjjs
参数: {
"zhanghao": "管理员账号(手机号)",
"dingdan_id": "订单ID",
"tkly": "拒绝结算理由(必填)"
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
try:
# 1. 获取参数
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
tkly = request.data.get('tkly', '').strip()
if not all([qianzhanghao, dingdan_id, tkly]):
return Response(
{'code': 400, 'message': '参数不完整需要zhanghao, dingdan_id, tkly', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 2. JWT用户验证
dangqianyonghu = request.user
# 3. 验证手机号匹配(管理员账号通常为手机号)
if str(dangqianyonghu.Phone) != qianzhanghao:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 4. 验证用户类型为管理员
if dangqianyonghu.UserType != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 查询订单
try:
dingdan = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 6. 校验订单条件平台发单1且状态为2或8
if dingdan.Platform != 1 or dingdan.Status not in [2, 8]:
return Response(
{'code': 400, 'message': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
jiedan_dashou_id = dingdan.PlayerID
if not jiedan_dashou_id:
return Response(
{'code': 400, 'message': '订单暂无接单打手', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
with transaction.atomic():
# 7. 更新订单状态为5已退款并保存拒绝理由
dingdan.Status = 5
dingdan.RefundReason = tkly
dingdan.save()
# 8. 更新打手扩展表
try:
dashou_user = User.query.select_related('DashouProfile').get(UserUID=jiedan_dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.zhuangtai = 1 # 打手状态设为正常
dashou_profile.tuikuanliang += 1 # 退款订单总量+1
dashou_profile.save()
except (User.DoesNotExist, AttributeError) as e:
logger.warning(f"拒绝结算:打手{jiedan_dashou_id}信息更新失败 - {str(e)}")
return Response({'code': 0, 'message': '拒绝结算成功', 'data': None})
except Exception as e:
logger.error(f"拒绝结算接口异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class AdZhuanYiDaTing(APIView):
"""
后台转移大厅接口
权限: JWT Token认证 + 管理员身份验证
前端调用: POST /dingdan/adddzydt
参数: {
"zhanghao": "管理员账号(手机号)",
"dingdan_id": "订单ID"
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
try:
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
if not all([qianzhanghao, dingdan_id]):
return Response(
{'code': 400, 'message': '参数不完整需要zhanghao, dingdan_id', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
dangqianyonghu = request.user
if str(dangqianyonghu.Phone) != qianzhanghao:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
if dangqianyonghu.UserType != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
try:
dingdan = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 校验订单状态允许2、8、4
if dingdan.Status not in [2, 8, 4]:
return Response(
{'code': 400, 'message': '仅状态为进行中(2)、结算中(8)或退款审核(4)的订单可转移大厅', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
jiedan_dashou_id = dingdan.PlayerID
with transaction.atomic():
# 更新订单状态改为1下单中清空接单打手ID
dingdan.Status = 1
dingdan.PlayerID = None
dingdan.save()
# 如果有接单打手,更新其扩展表
if jiedan_dashou_id:
try:
dashou_user = User.query.select_related('DashouProfile').get(UserUID=jiedan_dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.zhuangtai = 1
dashou_profile.tuikuanliang += 1
dashou_profile.save()
except (User.DoesNotExist, AttributeError) as e:
logger.warning(f"转移大厅:打手{jiedan_dashou_id}信息更新失败 - {str(e)}")
return Response({'code': 0, 'message': '转移大厅成功', 'data': None})
except Exception as e:
logger.error(f"转移大厅接口异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class AdQuXiaoZhiDing(APIView):
"""
后台取消指定接口
权限: JWT Token认证 + 管理员身份验证
前端调用: POST /dingdan/adddqxzd
参数: {
"zhanghao": "管理员账号(手机号)",
"dingdan_id": "订单ID"
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
try:
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
if not all([qianzhanghao, dingdan_id]):
return Response(
{'code': 400, 'message': '参数不完整需要zhanghao, dingdan_id', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
dangqianyonghu = request.user
if str(dangqianyonghu.Phone) != qianzhanghao:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
if dangqianyonghu.UserType != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
try:
dingdan = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 校验订单状态必须为7指定中
if dingdan.Status != 7:
return Response(
{'code': 400, 'message': '仅状态为指定中(7)的订单可取消指定', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
with transaction.atomic():
# 更新订单状态改为1下单中清空指定ID
dingdan.Status = 1
dingdan.AssignedID = None
dingdan.save()
return Response({'code': 0, 'message': '取消指定成功', 'data': None})
except Exception as e:
logger.error(f"取消指定接口异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class ZhiDingChaxunView(APIView):
"""
查询当前打手被指定的待接订单
POST /dingdan/zdcx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
dashou_uid = str(request.user.UserUID)
try:
dashou_profile = request.user.DashouProfile
if dashou_profile.zhanghaozhuangtai != 1:
return Response({'code': 403, 'msg': '账号状态异常'}, status=403)
except Exception:
return Response({'code': 403, 'msg': '您不是打手'}, status=403)
orders = Order.query.filter(
Status=7,
AssignedID=dashou_uid,
).filter(
Q(PlayerID__isnull=True) | Q(PlayerID='')
).order_by('-CreateTime')[:20]
result = []
for o in orders:
hf = getattr(o, 'AssignedResponse', None)
result.append({
'dingdan_id': o.OrderID,
'zhuangtai': o.Status,
'pingtai': o.Platform,
'dashou_fencheng': float(o.PlayerCommission or 0),
'leixing_id': o.ProductTypeID or 0,
'tupian': o.ImageURL or '',
'jieshao': o.Description or '',
'beizhu': o.Remark or '',
'creat_time': fmt_datetime(o.CreateTime),
'create_time': fmt_datetime(o.CreateTime),
'zhiding_uid': o.AssignedID or '',
'zhiding_hf': hf,
'zhiding_hf_text': ZHIDING_HF_TEXT.get(hf, ''),
'zhiding_dhj_sm': getattr(o, 'AssignedLaterNote', '') or '',
'zhiding_hf_sj': fmt_datetime(o.AssignedResponseAt) if getattr(o, 'AssignedResponseAt', None) else '',
'yaoqiuleixing': o.GrabRequirement or 0,
'huiyuan_id': o.MembershipID or '',
'yajin': float(o.CommissionReq or 0),
})
return Response({
'code': 200,
'msg': '获取成功',
'data': {'list': result, 'count': len(result)},
})
except Exception as e:
logger.error(f"指定订单查询失败: {e}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
class ZhiDingHuiFuView(APIView):
"""
指定单打手响应:想接 / 不想接 / 等会接
POST /dingdan/zdhf
参数: dingdan_id, huifu (1|2|3), deng_hui_note (等会接说明,可选)
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
data = request.data
dingdan_id = str(data.get('dingdan_id') or data.get('OrderID') or '').strip()
huifu = int(data.get('huifu') or 0)
deng_hui_note = str(data.get('deng_hui_note') or '').strip()[:100]
if not dingdan_id:
return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400)
if huifu not in (1, 2, 3):
return Response({'code': 400, 'msg': '无效的响应类型'}, status=400)
if huifu == 3 and not deng_hui_note:
return Response({'code': 400, 'msg': '请填写预计接待时间'}, status=400)
dashou_uid = str(request.user.UserUID)
with transaction.atomic():
try:
order = Order.query.select_for_update().get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=404)
if order.Status != 7:
return Response({'code': 400, 'msg': '订单已不是指定中状态'}, status=400)
if str(order.AssignedID or '') != dashou_uid:
return Response({'code': 403, 'msg': '您不是该订单的指定打手'}, status=403)
if order.PlayerID:
return Response({'code': 400, 'msg': '订单已被接单'}, status=400)
now = timezone.now()
order.AssignedResponse = huifu
order.AssignedResponseAt = now
if huifu == 2:
order.Status = 1
order.AssignedID = None
order.AssignedLaterNote = ''
elif huifu == 3:
order.AssignedLaterNote = deng_hui_note
else:
order.AssignedLaterNote = ''
order.save()
msg_map = {1: '已标记想接,请点击接待完成接单', 2: '已婉拒,订单已退回大厅', 3: '已记录,请按时接待'}
return Response({
'code': 200,
'msg': msg_map.get(huifu, '操作成功'),
'data': {
'dingdan_id': dingdan_id,
'huifu': huifu,
'huifu_text': ZHIDING_HF_TEXT.get(huifu, ''),
},
})
except Exception as e:
logger.error(f"指定订单响应失败: {e}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'}, status=500)
class ZxsjghdsView(APIView):
"""
商家更换打手接口
路径POST /dingdan/zxsjghds
权限仅限已认证商家通过JWT
功能:
1. 将原订单状态改为已退款(5),释放接单打手(若有)
2. 更新打手统计(退款量、状态、每日统计)
3. 写入退款记录
4. 若为跨平台订单,通知对方平台退款(必须成功)
5. 基于原订单生成新订单状态直接为1复制核心数据
6. 不操作商家余额、退款次数等(替换订单)
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# ========== 1. 获取当前商家用户 ==========
current_user = request.user
try:
yonghuid = current_user.UserUID
except AttributeError:
return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# ========== 2. 提取订单ID ==========
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
if not dingdan_id:
return Response({'code': 2, 'msg': '订单ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 3. 查询订单并校验权限 ==========
try:
# 关联查询商家扩展表,确保当前商家是该订单的发布者
shangjia_kuozhan = MerchantOrderExt.query.select_related('Order').get(
Order__OrderID=dingdan_id,
MerchantID=yonghuid
)
except MerchantOrderExt.DoesNotExist:
return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN)
old_order = shangjia_kuozhan.Order
# ========== 4. 状态校验(仅允许进行中或结算中) ==========
if old_order.Status not in [2, 8]:
return Response({'code': 4, 'msg': '当前订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 5. 收集订单关键数据(用于新订单复制) ==========
# 这些字段将在退款处理后用于创建新订单
jine = old_order.Amount
dashou_fencheng = old_order.PlayerCommission
leixing_id = old_order.ProductTypeID
yaoqiuleixing = old_order.GrabRequirement
huiyuan_id = old_order.MembershipID
yongjin = old_order.CommissionReq
jieshao = old_order.Description
nicheng = old_order.Nickname
beizhu = old_order.Remark
waibu_dingdan_id = old_order.ExternalOrderID
user1_id = old_order.User1ID
tupian = old_order.ImageURL
fadan_pingtai = old_order.Platform
dashou_id = old_order.PlayerID # 可能为空
# ========== 6. 退款处理事务块1退款原订单 + 打手统计 + 跨平台通知) ==========
try:
with transaction.atomic():
# 6.1 订单状态更新为已退款
old_order.Status = 5
old_order.save(update_fields=['Status'])
# 6.2 释放打手
if dashou_id:
try:
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1
dashou_profile.zhuangtai = 1 # 空闲
dashou_profile.save(update_fields=['tuikuanliang', 'zhuangtai'])
except (User.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"打手 {dashou_id} 不存在或扩展表缺失,跳过更新")
# 6.3 更新退款记录
RefundRecord.query.update_or_create(
OrderID=dingdan_id,
defaults={
'PlayerID': dashou_id or '',
'ApplicantID': yonghuid, # 申请人为当前商家
'ProcessorID': yonghuid, # 处理人也是商家自己
'Reason': '商家更换打手',
'ApplyStatus': 1, # 状态 1=成功
'Amount': jine or 0,
'PlayerCommission': dashou_fencheng or 0,
'Description': jieshao or '',
'Remark': '更换打手系统自动退款',
'Nickname': nicheng or '',
}
)
# 6.4 打手每日统计(退款行为)
if dashou_id:
try:
update_dashou_daily_by_action(
yonghuid=dashou_id,
amount=dashou_fencheng,
action=3 # 3=退款
)
except Exception as e:
logger.error(f"更新打手每日统计失败: {e}")
# 统计失败不应阻断主流程,但可记录
except Exception as e:
logger.error(f"退款处理事务失败: {e}", exc_info=True)
return Response({'code': 500, 'msg': '退款处理失败'})
# ========== 7. 生成新订单事务块2 ==========
try:
with transaction.atomic():
# 7.1 生成新的订单ID
def generate_dingdan_id():
timestamp = int(time.time() * 1000)
process_id = os.getpid() % 1000
random_num = random.randint(1000, 9999)
return f"SJ{timestamp}{process_id:03d}{random_num}"
new_dingdan_id = generate_dingdan_id()
# 确保唯一
while Order.query.filter(OrderID=new_dingdan_id).exists():
new_dingdan_id = generate_dingdan_id()
# 7.2 创建新订单(复制数据,状态为 1
new_order = Order.query.create(
OrderID=new_dingdan_id,
Status=1, # 已下单
Platform=fadan_pingtai,
Amount=jine,
PlayerCommission=dashou_fencheng,
ProductTypeID=leixing_id,
GrabRequirement=yaoqiuleixing,
MembershipID=huiyuan_id,
CommissionReq=yongjin,
Description=jieshao,
Nickname=nicheng,
Remark=beizhu,
User1ID=user1_id, # 商家标识
ImageURL=tupian,
ExternalOrderID=waibu_dingdan_id,
ClubID=getattr(old_order, 'ClubID', None) or 'xq',
# 不继承zhiding_id, jiedan_dashou_id, dashou_liuyan 等
)
# 7.3 创建商家扩展表(关联新订单)
MerchantOrderExt.query.create(
Order=new_order,
MerchantID=yonghuid,
MerchantNickname=shangjia_kuozhan.MerchantNickname or '',
)
# 7.4 异步通知打手(广播新订单)
try:
order_info = {
'OrderID': new_dingdan_id,
'game_type': self._get_game_type_name(leixing_id),
'amount': str(jine),
'order_desc': (jieshao or '')[:50],
}
dingdan_guangbo.delay(order_info)
except Exception as e:
logger.error(f"广播通知失败: {e}")
# 注意:此处不更新商家余额、发布量等统计,因为是替换订单
logger.info(f"商家{yonghuid}更换打手成功,旧订单{dingdan_id}已退款,新订单{new_dingdan_id}")
return Response({
'code': 0,
'msg': '更换成功',
'data': {
'xin_dingdan_id': new_dingdan_id,
}
})
except Exception as e:
logger.error(f"生成新订单事务失败: {e}", exc_info=True)
return Response({'code': 500, 'msg': '生成新订单失败'})
def _get_game_type_name(self, leixing_id):
"""根据类型ID获取游戏类型名称"""
try:
gt = ShangpinLeixing.query.filter(id=leixing_id).first()
return gt.jieshao if gt else '游戏订单'
except Exception:
return '游戏订单'