6555 lines
276 KiB
Python
6555 lines
276 KiB
Python
# ==================== 标准库 ====================
|
||
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
|
||
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
|
||
)
|
||
from orders.models import (
|
||
CommissionRate, PlayerRating, RefundRecord
|
||
)
|
||
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 (
|
||
Szjilu, 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 = Dingdan.objects.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 ''
|
||
|
||
# 3. 签名验证
|
||
if not self._verify_signature(xml_data):
|
||
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"""<xml>
|
||
<return_code><![CDATA[{return_code}]]></return_code>
|
||
<return_msg><![CDATA[{return_msg}]]></return_msg>
|
||
</xml>"""
|
||
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 = Dingdan.objects.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)
|
||
|
||
# 更新每日收入统计
|
||
update_daily_income(dingdan.Amount)
|
||
|
||
# 构建通知数据(你的 _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 = Dingdan.objects.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):
|
||
"""更新收支记录表(ID=1的记录)"""
|
||
try:
|
||
jine_decimal = Decimal(str(jine))
|
||
with transaction.atomic():
|
||
szjilu, created = Szjilu.objects.select_for_update().get_or_create(
|
||
id=1,
|
||
defaults={
|
||
'TotalIncome': Decimal('0.00'),
|
||
'TotalFlow': Decimal('0.00'),
|
||
'TotalExpense': Decimal('0.00'),
|
||
'DailyExpense': Decimal('0.00'),
|
||
'DailyFlow': Decimal('0.00')
|
||
}
|
||
)
|
||
if created:
|
||
logger.info("创建ID=1的收支记录")
|
||
szjilu.TotalIncome += jine_decimal
|
||
szjilu.TotalFlow += jine_decimal
|
||
szjilu.DailyFlow += jine_decimal
|
||
szjilu.save()
|
||
logger.info(f"收支记录更新成功,增加金额: {jine}")
|
||
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 = Dingdan.objects.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)
|
||
|
||
# 更新每日收入统计
|
||
update_daily_income(dingdan.Amount)
|
||
|
||
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):
|
||
"""更新收支记录表(ID=1的记录)"""
|
||
try:
|
||
jine_decimal = Decimal(str(jine))
|
||
with transaction.atomic():
|
||
szjilu, created = Szjilu.objects.select_for_update().get_or_create(
|
||
id=1,
|
||
defaults={
|
||
'TotalIncome': Decimal('0.00'),
|
||
'TotalFlow': Decimal('0.00'),
|
||
'TotalExpense': Decimal('0.00'),
|
||
'DailyExpense': Decimal('0.00'),
|
||
'DailyFlow': Decimal('0.00')
|
||
}
|
||
)
|
||
if created:
|
||
logger.info("创建ID=1的收支记录")
|
||
szjilu.TotalIncome += jine_decimal
|
||
szjilu.TotalFlow += jine_decimal
|
||
szjilu.DailyFlow += jine_decimal
|
||
szjilu.save()
|
||
logger.info(f"收支记录更新成功,增加金额: {jine}")
|
||
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 = ['<xml>']
|
||
for k, v in data.items():
|
||
xml.append(f'<{k}>{v}</{k}>')
|
||
xml.append('</xml>')
|
||
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. 查询利率并计算分成
|
||
# 查询打手利率(字符'1')
|
||
dashou_lilu = CommissionRate.query.filter(Platform='1').first()
|
||
|
||
|
||
# 🔥 新增:判断是否使用商品配置的额外分成
|
||
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 dashou_lilu: #or not guanshi_lilu:
|
||
return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None})
|
||
|
||
# 计算打手分成
|
||
#dashou_fencheng = Decimal(str(shangpin_jiage)) * Decimal(str(dashou_lilu.Rate))
|
||
|
||
|
||
# 开始数据库事务
|
||
with transaction.atomic():
|
||
# 获取当前用户信息
|
||
current_user = request.user
|
||
laoban_yonghuid = getattr(current_user, 'yonghuid', '')
|
||
user_openid = 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
|
||
}
|
||
|
||
# 创建订单记录
|
||
|
||
dingdan = Order.query.create(**order_data)
|
||
|
||
|
||
# CreateOrderView.post 事务内部,替换原有的扩展表创建代码
|
||
|
||
dianpu_id, pingtai_shouyi, dianpu_shouyi, shangji_dianpu_id, shangji_fenhong = calculate_pingtai_and_dianpu_shouyi(
|
||
ProductID=shangpin_id,
|
||
dingdan_jiage=shangpin_jiage,
|
||
PlayerCommission=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,
|
||
Amount=shangpin_jiage,
|
||
Remark=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"""<xml>
|
||
<appid>{APPID}</appid>
|
||
<body>{beizhu}</body>
|
||
<mch_id>{MCHID}</mch_id>
|
||
<nonce_str>{nonce_str}</nonce_str>
|
||
<notify_url>{NOTIFY_URL}</notify_url>
|
||
<openid>{openid}</openid>
|
||
<out_trade_no>{dingdanid}</out_trade_no>
|
||
<spbill_create_ip>{user_ip}</spbill_create_ip>
|
||
<total_fee>{total_fee}</total_fee>
|
||
<trade_type>{trade_type}</trade_type>
|
||
<sign>{sign}</sign>
|
||
</xml>"""
|
||
|
||
# 调用微信统一下单接口
|
||
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': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
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(
|
||
UserID=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(
|
||
OrderID=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]
|
||
|
||
def post(self, request):
|
||
|
||
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. 利率(打手+管事分成,打手优先) -----
|
||
dashou_fencheng, guanshi_fencheng = calc_shangjia_order_fencheng(jiage)
|
||
|
||
# ----- 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}"
|
||
)
|
||
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(
|
||
UserID=request.user.UserUID,
|
||
amount=Decimal(str(jiage)),
|
||
action=1
|
||
)
|
||
# 事务提交,订单已持久化
|
||
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 = data.get('ProductTypeID', None)
|
||
|
||
# 🆕 模糊搜索关键字(支持订单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:
|
||
try:
|
||
leixing_id_int = int(leixing_id)
|
||
base_qs = base_qs.filter(Order__ProductTypeID=leixing_id_int)
|
||
except (ValueError, TypeError):
|
||
# 如果leixing_id不是有效整数,忽略该筛选
|
||
pass
|
||
|
||
# 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 '',
|
||
'CreateTime': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else ''
|
||
}
|
||
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 = data.get('OrderID', '').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:
|
||
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': fadan_obj.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if fadan_obj.CreateTime else '',
|
||
'UpdateTime': fadan_obj.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if fadan_obj.UpdateTime else '',
|
||
}
|
||
|
||
# 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 '',
|
||
'CreateTime': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else '',
|
||
|
||
# 打手信息
|
||
'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,
|
||
}
|
||
|
||
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 = data.get('OrderID', '').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 = Dingdan.objects.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(
|
||
UserID=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(
|
||
UserID=request.user.UserUID,
|
||
amount=Decimal(str(jine)),
|
||
action=2 # 2 = 结算
|
||
)
|
||
|
||
# 商家订单管事分红(仅 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 = data.get('OrderID', '').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. 验证订单状态
|
||
if dingdan.Status not in [1, 7]:
|
||
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. 更新商家扩展表数据(使用F表达式原子操作)
|
||
UserShangjia.query.filter(id=shangjia.id).update(
|
||
yue=F('yue') + dingdan.Amount,
|
||
tuikuan=F('tuikuan') + 1,
|
||
jinridingdan=F('jinridingdan') - 1,
|
||
jinyuedingdan=F('jinyuedingdan') - 1,
|
||
jinriliushui=F('jinriliushui') - dingdan.Amount,
|
||
jinyueliushui=F('jinyueliushui') - dingdan.Amount,
|
||
|
||
)
|
||
|
||
# 10. 更新订单状态为5(已退款)
|
||
dingdan.Status = 5
|
||
|
||
dingdan.save()
|
||
|
||
# 行为驱动
|
||
update_shangjia_daily(
|
||
UserID=request.user.UserUID,
|
||
amount=Decimal(str(dingdan.Amount)),
|
||
action=3 # 3 = 退款
|
||
)
|
||
|
||
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 = data.get('OrderID', '').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 = data.get('OrderID', '').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:
|
||
# 检查打手积分是否足够
|
||
if dashou.DeductedPoints < jifen_kouchu:
|
||
dashou.DeductedPoints = 0
|
||
else:
|
||
dashou.DeductedPoints -= 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 = data.get('ProductTypeID')
|
||
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:
|
||
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': order['CreateTime'].strftime('%Y-%m-%d %H:%M:%S') if order['CreateTime'] else '',
|
||
'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', '')
|
||
|
||
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('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(
|
||
UserID=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(
|
||
OrderID=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.DeductedPoints < 5):
|
||
return None
|
||
return dashou_profile
|
||
except UserDashou.DoesNotExist:
|
||
return None
|
||
|
||
def _validate_and_lock_order(self, dingdan_id, dashou_yonghuid):
|
||
try:
|
||
order = Dingdan.objects.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 = data.get('ProductTypeID', None) # 可选,商品类型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 = [] # 非列表则视为全部
|
||
if leixing_id is not None:
|
||
try:
|
||
leixing_id = int(leixing_id)
|
||
except (ValueError, TypeError):
|
||
leixing_id = None
|
||
|
||
# 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 '',
|
||
'CreateTime': order.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if order.CreateTime else ''
|
||
}
|
||
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. 接收参数(修复 int 无 strip 问题)
|
||
dingdan_id = str(request.data.get('OrderID', '')).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 = request.data.get('OrderID', '').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 = data.get('OrderID', '')
|
||
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(
|
||
OrderID=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 = request.data.get('OrderID', '')
|
||
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 '',
|
||
'CreateTime': dingdan.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if dingdan.CreateTime else '',
|
||
'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 = request.data.get('OrderID', '').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': dingdan_obj.CreateTime.strftime(
|
||
'%Y-%m-%d %H:%M:%S') if dingdan_obj.CreateTime else '',
|
||
'genggaishijian': dingdan_obj.UpdateTime.strftime(
|
||
'%Y-%m-%d %H:%M:%S') if dingdan_obj.UpdateTime else '',
|
||
}
|
||
|
||
# 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 = request.data.get('OrderID', '').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 = request.data.get('OrderID', '').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 = request.data.get('OrderID', '').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 = request.data.get('OrderID', '').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
|
||
|
||
# ✅ 确认字段:yue 商家余额(加上订单金额)
|
||
shangjia_profile.yue += jine
|
||
|
||
shangjia_profile.save()
|
||
logger.info(f"同意退款成功,商家{shangjia_id}退款订单+1,余额增加:{jine}")
|
||
|
||
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 = request.data.get('OrderID', '').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 = Dingdan.objects.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. 更新收支记录表(id=1)
|
||
try:
|
||
|
||
update_daily_payout(jine)
|
||
|
||
|
||
szjilu_obj = Szjilu.query.get(id=1)
|
||
szjilu_obj.TotalIncome -= jine
|
||
szjilu_obj.TotalExpense += jine
|
||
szjilu_obj.DailyExpense += jine
|
||
szjilu_obj.save()
|
||
except Szjilu.DoesNotExist:
|
||
logger.error(f"平台退款:收支记录表(id=1)不存在,跳过")
|
||
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 = ["<xml>"]
|
||
for k, v in dict_data.items():
|
||
xml.append(f"<{k}>{v}</{k}>")
|
||
xml.append("</xml>")
|
||
return "".join(xml)
|
||
|
||
def xml_to_dict(self, xml_data):
|
||
try:
|
||
root = ET.fromstring(xml_data)
|
||
return {child.tag: child.text for child in root}
|
||
except ET.ParseError as e:
|
||
logger.error(f"XML解析失败: {str(e)}")
|
||
return {'return_code': 'FAIL', 'return_msg': 'XML解析失败'}
|
||
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class 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=1(正常状态)
|
||
# 排序:先按 paixu 降序(数值大的在前),再按 id 升序
|
||
leixing_queryset = ShangpinLeixing.query.filter(
|
||
shenhezhuangtai=1 # 🔴 只取正常状态,排除审核专用
|
||
).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 = request.data.get('OrderID', '').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 = request.data.get('OrderID', '').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 = request.data.get('OrderID', '').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 = data.get('OrderID', '')
|
||
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 = request.data.get('OrderID', '').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(
|
||
UserID=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 = request.data.get('OrderID', '').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, # 申请人是商家
|
||
)
|
||
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('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': '已重新申请罚款'}) |