Files
along_django/dingdan/views.py
2026-06-27 01:17:52 +08:00

9394 lines
380 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
from django.db.models import F
from django.shortcuts import render
from rest_framework.permissions import AllowAny # 新增这行
from dingdan.utils import sync_order_to_partners
from rest_framework.throttling import AnonRateThrottle
# dingdan/views.py
import time
import random
import hashlib
import xml.etree.ElementTree as ET
import requests
import uuid
from decimal import Decimal
from django.db import transaction
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from shangpin.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from dingdan.models import Dingdan, DingdanPingtai, Lilubiao, Pingfen, DingdanShangjia, Chufajilu, Liaotian
from yonghu.models import UserMain, UserDashou, UserShangjia
import os
import traceback
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from shangpin.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from dingdan.models import Dingdan, DingdanPingtai, Lilubiao, Pingfen, DingdanShangjia, Chufajilu, Liaotian, Chufatupian
from yonghu.models import UserMain, UserDashou, UserShangjia
# dingdan/views.py
import xml.etree.ElementTree as ET
from django.db import transaction
from django.http import HttpResponse
from rest_framework.views import APIView
from rest_framework import permissions
from shangpin.models import Shangpin
from yonghu.models import UserMain, UserBoss
# views/pay_views.py - WechatPayNotifyView完整代码
import xml.etree.ElementTree as ET
from django.db import transaction
from django.http import HttpResponse
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.throttling import AnonRateThrottle
from rest_framework.permissions import AllowAny
from peizhi.models import Szjilu
import logging
logger = logging.getLogger(__name__)
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('beizhu', '').strip()
zhiding = request.data.get('zhiding', '').strip()
jine = request.data.get('jine', 0)
# 4.0 临时关闭指定功能(如需开启,请删除或注释掉下面两行)
#if zhiding:
#return Response({'code': 8, 'msg': '指定功能尚未开启请不要填写指定打手ID', 'data': None})
# 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.objects.filter(id=shangpin_id).first()
if not shangpin:
return Response({'code': 4, '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 = UserMain.objects.filter(
yonghuid=zhiding,
#user_type='dashou'
).first()
if not zhiding_user:
return Response({'code': 7, 'msg': '指定打手不存在', 'data': None})
# 查询打手扩展表
dashou_profile = UserDashou.objects.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 = Lilubiao.objects.filter(fadanpingtai='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.lilu))
# 查询管事利率(字符'2'
#guanshi_lilu = Lilubiao.objects.filter(fadanpingtai='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.lilu))
# 开始数据库事务
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 = {
'dingdan_id': dingdanid,
'zhuangtai': 9, # 未付款状态
'jine': shangpin_jiage,
'fadan_pingtai':1,
'dashou_fencheng': dashou_fencheng,
'zhiding_id': zhiding_id_value,
'shangpin_id': shangpin_id,
'yaoqiuleixing': getattr(shangpin, 'yaoqiuleixing', None),
'huiyuan_id': getattr(shangpin, 'huiyuan_id', ''),
'yongjin': getattr(shangpin, 'yongjin', 0),
'jieshao': getattr(shangpin, 'jieshao', ''),
'beizhu': beizhu,
'nicheng': nicheng,
'tupian':shangpin_tupian,
'user1_id':f"Boss{request.user.yonghuid}",
'leixing_id':shangpin.leixing_id
}
# 创建订单记录
dingdan = Dingdan.objects.create(**order_data)
# 创建订单平台扩展表
pingtaikuozhan_data = {
'dingdan': dingdan,
'laoban_id': laoban_yonghuid,
}
DingdanPingtai.objects.create(**pingtaikuozhan_data)
# 7. 生成微信支付参数(向微信官方发送请求)
pay_params = self.generate_wechat_pay_params(
dingdanid=dingdanid,
jine=shangpin_jiage,
beizhu=getattr(settings, 'WEIXIN_PAY_DESCRIPTION'),
user_ip=self.get_client_ip(request),
openid=user_openid
)
if not pay_params:
# 手动抛出异常触发事务回滚
raise Exception('支付参数生成失败')
# 8. 返回成功响应
response_data = {
'code': 0,
'msg': '订单创建成功',
'data': {
'dingdanid': dingdanid,
'payParams': pay_params
}
}
return Response(response_data)
except Exception as e:
# 这里会自动回滚事务
return Response({'code': 99, 'msg': f'系统错误: {str(e)}', 'data': None})
def get_client_ip(self, request):
"""获取客户端真实IP地址"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
def generate_wechat_pay_params(self, dingdanid, jine, beizhu, user_ip, openid):
"""
调用微信支付统一下单接口,生成支付参数
完全按照您提供的正确示例代码编写
"""
try:
# 微信支付配置
APPID = wx_cfg.WEIXIN_APPID
MCHID = wx_cfg.WEIXIN_MCHID
KEY = wx_cfg.WEIXIN_SHANGHUMIYAO
NOTIFY_URL = wx_cfg.WEIXIN_NOTIFY_URL
if not all([APPID, MCHID, KEY, NOTIFY_URL]):
raise Exception('微信支付配置不完整')
# 生成随机字符串
import string
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 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(dingdan_id=dingdanid).first()
if not dingdan:
return Response({'code': 2, 'msg': '订单不存在', 'data': None})
# 只允许删除未支付订单状态9
if dingdan.zhuangtai != 9:
return Response({'code': 3, 'msg': '只能删除未支付的订单', 'data': None})
# 删除扩展表和主表
DingdanPingtai.objects.filter(dingdan=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})
import string
import requests
import xmltodict
import logging
from decimal import Decimal
from django.db import transaction, models
from django.core.cache import cache
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.parsers import JSONParser
from yonghu.models import UserMain, UserBoss
from shangpin.models import Shangpin
from dingdan.models import Dingdan, DingdanPingtai
from peizhi.models import Szjilu
logger = logging.getLogger(__name__)
from dingdan.utils import update_daily_income
import logging
import traceback
logger = logging.getLogger(__name__)
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={wx_cfg.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(dingdan_id=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.zhuangtai}")
# 幂等处理只有状态为9未支付才处理已处理则跳过
if dingdan.zhuangtai != 9:
logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.zhuangtai},跳过")
return
# 保存微信交易号
if transaction_id:
dingdan.wechat_transaction_id = transaction_id
logger.info(f"保存微信交易号: {transaction_id}")
else:
logger.warning("微信未返回transaction_id")
# 更新订单状态
if dingdan.zhiding_id and dingdan.zhiding_id.strip():
dingdan.zhuangtai = 7
logger.info("指定单状态设为7")
else:
dingdan.zhuangtai = 1
logger.info("普通单状态设为1")
dingdan.save()
logger.info(f"订单状态已更新为 {dingdan.zhuangtai}")
# 删除同一用户同一商品的其他未支付订单改为逻辑删除状态25
try:
abandoned_count = Dingdan.objects.filter(
user1_id=dingdan.user1_id,
shangpin_id=dingdan.shangpin_id,
zhuangtai=9
).exclude(dingdan_id=out_trade_no).update(zhuangtai=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.laoban_id
if laoban_id:
user_main = UserMain.objects.filter(yonghuid=laoban_id).first()
if user_main and hasattr(user_main, 'boss_profile'):
boss = user_main.boss_profile
boss.zonge = (boss.zonge or 0) + (dingdan.jine 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.shangpin_id:
Shangpin.objects.filter(id=dingdan.shangpin_id).update(
kucun=F('kucun') - 1
)
logger.info(f"商品 {dingdan.shangpin_id} 库存减1")
except Exception as e:
logger.error(f"更新商品库存异常: {e}")
# 更新收支记录(总累计)
self._update_szjilu(dingdan.jine)
# 更新每日收入统计
update_daily_income(dingdan.jine)
# 跨平台同步
sync_order_to_partners(dingdan.dingdan_id)
# 异步广播通知
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(dingdan_id=out_trade_no).first()
if not dingdan:
logger.warning(f"订单不存在: {out_trade_no}")
return
logger.info(f"订单当前状态: {dingdan.zhuangtai}")
# 仅当订单状态为9未支付时才标记失败不直接删除
if dingdan.zhuangtai == 9:
dingdan.zhuangtai = 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={
'zongsy': Decimal('0.00'),
'zongls': Decimal('0.00'),
'zongzc': Decimal('0.00'),
'jrzc': Decimal('0.00'),
'jrls': Decimal('0.00')
}
)
if created:
logger.info("创建ID=1的收支记录")
szjilu.zongsy += jine_decimal
szjilu.zongls += jine_decimal
szjilu.jrls += jine_decimal
szjilu.save()
logger.info(f"收支记录更新成功,增加金额: {jine}")
except Exception as e:
logger.error(f"更新收支记录失败: {e}")
def _broadcast_async(self, dingdan):
"""服务号模板消息广播Celery 优先队列,接口不阻塞)"""
try:
from utils.order_broadcast import schedule_dingdan_guangbo_from_order
schedule_dingdan_guangbo_from_order(dingdan)
except Exception as e:
logger.error(f'提交广播任务失败: {e}', exc_info=True)
def _get_game_type_name(self, leixing_id):
try:
from shangpin.models import ShangpinLeixing
gt = ShangpinLeixing.objects.filter(id=leixing_id).first()
return gt.jieshao if gt else '游戏订单'
except Exception as e:
logger.error(f"获取游戏类型名称异常: {e}")
return '游戏订单'
from django.core.cache import cache
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 = Dingdan.objects.filter(dingdan_id=dingdanid).first()
if not dingdan:
return Response({'code': 2, 'msg': '订单不存在'})
# ---------- 3. 验证订单归属(安全) ----------
current_user = request.user
user_identifier = f"Boss{current_user.yonghuid}"
if dingdan.user1_id != user_identifier:
logger.warning(f"用户 {current_user.yonghuid} 尝试复查他人订单 {dingdanid}")
return Response({'code': 403, 'msg': '无权访问'})
# ---------- 4. 如果订单状态已是1/7直接返回成功 ----------
if dingdan.zhuangtai in [1, 7]:
return Response({
'code': 0,
'msg': '订单状态正常',
'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai}
})
# ---------- 5. 如果状态为9则主动查询微信支付状态 ----------
if dingdan.zhuangtai == 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.yonghuid)
# 重新获取订单以获取最新状态
dingdan.refresh_from_db()
return Response({
'code': 0,
'msg': '支付成功',
'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai}
})
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.zhuangtai, '订单状态异常')
return Response({'code': 3, 'msg': status_msg, 'data': {'zhuangtai': dingdan.zhuangtai}})
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.zhuangtai != 9:
# 订单状态已被其他进程更新,直接返回(幂等)
return
# 更新订单状态
dingdan.zhuangtai = 7 if dingdan.zhiding_id and dingdan.zhiding_id.strip() else 1
dingdan.wechat_transaction_id = transaction_id
dingdan.clkf = operator_id # 可选记录操作人ID
dingdan.save()
# 删除同一用户同一商品的其他未支付订单逻辑删除状态25
Dingdan.objects.filter(
user1_id=dingdan.user1_id,
shangpin_id=dingdan.shangpin_id,
zhuangtai=9
).exclude(dingdan_id=dingdan.dingdan_id).update(zhuangtai=25)
# 更新老板扩展表
laoban_id = dingdan.pingtai_kuozhan.laoban_id if hasattr(dingdan, 'pingtai_kuozhan') else None
if laoban_id:
user_main = UserMain.objects.filter(yonghuid=laoban_id).first()
if user_main and hasattr(user_main, 'boss_profile'):
boss = user_main.boss_profile
boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0)
boss.alldingdan = (boss.alldingdan or 0) + 1
boss.save()
# 更新商品库存
if dingdan.shangpin_id:
Shangpin.objects.filter(id=dingdan.shangpin_id).update(
kucun=F('kucun') - 1
)
# 更新收支记录
self._update_szjilu(dingdan.jine)
# 更新每日收入统计
update_daily_income(dingdan.jine)
# 跨平台同步(复查成功也需要通知)
sync_order_to_partners(dingdan.dingdan_id)
def _query_wechat_payment(self, out_trade_no):
"""
调用微信支付订单查询接口,返回 (trade_state, transaction_id)
若查询失败,抛出异常
"""
appid = wx_cfg.WEIXIN_APPID
mch_id = wx_cfg.WEIXIN_MCHID
key = wx_cfg.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={
'zongsy': Decimal('0.00'),
'zongls': Decimal('0.00'),
'zongzc': Decimal('0.00'),
'jrzc': Decimal('0.00'),
'jrls': Decimal('0.00')
}
)
if created:
logger.info("创建ID=1的收支记录")
szjilu.zongsy += jine_decimal
szjilu.zongls += jine_decimal
szjilu.jrls += 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)
"""
虚拟支付视图(代币模式)
session_key 缓存策略:
- 优先从 UserMain 表读取
- 不存在或即将过期时,用前端传来的 code 换取
- 换取成功后存入 UserMain.session_key其他接口直接从用户实例取
业务逻辑与原有商户支付完全一致,仅支付参数生成/查单方式替换为虚拟支付 API
"""
import json
import hmac
import hashlib
import logging
import time
import random
from decimal import Decimal
import requests
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from django.db import transaction
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions
from rest_framework.parsers import JSONParser
from django.http import HttpResponse
import xmltodict
import threading
from .models import Dingdan, DingdanPingtai, Lilubiao
from shangpin.models import Shangpin
from yonghu.models import UserMain, UserDashou, UserBoss
from peizhi.models import Szjilu
from .utils import update_daily_income, sync_order_to_partners
logger = logging.getLogger(__name__)
class VirtualCreateOrderView(APIView):
"""
虚拟代币支付创建订单
流程与原 CreateOrderView 完全一致
session_key 管理(已修改为强制刷新):
每次下单必须携带 code用 code 换取最新 session_key 并存入 UserMain 表
不再信任数据库中缓存的旧 session_key
"""
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('beizhu', '').strip()
zhiding = request.data.get('zhiding', '').strip()
jine = request.data.get('jine', 0)
code = request.data.get('code') # 每次下单必须携带,用于换取最新 session_key
# 2. 指定功能临时关闭(与原逻辑一致)
if zhiding:
return Response({'code': 8, 'msg': '指定功能尚未开启请不要填写指定打手ID', 'data': None})
# 3. 必填字段验证
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})
# 4. 查询商品
shangpin = Shangpin.objects.filter(id=shangpin_id).first()
if not shangpin:
return Response({'code': 4, '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)
# 5. 验证指定打手(保留以备后续开启)
zhiding_id_value = None
if zhiding:
zhiding_user = UserMain.objects.filter(yonghuid=zhiding).first()
if not zhiding_user:
return Response({'code': 7, 'msg': '指定打手不存在', 'data': None})
dashou_profile = UserDashou.objects.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
# 6. 生成订单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}"
# 7. 计算打手分成(完全复用原逻辑)
dashou_lilu = Lilubiao.objects.filter(fadanpingtai='1').first()
if not dashou_lilu:
return Response({'code': 9, 'msg': '系统利率配置不完整', 'data': None})
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.lilu))
# ---------- 修改 session_key 获取逻辑 ----------
# 每次下单必须携带 code强制刷新 session_key
if not code:
return Response({'code': 10, 'msg': '缺少登录凭证code', 'data': None})
wx_resp = self._exchange_code_for_session_key(code)
if wx_resp is None:
return Response({'code': 11, 'msg': '微信认证失败', 'data': None})
session_key = wx_resp['session_key']
user_openid = wx_resp['openid']
# 立即更新当前用户的 openid 和 session_key 到数据库,保证其他接口可用
current_user = request.user
current_user.openid = user_openid
current_user.session_key = session_key
current_user.save(update_fields=['openid', 'session_key'])
logger.info(f"[VirtualCreate] 已刷新 session_key 并存入数据库: openid={user_openid}")
# ---------------------------------------------
# 9. 数据库事务创建订单(与原逻辑完全相同)
with transaction.atomic():
laoban_yonghuid = getattr(current_user, 'yonghuid', '')
# 订单主表
order_data = {
'dingdan_id': dingdanid,
'zhuangtai': 9,
'jine': shangpin_jiage,
'fadan_pingtai': 1,
'dashou_fencheng': dashou_fencheng,
'zhiding_id': zhiding_id_value,
'shangpin_id': shangpin_id,
'yaoqiuleixing': getattr(shangpin, 'yaoqiuleixing', None),
'huiyuan_id': getattr(shangpin, 'huiyuan_id', ''),
'yongjin': getattr(shangpin, 'yongjin', 0),
'jieshao': getattr(shangpin, 'jieshao', ''),
'beizhu': beizhu,
'nicheng': nicheng,
'tupian': shangpin_tupian,
'user1_id': f"Boss{current_user.yonghuid}",
'leixing_id': shangpin.leixing_id
}
dingdan = Dingdan.objects.create(**order_data)
# 订单平台扩展表
DingdanPingtai.objects.create(
dingdan=dingdan,
laoban_id=laoban_yonghuid,
)
# 生成代币虚拟支付参数
pay_params = self.generate_virtual_pay_params(
dingdanid=dingdanid,
jine=shangpin_jiage,
openid=user_openid,
session_key=session_key,
beizhu=beizhu
)
if not pay_params:
raise Exception('虚拟支付参数生成失败')
return Response({
'code': 0,
'msg': '订单创建成功',
'data': {
'dingdanid': dingdanid,
'payParams': pay_params
}
})
except Exception as e:
logger.error(f"虚拟支付下单失败: {e}", exc_info=True)
return Response({'code': 99, 'msg': '系统错误', 'data': None})
def _exchange_code_for_session_key(self, code):
"""
通过 jscode2session 换取 openid 和 session_key
"""
url = 'https://api.weixin.qq.com/sns/jscode2session'
params = {
'appid': wx_cfg.WEIXIN_APPID,
'secret': wx_cfg.WEIXIN_SECRET,
'js_code': code,
'grant_type': 'authorization_code'
}
try:
resp = requests.get(url, params=params, timeout=5)
data = resp.json()
if 'openid' in data and 'session_key' in data:
return data
else:
logger.error(f"jscode2session 失败: {data}")
except Exception as e:
logger.error(f"换取 session_key 异常: {e}")
return None
def generate_virtual_pay_params(self, dingdanid, jine, openid, session_key, beizhu=''):
"""
生成代币虚拟支付参数sell_coin 模式)
"""
env = settings.VP_ENV
appkey = settings.VP_APPKEY_SANDBOX if env == 1 else settings.VP_APPKEY
offer_id = settings.VP_OFFER_ID
product_id = 'coin_1' # 代币道具ID
# 购买代币数量 = 总价(分)
buy_quantity = int(float(jine) * 100)
# signData 构建(驼峰字段,代币模式不含 goodsPrice
sign_data = {
"offerId": offer_id,
"env": env,
"openid": openid,
"outTradeNo": dingdanid,
"productId": product_id,
"buyQuantity": buy_quantity,
"attach": beizhu or '游戏陪玩订单',
"currencyType": "CNY",
"zoneId": "1"
}
sign_data_str = json.dumps(sign_data, separators=(',', ':'), ensure_ascii=False)
# 支付签名paySig+ 用户态签名signature
uri = "requestVirtualPayment"
msg = uri + "&" + sign_data_str
pay_sig = hmac.new(appkey.encode('utf-8'), msg.encode('utf-8'), hashlib.sha256).hexdigest()
signature = hmac.new(session_key.encode('utf-8'), sign_data_str.encode('utf-8'), hashlib.sha256).hexdigest()
return {
"signData": sign_data_str,
"paySig": pay_sig,
"signature": signature,
"mode": "short_series_coin"
}
class VirtualPayNotifyView(APIView):
"""
虚拟支付回调接口
业务处理逻辑与原 WechatPayNotifyView 完全一致,仅 XML 解析和签名验证方式不同
"""
permission_classes = [permissions.AllowAny]
def post(self, request):
try:
body = request.body
# 虚拟支付回调签名验证
signature = request.headers.get('Wechatpay-Signature')
if not self._verify_virtual_signature(body, signature):
logger.warning("虚拟支付回调签名验证失败")
return HttpResponse('FAIL', status=401)
data = xmltodict.parse(body.decode('utf-8')).get('xml', {})
event = data.get('Event')
if event != 'xpay_goods_deliver_notify':
return HttpResponse('success')
out_trade_no = data.get('OutTradeNo')
transaction_id = ''
wechat_pay_info = data.get('WeChatPayInfo', {})
if isinstance(wechat_pay_info, dict):
transaction_id = wechat_pay_info.get('TransactionId', '')
# 执行支付成功处理(完全复用原 _handle_success 逻辑)
self._handle_success(out_trade_no, transaction_id)
return HttpResponse('success')
except Exception as e:
logger.error(f"虚拟支付回调异常: {e}", exc_info=True)
return HttpResponse('success')
def _verify_virtual_signature(self, body, signature):
"""验证虚拟支付回调签名"""
if not signature:
return False
appkey = settings.VP_APPKEY_SANDBOX if settings.VP_ENV == 1 else settings.VP_APPKEY
calc_sign = hmac.new(appkey.encode('utf-8'), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(calc_sign, signature)
@transaction.atomic
def _handle_success(self, out_trade_no, transaction_id):
"""
支付成功处理(与 WechatPayNotifyView._handle_success 完全相同)
"""
logger.info(f"虚拟支付回调成功, out_trade_no={out_trade_no}, transaction_id={transaction_id}")
try:
dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=out_trade_no).first()
except Exception as e:
logger.error(f"查询订单异常: {e}")
raise
if not dingdan:
logger.error(f"订单不存在: {out_trade_no}")
return
# 幂等:仅处理未支付订单
if dingdan.zhuangtai != 9:
logger.info(f"订单 {out_trade_no} 已处理过,状态={dingdan.zhuangtai},跳过")
return
# 保存交易号
dingdan.wechat_transaction_id = transaction_id or 'virtual_' + out_trade_no
# 更新状态:指定单 7普通单 1
if dingdan.zhiding_id and dingdan.zhiding_id.strip():
dingdan.zhuangtai = 7
else:
dingdan.zhuangtai = 1
dingdan.save()
# 废弃同商品其他未支付订单(状态 25
Dingdan.objects.filter(
user1_id=dingdan.user1_id,
shangpin_id=dingdan.shangpin_id,
zhuangtai=9
).exclude(dingdan_id=out_trade_no).update(zhuangtai=25)
# 更新老板消费数据
try:
laoban_id = dingdan.pingtai_kuozhan.laoban_id if hasattr(dingdan, 'pingtai_kuozhan') else None
if laoban_id:
user_main = UserMain.objects.filter(yonghuid=laoban_id).first()
if user_main and hasattr(user_main, 'boss_profile'):
boss = user_main.boss_profile
boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0)
boss.alldingdan = (boss.alldingdan or 0) + 1
boss.save()
except Exception as e:
logger.error(f"更新老板数据异常: {e}")
# 更新商品库存
if dingdan.shangpin_id:
Shangpin.objects.filter(id=dingdan.shangpin_id).update(kucun=F('kucun') - 1)
# 收支记录、日收入、跨平台同步
self._update_szjilu(dingdan.jine)
update_daily_income(dingdan.jine)
sync_order_to_partners(dingdan.dingdan_id)
# 异步广播通知
self._broadcast_async(dingdan)
logger.info(f"订单 {out_trade_no} 支付成功处理完成")
def _update_szjilu(self, jine):
"""更新收支记录表(与旧逻辑一致)"""
try:
jine_decimal = Decimal(str(jine))
with transaction.atomic():
szjilu, created = Szjilu.objects.select_for_update().get_or_create(
id=1,
defaults={
'zongsy': Decimal('0.00'), 'zongls': Decimal('0.00'),
'zongzc': Decimal('0.00'), 'jrzc': Decimal('0.00'),
'jrls': Decimal('0.00')
}
)
szjilu.zongsy += jine_decimal
szjilu.zongls += jine_decimal
szjilu.jrls += jine_decimal
szjilu.save()
except Exception as e:
logger.error(f"更新收支记录失败: {e}")
def _broadcast_async(self, dingdan):
try:
from utils.order_broadcast import schedule_dingdan_guangbo_from_order
schedule_dingdan_guangbo_from_order(dingdan)
except Exception as e:
logger.error(f'提交广播任务失败: {e}', exc_info=True)
def _get_game_type_name(self, leixing_id):
try:
from shangpin.models import ShangpinLeixing
gt = ShangpinLeixing.objects.filter(id=leixing_id).first()
return gt.jieshao if gt else '游戏订单'
except Exception:
return '游戏订单'
'''class VirtualPaymentVerifyView(APIView):
"""
虚拟支付复查接口(代币订单)- 完整调试版
"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
try:
dingdanid = request.data.get('dingdanid')
print(f"\n{'='*60}")
print(f"[复查-步骤1] 收到复查请求")
print(f"[复查-步骤1] 订单号: {dingdanid}")
print(f"{'='*60}")
logger.info(f"[复查] 收到复查请求 dingdanid={dingdanid}")
if not dingdanid:
return Response({'code': 1, 'msg': '订单ID不能为空'})
dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first()
if not dingdan:
print(f"[复查-错误] 订单不存在: {dingdanid}")
return Response({'code': 2, 'msg': '订单不存在'})
current_user = request.user
user_identifier = f"Boss{current_user.yonghuid}"
if dingdan.user1_id != user_identifier:
print(f"[复查-错误] 无权访问: user={user_identifier}, order_owner={dingdan.user1_id}")
return Response({'code': 403, 'msg': '无权访问'})
if dingdan.zhuangtai in [1, 7]:
print(f"[复查-步骤2] 订单已成功,状态: {dingdan.zhuangtai}")
return Response({
'code': 0,
'msg': '订单状态正常',
'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai}
})
if dingdan.zhuangtai == 9:
from django.core.cache import cache
lock_key = f"virtual_verify_lock_{dingdanid}"
if not cache.add(lock_key, "locked", timeout=10):
print(f"[复查-步骤3] 订单正在处理中,跳过")
return Response({'code': 1, 'msg': '订单正在处理中,请稍后'})
try:
print(f"[复查-步骤4] 开始查单...")
trade_state, transaction_id = self._query_virtual_payment(dingdanid)
print(f"[复查-步骤5] 查单结果: trade_state={trade_state}, transaction_id={transaction_id}")
if trade_state == 'SUCCESS':
self._process_payment_success(dingdan, transaction_id, current_user.yonghuid)
dingdan.refresh_from_db()
print(f"[复查-成功] 订单状态已更新为 {dingdan.zhuangtai}")
return Response({
'code': 0,
'msg': '支付成功',
'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai}
})
elif trade_state in ['NOTPAY', 'USERPAYING']:
return Response({'code': 1, 'msg': '支付中', 'data': {'zhuangtai': 9}})
else:
return Response({'code': 2, 'msg': '支付失败'})
except Exception as e:
print(f"[复查-异常] {e}")
return Response({'code': 1, 'msg': f'查询失败: {str(e)}'})
finally:
cache.delete(lock_key)
status_msg = {
2: '订单已取消', 3: '订单已完成', 4: '退款审核中',
5: '已退款', 6: '退款失败', 8: '结算中', 0: '订单已作废'
}
return Response({'code': 3, 'msg': status_msg.get(dingdan.zhuangtai, '订单状态异常'),
'data': {'zhuangtai': dingdan.zhuangtai}})
except Exception as e:
print(f"[复查-全局异常] {e}")
return Response({'code': 99, 'msg': '系统错误'})
def _get_access_token(self):
"""
获取 access_token打印每一步详细信息
"""
from django.core.cache import cache
print(f"\n{'='*60}")
print(f"[获取AccessToken-步骤1] 开始获取 access_token")
print(f"[获取AccessToken-步骤1] AppID: {wx_cfg.WEIXIN_APPID}")
print(f"{'='*60}")
cache_key = "virtual_payment_access_token"
access_token = cache.get(cache_key)
if access_token:
print(f"[获取AccessToken-成功] 从缓存获取到 access_token")
print(f"[获取AccessToken-成功] token 长度: {len(access_token)}")
print(f"[获取AccessToken-成功] token 前10位: {access_token[:10]}...")
print(f"[获取AccessToken-成功] token 后10位: ...{access_token[-10:]}")
return access_token
print(f"[获取AccessToken-步骤2] 缓存未命中,调用微信接口获取...")
try:
resp = requests.get(
"https://api.weixin.qq.com/cgi-bin/token",
params={
"grant_type": "client_credential",
"appid": wx_cfg.WEIXIN_APPID,
"secret": wx_cfg.WEIXIN_SECRET,
},
timeout=10,
)
data = resp.json()
print(f"[获取AccessToken-步骤3] 微信接口返回完整数据:")
print(f" {json.dumps(data, indent=2, ensure_ascii=False)}")
if "access_token" in data:
access_token = data["access_token"]
expires_in = data.get("expires_in", 7200)
cache.set(cache_key, access_token, expires_in - 300)
print(f"[获取AccessToken-成功] 新获取的 access_token:")
print(f"[获取AccessToken-成功] - 长度: {len(access_token)}")
print(f"[获取AccessToken-成功] - 前10位: {access_token[:10]}...")
print(f"[获取AccessToken-成功] - 后10位: ...{access_token[-10:]}")
print(f"[获取AccessToken-成功] - 有效期: {expires_in}")
return access_token
else:
errcode = data.get("errcode", "未知")
errmsg = data.get("errmsg", "未知")
print(f"[获取AccessToken-失败] 微信返回错误:")
print(f"[获取AccessToken-失败] - errcode: {errcode}")
print(f"[获取AccessToken-失败] - errmsg: {errmsg}")
print(f"[获取AccessToken-失败] - 可能原因: AppID或AppSecret错误")
raise Exception(f"获取 access_token 失败: errcode={errcode}, errmsg={errmsg}")
except Exception as e:
print(f"[获取AccessToken-异常] 网络请求失败: {e}")
raise
def _query_virtual_payment(self, out_trade_no):
"""
查询代币订单状态 - 完整调试版
"""
print(f"\n{'='*60}")
print(f"[查单-步骤1] 开始查单流程")
print(f"[查单-步骤1] 订单号: {out_trade_no}")
print(f"{'='*60}")
# 1. 获取 access_token
access_token = self._get_access_token()
# 2. 准备请求参数
uri = "/wxa/game/queryorderinfo"
base_url = "https://api.weixin.qq.com"
appkey = settings.VP_APPKEY_SANDBOX if settings.VP_ENV == 1 else settings.VP_APPKEY
offer_id = settings.VP_OFFER_ID
ts = int(time.time())
print(f"\n[查单-步骤2] 请求参数配置:")
print(f"[查单-步骤2] - offer_id: {offer_id}")
print(f"[查单-步骤2] - env: {settings.VP_ENV} (0=现网, 1=沙箱)")
print(f"[查单-步骤2] - ts: {ts}")
print(f"[查单-步骤2] - appkey 长度: {len(appkey)}")
print(f"[查单-步骤2] - appkey 前5位: {appkey[:5]}...")
print(f"[查单-步骤2] - openid: {self.request.user.openid}")
body_data = {
"openid": self.request.user.openid,
"offer_id": offer_id,
"ts": ts,
"zone_id": "1",
"env": settings.VP_ENV,
"out_trade_no": out_trade_no,
"biz_id": 1
}
body = json.dumps(body_data, separators=(",", ":"))
print(f"\n[查单-步骤3] 请求体 (Body):")
print(f" {body}")
# 3. 生成签名
msg = uri + "&" + body
pay_sig = hmac.new(
appkey.encode("utf-8"),
msg.encode("utf-8"),
hashlib.sha256
).hexdigest()
session_key = getattr(self.request.user, 'session_key', '')
signature = ""
if session_key:
signature = hmac.new(
session_key.encode("utf-8"),
body.encode("utf-8"),
hashlib.sha256
).hexdigest()
print(f"\n[查单-步骤4] 签名信息:")
print(f"[查单-步骤4] - pay_sig: {pay_sig}")
print(f"[查单-步骤4] - signature: {signature}")
print(f"[查单-步骤4] - session_key 长度: {len(session_key) if session_key else 0}")
# 4. 构建完整 URL
url = (
f"{base_url}{uri}"
f"?access_token={access_token}"
f"&signature={signature}"
f"&sig_method=hmac_sha256"
f"&pay_sig={pay_sig}"
)
print(f"\n[查单-步骤5] 完整请求 URL:")
print(f" {url}")
# 5. 发起请求
print(f"\n[查单-步骤6] 正在发送请求...")
headers = {"Content-Type": "application/json"}
response = requests.post(url, data=body, headers=headers, timeout=10)
result = response.json()
print(f"\n[查单-步骤7] 微信返回完整数据:")
print(f" HTTP 状态码: {response.status_code}")
print(f" {json.dumps(result, indent=2, ensure_ascii=False)}")
# 6. 解析结果
errcode = result.get("errcode", -1)
errmsg = result.get("errmsg", "")
print(f"\n[查单-步骤8] 结果分析:")
print(f"[查单-步骤8] - errcode: {errcode}")
print(f"[查单-步骤8] - errmsg: {errmsg}")
if errcode == 0:
pay_state = result.get("pay_state", 1)
trade_state_map = {1: "USERPAYING", 2: "SUCCESS", 3: "CLOSED"}
trade_state = trade_state_map.get(pay_state, "UNKNOWN")
transaction_id = result.get("transaction_id", "")
print(f"[查单-成功] pay_state={pay_state}, trade_state={trade_state}")
return trade_state, transaction_id
# 错误分析
print(f"\n[查单-失败] 错误分析:")
error_map = {
40001: "access_token 无效或过期 (invalid credential)",
48001: "API 权限未开通 (api unauthorized)",
90016: "session_key 已过期",
90019: "订单号未找到"
}
reason = error_map.get(errcode, f"未知错误码: {errcode}")
print(f"[查单-失败] 错误原因: {reason}")
if errcode == 48001:
print(f"\n[查单-排查建议] API 权限未开通,请检查:")
print(f" 1. 小程序后台 -> 功能 -> 虚拟支付2.0 -> 支付应用 -> 确认签约状态")
print(f" 2. 小程序后台 -> 功能 -> 虚拟支付2.0 -> 支付应用 -> 权限管理 -> 开启查单权限")
print(f" 3. 确认 offer_id ({offer_id}) 与当前小程序 AppID 匹配")
print(f" 4. 确认使用的 appkey 与支付应用后台一致")
raise Exception(f"查单失败: [{errcode}] {errmsg} - {reason}")
@transaction.atomic
def _process_payment_success(self, dingdan, transaction_id, operator_id):
"""支付成功原子操作,与旧流程完全一致"""
dingdan = Dingdan.objects.select_for_update().get(pk=dingdan.pk)
if dingdan.zhuangtai != 9:
return
dingdan.zhuangtai = 7 if (dingdan.zhiding_id and dingdan.zhiding_id.strip()) else 1
dingdan.wechat_transaction_id = transaction_id
dingdan.clkf = operator_id
dingdan.save()
Dingdan.objects.filter(
user1_id=dingdan.user1_id,
shangpin_id=dingdan.shangpin_id,
zhuangtai=9
).exclude(dingdan_id=dingdan.dingdan_id).update(zhuangtai=25)
laoban_id = dingdan.pingtai_kuozhan.laoban_id if hasattr(dingdan, 'pingtai_kuozhan') else None
if laoban_id:
user_main = UserMain.objects.filter(yonghuid=laoban_id).first()
if user_main and hasattr(user_main, 'boss_profile'):
boss = user_main.boss_profile
boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0)
boss.alldingdan = (boss.alldingdan or 0) + 1
boss.save()
if dingdan.shangpin_id:
Shangpin.objects.filter(id=dingdan.shangpin_id).update(kucun=F('kucun') - 1)
self._update_szjilu(dingdan.jine)
update_daily_income(dingdan.jine)
sync_order_to_partners(dingdan.dingdan_id)
def _update_szjilu(self, jine):
"""更新平台收支记录"""
try:
jine_decimal = Decimal(str(jine))
with transaction.atomic():
szjilu, created = Szjilu.objects.select_for_update().get_or_create(
id=1,
defaults={
'zongsy': Decimal('0.00'), 'zongls': Decimal('0.00'),
'zongzc': Decimal('0.00'), 'jrzc': Decimal('0.00'),
'jrls': Decimal('0.00')
}
)
szjilu.zongsy += jine_decimal
szjilu.zongls += jine_decimal
szjilu.jrls += jine_decimal
szjilu.save()
except Exception as e:
logger.error(f"更新收支记录失败: {e}")'''
class VirtualPaymentVerifyView(APIView):
"""
虚拟支付复查接口(代币订单)- 简化版
策略:
因微信后台查单API权限问题暂时绕过主动查单。
只要前端调起复查接口代表前端wx.requestVirtualPayment已成功回调
且订单状态为9未支付直接信任前端支付结果更新订单为已支付。
安全提示:
此方式依赖JWT Token的不可伪造性。若Token泄露可能被伪造支付。
后期建议开通查单权限后恢复主动查单逻辑。
业务逻辑与原 PaymentVerifyView 完全一致,仅跳过微信查单步骤。
"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
try:
dingdanid = request.data.get('dingdanid')
logger.info(f"[复查] 收到复查请求 dingdanid={dingdanid}")
if not dingdanid:
return Response({'code': 1, 'msg': '订单ID不能为空'})
dingdan = Dingdan.objects.filter(dingdan_id=dingdanid).first()
if not dingdan:
logger.error(f"[复查] 订单不存在: {dingdanid}")
return Response({'code': 2, 'msg': '订单不存在'})
current_user = request.user
user_identifier = f"Boss{current_user.yonghuid}"
if dingdan.user1_id != user_identifier:
logger.warning(f"[复查] 无权访问: user={user_identifier}, order_owner={dingdan.user1_id}")
return Response({'code': 403, 'msg': '无权访问'})
# 订单已经成功(状态 1 普通单 / 7 指定单)
if dingdan.zhuangtai in [1, 7]:
logger.info(f"[复查] 订单状态已成功: {dingdan.zhuangtai}")
return Response({
'code': 0,
'msg': '订单状态正常',
'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai}
})
# 订单为未支付,直接信任前端支付结果
if dingdan.zhuangtai == 9:
from django.core.cache import cache
lock_key = f"virtual_verify_lock_{dingdanid}"
if not cache.add(lock_key, "locked", timeout=10):
logger.info(f"[复查] 订单 {dingdanid} 正在处理中,稍后重试")
return Response({'code': 1, 'msg': '订单正在处理中,请稍后'})
try:
# 尝试调用微信查单(日志记录用,结果不影响业务流程)
try:
trade_state, transaction_id = self._query_virtual_payment(dingdanid)
logger.info(f"[复查] 微信查单结果: trade_state={trade_state}, transaction_id={transaction_id}")
except Exception as e:
logger.warning(f"[复查] 微信查单失败(忽略): {e}")
transaction_id = ""
# 无论查单结果如何,直接更新订单为已支付
logger.info(f"[复查] 信任前端支付结果,执行本地更新")
self._process_payment_success(dingdan, transaction_id, current_user.yonghuid)
dingdan.refresh_from_db()
logger.info(f"[复查] 订单状态已更新为 {dingdan.zhuangtai}")
return Response({
'code': 0,
'msg': '支付成功',
'data': {'dingdanid': dingdanid, 'zhuangtai': dingdan.zhuangtai}
})
finally:
cache.delete(lock_key)
# 其他异常状态
status_msg = {
2: '订单已取消', 3: '订单已完成', 4: '退款审核中',
5: '已退款', 6: '退款失败', 8: '结算中', 0: '订单已作废'
}
logger.info(f"[复查] 订单状态非9/1/7: zhuangtai={dingdan.zhuangtai}")
return Response({'code': 3, 'msg': status_msg.get(dingdan.zhuangtai, '订单状态异常'),
'data': {'zhuangtai': dingdan.zhuangtai}})
except Exception as e:
logger.error(f"[复查] 接口异常: {e}", exc_info=True)
return Response({'code': 99, 'msg': '系统错误'})
@transaction.atomic
def _process_payment_success(self, dingdan, transaction_id, operator_id):
"""
支付成功原子操作(与旧流程完全一致)
"""
logger.info(f"[复查-处理] 开始处理支付成功: dingdanid={dingdan.dingdan_id}, transaction_id={transaction_id}")
dingdan = Dingdan.objects.select_for_update().get(pk=dingdan.pk)
if dingdan.zhuangtai != 9:
logger.warning(f"[复查-处理] 订单状态已非9跳过: 当前状态={dingdan.zhuangtai}")
return
dingdan.zhuangtai = 7 if (dingdan.zhiding_id and dingdan.zhiding_id.strip()) else 1
dingdan.wechat_transaction_id = transaction_id
dingdan.clkf = operator_id
dingdan.save()
logger.info(f"[复查-处理] 订单状态更新为 {dingdan.zhuangtai}")
# 废弃同商品其他未支付订单
abandoned = Dingdan.objects.filter(
user1_id=dingdan.user1_id,
shangpin_id=dingdan.shangpin_id,
zhuangtai=9
).exclude(dingdan_id=dingdan.dingdan_id).update(zhuangtai=25)
logger.info(f"[复查-处理] 废弃 {abandoned} 个重复未支付订单")
# 更新老板消费数据
laoban_id = dingdan.pingtai_kuozhan.laoban_id if hasattr(dingdan, 'pingtai_kuozhan') else None
if laoban_id:
user_main = UserMain.objects.filter(yonghuid=laoban_id).first()
if user_main and hasattr(user_main, 'boss_profile'):
boss = user_main.boss_profile
boss.zonge = (boss.zonge or 0) + (dingdan.jine or 0)
boss.alldingdan = (boss.alldingdan or 0) + 1
boss.save()
logger.info(f"[复查-处理] 更新老板 {laoban_id} 消费数据: +{dingdan.jine}元, 累计订单+1")
# 减库存
if dingdan.shangpin_id:
Shangpin.objects.filter(id=dingdan.shangpin_id).update(kucun=F('kucun') - 1)
logger.info(f"[复查-处理] 商品 {dingdan.shangpin_id} 库存减1")
# 收支、日收入、跨平台同步
self._update_szjilu(dingdan.jine)
update_daily_income(dingdan.jine)
sync_order_to_partners(dingdan.dingdan_id)
logger.info(f"[复查-处理] 收支、日收入、跨平台同步完成")
def _query_virtual_payment(self, out_trade_no):
"""
尝试调用微信查单接口(仅日志记录)
若失败会抛出异常,由上层捕获
"""
# 1. 获取 access_token
from django.core.cache import cache
access_token = self._get_access_token()
# 2. 准备请求体
uri = "/wxa/game/queryorderinfo"
base_url = "https://api.weixin.qq.com"
appkey = settings.VP_APPKEY_SANDBOX if settings.VP_ENV == 1 else settings.VP_APPKEY
offer_id = settings.VP_OFFER_ID
ts = int(time.time())
body_data = {
"openid": self.request.user.openid,
"offer_id": offer_id,
"ts": ts,
"zone_id": "1",
"env": settings.VP_ENV,
"out_trade_no": out_trade_no,
"biz_id": 1 # 代币=1
}
body = json.dumps(body_data, separators=(",", ":"))
# 3. 双重签名
msg = uri + "&" + body
pay_sig = hmac.new(
appkey.encode("utf-8"),
msg.encode("utf-8"),
hashlib.sha256
).hexdigest()
session_key = getattr(self.request.user, 'session_key', '')
signature = hmac.new(
session_key.encode("utf-8"),
body.encode("utf-8"),
hashlib.sha256
).hexdigest() if session_key else ""
# 4. 发送请求
url = (
f"{base_url}{uri}"
f"?access_token={access_token}"
f"&signature={signature}"
f"&sig_method=hmac_sha256"
f"&pay_sig={pay_sig}"
)
headers = {"Content-Type": "application/json"}
response = requests.post(url, data=body, headers=headers, timeout=10)
result = response.json()
errcode = result.get("errcode", -1)
if errcode == 0:
pay_state = result.get("pay_state", 1)
trade_state_map = {1: "USERPAYING", 2: "SUCCESS", 3: "CLOSED"}
trade_state = trade_state_map.get(pay_state, "UNKNOWN")
transaction_id = result.get("transaction_id", "")
return trade_state, transaction_id
else:
raise Exception(f"查单失败: {result.get('errmsg', '未知错误')}")
def _get_access_token(self):
"""获取 access_token带缓存"""
from django.core.cache import cache
cache_key = "virtual_payment_access_token"
access_token = cache.get(cache_key)
if not access_token:
resp = requests.get(
"https://api.weixin.qq.com/cgi-bin/token",
params={
"grant_type": "client_credential",
"appid": wx_cfg.WEIXIN_APPID,
"secret": wx_cfg.WEIXIN_SECRET,
},
timeout=10,
)
data = resp.json()
if "access_token" in data:
access_token = data["access_token"]
cache.set(cache_key, access_token, data.get("expires_in", 7200) - 300)
else:
raise Exception(f"获取 access_token 失败: {data}")
return access_token
def _update_szjilu(self, jine):
"""更新平台收支记录(同旧逻辑)"""
try:
jine_decimal = Decimal(str(jine))
with transaction.atomic():
szjilu, created = Szjilu.objects.select_for_update().get_or_create(
id=1,
defaults={
'zongsy': Decimal('0.00'), 'zongls': Decimal('0.00'),
'zongzc': Decimal('0.00'), 'jrzc': Decimal('0.00'),
'jrls': Decimal('0.00')
}
)
szjilu.zongsy += jine_decimal
szjilu.zongls += jine_decimal
szjilu.jrls += jine_decimal
szjilu.save()
except Exception as e:
logger.error(f"更新收支记录失败: {e}")
# 支付失败删除订单接口(与旧 PaymentFailView 一致)
class VirtualPaymentFailView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
try:
dingdanid = request.data.get('dingdanid')
if not dingdanid:
return Response({'code': 1, 'msg': '订单ID不能为空'})
with transaction.atomic():
dingdan = Dingdan.objects.select_for_update().filter(dingdan_id=dingdanid).first()
if not dingdan:
return Response({'code': 2, 'msg': '订单不存在'})
if dingdan.zhuangtai != 9:
return Response({'code': 3, 'msg': '只能删除未支付的订单'})
DingdanPingtai.objects.filter(dingdan=dingdan).delete()
dingdan.delete()
return Response({'code': 0, 'msg': '订单已删除'})
except Exception as e:
return Response({'code': 99, 'msg': f'删除订单失败: {str(e)}'})
from yonghu.models import UserMain
# dingdan/views.py
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Q
from .models import Dingdan, DingdanPingtai
class DingdanHuoquView(APIView):
"""订单获取接口"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 获取当前登录用户
try:
current_user = request.user
yonghuid = current_user.yonghuid
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 = DingdanPingtai.objects.filter(laoban_id=yonghuid)
pingtai_order_ids = list(pingtai_orders.values_list('dingdan__dingdan_id', 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(zhuangtai__in=zhuangtai_list)
query = Q(dingdan_id__in=pingtai_order_ids) & status_query
orders = Dingdan.objects.filter(query).order_by('-create_time')
#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.dingdan_id,
'zhuangtai': order.zhuangtai,
'jine': float(order.jine) if order.jine else 0.0,
'tupian': order.tupian or '',
'jieshao': order.jieshao or '',
'beizhu': order.beizhu or '',
'nicheng': order.nicheng or '',
'chuangjianshijian': order.create_time.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
})
# dingdan/views.py
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django.db.models import Q
import json
from .models import Dingdan, DingdanPingtai, Dashoutupian
class DingdanXiangqingView(APIView):
"""订单详情接口"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 获取当前登录用户
try:
current_user = request.user
yonghuid = current_user.yonghuid
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 = DingdanPingtai.objects.filter(
laoban_id=yonghuid,
dingdan__dingdan_id=dingdan_id
).first()
if not pingtai_kuozhan:
# 如果不是平台订单,检查商家订单
from .models import DingdanShangjia
shangjia_kuozhan = DingdanShangjia.objects.filter(
shangjia_id=yonghuid,
dingdan__dingdan_id=dingdan_id
).first()
if not shangjia_kuozhan:
return Response({
'code': 4,
'msg': '订单不存在或无权限查看',
'data': None
})
# 2. 查询订单主表
dingdan = Dingdan.objects.filter(dingdan_id=dingdan_id).first()
if not dingdan:
return Response({
'code': 5,
'msg': '订单不存在',
'data': None
})
# 3. 获取订单信息
zhuangtai = dingdan.zhuangtai or 0
zhiding_id = dingdan.zhiding_id or ''
jiedan_dashou_id = dingdan.jiedan_dashou_id or ''
dashou_liuyan = dingdan.dashou_liuyan or ''
# 4. 查询打手交付图片
dashoutupian_records = Dashoutupian.objects.filter(
dingdan_id=dingdan_id
).order_by('create_time')[:9] # 最多取9条
dashoujiaofu_list = []
for record in dashoutupian_records:
if record.tupian:
dashoujiaofu_list.append(record.tupian)
# 5. 构建返回数据(字段名与前端对应)
response_data = {
'zhuangtai': zhuangtai, # 订单状态
'zhiding': zhiding_id, # 指定打手ID
'dashou': 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.yonghuid
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 = DingdanPingtai.objects.filter(
laoban_id=yonghuid,
dingdan__dingdan_id=dingdan_id
).select_related('dingdan').first()
if not pingtai_kuozhan:
return Response({
'code': 5,
'msg': '订单不存在或无权限操作',
'data': None
})
dingdan = pingtai_kuozhan.dingdan
# 2. 验证订单状态(必须是结算中 - 状态8
if dingdan.zhuangtai != 8:
return Response({
'code': 6,
'msg': '订单不在结算状态,无法结单',
'data': None
})
# 3. 获取订单相关数据
dashou_fencheng = dingdan.dashou_fencheng or Decimal('0.00') # 打手分成
jiedan_dashou_id = dingdan.jiedan_dashou_id or '' # 接单打手ID
shangpin_id = dingdan.shangpin_id # 商品ID
# 4. 更新平台扩展表的老板评价(如果有留言)
if liuyan:
pingtai_kuozhan.laoban_pingjia = liuyan
pingtai_kuozhan.save()
# 5. 如果有评分,更新评分表
if pingfen > 0 and jiedan_dashou_id:
# 使用filter查询更健壮
pingfen_records = Pingfen.objects.filter(dashou=jiedan_dashou_id)
if pingfen_records.exists():
# 已有记录,更新
pingfen_record = pingfen_records.first()
pingfen_record.zongfen += Decimal(str(pingfen))
pingfen_record.cishu += 1
pingfen_record.save()
else:
# 新记录,创建
Pingfen.objects.create(
dashou=jiedan_dashou_id,
zongfen=Decimal(str(pingfen)),
cishu=1
)
# 6. 更新商品真实销量如果有商品ID
if shangpin_id:
shangpin_records = Shangpin.objects.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()
# ========== 新增:跨平台我方派单且有对方信息,直接通知对方并跳过本地结算 ==========
if (dingdan.is_cross == 1
and dingdan.partner_order_id
and dingdan.partner_club_id):
# 更新订单状态为已完成
dingdan.zhuangtai = 3
dingdan.clkf = current_user.phone if hasattr(current_user, 'phone') else yonghuid
dingdan.save()
from utils.chat_utils import schedule_order_status_chat_push
schedule_order_status_chat_push(dingdan.dingdan_id)
# 事务提交后异步通知对方平台
transaction.on_commit(
lambda: self._notify_partner_jiedan(
my_dingdan_id=dingdan.dingdan_id,
partner_club_id=dingdan.partner_club_id,
partner_order_id=dingdan.partner_order_id,
new_status=3,
current_user=current_user
)
)
# 直接返回,跳过后续所有本地打手结算、评价、统计等
return Response({
'code': 0,
'msg': '结单成功(已通知对方平台)',
'data': None
})
# ====================================================================
# 7. 更新打手扩展表如果有打手ID和分成
if jiedan_dashou_id and dashou_fencheng > 0:
# 查询打手用户
dashou_user = UserMain.objects.filter(yonghuid=jiedan_dashou_id).first()
if dashou_user:
# 查询打手扩展表
dashou_kuozhan = UserDashou.objects.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.zhuangtai = 3
dingdan.save()
from utils.chat_utils import schedule_order_status_chat_push
schedule_order_status_chat_push(dingdan.dingdan_id)
# 9. 返回成功响应
return Response({
'code': 0,
'msg': '结单成功',
'data': None
})
except Exception as e:
# 事务会自动回滚
return Response({
'code': 7,
'msg': '结单失败,服务器内部错误',
'data': None
})
def _notify_partner_jiedan(self, my_dingdan_id, partner_club_id, partner_order_id, new_status, current_user):
"""
通知对方俱乐部我方订单已完成
参照 CrossPlatformForceCompleteView._notify_partner_order_status 实现
"""
try:
# 1. 获取我方俱乐部ID
from .models import CrossPlatformOrderData
my_club_config = ClubConfig.objects.filter(is_self=1).first()
if not my_club_config:
logger.error("未配置我方俱乐部信息,无法通知对方")
return
self_club_id = my_club_config.club_id
# 2. 查询对方服务器域名
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
partner_domain = club_rel.partner_domain
if not partner_domain:
logger.error(f"对方俱乐部 {partner_club_id} 未配置域名")
return
# 3. 构造通知请求
url = partner_domain.rstrip('/') + '/houtai/dfddtk'
payload = {
'order_id': partner_order_id, # 在对方看来这是他们的订单ID
'club_id': self_club_id,
'status': new_status
}
resp = requests.post(url, json=payload, timeout=3)
if resp.status_code == 200:
resp_json = resp.json()
if resp_json.get('code') == 0:
# 4. 更新跨平台订单扩展表(记录对方状态)
CrossPlatformOrderData.objects.update_or_create(
dingdan_id=my_dingdan_id, # 我方订单ID
defaults={
'partner_order_id': partner_order_id,
'partner_club_id': partner_club_id,
'partner_order_status': new_status,
'partner_dashou_id': '',
'partner_amount': Decimal('0')
}
)
logger.info(f"老板结单通知对方成功: {partner_order_id} -> {new_status}")
else:
logger.warning(f"对方接口返回错误: {resp_json.get('msg')}")
else:
logger.warning(f"对方接口HTTP错误: {resp.status_code}")
except requests.exceptions.Timeout:
logger.error(f"通知对方平台超时: {partner_club_id}")
except Exception as e:
logger.error(f"通知对方平台异常: {str(e)}")
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.db.models import Prefetch
from dengji.models import Chenghao
class ShangpinLeixingView(APIView):
"""
获取商家可用的商品类型列表(含关联板块的称号)
POST /dingdan/sjspleixing
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
shangpin_queryset = ShangpinLeixing.objects.filter(
shenhezhuangtai=3
).select_related('bankuai').order_by('paixu')
bankuai_ids = shangpin_queryset.values_list('bankuai_id', flat=True).distinct()
chenghao_list = Chenghao.objects.filter(
bankuai_id__in=bankuai_ids
).values('id', 'mingcheng', 'texiao_miaoshu', 'bankuai_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']
})
data_list = []
for sp in shangpin_queryset:
item = {
'id': sp.id,
'yaoqiuleixing': sp.yaoqiuleixing,
'huiyuan_id': sp.huiyuan_id,
'jieshao': sp.jieshao,
'yongjin': sp.yongjin,
'is_cross_enabled': sp.is_cross_enabled,
'is_special_type': sp.is_special_type, # 🔴 新增字段
'chenghaoList': bankuai_chenghao_map.get(sp.bankuai_id, [])
}
# 🔴 特殊类型:返回商品模板列表(押金>0
if sp.is_special_type:
goods_qs = Shangpin.objects.filter(
leixing_id=sp.id,
yaoqiuleixing=2,
yongjin__gt=0
).values('id', 'biaoqian', 'jieshao','yongjin')
item['goodsList'] = list(goods_qs)
else:
item['goodsList'] = []
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)
# views/order_views.py - ShangjiaPaifaView完整代码
import os
import logging
from shangpin.models import ShangpinLeixing
import threading
from utils.weixin_broadcast import WeixinBroadcastSender
logger = logging.getLogger(__name__)
# views/order_views.py - ShangjiaPaifaView完整代码
import os
import logging
from shangpin.models import ShangpinLeixing
import threading
from utils.weixin_broadcast import WeixinBroadcastSender
from dengji.models import DingdanBiaoqian,Chenghao
import os
import threading
import logging
from decimal import Decimal
from django.db import transaction
from django.db.models import F
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import status
from yonghu.models import UserMain, UserShangjia, UserDashou
from houtai.utils import update_shangjia_daily
# 导入 Celery 广播任务
from dingdan.tongzhi_tasks import dingdan_guangbo
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
class ShangjiaPaifaView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
logger.info(f"商家派单请求: user_id={request.user.yonghuid}, data={request.data}")
try:
data = request.data
# ----- 1. 获取原始参数(保持原类型)-----
shangpin_type_id = data.get('shangpinTypeId')
huiyuan_id = data.get('huiyuanId')
yaoqiuleixing = data.get('yaoqiuleixing')
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 < 10 or jiage > 10000:
return Response({'code': 400, 'msg': '价格需在10-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.objects.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.objects.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.shop_profile
except UserShangjia.DoesNotExist:
logger.warning(f"用户不是商家: {request.user.yonghuid}")
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 = UserMain.objects.get(yonghuid=zhiding_uid)
zhiding_dashou = zhiding_user.dashou_profile
if zhiding_dashou.zhanghaozhuangtai != 1:
return Response({'code': 400, 'msg': '指定打手状态异常'})
except UserMain.DoesNotExist:
return Response({'code': 400, 'msg': '指定用户不存在'})
except UserDashou.DoesNotExist:
return Response({'code': 400, 'msg': '指定用户不是打手'})
# ----- 10. 利率(打手+管事分成,打手优先) -----
from dingdan.utils import calc_shangjia_order_fencheng
dashou_fencheng, guanshi_fencheng, guanshi_shangjia_fencheng = calc_shangjia_order_fencheng(jiage)
# ----- 11. 生成订单ID -----
def generate_dingdan_id():
import time, random
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 = Dingdan.objects.create(
dingdan_id=generate_dingdan_id(),
zhuangtai=7 if zhiding_dashou else 1,
fadan_pingtai=2,
jine=jiage,
dashou_fencheng=dashou_fencheng,
guanshi_fencheng=guanshi_fencheng,
guanshi_shangjia_fencheng=guanshi_shangjia_fencheng,
zhiding_id=zhiding_uid if zhiding_dashou else '',
shangpin_id=0,
leixing_id=int(shangpin_type_id),
yaoqiuleixing=final_yaoqiu_type,
yongjin=final_yongjin,
huiyuan_id=final_huiyuan_id,
jieshao=dingdan_jieshao[:2000],
beizhu=dingdan_beizhu[:1000],
nicheng=laoban_name[:50],
tupian=request.user.avatar,
user1_id=f"Sj{request.user.yonghuid}"
)
logger.info(f"订单主表创建: {dingdan.dingdan_id}")
DingdanShangjia.objects.create(
dingdan=dingdan,
shangjia_id=request.user.yonghuid,
sjnicheng=shangjia.nicheng,
)
if chenghao_obj:
DingdanBiaoqian.objects.create(dingdan=dingdan, chenghao=chenghao_obj)
logger.info(f"订单关联称号: {chenghao_obj.mingcheng}")
# 更新商家统计
UserShangjia.objects.filter(id=shangjia.id).update(
fabu=F('fabu') + 1,
yue=F('yue') - jiage,
jinridingdan=F('jinridingdan') + 1,
jinriliushui=F('jinriliushui') + jiage,
jinyuedingdan=F('jinyuedingdan') + 1,
jinyueliushui=F('jinyueliushui') + jiage,
)
update_shangjia_daily(
yonghuid=request.user.yonghuid,
amount=Decimal(str(jiage)),
action=1
)
# 事务提交,订单已持久化
except Exception as e:
logger.error(f"订单创建事务失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '订单创建失败,请稍后重试'})
# ========== 以下操作均在事务外,避免异步任务查不到订单 ==========
# 跨平台同步(内部可能是异步任务,但函数本身是同步调用,现在订单已存在)
try:
sync_order_to_partners(dingdan.dingdan_id)
except Exception as e:
logger.error(f"跨平台同步失败: {e}", exc_info=True)
# Celery 广播任务broadcast 优先队列)
try:
from utils.order_broadcast import schedule_dingdan_guangbo_from_order
schedule_dingdan_guangbo_from_order(dingdan, order_type='商家派单')
except Exception as e:
logger.error(f"广播任务提交失败: {e}", exc_info=True)
logger.info(f"商家派单成功: {dingdan.dingdan_id}, 金额={jiage}")
return Response({'code': 200, 'msg': '派单成功', 'data': {'jine': str(dingdan.jine)}})
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.objects.filter(id=leixing_id).first()
return gt.jieshao if gt else '游戏订单'
except Exception:
return '游戏订单'
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.yonghuid
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('leixing_id', 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 = DingdanShangjia.objects.filter(
shangjia_id=yonghuid
).select_related('dingdan')
# 5. 状态过滤
base_qs = base_qs.filter(dingdan__zhuangtai__in=zhuangtai_list)
# 🆕 6. 商品类型筛选
if leixing_id is not None:
try:
leixing_id_int = int(leixing_id)
base_qs = base_qs.filter(dingdan__leixing_id=leixing_id_int)
except (ValueError, TypeError):
# 如果leixing_id不是有效整数忽略该筛选
pass
# 🆕 7. 模糊搜索支持订单ID、订单介绍、游戏昵称、打手ID
if keyword:
from django.db.models import Q
# 订单ID精确匹配dingdan_id有唯一索引先用精确匹配
dingdan_id_match = Q(dingdan__dingdan_id__exact=keyword)
# 订单介绍模糊匹配
jieshao_match = Q(dingdan__jieshao__icontains=keyword)
# 游戏昵称模糊匹配
nicheng_match = Q(dingdan__nicheng__icontains=keyword)
# 打手ID精确匹配
dashou_match = Q(dingdan__jiedan_dashou_id__exact=keyword)
base_qs = base_qs.filter(
dingdan_id_match | jieshao_match | nicheng_match | dashou_match
)
# 8. 计算待结算订单数量状态为8
pending_count = DingdanShangjia.objects.filter(
shangjia_id=yonghuid,
dingdan__zhuangtai=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('-dingdan__create_time')[start_index:end_index]
# 10. 构建返回数据
order_list = []
for shangjia_order in current_page_orders:
order = shangjia_order.dingdan
order_data = {
'dingdan_id': order.dingdan_id,
'zhuangtai': order.zhuangtai,
'jine': float(order.jine) if order.jine else 0.0,
'jieshao': order.jieshao or '',
'beizhu': order.beizhu or '',
'nicheng': order.nicheng or '',
'leixing_id': order.leixing_id, # 🆕 返回商品类型ID
'tupian': order.tupian or '', # 🆕 返回商品图片
'jiedan_dashou_id': order.jiedan_dashou_id or '', # 🆕 返回打手ID
'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time 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)
from dingdan.models import Dingdan, DingdanShangjia, Dashoutupian, Fadan
from .models import FadanFenhong # 注意fadan_fenhong 可能在其他 app 中,根据实际路径调整
logger = logging.getLogger(__name__)
class ShangjiaDingdanXiangqingView(APIView):
"""
商家订单详情接口
返回订单基础信息、打手信息、交付图片、罚款数据、分红利率、更换打手产生的新订单ID等
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 获取当前登录用户的 yonghuid
try:
current_user = request.user
yonghuid = current_user.yonghuid
except AttributeError:
return Response({
'code': 1,
'msg': '用户认证失败',
'data': None
}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 获取前端参数
try:
data = request.data
dingdan_id = data.get('dingdan_id', '').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 = DingdanShangjia.objects.filter(
shangjia_id=yonghuid,
dingdan__dingdan_id=dingdan_id
).select_related('dingdan').first()
if not shangjia_kuozhan:
return Response({
'code': 4,
'msg': '订单不存在或您无权限查看',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
order = shangjia_kuozhan.dingdan
# 4. 交付图片列表
dashou_tupian_list = Dashoutupian.objects.filter(
dingdan_id=dingdan_id
).order_by('create_time').values_list('tupian', flat=True)
dashou_jiaofu = list(dashou_tupian_list)
# 5. 打手详细信息头像、昵称、用户ID
dashou_yonghuid = order.jiedan_dashou_id or ''
dashou_nicheng = ''
dashou_avatar = ''
if dashou_yonghuid:
dashou_user = UserMain.objects.filter(
yonghuid=dashou_yonghuid
).select_related('dashou_profile').first()
if dashou_user:
dashou_avatar = dashou_user.avatar or ''
if hasattr(dashou_user, 'dashou_profile') and dashou_user.dashou_profile:
dashou_nicheng = dashou_user.dashou_profile.nicheng or ''
# 6. 罚款数据(如果存在)
fadan_obj = Fadan.objects.filter(
guanliandingdan_id=dingdan_id,
beichufa_id=dashou_yonghuid
).first()
fadan_dict = None
if fadan_obj:
fadan_dict = {
'id': fadan_obj.id,
'chufaliyou': fadan_obj.chufaliyou or '',
'fakuanjine': float(fadan_obj.fakuanjine) if fadan_obj.fakuanjine else 0.0,
'yingxiang_qiangdan': fadan_obj.yingxiang_qiangdan,
'zhuangtai': fadan_obj.zhuangtai,
'shensuliyou': fadan_obj.shensuliyou or '',
'tongyi_chufaliyou': fadan_obj.tongyi_chufaliyou or '',
'bohuiliyou': fadan_obj.bohuiliyou or '',
'create_time': fadan_obj.create_time.strftime('%Y-%m-%d %H:%M:%S') if fadan_obj.create_time else '',
'update_time': fadan_obj.update_time.strftime('%Y-%m-%d %H:%M:%S') if fadan_obj.update_time else '',
}
# 7. 商家自己的分红利率(用于罚款分红计算)
fenhong_lilv = 0.0
try:
# 查找商家作为分红者的最新记录,取应得利率
last_fenhong = FadanFenhong.objects.filter(
fenhongzhe_id=yonghuid,
shenfen=1 # 1=商家
).order_by('-create_time').first()
if last_fenhong:
fenhong_lilv = float(last_fenhong.yingde_lilv or 0)
except Exception as e:
logger.warning(f'获取分红利率失败: {e}')
# 8. 更换打手后产生的新订单ID如果当前订单被更换过
xin_dingdan_id = ''
try:
# 查找以当前订单作为“上一个订单ID”的新订单扩展记录
new_shangjia_order = DingdanShangjia.objects.filter(
shangyige_dingdan_id=dingdan_id,
shangjia_id=yonghuid
).select_related('dingdan').first()
if new_shangjia_order:
xin_dingdan_id = new_shangjia_order.dingdan.dingdan_id
except Exception as e:
logger.warning(f'查找新订单ID失败: {e}')
# 9. 构建返回数据
response_data = {
# 基础订单字段
'dingdan_id': order.dingdan_id,
'zhuangtai': order.zhuangtai or 0,
'jine': float(order.jine) if order.jine else 0.0,
'jieshao': order.jieshao or '',
'beizhu': order.beizhu or '',
'nicheng': order.nicheng or '',
'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else '',
'fadanpingtai': order.fadan_pingtai or 0,
'is_cross': order.is_cross or 0,
# 打手信息
'dashou_yonghuid': dashou_yonghuid,
'dashou_nicheng': dashou_nicheng,
'dashou_avatar': dashou_avatar,
# 服务详情
'fuwudashou_id': dashou_yonghuid, # 保留旧字段兼容
'dashou_liuyan': order.dashou_liuyan or '',
'dashou_jiaofu': dashou_jiaofu,
# 商家扩展字段(旧)
'shangjia_liuyan': shangjia_kuozhan.shangjia_pingjia or '',
'tkliyou': order.tkly or '',
'cfliyou': shangjia_kuozhan.cfliyou or '', # 保留,但不影响新罚款
'sqzhuangtai': shangjia_kuozhan.sqzhuangtai or 0,
'bhliyou': shangjia_kuozhan.bhliyou 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)
from dingdan.models import Dingdan, DingdanShangjia, PreSettlement
from dingdan.utils import update_daily_dispatch_stat
logger = logging.getLogger(__name__)
from dingdan.models import Dingdan, DingdanShangjia, PreSettlement
logger = logging.getLogger(__name__)
from houtai.utils import update_shangjia_daily
class ShangjiaJiesuanView(APIView):
"""商家结单接口(支持跨平台订单)"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
# 1. 获取当前登录用户
try:
current_user = request.user
yonghuid = current_user.yonghuid
try:
shangjia = current_user.shop_profile
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('dingdan_id', '').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)
settle_ctx = None
success_response = None
try:
with transaction.atomic():
# 4. 查询订单并加锁(修复并发问题)
order = Dingdan.objects.select_for_update().get(dingdan_id=dingdan_id)
# 验证订单是否属于当前商家
shangjia_kuozhan = DingdanShangjia.objects.filter(
dingdan=order,
shangjia_id=yonghuid
).first()
if not shangjia_kuozhan:
return Response({
'code': 5,
'msg': '订单不存在或您无权限操作',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 5. 验证订单状态
if order.zhuangtai != 8:
return Response({
'code': 6,
'msg': '订单不在结算状态,无法结单',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 获取订单字段
jine = order.jine or Decimal('0')
dashou_fencheng = order.dashou_fencheng or Decimal('0')
jiedan_dashou_id = order.jiedan_dashou_id
dispatch_type = order.dispatch_type # 1=我方派单2=对方派单
is_cross = order.is_cross
partner_club_id = order.partner_club_id
partner_order_id = order.partner_order_id
if not jiedan_dashou_id:
return Response({
'code': 7,
'msg': '订单没有接单打手,无法结单',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# ========== 跨平台判断 ==========
is_cross_claimed_by_partner = (is_cross == 1 and (partner_club_id or partner_order_id))
# ========== 根据订单类型处理 ==========
# 情况1对方派单我方打手接单-> 预结算,不直接给打手加钱
if dispatch_type == 2:
# 创建预结算记录
PreSettlement.objects.update_or_create(
dingdan_id=dingdan_id,
defaults={
'partner_order_id': partner_order_id,
'partner_club_id': partner_club_id or '000001',
'dashou_id': jiedan_dashou_id,
'amount': dashou_fencheng,
'order_amount': jine,
'status': 0,
'operator': request.user.phone,
}
)
# 更新订单状态
order.zhuangtai = 3
order.save()
# 更新统计表
update_daily_dispatch_stat(
direction=2,
partner_club_id=partner_club_id or '000001',
success_amount=jine,
success_count=1
)
# 情况2我方派单且被对方打手接单 -> 只改状态,不给本地打手加钱
elif dispatch_type == 1 and is_cross_claimed_by_partner:
order.zhuangtai = 3
order.save()
update_daily_dispatch_stat(
direction=1,
partner_club_id=partner_club_id or '000001',
success_amount=dashou_fencheng,
success_count=1
)
# 情况3本地订单 或 跨平台但自家打手接单 -> 正常结算给打手
else:
# 查询打手信息
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou = dashou_user.dashou_profile
except (UserMain.DoesNotExist, UserDashou.DoesNotExist):
return Response({
'code': 8,
'msg': '接单打手不存在或信息不完整',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 原子更新打手数据(使用 F 表达式)
UserDashou.objects.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.objects.filter(id=shangjia.id).update(
chengjiao=F('chengjiao') + 1,
)
# 更新订单状态
order.zhuangtai = 3
order.save()
# 处理评分(所有情况都处理)
if pingfen > 0:
pingfen_obj, created = Pingfen.objects.get_or_create(
dashou=jiedan_dashou_id,
defaults={'zongfen': pingfen, 'cishu': 1}
)
if not created:
Pingfen.objects.filter(id=pingfen_obj.id).update(
zongfen=F('zongfen') + pingfen,
cishu=F('cishu') + 1,
)
# 更新商家订单扩展表的评价
DingdanShangjia.objects.filter(id=shangjia_kuozhan.id).update(
shangjia_pingjia=liuyan,
)
# 🔥 新增:调用打手每日统计(成交)
try:
update_dashou_daily_by_action(
yonghuid=jiedan_dashou_id,
amount=dashou_fencheng,
action=2 # 2 表示成交(结算)
)
logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 不影响主流程
# ✅ 新方法(行为驱动,更安全)
update_shangjia_daily(
yonghuid=request.user.yonghuid,
amount=Decimal(str(jine)),
action=2 # 2 = 结算
)
from utils.chat_utils import schedule_order_status_chat_push
schedule_order_status_chat_push(dingdan_id)
settle_ctx = (dingdan_id, jiedan_dashou_id)
success_response = 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)
if settle_ctx:
from dingdan.utils import settle_shangjia_order_guanshi_fenhong
settle_order = Dingdan.objects.get(dingdan_id=settle_ctx[0])
settle_shangjia_order_guanshi_fenhong(settle_order, settle_ctx[1])
if success_response is not None:
return success_response
except Dingdan.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)
from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu
class ShangjiaChexiaoView(APIView):
"""商家撤销订单申请接口"""
permission_classes = [IsAuthenticated] # JWT认证
def post(self, request):
# 1. 获取当前登录用户并验证商家身份
try:
current_user = request.user
# 使用反向查询获取商家扩展信息(性能最佳)
shangjia = current_user.shop_profile
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('dingdan_id', '').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 = Dingdan.objects.filter(dingdan_id=dingdan_id).first()
if not dingdan:
return Response({
'code': 5,
'msg': '订单不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 6. 验证订单状态
if dingdan.zhuangtai not in [1, 7]:
return Response({
'code': 6,
'msg': '当前订单状态不可撤销',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 7. 验证订单所属商家(使用反向查询)
shangjia_kuozhan = DingdanShangjia.objects.filter(
dingdan__dingdan_id=dingdan_id,
shangjia_id=current_user.yonghuid
).first()
if not shangjia_kuozhan:
return Response({
'code': 7,
'msg': '您无权撤销此订单',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 8. 创建退款记录注意申请状态直接设为1-成功)
Tuikuanjilu.objects.create(
dingdan_id=dingdan_id,
qingqiuid=current_user.yonghuid,
liyou=chexiao_liyou,
sqzhuangtai=1, # 直接成功
jine=dingdan.jine,
dashou_fencheng=dingdan.dashou_fencheng,
jieshao=dingdan.jieshao,
beizhu=dingdan.beizhu,
nicheng=dingdan.nicheng,
)
# 9. 更新商家扩展表数据使用F表达式原子操作
UserShangjia.objects.filter(id=shangjia.id).update(
yue=F('yue') + dingdan.jine,
tuikuan=F('tuikuan') + 1,
jinridingdan=F('jinridingdan') - 1,
jinyuedingdan=F('jinyuedingdan') - 1,
jinriliushui=F('jinriliushui') - dingdan.jine,
jinyueliushui=F('jinyueliushui') - dingdan.jine,
)
# 10. 更新订单状态为5已退款
dingdan.zhuangtai = 5
dingdan.save()
from utils.chat_utils import schedule_order_status_chat_push
schedule_order_status_chat_push(dingdan_id)
return Response({
'code': 0,
'msg': '撤销成功',
'data': {
'dingdan_id': dingdan_id,
'new_zhuangtai': 5,
'tuikuan_liyou': chexiao_liyou,
'jine': float(dingdan.jine) if dingdan.jine else 0.0
}
}, status=status.HTTP_200_OK)
except Exception as e:
# 记录异常日志
import logging
logger = logging.getLogger(__name__)
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.shop_profile
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('dingdan_id', '').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 = Dingdan.objects.filter(dingdan_id=dingdan_id).first()
if not dingdan:
return Response({
'code': 5,
'msg': '订单不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 6. 验证订单状态
if dingdan.zhuangtai not in [2, 8]:
return Response({
'code': 6,
'msg': '当前订单状态不可申请退款',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 7. 验证订单所属商家
shangjia_kuozhan = DingdanShangjia.objects.filter(
dingdan__dingdan_id=dingdan_id,
shangjia_id=current_user.yonghuid
).first()
if not shangjia_kuozhan:
return Response({
'code': 7,
'msg': '您无权为此订单申请退款',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 8. 创建退款记录申请状态为0-待处理)
Tuikuanjilu.objects.create(
dingdan_id=dingdan_id,
dashouid=dingdan.jiedan_dashou_id,
qingqiuid=current_user.yonghuid,
liyou=tuikuan_liyou,
sqzhuangtai=0, # 待处理
jine=dingdan.jine,
dashou_fencheng=dingdan.dashou_fencheng,
jieshao=dingdan.jieshao,
beizhu=dingdan.beizhu,
nicheng=dingdan.nicheng,
)
# 9. 更新订单状态为4退款中
dingdan.zhuangtai = 4
dingdan.tkly = tuikuan_liyou
dingdan.save()
from utils.chat_utils import schedule_order_status_chat_push
schedule_order_status_chat_push(dingdan_id)
# ====== 原有功能:更新打手状态 ======
if dingdan.jiedan_dashou_id:
try:
dashou_main = UserMain.objects.filter(
yonghuid=dingdan.jiedan_dashou_id
).first()
if dashou_main and hasattr(dashou_main, 'dashou_profile'):
dashou_profile = dashou_main.dashou_profile
dashou_profile.zhuangtai = 1
dashou_profile.save()
logger.info(f"退款申请成功,打手状态已更新:订单{dingdan_id},打手{dingdan.jiedan_dashou_id}状态变更为1")
except Exception as e:
logger.error(f"更新打手状态失败:{str(e)},订单{dingdan_id}打手ID{dingdan.jiedan_dashou_id}")
# ========== 新增:如果是跨平台订单,异步通知对方平台 ==========
if dingdan.is_cross == 1:
import threading
import requests
from django.conf import settings
def send_refund_notify():
url = f"{settings.PARTNER_BASE_URL}{settings.PARTNER_REFUND_NOTIFY_URL}"
payload = {
'token': settings.CROSS_PLATFORM_TOKEN,
'order_id': dingdan.dingdan_id # 发送我方订单ID对方用 partner_order_id 查找
}
try:
resp = requests.post(url, json=payload, timeout=5)
if resp.status_code == 200:
data = resp.json()
if data.get('code') == 0:
logger.info(f"跨平台退款通知发送成功: {dingdan.dingdan_id}")
else:
logger.error(f"跨平台退款通知业务失败: {data}")
else:
logger.error(f"跨平台退款通知HTTP失败: {resp.status_code}")
except Exception as e:
logger.error(f"跨平台退款通知异常: {str(e)}", exc_info=True)
thread = threading.Thread(target=send_refund_notify)
thread.daemon = True
thread.start()
logger.info(f"跨平台退款通知线程已启动: {dingdan.dingdan_id}")
# ========== 新增结束 ==========
return Response({
'code': 0,
'msg': '退款申请提交成功',
'data': {
'dingdan_id': dingdan_id,
'new_zhuangtai': 4,
'tuikuan_liyou': tuikuan_liyou,
'jine': float(dingdan.jine) if dingdan.jine else 0.0,
'dashou_id': dingdan.jiedan_dashou_id
}
}, 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.shop_profile
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('dingdan_id', '').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 = UserMain.objects.get(yonghuid=dashou_id)
dashou = dashou_user.dashou_profile
except UserMain.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 = Dingdan.objects.filter(dingdan_id=dingdan_id).first()
if not dingdan:
return Response({
'code': 6,
'msg': '订单不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 验证订单状态
if dingdan.zhuangtai not in [2, 8, 4, 5, 6, 3]:
return Response({
'code': 7,
'msg': '当前订单状态不可申请处罚',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 验证订单所属商家
shangjia_kuozhan = DingdanShangjia.objects.filter(
dingdan__dingdan_id=dingdan_id,
shangjia_id=current_user.yonghuid
).first()
if not shangjia_kuozhan:
return Response({
'code': 8,
'msg': '您无权为此订单申请处罚',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 验证订单的打手是否与提供的打手ID一致
if dingdan.jiedan_dashou_id != dashou_id:
return Response({
'code': 9,
'msg': '该打手并非此订单的服务打手',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 创建处罚记录
chufajilu = Chufajilu.objects.create(
dashouid=dashou_id,
qingqiuid=current_user.yonghuid,
cfliyou=chufa_liyou,
sqzhuangtai=0, # 待处理
jifen=0,
dingdan_id=dingdan_id,
)
# 🔴【新增】保存处罚图片到数据库
if chufa_tupian_urls and isinstance(chufa_tupian_urls, list):
for tupian_url in chufa_tupian_urls:
if tupian_url and tupian_url.strip():
Chufatupian.objects.create(
dingdan_id=dingdan_id,
yonghuid=current_user.yonghuid,
tupian=tupian_url
)
# 更新商家订单扩展表的处罚相关字段
shangjia_kuozhan.cfliyou = chufa_liyou
shangjia_kuozhan.sqzhuangtai = 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': 0,
'tupian_count': len(chufa_tupian_urls) if chufa_tupian_urls else 0 # 🔴 返回图片数量
}
}, status=status.HTTP_200_OK)
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f"商家处罚申请失败: {str(e)}", exc_info=True)
return Response({
'code': 10,
'msg': '服务器内部错误,申请失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
from django.db.models import Q, OuterRef, Subquery, F, Prefetch
from rest_framework_simplejwt.authentication import JWTAuthentication
from dengji.models import DingdanBiaoqian
from dengji.models import YonghuChenghao, Chenghao
class DashouDingdanHuoquView(APIView):
"""
获取可抢订单列表(支持标签筛选)
POST /dingdan/ddhq
需要JWT Token认证
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 获取请求参数
data = request.data
leixing_id = data.get('leixing_id')
page = int(data.get('page', 1))
page_size = int(data.get('page_size', 10))
biaoqian_id = data.get('biaoqian_id')
# 2. 构建基础查询条件
base_query = Q(zhuangtai__in=[1, 7])
if leixing_id:
base_query &= Q(leixing_id=leixing_id)
# 3. 标签筛选
if biaoqian_id and int(biaoqian_id) > 0:
base_query &= Q(dingdanbiaoqian__chenghao_id=int(biaoqian_id))
# 4. 构建基础查询集
base_qs = Dingdan.objects.filter(base_query).order_by('-create_time').distinct()
# 5. 获取总记录数
total = base_qs.count()
# 6. 计算分页偏移量
offset = (page - 1) * page_size
limit = page_size
# 7. 获取当前页数据,并通过 select_related 预取商家扩展表(反向关联)
page_qs = base_qs[offset:offset + limit].select_related('shangjia_kuozhan')
# 8. 获取当前页所有需要的字段
dingdan_values = page_qs.values(
'dingdan_id', 'zhuangtai', 'fadan_pingtai', 'dashou_fencheng',
'zhiding_id', 'leixing_id', 'tupian', 'yaoqiuleixing',
'huiyuan_id', 'yongjin', 'jieshao', 'beizhu', 'create_time',
'shangjia_kuozhan__sjnicheng', # 商家昵称
'shangjia_kuozhan__shangjia_id', # 商家ID
)
# 9. 转换为列表并格式化
dingdan_list = []
for order in dingdan_values:
dingdan_list.append({
'dingdan_id': order['dingdan_id'],
'zhuangtai': order['zhuangtai'],
'pingtai': order['fadan_pingtai'],
'fadan_pingtai': order['fadan_pingtai'],
'dashou_fencheng': float(order['dashou_fencheng']) if order['dashou_fencheng'] else 0.0,
'zhiding_id': order['zhiding_id'] or '',
'leixing_id': order['leixing_id'] or 0,
'tupian': order['tupian'] or '',
'yaoqiuleixing': order['yaoqiuleixing'] or 0,
'huiyuan_id': order['huiyuan_id'] or '',
'yajin': float(order['yongjin']) if order['yongjin'] else 0.0,
'yongjin': float(order['yongjin']) if order['yongjin'] else 0.0,
'jieshao': order['jieshao'] or '',
'beizhu': order['beizhu'] or '',
'creat_time': order['create_time'].strftime('%Y-%m-%d %H:%M:%S') if order['create_time'] else '',
'sjnicheng': order['shangjia_kuozhan__sjnicheng'] or '',
'sj_id': order['shangjia_kuozhan__shangjia_id'] or '',
# 其他字段后面补充
})
# 10. 批量获取指定打手的头像和昵称
zhiding_ids = {item['zhiding_id'] for item in dingdan_list if item.get('zhiding_id')}
zhiding_info = {}
if zhiding_ids:
dashou_query = UserMain.objects.filter(
yonghuid__in=zhiding_ids
).select_related('dashou_profile').annotate(
nicheng=F('dashou_profile__nicheng')
).values('yonghuid', 'avatar', 'nicheng')
zhiding_info = {
item['yonghuid']: {
'avatar': item['avatar'] or '',
'nicheng': item['nicheng'] or ''
}
for item in dashou_query
}
# 11. 批量获取三层标签(不变)
order_ids = [item['dingdan_id'] for item in dingdan_list]
# --- 订单需求标签 ---
xuqiu_tags_map = {}
if order_ids:
try:
dbq_qs = DingdanBiaoqian.objects.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)
# --- 用户标签(打手+商家) ---
yonghuid_set = set(zhiding_ids)
# 商家ID从订单列表中提取已含 sj_id
for item in dingdan_list:
if item['fadan_pingtai'] == 2 and item.get('sj_id'):
yonghuid_set.add(item['sj_id'])
user_id_map = {}
if yonghuid_set:
users = UserMain.objects.filter(yonghuid__in=yonghuid_set).values('id', 'yonghuid')
for u in users:
user_id_map[u['yonghuid']] = u['id']
user_main_ids = list(user_id_map.values())
user_tags_map = {}
if user_main_ids:
uc_qs = YonghuChenghao.objects.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['dingdan_id']
# 打手标签
zhid = item['zhiding_id']
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 == 'dashou':
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['fadan_pingtai'] == 2:
sj_yonghuid = item.get('sj_id')
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 item in dingdan_list:
oid = item['dingdan_id']
zid = item.get('zhiding_id')
info = zhiding_info.get(zid, {})
formatted_list.append({
**item,
'zhiding_uid': zid,
'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 {}
# apps/dingdan/views.py
from shangpin.models import ShangpinLeixing # 假设这是你的商品类型模型
from django.db.models import Q, OuterRef, Subquery, F, Prefetch
from rest_framework_simplejwt.authentication import JWTAuthentication
class ShangpinLeixingHuoquView(APIView):
"""
获取商品类型列表
POST /dingdan/ddlxhq
需要JWT Token认证
"""
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 查询商品类型表
# 假设你的商品类型模型字段为id, jieshao, tupian_url
# 2. 格式化返回数据
# 3. 返回响应
return Response({
'code': 200,
'msg': '获取成功',
'data': {
}
})
# apps/dingdan/views.py
from django.db import transaction
from django.utils import timezone
from django.db.models import F
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
import logging
from .models import Dingdan, Liaotian
from utils.goeasy_service import GoEasyService
logger = logging.getLogger(__name__)
# apps/dingdan/views.py
from django.db import transaction
from django.utils import timezone
from django.db.models import F
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework_simplejwt.authentication import JWTAuthentication
import logging
import requests
import json
from .models import Dingdan, DingdanPingtai, DingdanShangjia
logger = logging.getLogger(__name__)
from django.db import transaction
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from django.utils import timezone
from django.db.models import F
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
import json
import requests
from datetime import datetime
from peizhi.models import Club
logger = logging.getLogger(__name__)
from peizhi.models import ClubConfig
from utils.chat_utils import establish_order_chat_with_retry
class QiangdanView(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
data = request.data
dingdan_id = data.get('dingdan_id')
if not dingdan_id:
return Response({'code': 400, 'msg': '订单ID不能为空'}, status=400)
chat_success = False
try:
with transaction.atomic():
# 1. 验证打手身份和状态
dashou_profile = self._validate_dashou_status(request.user)
if not dashou_profile:
return Response({'code': 403, 'msg': '您不是打手或账号状态异常'}, status=403)
# ==================== 【新增】罚单检查 ====================
from utils.fadan_utils import check_fadan_qiangdan_eligible
fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(request.user.yonghuid,1)
if not fadan_ok:
return Response({'code': 400, 'msg': fadan_msg})
# =========================================================
# ========== 标签快速校验(提前拒绝,避免锁资源) ==========
from dengji.utils import check_dashou_biaoqian_required
biaoqian_ok, biaoqian_msg = check_dashou_biaoqian_required(dingdan_id, request.user.yonghuid)
if not biaoqian_ok:
return Response({'code': 400, 'msg': biaoqian_msg})
# 2. 锁定并验证订单(行锁)
order_result = self._validate_and_lock_order(dingdan_id, request.user.yonghuid)
if isinstance(order_result, str):
return Response({'code': 400, 'msg': order_result})
order = order_result
is_cross = order.is_cross == 1
dispatch_type = order.dispatch_type
if is_cross and dispatch_type == 2:
validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid)
if not validation_result['success']:
return Response({'code': 400, 'msg': validation_result['message']})
claim_allowed = self._request_partner_check_claim(order, request.user.yonghuid)
if claim_allowed is None:
return Response({'code': 500, 'msg': '跨平台通信失败,请重试'}, status=500)
elif claim_allowed is False:
if order.zhuangtai == 1:
order.zhuangtai = 21
order.save()
logger.info(f"订单 {dingdan_id} 被对方拒绝抢单状态改为21")
else:
order.zhuangtai = 20
order.save()
logger.info(f"订单 {dingdan_id} 已被其他平台抢走状态改为20")
return Response({'code': 400, 'msg': '订单已被其他平台打手抢走或不可抢'})
self._execute_qiangdan(order, dashou_profile, request.user.yonghuid)
try:
update_daily_dispatch_stat(
direction=2,
partner_club_id=order.partner_club_id or '000001',
claimed_amount=order.jine,
claimed_count=1
)
except Exception as e:
logger.error(f"更新抢单统计失败: {e}", exc_info=True)
elif is_cross and dispatch_type == 1:
validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid)
if not validation_result['success']:
return Response({'code': 400, 'msg': validation_result['message']})
self._execute_qiangdan(order, dashou_profile, request.user.yonghuid)
self._async_notify_partners_order_claimed(order)
else:
validation_result = self._validate_order_requirements(order, dashou_profile, request.user.yonghuid)
if not validation_result['success']:
return Response({'code': 400, 'msg': validation_result['message']})
self._execute_qiangdan(order, dashou_profile, request.user.yonghuid)
# 事务提交后再建群/建聊,避免长耗时 HTTP 拖住行锁,并支持重试
chat_success = establish_order_chat_with_retry(dingdan_id, max_rounds=5)
if not chat_success:
if not getattr(settings, 'CELERY_ONLY_ORDER_BROADCAST', True):
try:
from dingdan.tasks import retry_establish_order_chat_task
retry_establish_order_chat_task.apply_async(args=[dingdan_id], countdown=8)
retry_establish_order_chat_task.apply_async(args=[dingdan_id], countdown=30)
except Exception as chat_task_err:
logger.error(f"提交建聊补偿任务失败: {chat_task_err}")
try:
update_dashou_daily_by_action(
yonghuid=order.jiedan_dashou_id,
amount=order.dashou_fencheng,
action=1
)
logger.info(f"打手 {order.jiedan_dashou_id} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
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.dashou_profile
if (dashou_profile.zhuangtai != 1 or
dashou_profile.zhanghaozhuangtai != 1):
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(dingdan_id=dingdan_id)
if order.zhuangtai not in [1, 7]:
return "订单状态不可抢"
if order.zhuangtai == 7:
if not order.zhiding_id or order.zhiding_id != dashou_yonghuid:
return "此订单为指定单,您不是指定打手"
if order.jiedan_dashou_id:
return "订单已被其他打手抢走"
return order
except Dingdan.DoesNotExist:
return "订单不存在"
def _validate_order_requirements(self, order, dashou_profile, dashou_yonghuid):
if order.yaoqiuleixing == 1:
return self._validate_huiyuan_requirement(order, dashou_yonghuid)
elif order.yaoqiuleixing == 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.objects.get(
huiyuan_id=order.huiyuan_id,
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.yongjin 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.zhuangtai = 2
order.jiedan_dashou_id = dashou_yonghuid
order.user2_id = 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()
# ---------- 跨平台询问对方(照抄) ----------
def _request_partner_check_claim(self, order, dashou_id):
partner_club_id = order.partner_club_id or '000001'
try:
club = Club.objects.filter(partner_club_id=partner_club_id).first()
if not club or not club.partner_domain:
logger.error(f"未找到对方俱乐部配置: partner_club_id={partner_club_id}")
return None
my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first()
if not my_club:
logger.error(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}")
return None
payload = {
'token': settings.CROSS_PLATFORM_TOKEN,
'wofangdingdan_id': order.partner_order_id,
'partner_order_id': order.dingdan_id,
'our_club_id': my_club.club_id,
'partner_club_id': club.partner_club_id,
'dashou_id': dashou_id,
'sj_id': f"Sj{settings.AGENT_SHANGJIA_ID}"
}
url = club.partner_domain.rstrip('/') + '/dingdan/partner_check_claim'
resp = requests.post(url, json=payload, timeout=5)
if resp.status_code != 200:
logger.error(f"跨平台抢单询问HTTP失败: {resp.status_code}")
return None
data = resp.json()
if data.get('code') == 0:
return data.get('data', {}).get('can_claim') is True
else:
logger.error(f"跨平台抢单询问业务失败: {data}")
return False
except Exception as e:
logger.error(f"跨平台抢单询问异常: {e}", exc_info=True)
return None
# ---------- 异步通知其他俱乐部 ----------
def _async_notify_partners_order_claimed(self, order):
def notify_task():
clubs = Club.objects.filter(is_interop_enabled=True).select_related()
for club in clubs:
for attempt in range(1, 4):
success = self._send_claim_notification_to_partner(order, club)
if success:
break
if attempt < 3:
time.sleep(2 ** attempt)
else:
logger.error(f"通知俱乐部 {club.club_id} 订单已抢失败重试3次后仍失败")
thread = threading.Thread(target=notify_task)
thread.daemon = True
thread.start()
def _send_claim_notification_to_partner(self, order, club):
try:
my_club = Club.objects.filter(partner_club_id=club.partner_club_id).first()
if not my_club:
logger.warning(f"未找到我方俱乐部配置: partner_club_id={club.partner_club_id}")
return False
payload = {
'token': settings.CROSS_PLATFORM_TOKEN,
'our_club_id': my_club.club_id,
'partner_club_id': club.partner_club_id,
'partner_order_id': order.dingdan_id,
'claimed': True
}
url = club.partner_domain.rstrip('/') + '/dingdan/partner_notify_claim'
resp = requests.post(url, json=payload, timeout=3)
return resp.status_code == 200
except Exception as e:
logger.error(f"通知俱乐部 {club.club_id} 异常: {e}")
return False
# dingdan/views.py
import logging
import requests
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions, parsers
from .models import Dingdan, DingdanPingtai, DingdanShangjia
from yonghu.models import UserMain, UserDashou, UserBoss, UserShangjia
from peizhi.models import ClubConfig
from utils.chat_utils import (
_send_group_message, _subscribe_users_to_group,
resolve_group_id_for_order, prepare_order_group_chat,
build_pair_to_data_for_order,
)
logger = logging.getLogger('dingdan')
# ----------------- 基础配置获取 -----------------
def get_self_goeasy_appkey():
try:
cfg = ClubConfig.objects.filter(is_self=1).first()
return cfg.chat_api_key if cfg else None
except Exception:
return None
def get_self_goeasy_secret():
try:
cfg = ClubConfig.objects.filter(is_self=1).first()
return cfg.chat_api_id if cfg else None
except Exception:
return None
def get_self_storage_domain():
try:
cfg = ClubConfig.objects.filter(is_self=1).first()
return cfg.storage_bucket_domain if cfg else ''
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='', is_cross=0,
to_data_extra=None, message_type='text', image_url=None,
):
"""
通过 GoEasy REST API 发送群消息。
message_text: 普通文本内容
custom_payload: 自定义消息体(如订单卡片),此时 type 设为 'order'
image_url: 图片完整 URLmessage_type='image' 时使用
"""
url = 'https://rest-hangzhou.goeasy.io/v2/im/message'
sender_avatar = sender_avatar or ''
if to_data_extra:
to_data = dict(to_data_extra)
if order_id and 'orderId' not in to_data:
to_data['orderId'] = order_id
if 'isCross' not in to_data:
to_data['isCross'] = is_cross
else:
to_data = {
"name": group_name or group_id,
"avatar": sender_avatar,
"orderId": order_id,
"isCross": is_cross
}
if message_type == 'image' and image_url:
msg_type = 'image'
payload = {"url": image_url}
elif custom_payload:
msg_type = 'order'
payload = custom_payload
else:
msg_type = 'text'
payload = 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 = UserMain.objects.get(yonghuid=uid)
except UserMain.DoesNotExist:
return False, '用户不存在'
if identity_type == 'dashou':
try:
dashou = UserDashou.objects.get(user=user)
if dashou.zhanghaozhuangtai != 1 :
return False, '打手账号已被封禁或状态异常'
except UserDashou.DoesNotExist:
return False, '该用户不是打手'
if order.jiedan_dashou_id != uid:
return False, '你不是该订单的接单打手'
elif identity_type == 'shangjia':
try:
shop = UserShangjia.objects.get(user=user)
if shop.zhuangtai != 1:
return False, '商家账号已被禁用'
except UserShangjia.DoesNotExist:
return False, '该用户不是商家'
try:
if order.shangjia_kuozhan.shangjia_id != uid:
return False, '你不是该订单的发布商家'
except DingdanShangjia.DoesNotExist:
return False, '订单缺少商家信息'
elif identity_type == 'boss':
try:
UserBoss.objects.get(user=user) # 老板暂无封禁字段,仅需存在
except UserBoss.DoesNotExist:
return False, '该用户不是老板'
try:
if order.pingtai_kuozhan.laoban_id != uid:
return False, '你不是该订单的老板'
except DingdanPingtai.DoesNotExist:
return False, '订单缺少平台信息'
else:
return False, '不支持的身份类型'
return True, ''
# ----------------- 跨平台对方配置获取 -----------------
def get_partner_club_config(partner_club_id):
try:
return ClubConfig.objects.get(club_id=partner_club_id, is_self=0)
except ClubConfig.DoesNotExist:
return None
# ----------------- 主视图 -----------------
class DingdanLiaotianZhunbeiView(APIView):
"""订单详情进入聊天前:确保配对群订阅并返回 groupId"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
dingdan_id = request.data.get('dingdan_id') or request.data.get('orderId')
identity_type = request.data.get('identityType')
push_order_card = request.data.get('push_order_card', True)
if isinstance(push_order_card, str):
push_order_card = push_order_card.lower() in ('1', 'true', 'yes')
if not dingdan_id:
return Response({'code': 400, 'msg': '缺少 dingdan_id'})
if not identity_type:
return Response({'code': 400, 'msg': '缺少 identityType'})
if identity_type not in ('dashou', 'shangjia', 'boss'):
return Response({'code': 400, 'msg': 'identityType 无效'})
uid = request.user.yonghuid
try:
order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
valid, err_msg = check_user_permission(identity_type, uid, order)
if not valid:
return Response({'code': 403, 'msg': err_msg})
prefix_map = {'dashou': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'}
caller_goeasy_id = prefix_map[identity_type] + uid
result = prepare_order_group_chat(dingdan_id, caller_goeasy_id, push_order_card=push_order_card)
if not result:
return Response({'code': 500, 'msg': '准备群聊失败,请稍后重试'})
return Response({'code': 0, 'msg': 'ok', 'data': result})
class DaiLiQunLiaoXiaoXiView(APIView):
"""
代理发送群聊消息
支持:
- 非跨平台订单:直接用我方 GoEasy Key 以真实身份发送
- 跨平台订单(双方任意一方派单):除了我方群外,还需用对方 GoEasy Key 以代理身份向对方群发送
跨平台判断条件:订单同时存在 partner_order_id 和 partner_club_id 即视为跨平台
"""
permission_classes = [permissions.IsAuthenticated]
parser_classes = [parsers.MultiPartParser, parsers.FormParser, parsers.JSONParser]
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)
image_file = request.FILES.get('file')
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 ('dashou', 'shangjia', 'boss'):
return Response({'code': 400, 'msg': 'identityType 无效'})
# ===== 2. 构造 senderId =====
uid = request.user.yonghuid
prefix_map = {'dashou': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'}
sender_id = prefix_map[identity_type] + uid
logger.info(f"[群聊代理] 登录用户 yonghuid={uid}, 构造 senderId={sender_id}")
# ===== 3. 查询订单 =====
try:
order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=order_id)
except Dingdan.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 = resolve_group_id_for_order(order)
group_name = (order.jieshao[:20] + '') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}"
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': '订阅群组失败'})
# ===== 8. 判断跨平台只看对方订单ID和俱乐部ID =====
partner_order_id = order.partner_order_id
partner_club_id = order.partner_club_id
is_cross = 1 if (partner_order_id and partner_club_id) else 0
logger.info(f"[群聊代理] 订单 {order_id} partner_order_id={partner_order_id}, partner_club_id={partner_club_id}, 判定跨平台={is_cross}")
custom_payload = order_payload if message_type == 'order' else None
text_content = message_text
image_url = None
if message_type == 'image':
if not image_file:
return Response({'code': 400, 'msg': '缺少图片文件'})
try:
from utils.oss_utils import upload_to_oss, validate_image
import uuid as _uuid
validate_image(image_file)
oss_path = f"chat/images/{order_id}/{_uuid.uuid4().hex}.jpg"
image_url = upload_to_oss(image_file, oss_path)
if not image_url:
return Response({'code': 500, 'msg': '图片上传失败'})
except Exception as e:
logger.error(f"[群聊代理] 图片上传失败: {e}", exc_info=True)
return Response({'code': 500, 'msg': '图片上传失败'})
pair_to_data = build_pair_to_data_for_order(order, viewer_goeasy_id=sender_id)
# ---------- 先向我方群发送消息(永远执行) ----------
if message_type == 'image' and image_url:
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,
group_name=group_name,
order_id=order.dingdan_id,
is_cross=is_cross,
to_data_extra=pair_to_data,
message_type='image',
image_url=image_url,
)
else:
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.dingdan_id,
is_cross=is_cross,
to_data_extra=pair_to_data,
)
if not local_ok:
return Response({'code': 500, 'msg': '消息发送失败'})
# ---------- 跨平台转发如果有对方订单ID和俱乐部ID ----------
if is_cross:
logger.info("[群聊代理] 满足跨平台条件,准备双平台发送")
partner_cfg = get_partner_club_config(partner_club_id)
if not partner_cfg or not partner_cfg.chat_api_key:
logger.warning("[群聊代理] 对方俱乐部配置不可用,仅发送我方群")
return Response({'code': 200, 'msg': '发送成功对方GoEasy未配置'})
partner_appkey = partner_cfg.chat_api_key
partner_secret = partner_cfg.chat_api_id or ''
partner_group_id = f"group_{partner_order_id}"
# 构造代理身份
agent_sender_id = f"{order.dingdan_id}_{sender_id}"
if identity_type == 'dashou':
agent_sender_name = f"[我方]{sender_name}"
elif identity_type == 'shangjia':
agent_sender_name = f"[我方商家]{sender_name}"
else:
agent_sender_name = f"[我方老板]{sender_name}"
agent_avatar = full_avatar('')
logger.info(f"[群聊代理] 对方群组={partner_group_id}, 代理身份={agent_sender_id}/{agent_sender_name}")
# 向对方群发送代理消息
partner_ok = send_group_message(
appkey=partner_appkey,
secret=partner_secret,
group_id=partner_group_id,
sender_id=agent_sender_id,
sender_name=agent_sender_name,
sender_avatar=agent_avatar,
message_text=text_content,
custom_payload=custom_payload,
group_name='跨平台订单交流',
order_id=partner_order_id,
is_cross=1
)
logger.info(f"[群聊代理] 对方群发送结果: {partner_ok}")
if partner_ok:
return Response({'code': 200, 'msg': '发送成功(双平台)'})
else:
return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'})
else:
logger.info("[群聊代理] 非跨平台,仅发送我方群")
return Response({'code': 200, 'msg': '发送成功'})
from peizhi.models import ClubConfig, ShangjiaLianjie
logger = logging.getLogger('dingdan')
# ---------- 跨平台对方配置获取 ----------
def get_partner_club_config(partner_club_id):
try:
return ClubConfig.objects.get(club_id=partner_club_id, is_self=0)
except ClubConfig.DoesNotExist:
return None
class ShangjiaLianjieLiuYanZhuanFaView(APIView):
"""
商家链接留言转发接口
路径POST /dingdan/sjljlbxxzf
固定以 B{商家ID} 身份发送群消息。通过 partner_order_id/partner_club_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.objects.get(lianjie_token=token)
except ShangjiaLianjie.DoesNotExist:
return Response({'code': 404, 'msg': '链接不存在或已失效'})
if not lianjie.dingdan_id:
return Response({'code': 400, 'msg': '链接缺少订单ID'})
try:
order = Dingdan.objects.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(dingdan_id=lianjie.dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 500, 'msg': '订单数据异常'})
shangjia_uid = lianjie.yonghu_id
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.dingdan_id}"
group_name = (order.jieshao[:20] + '') if order.jieshao and len(order.jieshao) > 20 else f"订单{order.dingdan_id}"
local_cross_flag = 1 if (order.partner_order_id and order.partner_club_id) else 0
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.dingdan_id,
is_cross=local_cross_flag
)
if not local_ok:
return Response({'code': 500, 'msg': '消息发送失败'})
# 跨平台转发
partner_order_id = order.partner_order_id
partner_club_id = order.partner_club_id
if not partner_order_id or not partner_club_id:
return Response({'code': 200, 'msg': '发送成功'})
partner_cfg = get_partner_club_config(partner_club_id)
if not partner_cfg or not partner_cfg.chat_api_key:
return Response({'code': 200, 'msg': '发送成功(对方未配置)'})
agent_sender_id = f"{order.dingdan_id}_{sender_id}"
agent_sender_name = f"[我方老板]{shangjia_uid}"
partner_group_id = f"group_{partner_order_id}"
partner_ok = _send_group_message(
appkey=partner_cfg.chat_api_key,
secret=partner_cfg.chat_api_id or '',
group_id=partner_group_id,
sender_id=agent_sender_id,
sender_name=agent_sender_name,
sender_avatar='',
message_text=text,
custom_payload=None,
group_name='跨平台订单交流',
order_id=partner_order_id,
is_cross=1
)
if partner_ok:
return Response({'code': 200, 'msg': '发送成功(双平台)'})
else:
return Response({'code': 200, 'msg': '我方发送成功,对方发送失败'})
class DashouDingdanHuoquView1(APIView):
"""打手订单获取接口"""
permission_classes = [IsAuthenticated] # JWT认证
def post(self, request):
# 1. 获取当前登录用户的yonghuid
try:
current_user = request.user
yonghuid = current_user.yonghuid
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))))
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. 构建查询条件 - 直接查询订单主表
# 条件1: 当前打手是接单打手
# 条件2: 订单状态在指定列表中
# 注意打手看不到状态1和7的订单已下单、指定中
# 但这里由前端控制传递的zhuangtai_list决定
dashou_query = Q(jiedan_dashou_id=yonghuid)
zhuangtai_query = Q(zhuangtai__in=zhuangtai_list)
# 5. 执行查询 - 只查询需要的字段,按创建时间倒序
dingdan_query = Dingdan.objects.filter(
dashou_query & zhuangtai_query
).order_by('-create_time')
# 只选择需要的字段,提高性能
dingdan_query = dingdan_query.only(
'dingdan_id',
'zhuangtai',
'dashou_fencheng', # ✅ 关键打手分成不是jine
'jieshao',
'tupian',
'nicheng',
'create_time'
)
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.dingdan_id, # 订单ID
'zhuangtai': order.zhuangtai, # 订单状态码
# ✅ 关键:这里返回的是打手分成,不是订单金额
'jine': float(order.dashou_fencheng) if order.dashou_fencheng else 0.0,
'jieshao': order.jieshao or '', # 订单介绍
'tupian': order.tupian or '', # 商品图片URL
'nicheng': order.nicheng or '', # 游戏昵称
'create_time': order.create_time.strftime('%Y-%m-%d %H:%M:%S') if order.create_time else ''
}
order_list.append(order_data)
# 8. 判断是否还有更多数据
# 条件1: 当前页获取了满page_size条数据
# 条件2: 还有更多数据没取完
has_more = current_page_count == page_size and end_index < total_count
# 如果总数为0直接没有更多
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:
# 记录异常日志
import logging
logger = logging.getLogger(__name__)
logger.error(f"打手订单获取失败: {str(e)}", exc_info=True)
return Response({
'code': 4,
'msg': '服务器内部错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# views.py - 打手COS凭证接口完整替换
import json
import time
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sts.v20180813 import sts_client, models
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
import logging
logger = logging.getLogger(__name__)
class DashouCOSZhengshuView(APIView):
"""打手获取COS上传凭证接口调试版本"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 接收参数(修复 int 无 strip 问题)
dingdan_id = str(request.data.get('dingdan_id', '')).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:
from .models import Chufajilu
chufa_record = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
dashouid=request.user.yonghuid,
sqzhuangtai__in=[0, 3]
).first()
if not chufa_record:
return Response(
{'code': 403, 'msg': '处罚记录无权限或状态不对',
'error': f'No punishment record found for user {request.user.yonghuid} 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:
from .models import Dingdan
dingdan = Dingdan.objects.get(
dingdan_id=dingdan_id,
jiedan_dashou_id=request.user.yonghuid,
zhuangtai__in=[2, 8]
)
except Dingdan.DoesNotExist:
return Response(
{'code': 403, 'msg': '此功能暂未开放', 'error': f'Order {dingdan_id} not found or not assigned to user {request.user.yonghuid}'},
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('dingdan_id', '').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.shop_profile
# 验证商家状态是否为1正常
if shangjia_profile.zhuangtai != 1:
return Response({'code': 3, 'msg': '商家账号状态异常,无法操作', 'data': None},
status=status.HTTP_403_FORBIDDEN)
# 🔴【重要】获取商家ID从商家扩展表中获取
shangjia_id = request.user.yonghuid # 假设商家扩展表中有yonghuid字段
except AttributeError:
# 没有shop_profile属性说明不是商家
return Response({'code': 3, 'msg': '非商家用户无权限', 'data': None},
status=status.HTTP_403_FORBIDDEN)
# 3. 🔴【修改】验证订单权限 - 通过商家订单扩展表验证
try:
from .models import Dingdan, DingdanShangjia
# 获取订单
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
# 🔴【重要】通过反向查询商家扩展表验证订单是否属于当前商家
try:
shangjia_kuozhan = dingdan.shangjia_kuozhan # 使用反向关系名
# 验证商家ID是否匹配
if str(shangjia_kuozhan.shangjia_id) != str(shangjia_id):
return Response({'code': 2, 'msg': '订单不属于当前商家', 'data': None},
status=status.HTTP_403_FORBIDDEN)
# 验证订单状态是否允许申请处罚(根据前端逻辑)
allowed_zhuangtai = [2, 4, 5, 6, 8]
if dingdan.zhuangtai not in allowed_zhuangtai:
return Response({'code': 4, 'msg': '当前订单状态不允许申请处罚', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
# 🔴【新增】检查是否已存在处罚申请通过Chufajilu模型
# 注意这里假设Chufajilu模型存在并且有shangjia_id字段
try:
from .models import Chufajilu
existing_chufa = Chufajilu.objects.filter(
dingdan_id=dingdan_id,
qingqiuid=shangjia_id,
sqzhuangtai__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 DingdanShangjia.DoesNotExist:
# 没有商家扩展记录,说明这不是商家订单
return Response({'code': 2, 'msg': '非商家订单无法操作', 'data': None},
status=status.HTTP_403_FORBIDDEN)
except Dingdan.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)
'''# views.py - 打手COS凭证接口完整替换
import json
import time
from tencentcloud.common import credential
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sts.v20180813 import sts_client, models
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
import logging
logger = logging.getLogger(__name__)
class DashouCOSZhengshuView(APIView):
"""打手获取COS上传凭证接口使用腾讯云STS临时密钥"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
# 1. 接收参数
dingdan_id = request.data.get('dingdan_id', '').strip()
if not dingdan_id:
return Response({'code': 1, 'msg': '订单ID不能为空', 'data': None}, status=status.HTTP_400_BAD_REQUEST)
# 2. 业务验证逻辑(保持你的原有逻辑)
try:
from .models import Dingdan
dingdan = Dingdan.objects.get(
dingdan_id=dingdan_id,
jiedan_dashou_id=request.user.yonghuid,
zhuangtai=2
)
except Dingdan.DoesNotExist:
return Response({'code': 5, 'msg': '订单无权限或状态不对', 'data': None},
status=status.HTTP_403_FORBIDDEN)
except Exception as e:
return Response({'code': 500, 'msg': '服务器验证异常', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 3. 获取腾讯云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)
# 构建策略(允许上传到订单目录)
policy = {
"version": "2.0",
"statement": [
{
"effect": "allow",
"action": [
"cos:PutObject",
"cos:PostObject",
"cos:InitiateMultipartUpload",
"cos:UploadPart",
"cos:CompleteMultipartUpload"
],
"resource": [
f"qcs::cos:{settings.COS_REGION}:uid/{settings.COS_APPID}:{settings.COS_BUCKET}/order/{dingdan_id}/*"
]
}
]
}
# 请求STS临时凭证
req = models.GetFederationTokenRequest()
req.Name = f"dashou_{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)
# 4. 构建响应数据严格遵循小程序COS SDK要求
current_time = int(time.time())
response_data = {
'credentials': {
'tmpSecretId': credentials.TmpSecretId, # 临时SecretId
'tmpSecretKey': credentials.TmpSecretKey, # 临时SecretKey
'sessionToken': credentials.Token, # 必须的Token
},
'startTime': current_time,
'expiredTime': current_time + 7200,
'bucket': settings.COS_BUCKET,
'region': settings.COS_REGION,
'upload_dir': f'order/{dingdan_id}/'
}
# 5. 返回成功
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('dingdan_id', '')
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.yonghuid
try:
dashou_profile = current_user.dashou_profile
except UserDashou.DoesNotExist:
return Response({
'code': 6,
'msg': '用户不是打手身份',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 4. 验证订单状态和权限
try:
dingdan = Dingdan.objects.get(
dingdan_id=dingdan_id,
jiedan_dashou_id=yonghuid,
zhuangtai=2 # 必须是进行中的订单
)
except Dingdan.DoesNotExist:
return Response({
'code': 7,
'msg': '订单不存在或没有提交权限',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 5. 存储图片到Dashoutupian表
for tupian_url in jiaofu_tupian_urls:
# 创建打手提交图片记录
Dashoutupian.objects.create(
dingdan_id=dingdan_id,
dashou=yonghuid,
tupian=tupian_url,
create_time=timezone.now()
)
# 6. 更新订单主表
dingdan.dashou_liuyan = liuyan
dingdan.zhuangtai = 8 # 状态改为结算中
dingdan.update_time = timezone.now()
dingdan.save()
from utils.chat_utils import schedule_order_status_chat_push
schedule_order_status_chat_push(dingdan_id)
# 7. 更新打手扩展表状态
dashou_profile.zhuangtai = 1 # 改为数字1根据你之前说的
dashou_profile.save()
# 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:
import logging
logger = logging.getLogger(__name__)
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('dingdan_id', '')
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.yonghuid
try:
dashou_profile = current_user.dashou_profile
except UserDashou.DoesNotExist:
return Response({
'code': 2,
'msg': '用户不是打手身份',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 3. 查询订单主表
try:
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({
'code': 3,
'msg': '订单不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
# 4. 验证订单是否属于当前打手
if dingdan.jiedan_dashou_id != yonghuid:
return Response({
'code': 4,
'msg': '没有权限查看此订单',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 5. 构建基础订单数据
dingdan_data = {
'dingdan_id': dingdan.dingdan_id,
'zhuangtai': dingdan.zhuangtai,
'jine': float(dingdan.dashou_fencheng) if dingdan.dashou_fencheng else 0.0,
'jieshao': dingdan.jieshao or '',
'tupian': dingdan.tupian or '',
'nicheng': dingdan.nicheng or '',
'beizhu': dingdan.beizhu or '',
'create_time': dingdan.create_time.strftime('%Y-%m-%d %H:%M:%S') if dingdan.create_time else '',
'dashou_liuyan': dingdan.dashou_liuyan or '',
'fuwudashou_id': dingdan.jiedan_dashou_id or '',
'fadanpingtai': dingdan.fadan_pingtai or '',
'tuikuan_liyou': dingdan.tkly or '',
'is_cross': dingdan.is_cross or 0,
}
# 6. 查询打手提交的图片
dashou_tupian_list = Dashoutupian.objects.filter(
dingdan_id=dingdan_id,
dashou=yonghuid
).order_by('create_time')
jiaofu_tupian_urls = []
for tupian_record in dashou_tupian_list:
if tupian_record.tupian:
jiaofu_tupian_urls.append(tupian_record.tupian)
dingdan_data['dashou_jiaofu'] = jiaofu_tupian_urls
# 7. 查询商家扩展表信息
shangjia_info = {}
try:
shangjia_kuozhan = dingdan.shangjia_kuozhan
shangjia_info = {
'shangjia_id': shangjia_kuozhan.shangjia_id or '',
'shangjia_nicheng': shangjia_kuozhan.sjnicheng or '',
'shangjia_liuyan': shangjia_kuozhan.shangjia_pingjia or '',
#'tuikuan_liyou': '',
'chufa_liyou': shangjia_kuozhan.cfliyou or '',
'chufa_zhuangtai': shangjia_kuozhan.sqzhuangtai or 0,
#'chufa_jieguo': shangjia_kuozhan.chufa_jieguo or '',
'bohui_liyou': shangjia_kuozhan.bhliyou or ''
}
except DingdanShangjia.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.laoban_id or '',
'laoban_pingjia': pingtai_kuozhan.laoban_pingjia or ''
}
except DingdanPingtai.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:
import logging
logger = logging.getLogger(__name__)
logger.error(f"获取打手订单详情失败: {str(e)}", exc_info=True)
return Response({
'code': 5,
'msg': '服务器内部错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# dingdan/views.py
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
import logging
logger = logging.getLogger(__name__)
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('dingdan_id', '').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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询订单主表
try:
from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia, Dashoutupian
# 查询订单主表 - 使用select_related预加载关联表
dingdan_obj = Dingdan.objects.select_related(
'pingtai_kuozhan', 'shangjia_kuozhan'
).get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 7. 构建返回数据 - 主表字段
response_data = {
# 基础信息字段
'dingdan_id': dingdan_obj.dingdan_id or '',
'jieshao': dingdan_obj.jieshao or '',
'zhuangtai': dingdan_obj.zhuangtai or 0,
'leixing': dingdan_obj.leixing_id or 0, # 前端需要 leixing对应 leixing_id
'fadanpingtai': dingdan_obj.fadan_pingtai or 0, # 前端需要 fadanpingtai对应 fadan_pingtai
'jiage': float(dingdan_obj.jine) if dingdan_obj.jine else 0.00, # 前端需要 jiage对应 jine
'tupian_url': dingdan_obj.tupian or '', # 前端需要 tupian_url对应 tupian
'beizhu': dingdan_obj.beizhu or '',
# 服务信息字段
'jiedan_dashou_id': dingdan_obj.jiedan_dashou_id or '',
'dashou_fencheng': float(dingdan_obj.dashou_fencheng) if dingdan_obj.dashou_fencheng else 0.00,
'zhiding_id': dingdan_obj.zhiding_id or '',
'dashou_liuyan': dingdan_obj.dashou_liuyan or '',
# 时间字段
'chuangjianshijian': dingdan_obj.create_time.strftime(
'%Y-%m-%d %H:%M:%S') if dingdan_obj.create_time else '',
'genggaishijian': dingdan_obj.update_time.strftime(
'%Y-%m-%d %H:%M:%S') if dingdan_obj.update_time else '',
}
# 8. 根据发单平台查询扩展表数据
fadan_pingtai = dingdan_obj.fadan_pingtai
if fadan_pingtai == 1:
# 平台发单 - 查询平台订单扩展表
try:
# 使用反向关联 pingtai_kuozhan
pingtai_kuozhan = dingdan_obj.pingtai_kuozhan
response_data.update({
'laoban_id': pingtai_kuozhan.laoban_id or '',
'nicheng': dingdan_obj.nicheng or '', # 这个字段在订单主表
'laoban_pingjia': pingtai_kuozhan.laoban_pingjia or '',
})
except DingdanPingtai.DoesNotExist:
# 扩展表不存在,返回空值
response_data.update({
'laoban_id': '',
'nicheng': dingdan_obj.nicheng or '',
'laoban_pingjia': '',
})
elif fadan_pingtai == 2:
# 商家发单 - 查询商家订单扩展表
try:
# 使用反向关联 shangjia_kuozhan
shangjia_kuozhan = dingdan_obj.shangjia_kuozhan
response_data.update({
'shangjia_id': shangjia_kuozhan.shangjia_id or '',
'sjnicheng': shangjia_kuozhan.sjnicheng or '',
'shangjia_pingjia': shangjia_kuozhan.shangjia_pingjia or '',
'cfliyou': shangjia_kuozhan.cfliyou or '', # 商家处罚理由
'bhliyou': shangjia_kuozhan.bhliyou or '', # 驳回理由
})
except DingdanShangjia.DoesNotExist:
# 扩展表不存在,返回空值
response_data.update({
'shangjia_id': '',
'sjnicheng': '',
'shangjia_pingjia': '',
'cfliyou': '',
'bhliyou': '',
})
# 9. 查询打手提交的图片
try:
# 查询该订单的所有打手提交图片
dashou_tupian_list = Dashoutupian.objects.filter(
dingdan_id=dingdan_id
).values_list('tupian', 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:
# 导入退款记录模型
from dingdan.models import Tuikuanjilu
# 查询该订单的退款记录(一个订单可能有多次退款申请,我们取最新的)
tuikuan_record = Tuikuanjilu.objects.filter(
dingdan_id=dingdan_id
).order_by('-create_time').first() # 按创建时间倒序,取最新的
if tuikuan_record and tuikuan_record.liyou:
# 如果有退款记录且理由不为空,添加到返回数据
response_data['tuikuan_liyou'] = tuikuan_record.liyou
else:
# 没有找到退款记录或理由为空
response_data['tuikuan_liyou'] = ''
except Tuikuanjilu.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. 验证新打手状态正常zhuangtai=1且zhanghaozhuangtai=1
5. 根据订单的抢单要求类型进行验证:
- 会员抢单(1):验证打手有对应会员且未过期
- 押金抢单(2):验证打手押金≥订单押金
6. 更新新打手状态和统计数据
7. 更新原打手状态和统计数据
8. 更新订单信息
"""
try:
# 1. 获取前端参数
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = request.data.get('dingdan_id', '').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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询订单主表
try:
from dingdan.models import Dingdan
dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 7. 验证订单状态是否为进行中(2)或结算中(8)
if dingdan_obj.zhuangtai not in [2, 8]:
return Response(
{'code': 400, 'message': '订单状态不允许更换打手', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 8. 验证新打手ID是否存在并且是打手身份
try:
new_dashou_user = UserMain.objects.get(
yonghuid=new_dashou_id,
#user_type='dashou'
)
except UserMain.DoesNotExist:
return Response(
{'code': 404, 'message': '打手不存在或不是打手身份', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 9. 获取新打手扩展表
try:
new_dashou_profile = new_dashou_user.dashou_profile
except AttributeError:
return Response(
{'code': 404, 'message': '打手信息不完整', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 10. 验证新打手状态zhuangtai=1且zhanghaozhuangtai=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.yaoqiuleixing
if yaoqiuleixing == 1: # 会员抢单
# 获取订单的会员ID
huiyuan_id = dingdan_obj.huiyuan_id
if not huiyuan_id:
return Response(
{'code': 400, 'message': '订单会员ID为空', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 查询会员购买记录
try:
from shangpin.models import Huiyuangoumai
huiyuan_goumai = Huiyuangoumai.objects.get(
huiyuan_id=huiyuan_id,
yonghu_id=new_dashou_id # 注意yonghu_id存储的是用户IDyonghuid
)
except Huiyuangoumai.DoesNotExist:
return Response(
{'code': 400, 'message': '打手未购买该订单要求的会员', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 检查会员是否过期
# ✅ 可以调用因为jiance_shifou_daoqi()是Huiyuangoumai模型的实例方法
is_expired = huiyuan_goumai.jiance_shifou_daoqi()
if is_expired or huiyuan_goumai.huiyuan_zhuangtai != 1:
return Response(
{'code': 400, 'message': '打手的会员已过期', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
elif yaoqiuleixing == 2: # 押金抢单
# 获取订单的押金yongjin字段
order_yajin = dingdan_obj.yongjin
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.jiedan_dashou_id
if old_dashou_id:
try:
# 查询原打手主表
old_dashou_user = UserMain.objects.get(
yonghuid=old_dashou_id,
#user_type='dashou'
)
# 获取原打手扩展表
old_dashou_profile = old_dashou_user.dashou_profile
# 更新原打手状态
old_dashou_profile.zhuangtai = 1 # 空闲
old_dashou_profile.tuikuanliang += 1 # 退款订单总量+1
old_dashou_profile.save()
logger.info(f"成功更新原打手{old_dashou_id}状态")
except UserMain.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.jiedan_dashou_id = new_dashou_id
dingdan_obj.zhuangtai = 2 # 进行中状态
dingdan_obj.save()
try:
establish_order_chat_with_retry(dingdan_obj.dingdan_id, max_rounds=3)
except Exception as chat_err:
logger.error(f"更换打手后建聊失败: {chat_err}", exc_info=True)
# 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
)
import logging
# 获取日志器
logger = logging.getLogger(__name__)
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('dingdan_id', '').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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询订单主表
try:
from dingdan.models import Dingdan, DingdanPingtai, DingdanShangjia
dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 7. 验证订单状态是否为2、8或4
if dingdan_obj.zhuangtai not in [2, 8, 4]:
return Response(
{'code': 400, 'message': '订单状态不允许强制结单', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 8. 获取订单的重要字段
jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID
dashou_fencheng = dingdan_obj.dashou_fencheng # 打手分成金额
fadan_pingtai = dingdan_obj.fadan_pingtai # 发单平台
# 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.zhuangtai = 3
dingdan_obj.save()
from utils.chat_utils import schedule_order_status_chat_push
schedule_order_status_chat_push(dingdan_id)
# 10.2 处理打手结算
if jiedan_dashou_id:
try:
# 查询打手用户主表
dashou_user = UserMain.objects.get(
yonghuid=jiedan_dashou_id,
#user_type='dashou'
)
# 获取打手扩展表
dashou_profile = dashou_user.dashou_profile
# 更新打手扩展表数据
# ✅ 确认字段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.zhuangtai = 1
dashou_profile.save()
logger.info(f"强制结单成功,打手{jiedan_dashou_id}结算分成:{dashou_fencheng}")
except UserMain.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.shangjia_id
if shangjia_id:
# 查询商家用户主表
shangjia_user = UserMain.objects.get(
yonghuid=shangjia_id,
#user_type='shop'
)
# 获取商家扩展表
shangjia_profile = shangjia_user.shop_profile
# ✅ 确认字段chengjiao 商家成交订单总量
shangjia_profile.chengjiao += 1
shangjia_profile.save()
logger.info(f"强制结单成功,商家{shangjia_id}成交订单+1")
except DingdanShangjia.DoesNotExist:
# 商家订单扩展表不存在,跳过商家更新
logger.warning(f"强制结单:订单{dingdan_id}商家扩展表不存在,跳过商家更新")
except UserMain.DoesNotExist:
# 商家用户不存在,跳过商家更新
logger.warning(f"强制结单:商家{shangjia_id}不存在,跳过商家更新")
except AttributeError:
# 商家扩展表不存在,跳过商家更新
logger.warning(f"强制结单:商家{shangjia_id}扩展信息不存在,跳过商家更新")
if fadan_pingtai == 2 and jiedan_dashou_id:
from dingdan.utils import settle_shangjia_order_guanshi_fenhong
settle_order = Dingdan.objects.get(dingdan_id=dingdan_id)
settle_shangjia_order_guanshi_fenhong(settle_order, jiedan_dashou_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('dingdan_id', '').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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
except AttributeError:
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 6. 查询订单主表
try:
from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu
dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 7. 获取订单的重要字段
jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID
dashou_fencheng = dingdan_obj.dashou_fencheng # 打手分成金额
fadan_pingtai = dingdan_obj.fadan_pingtai # 发单平台
# 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.zhuangtai = 6
dingdan_obj.save()
# 9.2 处理打手结算
if jiedan_dashou_id:
try:
# 查询打手用户主表
dashou_user = UserMain.objects.get(
yonghuid=jiedan_dashou_id,
#user_type='dashou'
)
# 获取打手扩展表
dashou_profile = dashou_user.dashou_profile
# 更新打手扩展表数据
# ✅ 确认字段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 UserMain.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.shangjia_id
if shangjia_id:
# 查询商家用户主表
shangjia_user = UserMain.objects.get(
yonghuid=shangjia_id,
#user_type='shop'
)
# 获取商家扩展表
shangjia_profile = shangjia_user.shop_profile
# ✅ 确认字段chengjiao 商家成交订单总量
shangjia_profile.chengjiao += 1
shangjia_profile.save()
logger.info(f"拒绝退款成功,商家{shangjia_id}成交订单+1")
except DingdanShangjia.DoesNotExist:
# 商家订单扩展表不存在,跳过商家更新
logger.warning(f"拒绝退款:订单{dingdan_id}商家扩展表不存在,跳过商家更新")
except UserMain.DoesNotExist:
# 商家用户不存在,跳过商家更新
logger.warning(f"拒绝退款:商家{shangjia_id}不存在,跳过商家更新")
except AttributeError:
# 商家扩展表不存在,跳过商家更新
logger.warning(f"拒绝退款:商家{shangjia_id}扩展信息不存在,跳过商家更新")
# 9.4 更新退款记录表
try:
# 查询退款记录表
tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id)
# ✅ 确认字段sqzhuangtai 申请状态改为2驳回
tuikuan_jilu.sqzhuangtai = 2
# ✅ 确认字段bhliyou 驳回理由
tuikuan_jilu.bhliyou = jujue_liyou
# ✅ 确认字段chuliid 处理人ID使用管理员账号即手机号
tuikuan_jilu.chuliid = qianzhanghao
tuikuan_jilu.save()
logger.info(f"拒绝退款成功,更新退款记录表,订单:{dingdan_id}")
except Tuikuanjilu.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('dingdan_id', '').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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
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:
from dingdan.models import Dingdan, DingdanShangjia, Tuikuanjilu
dingdan_obj = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 8. 验证订单状态为4退款审核中
# 8. 验证订单状态是否为允许退款的状态
allowed_statuses = [4, 8, 1, 2, 7] # 允许退款的状态:退款审核中、已下单、结算中、进行中、指定中
if dingdan_obj.zhuangtai not in allowed_statuses:
return Response(
{
'code': 400,
'message': f'订单当前状态不允许退款。允许退款的状态为:退款审核中(4)、已下单(8)、结算中(1)、进行中(2)、指定中(7)',
'data': None
},
status=status.HTTP_400_BAD_REQUEST
)
'''if dingdan_obj.zhuangtai != 4:
return Response(
{'code': 400, 'message': '订单状态不是退款审核中', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)'''
# 9. 验证订单为商家发单
if dingdan_obj.fadan_pingtai != 2:
return Response(
{'code': 400, 'message': '此接口只处理商家发单的退款', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 10. 获取订单的重要字段
jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID
jine = dingdan_obj.jine # 订单金额(不是打手分成)
# 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.shangjia_id
except DingdanShangjia.DoesNotExist:
return Response(
{'code': 400, 'message': '订单商家信息不存在', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 13. 使用事务保证数据一致性
with transaction.atomic():
# 13.1 更新订单状态为5已退款
dingdan_obj.zhuangtai = 5
dingdan_obj.save()
# 13.2 更新打手扩展表(退款订单总量+1
if jiedan_dashou_id:
try:
# 查询打手用户主表
dashou_user = UserMain.objects.get(
yonghuid=jiedan_dashou_id,
#user_type='dashou'
)
# 获取打手扩展表
dashou_profile = dashou_user.dashou_profile
dashou_profile.zhuangtai = 1
# ✅ 确认字段tuikuanliang 退款订单总量
dashou_profile.tuikuanliang += 1
dashou_profile.save()
logger.info(f"同意退款成功,打手{jiedan_dashou_id}退款订单+1")
except UserMain.DoesNotExist:
# 打手用户不存在,记录日志但不影响操作
logger.warning(f"同意退款:打手{jiedan_dashou_id}不存在,跳过打手更新")
except AttributeError:
# 打手扩展表不存在,记录日志但不影响操作
logger.warning(f"同意退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手更新")
# 13.3 更新商家扩展表
if shangjia_id:
try:
# 查询商家用户主表
shangjia_user = UserMain.objects.get(
yonghuid=shangjia_id,
#user_type='shop'
)
# 获取商家扩展表
shangjia_profile = shangjia_user.shop_profile
# ✅ 确认字段tuikuan 退款订单总量
shangjia_profile.tuikuan += 1
# ✅ 确认字段yue 商家余额(加上订单金额)
shangjia_profile.yue += jine
shangjia_profile.save()
logger.info(f"同意退款成功,商家{shangjia_id}退款订单+1余额增加{jine}")
except UserMain.DoesNotExist:
# 商家用户不存在,记录日志但不影响操作
logger.warning(f"同意退款:商家{shangjia_id}不存在,跳过商家更新")
except AttributeError:
# 商家扩展表不存在,记录日志但不影响操作
logger.warning(f"同意退款:商家{shangjia_id}扩展信息不存在,跳过商家更新")
# 13.4 更新退款记录表
try:
# 查询退款记录表
tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id)
# ✅ 确认字段sqzhuangtai 申请状态改为1成功
tuikuan_jilu.sqzhuangtai = 1
# 可以添加同意理由字段tongyi_liyou
# tuikuan_jilu.tongyi_liyou = tuikuan_liyou
# ✅ 确认字段chuliid 处理人ID使用管理员账号即手机号
tuikuan_jilu.chuliid = qianzhanghao
tuikuan_jilu.save()
logger.info(f"同意退款成功,更新退款记录表,订单:{dingdan_id}")
except Tuikuanjilu.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
)
# dingdan/views.py
import logging
import time
import random
import hashlib
import requests
import xml.etree.ElementTree as ET
from decimal import Decimal
from django.db import transaction
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from yonghu.models import UserMain, UserDashou, UserBoss
from dingdan.models import Dingdan, DingdanPingtai, Tuikuanjilu
from peizhi.models import Szjilu
logger = logging.getLogger(__name__)
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('dingdan_id', '').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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
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(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 8. 验证订单状态为1、7、2、8、4允许退款的状态
if dingdan_obj.zhuangtai not in [1, 7, 2, 8, 4]:
return Response(
{'code': 400, 'message': '订单状态不允许退款', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 9. 验证订单为平台发单
if dingdan_obj.fadan_pingtai != 1:
return Response(
{'code': 400, 'message': '此接口只处理平台发单的退款', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 10. 获取订单的重要字段
jiedan_dashou_id = dingdan_obj.jiedan_dashou_id
jine = dingdan_obj.jine
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.laoban_id
# 12. 调用微信退款接口(优先使用 wechat_transaction_id
transaction_id = getattr(dingdan_obj, 'wechat_transaction_id', None)
weixin_refund_result = self.call_weixin_refund(
out_trade_no=dingdan_id,
jine=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.zhuangtai = 5
dingdan_obj.save()
# 14. 更新打手扩展表
if jiedan_dashou_id:
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou_profile = dashou_user.dashou_profile
dashou_profile.zhuangtai = 1 # 打手状态设为空闲
dashou_profile.tuikuanliang += 1
dashou_profile.save()
except (UserMain.DoesNotExist, AttributeError):
logger.warning(f"平台退款:打手{jiedan_dashou_id}不存在或扩展信息缺失,跳过")
# 15. 更新老板扩展表
if laoban_id:
try:
laoban_user = UserMain.objects.get(yonghuid=laoban_id)
laoban_profile = laoban_user.boss_profile
laoban_profile.alltui += 1
laoban_profile.zonge -= jine
if laoban_profile.zonge < 0:
laoban_profile.zonge = 0
laoban_profile.save()
except (UserMain.DoesNotExist, AttributeError):
logger.warning(f"平台退款:老板{laoban_id}不存在或扩展信息缺失,跳过")
# 16. 更新收支记录表id=1
try:
szjilu_obj = Szjilu.objects.get(id=1)
szjilu_obj.zongsy -= jine
szjilu_obj.zongzc += jine
szjilu_obj.jrzc += 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 = Tuikuanjilu.objects.get_or_create(
dingdan_id=dingdan_id,
defaults={
'dashouid': jiedan_dashou_id if jiedan_dashou_id else '',
'qingqiuid': laoban_id if laoban_id else '',
'chuliid': qianzhanghao,
'liyou': tuikuan_liyou if tuikuan_liyou else '给老板退款',
'sqzhuangtai': 1,
'jine': jine,
'dashou_fencheng': dingdan_obj.dashou_fencheng or 0,
'jieshao': dingdan_obj.jieshao or '',
'beizhu': '给老板退款',
'nicheng': dingdan_obj.nicheng or '',
}
)
if not created:
tuikuan_jilu.sqzhuangtai = 1
if tuikuan_liyou:
tuikuan_jilu.liyou = tuikuan_liyou
tuikuan_jilu.chuliid = 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': wx_cfg.WEIXIN_APPID,
'mch_id': wx_cfg.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': wx_cfg.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={wx_cfg.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=(wx_cfg.WEIXIN_CERT_PATH, wx_cfg.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解析失败'}
'''from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework import status
from django.db import transaction
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
import logging
import time
import random
import hashlib
import requests
import xml.etree.ElementTree as ET
# 获取日志器
logger = logging.getLogger(__name__)
# 在 dingdan/utils.py如果没有就创建或任何合适的公共工具文件中添加
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework import status
from django.db import transaction
from django.conf import settings
from utils.xcx_sys_config import wx_cfg
import logging
import time
import random
import hashlib
import requests
import xml.etree.ElementTree as ET
# 获取日志器
logger = logging.getLogger(__name__)
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):
"""
同意退款接口(平台发单)
核心逻辑:
1. 验证管理员身份和支付密码
2. 验证订单状态为2、8或4进行中、结算中、退款审核中
3. 调用微信退款接口
4. 如果微信受理成功,更新数据库(订单、打手、老板、收支、退款记录)
5. 如果微信受理失败,返回错误,数据库不变
"""
try:
# 1. 获取前端参数
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = request.data.get('dingdan_id', '').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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 验证管理员扩展表是否存在
try:
guanliyuan = dangqianyonghu.admin_profile
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:
from dingdan.models import Dingdan, DingdanPingtai, Tuikuanjilu
from peizhi.models import Szjilu
# 使用select_related预加载平台订单扩展表
dingdan_obj = Dingdan.objects.select_related('pingtai_kuozhan').get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 8. 验证订单状态为2、8或4
if dingdan_obj.zhuangtai not in [1,7,2, 8, 4]:
return Response(
{'code': 400, 'message': '订单状态不允许退款', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 9. 验证订单为平台发单
if dingdan_obj.fadan_pingtai != 1:
return Response(
{'code': 400, 'message': '此接口只处理平台发单的退款', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 10. 获取订单的重要字段
jiedan_dashou_id = dingdan_obj.jiedan_dashou_id # 接单打手ID
jine = dingdan_obj.jine # 订单金额(全额退款)
# 11. 验证订单金额是否存在
if jine is None:
return Response(
{'code': 400, 'message': '订单缺少订单金额', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# 12. 获取老板ID从平台订单扩展表
try:
pingtai_kuozhan = dingdan_obj.pingtai_kuozhan
laoban_id = pingtai_kuozhan.laoban_id
except DingdanPingtai.DoesNotExist:
return Response(
{'code': 400, 'message': '订单老板信息不存在', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
# ========== 修改点开始 ==========
# 13. 先调用微信退款接口,成功后才更新数据库
weixin_refund_result = self.call_weixin_refund(dingdan_id, jine)
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
)
# 14. 微信退款受理成功,开始事务更新数据库
with transaction.atomic():
# 14.1 更新订单状态为5已退款
dingdan_obj.zhuangtai = 5
dingdan_obj.save()
# 14.2 更新打手扩展表(退款订单总量+1
if jiedan_dashou_id:
try:
# 查询打手用户主表
dashou_user = UserMain.objects.get(
yonghuid=jiedan_dashou_id,
#user_type='dashou'
)
dashou_profile = dashou_user.dashou_profile
dashou_profile.zhuangtai = 1
dashou_profile.tuikuanliang += 1
dashou_profile.save()
except UserMain.DoesNotExist:
logger.warning(f"平台退款:打手{jiedan_dashou_id}不存在,跳过打手更新")
except AttributeError:
logger.warning(f"平台退款:打手{jiedan_dashou_id}扩展信息不存在,跳过打手更新")
# 14.3 更新老板扩展表
if laoban_id:
try:
laoban_user = UserMain.objects.get(
yonghuid=laoban_id,
#user_type='normal'
)
laoban_profile = laoban_user.boss_profile
laoban_profile.alltui += 1
laoban_profile.zonge -= jine
if laoban_profile.zonge < 0:
laoban_profile.zonge = 0
laoban_profile.save()
except UserMain.DoesNotExist:
logger.warning(f"平台退款:老板{laoban_id}不存在,跳过老板更新")
except AttributeError:
logger.warning(f"平台退款:老板{laoban_id}扩展信息不存在,跳过老板更新")
# 14.4 更新收支记录表id=1的记录
try:
#更新支出记录
from dingdan.utils import update_daily_payout
update_daily_payout(jine)
szjilu_obj = Szjilu.objects.get(id=1)
szjilu_obj.zongsy -= jine
szjilu_obj.zongzc += jine
szjilu_obj.jrzc += jine
szjilu_obj.save()
except Szjilu.DoesNotExist:
logger.error(f"平台退款:收支记录表(id=1)不存在,跳过更新")
except Exception as e:
logger.error(f"更新收支记录表异常: {str(e)}")
# 14.5 更新退款记录表
try:
# 查询退款记录表,是否已有该订单的记录
try:
tuikuan_jilu = Tuikuanjilu.objects.get(dingdan_id=dingdan_id)
tuikuan_jilu.sqzhuangtai = 1
if tuikuan_liyou:
tuikuan_jilu.liyou = tuikuan_liyou
tuikuan_jilu.chuliid = qianzhanghao
tuikuan_jilu.save()
except Tuikuanjilu.DoesNotExist:
# 退款记录表不存在,创建新的记录
Tuikuanjilu.objects.create(
dingdan_id=dingdan_id,
dashouid=jiedan_dashou_id if jiedan_dashou_id else '',
qingqiuid=laoban_id if laoban_id else '',
chuliid=qianzhanghao,
liyou=tuikuan_liyou if tuikuan_liyou else '给老板退款',
sqzhuangtai=1,
jine=jine,
dashou_fencheng=dingdan_obj.dashou_fencheng if dingdan_obj.dashou_fencheng else 0,
jieshao=dingdan_obj.jieshao if dingdan_obj.jieshao else '',
beizhu='给老板退款',
nicheng=dingdan_obj.nicheng if dingdan_obj.nicheng else '',
)
except Exception as e:
logger.error(f"更新退款记录表异常: {str(e)}")
logger.info(f"平台退款成功,订单:{dingdan_id},退款单号:{weixin_refund_result.get('refund_id')}")
# ========== 修改点结束 ==========
# 15. 返回成功响应
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, dingdan_id, jine):
"""
调用微信退款接口
参考你提供的接口,但根据现有模型和配置调整
需要在settings.py中配置以下参数
WEIXIN_APPID = '你的小程序appid'
WEIXIN_MCHID = '你的商户号'
WEIXIN_SHANGHUMIYAO = '你的商户密钥'
WEIXIN_CERT_PATH = '证书路径/cert/apiclient_cert.pem'
WEIXIN_KEY_PATH = '证书路径/cert/apiclient_key.pem'
返回:{'success': True/False, 'msg': '消息'}
"""
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': wx_cfg.WEIXIN_APPID,
'mch_id': wx_cfg.WEIXIN_MCHID,
'nonce_str': hashlib.md5(str(time.time()).encode()).hexdigest(),
'out_trade_no': dingdan_id, # 商户订单号我们的订单ID
'out_refund_no': refund_id, # 商户退款单号
'total_fee': total_fee_in_fen, # 订单总金额(分)
'refund_fee': total_fee_in_fen, # 退款金额(分),全额退款
'op_user_id': wx_cfg.WEIXIN_MCHID, # 操作员ID默认为商户号
'refund_account': 'REFUND_SOURCE_UNSETTLED_FUNDS' # 退款资金来源
}
# 生成签名
sorted_params = sorted(refund_data.items())
sign_string = '&'.join([f"{key}={value}" for key, value in sorted_params if value])
sign_string += f"&key={wx_cfg.WEIXIN_SHANGHUMIYAO}"
sign = hashlib.md5(sign_string.encode()).hexdigest().upper()
refund_data['sign'] = sign
# 将字典转换为XML
xml_data = self.dict_to_xml(refund_data)
# 微信退款API地址
weixin_refund_url = "https://api.mch.weixin.qq.com/secapi/pay/refund"
# 设置超时时间
timeout = 30
# 调用微信退款API需要证书
headers = {'Content-Type': 'application/xml'}
response = requests.post(
weixin_refund_url,
data=xml_data.encode('utf-8'),
headers=headers,
cert=(wx_cfg.WEIXIN_CERT_PATH, wx_cfg.WEIXIN_KEY_PATH),
timeout=timeout
)
# 解析返回结果
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"微信退款成功,订单:{dingdan_id},退款单号:{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"微信退款失败,订单:{dingdan_id},错误:{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"微信退款接口请求超时,订单:{dingdan_id}")
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 = ["<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):
"""将XML转换为字典"""
try:
root = ET.fromstring(xml_data)
result = {}
for child in root:
result[child.tag] = child.text
return result
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.yonghuid}")
# 🔴 导入商品类型模型
from shangpin.models import ShangpinLeixing
# 🔴 查询所有正常状态的商品类型
# 条件shenhezhuangtai=1正常状态
# 排序:先按 paixu 降序(数值大的在前),再按 id 升序
leixing_queryset = ShangpinLeixing.objects.filter(
shenhezhuangtai__in=[3] # 查询状态为1或9的记录
).order_by('-paixu', 'id')
'''leixing_queryset = ShangpinLeixing.objects.filter(
shenhezhuangtai__in= [1,9] # 🔴 只取正常状态,排除审核专用
).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.yonghuid}")
# 🔴 返回成功响应
# 注意:保持与前端原数据格式完全一致
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.yonghuid}, 错误: {str(e)}", exc_info=True)
return Response({
"code": 500,
"msg": f"服务器内部错误: {str(e)}",
"data": None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
logger = logging.getLogger(__name__)
class AdJuJueJieSuan(APIView):
"""
后台拒绝结算接口(平台发单)
权限: JWT Token认证 + 管理员身份验证
前端调用: POST /dingdan/adddjjjs
参数: {
"zhanghao": "管理员账号(手机号)",
"dingdan_id": "订单ID",
"tkly": "拒绝结算理由(必填)"
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
try:
# 1. 获取参数
qianzhanghao = request.data.get('zhanghao', '').strip()
dingdan_id = request.data.get('dingdan_id', '').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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
# 5. 查询订单
try:
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 6. 校验订单条件平台发单1且状态为2或8
if dingdan.fadan_pingtai != 1 or dingdan.zhuangtai not in [2, 8]:
return Response(
{'code': 400, 'message': '仅平台发单且状态为进行中(2)或结算中(8)的订单可拒绝结算', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
jiedan_dashou_id = dingdan.jiedan_dashou_id
if not jiedan_dashou_id:
return Response(
{'code': 400, 'message': '订单暂无接单打手', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
with transaction.atomic():
# 7. 更新订单状态为5已退款并保存拒绝理由
dingdan.zhuangtai = 5
dingdan.tkly = tkly
dingdan.save()
# 8. 更新打手扩展表
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou_profile = dashou_user.dashou_profile
dashou_profile.zhuangtai = 1 # 打手状态设为正常
dashou_profile.tuikuanliang += 1 # 退款订单总量+1
dashou_profile.save()
except (UserMain.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('dingdan_id', '').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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
try:
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 校验订单状态允许2、8、4
if dingdan.zhuangtai 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.jiedan_dashou_id
with transaction.atomic():
# 更新订单状态改为1下单中清空接单打手ID
dingdan.zhuangtai = 1
dingdan.jiedan_dashou_id = None
dingdan.save()
# 如果有接单打手,更新其扩展表
if jiedan_dashou_id:
try:
dashou_user = UserMain.objects.get(yonghuid=jiedan_dashou_id)
dashou_profile = dashou_user.dashou_profile
dashou_profile.zhuangtai = 1
dashou_profile.tuikuanliang += 1
dashou_profile.save()
except (UserMain.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('dingdan_id', '').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.user_type != 'admin':
return Response(
{'code': 401, 'message': '身份验证失败', 'data': None},
status=status.HTTP_401_UNAUTHORIZED
)
try:
dingdan = Dingdan.objects.get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response(
{'code': 404, 'message': '订单不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND
)
# 校验订单状态必须为7指定中
if dingdan.zhuangtai != 7:
return Response(
{'code': 400, 'message': '仅状态为指定中(7)的订单可取消指定', 'data': None},
status=status.HTTP_400_BAD_REQUEST
)
with transaction.atomic():
# 更新订单状态改为1下单中清空指定ID
dingdan.zhuangtai = 1
dingdan.zhiding_id = 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
)
logger = logging.getLogger(__name__)
from peizhi.models import Club ,OrderTypeMapping
logger = logging.getLogger(__name__)
logger = logging.getLogger(__name__)
from peizhi.models import Club ,OrderTypeMapping
logger = logging.getLogger(__name__)
class PartnerSyncOrderView(APIView):
"""
接收对方平台派单通知接口
对方平台商家派单成功后,异步调用此接口,将订单信息同步过来。
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
# 1. 验证 token
token = request.data.get('token')
if token != settings.CROSS_PLATFORM_TOKEN:
logger.warning(f"跨平台派单 token 无效: {token}")
return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN)
# 2. 获取必要参数
our_club_id = request.data.get('our_club_id') # 对方平台发送的我方俱乐部ID即我方在对方表中的 partner_club_id
partner_club_id = request.data.get('partner_club_id') # 对方平台的俱乐部ID对方自己的ID
partner_order_id = request.data.get('partner_order_id')
jine = request.data.get('jine')
dashou_fencheng = request.data.get('dashou_fencheng')
jieshao = request.data.get('jieshao')
beizhu = request.data.get('beizhu', '')
create_time_str = request.data.get('create_time')
nicheng = request.data.get('nicheng')
leixing_id = request.data.get('leixing_id') # 对方平台的商品类型ID
partner_yonghu_id = request.data.get('user_id') # 对方下单用户ID
# 必需字段校验
required_fields = [our_club_id, partner_club_id, partner_order_id, jine, dashou_fencheng,
jieshao, create_time_str, nicheng, leixing_id]
if not all(required_fields):
logger.error(f"跨平台派单参数缺失: {request.data}")
return Response({'code': 400, 'message': 'Missing required parameters'}, status=status.HTTP_400_BAD_REQUEST)
# 3. 验证我方俱乐部是否存在且同意接收订单
try:
my_club = Club.objects.get(partner_club_id=our_club_id)
except Club.DoesNotExist:
logger.warning(f"我方俱乐部不存在partner_club_id={our_club_id}")
return Response({'code': 404, 'message': 'Club not found'}, status=status.HTTP_404_NOT_FOUND)
if not my_club.is_receive_enabled:
logger.warning(f"我方俱乐部 {my_club.club_id} 未同意接收订单")
return Response({'code': 403, 'message': 'Receiving not allowed'}, status=status.HTTP_403_FORBIDDEN)
# 4. 查找订单类型映射
try:
mapping = OrderTypeMapping.objects.get(
partner_club_id=our_club_id, # 对方在我方的标识
partner_type_id=leixing_id
)
our_type_id = mapping.our_type_id
except OrderTypeMapping.DoesNotExist:
logger.error(f"未找到订单类型映射: 对方俱乐部={our_club_id}, 对方类型ID={leixing_id}")
return Response({'code': 400, 'message': 'Order type mapping not found'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 获取我方商品类型信息用于获取会员ID、佣金等
try:
our_leixing = ShangpinLeixing.objects.get(id=our_type_id)
except ShangpinLeixing.DoesNotExist:
logger.error(f"我方商品类型不存在: {our_type_id}")
return Response({'code': 400, 'message': 'Local product type not found'}, status=status.HTTP_400_BAD_REQUEST)
# 6. 幂等检查:如果已存在相同 partner_order_id 的订单,直接返回(附带 status='existed'
existing = Dingdan.objects.filter(partner_order_id=partner_order_id).first()
if existing:
logger.info(f"跨平台派单幂等返回: partner_order_id={partner_order_id} 已存在")
return Response({
'code': 0,
'data': {
'local_order_id': existing.dingdan_id,
'status': 'existed' # 明确告知已存在
}
})
# 7. 生成我方订单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}"
# 8. 获取代理商家头像(用于订单图片)
agent_user = UserMain.objects.filter(yonghuid=settings.AGENT_SHANGJIA_ID).first()
agent_avatar = agent_user.avatar if agent_user else ''
# 9. 计算我方实际打手分成金额:对方传来的打手分成 × 我方接收费率
try:
actual_dashou_fencheng = Decimal(dashou_fencheng) * my_club.receive_rate
except Exception:
actual_dashou_fencheng = Decimal(dashou_fencheng)
# 10. 事务创建订单
try:
with transaction.atomic():
new_id = generate_dingdan_id()
# 创建主订单
dingdan = Dingdan.objects.create(
dingdan_id=new_id,
zhuangtai=1, # 普通订单(待接单)
fadan_pingtai=2, # 商家发单
jine=jine, # 原始订单金额
dashou_fencheng=actual_dashou_fencheng, # 调整后的打手分成
jieshao=jieshao,
beizhu=beizhu,
create_time=create_time_str,
yaoqiuleixing=our_leixing.yaoqiuleixing,
nicheng=nicheng,
huiyuan_id=our_leixing.huiyuan_id,
yongjin=our_leixing.yongjin,
leixing_id=our_type_id,
partner_order_id=partner_order_id, # 存储对方订单ID
is_cross=1,
dispatch_type=2, # 对方派单
partner_club_id=our_club_id, # 对方俱乐部ID
tupian=agent_avatar, # 使用代理商家头像作为订单图片
)
# 创建商家扩展表(使用代理商家)
DingdanShangjia.objects.create(
dingdan=dingdan,
shangjia_id=settings.AGENT_SHANGJIA_ID,
sjnicheng='跨平台代理',
partner_yonghu_id=partner_yonghu_id or '',
)
# 成功创建后更新每日派单统计(方向=对方派单)
try:
update_daily_dispatch_stat(
direction=2, # 对方派单
partner_club_id=our_club_id,
dispatch_amount=Decimal(jine), # 统计使用原始订单金额
)
except Exception as e:
logger.error(f"更新派单统计失败: {e}", exc_info=True)
logger.info(f"跨平台订单同步成功: partner_order_id={partner_order_id}, local_id={new_id}")
return Response({
'code': 0,
'data': {
'local_order_id': new_id,
'status': 'created'
}
})
except Exception as e:
logger.error(f"跨平台订单同步失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
from dingdan.utils import update_daily_dispatch_stat
logger = logging.getLogger(__name__)
class PartnerCheckClaimView(APIView):
"""
接收对方平台抢单询问(对方打手抢我方派出的订单)
如果允许抢单:
1. 订单状态改为2已接单
2. 记录对方打手ID、对方俱乐部ID、对方订单ID
3. 更新派单成交统计success_count/success_amount
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
token = request.data.get('token')
if token != settings.CROSS_PLATFORM_TOKEN:
logger.warning(f"token无效: {token}")
return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN)
wofangdingdan_id = request.data.get('wofangdingdan_id')
partner_order_id = request.data.get('partner_order_id')
our_club_id = request.data.get('our_club_id')
dashou_id = request.data.get('dashou_id')
sj_id = request.data.get('sj_id')
if not all([wofangdingdan_id, partner_order_id, our_club_id, dashou_id, sj_id]):
logger.error(f"参数缺失: {request.data}")
return Response({'code': 400, 'message': 'Missing parameters'}, status=status.HTTP_400_BAD_REQUEST)
try:
with transaction.atomic():
# 查询我方俱乐部配置
try:
club = Club.objects.select_for_update().get(partner_club_id=our_club_id)
except Club.DoesNotExist:
logger.warning(f"俱乐部配置不存在 partner_club_id={our_club_id}")
return Response({'code': 0, 'data': {'can_claim': False}})
if not club.is_interop_enabled:
logger.warning(f"俱乐部 {club.club_id} 未开启互通")
return Response({'code': 0, 'data': {'can_claim': False}})
# 锁定订单
try:
order = Dingdan.objects.select_for_update().get(dingdan_id=wofangdingdan_id)
except Dingdan.DoesNotExist:
logger.warning(f"订单不存在: {wofangdingdan_id}")
return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND)
# 只有状态1可抢
if order.zhuangtai != 1:
logger.info(f"订单 {order.dingdan_id} 状态 {order.zhuangtai} 不可抢")
return Response({'code': 0, 'data': {'can_claim': False}})
# 商品类型检查
try:
leixing = ShangpinLeixing.objects.get(id=order.leixing_id)
except ShangpinLeixing.DoesNotExist:
logger.warning(f"商品类型不存在: {order.leixing_id}")
return Response({'code': 0, 'data': {'can_claim': False}})
if not leixing.is_cross_enabled:
logger.warning(f"商品类型 {leixing.jieshao} 未开启跨平台")
return Response({'code': 0, 'data': {'can_claim': False}})
# 执行抢单状态改为2已接单
order.zhuangtai = 2
order.jiedan_dashou_id = dashou_id
order.partner_club_id = our_club_id
order.partner_order_id = partner_order_id
if order.is_cross != 1:
order.is_cross = 1
# dispatch_type 保持原值1-我方派单),不变
order.save()
# 更新派单成交统计(我方派单被成功接单)
update_daily_dispatch_stat(
direction=1,
partner_club_id=our_club_id,
claimed_amount=order.jine,
)
logger.info(f"对方抢单成功: 订单 {order.dingdan_id}, 对方打手 {dashou_id}")
return Response({'code': 0, 'data': {'can_claim': True}})
except Exception as e:
logger.error(f"异常: {e}", exc_info=True)
return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class PartnerNotifyClaimView(APIView):
"""
接收对方平台抢单成功通知(对方派单被他们自己的打手抢了)
我方根据 partner_order_id 找到订单如果状态为1则改为20不更新任何统计。
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
token = request.data.get('token')
if token != settings.CROSS_PLATFORM_TOKEN:
logger.warning(f"token无效: {token}")
return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN)
partner_order_id = request.data.get('partner_order_id')
if not partner_order_id:
logger.error("缺少 partner_order_id")
return Response({'code': 400, 'message': 'Missing partner_order_id'}, status=status.HTTP_400_BAD_REQUEST)
try:
with transaction.atomic():
order = Dingdan.objects.select_for_update().get(partner_order_id=partner_order_id)
# 只有状态为1未被抢时才改为20
if order.zhuangtai == 1:
order.zhuangtai = 20
order.save()
logger.info(f"订单 {order.dingdan_id} 状态改为20被其他俱乐部抢走")
else:
logger.info(f"订单 {order.dingdan_id} 当前状态 {order.zhuangtai},无需修改")
return Response({'code': 0, 'message': 'OK'})
except Dingdan.DoesNotExist:
logger.warning(f"订单不存在 partner_order_id={partner_order_id}")
return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"异常: {e}", exc_info=True)
return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
'''class PartnerSyncOrderView(APIView):
"""
接收对方平台派单通知接口
对方平台商家派单成功后,异步调用此接口,将订单信息同步过来。
请求方式: POST
接口地址: /dingdan/partner_sync_order
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
# 1. 验证 token
token = request.data.get('token')
if token != settings.CROSS_PLATFORM_TOKEN:
logger.warning(f"跨平台派单 token 无效: {token}")
return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN)
# 2. 获取必要参数
our_club_id = request.data.get('our_club_id') # 对方平台发送的我方俱乐部ID即对方在 Club 表中 partner_club_id
partner_club_id = request.data.get('partner_club_id') # 对方平台的俱乐部ID对方自己的ID
partner_order_id = request.data.get('partner_order_id')
jine = request.data.get('jine')
dashou_fencheng = request.data.get('dashou_fencheng')
jieshao = request.data.get('jieshao')
beizhu = request.data.get('beizhu', '')
create_time_str = request.data.get('create_time')
nicheng = request.data.get('nicheng')
leixing_id = request.data.get('leixing_id') # 对方平台的商品类型ID
# 必需字段校验
required_fields = [our_club_id, partner_club_id, partner_order_id, jine, dashou_fencheng,
jieshao, create_time_str, nicheng, leixing_id]
if not all(required_fields):
logger.error(f"跨平台派单参数缺失: {request.data}")
return Response({'code': 400, 'message': 'Missing required parameters'}, status=status.HTTP_400_BAD_REQUEST)
# 3. 验证我方俱乐部是否存在且同意接收订单
# 注意our_club_id 是对方发送过来的代表我方在对方系统中的ID所以用 partner_club_id 匹配
try:
my_club = Club.objects.get(partner_club_id=our_club_id)
except Club.DoesNotExist:
logger.warning(f"我方俱乐部不存在partner_club_id={our_club_id}")
return Response({'code': 404, 'message': 'Club not found'}, status=status.HTTP_404_NOT_FOUND)
if not my_club.is_receive_enabled:
logger.warning(f"我方俱乐部 {my_club.club_id} 未同意接收订单")
return Response({'code': 403, 'message': 'Receiving not allowed'}, status=status.HTTP_403_FORBIDDEN)
# 4. 查找订单类型映射
try:
mapping = OrderTypeMapping.objects.get(
partner_club_id=our_club_id,
partner_type_id=leixing_id
)
our_type_id = mapping.our_type_id
except OrderTypeMapping.DoesNotExist:
logger.error(f"未找到订单类型映射: 对方俱乐部={our_club_id}, 对方类型ID={leixing_id}")
return Response({'code': 400, 'message': 'Order type mapping not found'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 获取我方商品类型信息用于获取会员ID、佣金等
try:
our_leixing = ShangpinLeixing.objects.get(id=our_type_id)
except ShangpinLeixing.DoesNotExist:
logger.error(f"我方商品类型不存在: {our_type_id}")
return Response({'code': 400, 'message': 'Local product type not found'}, status=status.HTTP_400_BAD_REQUEST)
# 6. 幂等检查:如果已存在相同 partner_order_id 的订单,直接返回
existing = Dingdan.objects.filter(partner_order_id=partner_order_id).first()
if existing:
logger.info(f"跨平台派单幂等返回: partner_order_id={partner_order_id} 已存在")
return Response({
'code': 0,
'data': {'local_order_id': existing.dingdan_id}
})
# 7. 生成我方订单ID
def generate_dingdan_id():
import time, random, os
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}"
# 8. 获取代理商家头像(用于订单图片)
agent_user = UserMain.objects.filter(yonghuid=settings.AGENT_SHANGJIA_ID).first()
agent_avatar = agent_user.avatar if agent_user else ''
# 9. 事务创建订单
try:
with transaction.atomic():
new_id = generate_dingdan_id()
# 创建主订单
dingdan = Dingdan.objects.create(
dingdan_id=new_id,
zhuangtai=1, # 普通订单(默认)
fadan_pingtai=2, # 商家发单
jine=jine,
dashou_fencheng=dashou_fencheng,
jieshao=jieshao,
beizhu=beizhu,
create_time=create_time_str,
yaoqiuleixing=our_leixing.yaoqiuleixing,
nicheng=nicheng,
huiyuan_id=our_leixing.huiyuan_id,
yongjin=our_leixing.yongjin,
leixing_id=our_type_id,
partner_order_id=partner_order_id,
is_cross=1,
dispatch_type=2, # 对方派给我方
partner_club_id=our_club_id, # 对方的俱乐部ID
tupian=agent_avatar, # 使用代理商家头像作为订单图片
)
# 创建商家扩展表(使用代理商家)
DingdanShangjia.objects.create(
dingdan=dingdan,
shangjia_id=settings.AGENT_SHANGJIA_ID,
sjnicheng='跨平台代理'
)
logger.info(f"跨平台订单同步成功: partner_order_id={partner_order_id}, local_id={new_id}")
return Response({
'code': 0,
'data': {'local_order_id': new_id}
},status = 200)
except Exception as e:
logger.error(f"跨平台订单同步失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
from dingdan.utils import update_daily_dispatch_stat
logger = logging.getLogger(__name__)
class PartnerCheckClaimView(APIView):
"""
接收对方平台抢单询问(对方打手抢我方派出的订单)
如果允许抢单:
1. 订单状态改为2已接单
2. 记录对方打手ID、对方俱乐部ID、对方订单ID
3. 更新派单成交统计success_count/success_amount
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
token = request.data.get('token')
if token != settings.CROSS_PLATFORM_TOKEN:
logger.warning(f"token无效: {token}")
return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN)
wofangdingdan_id = request.data.get('wofangdingdan_id')
partner_order_id = request.data.get('partner_order_id')
our_club_id = request.data.get('our_club_id')
dashou_id = request.data.get('dashou_id')
sj_id = request.data.get('sj_id')
if not all([wofangdingdan_id, partner_order_id, our_club_id, dashou_id, sj_id]):
logger.error(f"参数缺失: {request.data}")
return Response({'code': 400, 'message': 'Missing parameters'}, status=status.HTTP_400_BAD_REQUEST)
try:
with transaction.atomic():
# 查询我方俱乐部配置
try:
club = Club.objects.select_for_update().get(partner_club_id=our_club_id)
except Club.DoesNotExist:
logger.warning(f"俱乐部配置不存在 partner_club_id={our_club_id}")
return Response({'code': 0, 'data': {'can_claim': False}})
if not club.is_interop_enabled:
logger.warning(f"俱乐部 {club.club_id} 未开启互通")
return Response({'code': 0, 'data': {'can_claim': False}})
# 锁定订单
try:
order = Dingdan.objects.select_for_update().get(dingdan_id=wofangdingdan_id)
except Dingdan.DoesNotExist:
logger.warning(f"订单不存在: {wofangdingdan_id}")
return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND)
# 只有状态1可抢
if order.zhuangtai != 1:
logger.info(f"订单 {order.dingdan_id} 状态 {order.zhuangtai} 不可抢")
return Response({'code': 0, 'data': {'can_claim': False}})
# 商品类型检查
try:
leixing = ShangpinLeixing.objects.get(id=order.leixing_id)
except ShangpinLeixing.DoesNotExist:
logger.warning(f"商品类型不存在: {order.leixing_id}")
return Response({'code': 0, 'data': {'can_claim': False}})
if not leixing.is_cross_enabled:
logger.warning(f"商品类型 {leixing.jieshao} 未开启跨平台")
return Response({'code': 0, 'data': {'can_claim': False}})
# 执行抢单状态改为2已接单
order.zhuangtai = 2
order.jiedan_dashou_id = dashou_id
order.partner_club_id = our_club_id
order.partner_order_id = partner_order_id
if order.is_cross != 1:
order.is_cross = 1
# dispatch_type 保持原值1-我方派单),不变
order.save()
# 更新派单成交统计(我方派单被成功接单)
update_daily_dispatch_stat(
direction=1,
partner_club_id=our_club_id,
claimed_amount=order.jine,
)
logger.info(f"对方抢单成功: 订单 {order.dingdan_id}, 对方打手 {dashou_id}")
return Response({'code': 0, 'data': {'can_claim': True}})
except Exception as e:
logger.error(f"异常: {e}", exc_info=True)
return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class PartnerNotifyClaimView(APIView):
"""
接收对方平台抢单成功通知接口
对方平台打手抢单成功后,调用此接口,我方处理本地订单。
请求方式: POST
接口地址: /dingdan/partner_notify_claim
请求参数:
token: 认证令牌需与settings.CROSS_PLATFORM_TOKEN一致
order_id: 对方平台的订单ID即我方记录的 partner_order_id
返回:
code: 0成功其他失败
message: 描述信息
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
# 1. 验证token
token = request.data.get('token')
if token != settings.CROSS_PLATFORM_TOKEN:
logger.warning(f"跨平台抢单通知 token 无效: {token}")
return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN)
order_id = request.data.get('local_order_id') # 对方平台的订单ID即我方的 partner_order_id
if not order_id:
logger.error("跨平台抢单通知参数缺失: order_id")
return Response({'code': 400, 'message': 'Missing order_id'}, status=status.HTTP_400_BAD_REQUEST)
try:
with transaction.atomic():
# 2. 根据 partner_order_id 查找订单
order = Dingdan.objects.select_for_update().get(partner_order_id=order_id)
# 3. 根据订单状态处理
if order.zhuangtai == 1:
# 订单未被抢,直接删除(包括关联的商家扩展表)
# 先删除扩展表
DingdanShangjia.objects.filter(dingdan=order).delete()
# 再删除主订单
order.delete()
logger.info(f"跨平台抢单通知订单状态为1已删除partner_order_id={order_id}")
return Response({'code': 0, 'message': 'Order deleted'})
else:
# 订单状态不是1已被抢或异常改为状态15异常订单
order.zhuangtai = 15
order.save()
logger.info(f"跨平台抢单通知订单状态异常已改为15partner_order_id={order_id}")
return Response({'code': 0, 'message': 'Order marked as abnormal'})
except Dingdan.DoesNotExist:
logger.warning(f"跨平台抢单通知:订单不存在 partner_order_id={order_id}")
return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"跨平台抢单通知异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'message': 'Internal error'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)'''
class PartnerRefundNotifyView(APIView):
"""
接收对方平台退款通知接口
对方平台商家申请退款时,调用此接口通知我方,我方将本地对应订单改为退款状态。
请求方式: POST
接口地址: /dingdan/partner_refund_notify
请求参数:
token: 认证令牌需与settings.CROSS_PLATFORM_TOKEN一致
order_id: 对方平台的订单ID即我方记录的 partner_order_id
返回:
code: 0成功其他失败
message: 描述信息
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
parser_classes = [JSONParser]
def post(self, request):
# 1. 验证token
token = request.data.get('token')
if token != settings.CROSS_PLATFORM_TOKEN:
logger.warning(f"跨平台退款通知 token 无效: {token}")
return Response({'code': 403, 'message': 'Invalid token'}, status=status.HTTP_403_FORBIDDEN)
order_id = request.data.get('order_id') # 对方平台的订单ID即我方的 partner_order_id
if not order_id:
logger.error("跨平台退款通知参数缺失: order_id")
return Response({'code': 400, 'message': 'Missing order_id'}, status=status.HTTP_400_BAD_REQUEST)
try:
with transaction.atomic():
# 2. 根据 partner_order_id 查找订单(必须存在且为跨平台订单)
order = Dingdan.objects.select_for_update().get(partner_order_id=order_id, is_cross=1)
# 如果订单已完成,不允许退款
if order.zhuangtai == 3:
logger.warning(f"跨平台退款通知:订单 {order_id} 已完成,不可退款")
return Response({'code': 400, 'message': 'Order already completed, cannot refund'},
status=status.HTTP_400_BAD_REQUEST)
# 3. 如果订单状态不是5则改为5已退款并更新打手信息
if order.zhuangtai != 5:
# 记录原打手ID
old_dashou_id = order.jiedan_dashou_id
if old_dashou_id:
try:
# 查询打手用户主表
dashou_user = UserMain.objects.get(yonghuid=old_dashou_id)
# 通过反向关系获取打手扩展表
if hasattr(dashou_user, 'dashou_profile'):
dashou_profile = dashou_user.dashou_profile
dashou_profile.zhuangtai = 1 # 恢复打手状态
dashou_profile.tuikuanliang += 1 # 退款订单量+1
dashou_profile.save()
logger.info(f"退款通知:打手 {old_dashou_id} 状态已恢复,退款订单+1")
except UserMain.DoesNotExist:
logger.warning(f"退款通知:打手用户不存在 {old_dashou_id}")
except Exception as e:
logger.error(f"退款通知:更新打手信息失败 {str(e)}")
# 更新订单状态为5
order.zhuangtai = 5
# 可选清空接单打手ID
#order.jiedan_dashou_id = None
order.save()
logger.info(f"跨平台退款通知成功:订单 {order_id} 已改为退款状态")
else:
logger.info(f"跨平台退款通知:订单 {order_id} 已是退款状态,无需重复处理")
return Response({'code': 0, 'message': 'Refund processed successfully'})
except Dingdan.DoesNotExist:
logger.warning(f"跨平台退款通知订单不存在或不是跨平台订单partner_order_id={order_id}")
return Response({'code': 404, 'message': 'Order not found'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"跨平台退款通知异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'message': 'Internal error'}, 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('dingdan_id', '')
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.yonghuid
# 检查是否为打手
try:
dashou_profile = current_user.dashou_profile # 假设User模型有related_name='dashou_profile'
except UserDashou.DoesNotExist:
return Response({'code': 5, 'msg': '用户不是打手身份', 'data': None},
status=status.HTTP_403_FORBIDDEN)
# ---------- 订单查询及权限验证 ----------
try:
# 订单必须属于该打手且状态为4退款中或8结算中
dingdan = Dingdan.objects.get(
dingdan_id=dingdan_id,
jiedan_dashou_id=yonghuid,
zhuangtai__in=[4, 8] # 只允许修改退款中或结算中的订单
)
except Dingdan.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, _ = Dashoutupian.objects.filter(
dingdan_id=dingdan_id,
dashou=yonghuid,
tupian__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(Dashoutupian(
dingdan_id=dingdan_id,
dashou=yonghuid,
tupian=url,
create_time=timezone.now()
))
Dashoutupian.objects.bulk_create(new_records)
logger.info(f"订单 {dingdan_id} 新增图片 {len(new_records)}")
# 3. 更新订单留言(如果发生变化)
# 注意:即使留言为空字符串,也允许更新,表示清空留言
if 'liuyan' in data: # 判断是否传了liuyan字段可能为空字符串
dingdan.dashou_liuyan = liuyan
dingdan.update_time = timezone.now()
dingdan.save(update_fields=['dashou_liuyan', 'update_time'])
# 全部成功,返回成功响应
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)
from django.core.exceptions import ObjectDoesNotExist
from dingdan.tongzhi_tasks import dingdan_guangbo
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.yonghuid
except AttributeError:
return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# ========== 2. 提取订单ID ==========
dingdan_id = request.data.get('dingdan_id', '').strip()
if not dingdan_id:
return Response({'code': 2, 'msg': '订单ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 3. 查询订单并校验权限 ==========
try:
# 关联查询商家扩展表,确保当前商家是该订单的发布者
shangjia_kuozhan = DingdanShangjia.objects.select_related('dingdan').get(
dingdan__dingdan_id=dingdan_id,
shangjia_id=yonghuid
)
except DingdanShangjia.DoesNotExist:
return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN)
old_order = shangjia_kuozhan.dingdan
# ========== 4. 状态校验(仅允许进行中或结算中) ==========
if old_order.zhuangtai not in [2, 8]:
return Response({'code': 4, 'msg': '当前订单状态不允许更换打手'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 5. 收集订单关键数据(用于新订单复制) ==========
# 这些字段将在退款处理后用于创建新订单
jine = old_order.jine
dashou_fencheng = old_order.dashou_fencheng
leixing_id = old_order.leixing_id
yaoqiuleixing = old_order.yaoqiuleixing
huiyuan_id = old_order.huiyuan_id
yongjin = old_order.yongjin
jieshao = old_order.jieshao
nicheng = old_order.nicheng
beizhu = old_order.beizhu
user1_id = old_order.user1_id
tupian = old_order.tupian
fadan_pingtai = old_order.fadan_pingtai
# 跨平台相关
is_cross = old_order.is_cross
partner_club_id = old_order.partner_club_id
partner_order_id = old_order.partner_order_id
need_notify_partner = is_cross and partner_club_id and partner_order_id
dashou_id = old_order.jiedan_dashou_id # 可能为空
# ========== 6. 退款处理事务块1退款原订单 + 打手统计 + 跨平台通知) ==========
try:
with transaction.atomic():
# 6.1 订单状态更新为已退款
old_order.zhuangtai = 5
old_order.save(update_fields=['zhuangtai'])
# 6.2 释放打手
if dashou_id:
try:
dashou_user = UserMain.objects.get(yonghuid=dashou_id)
dashou_profile = dashou_user.dashou_profile
dashou_profile.tuikuanliang = (dashou_profile.tuikuanliang or 0) + 1
dashou_profile.zhuangtai = 1 # 空闲
dashou_profile.save(update_fields=['tuikuanliang', 'zhuangtai'])
except (UserMain.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"打手 {dashou_id} 不存在或扩展表缺失,跳过更新")
# 6.3 更新退款记录
Tuikuanjilu.objects.update_or_create(
dingdan_id=dingdan_id,
defaults={
'dashouid': dashou_id or '',
'qingqiuid': yonghuid, # 申请人为当前商家
'chuliid': yonghuid, # 处理人也是商家自己
'liyou': '商家更换打手',
'sqzhuangtai': 1, # 状态 1=成功
'jine': jine or 0,
'dashou_fencheng': dashou_fencheng or 0,
'jieshao': jieshao or '',
'beizhu': '更换打手系统自动退款',
'nicheng': nicheng or '',
}
)
# 6.4 打手每日统计(退款行为)
if dashou_id:
try:
from houtai.utils import update_dashou_daily_by_action
update_dashou_daily_by_action(
yonghuid=dashou_id,
amount=dashou_fencheng,
action=3 # 3=退款
)
except Exception as e:
logger.error(f"更新打手每日统计失败: {e}")
# 统计失败不应阻断主流程,但可记录
# 6.5 跨平台退款通知
if need_notify_partner:
try:
from peizhi.models import ClubConfig, Club
my_club_config = ClubConfig.objects.filter(is_self=1).first()
if not my_club_config:
raise Exception("我方俱乐部配置不存在")
self_club_id = my_club_config.club_id
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
partner_domain = club_rel.partner_domain.rstrip('/')
notify_url = f"{partner_domain}/houtai/dfddtk"
payload = {
'order_id': partner_order_id,
'club_id': self_club_id,
'status': 5,
'reason': '商家更换打手退款'
}
resp = requests.post(notify_url, json=payload, timeout=5)
if resp.status_code != 200:
transaction.set_rollback(True)
logger.error(f"对方平台接口HTTP错误: {resp.status_code}")
return Response({'code': 500, 'msg': '通知对方平台失败'})
resp_json = resp.json()
if resp_json.get('code') != 0:
transaction.set_rollback(True)
logger.error(f"对方平台退款通知失败: {resp_json.get('msg')}")
return Response({'code': 500, 'msg': f"对方平台通知失败: {resp_json.get('msg')}"})
# 更新跨平台扩展数据
from dingdan.models import CrossPlatformOrderData
CrossPlatformOrderData.objects.update_or_create(
dingdan_id=dingdan_id,
defaults={
'partner_order_id': partner_order_id,
'partner_club_id': partner_club_id,
'partner_order_status': 5,
}
)
except Exception as e:
transaction.set_rollback(True)
logger.error(f"通知对方平台异常: {e}")
return Response({'code': 500, 'msg': f'通知对方平台失败: {str(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 Dingdan.objects.filter(dingdan_id=new_dingdan_id).exists():
new_dingdan_id = generate_dingdan_id()
# 7.2 创建新订单(复制数据,状态为 1
new_order = Dingdan.objects.create(
dingdan_id=new_dingdan_id,
zhuangtai=1, # 已下单
fadan_pingtai=fadan_pingtai,
jine=jine,
dashou_fencheng=dashou_fencheng,
leixing_id=leixing_id,
yaoqiuleixing=yaoqiuleixing,
huiyuan_id=huiyuan_id,
yongjin=yongjin,
jieshao=jieshao,
nicheng=nicheng,
beizhu=beizhu,
user1_id=user1_id, # 商家标识
tupian=tupian,
# 不继承zhiding_id, jiedan_dashou_id, dashou_liuyan 等
)
# 7.3 创建商家扩展表(关联新订单)
DingdanShangjia.objects.create(
dingdan=new_order,
shangjia_id=yonghuid,
sjnicheng=shangjia_kuozhan.sjnicheng or '',
)
# 7.4 异步通知打手(广播新订单)
try:
from utils.order_broadcast import submit_dingdan_guangbo
submit_dingdan_guangbo({
'dingdan_id': new_dingdan_id,
'game_type': self._get_game_type_name(leixing_id),
'amount': str(jine),
'order_desc': (jieshao or '')[:50],
'order_type': '商家派单',
})
except Exception as e:
logger.error(f"广播通知失败: {e}")
# 7.5 跨平台同步(如果适用)
try:
sync_order_to_partners(new_dingdan_id)
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.objects.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.yonghuid # 商家的用户ID
except AttributeError:
return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# ========== 2. 参数提取与校验 ==========
dingdan_id = request.data.get('dingdan_id', '').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 = DingdanShangjia.objects.select_related('dingdan').get(
dingdan__dingdan_id=dingdan_id,
shangjia_id=shangjia_id
)
order = shangjia_kuozhan.dingdan
except DingdanShangjia.DoesNotExist:
return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN)
# ========== 4. 获取被打手ID ==========
dashou_id = order.jiedan_dashou_id
if not dashou_id:
return Response({'code': 4, 'msg': '该订单暂无接单打手,无法罚款'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 5. 重复罚款检查 ==========
if Fadan.objects.filter(beichufa_id=dashou_id, guanliandingdan_id=dingdan_id).exists():
return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 6. 跨平台信息 ==========
is_cross = order.is_cross
partner_order_id = order.partner_order_id or ''
partner_club_id = order.partner_club_id or ''
dispatch_type = order.dispatch_type or 0 # 1=我方派单2=对方派单
# 是否需要通知对方平台我方派单且存在对方订单ID
need_notify_partner = is_cross and partner_order_id and dispatch_type == 1
# ========== 7. 本地创建罚单(可重用) ==========
def create_local_fadan():
Fadan.objects.create(
beichufa_id=dashou_id,
shenqing_chufa=shangjia_id, # 申请人为当前商家
shenfen=1, # 被打者身份1=打手
chufaliyou=chufa_liyou,
fakuanjine=fakuanjine,
guanliandingdan_id=dingdan_id,
zhuangtai=1, # 待缴纳
yingxiang_qiangdan=yingxiang_qiangdan,
shenqingren_shenfen=5, # 申请人是商家
partner_club_id='',
)
# ========== 8. 本地处罚分支 ==========
if not need_notify_partner:
with transaction.atomic():
create_local_fadan()
logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}罚款成功,金额{fakuanjine}")
return Response({'code': 0, 'msg': '罚款申请已提交'})
# ========== 9. 跨平台通知分支 ==========
try:
# 9.1 获取我方俱乐部ID
my_club = ClubConfig.objects.filter(is_self=1).first()
if not my_club:
return Response({'code': 500, 'msg': '系统配置错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
self_club_id = my_club.club_id
# 9.2 获取对方平台域名
club_rel = Club.objects.get(club_id=self_club_id, partner_club_id=partner_club_id)
partner_domain = club_rel.partner_domain.rstrip('/')
except ObjectDoesNotExist:
return Response({'code': 500, 'msg': '对方平台信息未配置'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 9.3 构造通知参数
notify_url = f"{partner_domain}/houtai/kffkdssq_notify" # 与后台接口保持一致
payload = {
'order_id': partner_order_id,
'club_id': self_club_id,
'chufaliyou': chufa_liyou,
'fakuanjine': fakuanjine,
'yingxiang_qiangdan': yingxiang_qiangdan,
'shenqingren_shenfen': 5, # 商家身份
'operator': shangjia_id, # 操作人ID
}
# 9.4 带重试的通知
timeout = 3
retries = 2
success = False
for attempt in range(retries):
try:
resp = requests.post(notify_url, json=payload, timeout=timeout)
if resp.status_code == 200:
resp_data = resp.json()
if resp_data.get('code') == 0:
success = True
break
else:
logger.warning(f"对方平台罚款通知失败,响应: {resp_data}")
else:
logger.warning(f"对方平台HTTP错误: {resp.status_code}")
except Exception as e:
logger.warning(f"通知对方平台异常,第{attempt+1}次尝试: {e}")
continue
if not success:
return Response({'code': 500, 'msg': '通知对方平台失败,请稍后重试'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 9.5 通知成功后,本地也创建罚单(标记跨平台)
with transaction.atomic():
Fadan.objects.create(
beichufa_id=dashou_id,
shenqing_chufa=shangjia_id,
shenfen=1,
chufaliyou=chufa_liyou,
fakuanjine=fakuanjine,
guanliandingdan_id=dingdan_id,
zhuangtai=1,
yingxiang_qiangdan=yingxiang_qiangdan,
shenqingren_shenfen=5,
partner_club_id=partner_club_id, # 记录对方俱乐部ID
)
logger.info(f"商家{shangjia_id}跨平台罚款成功: {dingdan_id}")
return Response({'code': 0, 'msg': '罚款已提交至对方平台'})
class ShangjiaFakuaiXiugaiView(APIView):
"""
商家端统一修改接口:罚款修改/撤销/再次申请,订单退款撤销/修改理由
URL: /dingdan/sjfkxgycx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
# 验证商家身份
try:
shangjia = user.shop_profile
except UserShangjia.DoesNotExist:
return Response({'code': 403, 'msg': '您不是商家'}, status=status.HTTP_403_FORBIDDEN)
if shangjia.zhuangtai != 1:
return Response({'code': 403, 'msg': '商家状态异常'}, status=status.HTTP_403_FORBIDDEN)
operation = request.data.get('operation')
dingdan_id = request.data.get('dingdan_id')
if not dingdan_id:
return Response({'code': 400, 'msg': '缺少订单ID'}, status=status.HTTP_400_BAD_REQUEST)
# 验证订单属于该商家
try:
dingdan = Dingdan.objects.select_related('shangjia_kuozhan').get(dingdan_id=dingdan_id)
except Dingdan.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
if not hasattr(dingdan, 'shangjia_kuozhan') or dingdan.shangjia_kuozhan.shangjia_id != user.yonghuid:
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 = Fadan.objects.get(id=fadan_id)
except Fadan.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND)
if fadan.guanliandingdan_id != 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.zhuangtai != 4:
return Response({'code': 400, 'msg': '只有退款中的订单可以撤销退款申请'})
with transaction.atomic():
dingdan.zhuangtai = 8
dingdan.tkly = '' # 清空退款理由
dingdan.save()
# 记录撤销理由可存入日志(如需要可另建日志表,此处简单处理)
logger.info(f"商家撤销退款订单 {dingdan.dingdan_id},理由: {liyou}")
return Response({'code': 0, 'msg': '已撤销退款申请'})
def _modify_refund_reason(self, dingdan, liyou):
"""修改退款理由仅状态4时"""
if dingdan.zhuangtai != 4:
return Response({'code': 400, 'msg': '只有退款中的订单可以修改退款理由'})
if not liyou.strip():
return Response({'code': 400, 'msg': '退款理由不能为空'})
dingdan.tkly = liyou[:500]
dingdan.save()
return Response({'code': 0, 'msg': '退款理由修改成功'})
def _modify_penalty(self, fadan, liyou, jine_str):
"""修改罚款仅状态1待缴纳 或 3申诉中"""
if fadan.zhuangtai 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.chufaliyou = liyou[:500]
fadan.fakuanjine = jine
# 如果之前是驳回状态修改,需要重置状态为待缴纳?不,修改不应改变状态
fadan.save()
return Response({'code': 0, 'msg': '罚款修改成功'})
def _cancel_penalty(self, fadan, liyou):
"""撤销罚款状态1或3 → 状态4 已驳回)"""
if fadan.zhuangtai not in (1, 3):
return Response({'code': 400, 'msg': '当前罚单状态不可撤销'})
if not liyou.strip():
return Response({'code': 400, 'msg': '驳回理由不能为空'})
with transaction.atomic():
fadan.zhuangtai = 4 # 已驳回
fadan.bohuiliyou = liyou[:500]
fadan.save()
return Response({'code': 0, 'msg': '罚款已撤销(驳回)'})
def _resubmit_penalty(self, fadan, liyou, jine_str):
"""再次申请罚款状态4 → 状态1修改理由和金额"""
if fadan.zhuangtai != 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.zhuangtai = 1 # 待缴纳
fadan.chufaliyou = liyou[:500]
fadan.fakuanjine = jine
fadan.bohuiliyou = '' # 清空驳回理由
fadan.save()
return Response({'code': 0, 'msg': '已重新申请罚款'})