# ==================== 标准库 ==================== 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.chat_utils import _send_group_message, _subscribe_users_to_group, establish_order_chat from utils.fadan_utils import check_fadan_qiangdan_eligible from orders.utils import ( calc_shangjia_order_fencheng, update_daily_income, 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, PenaltyBonus, 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_penalty import resolve_penalty_club_id from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai from users.models import UserDashou, UserShangjia, UserBoss from users.business_models import User from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao from config.models import ( ShangjiaLianjie ) logger = logging.getLogger(__name__) class PaymentFailView(APIView): """ 前端支付失败接口 - 处理用户主动取消或支付失败 只允许删除状态为9(未支付)的订单 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}) with transaction.atomic(): # 锁定订单防止并发 dingdan = Order.query.select_for_update().filter(OrderID=dingdanid).first() if not dingdan: return Response({'code': 2, 'msg': '订单不存在', 'data': None}) if dingdan.User1ID != f"Boss{request.user.UserUID}": return Response({'code': 3, 'msg': '无权操作此订单', 'data': None}) # 只允许删除未支付订单(状态9) if dingdan.Status != 9: return Response({'code': 3, 'msg': '只能删除未支付的订单', 'data': None}) # 删除扩展表和主表 PlatformOrderExt.query.filter(Order=dingdan).delete() dingdan.delete() return Response({'code': 0, 'msg': '订单已删除', 'data': None}) except Exception as e: return Response({'code': 99, 'msg': f'删除订单失败: {str(e)}', 'data': None}) class WechatPayNotifyView(APIView): """微信支付回调接口 - 接收微信官方支付结果通知(含详细调试日志)""" throttle_classes = [AnonRateThrottle] permission_classes = [AllowAny] def post(self, request): # 记录原始请求数据(调试用) xml_data = request.body.decode('utf-8') # 最外层异常捕获,确保任何错误都能被记录并返回适当响应 try: # 1. 解析 XML try: root = ET.fromstring(xml_data) except ET.ParseError as e: logger.error(f"XML解析失败: {e}") return self._gen_response_xml('FAIL', 'XML解析失败') # 2. 获取关键参数 return_code = root.find('return_code').text if root.find('return_code') is not None else '' result_code = root.find('result_code').text if root.find('result_code') is not None else '' out_trade_no = root.find('out_trade_no').text if root.find('out_trade_no') is not None else '' transaction_id = root.find('transaction_id').text if root.find('transaction_id') is not None else '' from jituan.services.wechat_pay import club_id_from_order, verify_wechat_v2_signature notify_club_id = club_id_from_order(out_trade_no) # 3. 签名验证 if not verify_wechat_v2_signature(xml_data, notify_club_id): logger.warning("签名验证失败") return self._gen_response_xml('FAIL', '签名验证失败') # 4. 根据支付结果处理 if return_code == 'SUCCESS' and result_code == 'SUCCESS': self._handle_success(out_trade_no, transaction_id, root) else: logger.info(f"支付失败或异常,return_code={return_code}, result_code={result_code}") self._handle_failure(out_trade_no) # 返回微信要求的成功响应(必须返回SUCCESS,即使内部处理失败也要返回,否则微信会重复通知) return self._gen_response_xml('SUCCESS', 'OK') except Exception as e: # 捕获任何未处理的异常,记录完整堆栈 logger.error(f"回调处理发生未捕获异常: {str(e)}") logger.error(traceback.format_exc()) return self._gen_response_xml('FAIL', f'服务器内部错误: {str(e)}') def _verify_signature(self, xml_data): """微信支付签名验证(MD5)""" try: root = ET.fromstring(xml_data) sign = root.find('sign').text # 构建待签名字符串(排除sign字段) data = {child.tag: child.text for child in root if child.tag != 'sign'} # 按key排序 sorted_keys = sorted(data.keys()) stringA = '&'.join([f"{k}={data[k]}" for k in sorted_keys]) stringSignTemp = f"{stringA}&key={settings.WEIXIN_SHANGHUMIYAO}" calc_sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() logger.debug(f"计算签名: {calc_sign}, 微信签名: {sign}") return calc_sign == sign except Exception as e: logger.error(f"签名验证过程异常: {e}") logger.error(traceback.format_exc()) return False def _gen_response_xml(self, return_code, return_msg): """生成返回给微信的XML响应""" xml = f""" """ logger.info(f"返回给微信的响应: {xml}") return HttpResponse(xml, content_type='application/xml') @transaction.atomic def _handle_success(self, out_trade_no, transaction_id, root): """处理支付成功""" logger.info(f"进入 _handle_success, out_trade_no={out_trade_no}, transaction_id={transaction_id}") # 锁定订单防止并发 try: dingdan = Order.query.select_for_update().filter(OrderID=out_trade_no).first() except Exception as e: logger.error(f"查询订单异常: {e}") raise if not dingdan: logger.error(f"订单不存在: {out_trade_no}") return logger.info(f"订单当前状态: {dingdan.Status}") # 幂等处理:只有状态为9(未支付)才处理,已处理则跳过 if dingdan.Status != 9: logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.Status},跳过") return # 保存微信交易号 if transaction_id: dingdan.WechatTransactionID = transaction_id logger.info(f"保存微信交易号: {transaction_id}") else: logger.warning("微信未返回transaction_id") # 更新订单状态 if dingdan.AssignedID and dingdan.AssignedID.strip(): dingdan.Status = 7 logger.info("指定单,状态设为7") else: dingdan.Status = 1 logger.info("普通单,状态设为1") dingdan.save() logger.info(f"订单状态已更新为 {dingdan.Status}") # 删除同一用户同一商品的其他未支付订单(改为逻辑删除,状态25) try: abandoned_count = Order.query.filter( User1ID=dingdan.User1ID, ProductID=dingdan.ProductID, Status=9 ).exclude(OrderID=out_trade_no).update(Status=25) logger.info(f"将 {abandoned_count} 个未支付订单标记为已废弃(25)") except Exception as e: logger.error(f"标记其他未支付订单异常: {e}") # 更新老板扩展表 try: laoban_id = None if hasattr(dingdan, 'pingtai_kuozhan'): laoban_id = dingdan.pingtai_kuozhan.BossID if laoban_id: user_main = User.query.filter(UserUID=laoban_id).first() if user_main and hasattr(user_main, 'BossProfile'): boss = user_main.BossProfile boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0) boss.alldingdan = (boss.alldingdan or 0) + 1 boss.save() logger.info(f"更新老板 {laoban_id} 数据成功") except Exception as e: logger.error(f"更新老板数据异常: {e}") # 更新商品库存 try: if dingdan.ProductID: Shangpin.query.filter(id=dingdan.ProductID).update( kucun=F('kucun') - 1 ) logger.info(f"商品 {dingdan.ProductID} 库存减1") except Exception as e: logger.error(f"更新商品库存异常: {e}") # ========== 新增:商品和店铺每日统计 ========== try: # 商品每日统计(只要有商品ID就执行) if dingdan.ProductID: # 从扩展表获取已计算好的收益值(下单时已存储) pingtai_shouyi = Decimal('0.00') dianpu_shouyi = Decimal('0.00') if hasattr(dingdan, 'pingtai_kuozhan'): pingtai_shouyi = dingdan.pingtai_kuozhan.PlatformIncome or Decimal('0.00') dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_shangpin_daily_stat( ProductID=dingdan.ProductID, caozuo_leixing=1, # 1 = 下单 dingdan_jiage=dingdan.Amount, PlatformIncome=pingtai_shouyi, dianpu_zongshouyi=dianpu_shouyi ) logger.info(f"商品统计更新完成,商品ID: {dingdan.ProductID}") # 店铺每日统计(仅当存在店铺ID时执行) dianpu_id = None if hasattr(dingdan, 'pingtai_kuozhan'): dianpu_id = dingdan.pingtai_kuozhan.ShopID if dianpu_id: dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_dianpu_daily_stat( ShopID=dianpu_id, caozuo_leixing=1, # 1 = 下单 dingdan_jiage=dingdan.Amount, ShopIncome=dianpu_shouyi ) logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") except Exception as e: logger.error(f"商品/店铺统计更新失败: {e}") logger.error(traceback.format_exc()) # 统计失败不影响主流程 # ============================================= # 更新收支记录(总累计) self._update_szjilu(dingdan.Amount, getattr(dingdan, 'ClubID', None)) # 更新每日收入统计 update_daily_income(dingdan.Amount, getattr(dingdan, 'ClubID', None)) # 构建通知数据(你的 _get_game_type_name 方法保留在视图里直接用) order_info = { 'OrderID': dingdan.OrderID, 'game_type': self._get_game_type_name(dingdan.ProductTypeID), 'amount': str(dingdan.Amount), 'order_desc': (dingdan.Description or '')[:50], } dingdan_guangbo.delay(order_info) # 瞬间返回,不阻塞请求 # 异步广播通知 #self._broadcast_async(dingdan) logger.info(f"订单 {out_trade_no} 支付成功处理完成") @transaction.atomic def _handle_failure(self, out_trade_no): """处理支付失败""" logger.info(f"进入 _handle_failure, out_trade_no={out_trade_no}") try: dingdan = Order.query.select_for_update().filter(OrderID=out_trade_no).first() if not dingdan: logger.warning(f"订单不存在: {out_trade_no}") return logger.info(f"订单当前状态: {dingdan.Status}") # 仅当订单状态为9(未支付)时才标记失败,不直接删除 if dingdan.Status == 9: dingdan.Status = 14 # 自定义支付失败状态 dingdan.save() logger.info(f"订单 {out_trade_no} 支付失败,标记为14") else: logger.info(f"订单 {out_trade_no} 状态非9,不处理") except Exception as e: logger.error(f"处理支付失败异常: {e}") logger.error(traceback.format_exc()) def _update_szjilu(self, jine, club_id=None): """更新收支记录表(按俱乐部)""" from jituan.services.szjilu_accounting import apply_szjilu_income try: apply_szjilu_income(jine, club_id) logger.info(f"收支记录更新成功,增加金额: {jine}, club={club_id}") except Exception as e: logger.error(f"更新收支记录失败: {e}") def _broadcast_async(self, dingdan): """异步广播通知""" try: def task(): try: order_info = { 'OrderID': dingdan.OrderID, 'game_type': self._get_game_type_name(dingdan.ProductTypeID), 'amount': str(dingdan.Amount), 'order_desc': (dingdan.Description or '')[:50], } logger.info(f"准备广播订单: {order_info}") sender = WeixinBroadcastSender() result = sender.broadcast_order(order_info) if result.get('success'): logger.info(f"订单广播完成: {result.get('msg')}") else: logger.warning(f"订单广播异常: {result.get('msg')}") except Exception as e: logger.error(f"广播任务异常: {e}") logger.error(traceback.format_exc()) thread = threading.Thread(target=task) thread.daemon = True thread.start() logger.info("广播线程已启动") except Exception as e: logger.error(f"启动广播线程异常: {e}") def _get_game_type_name(self, leixing_id): try: gt = ShangpinLeixing.query.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception as e: logger.error(f"获取游戏类型名称异常: {e}") return '游戏订单' class PaymentVerifyView(APIView): """ 订单支付成功复查接口 - 前端支付成功后调用,确认订单状态 当订单状态为9时,主动查询微信支付状态,若已支付则更新本地状态 需要token认证 """ permission_classes = [permissions.IsAuthenticated] parser_classes = [JSONParser] def post(self, request): try: # ---------- 1. 获取参数 ---------- dingdanid = request.data.get('dingdanid') if not dingdanid: return Response({'code': 1, 'msg': '订单ID不能为空'}) # ---------- 2. 查询订单 ---------- dingdan = Order.query.filter(OrderID=dingdanid).first() if not dingdan: return Response({'code': 2, 'msg': '订单不存在'}) # ---------- 3. 验证订单归属(安全) ---------- current_user = request.user user_identifier = f"Boss{current_user.UserUID}" if dingdan.User1ID != user_identifier: logger.warning(f"用户 {current_user.UserUID} 尝试复查他人订单 {dingdanid}") return Response({'code': 403, 'msg': '无权访问'}) # ---------- 4. 如果订单状态已是1/7,直接返回成功 ---------- if dingdan.Status in [1, 7]: return Response({ 'code': 0, 'msg': '订单状态正常', 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.Status} }) # ---------- 5. 如果状态为9,则主动查询微信支付状态 ---------- if dingdan.Status == 9: # 使用分布式锁防止并发处理同一订单 lock_key = f"payment_verify_lock_{dingdanid}" if not cache.add(lock_key, "locked", timeout=10): # 已有其他进程在处理,让前端稍后重试 return Response({'code': 1, 'msg': '订单正在处理中,请稍后'}) try: # 调用微信订单查询接口 trade_state, transaction_id = self._query_wechat_payment(dingdanid) if trade_state == 'SUCCESS': # 支付成功,执行本地更新 self._process_payment_success(dingdan, transaction_id, current_user.UserUID) # 重新获取订单以获取最新状态 dingdan.refresh_from_db() return Response({ 'code': 0, 'msg': '支付成功', 'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.Status} }) elif trade_state in ['NOTPAY', 'USERPAYING']: # 用户尚未支付或支付中 return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}}) else: # 其他状态:CLOSED, REVOKED, PAYERROR 等,视为支付失败 return Response({'code': 2, 'msg': '支付失败'}) except Exception as e: logger.error(f"查询微信支付异常: {e}", exc_info=True) return Response({'code': 1, 'msg': '查询失败,请稍后重试'}) finally: cache.delete(lock_key) else: # ---------- 6. 其他异常状态 ---------- status_msg = { 2: '订单已取消', 3: '订单已完成', 4: '退款审核中', 5: '已退款', 6: '退款失败', 8: '结算中', 0: '订单已作废', }.get(dingdan.Status, '订单状态异常') return Response({'code': 3, 'msg': status_msg, 'data': {'zhuangtai': dingdan.Status}}) except Exception as e: logger.error(f"订单复查接口异常: {e}", exc_info=True) return Response({'code': 99, 'msg': f'系统错误: {str(e)}'}) def _process_payment_success(self, dingdan, transaction_id, operator_id): """ 支付成功后的原子操作(与微信回调逻辑完全一致) 在事务内完成所有状态更新和数据一致性操作 """ with transaction.atomic(): # 重新锁定订单,防止并发更新 dingdan = Order.query.select_for_update().get(pk=dingdan.pk) if dingdan.Status != 9: # 订单状态已被其他进程更新,直接返回(幂等) return # 更新订单状态 dingdan.Status = 7 if dingdan.AssignedID and dingdan.AssignedID.strip() else 1 dingdan.WechatTransactionID = transaction_id dingdan.AssignedCS = operator_id # 可选:记录操作人ID dingdan.save() # 删除同一用户同一商品的其他未支付订单(逻辑删除,状态25) Order.query.filter( User1ID=dingdan.User1ID, ProductID=dingdan.ProductID, Status=9 ).exclude(OrderID=dingdan.OrderID).update(Status=25) # 更新老板扩展表 laoban_id = dingdan.pingtai_kuozhan.BossID if hasattr(dingdan, 'pingtai_kuozhan') else None if laoban_id: user_main = User.query.filter(UserUID=laoban_id).first() if user_main and hasattr(user_main, 'BossProfile'): boss = user_main.BossProfile boss.zonge = (boss.zonge or 0) + (dingdan.Amount or 0) boss.alldingdan = (boss.alldingdan or 0) + 1 boss.save() # 更新商品库存 if dingdan.ProductID: Shangpin.query.filter(id=dingdan.ProductID).update( kucun=F('kucun') - 1 ) # ========== 新增:商品和店铺每日统计 ========== try: # 商品每日统计(只要有商品ID就执行) if dingdan.ProductID: # 从扩展表获取已计算好的收益值(下单时已存储) pingtai_shouyi = Decimal('0.00') dianpu_shouyi = Decimal('0.00') if hasattr(dingdan, 'pingtai_kuozhan'): pingtai_shouyi = dingdan.pingtai_kuozhan.PlatformIncome or Decimal('0.00') dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_shangpin_daily_stat( ProductID=dingdan.ProductID, caozuo_leixing=1, # 1 = 下单 dingdan_jiage=dingdan.Amount, PlatformIncome=pingtai_shouyi, dianpu_zongshouyi=dianpu_shouyi ) logger.info(f"商品统计更新完成,商品ID: {dingdan.ProductID}") # 店铺每日统计(仅当存在店铺ID时执行) dianpu_id = None if hasattr(dingdan, 'pingtai_kuozhan'): dianpu_id = dingdan.pingtai_kuozhan.ShopID if dianpu_id: dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_dianpu_daily_stat( ShopID=dianpu_id, caozuo_leixing=1, # 1 = 下单 dingdan_jiage=dingdan.Amount, ShopIncome=dianpu_shouyi ) logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}") except Exception as e: logger.error(f"商品/店铺统计更新失败: {e}") logger.error(traceback.format_exc()) # 统计失败不影响主流程 # ============================================= # 更新收支记录 self._update_szjilu(dingdan.Amount, getattr(dingdan, 'ClubID', None)) # 更新每日收入统计 update_daily_income(dingdan.Amount, getattr(dingdan, 'ClubID', None)) def _query_wechat_payment(self, out_trade_no): """ 调用微信支付订单查询接口,返回 (trade_state, transaction_id) 若查询失败,抛出异常 """ appid = settings.WEIXIN_APPID mch_id = settings.WEIXIN_MCHID key = settings.WEIXIN_SHANGHUMIYAO nonce_str = self._generate_nonce_str() params = { 'appid': appid, 'mch_id': mch_id, 'out_trade_no': out_trade_no, 'nonce_str': nonce_str, } # 生成签名 sorted_keys = sorted(params.keys()) stringA = '&'.join([f"{k}={params[k]}" for k in sorted_keys]) stringSignTemp = f"{stringA}&key={key}" sign = hashlib.md5(stringSignTemp.encode('utf-8')).hexdigest().upper() params['sign'] = sign xml_data = self._dict_to_xml(params) url = 'https://api.mch.weixin.qq.com/pay/orderquery' response = requests.post(url, data=xml_data, timeout=10) result = xmltodict.parse(response.content)['xml'] if result.get('return_code') == 'SUCCESS' and result.get('result_code') == 'SUCCESS': return result.get('trade_state'), result.get('transaction_id') else: err_msg = result.get('return_msg', '未知错误') raise Exception(f"微信订单查询失败: {err_msg}") def _update_szjilu(self, jine, club_id=None): """更新收支记录表(按俱乐部)""" from jituan.services.szjilu_accounting import apply_szjilu_income try: apply_szjilu_income(jine, club_id) logger.info(f"收支记录更新成功,增加金额: {jine}, club={club_id}") except Exception as e: logger.error(f"更新收支记录失败: {e}") # 不抛出异常,因为收支记录不是核心流程,但记录错误供排查 def _generate_nonce_str(self): """生成随机字符串,长度32""" return ''.join(random.choices(string.ascii_letters + string.digits, k=32)) def _dict_to_xml(self, data): """将字典转换为XML字符串""" xml = [''] for k, v in data.items(): xml.append(f'<{k}>{v}') xml.append('') return ''.join(xml) class CreateOrderView(APIView): """ 创建订单并调用微信支付接口 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): try: # 1. 获取前端传递的字段 shangpin_id = request.data.get('shangpin_id') nicheng = request.data.get('nicheng', '').strip() beizhu = request.data.get('Remark', '').strip() zhiding = request.data.get('zhiding', '').strip() jine = request.data.get('jine', 0) # 2. 验证必填字段 if not shangpin_id: return Response({'code': 1, 'msg': '商品ID不能为空', 'data': None}) if not nicheng: return Response({'code': 2, 'msg': '游戏昵称不能为空', 'data': None}) if len(nicheng) > 50: return Response({'code': 3, 'msg': '游戏昵称过长', 'data': None}) # 3. 查询商品信息 shangpin = Shangpin.query.filter(id=shangpin_id).first() if not shangpin: return Response({'code': 4, 'msg': '商品不存在', 'data': None}) is_valid, err_msg, _ = validate_shangpin_and_dianpu(shangpin_id) if not is_valid: return Response({'code': 5, 'msg': err_msg, 'data': None}) # 检查库存 if hasattr(shangpin, 'kucun') and shangpin.kucun <= 0: return Response({'code': 5, 'msg': '商品库存不足', 'data': None}) # 获取商品价格 shangpin_jiage = getattr(shangpin, 'jiage', 0) if not shangpin_jiage: return Response({'code': 6, 'msg': '商品价格异常', 'data': None}) #获取商品图片 shangpin_tupian = getattr(shangpin, 'tupian_url', None) # 4. 验证指定打手(如果存在) zhiding_id_value = None if zhiding: # 查询主表中是否存在该打手 zhiding_user = User.query.filter( UserUID=zhiding, #user_type='PlayerID' ).first() if not zhiding_user: return Response({'code': 7, 'msg': '指定打手不存在', 'data': None}) # 查询打手扩展表 dashou_profile = UserDashou.query.filter(user=zhiding_user).first() if not dashou_profile: return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None}) # 检查在线状态 if getattr(dashou_profile, 'zhanghaozhuangtai', 0) == 0: return Response({'code': 8, 'msg': '指定用户不存在或已封禁', 'data': None}) zhiding_id_value = zhiding # 5. 生成订单ID timestamp = int(time.time()) timestamp_ns = int(time.time_ns() // 1000) % 1000000 random_num = random.randint(0, 99) dingdanid = f"PT{timestamp:011d}{timestamp_ns:06d}{random_num:02d}" # 6. 查询利率并计算分成 club_id = resolve_club_id_from_request(request) dashou_lilu = get_commission_rate_object(club_id, '1') # 🔥 新增:判断是否使用商品配置的额外分成 if shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None: # 使用商品指定的额外分成金额 dashou_fencheng = shangpin.ewai_dashou_fencheng else: # 按原有逻辑计算打手分成 dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.Rate)) # 查询管事利率(字符'2') #guanshi_lilu = CommissionRate.query.filter(Platform='2').first() if not (shangpin.kaioi_ewai_dashou_fencheng and shangpin.ewai_dashou_fencheng is not None): if get_commission_rate(club_id, '1') <= 0 and not CommissionRate.query.filter(Platform='1').exists(): return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None}) # 开始数据库事务 with transaction.atomic(): # 获取当前用户信息 current_user = request.user laoban_yonghuid = getattr(current_user, 'UserUID', '') or getattr(current_user, 'yonghuid', '') user_openid = getattr(current_user, 'OpenID', '') or getattr(current_user, 'openid', '') if not user_openid: return Response({'code': 10, 'msg': '用户openid不存在', 'data': None}) # 创建订单主表数据 order_data = { 'OrderID': dingdanid, 'Status': 9, # 未付款状态 'Amount': shangpin_jiage, 'Platform':1, 'PlayerCommission': dashou_fencheng, 'AssignedID': zhiding_id_value, 'ProductID': shangpin_id, 'GrabRequirement': getattr(shangpin, 'yaoqiuleixing', None), 'MembershipID': getattr(shangpin, 'huiyuan_id', ''), 'CommissionReq': getattr(shangpin, 'yongjin', 0), 'Description': getattr(shangpin, 'jieshao', ''), 'Remark': beizhu, 'Nickname': nicheng, 'ImageURL':shangpin_tupian, 'User1ID':f"Boss{request.user.UserUID}", 'ProductTypeID':shangpin.leixing_id, 'ClubID': getattr(request, 'club_id', None) or 'xq', } # 创建订单记录 dingdan = Order.query.create(**order_data) # CreateOrderView.post 事务内部,替换原有的扩展表创建代码 dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong = calculate_pingtai_and_dianpu_shouyi( shangpin_id=shangpin_id, dingdan_jiage=shangpin_jiage, dashou_fencheng=dashou_fencheng ) pingtaikuozhan_data = { 'Order': dingdan, 'BossID': laoban_yonghuid, 'PlatformIncome': pingtai_shouyi, # ---------- 新增上级分红快照 ---------- 'ParentShopID': shangji_dianpu_id, 'ParentBonus': shangji_fenhong if shangji_fenhong else Decimal('0.00'), } if dianpu_id: pingtaikuozhan_data['ShopID'] = dianpu_id pingtaikuozhan_data['ShopIncome'] = dianpu_shouyi else: pingtaikuozhan_data['ShopIncome'] = Decimal('0.00') PlatformOrderExt.query.create(**pingtaikuozhan_data) # 计算收益 '''dianpu_id, pingtai_shouyi, dianpu_shouyi = calculate_pingtai_and_dianpu_shouyi( ProductID=shangpin_id, dingdan_jiage=shangpin_jiage, PlayerCommission=dashou_fencheng ) # 构建扩展表数据,平台收益必定保存,店铺收益和店铺ID仅当dianpu_id存在时才赋值 pingtaikuozhan_data = { 'dingdan': dingdan, 'laoban_id': laoban_yonghuid, 'pingtai_shouyi': pingtai_shouyi, } if dianpu_id: pingtaikuozhan_data['dianpu_id'] = dianpu_id pingtaikuozhan_data['dianpu_shouyi'] = dianpu_shouyi else: # 无店铺时,店铺收益字段保持默认(0.00),也可显式设为0 pingtaikuozhan_data['dianpu_shouyi'] = Decimal('0.00') PlatformOrderExt.query.create(**pingtaikuozhan_data)''' # 7. 生成微信支付参数(向微信官方发送请求) pay_params = self.generate_wechat_pay_params( dingdanid=dingdanid, jine=shangpin_jiage, beizhu=getattr(settings, 'WEIXIN_PAY_DESCRIPTION', '星阙订单'), user_ip=self.get_client_ip(request), openid=user_openid ) if not pay_params: # 手动抛出异常触发事务回滚 raise Exception('支付参数生成失败') # 8. 返回成功响应 response_data = { 'code': 0, 'msg': '订单创建成功', 'data': { 'dingdanid': dingdanid, 'payParams': pay_params } } return Response(response_data) except Exception as e: # 这里会自动回滚事务 return Response({'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None}) def get_client_ip(self, request): """获取客户端真实IP地址""" x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: ip = x_forwarded_for.split(',')[0] else: ip = request.META.get('REMOTE_ADDR') return ip def generate_wechat_pay_params(self, dingdanid, jine, beizhu, user_ip, openid): """ 调用微信支付统一下单接口,生成支付参数 完全按照您提供的正确示例代码编写 """ try: # 微信支付配置 APPID = getattr(settings, 'WEIXIN_APPID', '') MCHID = getattr(settings, 'WEIXIN_MCHID', '') KEY = getattr(settings, 'WEIXIN_SHANGHUMIYAO', '') NOTIFY_URL = getattr(settings, 'WEIXIN_NOTIFY_URL', '') if not all([APPID, MCHID, KEY, NOTIFY_URL]): raise Exception('微信支付配置不完整') # 生成随机字符串 nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) # 金额(单位:分) total_fee = int(float(jine) * 100) # 交易类型 trade_type = 'JSAPI' # 生成签名 sign_data = { 'appid': APPID, 'mch_id': MCHID, 'nonce_str': nonce_str, 'body': beizhu, 'out_trade_no': dingdanid, 'total_fee': total_fee, 'spbill_create_ip': user_ip, 'notify_url': NOTIFY_URL, 'trade_type': trade_type, 'openid': openid } # 按照参数名ASCII码从小到大排序 sorted_sign_data = sorted(sign_data.items(), key=lambda x: x[0]) # 拼接字符串 sign_string = '&'.join([f"{key}={value}" for key, value in sorted_sign_data if value]) sign_string += f'&key={KEY}' # MD5签名 sign = hashlib.md5(sign_string.encode('utf-8')).hexdigest().upper() # 构造XML数据 xml_data = f""" {APPID} {beizhu} {MCHID} {nonce_str} {NOTIFY_URL} {openid} {dingdanid} {user_ip} {total_fee} {trade_type} {sign} """ # 调用微信统一下单接口 response = requests.post( 'https://api.mch.weixin.qq.com/pay/unifiedorder', data=xml_data.encode('utf-8'), headers={'Content-Type': 'application/xml'}, timeout=10 ) # 解析返回的XML root = ET.fromstring(response.content) return_code = root.find('return_code').text result_code = root.find('result_code').text if return_code == 'SUCCESS' and result_code == 'SUCCESS': prepay_id = root.find('prepay_id').text # 生成前端支付参数 timestamp = str(int(time.time())) nonce_str = ''.join(random.choices(string.ascii_letters + string.digits, k=32)) package = f'prepay_id={prepay_id}' # 生成支付签名 pay_sign_data = { 'appId': APPID, 'timeStamp': timestamp, 'nonceStr': nonce_str, 'package': package, 'signType': 'MD5' } sorted_pay_sign_data = sorted(pay_sign_data.items(), key=lambda x: x[0]) pay_sign_string = '&'.join([f"{key}={value}" for key, value in sorted_pay_sign_data]) pay_sign_string += f'&key={KEY}' pay_sign = hashlib.md5(pay_sign_string.encode('utf-8')).hexdigest().upper() # 返回支付参数(格式与前端完全匹配) result = { 'timeStamp': timestamp, 'nonceStr': nonce_str, 'package': package, 'signType': 'MD5', 'paySign': pay_sign } return result else: # 获取详细的错误信息 err_code = root.find('err_code') err_code_des = root.find('err_code_des') return_msg = root.find('return_msg') error_details = f"return_code: {return_code}, " if err_code is not None: error_details += f"err_code: {err_code.text}, " if err_code_des is not None: error_details += f"err_code_des: {err_code_des.text}, " if return_msg is not None: error_details += f"return_msg: {return_msg.text}" raise Exception(f"微信支付下单失败: {error_details}") except Exception as e: raise e class DingdanHuoquView(APIView): """订单获取接口""" permission_classes = [IsAuthenticated] def post(self, request): # 获取当前登录用户 try: current_user = request.user yonghuid = current_user.UserUID except Exception as e: return Response({ 'code': 1, 'msg': '用户认证失败', 'data': None }) # 获取前端参数 try: data = request.data zhuangtai_list = data.get('zhuangtai_list', []) page = int(data.get('page', 1)) page_size = int(data.get('page_size', 10)) except Exception as e: return Response({ 'code': 2, 'msg': '参数格式错误', 'data': None }) # 参数验证 if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: return Response({ 'code': 3, 'msg': '状态列表不能为空', 'data': None }) if page < 1: page = 1 if page_size <= 0: page_size = 10 try: # 查询扩展表,获取用户的所有订单ID pingtai_orders = PlatformOrderExt.query.filter(BossID=yonghuid) pingtai_order_ids = list(pingtai_orders.values_list('Order__OrderID', flat=True)) if not pingtai_order_ids: return Response({ 'code': 0, 'msg': '成功', 'data': { 'list': [], 'has_more': False, 'total': 0 } }) # 查询主表 status_query = Q() if zhuangtai_list != [1, 2, 3, 4, 5, 6, 7, 8]: status_query = Q(Status__in=zhuangtai_list) query = Q(OrderID__in=pingtai_order_ids) & status_query orders = Order.query.filter(query).order_by('-CreateTime') #total_orders = orders.count() # 手动分页逻辑 start_index = (page - 1) * page_size end_index = start_index + page_size # 获取当前页的数据 current_page_orders = orders[start_index:end_index] # 构建返回数据 order_list = [] for order in current_page_orders: order_data = { 'dingdanId': order.OrderID, 'zhuangtai': order.Status, 'jine': float(order.Amount) if order.Amount else 0.0, 'tupian': order.ImageURL or '', 'jieshao': order.Description or '', 'beizhu': order.Remark or '', 'nicheng': order.Nickname or '', 'chuangjianshijian': fmt_datetime(order.CreateTime), 'create_time': fmt_datetime(order.CreateTime), } order_list.append(order_data) # 判断是否还有更多数据 current_page_count = len(current_page_orders) has_more = current_page_count == page_size return Response({ 'code': 0, 'msg': '成功', 'data': { 'list': order_list, 'has_more': has_more, #'total': total_orders } }) except Exception as e: return Response({ 'code': 4, 'msg': '服务器内部错误', 'data': None }) class DingdanXiangqingView(APIView): """订单详情接口""" permission_classes = [IsAuthenticated] def post(self, request): # 获取当前登录用户 try: current_user = request.user yonghuid = current_user.UserUID except Exception as e: return Response({ 'code': 1, 'msg': '用户认证失败', 'data': None }) # 获取前端参数 try: data = request.data dingdan_id = data.get('dingdanId', '') except Exception as e: return Response({ 'code': 2, 'msg': '参数格式错误', 'data': None }) # 参数验证 if not dingdan_id: return Response({ 'code': 3, 'msg': '订单ID不能为空', 'data': None }) try: # 1. 验证订单是否属于当前用户 # 查询平台订单扩展表,检查laoban_id是否等于当前用户 pingtai_kuozhan = PlatformOrderExt.query.filter( BossID=yonghuid, Order__OrderID=dingdan_id ).first() if not pingtai_kuozhan: # 如果不是平台订单,检查商家订单 shangjia_kuozhan = MerchantOrderExt.query.filter( MerchantID=yonghuid, Order__OrderID=dingdan_id ).first() if not shangjia_kuozhan: return Response({ 'code': 4, 'msg': '订单不存在或无权限查看', 'data': None }) # 2. 查询订单主表 dingdan = Order.query.filter(OrderID=dingdan_id).first() if not dingdan: return Response({ 'code': 5, 'msg': '订单不存在', 'data': None }) # 3. 获取订单信息 zhuangtai = dingdan.Status or 0 zhiding_id = dingdan.AssignedID or '' jiedan_dashou_id = dingdan.PlayerID or '' dashou_liuyan = dingdan.PlayerRemark or '' # 4. 查询打手交付图片 dashoutupian_records = PlayerDeliveryImage.query.filter( OrderID=dingdan_id ).order_by('CreateTime')[:9] # 最多取9条 dashoujiaofu_list = [] for record in dashoutupian_records: if record.ImageURL: dashoujiaofu_list.append(record.ImageURL) # 5. 构建返回数据(字段名与前端对应) response_data = { 'zhuangtai': zhuangtai, # 订单状态 'zhiding': zhiding_id, # 指定打手ID 'jiedan_dashou_id': jiedan_dashou_id, # 服务打手ID 'dashouliuyan': dashou_liuyan, # 打手留言 'dashoujiaofu': dashoujiaofu_list # 打手交付图片列表 } # 6. 返回成功响应 return Response({ 'code': 0, 'msg': '成功', 'data': response_data }) except Exception as e: print(f"订单详情查询失败: {str(e)}") return Response({ 'code': 6, 'msg': '服务器内部错误', 'data': None }) class JiedanView(APIView): """结单接口""" permission_classes = [IsAuthenticated] def post(self, request): # 获取当前登录用户 try: current_user = request.user yonghuid = current_user.UserUID except Exception as e: return Response({ 'code': 1, 'msg': '用户认证失败', 'data': None }) # 获取前端参数 try: data = request.data dingdan_id = data.get('dingdanId', '') pingfen = int(data.get('pingfen', 0)) # 评分,默认0 liuyan = data.get('liuyan', '') # 留言,默认空 except Exception as e: return Response({ 'code': 2, 'msg': '参数格式错误', 'data': None }) # 参数验证 if not dingdan_id: return Response({ 'code': 3, 'msg': '订单ID不能为空', 'data': None }) # 验证评分范围 if pingfen < 0 or pingfen > 5: return Response({ 'code': 4, 'msg': '评分必须在0-5分之间', 'data': None }) # 使用事务确保数据一致性 try: with transaction.atomic(): # 1. 验证订单是否存在且属于当前用户 pingtai_kuozhan = PlatformOrderExt.query.filter( BossID=yonghuid, Order__OrderID=dingdan_id ).select_related('Order').first() if not pingtai_kuozhan: return Response({ 'code': 5, 'msg': '订单不存在或无权限操作', 'data': None }) dingdan = pingtai_kuozhan.Order # 2. 验证订单状态(必须是结算中 - 状态8) if dingdan.Status != 8: return Response({ 'code': 6, 'msg': '订单不在结算状态,无法结单', 'data': None }) # 3. 获取订单相关数据 dashou_fencheng = dingdan.PlayerCommission or Decimal('0.00') # 打手分成 jiedan_dashou_id = dingdan.PlayerID or '' # 接单打手ID shangpin_id = dingdan.ProductID # 商品ID # 4. 更新平台扩展表的老板评价(如果有留言) if liuyan: pingtai_kuozhan.BossReview = liuyan pingtai_kuozhan.save() # 5. 如果有评分,更新评分表 if pingfen > 0 and jiedan_dashou_id: # 使用filter查询,更健壮 pingfen_records = PlayerRating.query.filter(PlayerID=jiedan_dashou_id) if pingfen_records.exists(): # 已有记录,更新 pingfen_record = pingfen_records.first() pingfen_record.TotalScore += Decimal(str(pingfen)) pingfen_record.RatingCount += 1 pingfen_record.save() else: # 新记录,创建 PlayerRating.query.create( PlayerID=jiedan_dashou_id, TotalScore=Decimal(str(pingfen)), RatingCount=1 ) # 6. 更新商品真实销量(如果有商品ID) if shangpin_id: shangpin_records = Shangpin.query.filter(id=shangpin_id) if shangpin_records.exists(): shangpin = shangpin_records.first() # 检查字段是否存在 if hasattr(shangpin, 'zhenshi_xiaoliang'): shangpin.zhenshi_xiaoliang = (shangpin.zhenshi_xiaoliang or 0) + 1 shangpin.save() # 7. 更新打手扩展表(如果有打手ID和分成) if jiedan_dashou_id and dashou_fencheng > 0: # 查询打手用户 dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first() if dashou_user: # 查询打手扩展表 dashou_kuozhan = UserDashou.query.filter(user=dashou_user).first() if dashou_kuozhan: # 更新四个金额字段 dashou_kuozhan.zonge = (dashou_kuozhan.zonge or Decimal('0.00')) + dashou_fencheng dashou_kuozhan.yue = (dashou_kuozhan.yue or Decimal('0.00')) + dashou_fencheng dashou_kuozhan.jinrishouyi = (dashou_kuozhan.jinrishouyi or Decimal( '0.00')) + dashou_fencheng dashou_kuozhan.jinyueshouyi = (dashou_kuozhan.jinyueshouyi or Decimal( '0.00')) + dashou_fencheng # 成交总量加1 dashou_kuozhan.chengjiaozongliang = (dashou_kuozhan.chengjiaozongliang or 0) + 1 # 状态改为数字1 dashou_kuozhan.zhuangtai = 1 dashou_kuozhan.save() # 8. 更新订单状态为已完成(状态3) dingdan.Status = 3 dingdan.save() # 🔥 新增:调用打手每日统计(成交) try: update_dashou_daily_by_action( yonghuid=jiedan_dashou_id, amount=dashou_fencheng, action=2 # 2 表示成交(结算) ) logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功") except Exception as e: logger.error(f"打手每日统计更新失败: {e}") # 不影响主流程 # ========== 新增:商品和店铺每日统计 ========== try: # 商品每日统计(只要有商品ID就执行) if dingdan.ProductID: # 从扩展表获取已计算好的收益值(下单时已存储) pingtai_shouyi = Decimal('0.00') dianpu_shouyi = Decimal('0.00') if hasattr(dingdan, 'pingtai_kuozhan'): pingtai_shouyi = dingdan.pingtai_kuozhan.PlatformIncome or Decimal('0.00') dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_shangpin_daily_stat( ProductID=dingdan.ProductID, caozuo_leixing=2, # 2 = 结单 dingdan_jiage=dingdan.Amount, PlatformIncome=pingtai_shouyi, dianpu_zongshouyi=dianpu_shouyi ) logger.info(f"商品统计更新完成,商品ID: {dingdan.ProductID}") # 店铺每日统计(仅当存在店铺ID时执行) # ========== 修改为 ========== # 店铺每日统计(包含上级分红) dianpu_id = None shangji_id = None shangji_fen = Decimal('0.00') if hasattr(dingdan, 'pingtai_kuozhan'): dianpu_id = dingdan.pingtai_kuozhan.ShopID shangji_id = dingdan.pingtai_kuozhan.ParentShopID shangji_fen = dingdan.pingtai_kuozhan.ParentBonus or Decimal('0.00') if dianpu_id: dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_dianpu_daily_stat( ShopID=dianpu_id, caozuo_leixing=2, # 结算 dingdan_jiage=dingdan.Amount, ShopIncome=dianpu_shouyi, shangji_ShopID=shangji_id, # 新增:上级店铺ID ParentBonus=shangji_fen # 新增:该单给上级的分红 ) logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}, 上级ID: {shangji_id}, 上级分红: {shangji_fen}") '''dianpu_id = None if hasattr(dingdan, 'pingtai_kuozhan'): dianpu_id = dingdan.pingtai_kuozhan.ShopID if dianpu_id: dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_dianpu_daily_stat( ShopID=dianpu_id, caozuo_leixing=2, # 2 = 结单 dingdan_jiage=dingdan.Amount, ShopIncome=dianpu_shouyi ) logger.info(f"店铺统计更新完成,店铺ID: {dianpu_id}")''' except Exception as e: logger.error(f"商品/店铺统计更新失败: {e}") logger.error(traceback.format_exc()) # 统计失败不影响主流程 # ============================================= # 9. 返回成功响应 return Response({ 'code': 0, 'msg': '结单成功', 'data': None }) except Exception as e: # 事务会自动回滚 return Response({ 'code': 7, 'msg': '结单失败,服务器内部错误', 'data': None }) class DingdanXiangqingView2(APIView): """订单详情接口(老板端)- 返回完整评价信息""" permission_classes = [IsAuthenticated] def post(self, request): try: yonghuid = request.user.UserUID except Exception: return Response({'code': 1, 'msg': '用户认证失败', 'data': None}) dingdan_id = request.data.get('dingdanId', '') if not dingdan_id: return Response({'code': 2, 'msg': '订单ID不能为空', 'data': None}) try: # 权限验证 pingtai_ext = PlatformOrderExt.query.filter( BossID=yonghuid, Order__OrderID=dingdan_id ).select_related('Order').first() if not pingtai_ext: return Response({'code': 3, 'msg': '订单不存在或无权限查看', 'data': None}) order = pingtai_ext.Order zhuangtai = order.Status or 0 jiedan_dashou_id = order.PlayerID or '' dashou_liuyan = order.PlayerRemark or '' UpdateTime = order.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if order.UpdateTime else '' # 交付图片 dashoujiaofu_list = list( PlayerDeliveryImage.query.filter(OrderID=dingdan_id) .order_by('CreateTime')[:9] .values_list('ImageURL', flat=True) ) # 打手信息 dashou_nicheng = '' dashou_touxiang = '' jiedan_time = '' tijiao_time = '' if jiedan_dashou_id: dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first() if dashou_user: dashou_touxiang = dashou_user.Avatar or '' try: dashou_nicheng = dashou_user.DashouProfile.nicheng or '' except UserDashou.DoesNotExist: pass biaoxian = DashouBiaoxian.query.filter( OrderID=dingdan_id, dashou_id=jiedan_dashou_id ).first() if biaoxian: if biaoxian.jiedan_time: jiedan_time = biaoxian.jiedan_time.strftime('%Y-%m-%d %H:%M:%S') if biaoxian.tijiao_time: tijiao_time = biaoxian.tijiao_time.strftime('%Y-%m-%d %H:%M:%S') # 老板自己的评价(如果已经结算过,可能已有评价) laoban_pingjia = pingtai_ext.BossReview or '' shangxian_sudu = None fuwu_taidu = None fanche_cishu = None if jiedan_dashou_id and zhuangtai in [3, 4, 5, 6]: # 已完成或退款相关状态才可能有评价 biaoxian = DashouBiaoxian.query.filter( OrderID=dingdan_id, dashou_id=jiedan_dashou_id ).first() if biaoxian: shangxian_sudu = biaoxian.shangxian_sudu fuwu_taidu = biaoxian.fuwu_taidu fanche_cishu = biaoxian.fanche_cishu data = { 'zhuangtai': zhuangtai, 'dashou': jiedan_dashou_id, 'dashou_nicheng': dashou_nicheng, 'dashou_touxiang': dashou_touxiang, 'dashouliuyan': dashou_liuyan, 'dashoujiaofu': dashoujiaofu_list, 'UpdateTime': UpdateTime, 'jiedan_time': jiedan_time, 'tijiao_time': tijiao_time, 'laoban_pingjia': laoban_pingjia, 'shangxian_sudu': shangxian_sudu, # 1-5 或 null 'fuwu_taidu': fuwu_taidu, # 1-5 或 null 'fanche_cishu': fanche_cishu # int 或 null } return Response({'code': 0, 'msg': '成功', 'data': data}) except Exception as e: logger.error(f"订单详情查询异常: {e}", exc_info=True) return Response({'code': 4, 'msg': '服务器内部错误', 'data': None}) class JiedanView2(APIView): """结单接口(老板端)- 接收全新评价字段并写入表现表""" permission_classes = [IsAuthenticated] def post(self, request): # 1. 用户认证 try: current_user = request.user yonghuid = current_user.UserUID except Exception: return Response({'code': 1, 'msg': '用户认证失败', 'data': None}) # 2. 获取参数(字段名与前端完全一致) try: data = request.data dingdan_id = data.get('dingdanId', '') fanche_cishu = data.get('fanche_cishu') # 前端传的字符串或数字,后端转为整数 shangxian_sudu = data.get('shangxian_sudu') fuwu_taidu = data.get('fuwu_taidu') liuyan = data.get('liuyan', '') except Exception: return Response({'code': 2, 'msg': '参数格式错误', 'data': None}) # 参数校验 if not dingdan_id: return Response({'code': 3, 'msg': '订单ID不能为空', 'data': None}) # 转换并校验必填评价字段 try: fanche_cishu = int(fanche_cishu) if fanche_cishu is not None else None shangxian_sudu = int(shangxian_sudu) if shangxian_sudu is not None else None fuwu_taidu = int(fuwu_taidu) if fuwu_taidu is not None else None except (ValueError, TypeError): return Response({'code': 4, 'msg': '评价参数必须为整数', 'data': None}) if fanche_cishu is None: return Response({'code': 5, 'msg': '翻车次数不能为空', 'data': None}) if not (1 <= shangxian_sudu <= 5 and 1 <= fuwu_taidu <= 5): return Response({'code': 6, 'msg': '评价分数必须在1-5之间', 'data': None}) # 使用事务保证数据一致性 try: with transaction.atomic(): # 3. 权限验证:订单属于当前老板 pingtai_ext = PlatformOrderExt.query.filter( BossID=yonghuid, Order__OrderID=dingdan_id ).select_related('Order').first() if not pingtai_ext: return Response({'code': 7, 'msg': '订单不存在或无权限操作', 'data': None}) order = pingtai_ext.Order # 4. 状态校验:必须是结算中(状态8) if order.Status != 8: return Response({'code': 8, 'msg': '订单不在结算状态', 'data': None}) # 5. 提取订单关键数据 dashou_fencheng = order.PlayerCommission or Decimal('0.00') jiedan_dashou_id = order.PlayerID or '' shangpin_id = order.ProductID # 6. 保存老板评价(兼容旧逻辑) if liuyan: pingtai_ext.BossReview = liuyan pingtai_ext.save() # 7. 更新评分表(用服务态度分作为综合评分,保留原有统计) if jiedan_dashou_id and fuwu_taidu: pingfen_records = PlayerRating.query.filter(PlayerID=jiedan_dashou_id) if pingfen_records.exists(): pf = pingfen_records.first() pf.TotalScore += Decimal(str(fuwu_taidu)) pf.RatingCount += 1 pf.save() else: PlayerRating.query.create( PlayerID=jiedan_dashou_id, TotalScore=Decimal(str(fuwu_taidu)), RatingCount=1 ) # 8. 更新商品真实销量 if shangpin_id: shangpin_qs = Shangpin.query.filter(id=shangpin_id) if shangpin_qs.exists(): sp = shangpin_qs.first() if hasattr(sp, 'zhenshi_xiaoliang'): sp.zhenshi_xiaoliang = (sp.zhenshi_xiaoliang or 0) + 1 sp.save() # 9. 更新打手收益与状态 if jiedan_dashou_id and dashou_fencheng > 0: dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first() if dashou_user: try: dashou_ext = dashou_user.DashouProfile dashou_ext.zonge = (dashou_ext.zonge or Decimal('0.00')) + dashou_fencheng dashou_ext.yue = (dashou_ext.yue or Decimal('0.00')) + dashou_fencheng dashou_ext.jinrishouyi = (dashou_ext.jinrishouyi or Decimal('0.00')) + dashou_fencheng dashou_ext.jinyueshouyi = (dashou_ext.jinyueshouyi or Decimal('0.00')) + dashou_fencheng dashou_ext.chengjiaozongliang = (dashou_ext.chengjiaozongliang or 0) + 1 dashou_ext.zhuangtai = 1 dashou_ext.save() except UserDashou.DoesNotExist: pass # 10. 更新订单状态为已完成 order.Status = 3 order.save() # 11. 打手每日统计(成交) try: update_dashou_daily_by_action( UserID=jiedan_dashou_id, amount=dashou_fencheng, action=2 # 2=结算/成交 ) except Exception as e: logger.error(f"打手每日统计失败: {e}") # 12. 商品/店铺每日统计 try: if shangpin_id: # 从扩展表取已计算收益 pingtai_shouyi = Decimal('0.00') dianpu_shouyi = Decimal('0.00') if hasattr(order, 'pingtai_kuozhan'): pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00') dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00') update_shangpin_daily_stat( ProductID=shangpin_id, caozuo_leixing=2, dingdan_jiage=order.Amount, PlatformIncome=pingtai_shouyi, dianpu_zongshouyi=dianpu_shouyi ) # 店铺统计(含上级分红) if hasattr(order, 'pingtai_kuozhan'): dianpu_id = order.pingtai_kuozhan.ShopID shangji_id = order.pingtai_kuozhan.ParentShopID shangji_fen = order.pingtai_kuozhan.ParentBonus or Decimal('0.00') if dianpu_id: update_dianpu_daily_stat( ShopID=dianpu_id, caozuo_leixing=2, dingdan_jiage=order.Amount, ShopIncome=dianpu_shouyi, shangji_ShopID=shangji_id, ParentBonus=shangji_fen ) except Exception as e: logger.error(f"商品/店铺统计失败: {e}") # 14. 🆕 写入打手表现表(结算评价) if jiedan_dashou_id: record_dashou_biaoxian( dingdan_id=dingdan_id, dashou_id=jiedan_dashou_id, xingwei=3, # 3=结算 fanche_RatingCount=fanche_cishu, shangxian_sudu=shangxian_sudu, fuwu_taidu=fuwu_taidu # bankuai 暂不传入,后续可从订单商品关联 ) # 15. 返回成功 return Response({'code': 0, 'msg': '结单成功', 'data': None}) except Exception as e: logger.error(f"结单事务异常: {e}", exc_info=True) return Response({'code': 9, 'msg': '服务器内部错误,结单失败', 'data': None}) class ShangpinLeixingView(APIView): """ 获取商家可用的商品类型列表(含关联板块的称号) POST /dingdan/sjspleixing """ permission_classes = [IsAuthenticated] @staticmethod def _can_access(request): """商家老板、有效子客服(含代理调用)方可获取""" if getattr(request, '_staff_member', None) is not None: return True from merchant_ops.services.authz import ( get_active_staff_member, is_merchant_owner, member_has_perm, staff_order_read_perm, ) if is_merchant_owner(request.user): return True member = get_active_staff_member(request.user) return bool(member and staff_order_read_perm(member)) def post(self, request): if not self._can_access(request): return Response({'code': 403, 'msg': '非商家或有效客服,无权获取订单类型', 'data': []}) try: # 1. 查询审核通过且抢单要求类型为1的商品类型,按排序权重升序 shangpin_queryset = ShangpinLeixing.query.filter( yaoqiuleixing=1, shenhezhuangtai=1 ).select_related('bankuai').order_by('paixu') # 2. 收集所有涉及到的板块ID bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct() # 3. 查询这些板块下的所有称号(每个称号绑定到一个板块) chenghao_list = Chenghao.query.filter( bankuai_id__in=bankuai_ids ).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_id') # 4. 构建 板块ID -> 称号列表 的映射(确保准确关联) bankuai_chenghao_map = {} for ch in chenghao_list: bid = ch['bankuai_id'] if bid not in bankuai_chenghao_map: bankuai_chenghao_map[bid] = [] bankuai_chenghao_map[bid].append({ 'id': ch['id'], 'mingcheng': ch['mingcheng'], 'texiao_json': ch['texiao_miaoshu'] # 前端 chenghao-tag 组件需要 }) # 5. 组装最终数据 data_list = [] for sp in shangpin_queryset: item = { 'id': sp.id, 'yaoqiuleixing': sp.yaoqiuleixing, 'huiyuan_id': sp.huiyuan_id, 'jieshao': sp.jieshao, # 当前类型所属板块的称号列表(无板块则为空数组) 'chenghaoList': bankuai_chenghao_map.get(sp.bankuai_id, []) } data_list.append(item) return Response({ 'code': 200, 'msg': '获取成功', 'data': data_list }) except Exception as e: logger.error(f"获取商品类型失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误', 'data': {}}, status=500) class ShangjiaPaifaView(APIView): permission_classes = [IsAuthenticated] def post(self, request): logger.info(f"商家派单请求: user_id={request.user.UserUID}, data={request.data}") try: data = request.data # ----- 1. 获取原始参数(保持原类型)----- shangpin_type_id = data.get('shangpinTypeId') huiyuan_id = data.get('huiyuanId') yaoqiuleixing = data.get('GrabRequirement') dingdan_jieshao_raw = data.get('dingdanJieshao', '') dingdan_beizhu_raw = data.get('dingdanBeizhu', '') laoban_name_raw = data.get('laobanName', '') zhiding_uid_raw = data.get('zhidingUid', '') jiage_str = data.get('jiage') label_id = data.get('labelId') # 整数或 None commission_enabled = data.get('commissionEnabled', False) commission_value_raw = data.get('commissionValue') # ----- 2. 字符串字段安全处理 ----- dingdan_jieshao = str(dingdan_jieshao_raw).strip() dingdan_beizhu = str(dingdan_beizhu_raw).strip() laoban_name = str(laoban_name_raw).strip() zhiding_uid = str(zhiding_uid_raw).strip() commission_value_str = str(commission_value_raw).strip() if commission_value_raw is not None else '' # ----- 3. 必填校验 ----- if not all([shangpin_type_id, dingdan_jieshao, laoban_name, jiage_str]): logger.warning(f"参数缺失: {request.data}") return Response({'code': 400, 'msg': '缺少必填参数'}) # ----- 4. 价格验证 ----- try: jiage = float(jiage_str) if jiage < 1 or jiage > 10000: return Response({'code': 400, 'msg': '价格需在1-10000元之间'}) if '.' in str(jiage_str) and len(str(jiage_str).split('.')[1]) > 2: return Response({'code': 400, 'msg': '最多支持两位小数'}) except ValueError: return Response({'code': 400, 'msg': '价格格式错误'}) # ----- 5. 商品类型 ----- try: shangpin_type = ShangpinLeixing.query.get(id=shangpin_type_id) except ShangpinLeixing.DoesNotExist: logger.error(f"商品类型不存在: {shangpin_type_id}") return Response({'code': 400, 'msg': '商品类型不存在'}) # ----- 6. 押金 / 会员模式决定 ----- final_yongjin = None if commission_enabled and commission_value_str: try: cv = float(commission_value_str) if cv > 0: final_yaoqiu_type = 2 final_yongjin = Decimal(str(cv)) final_huiyuan_id = '' logger.info(f"押金模式: 佣金={cv}") else: final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' logger.warning("佣金为0,降级为会员模式") except ValueError: final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' logger.warning("佣金格式错误,降级为会员模式") else: final_yaoqiu_type = shangpin_type.yaoqiuleixing or 1 final_huiyuan_id = str(huiyuan_id) if huiyuan_id else '' logger.info(f"会员模式: 类型={final_yaoqiu_type}") # ----- 7. 称号(可选:不传/空值/无效/不存在均视为无标签)----- chenghao_obj = None label_id_str = str(label_id).strip() if label_id is not None else '' if label_id_str and label_id_str not in ('0', 'null', 'undefined'): try: lid = int(label_id_str) if lid > 0: chenghao_obj = Chenghao.query.filter(id=lid).first() if chenghao_obj: logger.info(f"订单将关联称号: {chenghao_obj.mingcheng}") else: logger.info(f"标签不存在,按无标签派单: {label_id}") except (ValueError, TypeError): logger.info(f"标签ID无效,按无标签派单: {label_id}") # ----- 8. 商家信息 ----- try: shangjia = request.user.ShopProfile except UserShangjia.DoesNotExist: logger.warning(f"用户不是商家: {request.user.UserUID}") return Response({'code': 403, 'msg': '您不是商家'}) if shangjia.zhuangtai != 1: logger.warning(f"商家状态异常: {shangjia.zhuangtai}") return Response({'code': 403, 'msg': '商家状态异常'}) if jiage > shangjia.yue: logger.warning(f"余额不足: {shangjia.yue} < {jiage}") return Response({'code': 400, 'msg': f'余额不足,当前余额{shangjia.yue}元'}) # ----- 9. 指定打手 ----- zhiding_dashou = None if zhiding_uid: try: zhiding_user = User.query.get(UserUID=zhiding_uid) zhiding_dashou = zhiding_user.DashouProfile if zhiding_dashou.zhanghaozhuangtai != 1: return Response({'code': 400, 'msg': '指定打手状态异常'}) except User.DoesNotExist: return Response({'code': 400, 'msg': '指定用户不存在'}) except UserDashou.DoesNotExist: return Response({'code': 400, 'msg': '指定用户不是打手'}) # ----- 10. 利率(打手+管事分成,打手优先) ----- club_id = resolve_club_id_from_request(request) dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage, club_id=club_id) # ----- 11. 生成订单ID ----- def generate_dingdan_id(): ts = int(time.time() * 1000) pid = os.getpid() % 1000 rnd = random.randint(1000, 9999) return f"SJ{ts}{pid:03d}{rnd}" # ----- 12. 事务:创建订单、更新商家统计 ----- dingdan = None try: with transaction.atomic(): dingdan = Order.query.create( OrderID=generate_dingdan_id(), Status=7 if zhiding_dashou else 1, Platform=2, Amount=jiage, PlayerCommission=dashou_fencheng, ManagerCommission=guanshi_fencheng, AssignedID=zhiding_uid if zhiding_dashou else '', ProductID=0, ProductTypeID=int(shangpin_type_id), GrabRequirement=final_yaoqiu_type, CommissionReq=final_yongjin, MembershipID=final_huiyuan_id, Description=dingdan_jieshao[:2000], Remark=dingdan_beizhu[:1000], Nickname=laoban_name[:50], ImageURL=request.user.Avatar, User1ID=f"Sj{request.user.UserUID}", ClubID=club_id, ) logger.info(f"订单主表创建: {dingdan.OrderID}") MerchantOrderExt.query.create( Order=dingdan, MerchantID=request.user.UserUID, MerchantNickname=shangjia.nicheng, ) if chenghao_obj: DingdanBiaoqian.query.create(dingdan=dingdan, chenghao=chenghao_obj) logger.info(f"订单关联称号: {chenghao_obj.mingcheng}") # 更新商家统计 UserShangjia.query.filter(id=shangjia.id).update( fabu=F('fabu') + 1, yue=F('yue') - jiage, jinridingdan=F('jinridingdan') + 1, jinriliushui=F('jinriliushui') + jiage, jinyuedingdan=F('jinyuedingdan') + 1, jinyueliushui=F('jinyueliushui') + jiage, ) update_shangjia_daily( yonghuid=request.user.UserUID, amount=Decimal(str(jiage)), action=1, order_id=dingdan.OrderID, ) # 事务提交,订单已持久化 except Exception as e: logger.error(f"订单创建事务失败: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '订单创建失败,请稍后重试'}) # ========== 以下操作均在事务外,避免异步任务查不到订单 ========== # Celery 广播任务(异步) try: order_info = { 'OrderID': dingdan.OrderID, 'game_type': self._get_game_type_name(dingdan.ProductTypeID), 'amount': str(dingdan.Amount), 'order_desc': (dingdan.Description or '')[:50], } dingdan_guangbo.delay(order_info) logger.info(f"已提交广播任务: {dingdan.OrderID}") except Exception as e: logger.error(f"广播任务提交失败: {e}", exc_info=True) logger.info(f"商家派单成功: {dingdan.OrderID}, 金额={jiage}") return Response({'code': 200, 'msg': '派单成功', 'data': {'jine': str(dingdan.Amount)}}) except Exception as e: logger.error(f"商家派单接口未捕获异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '系统内部错误'}) def _get_game_type_name(self, leixing_id): try: gt = ShangpinLeixing.query.filter(id=leixing_id).first() return gt.jieshao if gt else '游戏订单' except Exception: return '游戏订单' # views.py 中 ShangjiaDingdanHuoquView 完整改造 class ShangjiaDingdanHuoquView(APIView): """ 商家订单获取接口 POST /dingdan/sjdingdanhq 需要JWT认证 支持分页、状态筛选、商品类型筛选、模糊搜索(订单ID/订单介绍/游戏昵称/打手ID) 返回待结算订单数量(pending_count) """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 获取当前登录用户的yonghuid try: current_user = request.user yonghuid = current_user.UserUID except AttributeError: return Response({ 'code': 1, 'msg': '用户认证失败', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 2. 解析参数 try: data = request.data zhuangtai_list = data.get('zhuangtai_list', []) page = max(1, int(data.get('page', 1))) page_size = min(100, max(1, int(data.get('page_size', 10)))) # 🆕 商品类型筛选(兼容 leixing_id / ProductTypeID) leixing_id = pick_leixing_id(data) # 🆕 模糊搜索关键字(支持订单ID、订单介绍、游戏昵称、打手ID) keyword = data.get('keyword', '').strip() except (ValueError, TypeError): return Response({ 'code': 2, 'msg': '参数格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 3. 验证状态列表 if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0: return Response({ 'code': 3, 'msg': '状态列表不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) try: # 4. 构建基础查询(该商家的所有订单) base_qs = MerchantOrderExt.query.filter( MerchantID=yonghuid ).select_related('Order') # 5. 状态过滤 base_qs = base_qs.filter(Order__Status__in=zhuangtai_list) # 6. 商品类型筛选 if leixing_id is not None: base_qs = base_qs.filter(Order__ProductTypeID=leixing_id) # 7. 模糊搜索(支持订单ID、订单介绍、游戏昵称、打手ID) if keyword: # 订单ID精确匹配(dingdan_id有唯一索引,先用精确匹配) dingdan_id_match = Q(Order__OrderID__exact=keyword) # 订单介绍模糊匹配 jieshao_match = Q(Order__Description__icontains=keyword) # 游戏昵称模糊匹配 nicheng_match = Q(Order__Nickname__icontains=keyword) # 打手ID精确匹配 dashou_match = Q(Order__PlayerID__exact=keyword) base_qs = base_qs.filter( dingdan_id_match | jieshao_match | nicheng_match | dashou_match ) # 8. 计算待结算订单数量(状态为8) pending_count = MerchantOrderExt.query.filter( MerchantID=yonghuid, Order__Status=8 ).count() # 9. 排序、分页 total_count = base_qs.count() start_index = (page - 1) * page_size end_index = start_index + page_size current_page_orders = base_qs.order_by('-Order__CreateTime')[start_index:end_index] # 10. 构建返回数据 order_list = [] for shangjia_order in current_page_orders: order = shangjia_order.Order order_data = { 'dingdan_id': order.OrderID, 'zhuangtai': order.Status, 'jine': float(order.Amount) if order.Amount else 0.0, 'jieshao': order.Description or '', 'beizhu': order.Remark or '', 'nicheng': order.Nickname or '', 'leixing_id': order.ProductTypeID, 'tupian': order.ImageURL or '', 'jiedan_dashou_id': order.PlayerID or '', **datetime_aliases(order.CreateTime), } order_list.append(order_data) # 11. 判断是否还有更多 current_page_count = len(current_page_orders) has_more = current_page_count == page_size and end_index < total_count return Response({ 'code': 0, 'msg': '成功', 'data': { 'list': order_list, 'has_more': has_more, 'total': total_count, 'pending_count': pending_count } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"商家订单获取失败: {str(e)}", exc_info=True) return Response({ 'code': 4, 'msg': '服务器内部错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangjiaDingdanXiangqingView(APIView): """ 商家订单详情接口 返回订单基础信息、打手信息、交付图片、罚款数据、分红利率、更换打手产生的新订单ID等 """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 获取当前登录用户的 yonghuid try: current_user = request.user yonghuid = current_user.UserUID except AttributeError: return Response({ 'code': 1, 'msg': '用户认证失败', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 2. 获取前端参数 try: data = request.data dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip() except Exception: return Response({ 'code': 2, 'msg': '参数格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not dingdan_id: return Response({ 'code': 3, 'msg': '订单ID不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) try: # 3. 查询商家订单扩展表,验证权限并获取基础订单数据 shangjia_kuozhan = MerchantOrderExt.query.filter( MerchantID=yonghuid, Order__OrderID=dingdan_id ).select_related('Order').first() if not shangjia_kuozhan: return Response({ 'code': 4, 'msg': '订单不存在或您无权限查看', 'data': None }, status=status.HTTP_403_FORBIDDEN) order = shangjia_kuozhan.Order # 4. 交付图片列表 dashou_tupian_list = PlayerDeliveryImage.query.filter( OrderID=dingdan_id ).order_by('CreateTime').values_list('ImageURL', flat=True) dashou_jiaofu = list(dashou_tupian_list) # 5. 打手详细信息(头像、昵称、用户ID) dashou_yonghuid = order.PlayerID or '' dashou_nicheng = '' dashou_avatar = '' if dashou_yonghuid: dashou_user = User.query.filter( UserUID=dashou_yonghuid ).select_related('DashouProfile').first() if dashou_user: dashou_avatar = dashou_user.Avatar or '' if hasattr(dashou_user, 'DashouProfile') and dashou_user.DashouProfile: dashou_nicheng = dashou_user.DashouProfile.nicheng or '' # 6. 罚款数据(如果存在) fadan_obj = Penalty.query.filter( RelatedOrderID=dingdan_id, PenalizedUserID=dashou_yonghuid ).first() fadan_dict = None if fadan_obj: ct = fmt_datetime(fadan_obj.CreateTime) ut = fmt_datetime(fadan_obj.UpdateTime) fadan_dict = { 'id': fadan_obj.id, 'chufaliyou': fadan_obj.Reason or '', 'fakuanjine': float(fadan_obj.FineAmount) if fadan_obj.FineAmount else 0.0, 'yingxiang_qiangdan': fadan_obj.AffectsGrabbing, 'zhuangtai': fadan_obj.Status, 'shensuliyou': fadan_obj.AppealReason or '', 'tongyi_chufaliyou': fadan_obj.ApproveReason or '', 'bohuiliyou': fadan_obj.RejectReason or '', 'CreateTime': ct, 'create_time': ct, 'UpdateTime': ut, 'update_time': ut, } # 7. 商家自己的分红利率(用于罚款分红计算) fenhong_lilv = 0.0 try: # 查找商家作为分红者的最新记录,取应得利率 last_fenhong = PenaltyBonus.query.filter( BeneficiaryID=yonghuid, Identity=1 # 1=商家 ).order_by('-CreateTime').first() if last_fenhong: fenhong_lilv = float(last_fenhong.IndividualRate or 0) except Exception as e: logger.warning(f'获取分红利率失败: {e}') # 8. 更换打手后产生的新订单ID(如果当前订单被更换过) xin_dingdan_id = '' try: # 查找以当前订单作为“上一个订单ID”的新订单扩展记录 new_shangjia_order = MerchantOrderExt.query.filter( PrevOrderID=dingdan_id, MerchantID=yonghuid ).select_related('Order').first() if new_shangjia_order: xin_dingdan_id = new_shangjia_order.Order.OrderID except Exception as e: logger.warning(f'查找新订单ID失败: {e}') # 9. 构建返回数据 response_data = { # 基础订单字段 'dingdan_id': order.OrderID, 'zhuangtai': order.Status or 0, 'jine': float(order.Amount) if order.Amount else 0.0, 'jieshao': order.Description or '', 'beizhu': order.Remark or '', 'nicheng': order.Nickname or '', **datetime_aliases(order.CreateTime), # 打手信息 'dashou_yonghuid': dashou_yonghuid, 'dashou_nicheng': dashou_nicheng, 'dashou_avatar': dashou_avatar, # 服务详情 'fuwudashou_id': dashou_yonghuid, # 保留旧字段兼容 'dashou_liuyan': order.PlayerRemark or '', 'dashou_jiaofu': dashou_jiaofu, # 商家扩展字段(旧) 'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '', 'tkliyou': order.RefundReason or '', 'cfliyou': shangjia_kuozhan.PenaltyReason or '', # 保留,但不影响新罚款 'sqzhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0, 'bhliyou': shangjia_kuozhan.RejectReason or '', 'chufa_jieguo': '', # 已弃用 # 新增:罚款数据 'fadan': fadan_dict, # 新增:分红利率 'fenhong_lilv': fenhong_lilv, # 新增:更换打手后的新订单ID 'xin_dingdan_id': xin_dingdan_id, } try: from merchant_ops.services.order_enrich import enrich_order_detail_data enrich_order_detail_data(response_data, dingdan_id) except Exception as e: logger.warning(f'派单客服信息附加失败: {e}') return Response({ 'code': 0, 'msg': '成功', 'data': response_data }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"商家订单详情查询失败: {str(e)}", exc_info=True) return Response({ 'code': 5, 'msg': '服务器内部错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangjiaJiesuanView(APIView): """商家结单接口""" permission_classes = [permissions.IsAuthenticated] def post(self, request): # 1. 获取当前登录用户 try: current_user = request.user yonghuid = current_user.UserUID try: shangjia = current_user.ShopProfile except UserShangjia.DoesNotExist: return Response({ 'code': 1, 'msg': '您不是商家或商家信息未完善', 'data': None }, status=status.HTTP_403_FORBIDDEN) except AttributeError: return Response({ 'code': 1, 'msg': '用户认证失败', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 2. 获取前端参数 try: data = request.data dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip() pingfen = int(data.get('pingfen', 0)) liuyan = data.get('liuyan', '').strip() except (ValueError, TypeError): return Response({ 'code': 2, 'msg': '参数格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 3. 参数验证 if not dingdan_id: return Response({ 'code': 3, 'msg': '订单ID不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if pingfen < 0 or pingfen > 5: return Response({ 'code': 4, 'msg': '评分必须在0-5分之间', 'data': None }, status=status.HTTP_400_BAD_REQUEST) try: with transaction.atomic(): # 4. 查询订单并加锁(修复并发问题) order = Order.query.select_for_update().get(OrderID=dingdan_id) # 验证订单是否属于当前商家 shangjia_kuozhan = MerchantOrderExt.query.filter( Order=order, MerchantID=yonghuid ).first() if not shangjia_kuozhan: return Response({ 'code': 5, 'msg': '订单不存在或您无权限操作', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 5. 验证订单状态 if order.Status != 8: return Response({ 'code': 6, 'msg': '订单不在结算状态,无法结单', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 获取订单字段 jine = order.Amount or Decimal('0') dashou_fencheng = order.PlayerCommission or Decimal('0') jiedan_dashou_id = order.PlayerID if not jiedan_dashou_id: return Response({ 'code': 7, 'msg': '订单没有接单打手,无法结单', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # ========== 本地订单正常结算给打手 ========== # 查询打手信息 try: dashou_user = User.query.get(UserUID=jiedan_dashou_id) dashou = dashou_user.DashouProfile except (User.DoesNotExist, UserDashou.DoesNotExist): return Response({ 'code': 8, 'msg': '接单打手不存在或信息不完整', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 原子更新打手数据(使用 F 表达式) UserDashou.query.filter(id=dashou.id).update( chengjiaozongliang=F('chengjiaozongliang') + 1, yue=F('yue') + dashou_fencheng, zonge=F('zonge') + dashou_fencheng, jinrishouyi=F('jinrishouyi') + dashou_fencheng, jinyueshouyi=F('jinyueshouyi') + dashou_fencheng, ) # 更新商家成交单量 UserShangjia.query.filter(id=shangjia.id).update( chengjiao=F('chengjiao') + 1, ) # 更新订单状态 order.Status = 3 order.save() # 处理评分(所有情况都处理) if pingfen > 0: pingfen_obj, created = PlayerRating.query.get_or_create( PlayerID=jiedan_dashou_id, defaults={'TotalScore': pingfen, 'RatingCount': 1} ) if not created: PlayerRating.query.filter(id=pingfen_obj.id).update( TotalScore=F('TotalScore') + pingfen, RatingCount=F('RatingCount') + 1, ) # 更新商家订单扩展表的评价 MerchantOrderExt.query.filter(id=shangjia_kuozhan.id).update( MerchantReview=liuyan, ) # 新增:调用打手每日统计(成交) try: update_dashou_daily_by_action( yonghuid=jiedan_dashou_id, amount=dashou_fencheng, action=2 # 2 表示成交(结算) ) logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功") except Exception as e: logger.error(f"打手每日统计更新失败: {e}") # 不影响主流程 # ✅ 新方法(行为驱动,更安全) update_shangjia_daily( yonghuid=request.user.UserUID, amount=Decimal(str(jine)), action=2, # 2 = 结算 order_id=dingdan_id, ) # 商家订单管事分红(仅 Platform=2) if order.Platform == 2: settle_shangjia_order_guanshi_fenhong(order, jiedan_dashou_id) # 返回成功 return Response({ 'code': 0, 'msg': '结单成功', 'data': { 'dingdan_id': dingdan_id, 'new_zhuangtai': 3, 'jine': float(jine), 'dashou_fencheng': float(dashou_fencheng), 'jiedan_dashou_id': jiedan_dashou_id, 'pingfen': pingfen, 'liuyan': liuyan } }, status=status.HTTP_200_OK) except Order.DoesNotExist: return Response({ 'code': 5, 'msg': '订单不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f"商家结单失败: {str(e)}", exc_info=True) return Response({ 'code': 9, 'msg': '服务器内部错误,结单失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangjiaChexiaoView(APIView): """商家撤销订单申请接口""" permission_classes = [IsAuthenticated] # JWT认证 def post(self, request): # 1. 获取当前登录用户并验证商家身份 try: current_user = request.user # 使用反向查询获取商家扩展信息(性能最佳) shangjia = current_user.ShopProfile except UserShangjia.DoesNotExist: return Response({ 'code': 1, 'msg': '您不是商家或商家信息未完善', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 2. 检查商家账号状态 if shangjia.zhuangtai != 1: return Response({ 'code': 2, 'msg': '商家账号状态异常,无法操作', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 3. 获取前端参数 try: data = request.data dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip() chexiao_liyou = data.get('chexiao_liyou', '').strip() except Exception: return Response({ 'code': 3, 'msg': '参数格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 参数验证 if not dingdan_id or not chexiao_liyou: return Response({ 'code': 4, 'msg': '订单ID和撤销原因不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) try: # 5. 使用事务确保数据一致性 with transaction.atomic(): # 查询订单主表 dingdan = Order.query.filter(OrderID=dingdan_id).first() if not dingdan: return Response({ 'code': 5, 'msg': '订单不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 6. 验证订单状态(13=链接待填单,可撤销释放额度) if dingdan.Status not in [1, 7, 13]: return Response({ 'code': 6, 'msg': '当前订单状态不可撤销', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 7. 验证订单所属商家(使用反向查询) shangjia_kuozhan = MerchantOrderExt.query.filter( Order__OrderID=dingdan_id, MerchantID=current_user.UserUID ).first() if not shangjia_kuozhan: return Response({ 'code': 7, 'msg': '您无权撤销此订单', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 8. 创建退款记录(注意:申请状态直接设为1-成功) RefundRecord.query.create( OrderID=dingdan_id, ApplicantID=current_user.UserUID, Reason=chexiao_liyou, ApplyStatus=1, # 直接成功 Amount=dingdan.Amount, PlayerCommission=dingdan.PlayerCommission, Description=dingdan.Description, Remark=dingdan.Remark, Nickname=dingdan.Nickname, ) # 9. 更新商家扩展表(客服额度单不退商家余额,只释放额度) from merchant_ops.services.wallet import is_staff_quota_order staff_quota_order = is_staff_quota_order(dingdan_id) shangjia_updates = { 'tuikuan': F('tuikuan') + 1, 'jinridingdan': F('jinridingdan') - 1, 'jinyuedingdan': F('jinyuedingdan') - 1, 'jinriliushui': F('jinriliushui') - dingdan.Amount, 'jinyueliushui': F('jinyueliushui') - dingdan.Amount, } if not staff_quota_order: shangjia_updates['yue'] = F('yue') + dingdan.Amount UserShangjia.query.filter(id=shangjia.id).update(**shangjia_updates) # 10. 更新订单状态为5(已退款) dingdan.Status = 5 dingdan.save() # 行为驱动 update_shangjia_daily( yonghuid=request.user.UserUID, amount=Decimal(str(dingdan.Amount)), action=3, # 3 = 退款 order_id=dingdan_id, ) return Response({ 'code': 0, 'msg': '撤销成功', 'data': { 'dingdan_id': dingdan_id, 'new_zhuangtai': 5, 'tuikuan_liyou': chexiao_liyou, 'jine': float(dingdan.Amount) if dingdan.Amount else 0.0 } }, status=status.HTTP_200_OK) except Exception as e: # 记录异常日志 logger.error(f"商家撤销订单失败: {str(e)}", exc_info=True) return Response({ 'code': 8, 'msg': '服务器内部错误,撤销失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangjiaTuikuanShenqingView(APIView): """商家退款申请接口""" permission_classes = [IsAuthenticated] # JWT认证 def post(self, request): # 1. 获取当前登录用户并验证商家身份 try: current_user = request.user shangjia = current_user.ShopProfile except UserShangjia.DoesNotExist: return Response({ 'code': 1, 'msg': '您不是商家或商家信息未完善', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 2. 检查商家账号状态 if shangjia.zhuangtai != 1: return Response({ 'code': 2, 'msg': '商家账号状态异常,无法操作', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 3. 获取前端参数 try: data = request.data dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip() tuikuan_liyou = data.get('tuikuan_liyou', '').strip() except Exception: return Response({ 'code': 3, 'msg': '参数格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 参数验证 if not dingdan_id or not tuikuan_liyou: return Response({ 'code': 4, 'msg': '订单ID和退款原因不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) try: # 5. 使用事务确保数据一致性 with transaction.atomic(): # 查询订单主表 dingdan = Order.query.filter(OrderID=dingdan_id).first() if not dingdan: return Response({ 'code': 5, 'msg': '订单不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 6. 验证订单状态 if dingdan.Status not in [2, 8]: return Response({ 'code': 6, 'msg': '当前订单状态不可申请退款', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 7. 验证订单所属商家 shangjia_kuozhan = MerchantOrderExt.query.filter( Order__OrderID=dingdan_id, MerchantID=current_user.UserUID ).first() if not shangjia_kuozhan: return Response({ 'code': 7, 'msg': '您无权为此订单申请退款', 'data': None }, status=status.HTTP_403_FORBIDDEN) # ========== 8. 处理退款记录(修改点:先查询是否存在) ========== refund_record, created = RefundRecord.query.get_or_create( OrderID=dingdan_id, defaults={ 'PlayerID': dingdan.PlayerID, 'ApplicantID': current_user.UserUID, 'Reason': tuikuan_liyou, 'ApplyStatus': 0, # 待处理 'Amount': dingdan.Amount, 'PlayerCommission': dingdan.PlayerCommission, 'Description': dingdan.Description, 'Remark': dingdan.Remark, 'Nickname': dingdan.Nickname, } ) if not created: # 记录已存在,更新退款理由和申请时间 refund_record.Reason = tuikuan_liyou refund_record.save(update_fields=['Reason', 'UpdateTime']) logger.info(f"订单 {dingdan_id} 已有退款记录,已更新退款理由") # 9. 更新订单状态为4(退款中) dingdan.Status = 4 dingdan.RefundReason = tuikuan_liyou dingdan.save() # ====== 原有功能:更新打手状态 ====== if dingdan.PlayerID: try: dashou_main = User.query.filter( UserUID=dingdan.PlayerID ).first() if dashou_main and hasattr(dashou_main, 'DashouProfile'): dashou_profile = dashou_main.DashouProfile dashou_profile.zhuangtai = 1 dashou_profile.save() logger.info(f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.PlayerID}状态变更为1") except Exception as e: logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id},打手ID{dingdan.PlayerID}") return Response({ 'code': 0, 'msg': '退款申请提交成功', 'data': { 'dingdan_id': dingdan_id, 'new_zhuangtai': 4, 'tuikuan_liyou': tuikuan_liyou, 'jine': float(dingdan.Amount) if dingdan.Amount else 0.0, 'dashou_id': dingdan.PlayerID } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"商家退款申请失败: {str(e)}", exc_info=True) return Response({ 'code': 8, 'msg': '服务器内部错误,申请失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangjiaChufaShenqingView(APIView): """商家处罚申请接口(已添加图片上传功能)""" permission_classes = [IsAuthenticated] # JWT认证 def post(self, request): # 1. 获取当前登录用户并验证商家身份 try: current_user = request.user shangjia = current_user.ShopProfile except UserShangjia.DoesNotExist: return Response({ 'code': 1, 'msg': '您不是商家或商家信息未完善', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 2. 检查商家账号状态 if shangjia.zhuangtai != 1: return Response({ 'code': 2, 'msg': '商家账号状态异常,无法操作', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 3. 获取前端参数 try: data = request.data dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip() dashou_id = data.get('dashou_id', '').strip() chufa_liyou = data.get('chufa_liyou', '').strip() chufa_tupian_urls = data.get('chufa_tupian_urls', []) # 🔴 新增:图片URL数组 except Exception: return Response({ 'code': 3, 'msg': '参数格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 参数验证 if not dingdan_id or not dashou_id or not chufa_liyou: return Response({ 'code': 4, 'msg': '订单ID、打手ID和处罚原因不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) try: # 5. 使用事务确保数据一致性 with transaction.atomic(): # 验证打手身份 try: dashou_user = User.query.get(UserUID=dashou_id) dashou = dashou_user.DashouProfile except User.DoesNotExist: return Response({ 'code': 5, 'msg': '打手用户不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) except UserDashou.DoesNotExist: return Response({ 'code': 5, 'msg': '该用户不是打手', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 查询订单主表 dingdan = Order.query.filter(OrderID=dingdan_id).first() if not dingdan: return Response({ 'code': 6, 'msg': '订单不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 验证订单状态 if dingdan.Status not in [2, 8, 4, 5, 6, 3]: return Response({ 'code': 7, 'msg': '当前订单状态不可申请处罚', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 验证订单所属商家 shangjia_kuozhan = MerchantOrderExt.query.filter( Order__OrderID=dingdan_id, MerchantID=current_user.UserUID ).first() if not shangjia_kuozhan: return Response({ 'code': 8, 'msg': '您无权为此订单申请处罚', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 验证订单的打手是否与提供的打手ID一致 if dingdan.PlayerID != dashou_id: return Response({ 'code': 9, 'msg': '该打手并非此订单的服务打手', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 创建处罚记录 chufajilu = PenaltyRecord.query.create( PlayerID=dashou_id, ApplicantID=current_user.UserUID, PenaltyReason=chufa_liyou, ApplyStatus=0, # 待处理 DeductedPoints=5, # 扣除5积分 OrderID=dingdan_id, ) # 🔴【新增】在创建处罚记录后立即扣除打手积分 # 注意:这里使用前面已经获取的dashou对象(打手扩展表) jifen_kouchu = chufajilu.DeductedPoints # 扣除的积分,这里是5 if jifen_kouchu > 0: # 检查打手积分是否足够(UserDashou 字段为 jifen) if dashou.jifen < jifen_kouchu: dashou.jifen = 0 else: dashou.jifen -= jifen_kouchu dashou.save() # 🔴【新增】保存处罚图片到数据库 if chufa_tupian_urls and isinstance(chufa_tupian_urls, list): for tupian_url in chufa_tupian_urls: if tupian_url and tupian_url.strip(): PenaltyEvidenceImage.query.create( OrderID=dingdan_id, UserID=current_user.UserUID, ImageURL=tupian_url ) # 更新商家订单扩展表的处罚相关字段 shangjia_kuozhan.PenaltyReason = chufa_liyou shangjia_kuozhan.PenaltyApplyStatus = 0 shangjia_kuozhan.save() return Response({ 'code': 0, 'msg': '处罚申请提交成功', 'data': { 'dingdan_id': dingdan_id, 'dashou_id': dashou_id, 'chufa_liyou': chufa_liyou, 'sqzhuangtai': 0, 'jifen': 5, 'tupian_count': len(chufa_tupian_urls) if chufa_tupian_urls else 0 # 返回图片数量 } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"商家处罚申请失败: {str(e)}", exc_info=True) return Response({ 'code': 10, 'msg': '服务器内部错误,申请失败', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class DashouDingdanHuoquView(APIView): """ 获取可抢订单列表(支持标签筛选) POST /dingdan/ddhq 需要JWT Token认证 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取请求参数 data = request.data leixing_id = pick_leixing_id(data) page = int(data.get('page', 1)) page_size = int(data.get('page_size', 10)) biaoqian_id = data.get('biaoqian_id') # 2. 构建基础查询条件 base_query = Q(Status__in=[1, 7]) if leixing_id is not None: base_query &= Q(ProductTypeID=leixing_id) # 3. 标签筛选:先查出符合条件的订单ID,避免跨表 JOIN 排序冲突 if biaoqian_id and int(biaoqian_id) > 0: biaoqian_order_ids = DingdanBiaoqian.query.filter( chenghao_id=int(biaoqian_id) ).values_list('dingdan_id', flat=True) base_query &= Q(OrderID__in=list(biaoqian_order_ids)) # 4. 构建基础查询集 base_qs = Order.query.filter(base_query).order_by('-CreateTime').distinct() # 5. 获取总记录数 total = base_qs.count() # 6. 计算分页偏移量 offset = (page - 1) * page_size limit = page_size # 7. 获取当前页数据 page_qs = base_qs[offset:offset + limit] # 8. 使用子查询获取商家昵称(Subquery 须用 Django 原生 QuerySet,不能用 .query.values()) shangjia_sub = MerchantOrderExt.objects.filter( Order__OrderID=OuterRef('OrderID') ).values('MerchantNickname')[:1] # 9. 获取当前页所有需要的字段 dingdan_values = page_qs.annotate( MerchantNickname=Subquery(shangjia_sub) ).values( 'OrderID', 'Status', 'Platform', 'PlayerCommission', 'AssignedID', 'ProductTypeID', 'ImageURL', 'GrabRequirement', 'MembershipID', 'CommissionReq', 'Description', 'Remark', 'CreateTime', 'MerchantNickname' ) # 10. 转换为列表 dingdan_list = list(dingdan_values) # 11. 批量获取指定打手的头像和昵称 zhiding_ids = {item['AssignedID'] for item in dingdan_list if item.get('AssignedID')} zhiding_info = {} if zhiding_ids: dashou_query = User.query.filter( UserUID__in=zhiding_ids ).select_related('DashouProfile').annotate( Nickname=F('DashouProfile__nicheng') ).values('UserUID', 'Avatar', 'Nickname') zhiding_info = { item['UserUID']: { 'avatar': item['Avatar'] or '', 'nicheng': item['Nickname'] or '' } for item in dashou_query } # ============ 批量获取三层标签 ============ order_ids = [item['OrderID'] for item in dingdan_list] # --- 订单需求标签 --- xuqiu_tags_map = {} if order_ids: try: dbq_qs = DingdanBiaoqian.query.filter( dingdan_id__in=order_ids ).select_related('chenghao') for dbq in dbq_qs: oid = dbq.dingdan_id if oid not in xuqiu_tags_map: xuqiu_tags_map[oid] = [] ch = dbq.chenghao xuqiu_tags_map[oid].append({ 'id': ch.id, 'mingcheng': ch.mingcheng, 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) }) except Exception as e: logger.error(f"查询订单需求标签失败: {e}", exc_info=True) # --- 所有需要查询标签的用户ID集合(指定打手 + 商家) --- yonghuid_set = set(zhiding_ids) shangjia_order_ids = [item['OrderID'] for item in dingdan_list if item['Platform'] == 2] shangjia_id_map = {} if shangjia_order_ids: # 使用跨表查询 Order__OrderID__in 匹配字符串订单ID sj_exts = MerchantOrderExt.query.filter( Order__OrderID__in=shangjia_order_ids ).values('Order__OrderID', 'MerchantID') for sj in sj_exts: shangjia_id_map[sj['Order__OrderID']] = sj['MerchantID'] if sj['MerchantID']: yonghuid_set.add(sj['MerchantID']) # 将 yonghuid 字符串转换为 User 的主键 UserUUID user_id_map = {} if yonghuid_set: users = User.query.filter(UserUID__in=yonghuid_set).values('UserUUID', 'UserUID') for u in users: user_id_map[u['UserUID']] = u['UserUUID'] user_main_ids = list(user_id_map.values()) user_tags_map = {} if user_main_ids: uc_qs = YonghuChenghao.query.filter( yonghu_id__in=user_main_ids ).select_related('chenghao') for uc in uc_qs: uid = uc.yonghu_id if uid not in user_tags_map: user_tags_map[uid] = [] user_tags_map[uid].append(uc.chenghao) dashou_tags_map = {} shangjia_tags_map = {} for item in dingdan_list: oid = item['OrderID'] # 指定打手标签 zhid = item['AssignedID'] dashou_tags = [] if zhid and zhid in user_id_map: uid = user_id_map[zhid] if uid in user_tags_map: for ch in user_tags_map[uid]: if ch.leixing == 'PlayerID': dashou_tags.append({ 'id': ch.id, 'mingcheng': ch.mingcheng, 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) }) dashou_tags_map[oid] = dashou_tags # 商家标签 sj_tags = [] if item['Platform'] == 2 and oid in shangjia_id_map: sj_yonghuid = shangjia_id_map[oid] if sj_yonghuid and sj_yonghuid in user_id_map: uid = user_id_map[sj_yonghuid] if uid in user_tags_map: for ch in user_tags_map[uid]: if ch.leixing == 'shangjia': sj_tags.append({ 'id': ch.id, 'mingcheng': ch.mingcheng, 'texiao_json': self._parse_texiao_json(ch.texiao_miaoshu) }) shangjia_tags_map[oid] = sj_tags # 12. 格式化返回数据 formatted_list = [] for order in dingdan_list: oid = order['OrderID'] zid = order.get('AssignedID') info = zhiding_info.get(zid, {}) formatted_list.append({ 'dingdan_id': order['OrderID'], 'zhuangtai': order['Status'], 'pingtai': order['Platform'], 'dashou_fencheng': float(order['PlayerCommission']) if order['PlayerCommission'] else 0.0, 'leixing_id': order['ProductTypeID'] or 0, 'tupian': order['ImageURL'] or '', 'yaoqiuleixing': order['GrabRequirement'] or 0, 'huiyuan_id': order['MembershipID'] or '', 'yajin': float(order['CommissionReq']) if order['CommissionReq'] else 0.0, 'jieshao': order['Description'] or '', 'beizhu': order['Remark'] or '', 'creat_time': fmt_datetime(order['CreateTime']), 'create_time': fmt_datetime(order['CreateTime']), 'sjnicheng': order['MerchantNickname'] or '', 'zhiding_uid': order['AssignedID'] or '', 'zhiding_avatar': info.get('avatar', ''), 'zhiding_nicheng': info.get('nicheng', ''), # 三层标签 'xuqiu_biaoqian': xuqiu_tags_map.get(oid, []), 'dashou_biaoqian': dashou_tags_map.get(oid, []), 'shangjia_biaoqian': shangjia_tags_map.get(oid, []), }) has_more = (page * page_size) < total return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'list': formatted_list, 'has_more': has_more, 'total': total, 'current_page': page, 'page_size': page_size, 'total_pages': (total + page_size - 1) // page_size, } }) except Exception as e: logger.error(f"订单列表查询失败: {e}", exc_info=True) return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'}, status=500) def _parse_texiao_json(self, texiao_miaoshu): if not texiao_miaoshu: return {} try: return json.loads(texiao_miaoshu) except: return {} class ShangpinLeixingHuoquView(APIView): """ 获取商品类型列表 POST /dingdan/ddlxhq 需要JWT Token认证 """ authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] def post(self, request): # 1. 查询商品类型表 # 假设你的商品类型模型字段为:id, jieshao, tupian_url leixing_list = ShangpinLeixing.query.all().values( 'id', # 商品类型ID 'jieshao', # 类型介绍 'tupian_url' # 类型图片URL(相对URL) ).order_by('id') # 按ID排序,可根据需要调整 # 2. 格式化返回数据 formatted_list = [] for leixing in leixing_list: formatted_leixing = { 'id': leixing['id'], 'jieshao': leixing['jieshao'] or '', # 处理空值 'tupian_url': leixing['tupian_url'] or '' # 处理空值 } formatted_list.append(formatted_leixing) # 3. 返回响应 return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'list': formatted_list, 'total': len(formatted_list) } }) # ----------------- 基础配置获取 ----------------- def get_self_goeasy_appkey(): try: return getattr(settings, 'GOEASY_APPKEY', None) except Exception: return None def get_self_goeasy_secret(): try: return getattr(settings, 'GOEASY_SECRET', None) except Exception: return None def get_self_storage_domain(): try: return getattr(settings, 'COS_DOMAIN', '') except Exception: return '' def full_avatar(relative_url, bucket_domain=None): if not relative_url: return '' if relative_url.startswith('http'): return relative_url domain = bucket_domain or get_self_storage_domain() if domain: domain = domain.rstrip('/') + '/' return domain + relative_url.lstrip('/') return relative_url # ----------------- GoEasy API 封装 ----------------- def subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None): """订阅用户到群组,确保能收发该群消息""" if not appkey: appkey = get_self_goeasy_appkey() if not secret: secret = get_self_goeasy_secret() if not appkey: logger.error("订阅失败:GoEasy AppKey 未配置") return False url = 'https://rest-hangzhou.goeasy.io/v2/im/subscribe-groups' body = {"appkey": appkey, "userIds": user_ids, "groupIds": group_ids} headers = {"Content-Type": "application/json"} if secret: headers["Authorization"] = f"Bearer {secret}" try: r = requests.post(url, headers=headers, json=body, timeout=10) if r.status_code == 200: logger.info(f"订阅成功:用户 {user_ids} -> 群 {group_ids}") return True logger.error(f"订阅失败:{r.status_code} {r.text}") return False except Exception as e: logger.exception("订阅异常") return False def send_group_message( appkey, secret, group_id, sender_id, sender_name, sender_avatar, message_text='', custom_payload=None, group_name='', order_id='' ): """ 通过 GoEasy REST API 发送群消息。 message_text: 普通文本内容 custom_payload: 自定义消息体(如订单卡片),此时 type 设为 'order' """ url = 'https://rest-hangzhou.goeasy.io/v2/im/message' to_data = { "name": group_name or group_id, "avatar": sender_avatar or '', "orderId": order_id, } msg_type = 'order' if custom_payload else 'text' payload = custom_payload if custom_payload else message_text body = { "appkey": appkey, "senderId": sender_id, "senderData": {"avatar": sender_avatar, "name": sender_name}, "to": {"type": "group", "id": group_id, "data": to_data}, "type": msg_type, "payload": payload } headers = {"Content-Type": "application/json"} if secret: headers["Authorization"] = f"Bearer {secret}" for attempt in range(3): # 最多重试3次 try: r = requests.post(url, headers=headers, json=body, timeout=10) if r.status_code == 200: logger.info(f"消息发送成功:sender={sender_id}, group={group_id}, type={msg_type}") return True logger.warning(f"消息发送失败 (尝试 {attempt+1}/3):状态码 {r.status_code}, 响应 {r.text}") except Exception as e: logger.warning(f"消息发送异常 (尝试 {attempt+1}/3):{e}") if attempt < 2: time.sleep(1) logger.error(f"消息最终发送失败:sender={sender_id}, group={group_id}") return False # ----------------- 权限校验 ----------------- def check_user_permission(identity_type, uid, order): """验证用户身份、账号状态、以及是否属于该订单""" try: user = User.query.get(UserUID=uid) except User.DoesNotExist: return False, '用户不存在' if identity_type == 'PlayerID': try: dashou = UserDashou.query.get(user=user) if dashou.zhanghaozhuangtai != 1 : return False, '打手账号已被封禁或状态异常' except UserDashou.DoesNotExist: return False, '该用户不是打手' if order.PlayerID != uid: return False, '你不是该订单的接单打手' elif identity_type == 'shangjia': try: shop = UserShangjia.query.get(user=user) if shop.zhuangtai != 1: return False, '商家账号已被禁用' except UserShangjia.DoesNotExist: return False, '该用户不是商家' try: if order.shangjia_kuozhan.MerchantID != uid: return False, '你不是该订单的发布商家' except MerchantOrderExt.DoesNotExist: return False, '订单缺少商家信息' elif identity_type == 'boss': try: UserBoss.query.get(user=user) # 老板暂无封禁字段,仅需存在 except UserBoss.DoesNotExist: return False, '该用户不是老板' try: if order.pingtai_kuozhan.BossID != uid: return False, '你不是该订单的老板' except PlatformOrderExt.DoesNotExist: return False, '订单缺少平台信息' else: return False, '不支持的身份类型' return True, '' # ----------------- 主视图 ----------------- class DaiLiQunLiaoXiaoXiView(APIView): """ 代理发送群聊消息 """ permission_classes = [permissions.IsAuthenticated] def post(self, request): # ===== 1. 参数解析 ===== order_id = request.data.get('orderId') identity_type = request.data.get('identityType') # dashou / shangjia / boss sender_name = request.data.get('senderName', '') sender_avatar_raw = request.data.get('senderAvatar', '') message_type = request.data.get('messageType', 'text') message_text = request.data.get('text', '') order_payload = request.data.get('orderPayload', None) logger.info(f"[群聊代理] 收到请求: orderId={order_id}, identityType={identity_type}, " f"messageType={message_type}, text={message_text[:20]}...") if not order_id or not identity_type: return Response({'code': 400, 'msg': '缺少 orderId 或 identityType'}) if identity_type not in ('PlayerID', 'shangjia', 'boss'): return Response({'code': 400, 'msg': 'identityType 无效'}) # ===== 2. 构造 senderId ===== uid = request.user.UserUID prefix_map = {'PlayerID': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'} sender_id = prefix_map[identity_type] + uid logger.info(f"[群聊代理] 登录用户 UserID={uid}, 构造 senderId={sender_id}") # ===== 3. 查询订单 ===== try: order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=order_id) except Order.DoesNotExist: logger.warning(f"[群聊代理] 订单 {order_id} 不存在") return Response({'code': 404, 'msg': '订单不存在'}) # ===== 4. 权限校验 ===== valid, err_msg = check_user_permission(identity_type, uid, order) if not valid: logger.warning(f"[群聊代理] 权限校验失败: {err_msg}, uid={uid}, identity={identity_type}") return Response({'code': 403, 'msg': err_msg}) # ===== 5. 我方 GoEasy 配置 ===== self_appkey = get_self_goeasy_appkey() self_secret = get_self_goeasy_secret() if not self_appkey: logger.error("[群聊代理] 我方 GoEasy AppKey 未配置") return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) # ===== 6. 公共群信息 ===== real_group_id = f"group_{order.OrderID}" group_name = (order.Description[:20] + '…') if order.Description and len(order.Description) > 20 else f"订单{order.OrderID}" sender_avatar = full_avatar(sender_avatar_raw) if sender_avatar_raw else full_avatar('') # ===== 7. 订阅发送者到群 ===== if not subscribe_users_to_group([sender_id], [real_group_id], self_appkey, self_secret): logger.error("[群聊代理] 订阅群组失败") return Response({'code': 500, 'msg': '订阅群组失败'}) custom_payload = order_payload if message_type == 'order' else None text_content = message_text # ---------- 向我方群发送消息 ---------- local_ok = send_group_message( appkey=self_appkey, secret=self_secret, group_id=real_group_id, sender_id=sender_id, sender_name=sender_name, sender_avatar=sender_avatar, message_text=text_content, custom_payload=custom_payload, group_name=group_name, order_id=order.OrderID, ) if not local_ok: return Response({'code': 500, 'msg': '消息发送失败'}) return Response({'code': 200, 'msg': '发送成功'}) class ShangjiaLianjieLiuYanZhuanFaView(APIView): """ 商家链接留言转发接口 路径:POST /dingdan/sjljlbxxzf 固定以 B{商家ID} 身份发送群消息。 """ permission_classes = [] def post(self, request): token = request.data.get('token') text = (request.data.get('text') or '').strip() if not token or not text: return Response({'code': 400, 'msg': '缺少参数'}) try: lianjie = ShangjiaLianjie.query.get(LinkToken=token) except ShangjiaLianjie.DoesNotExist: return Response({'code': 404, 'msg': '链接不存在或已失效'}) if not lianjie.OrderID: return Response({'code': 400, 'msg': '链接缺少订单ID'}) try: order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=lianjie.OrderID) except Order.DoesNotExist: return Response({'code': 500, 'msg': '订单数据异常'}) shangjia_uid = lianjie.UserID sender_id = f"B{shangjia_uid}" sender_name = "老板" sender_avatar = '' appkey = get_self_goeasy_appkey() secret = get_self_goeasy_secret() if not appkey: return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'}) group_id = f"group_{order.OrderID}" group_name = (order.Description[:20] + '…') if order.Description and len(order.Description) > 20 else f"订单{order.OrderID}" if not _subscribe_users_to_group([sender_id], [group_id], appkey, secret): return Response({'code': 500, 'msg': '订阅群组失败'}) local_ok = _send_group_message( appkey=appkey, secret=secret, group_id=group_id, sender_id=sender_id, sender_name=sender_name, sender_avatar=sender_avatar, message_text=text, custom_payload=None, group_name=group_name, order_id=order.OrderID, ) if not local_ok: return Response({'code': 500, 'msg': '消息发送失败'}) return Response({'code': 200, 'msg': '发送成功'}) class QiangdanView(APIView): authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] def post(self, request): data = request.data dingdan_id = data.get('dingdan_id') or data.get('OrderID') if not dingdan_id: return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400) try: with transaction.atomic(): # 1. 验证打手身份和状态 dashou_profile = self._validate_dashou_status(request.user) if not dashou_profile: return Response({'code': 403, 'msg': '您不是打手或账号状态异常'}, status=403) # ==================== 【新增】罚单检查 ==================== fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(request.user.UserUID,1) if not fadan_ok: return Response({'code': 400, 'msg': fadan_msg}) # ========================================================= # ========== 标签快速校验(提前拒绝,避免锁资源) ========== biaoqian_ok, biaoqian_msg = check_dashou_biaoqian_required(dingdan_id, request.user.UserUID) if not biaoqian_ok: return Response({'code': 400, 'msg': biaoqian_msg}) # =========================================================== # 2. 锁定并验证订单(行锁) order_result = self._validate_and_lock_order(dingdan_id, request.user.UserUID) if isinstance(order_result, str): return Response({'code': 400, 'msg': order_result}) order = order_result validation_result = self._validate_order_requirements(order, dashou_profile, request.user.UserUID) if not validation_result['success']: return Response({'code': 400, 'msg': validation_result['message']}) self._execute_qiangdan(order, dashou_profile, request.user.UserUID) # ========== 【唯一改动】聊天建立方式 ========== chat_success = establish_order_chat(dingdan_id) try: update_dashou_daily_by_action( yonghuid=order.PlayerID, amount=order.PlayerCommission, action=1 ) logger.info(f"打手 {order.PlayerID} 每日统计更新成功") except Exception as e: logger.error(f"打手每日统计更新失败: {e}") # 在 QiangdanView.post 里,_execute_qiangdan 之后添加: # 记录抢单表现(不影响主流程) # 如果订单表里有 bankuai 信息,可以用 order.bankuai 代替 None record_dashou_biaoxian( dingdan_id=dingdan_id, dashou_id=order.PlayerID, xingwei=1, jiedan_time=timezone.now(), # 直接取当前时间 bankuai=None # 暂不填,后续提交时补上 ) return Response({ 'code': 200, 'msg': '抢单成功', 'data': { 'dingdan_id': dingdan_id, 'chat_established': chat_success } }) except Exception as e: logger.error(f"抢单异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '系统异常,请稍后重试'}, status=500) # ---------- 以下所有方法原封不动照抄旧版 ---------- def _validate_dashou_status(self, user): try: dashou_profile = user.DashouProfile if (dashou_profile.zhuangtai != 1 or dashou_profile.zhanghaozhuangtai != 1 or dashou_profile.jifen < 5): return None return dashou_profile except UserDashou.DoesNotExist: return None def _validate_and_lock_order(self, dingdan_id, dashou_yonghuid): try: order = Order.query.select_for_update().get(OrderID=dingdan_id) if order.Status not in [1, 7]: return "订单状态不可抢" if order.Status == 7: if not order.AssignedID or order.AssignedID != dashou_yonghuid: return "此订单为指定单,您不是指定打手" if order.PlayerID: return "订单已被其他打手抢走" return order except Order.DoesNotExist: return "订单不存在" def _validate_order_requirements(self, order, dashou_profile, dashou_yonghuid): if order.GrabRequirement == 1: return self._validate_huiyuan_requirement(order, dashou_yonghuid) elif order.GrabRequirement == 2: return self._validate_yajin_requirement(order, dashou_profile) return {'success': True, 'message': '验证通过'} def _validate_huiyuan_requirement(self, order, dashou_yonghuid): try: huiyuan_record = Huiyuangoumai.query.get( huiyuan_id=order.MembershipID, yonghu_id=dashou_yonghuid ) if huiyuan_record.jiance_shifou_daoqi() or huiyuan_record.huiyuan_zhuangtai != 1: return {'success': False, 'message': '会员已过期或未开通对应会员'} return {'success': True, 'message': '会员验证通过'} except Huiyuangoumai.DoesNotExist: return {'success': False, 'message': '未开通对应会员,无法抢此订单'} def _validate_yajin_requirement(self, order, dashou_profile): yajin_required = order.CommissionReq or 0 dashou_yajin = dashou_profile.yajin or 0 if dashou_yajin < yajin_required: return {'success': False, 'message': f'押金不足,需要{yajin_required}元,当前有{dashou_yajin}元'} return {'success': True, 'message': '押金验证通过'} def _execute_qiangdan(self, order, dashou_profile, dashou_yonghuid): order.Status = 2 order.PlayerID = dashou_yonghuid order.User2ID = f"Ds{dashou_yonghuid}" order.save() dashou_profile.zhuangtai = 0 dashou_profile.jiedanzongliang = F('jiedanzongliang') + 1 dashou_profile.jinrijiedan = F('jinrijiedan') + 1 dashou_profile.jinyuejiedan = F('jinyuejiedan') + 1 dashou_profile.save() class DashouDingdanHuoquView1(APIView): """ 打手订单获取接口 支持筛选条件: - zhuangtai_list: 订单状态列表(可选,空数组或不传表示全部状态) - leixing_id: 商品类型ID(可选) - keyword: 搜索关键词,模糊匹配订单ID和订单介绍(可选) """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 获取当前登录用户的 yonghuid try: current_user = request.user yonghuid = current_user.UserUID except AttributeError: return Response({ 'code': 1, 'msg': '用户认证失败', 'data': None }, status=status.HTTP_401_UNAUTHORIZED) # 2. 获取并校验前端参数 try: data = request.data zhuangtai_list = data.get('zhuangtai_list', []) # 可选,默认空列表(全部) page = max(1, int(data.get('page', 1))) page_size = min(100, max(1, int(data.get('page_size', 10)))) leixing_id = pick_leixing_id(data) # 可选,商品类型ID keyword = data.get('keyword', '').strip() # 可选,搜索关键词 except (ValueError, TypeError): return Response({ 'code': 2, 'msg': '参数格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 3. 参数默认值与类型修正 if not isinstance(zhuangtai_list, list): zhuangtai_list = [] # 非列表则视为全部 # 4. 构建查询条件 try: # 基础条件:当前打手接单 query = Q(PlayerID=yonghuid) # 状态筛选:如果 zhuangtai_list 不为空列表,则加入筛选;否则查询全部状态 if zhuangtai_list: query &= Q(Status__in=zhuangtai_list) # 商品类型筛选 if leixing_id is not None: query &= Q(ProductTypeID=leixing_id) # 关键词搜索:模糊匹配订单ID和订单介绍 if keyword: keyword_query = Q(OrderID__icontains=keyword) | Q(Description__icontains=keyword) query &= keyword_query # 5. 执行查询,按创建时间倒序 dingdan_query = Order.query.filter(query).order_by('-CreateTime') # 只选择需要的字段,提高性能 dingdan_query = dingdan_query.only( 'OrderID', 'Status', 'PlayerCommission', # 打手分成金额 'Description', 'ImageURL', 'Nickname', 'CreateTime' ) total_count = dingdan_query.count() # 6. 分页处理 start_index = (page - 1) * page_size end_index = start_index + page_size current_page_orders = dingdan_query[start_index:end_index] current_page_count = len(current_page_orders) # 7. 构建返回数据列表 order_list = [] for order in current_page_orders: order_data = { 'dingdan_id': order.OrderID, 'zhuangtai': order.Status, 'jine': float(order.PlayerCommission) if order.PlayerCommission else 0.0, 'jieshao': order.Description or '', 'tupian': order.ImageURL or '', 'nicheng': order.Nickname or '', **datetime_aliases(order.CreateTime), } order_list.append(order_data) # 8. 判断是否还有更多数据 has_more = (current_page_count == page_size) and (end_index < total_count) if total_count == 0: has_more = False return Response({ 'code': 0, 'msg': '成功', 'data': { 'list': order_list, 'has_more': has_more, 'total': total_count } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"打手订单获取失败: {str(e)}", exc_info=True) return Response({ 'code': 4, 'msg': '服务器内部错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class DashouCOSZhengshuView(APIView): """打手获取COS上传凭证接口(调试版本)""" permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 接收参数(兼容 OrderID / dingdan_id) dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip() yongtu = request.data.get('yongtu', 'dingdan') # 2. 条件判断修正:除了 fakuan 之外,其他用途均需提供 dingdan_id if yongtu != 'fakuan' and not dingdan_id: return Response( {'code': 400, 'msg': '订单ID不能为空', 'error': 'dingdan_id is required when yongtu is not fakuan'}, status=status.HTTP_400_BAD_REQUEST ) # 3. 业务验证逻辑(根据用途分别验证) if yongtu == 'chufa': try: chufa_record = PenaltyRecord.query.filter( OrderID=dingdan_id, PlayerID=request.user.UserUID, ApplyStatus__in=[0, 3] ).first() if not chufa_record: return Response( {'code': 403, 'msg': '处罚记录无权限或状态不对', 'error': f'No punishment record found for user {request.user.UserUID} on order {dingdan_id}'}, status=status.HTTP_403_FORBIDDEN ) except Exception as e: logger.error(f"查处罚记录异常: {str(e)}") return Response( {'code': 500, 'msg': '服务器内部错误', 'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR ) elif yongtu == 'fakuan': # 罚款申诉不需要订单验证 pass else: # 默认为订单相关,需要验证订单且打手必须接单 try: dingdan = Order.query.get( OrderID=dingdan_id, PlayerID=request.user.UserUID, Status__in=[2, 8] ) except Order.DoesNotExist: return Response( {'code': 403, 'msg': '此功能暂未开放', 'error': f'Order {dingdan_id} not found or not assigned to user {request.user.UserUID}'}, status=status.HTTP_403_FORBIDDEN ) # 4. 获取腾讯云STS临时密钥 try: # 初始化固定凭证 cred = credential.Credential( settings.COS_SECRET_ID, settings.COS_SECRET_KEY ) # 创建STS客户端 http_profile = HttpProfile() http_profile.endpoint = "sts.tencentcloudapi.com" client_profile = ClientProfile() client_profile.httpProfile = http_profile client = sts_client.StsClient(cred, "ap-shanghai", client_profile) # 构建策略资源 resources = [] if yongtu == 'chufa': resources.append( f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/dssstp/*" ) elif yongtu == 'fakuan': resources.append( f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/fakuan/dashoufakuan/dashoufakuanshensu/*" ) else: resources.append( f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*" ) policy = { "version": "2.0", "statement": [{ "effect": "allow", "action": [ "cos:PutObject", "cos:PostObject", "cos:InitiateMultipartUpload", "cos:UploadPart", "cos:CompleteMultipartUpload" ], "resource": resources }] } req = models.GetFederationTokenRequest() req.Name = f"dashou_{dingdan_id}_{int(time.time())}" req.Policy = json.dumps(policy) req.DurationSeconds = 7200 resp = client.GetFederationToken(req) credentials = resp.Credentials except TencentCloudSDKException as e: logger.error(f"获取STS临时密钥失败: {str(e)}") return Response({ 'code': 500, 'msg': f'获取上传凭证失败: {str(e)}', 'error_detail': str(e) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 5. 构建响应 current_time = int(time.time()) upload_dir = 'fakuan/dashoufakuan/dashoufakuanshensu/' if yongtu == 'fakuan' else ( 'a_long/chfajltp/dssstp/' if yongtu == 'chufa' else f'order/{dingdan_id}/' ) response_data = { 'credentials': { 'tmpSecretId': credentials.TmpSecretId, 'tmpSecretKey': credentials.TmpSecretKey, 'sessionToken': credentials.Token, }, 'startTime': current_time, 'expiredTime': current_time + 7200, 'bucket': settings.COS_BUCKET, 'region': settings.COS_REGION, 'upload_dir': upload_dir, } return Response({'code': 0, 'msg': '成功', 'data': response_data}) except Exception as e: # 捕捉所有未预期的异常,返回完整 traceback logger.error(f'COS凭证接口未知错误: {traceback.format_exc()}') return Response({ 'code': 500, 'msg': f'服务器内部错误: {str(e)}', 'error_detail': traceback.format_exc() }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class ShangjiaCOSZhengshuView(APIView): """商家获取COS上传凭证接口(用于处罚申诉图片上传)""" permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 接收参数 dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip() yongtu = request.data.get('yongtu', 'chufa') # 用途:chufa=处罚申诉 if not dingdan_id: return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 2. 🔴【修改】验证商家身份 - 通过反向查询shop_profile判断是否为商家 try: # 检查是否有商家扩展表 shangjia_profile = request.user.ShopProfile # 验证商家状态是否为1(正常) if shangjia_profile.zhuangtai != 1: return Response({'code': 3, 'msg': '商家账号状态异常,无法操作', 'data': None}, status=status.HTTP_403_FORBIDDEN) # 🔴【重要】获取商家ID(从商家扩展表中获取) shangjia_id = request.user.UserUID # 假设商家扩展表中有yonghuid字段 except AttributeError: # 没有shop_profile属性,说明不是商家 return Response({'code': 3, 'msg': '非商家用户无权限', 'data': None}, status=status.HTTP_403_FORBIDDEN) # 3. 🔴【修改】验证订单权限 - 通过商家订单扩展表验证 try: # 获取订单 dingdan = Order.query.get(OrderID=dingdan_id) # 🔴【重要】通过反向查询商家扩展表验证订单是否属于当前商家 try: shangjia_kuozhan = dingdan.shangjia_kuozhan # 使用反向关系名 # 验证商家ID是否匹配 if str(shangjia_kuozhan.MerchantID) != str(shangjia_id): return Response({'code': 2, 'msg': '订单不属于当前商家', 'data': None}, status=status.HTTP_403_FORBIDDEN) # 验证订单状态是否允许申请处罚(根据前端逻辑) allowed_zhuangtai = [2, 4, 5, 6, 8] if dingdan.Status not in allowed_zhuangtai: return Response({'code': 4, 'msg': '当前订单状态不允许申请处罚', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # 🔴【新增】检查是否已存在处罚申请(通过Chufajilu模型) # 注意:这里假设Chufajilu模型存在,并且有shangjia_id字段 try: existing_chufa = PenaltyRecord.query.filter( OrderID=dingdan_id, ApplicantID=shangjia_id, ApplyStatus__in=[0, 1] # 审核中或已处罚状态 ).exists() if existing_chufa: return Response({'code': 5, 'msg': '该订单已有处罚申请,请勿重复上传', 'data': None}, status=status.HTTP_400_BAD_REQUEST) except ImportError: # 如果没有Chufajilu模型,跳过这个检查 pass except MerchantOrderExt.DoesNotExist: # 没有商家扩展记录,说明这不是商家订单 return Response({'code': 2, 'msg': '非商家订单无法操作', 'data': None}, status=status.HTTP_403_FORBIDDEN) except Order.DoesNotExist: return Response({'code': 2, 'msg': '订单不存在', 'data': None}, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f"订单验证异常: {str(e)}") return Response({'code': 500, 'msg': '服务器验证异常', 'data': None}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 4. 获取腾讯云STS临时密钥 try: # 初始化固定凭证 cred = credential.Credential( settings.COS_SECRET_ID, settings.COS_SECRET_KEY ) # 创建STS客户端 http_profile = HttpProfile() http_profile.endpoint = "sts.tencentcloudapi.com" client_profile = ClientProfile() client_profile.httpProfile = http_profile client = sts_client.StsClient(cred, "ap-shanghai", client_profile) # 🔴【修改】构建策略 - 只允许上传到商家处罚证据图片目录 resources = [ f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/a_long/chfajltp/sjzjtp/*" ] policy = { "version": "2.0", "statement": [ { "effect": "allow", "action": [ "cos:PutObject", "cos:PostObject", "cos:InitiateMultipartUpload", "cos:UploadPart", "cos:CompleteMultipartUpload" ], "resource": resources } ] } # 请求STS临时凭证 req = models.GetFederationTokenRequest() # 🔴【修改】命名包含商家ID req.Name = f"shangjia_{shangjia_id}_{dingdan_id}_{int(time.time())}" req.Policy = json.dumps(policy) req.DurationSeconds = 7200 # 2小时有效期 resp = client.GetFederationToken(req) # 提取临时密钥 credentials = resp.Credentials except TencentCloudSDKException as e: logger.error(f"商家获取STS临时密钥失败: {str(e)}") return Response({ 'code': 500, 'msg': f'获取上传凭证失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 5. 构建响应数据 current_time = int(time.time()) response_data = { 'credentials': { 'tmpSecretId': credentials.TmpSecretId, 'tmpSecretKey': credentials.TmpSecretKey, 'sessionToken': credentials.Token, }, 'startTime': current_time, 'expiredTime': current_time + 7200, 'bucket': settings.COS_BUCKET, 'region': settings.COS_REGION, # 🔴【修改】商家专用上传目录 'upload_dir': 'a_long/chfajltp/sjzjtp/', # 返回商家ID和订单ID,便于前端生成文件名 'dingdan_id': dingdan_id, 'shangjia_id': shangjia_id, # 返回商家昵称,便于前端显示 'shangjia_nicheng': shangjia_profile.nicheng if hasattr(shangjia_profile, 'nicheng') else '' } # 6. 返回成功 return Response({ 'code': 0, 'msg': '成功', 'data': response_data }, status=status.HTTP_200_OK) except Exception as e: logger.error(f'商家COS凭证接口未知错误: {str(e)}', exc_info=True) return Response({ 'code': 500, 'msg': f'服务器内部错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class DashouTijiaoView(APIView): """打手提交订单完成接口""" permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取参数 data = request.data dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip() liuyan = data.get('liuyan', '') jiaofu_tupian_urls = data.get('jiaofu_tupian_urls', []) # 2. 参数验证 if not dingdan_id: return Response({ 'code': 1, 'msg': '订单ID不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if not isinstance(jiaofu_tupian_urls, list): return Response({ 'code': 2, 'msg': '图片URL列表格式错误', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 限制图片数量(最多9张,最少1张) if len(jiaofu_tupian_urls) == 0: return Response({ 'code': 3, 'msg': '至少需要上传一张图片', 'data': None }, status=status.HTTP_400_BAD_REQUEST) if len(jiaofu_tupian_urls) > 9: return Response({ 'code': 4, 'msg': '最多只能上传9张图片', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 留言长度限制(50字) if liuyan and len(liuyan) > 50: return Response({ 'code': 5, 'msg': '留言最多50个字', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 3. 获取当前用户并验证打手身份 current_user = request.user yonghuid = current_user.UserUID try: dashou_profile = current_user.DashouProfile except UserDashou.DoesNotExist: return Response({ 'code': 6, 'msg': '用户不是打手身份', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 4. 验证订单状态和权限 try: dingdan = Order.query.get( OrderID=dingdan_id, PlayerID=yonghuid, Status=2 # 必须是进行中的订单 ) except Order.DoesNotExist: return Response({ 'code': 7, 'msg': '订单不存在或没有提交权限', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 5. 存储图片到Dashoutupian表 for tupian_url in jiaofu_tupian_urls: # 创建打手提交图片记录 PlayerDeliveryImage.query.create( OrderID=dingdan_id, PlayerID=yonghuid, ImageURL=tupian_url, CreateTime=timezone.now() ) # 6. 更新订单主表 dingdan.PlayerRemark = liuyan dingdan.Status = 8 # 状态改为结算中 dingdan.UpdateTime = timezone.now() dingdan.save() # 7. 更新打手扩展表状态 dashou_profile.zhuangtai = 1 # 改为数字1(根据你之前说的) dashou_profile.save() # ========== 新增:记录打手提交时间 ========== record_dashou_biaoxian( dingdan_id=dingdan_id, dashou_id=yonghuid, xingwei=2, # 提交 tijiao_time=timezone.now() ) # =========================================== # 8. 返回成功响应 return Response({ 'code': 0, 'msg': '订单提交成功', 'data': { 'dingdan_id': dingdan_id, 'new_zhuangtai': 8, # 新的订单状态 'dashou_zhuangtai': 1 # 打手新的状态 } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"打手提交订单失败: {str(e)}", exc_info=True) return Response({ 'code': 8, 'msg': '服务器内部错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class DashouDingdanXiangqingView(APIView): """打手订单详情获取接口""" permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取订单ID dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip() if not dingdan_id: return Response({ 'code': 1, 'msg': '订单ID不能为空', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 2. 获取当前用户并验证权限 current_user = request.user yonghuid = current_user.UserUID try: dashou_profile = current_user.DashouProfile except UserDashou.DoesNotExist: return Response({ 'code': 2, 'msg': '用户不是打手身份', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 3. 查询订单主表 try: dingdan = Order.query.get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({ 'code': 3, 'msg': '订单不存在', 'data': None }, status=status.HTTP_404_NOT_FOUND) # 4. 验证订单是否属于当前打手 if dingdan.PlayerID != yonghuid: return Response({ 'code': 4, 'msg': '没有权限查看此订单', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 5. 构建基础订单数据 dingdan_data = { 'dingdan_id': dingdan.OrderID, 'zhuangtai': dingdan.Status, 'jine': float(dingdan.PlayerCommission) if dingdan.PlayerCommission else 0.0, 'jieshao': dingdan.Description or '', 'tupian': dingdan.ImageURL or '', 'nicheng': dingdan.Nickname or '', 'beizhu': dingdan.Remark or '', **datetime_aliases(dingdan.CreateTime), 'tuikuan_liyou':dingdan.RefundReason or '', 'dashou_liuyan': dingdan.PlayerRemark or '', 'fuwudashou_id': dingdan.PlayerID or '', 'fadanpingtai': dingdan.Platform or '' } # 6. 查询打手提交的图片 dashou_tupian_list = PlayerDeliveryImage.query.filter( OrderID=dingdan_id, PlayerID=yonghuid ).order_by('CreateTime') jiaofu_tupian_urls = [] for tupian_record in dashou_tupian_list: if tupian_record.ImageURL: jiaofu_tupian_urls.append(tupian_record.ImageURL) dingdan_data['dashou_jiaofu'] = jiaofu_tupian_urls # 7. 查询商家扩展表信息 shangjia_info = {} try: shangjia_kuozhan = dingdan.shangjia_kuozhan shangjia_info = { 'shangjia_id': shangjia_kuozhan.MerchantID or '', 'shangjia_nicheng': shangjia_kuozhan.MerchantNickname or '', 'shangjia_liuyan': shangjia_kuozhan.MerchantReview or '', #'tuikuan_liyou': '', 'chufa_liyou': shangjia_kuozhan.PenaltyReason or '', 'chufa_zhuangtai': shangjia_kuozhan.PenaltyApplyStatus or 0, #'chufa_jieguo': shangjia_kuozhan.chufa_jieguo or '', 'bohui_liyou': shangjia_kuozhan.RejectReason or '' } except MerchantOrderExt.DoesNotExist: # 没有商家扩展信息,返回空值 shangjia_info = { 'shangjia_id': '', 'shangjia_nicheng': '', 'shangjia_liuyan': '', 'tuikuan_liyou': '', 'chufa_liyou': '', 'chufa_zhuangtai': 0, 'chufa_jieguo': '', 'bohui_liyou': '' } # 8. 查询平台扩展表信息(如果有的话) pingtai_info = {} try: pingtai_kuozhan = dingdan.pingtai_kuozhan pingtai_info = { 'laoban_id': pingtai_kuozhan.BossID or '', 'laoban_pingjia': pingtai_kuozhan.BossReview or '' } except PlatformOrderExt.DoesNotExist: pingtai_info = { 'laoban_id': '', 'laoban_pingjia': '' } # 9. 合并所有数据 response_data = { **dingdan_data, **shangjia_info, **pingtai_info } return Response({ 'code': 0, 'msg': '成功', 'data': response_data }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"获取打手订单详情失败: {str(e)}", exc_info=True) return Response({ 'code': 5, 'msg': '服务器内部错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class 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.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.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.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.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.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.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.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.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.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.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.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.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.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 = int(float(jine) * 100) # 构造请求参数 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 DashouHuoquLeixingView(APIView): """ 打手获取商品类型接口 - 抢单页面使用 接口地址:POST /dingdan/dsqdhqddlx 功能:返回所有正常的商品类型,已按排序权重排序 返回格式(与前端原数据格式完全一致): { "code": 200, "msg": "成功", "data": { "list": [ { "id": 1, "jieshao": "王者荣耀", "tupian_url": "leixing/wzry.jpg", "yaoqiuleixing": null, "huiyuan_id": null, "yongjin": null, "paixu": 100, "shenhezhuangtai": 1 }, // ... 更多类型 ] } } """ # 🔴 使用JWT Token认证 permission_classes = [IsAuthenticated] def post(self, request): """ 处理POST请求,返回商品类型列表 请求参数:无特殊参数,只验证用户身份 安全验证: 1. JWT Token验证用户身份 2. 只返回 shenhezhuangtai=1(正常状态)的类型 3. 数据已排序:paixu降序 → id升序 """ try: logger.info(f"打手获取商品类型请求 - 用户ID: {request.user.UserUID}") # 排除审核专用类型(shenhezhuangtai=2),兼容历史数据 shenhezhuangtai 为空或 0 leixing_queryset = ShangpinLeixing.query.exclude( shenhezhuangtai=2 ).order_by('-paixu', 'id') # 🔴 构建返回数据列表 leixing_list = [] for leixing in leixing_queryset: leixing_list.append({ "id": leixing.id, "jieshao": leixing.jieshao or "", "tupian_url": leixing.tupian_url or "", "yaoqiuleixing": leixing.yaoqiuleixing, "huiyuan_id": leixing.huiyuan_id, "yongjin": float(leixing.yongjin) if leixing.yongjin else None, "paixu": leixing.paixu or 0, "shenhezhuangtai": leixing.shenhezhuangtai or 1 }) # 🔴 记录日志 logger.info(f"返回商品类型数据:{len(leixing_list)}条,用户: {request.user.UserUID}") # 🔴 返回成功响应 # 注意:保持与前端原数据格式完全一致 return Response({ "code": 200, "msg": "成功", "data": { "list": leixing_list # 🔴 保持原字段名 "list" } }, status=status.HTTP_200_OK) except Exception as e: # 🔴 异常处理 logger.error(f"打手获取商品类型失败 - 用户ID: {request.user.UserUID}, 错误: {str(e)}", exc_info=True) return Response({ "code": 500, "msg": f"服务器内部错误: {str(e)}", "data": None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 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.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.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 DashouXiugaiView(APIView): """ 打手修改订单交付内容接口 请求方式:POST 请求路径:/dingdan/dsxiugaidd 权限:需携带有效JWT Token,且用户为打手身份 请求参数: dingdan_id : 订单ID(必填) liuyan : 新的留言内容(可选,最长50字) deleted_images : 要删除的图片URL列表(数组,每个元素为完整COS URL) new_images : 新增的图片URL列表(数组,每个元素为完整COS URL) 返回: 成功:{"code":0,"msg":"修改成功","data":null} 失败:返回相应错误码和提示 """ permission_classes = [IsAuthenticated] def post(self, request): # 获取请求数据 data = request.data dingdan_id = str(data.get('OrderID') or data.get('dingdan_id') or '').strip() liuyan = data.get('liuyan', '') deleted_images = data.get('deleted_images', []) new_images = data.get('new_images', []) # ---------- 参数校验 ---------- if not dingdan_id: return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST) if not isinstance(deleted_images, list): return Response({'code': 2, 'msg': 'deleted_images 必须为数组', 'data': None}, status=status.HTTP_400_BAD_REQUEST) if not isinstance(new_images, list): return Response({'code': 3, 'msg': 'new_images 必须为数组', 'data': None}, status=status.HTTP_400_BAD_REQUEST) if liuyan and len(liuyan) > 50: return Response({'code': 4, 'msg': '留言最多50个字', 'data': None}, status=status.HTTP_400_BAD_REQUEST) # ---------- 用户身份验证 ---------- current_user = request.user yonghuid = current_user.UserUID # 检查是否为打手 try: dashou_profile = current_user.DashouProfile # User 模型反向 related_name='DashouProfile' except UserDashou.DoesNotExist: return Response({'code': 5, 'msg': '用户不是打手身份', 'data': None}, status=status.HTTP_403_FORBIDDEN) # ---------- 订单查询及权限验证 ---------- try: # 订单必须属于该打手,且状态为4(退款中)或8(结算中) dingdan = Order.query.get( OrderID=dingdan_id, PlayerID=yonghuid, Status__in=[4, 8] # 只允许修改退款中或结算中的订单 ) except Order.DoesNotExist: return Response({'code': 6, 'msg': '订单不存在或没有修改权限', 'data': None}, status=status.HTTP_403_FORBIDDEN) # ---------- 执行修改(使用事务保证原子性) ---------- try: with transaction.atomic(): # 1. 处理删除的图片:从 Dashoutupian 表中删除记录 if deleted_images: # 根据订单ID、打手ID和图片URL删除记录 delete_count, _ = PlayerDeliveryImage.query.filter( OrderID=dingdan_id, PlayerID=yonghuid, ImageURL__in=deleted_images ).delete() # 可选:记录删除数量 logger.info(f"订单 {dingdan_id} 删除图片 {delete_count} 张") # 2. 处理新增的图片:插入新记录 if new_images: # 批量创建 new_records = [] for url in new_images: new_records.append(PlayerDeliveryImage( OrderID=dingdan_id, PlayerID=yonghuid, ImageURL=url, CreateTime=timezone.now() )) PlayerDeliveryImage.objects.bulk_create(new_records) logger.info(f"订单 {dingdan_id} 新增图片 {len(new_records)} 张") # 3. 更新订单留言(如果发生变化) # 注意:即使留言为空字符串,也允许更新,表示清空留言 if 'liuyan' in data: # 判断是否传了liuyan字段(可能为空字符串) dingdan.PlayerRemark = liuyan dingdan.UpdateTime = timezone.now() dingdan.save(update_fields=['PlayerRemark', 'UpdateTime']) # 全部成功,返回成功响应 return Response({ 'code': 0, 'msg': '修改成功', 'data': None }, status=status.HTTP_200_OK) except Exception as e: logger.error(f"修改订单失败: {str(e)}", exc_info=True) return Response({ 'code': 7, 'msg': '服务器内部错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) 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 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.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, # 不继承: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 '游戏订单' # ===================== 视图:商家罚款申请 ===================== # 文件路径:dingdan/views.py 或单独文件,根据项目结构放置 class ShangjiaFakuanApplyView(APIView): """ 商家罚款申请接口 路径:POST /dingdan/sjfksq 权限:JWT认证的商家用户 参数: dingdan_id - 订单ID (必填) chufa_liyou - 罚款原因 (必填) fakuanjine - 罚款金额,元 (必填) yingxiang_qiangdan - 是否影响抢单 1是 0否 (必填) 说明: 1. 自动从订单中获取被打手ID,防止商家篡改目标。 2. 同一订单同一打手不允许重复罚款。 3. 跨平台订单若为我方派单,则通知对方平台处罚;否则本地生成罚单。 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # ========== 1. 身份验证 ========== current_user = request.user try: shangjia_id = current_user.UserUID # 商家的用户ID except AttributeError: return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED) # ========== 2. 参数提取与校验 ========== dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip() chufa_liyou = request.data.get('chufa_liyou', '').strip() fakuanjine = request.data.get('fakuanjine', 0) yingxiang_qiangdan = int(request.data.get('yingxiang_qiangdan', 1)) if not dingdan_id or not chufa_liyou or not fakuanjine: return Response({'code': 2, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST) try: fakuanjine = float(fakuanjine) if fakuanjine <= 0: raise ValueError except (ValueError, TypeError): return Response({'code': 2, 'msg': '罚款金额格式错误'}, status=status.HTTP_400_BAD_REQUEST) # ========== 3. 获取订单,校验归属商家 ========== try: # 关联查询商家扩展表,确保订单属于当前商家 shangjia_kuozhan = MerchantOrderExt.query.select_related('Order').get( Order__OrderID=dingdan_id, MerchantID=shangjia_id ) order = shangjia_kuozhan.Order except MerchantOrderExt.DoesNotExist: return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN) # ========== 4. 获取被打手ID ========== dashou_id = order.PlayerID if not dashou_id: return Response({'code': 4, 'msg': '该订单暂无接单打手,无法罚款'}, status=status.HTTP_400_BAD_REQUEST) # ========== 5. 重复罚款检查 ========== if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=dingdan_id).exists(): return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST) # ========== 6. 本地创建罚单 ========== with transaction.atomic(): Penalty.query.create( PenalizedUserID=dashou_id, ApplicantID=shangjia_id, # 申请人为当前商家 Identity=1, # 被打者身份:1=打手 Reason=chufa_liyou, FineAmount=fakuanjine, RelatedOrderID=dingdan_id, Status=1, # 待缴纳 AffectsGrabbing=yingxiang_qiangdan, ApplicantIdentity=5, # 申请人是商家 ClubID=resolve_penalty_club_id(order=order, penalized_user_id=dashou_id), ) logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}罚款成功,金额{fakuanjine}") return Response({'code': 0, 'msg': '罚款申请已提交'}) class ShangjiaFakuaiXiugaiView(APIView): """ 商家端统一修改接口:罚款修改/撤销/再次申请,订单退款撤销/修改理由 URL: /dingdan/sjfkxgycx """ permission_classes = [IsAuthenticated] def post(self, request): user = request.user # 验证商家身份 try: shangjia = user.ShopProfile except UserShangjia.DoesNotExist: return Response({'code': 403, 'msg': '您不是商家'}, status=status.HTTP_403_FORBIDDEN) if shangjia.zhuangtai != 1: return Response({'code': 403, 'msg': '商家状态异常'}, status=status.HTTP_403_FORBIDDEN) operation = request.data.get('operation') dingdan_id = request.data.get('dingdan_id') or request.data.get('OrderID') if not dingdan_id: return Response({'code': 400, 'msg': '缺少订单ID'}, status=status.HTTP_400_BAD_REQUEST) # 验证订单属于该商家 try: dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id) except Order.DoesNotExist: return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND) if not hasattr(dingdan, 'shangjia_kuozhan') or dingdan.shangjia_kuozhan.MerchantID != user.UserUID: return Response({'code': 403, 'msg': '无权操作此订单'}, status=status.HTTP_403_FORBIDDEN) # 处理订单相关操作 if operation == 'cancel_refund': return self._cancel_refund(dingdan, request.data.get('liyou', '')) elif operation == 'modify_refund_reason': return self._modify_refund_reason(dingdan, request.data.get('liyou', '')) # 处理罚款相关操作 fadan_id = request.data.get('fadan_id') if not fadan_id: return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST) try: fadan = Penalty.query.get(id=fadan_id) except Penalty.DoesNotExist: return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND) if fadan.RelatedOrderID != dingdan_id: return Response({'code': 403, 'msg': '罚单与订单不匹配'}, status=status.HTTP_403_FORBIDDEN) if operation == 'modify_penalty': return self._modify_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine')) elif operation == 'cancel_penalty': return self._cancel_penalty(fadan, request.data.get('liyou', '')) elif operation == 'resubmit_penalty': return self._resubmit_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine')) else: return Response({'code': 400, 'msg': '无效的操作类型'}, status=status.HTTP_400_BAD_REQUEST) def _cancel_refund(self, dingdan, liyou): """撤销退款申请(状态4 → 状态8,清空退款理由)""" if dingdan.Status != 4: return Response({'code': 400, 'msg': '只有退款中的订单可以撤销退款申请'}) with transaction.atomic(): dingdan.Status = 8 dingdan.RefundReason = '' # 清空退款理由 dingdan.save() # 记录撤销理由可存入日志(如需要可另建日志表,此处简单处理) logger.info(f"商家撤销退款订单 {dingdan.OrderID},理由: {liyou}") return Response({'code': 0, 'msg': '已撤销退款申请'}) def _modify_refund_reason(self, dingdan, liyou): """修改退款理由(仅状态4时)""" if dingdan.Status != 4: return Response({'code': 400, 'msg': '只有退款中的订单可以修改退款理由'}) if not liyou.strip(): return Response({'code': 400, 'msg': '退款理由不能为空'}) dingdan.RefundReason = liyou[:500] dingdan.save() return Response({'code': 0, 'msg': '退款理由修改成功'}) def _modify_penalty(self, fadan, liyou, jine_str): """修改罚款(仅状态1待缴纳 或 3申诉中)""" if fadan.Status not in (1, 3): return Response({'code': 400, 'msg': '当前罚单状态不可修改'}) if not liyou.strip(): return Response({'code': 400, 'msg': '罚款理由不能为空'}) try: jine = Decimal(jine_str) if jine <= 0: raise ValueError except: return Response({'code': 400, 'msg': '罚款金额必须为正数'}) with transaction.atomic(): fadan.Reason = liyou[:500] fadan.FineAmount = jine # 如果之前是驳回状态修改,需要重置状态为待缴纳?不,修改不应改变状态 fadan.save() return Response({'code': 0, 'msg': '罚款修改成功'}) def _cancel_penalty(self, fadan, liyou): """撤销罚款(状态1或3 → 状态4 已驳回)""" if fadan.Status not in (1, 3): return Response({'code': 400, 'msg': '当前罚单状态不可撤销'}) if not liyou.strip(): return Response({'code': 400, 'msg': '驳回理由不能为空'}) with transaction.atomic(): fadan.Status = 4 # 已驳回 fadan.RejectReason = liyou[:500] fadan.save() return Response({'code': 0, 'msg': '罚款已撤销(驳回)'}) def _resubmit_penalty(self, fadan, liyou, jine_str): """再次申请罚款(状态4 → 状态1,修改理由和金额)""" if fadan.Status != 4: return Response({'code': 400, 'msg': '只有已驳回的罚单可再次申请'}) if not liyou.strip(): return Response({'code': 400, 'msg': '罚款理由不能为空'}) try: jine = Decimal(jine_str) if jine <= 0: raise ValueError except: return Response({'code': 400, 'msg': '罚款金额必须为正数'}) with transaction.atomic(): fadan.Status = 1 # 待缴纳 fadan.Reason = liyou[:500] fadan.FineAmount = jine fadan.RejectReason = '' # 清空驳回理由 fadan.save() return Response({'code': 0, 'msg': '已重新申请罚款'})