进行了完整的视图拆分,同步了 merchant_ops 的修改

This commit is contained in:
2026-07-05 10:01:09 +08:00
parent 9d98ac9e83
commit c5ae30507b
46 changed files with 32768 additions and 188172 deletions

File diff suppressed because it is too large Load Diff

87
orders/views/__init__.py Normal file
View File

@@ -0,0 +1,87 @@
"""orders.views package aggregate entry - re-export all submodule symbols.
Preserves external `from orders.views import X` for orders/urls.py and merchant_ops.
"""
from .payment import (
PaymentFailView,
WechatPayNotifyView,
PaymentVerifyView,
CreateOrderView,
)
from .boss_orders import (
DingdanHuoquView,
DingdanXiangqingView,
JiedanView,
DingdanXiangqingView2,
JiedanView2,
)
from .merchant import (
ShangpinLeixingView,
ShangjiaPaifaView,
ShangjiaDingdanHuoquView,
ShangjiaShujuTongjiView,
ShangjiaDingdanXiangqingView,
ShangjiaJiesuanView,
ShangjiaChexiaoView,
ShangjiaTuikuanShenqingView,
ShangjiaChufaShenqingView,
)
from .chat import (
DaiLiQunLiaoXiaoXiView,
ShangjiaLianjieLiuYanZhuanFaView,
LianTongDuiHuaZhunBeiView,
LianTongPeiDuiDingDanLieBiaoView,
LianTongDingDanZhuangTaiView,
LianTongDingDanZhuangTaiPiLiangView,
)
from .dashou import (
DashouDingdanHuoquView,
ShangpinLeixingHuoquView,
QiangdanView,
DashouDingdanHuoquView1,
DashouCOSZhengshuView,
ShangjiaCOSZhengshuView,
DashouTijiaoView,
DashouDingdanXiangqingView,
DashouHuoquLeixingView,
DashouXiugaiView,
)
from .admin import (
AdGetDingdanXiangQing,
AdGengHuanDaShou,
AdQiangZhiJieDan,
AdJuJueTuiKuan,
AdTongYiTuiKuanShangJia,
AdTongYiTuiKuanPingTai,
AdJuJueJieSuan,
AdZhuanYiDaTing,
AdQuXiaoZhiDing,
ZhiDingChaxunView,
ZhiDingHuiFuView,
ZxsjghdsView,
)
from .merchant_penalty import (
ShangjiaFakuanApplyView,
ShangjiaFakuaiXiugaiView,
)
__all__ = [
"PaymentFailView", "WechatPayNotifyView", "PaymentVerifyView", "CreateOrderView",
"DingdanHuoquView", "DingdanXiangqingView", "JiedanView",
"DingdanXiangqingView2", "JiedanView2",
"ShangpinLeixingView", "ShangjiaPaifaView", "ShangjiaDingdanHuoquView",
"ShangjiaShujuTongjiView", "ShangjiaDingdanXiangqingView", "ShangjiaJiesuanView",
"ShangjiaChexiaoView", "ShangjiaTuikuanShenqingView", "ShangjiaChufaShenqingView",
"DaiLiQunLiaoXiaoXiView", "ShangjiaLianjieLiuYanZhuanFaView",
"LianTongDuiHuaZhunBeiView", "LianTongPeiDuiDingDanLieBiaoView",
"LianTongDingDanZhuangTaiView", "LianTongDingDanZhuangTaiPiLiangView",
"DashouDingdanHuoquView", "ShangpinLeixingHuoquView", "QiangdanView",
"DashouDingdanHuoquView1", "DashouCOSZhengshuView", "ShangjiaCOSZhengshuView",
"DashouTijiaoView", "DashouDingdanXiangqingView", "DashouHuoquLeixingView",
"DashouXiugaiView",
"AdGetDingdanXiangQing", "AdGengHuanDaShou", "AdQiangZhiJieDan",
"AdJuJueTuiKuan", "AdTongYiTuiKuanShangJia", "AdTongYiTuiKuanPingTai",
"AdJuJueJieSuan", "AdZhuanYiDaTing", "AdQuXiaoZhiDing",
"ZhiDingChaxunView", "ZhiDingHuiFuView", "ZxsjghdsView",
"ShangjiaFakuanApplyView", "ShangjiaFakuaiXiugaiView",
]

1958
orders/views/admin.py Normal file

File diff suppressed because it is too large Load Diff

821
orders/views/boss_orders.py Normal file
View File

@@ -0,0 +1,821 @@
"""orders.views.boss_orders - auto-generated by split script."""
# ==================== 标准库 ====================
import hmac
import os
import json
import time
import random
import string
import traceback
import threading
import requests
import hashlib
import xml.etree.ElementTree as ET
import xmltodict
import logging
from decimal import Decimal
from django.conf import settings
from django.db import models, transaction
from gvsdsdk.fluent import db, func, FQ
from django.db.models import F, Q, OuterRef, Subquery
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.authentication import JWTAuthentication
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sts.v20180813 import sts_client, models
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.money import yuan_to_fen
from utils.chat_utils import (
_send_group_message, _subscribe_users_to_group, establish_order_chat,
prepare_order_group_chat, resolve_pair_group_id,
parse_pair_from_group_id, resolve_pair_from_order, check_pair_chat_permission,
query_pair_orders, order_to_chat_dict,
)
from utils.fadan_utils import check_fadan_qiangdan_eligible
from orders.utils import (
calc_shangjia_order_fencheng,
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from backend.utils import update_shangjia_daily, update_dashou_daily_by_action, pick_leixing_id, datetime_aliases, fmt_datetime
from rank.services import record_dashou_biaoxian
from rank.utils import check_dashou_biaoqian_required
from orders.notice_tasks import dingdan_guangbo
from ..models import (
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate,
CommissionRate, PlayerRating, RefundRecord,
)
from jituan.services.club_config import get_commission_rate, get_commission_rate_object
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_penalty_club_id, resolve_penalty_record_club_id
from users.fadan_fenhong_utils import lock_penalty_bonus
from users.fadan_fenhong_utils import lock_penalty_bonus
from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from users.models import UserDashou, UserShangjia, UserBoss
from users.business_models import User
from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
from config.models import (
ShangjiaLianjie
)
logger = logging.getLogger(__name__)
class DingdanHuoquView(APIView):
"""订单获取接口"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 获取当前登录用户
try:
current_user = request.user
yonghuid = current_user.UserUID
except Exception as e:
return Response({
'code': 1,
'msg': '用户认证失败',
'data': None
})
# 获取前端参数
try:
data = request.data
zhuangtai_list = data.get('zhuangtai_list', [])
page = int(data.get('page', 1))
page_size = int(data.get('page_size', 10))
except Exception as e:
return Response({
'code': 2,
'msg': '参数格式错误',
'data': None
})
# 参数验证
if not isinstance(zhuangtai_list, list) or len(zhuangtai_list) == 0:
return Response({
'code': 3,
'msg': '状态列表不能为空',
'data': None
})
if page < 1:
page = 1
if page_size <= 0:
page_size = 10
try:
# 查询扩展表获取用户的所有订单ID
pingtai_orders = PlatformOrderExt.query.filter(BossID=yonghuid)
pingtai_order_ids = list(pingtai_orders.values_list('Order__OrderID', flat=True))
if not pingtai_order_ids:
return Response({
'code': 0,
'msg': '成功',
'data': {
'list': [],
'has_more': False,
'total': 0
}
})
# 查询主表
status_query = Q()
if zhuangtai_list != [1, 2, 3, 4, 5, 6, 7, 8]:
status_query = Q(Status__in=zhuangtai_list)
query = Q(OrderID__in=pingtai_order_ids) & status_query
orders = Order.query.filter(query).order_by('-CreateTime')
#total_orders = orders.count()
# 手动分页逻辑
start_index = (page - 1) * page_size
end_index = start_index + page_size
# 获取当前页的数据
current_page_orders = orders[start_index:end_index]
# 构建返回数据
order_list = []
for order in current_page_orders:
order_data = {
'dingdanId': order.OrderID,
'zhuangtai': order.Status,
'jine': float(order.Amount) if order.Amount else 0.0,
'tupian': order.ImageURL or '',
'jieshao': order.Description or '',
'beizhu': order.Remark or '',
'nicheng': order.Nickname or '',
'chuangjianshijian': fmt_datetime(order.CreateTime),
'create_time': fmt_datetime(order.CreateTime),
}
order_list.append(order_data)
# 判断是否还有更多数据
current_page_count = len(current_page_orders)
has_more = current_page_count == page_size
return Response({
'code': 0,
'msg': '成功',
'data': {
'list': order_list,
'has_more': has_more,
#'total': total_orders
}
})
except Exception as e:
return Response({
'code': 4,
'msg': '服务器内部错误',
'data': None
})
class DingdanXiangqingView(APIView):
"""订单详情接口"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 获取当前登录用户
try:
current_user = request.user
yonghuid = current_user.UserUID
except Exception as e:
return Response({
'code': 1,
'msg': '用户认证失败',
'data': None
})
# 获取前端参数
try:
data = request.data
dingdan_id = data.get('dingdanId', '')
except Exception as e:
return Response({
'code': 2,
'msg': '参数格式错误',
'data': None
})
# 参数验证
if not dingdan_id:
return Response({
'code': 3,
'msg': '订单ID不能为空',
'data': None
})
try:
# 1. 验证订单是否属于当前用户
# 查询平台订单扩展表检查laoban_id是否等于当前用户
pingtai_kuozhan = PlatformOrderExt.query.filter(
BossID=yonghuid,
Order__OrderID=dingdan_id
).first()
if not pingtai_kuozhan:
# 如果不是平台订单,检查商家订单
shangjia_kuozhan = MerchantOrderExt.query.filter(
MerchantID=yonghuid,
Order__OrderID=dingdan_id
).first()
if not shangjia_kuozhan:
return Response({
'code': 4,
'msg': '订单不存在或无权限查看',
'data': None
})
# 2. 查询订单主表
dingdan = Order.query.filter(OrderID=dingdan_id).first()
if not dingdan:
return Response({
'code': 5,
'msg': '订单不存在',
'data': None
})
# 3. 获取订单信息
zhuangtai = dingdan.Status or 0
zhiding_id = dingdan.AssignedID or ''
jiedan_dashou_id = dingdan.PlayerID or ''
dashou_liuyan = dingdan.PlayerRemark or ''
# 4. 查询打手交付图片
dashoutupian_records = PlayerDeliveryImage.query.filter(
OrderID=dingdan_id
).order_by('CreateTime')[:9] # 最多取9条
dashoujiaofu_list = []
for record in dashoutupian_records:
if record.ImageURL:
dashoujiaofu_list.append(record.ImageURL)
# 5. 构建返回数据(字段名与前端对应)
response_data = {
'zhuangtai': zhuangtai, # 订单状态
'zhiding': zhiding_id, # 指定打手ID
'jiedan_dashou_id': jiedan_dashou_id, # 服务打手ID
'dashouliuyan': dashou_liuyan, # 打手留言
'dashoujiaofu': dashoujiaofu_list # 打手交付图片列表
}
# 6. 返回成功响应
return Response({
'code': 0,
'msg': '成功',
'data': response_data
})
except Exception as e:
print(f"订单详情查询失败: {str(e)}")
return Response({
'code': 6,
'msg': '服务器内部错误',
'data': None
})
class JiedanView(APIView):
"""结单接口"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 获取当前登录用户
try:
current_user = request.user
yonghuid = current_user.UserUID
except Exception as e:
return Response({
'code': 1,
'msg': '用户认证失败',
'data': None
})
# 获取前端参数
try:
data = request.data
dingdan_id = data.get('dingdanId', '')
pingfen = int(data.get('pingfen', 0)) # 评分默认0
liuyan = data.get('liuyan', '') # 留言,默认空
except Exception as e:
return Response({
'code': 2,
'msg': '参数格式错误',
'data': None
})
# 参数验证
if not dingdan_id:
return Response({
'code': 3,
'msg': '订单ID不能为空',
'data': None
})
# 验证评分范围
if pingfen < 0 or pingfen > 5:
return Response({
'code': 4,
'msg': '评分必须在0-5分之间',
'data': None
})
# 使用事务确保数据一致性
try:
with transaction.atomic():
# 1. 验证订单是否存在且属于当前用户
pingtai_kuozhan = PlatformOrderExt.query.filter(
BossID=yonghuid,
Order__OrderID=dingdan_id
).select_related('Order').first()
if not pingtai_kuozhan:
return Response({
'code': 5,
'msg': '订单不存在或无权限操作',
'data': None
})
dingdan = pingtai_kuozhan.Order
# 2. 验证订单状态(必须是结算中 - 状态8
if dingdan.Status != 8:
return Response({
'code': 6,
'msg': '订单不在结算状态,无法结单',
'data': None
})
# 3. 获取订单相关数据
dashou_fencheng = dingdan.PlayerCommission or Decimal('0.00') # 打手分成
jiedan_dashou_id = dingdan.PlayerID or '' # 接单打手ID
shangpin_id = dingdan.ProductID # 商品ID
# 4. 更新平台扩展表的老板评价(如果有留言)
if liuyan:
pingtai_kuozhan.BossReview = liuyan
pingtai_kuozhan.save()
# 5. 如果有评分,更新评分表
if pingfen > 0 and jiedan_dashou_id:
# 使用filter查询更健壮
pingfen_records = PlayerRating.query.filter(PlayerID=jiedan_dashou_id)
if pingfen_records.exists():
# 已有记录,更新
pingfen_record = pingfen_records.first()
pingfen_record.TotalScore += Decimal(str(pingfen))
pingfen_record.RatingCount += 1
pingfen_record.save()
else:
# 新记录,创建
PlayerRating.query.create(
PlayerID=jiedan_dashou_id,
TotalScore=Decimal(str(pingfen)),
RatingCount=1
)
# 6. 更新商品真实销量如果有商品ID
if shangpin_id:
shangpin_records = Shangpin.query.filter(id=shangpin_id)
if shangpin_records.exists():
shangpin = shangpin_records.first()
# 检查字段是否存在
if hasattr(shangpin, 'zhenshi_xiaoliang'):
shangpin.zhenshi_xiaoliang = (shangpin.zhenshi_xiaoliang or 0) + 1
shangpin.save()
# 7. 更新打手扩展表如果有打手ID和分成
if jiedan_dashou_id and dashou_fencheng > 0:
# 查询打手用户
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first()
if dashou_user:
# 查询打手扩展表
dashou_kuozhan = UserDashou.query.filter(user=dashou_user).first()
if dashou_kuozhan:
# 更新四个金额字段
dashou_kuozhan.zonge = (dashou_kuozhan.zonge or Decimal('0.00')) + dashou_fencheng
dashou_kuozhan.yue = (dashou_kuozhan.yue or Decimal('0.00')) + dashou_fencheng
dashou_kuozhan.jinrishouyi = (dashou_kuozhan.jinrishouyi or Decimal(
'0.00')) + dashou_fencheng
dashou_kuozhan.jinyueshouyi = (dashou_kuozhan.jinyueshouyi or Decimal(
'0.00')) + dashou_fencheng
# 成交总量加1
dashou_kuozhan.chengjiaozongliang = (dashou_kuozhan.chengjiaozongliang or 0) + 1
# 状态改为数字1
dashou_kuozhan.zhuangtai = 1
dashou_kuozhan.save()
# 8. 更新订单状态为已完成状态3
dingdan.Status = 3
dingdan.save()
# 🔥 新增:调用打手每日统计(成交)
try:
update_dashou_daily_by_action(
yonghuid=jiedan_dashou_id,
amount=dashou_fencheng,
action=2 # 2 表示成交(结算)
)
logger.info(f"打手 {jiedan_dashou_id} 每日统计更新成功")
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 不影响主流程
# ========== 新增:商品和店铺每日统计 ==========
try:
# 商品每日统计只要有商品ID就执行
if dingdan.ProductID:
# 从扩展表获取已计算好的收益值(下单时已存储)
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(dingdan, 'pingtai_kuozhan'):
pingtai_shouyi = dingdan.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_shangpin_daily_stat(
ProductID=dingdan.ProductID,
caozuo_leixing=2, # 2 = 结单
dingdan_jiage=dingdan.Amount,
PlatformIncome=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
logger.info(f"商品统计更新完成商品ID: {dingdan.ProductID}")
# 店铺每日统计仅当存在店铺ID时执行
# ========== 修改为 ==========
# 店铺每日统计(包含上级分红)
dianpu_id = None
shangji_id = None
shangji_fen = Decimal('0.00')
if hasattr(dingdan, 'pingtai_kuozhan'):
dianpu_id = dingdan.pingtai_kuozhan.ShopID
shangji_id = dingdan.pingtai_kuozhan.ParentShopID
shangji_fen = dingdan.pingtai_kuozhan.ParentBonus or Decimal('0.00')
if dianpu_id:
dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_dianpu_daily_stat(
ShopID=dianpu_id,
caozuo_leixing=2, # 结算
dingdan_jiage=dingdan.Amount,
ShopIncome=dianpu_shouyi,
shangji_ShopID=shangji_id, # 新增上级店铺ID
ParentBonus=shangji_fen # 新增:该单给上级的分红
)
logger.info(f"店铺统计更新完成店铺ID: {dianpu_id}, 上级ID: {shangji_id}, 上级分红: {shangji_fen}")
'''dianpu_id = None
if hasattr(dingdan, 'pingtai_kuozhan'):
dianpu_id = dingdan.pingtai_kuozhan.ShopID
if dianpu_id:
dianpu_shouyi = dingdan.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_dianpu_daily_stat(
ShopID=dianpu_id,
caozuo_leixing=2, # 2 = 结单
dingdan_jiage=dingdan.Amount,
ShopIncome=dianpu_shouyi
)
logger.info(f"店铺统计更新完成店铺ID: {dianpu_id}")'''
except Exception as e:
logger.error(f"商品/店铺统计更新失败: {e}")
logger.error(traceback.format_exc())
# 统计失败不影响主流程
# =============================================
# 9. 返回成功响应
return Response({
'code': 0,
'msg': '结单成功',
'data': None
})
except Exception as e:
# 事务会自动回滚
return Response({
'code': 7,
'msg': '结单失败,服务器内部错误',
'data': None
})
class DingdanXiangqingView2(APIView):
"""订单详情接口(老板端)- 返回完整评价信息"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
yonghuid = request.user.UserUID
except Exception:
return Response({'code': 1, 'msg': '用户认证失败', 'data': None})
dingdan_id = request.data.get('dingdanId', '')
if not dingdan_id:
return Response({'code': 2, 'msg': '订单ID不能为空', 'data': None})
try:
# 权限验证
pingtai_ext = PlatformOrderExt.query.filter(
BossID=yonghuid,
Order__OrderID=dingdan_id
).select_related('Order').first()
if not pingtai_ext:
return Response({'code': 3, 'msg': '订单不存在或无权限查看', 'data': None})
order = pingtai_ext.Order
zhuangtai = order.Status or 0
jiedan_dashou_id = order.PlayerID or ''
dashou_liuyan = order.PlayerRemark or ''
UpdateTime = order.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if order.UpdateTime else ''
# 交付图片
dashoujiaofu_list = list(
PlayerDeliveryImage.query.filter(OrderID=dingdan_id)
.order_by('CreateTime')[:9]
.values_list('ImageURL', flat=True)
)
# 打手信息
dashou_nicheng = ''
dashou_touxiang = ''
jiedan_time = ''
tijiao_time = ''
if jiedan_dashou_id:
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first()
if dashou_user:
dashou_touxiang = dashou_user.Avatar or ''
try:
dashou_nicheng = dashou_user.DashouProfile.nicheng or ''
except UserDashou.DoesNotExist:
pass
biaoxian = DashouBiaoxian.query.filter(
OrderID=dingdan_id,
dashou_id=jiedan_dashou_id
).first()
if biaoxian:
if biaoxian.jiedan_time:
jiedan_time = biaoxian.jiedan_time.strftime('%Y-%m-%d %H:%M:%S')
if biaoxian.tijiao_time:
tijiao_time = biaoxian.tijiao_time.strftime('%Y-%m-%d %H:%M:%S')
# 老板自己的评价(如果已经结算过,可能已有评价)
laoban_pingjia = pingtai_ext.BossReview or ''
shangxian_sudu = None
fuwu_taidu = None
fanche_cishu = None
if jiedan_dashou_id and zhuangtai in [3, 4, 5, 6]: # 已完成或退款相关状态才可能有评价
biaoxian = DashouBiaoxian.query.filter(
OrderID=dingdan_id,
dashou_id=jiedan_dashou_id
).first()
if biaoxian:
shangxian_sudu = biaoxian.shangxian_sudu
fuwu_taidu = biaoxian.fuwu_taidu
fanche_cishu = biaoxian.fanche_cishu
data = {
'zhuangtai': zhuangtai,
'dashou': jiedan_dashou_id,
'dashou_nicheng': dashou_nicheng,
'dashou_touxiang': dashou_touxiang,
'dashouliuyan': dashou_liuyan,
'dashoujiaofu': dashoujiaofu_list,
'UpdateTime': UpdateTime,
'jiedan_time': jiedan_time,
'tijiao_time': tijiao_time,
'laoban_pingjia': laoban_pingjia,
'shangxian_sudu': shangxian_sudu, # 1-5 或 null
'fuwu_taidu': fuwu_taidu, # 1-5 或 null
'fanche_cishu': fanche_cishu # int 或 null
}
return Response({'code': 0, 'msg': '成功', 'data': data})
except Exception as e:
logger.error(f"订单详情查询异常: {e}", exc_info=True)
return Response({'code': 4, 'msg': '服务器内部错误', 'data': None})
class JiedanView2(APIView):
"""结单接口(老板端)- 接收全新评价字段并写入表现表"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 用户认证
try:
current_user = request.user
yonghuid = current_user.UserUID
except Exception:
return Response({'code': 1, 'msg': '用户认证失败', 'data': None})
# 2. 获取参数(字段名与前端完全一致)
try:
data = request.data
dingdan_id = data.get('dingdanId', '')
fanche_cishu = data.get('fanche_cishu') # 前端传的字符串或数字,后端转为整数
shangxian_sudu = data.get('shangxian_sudu')
fuwu_taidu = data.get('fuwu_taidu')
liuyan = data.get('liuyan', '')
except Exception:
return Response({'code': 2, 'msg': '参数格式错误', 'data': None})
# 参数校验
if not dingdan_id:
return Response({'code': 3, 'msg': '订单ID不能为空', 'data': None})
# 转换并校验必填评价字段
try:
fanche_cishu = int(fanche_cishu) if fanche_cishu is not None else None
shangxian_sudu = int(shangxian_sudu) if shangxian_sudu is not None else None
fuwu_taidu = int(fuwu_taidu) if fuwu_taidu is not None else None
except (ValueError, TypeError):
return Response({'code': 4, 'msg': '评价参数必须为整数', 'data': None})
if fanche_cishu is None:
return Response({'code': 5, 'msg': '翻车次数不能为空', 'data': None})
if not (1 <= shangxian_sudu <= 5 and 1 <= fuwu_taidu <= 5):
return Response({'code': 6, 'msg': '评价分数必须在1-5之间', 'data': None})
# 使用事务保证数据一致性
try:
with transaction.atomic():
# 3. 权限验证:订单属于当前老板
pingtai_ext = PlatformOrderExt.query.filter(
BossID=yonghuid,
Order__OrderID=dingdan_id
).select_related('Order').first()
if not pingtai_ext:
return Response({'code': 7, 'msg': '订单不存在或无权限操作', 'data': None})
order = pingtai_ext.Order
# 4. 状态校验必须是结算中状态8
if order.Status != 8:
return Response({'code': 8, 'msg': '订单不在结算状态', 'data': None})
# 5. 提取订单关键数据
dashou_fencheng = order.PlayerCommission or Decimal('0.00')
jiedan_dashou_id = order.PlayerID or ''
shangpin_id = order.ProductID
# 6. 保存老板评价(兼容旧逻辑)
if liuyan:
pingtai_ext.BossReview = liuyan
pingtai_ext.save()
# 7. 更新评分表(用服务态度分作为综合评分,保留原有统计)
if jiedan_dashou_id and fuwu_taidu:
pingfen_records = PlayerRating.query.filter(PlayerID=jiedan_dashou_id)
if pingfen_records.exists():
pf = pingfen_records.first()
pf.TotalScore += Decimal(str(fuwu_taidu))
pf.RatingCount += 1
pf.save()
else:
PlayerRating.query.create(
PlayerID=jiedan_dashou_id,
TotalScore=Decimal(str(fuwu_taidu)),
RatingCount=1
)
# 8. 更新商品真实销量
if shangpin_id:
shangpin_qs = Shangpin.query.filter(id=shangpin_id)
if shangpin_qs.exists():
sp = shangpin_qs.first()
if hasattr(sp, 'zhenshi_xiaoliang'):
sp.zhenshi_xiaoliang = (sp.zhenshi_xiaoliang or 0) + 1
sp.save()
# 9. 更新打手收益与状态
if jiedan_dashou_id and dashou_fencheng > 0:
dashou_user = User.query.filter(UserUID=jiedan_dashou_id).first()
if dashou_user:
try:
dashou_ext = dashou_user.DashouProfile
dashou_ext.zonge = (dashou_ext.zonge or Decimal('0.00')) + dashou_fencheng
dashou_ext.yue = (dashou_ext.yue or Decimal('0.00')) + dashou_fencheng
dashou_ext.jinrishouyi = (dashou_ext.jinrishouyi or Decimal('0.00')) + dashou_fencheng
dashou_ext.jinyueshouyi = (dashou_ext.jinyueshouyi or Decimal('0.00')) + dashou_fencheng
dashou_ext.chengjiaozongliang = (dashou_ext.chengjiaozongliang or 0) + 1
dashou_ext.zhuangtai = 1
dashou_ext.save()
except UserDashou.DoesNotExist:
pass
# 10. 更新订单状态为已完成
order.Status = 3
order.save()
# 11. 打手每日统计(成交)
try:
update_dashou_daily_by_action(
UserID=jiedan_dashou_id,
amount=dashou_fencheng,
action=2 # 2=结算/成交
)
except Exception as e:
logger.error(f"打手每日统计失败: {e}")
# 12. 商品/店铺每日统计
try:
if shangpin_id:
# 从扩展表取已计算收益
pingtai_shouyi = Decimal('0.00')
dianpu_shouyi = Decimal('0.00')
if hasattr(order, 'pingtai_kuozhan'):
pingtai_shouyi = order.pingtai_kuozhan.PlatformIncome or Decimal('0.00')
dianpu_shouyi = order.pingtai_kuozhan.ShopIncome or Decimal('0.00')
update_shangpin_daily_stat(
ProductID=shangpin_id,
caozuo_leixing=2,
dingdan_jiage=order.Amount,
PlatformIncome=pingtai_shouyi,
dianpu_zongshouyi=dianpu_shouyi
)
# 店铺统计(含上级分红)
if hasattr(order, 'pingtai_kuozhan'):
dianpu_id = order.pingtai_kuozhan.ShopID
shangji_id = order.pingtai_kuozhan.ParentShopID
shangji_fen = order.pingtai_kuozhan.ParentBonus or Decimal('0.00')
if dianpu_id:
update_dianpu_daily_stat(
ShopID=dianpu_id,
caozuo_leixing=2,
dingdan_jiage=order.Amount,
ShopIncome=dianpu_shouyi,
shangji_ShopID=shangji_id,
ParentBonus=shangji_fen
)
except Exception as e:
logger.error(f"商品/店铺统计失败: {e}")
# 14. 🆕 写入打手表现表(结算评价)
if jiedan_dashou_id:
record_dashou_biaoxian(
dingdan_id=dingdan_id,
dashou_id=jiedan_dashou_id,
xingwei=3, # 3=结算
fanche_RatingCount=fanche_cishu,
shangxian_sudu=shangxian_sudu,
fuwu_taidu=fuwu_taidu
# bankuai 暂不传入,后续可从订单商品关联
)
# 15. 返回成功
return Response({'code': 0, 'msg': '结单成功', 'data': None})
except Exception as e:
logger.error(f"结单事务异常: {e}", exc_info=True)
return Response({'code': 9, 'msg': '服务器内部错误,结单失败', 'data': None})

541
orders/views/chat.py Normal file
View File

@@ -0,0 +1,541 @@
"""orders.views.chat - auto-generated by split script."""
# ==================== 标准库 ====================
import hmac
import os
import json
import time
import random
import string
import traceback
import threading
import requests
import hashlib
import xml.etree.ElementTree as ET
import xmltodict
import logging
from decimal import Decimal
from django.conf import settings
from django.db import models, transaction
from gvsdsdk.fluent import db, func, FQ
from django.db.models import F, Q, OuterRef, Subquery
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.authentication import JWTAuthentication
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sts.v20180813 import sts_client, models
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.money import yuan_to_fen
from utils.chat_utils import (
_send_group_message, _subscribe_users_to_group, establish_order_chat,
prepare_order_group_chat, resolve_pair_group_id,
parse_pair_from_group_id, resolve_pair_from_order, check_pair_chat_permission,
query_pair_orders, order_to_chat_dict,
)
from utils.fadan_utils import check_fadan_qiangdan_eligible
from orders.utils import (
calc_shangjia_order_fencheng,
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from backend.utils import update_shangjia_daily, update_dashou_daily_by_action, pick_leixing_id, datetime_aliases, fmt_datetime
from rank.services import record_dashou_biaoxian
from rank.utils import check_dashou_biaoqian_required
from orders.notice_tasks import dingdan_guangbo
from ..models import (
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate,
CommissionRate, PlayerRating, RefundRecord,
)
from jituan.services.club_config import get_commission_rate, get_commission_rate_object
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_penalty_club_id, resolve_penalty_record_club_id
from users.fadan_fenhong_utils import lock_penalty_bonus
from users.fadan_fenhong_utils import lock_penalty_bonus
from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from users.models import UserDashou, UserShangjia, UserBoss
from users.business_models import User
from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
from config.models import (
ShangjiaLianjie
)
logger = logging.getLogger(__name__)
def get_self_goeasy_appkey():
try:
return getattr(settings, 'GOEASY_APPKEY', None)
except Exception:
return None
def get_self_goeasy_secret():
try:
return getattr(settings, 'GOEASY_SECRET', None)
except Exception:
return None
def get_self_storage_domain():
try:
return getattr(settings, 'COS_DOMAIN', '')
except Exception:
return ''
def full_avatar(relative_url, bucket_domain=None):
if not relative_url:
return ''
if relative_url.startswith('http'):
return relative_url
domain = bucket_domain or get_self_storage_domain()
if domain:
domain = domain.rstrip('/') + '/'
return domain + relative_url.lstrip('/')
return relative_url
# ----------------- GoEasy API 封装 -----------------
def subscribe_users_to_group(user_ids, group_ids, appkey=None, secret=None):
"""订阅用户到群组,确保能收发该群消息"""
if not appkey:
appkey = get_self_goeasy_appkey()
if not secret:
secret = get_self_goeasy_secret()
if not appkey:
logger.error("订阅失败GoEasy AppKey 未配置")
return False
url = 'https://rest-hangzhou.goeasy.io/v2/im/subscribe-groups'
body = {"appkey": appkey, "userIds": user_ids, "groupIds": group_ids}
headers = {"Content-Type": "application/json"}
if secret:
headers["Authorization"] = f"Bearer {secret}"
try:
r = requests.post(url, headers=headers, json=body, timeout=10)
if r.status_code == 200:
logger.info(f"订阅成功:用户 {user_ids} -> 群 {group_ids}")
return True
logger.error(f"订阅失败:{r.status_code} {r.text}")
return False
except Exception as e:
logger.exception("订阅异常")
return False
def send_group_message(
appkey, secret, group_id, sender_id, sender_name, sender_avatar,
message_text='', custom_payload=None, group_name='', order_id=''
):
"""
通过 GoEasy REST API 发送群消息。
message_text: 普通文本内容
custom_payload: 自定义消息体(如订单卡片),此时 type 设为 'order'
"""
url = 'https://rest-hangzhou.goeasy.io/v2/im/message'
to_data = {
"name": group_name or group_id,
"avatar": sender_avatar or '',
"orderId": order_id,
}
msg_type = 'order' if custom_payload else 'text'
payload = custom_payload if custom_payload else message_text
body = {
"appkey": appkey,
"senderId": sender_id,
"senderData": {"avatar": sender_avatar, "name": sender_name},
"to": {"type": "group", "id": group_id, "data": to_data},
"type": msg_type,
"payload": payload
}
headers = {"Content-Type": "application/json"}
if secret:
headers["Authorization"] = f"Bearer {secret}"
for attempt in range(3): # 最多重试3次
try:
r = requests.post(url, headers=headers, json=body, timeout=10)
if r.status_code == 200:
logger.info(f"消息发送成功sender={sender_id}, group={group_id}, type={msg_type}")
return True
logger.warning(f"消息发送失败 (尝试 {attempt+1}/3):状态码 {r.status_code}, 响应 {r.text}")
except Exception as e:
logger.warning(f"消息发送异常 (尝试 {attempt+1}/3){e}")
if attempt < 2:
time.sleep(1)
logger.error(f"消息最终发送失败sender={sender_id}, group={group_id}")
return False
# ----------------- 权限校验 -----------------
def check_user_permission(identity_type, uid, order):
"""验证用户身份、账号状态、以及是否属于该订单"""
try:
user = User.query.get(UserUID=uid)
except User.DoesNotExist:
return False, '用户不存在'
if identity_type == 'PlayerID':
try:
dashou = UserDashou.query.get(user=user)
if dashou.zhanghaozhuangtai != 1 :
return False, '打手账号已被封禁或状态异常'
except UserDashou.DoesNotExist:
return False, '该用户不是打手'
if order.PlayerID != uid:
return False, '你不是该订单的接单打手'
elif identity_type == 'shangjia':
try:
shop = UserShangjia.query.get(user=user)
if shop.zhuangtai != 1:
return False, '商家账号已被禁用'
except UserShangjia.DoesNotExist:
return False, '该用户不是商家'
try:
if order.shangjia_kuozhan.MerchantID != uid:
return False, '你不是该订单的发布商家'
except MerchantOrderExt.DoesNotExist:
return False, '订单缺少商家信息'
elif identity_type == 'boss':
try:
UserBoss.query.get(user=user) # 老板暂无封禁字段,仅需存在
except UserBoss.DoesNotExist:
return False, '该用户不是老板'
try:
if order.pingtai_kuozhan.BossID != uid:
return False, '你不是该订单的老板'
except PlatformOrderExt.DoesNotExist:
return False, '订单缺少平台信息'
else:
return False, '不支持的身份类型'
return True, ''
# ----------------- 主视图 -----------------
class DaiLiQunLiaoXiaoXiView(APIView):
"""
代理发送群聊消息
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
# ===== 1. 参数解析 =====
order_id = request.data.get('orderId')
identity_type = request.data.get('identityType') # dashou / shangjia / boss
sender_name = request.data.get('senderName', '')
sender_avatar_raw = request.data.get('senderAvatar', '')
message_type = request.data.get('messageType', 'text')
message_text = request.data.get('text', '')
order_payload = request.data.get('orderPayload', None)
logger.info(f"[群聊代理] 收到请求: orderId={order_id}, identityType={identity_type}, "
f"messageType={message_type}, text={message_text[:20]}...")
if not order_id or not identity_type:
return Response({'code': 400, 'msg': '缺少 orderId 或 identityType'})
if identity_type not in ('PlayerID', 'shangjia', 'boss'):
return Response({'code': 400, 'msg': 'identityType 无效'})
# ===== 2. 构造 senderId =====
uid = request.user.UserUID
prefix_map = {'PlayerID': 'Ds', 'shangjia': 'Sj', 'boss': 'Boss'}
sender_id = prefix_map[identity_type] + uid
logger.info(f"[群聊代理] 登录用户 UserID={uid}, 构造 senderId={sender_id}")
# ===== 3. 查询订单 =====
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=order_id)
except Order.DoesNotExist:
logger.warning(f"[群聊代理] 订单 {order_id} 不存在")
return Response({'code': 404, 'msg': '订单不存在'})
# ===== 4. 权限校验 =====
valid, err_msg = check_user_permission(identity_type, uid, order)
if not valid:
logger.warning(f"[群聊代理] 权限校验失败: {err_msg}, uid={uid}, identity={identity_type}")
return Response({'code': 403, 'msg': err_msg})
# ===== 5. 我方 GoEasy 配置 =====
self_appkey = get_self_goeasy_appkey()
self_secret = get_self_goeasy_secret()
if not self_appkey:
logger.error("[群聊代理] 我方 GoEasy AppKey 未配置")
return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'})
# ===== 6. 公共群信息(新单走配对群,与文赫一致) =====
real_group_id = resolve_pair_group_id(order) or f"group_{order.OrderID}"
group_name = (order.Description[:20] + '') if order.Description and len(order.Description) > 20 else f"订单{order.OrderID}"
sender_avatar = full_avatar(sender_avatar_raw) if sender_avatar_raw else full_avatar('')
# ===== 7. 订阅发送者到群 =====
if not subscribe_users_to_group([sender_id], [real_group_id], self_appkey, self_secret):
logger.error("[群聊代理] 订阅群组失败")
return Response({'code': 500, 'msg': '订阅群组失败'})
custom_payload = order_payload if message_type == 'order' else None
text_content = message_text
# ---------- 向我方群发送消息 ----------
local_ok = send_group_message(
appkey=self_appkey,
secret=self_secret,
group_id=real_group_id,
sender_id=sender_id,
sender_name=sender_name,
sender_avatar=sender_avatar,
message_text=text_content,
custom_payload=custom_payload,
group_name=group_name,
order_id=order.OrderID,
)
if not local_ok:
return Response({'code': 500, 'msg': '消息发送失败'})
return Response({'code': 200, 'msg': '发送成功'})
class ShangjiaLianjieLiuYanZhuanFaView(APIView):
"""
商家链接留言转发接口
路径POST /dingdan/sjljlbxxzf
固定以 B{商家ID} 身份发送群消息。
"""
permission_classes = []
def post(self, request):
token = request.data.get('token', '').strip()
dingdan_id = str(request.data.get('dingdan_id') or request.data.get('dingdanId') or '').strip()
text = (request.data.get('text') or '').strip()
if not text or (not token and not dingdan_id):
return Response({'code': 400, 'msg': '缺少参数'})
if token:
try:
lianjie = ShangjiaLianjie.query.get(LinkToken=token)
except ShangjiaLianjie.DoesNotExist:
return Response({'code': 404, 'msg': '链接不存在或已失效'})
else:
try:
lianjie = ShangjiaLianjie.query.get(OrderID=dingdan_id)
except ShangjiaLianjie.DoesNotExist:
return Response({'code': 404, 'msg': '订单链接不存在或已失效'})
if not lianjie.OrderID:
return Response({'code': 400, 'msg': '链接缺少订单ID'})
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=lianjie.OrderID)
except Order.DoesNotExist:
return Response({'code': 500, 'msg': '订单数据异常'})
shangjia_uid = lianjie.UserID
sender_id = f"B{shangjia_uid}"
sender_name = "老板"
sender_avatar = ''
appkey = get_self_goeasy_appkey()
secret = get_self_goeasy_secret()
if not appkey:
return Response({'code': 500, 'msg': '系统配置错误(APPKEY)'})
group_id = resolve_pair_group_id(order) or f"group_{order.OrderID}"
group_name = (order.Description[:20] + '') if order.Description and len(order.Description) > 20 else f"订单{order.OrderID}"
if not _subscribe_users_to_group([sender_id], [group_id], appkey, secret):
return Response({'code': 500, 'msg': '订阅群组失败'})
local_ok = _send_group_message(
appkey=appkey,
secret=secret,
group_id=group_id,
sender_id=sender_id,
sender_name=sender_name,
sender_avatar=sender_avatar,
message_text=text,
custom_payload=None,
group_name=group_name,
order_id=order.OrderID,
)
if not local_ok:
return Response({'code': 500, 'msg': '消息发送失败'})
return Response({'code': 200, 'msg': '发送成功'})
class LianTongDuiHuaZhunBeiView(APIView):
"""进入订单群聊前准备:返回配对 groupId 及双方资料,并订阅 GoEasy 群(不影响抢单校验)"""
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
IDENTITY_MAP = {
'dashou': 'PlayerID',
'shangjia': 'shangjia',
'normal': 'boss',
'boss': 'boss',
}
def post(self, request):
dingdan_id = request.data.get('dingdan_id') or request.data.get('orderId')
identity_type = request.data.get('identityType') or ''
if not dingdan_id:
return Response({'code': 400, 'msg': '缺少 dingdan_id'})
backend_identity = self.IDENTITY_MAP.get(identity_type, identity_type)
if backend_identity not in ('PlayerID', 'shangjia', 'boss'):
return Response({'code': 400, 'msg': 'identityType 无效'})
uid = request.user.UserUID
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
valid, err_msg = check_user_permission(backend_identity, uid, order)
if not valid:
return Response({'code': 403, 'msg': err_msg})
if backend_identity == 'PlayerID' and not order.PlayerID:
return Response({'code': 400, 'msg': '订单尚未接单,暂无法进入群聊'})
data, subscribed = prepare_order_group_chat(order, backend_identity, uid, subscribe=True)
if not subscribed:
logger.warning(f'ltdhzb 订阅群失败 order={dingdan_id} uid={uid}')
return Response({'code': 0, 'msg': 'ok', 'data': data})
class LianTongPeiDuiDingDanLieBiaoView(APIView):
"""聊天页:获取打手与商家/老板之间的历史订单列表(可搜索)"""
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
IDENTITY_MAP = {
'dashou': 'PlayerID',
'shangjia': 'shangjia',
'normal': 'boss',
'boss': 'boss',
}
def post(self, request):
group_id = request.data.get('groupId') or request.data.get('group_id')
dingdan_id = request.data.get('dingdan_id') or request.data.get('orderId')
keyword = (request.data.get('keyword') or '').strip()
identity_type = request.data.get('identityType') or ''
backend_identity = self.IDENTITY_MAP.get(identity_type, identity_type)
if backend_identity not in ('PlayerID', 'shangjia', 'boss'):
return Response({'code': 400, 'msg': 'identityType 无效'})
dashou_uid, partner_uid, partner_role = parse_pair_from_group_id(group_id)
if not dashou_uid and dingdan_id:
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
dashou_uid, partner_uid, partner_role = resolve_pair_from_order(order)
if not dashou_uid or not partner_uid:
return Response({'code': 400, 'msg': '无法解析配对信息,请确认订单已接单'})
uid = request.user.UserUID
valid, err_msg = check_pair_chat_permission(backend_identity, uid, dashou_uid, partner_uid, partner_role)
if not valid:
return Response({'code': 403, 'msg': err_msg})
orders = query_pair_orders(dashou_uid, partner_uid, partner_role, keyword=keyword, limit=40)
latest = orders[0] if orders else None
return Response({'code': 0, 'msg': 'ok', 'data': {'list': orders, 'latest': latest}})
class LianTongDingDanZhuangTaiView(APIView):
"""聊天页:刷新单个订单状态"""
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
IDENTITY_MAP = LianTongPeiDuiDingDanLieBiaoView.IDENTITY_MAP
def post(self, request):
dingdan_id = request.data.get('dingdan_id') or request.data.get('orderId')
identity_type = request.data.get('identityType') or ''
if not dingdan_id:
return Response({'code': 400, 'msg': '缺少 dingdan_id'})
backend_identity = self.IDENTITY_MAP.get(identity_type, identity_type)
if backend_identity not in ('PlayerID', 'shangjia', 'boss'):
return Response({'code': 400, 'msg': 'identityType 无效'})
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
valid, err_msg = check_user_permission(backend_identity, request.user.UserUID, order)
if not valid:
return Response({'code': 403, 'msg': err_msg})
data = order_to_chat_dict(order)
return Response({'code': 0, 'msg': 'ok', 'data': data})
class LianTongDingDanZhuangTaiPiLiangView(APIView):
"""消息列表:批量刷新订单状态"""
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
def post(self, request):
order_ids = request.data.get('order_ids') or []
if not isinstance(order_ids, list):
return Response({'code': 400, 'msg': 'order_ids 格式错误'})
uid = request.user.UserUID
result = {}
for oid in order_ids[:20]:
oid = str(oid).strip()
if not oid:
continue
try:
order = Order.query.select_related('pingtai_kuozhan', 'shangjia_kuozhan').get(OrderID=oid)
except Order.DoesNotExist:
continue
for identity in ('PlayerID', 'shangjia', 'boss'):
valid, _ = check_user_permission(identity, uid, order)
if valid:
d = order_to_chat_dict(order)
result[oid] = {
'jieshao': d['jieshao'],
'jine': d['jine'],
'zhuangtai': d['zhuangtai'],
'zhuangtaiText': d['zhuangtaiText'],
}
break
return Response({'code': 0, 'msg': 'ok', 'data': result})

1513
orders/views/dashou.py Normal file

File diff suppressed because it is too large Load Diff

1408
orders/views/merchant.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,378 @@
"""orders.views.merchant_penalty - auto-generated by split script."""
# ==================== 标准库 ====================
import hmac
import os
import json
import time
import random
import string
import traceback
import threading
import requests
import hashlib
import xml.etree.ElementTree as ET
import xmltodict
import logging
from decimal import Decimal
from django.conf import settings
from django.db import models, transaction
from gvsdsdk.fluent import db, func, FQ
from django.db.models import F, Q, OuterRef, Subquery
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions, status
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.authentication import JWTAuthentication
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.sts.v20180813 import sts_client, models
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.money import yuan_to_fen
from utils.chat_utils import (
_send_group_message, _subscribe_users_to_group, establish_order_chat,
prepare_order_group_chat, resolve_pair_group_id,
parse_pair_from_group_id, resolve_pair_from_order, check_pair_chat_permission,
query_pair_orders, order_to_chat_dict,
)
from utils.fadan_utils import check_fadan_qiangdan_eligible
from orders.utils import (
calc_shangjia_order_fencheng,
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from shop.utils import calculate_pingtai_and_dianpu_shouyi, validate_shangpin_and_dianpu, update_dianpu_daily_stat
from products.utils import update_shangpin_daily_stat
from backend.utils import update_shangjia_daily, update_dashou_daily_by_action, pick_leixing_id, datetime_aliases, fmt_datetime
from rank.services import record_dashou_biaoxian
from rank.utils import check_dashou_biaoqian_required
from orders.notice_tasks import dingdan_guangbo
from ..models import (
Order, MerchantOrderExt, PlatformOrderExt, PlayerDeliveryImage,
PenaltyRecord, PenaltyEvidenceImage, Penalty, PenaltyAppealImage, PenaltyBonus, PenaltyBonusRate,
CommissionRate, PlayerRating, RefundRecord,
)
from jituan.services.club_config import get_commission_rate, get_commission_rate_object
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_user import get_payment_openid
from jituan.services.club_penalty import resolve_penalty_club_id, resolve_penalty_record_club_id
from users.fadan_fenhong_utils import lock_penalty_bonus
from users.fadan_fenhong_utils import lock_penalty_bonus
from products.models import Shangpin, ShangpinLeixing, Huiyuangoumai
from users.models import UserDashou, UserShangjia, UserBoss
from users.business_models import User
from rank.models import DashouBiaoxian, Chenghao, DingdanBiaoqian, YonghuChenghao
from config.models import (
ShangjiaLianjie
)
logger = logging.getLogger(__name__)
# ===================== 视图:商家罚款申请 =====================
# 文件路径dingdan/views.py 或单独文件,根据项目结构放置
class ShangjiaFakuanApplyView(APIView):
"""
商家罚款申请接口
路径POST /dingdan/sjfksq
权限JWT认证的商家用户
参数:
dingdan_id - 订单ID (必填)
chufa_liyou - 罚款原因 (必填)
fakuanjine - 罚款金额,元 (必填)
yingxiang_qiangdan - 是否影响抢单 1是 0否 (必填)
说明:
1. 自动从订单中获取被打手ID防止商家篡改目标。
2. 同一订单同一打手不允许重复罚款。
3. 跨平台订单若为我方派单,则通知对方平台处罚;否则本地生成罚单。
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# ========== 1. 身份验证 ==========
current_user = request.user
try:
shangjia_id = current_user.UserUID # 商家的用户ID
except AttributeError:
return Response({'code': 1, 'msg': '用户认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# ========== 2. 参数提取与校验 ==========
dingdan_id = str(request.data.get('OrderID') or request.data.get('dingdan_id') or '').strip()
chufa_liyou = request.data.get('chufa_liyou', '').strip()
fakuanjine = request.data.get('fakuanjine', 0)
yingxiang_qiangdan = int(request.data.get('yingxiang_qiangdan', 1))
evidence_urls = request.data.get('evidence_urls') or []
if not dingdan_id or not chufa_liyou or not fakuanjine:
return Response({'code': 2, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
if not isinstance(evidence_urls, list):
return Response({'code': 2, 'msg': '证据图片格式错误'}, status=status.HTTP_400_BAD_REQUEST)
evidence_urls = [str(u).strip() for u in evidence_urls if str(u).strip()]
if len(evidence_urls) > 9:
return Response({'code': 2, 'msg': '证据图片最多9张'}, status=status.HTTP_400_BAD_REQUEST)
if len(evidence_urls) < 1:
return Response({'code': 2, 'msg': '请至少上传1张证据图片'}, status=status.HTTP_400_BAD_REQUEST)
try:
fakuanjine = float(fakuanjine)
if fakuanjine <= 0:
raise ValueError
except (ValueError, TypeError):
return Response({'code': 2, 'msg': '罚款金额格式错误'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 3. 获取订单,校验归属商家 ==========
try:
# 关联查询商家扩展表,确保订单属于当前商家
shangjia_kuozhan = MerchantOrderExt.query.select_related('Order').get(
Order__OrderID=dingdan_id,
MerchantID=shangjia_id
)
order = shangjia_kuozhan.Order
except MerchantOrderExt.DoesNotExist:
return Response({'code': 3, 'msg': '订单不存在或您无权操作'}, status=status.HTTP_403_FORBIDDEN)
# ========== 4. 获取被打手ID ==========
dashou_id = order.PlayerID
if not dashou_id:
return Response({'code': 4, 'msg': '该订单暂无接单打手,无法罚款'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 5. 重复罚款检查 ==========
if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=dingdan_id).exists():
return Response({'code': 5, 'msg': '该打手在此订单已被罚款,不能重复申请'}, status=status.HTTP_400_BAD_REQUEST)
# ========== 6. 本地创建罚单 ==========
staff_kefu_id = None
staff_member = getattr(request, '_staff_member', None)
if staff_member is not None:
staff_kefu_id = getattr(staff_member, 'staff_user_id', None) or None
from utils.penalty_status import PENALTY_PLATFORM_AUDIT
with transaction.atomic():
fadan = Penalty.query.create(
PenalizedUserID=dashou_id,
ApplicantID=shangjia_id, # 申请人为当前商家
Identity=1, # 被打者身份1=打手
Reason=chufa_liyou,
FineAmount=fakuanjine,
RelatedOrderID=dingdan_id,
Status=PENALTY_PLATFORM_AUDIT, # 平台审核中,通过后变为待缴纳
AffectsGrabbing=yingxiang_qiangdan,
ApplicantIdentity=5, # 申请人是商家
ClubID=resolve_penalty_club_id(
order=order,
penalized_user_id=dashou_id,
request=request,
applicant_id=shangjia_id,
user=current_user,
),
ShopStaffUserID=staff_kefu_id,
)
for url in evidence_urls:
PenaltyAppealImage.query.create(
Penalty=fadan,
PenalizedUserID=dashou_id,
ImageURL=url,
Purpose=1,
)
lock_penalty_bonus(fadan)
logger.info(f"商家{shangjia_id}对订单{dingdan_id}打手{dashou_id}提交罚款申请(平台审核),金额{fakuanjine}")
return Response({'code': 0, 'msg': '罚款申请已提交,等待平台审核', 'data': {'fadan_id': fadan.id}})
class ShangjiaFakuaiXiugaiView(APIView):
"""
商家端统一修改接口:罚款修改/撤销/再次申请,订单退款撤销/修改理由
URL: /dingdan/sjfkxgycx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
# 验证商家身份
try:
shangjia = user.ShopProfile
except UserShangjia.DoesNotExist:
return Response({'code': 403, 'msg': '您不是商家'}, status=status.HTTP_403_FORBIDDEN)
if shangjia.zhuangtai != 1:
return Response({'code': 403, 'msg': '商家状态异常'}, status=status.HTTP_403_FORBIDDEN)
operation = request.data.get('operation')
dingdan_id = request.data.get('dingdan_id') or request.data.get('OrderID')
if not dingdan_id:
return Response({'code': 400, 'msg': '缺少订单ID'}, status=status.HTTP_400_BAD_REQUEST)
# 验证订单属于该商家
try:
dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
if not hasattr(dingdan, 'shangjia_kuozhan') or dingdan.shangjia_kuozhan.MerchantID != user.UserUID:
return Response({'code': 403, 'msg': '无权操作此订单'}, status=status.HTTP_403_FORBIDDEN)
# 处理订单相关操作
if operation == 'cancel_refund':
return self._cancel_refund(dingdan, request.data.get('liyou', ''))
elif operation == 'modify_refund_reason':
return self._modify_refund_reason(dingdan, request.data.get('liyou', ''))
# 处理罚款相关操作
fadan_id = request.data.get('fadan_id')
if not fadan_id:
return Response({'code': 400, 'msg': '缺少罚单ID'}, status=status.HTTP_400_BAD_REQUEST)
try:
fadan = Penalty.query.get(id=fadan_id)
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在'}, status=status.HTTP_404_NOT_FOUND)
if fadan.RelatedOrderID != dingdan_id:
return Response({'code': 403, 'msg': '罚单与订单不匹配'}, status=status.HTTP_403_FORBIDDEN)
if operation == 'modify_penalty':
return self._modify_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine'), user.UserUID)
elif operation == 'cancel_penalty':
return self._cancel_penalty(fadan, request.data.get('liyou', ''), user.UserUID)
elif operation == 'resubmit_penalty':
return self._resubmit_penalty(fadan, request.data.get('liyou', ''), request.data.get('jine'), user.UserUID)
elif operation == 'manage_penalty_evidence':
return self._manage_penalty_evidence(fadan, request.data)
else:
return Response({'code': 400, 'msg': '无效的操作类型'}, status=status.HTTP_400_BAD_REQUEST)
def _cancel_refund(self, dingdan, liyou):
"""撤销退款申请状态4 → 状态8清空退款理由"""
if dingdan.Status != 4:
return Response({'code': 400, 'msg': '只有退款中的订单可以撤销退款申请'})
with transaction.atomic():
dingdan.Status = 8
dingdan.RefundReason = '' # 清空退款理由
dingdan.save()
# 记录撤销理由可存入日志(如需要可另建日志表,此处简单处理)
logger.info(f"商家撤销退款订单 {dingdan.OrderID},理由: {liyou}")
return Response({'code': 0, 'msg': '已撤销退款申请'})
def _modify_refund_reason(self, dingdan, liyou):
"""修改退款理由仅状态4时"""
if dingdan.Status != 4:
return Response({'code': 400, 'msg': '只有退款中的订单可以修改退款理由'})
if not liyou.strip():
return Response({'code': 400, 'msg': '退款理由不能为空'})
dingdan.RefundReason = liyou[:500]
dingdan.save()
return Response({'code': 0, 'msg': '退款理由修改成功'})
def _modify_penalty(self, fadan, liyou, jine_str, processor_id=None):
"""修改罚款(待缴纳 / 申诉中 / 平台审核中)"""
from utils.penalty_status import PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT
if fadan.Status not in (PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT):
return Response({'code': 400, 'msg': '当前罚单状态不可修改'})
if not liyou.strip():
return Response({'code': 400, 'msg': '罚款理由不能为空'})
try:
jine = Decimal(jine_str)
if jine <= 0:
raise ValueError
except:
return Response({'code': 400, 'msg': '罚款金额必须为正数'})
with transaction.atomic():
fadan.Reason = liyou[:500]
fadan.FineAmount = jine
if processor_id:
fadan.ProcessorID = processor_id
fadan.ProcessorIdentity = 5 # 小程序商家自行处理
fadan.save()
lock_penalty_bonus(fadan)
return Response({'code': 0, 'msg': '罚款修改成功'})
def _cancel_penalty(self, fadan, liyou, processor_id=None):
"""撤销罚款(待缴纳 / 申诉中 / 平台审核中 → 已驳回)"""
from utils.penalty_status import PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT, PENALTY_REJECTED
if fadan.Status not in (PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT):
return Response({'code': 400, 'msg': '当前罚单状态不可撤销'})
if not liyou.strip():
return Response({'code': 400, 'msg': '驳回理由不能为空'})
with transaction.atomic():
fadan.Status = PENALTY_REJECTED # 已驳回
fadan.RejectReason = liyou[:500]
if processor_id:
fadan.ProcessorID = processor_id
fadan.ProcessorIdentity = 5 # 小程序商家撤销=商家作为处理人
fadan.save()
return Response({'code': 0, 'msg': '罚款已撤销(驳回)'})
def _resubmit_penalty(self, fadan, liyou, jine_str, processor_id=None):
"""再次申请罚款(已驳回 → 平台审核中)"""
from utils.penalty_status import PENALTY_REJECTED, PENALTY_PLATFORM_AUDIT
if fadan.Status != PENALTY_REJECTED:
return Response({'code': 400, 'msg': '只有已驳回的罚单可再次申请'})
if not liyou.strip():
return Response({'code': 400, 'msg': '罚款理由不能为空'})
try:
jine = Decimal(jine_str)
if jine <= 0:
raise ValueError
except:
return Response({'code': 400, 'msg': '罚款金额必须为正数'})
with transaction.atomic():
fadan.Status = PENALTY_PLATFORM_AUDIT
fadan.Reason = liyou[:500]
fadan.FineAmount = jine
fadan.RejectReason = '' # 清空驳回理由
if processor_id:
fadan.ProcessorID = processor_id
fadan.ProcessorIdentity = 5
fadan.save()
lock_penalty_bonus(fadan)
logger.info(f"商家重新申请罚单{fadan.id},金额={jine}")
return Response({'code': 0, 'msg': '已重新申请罚款'})
def _manage_penalty_evidence(self, fadan, data):
"""管理罚单证据图片Purpose=1与申诉图片严格隔离"""
from utils.penalty_status import PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT
if fadan.Status not in (PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_PLATFORM_AUDIT):
return Response({'code': 400, 'msg': '当前罚单状态不可修改证据图片'})
add_urls = data.get('add_urls') or []
remove_ids = data.get('remove_ids') or []
if not add_urls and not remove_ids:
return Response({'code': 400, 'msg': '未提供图片变更'})
with transaction.atomic():
if remove_ids:
PenaltyAppealImage.query.filter(
Penalty=fadan, Purpose=1, id__in=remove_ids
).delete()
existing_count = PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=1).count()
if existing_count + len(add_urls) > 9:
return Response({'code': 400, 'msg': '证据图片最多9张'})
for url in add_urls:
url = str(url).strip()
if not url:
continue
PenaltyAppealImage.query.create(
Penalty=fadan,
PenalizedUserID=fadan.PenalizedUserID,
ImageURL=url,
Purpose=1,
)
logger.info(
f"罚单{fadan.id}证据图变更:新增{len(add_urls)}张,删除{len(remove_ids)}"
)
return Response({'code': 0, 'msg': '证据图片已更新'})

1009
orders/views/payment.py Normal file

File diff suppressed because it is too large Load Diff