"""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 ) from merchant_ops.constants import ACTION_CHANGE_PLAYER from merchant_ops.services.action_log import record_order_action_from_request 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存储的是用户ID(yonghuid) ) 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: from jituan.services.pay_refund_router import try_fubei_refund from jituan.services.wechat_pay import club_id_from_order fb = try_fubei_refund( out_trade_no=out_trade_no, amount_yuan=jine, club_id=club_id_from_order(out_trade_no), ) if fb is not None: if fb.get('code') == 0: return {'success': True, 'msg': fb.get('msg') or 'success', 'refund_id': fb.get('refund_id')} return {'success': False, 'msg': fb.get('msg') or '付呗退款失败'} # 检查配置 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 = [""] for k, v in dict_data.items(): xml.append(f"<{k}>{v}") xml.append("") 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}") record_order_action_from_request( request, ACTION_CHANGE_PLAYER, dingdan_id, merchant_id=yonghuid, amount=jine, leixing_id=leixing_id, remark=f'新订单:{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 '游戏订单'