P1 性能优化(避免循环内 N+1 / N 次 DB 查询): - shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组 - product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断 - roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取 - guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete - admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update - jituan 服务层多处合并统计查询、批量预取 map - rank 多处循环 N+1 改批量预取 - backend 多处循环内 count/create 改批量 - config/orders/merchant_ops 多处循环 N+1 改批量预取 其他改动: - users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件 - 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
348 lines
15 KiB
Python
348 lines
15 KiB
Python
import hmac
|
||
import threading
|
||
import traceback
|
||
import uuid
|
||
import hashlib
|
||
import xmltodict
|
||
import time
|
||
import logging
|
||
import requests
|
||
import os
|
||
import secrets
|
||
import random
|
||
import string
|
||
from calendar import monthrange
|
||
from decimal import Decimal
|
||
from datetime import date, datetime, timedelta
|
||
from django.utils import timezone
|
||
from django.conf import settings
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.core.paginator import Paginator
|
||
from django.db import models, transaction, IntegrityError
|
||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||
from django.contrib.auth.hashers import make_password
|
||
from django.views.decorators.csrf import csrf_exempt
|
||
from django.utils.decorators import method_decorator
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||
|
||
# 工具类导入
|
||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||
from backend.utils import (
|
||
update_dashou_daily_by_action,
|
||
update_shangjia_daily
|
||
)
|
||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||
from jituan.services.club_penalty import (
|
||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||
)
|
||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||
from shop.utils import update_dianpu_daily_stat
|
||
from products.utils import update_shangpin_daily_stat
|
||
from orders.notice_tasks import dingdan_guangbo
|
||
from ..utils import (
|
||
verify_kefu_permission,
|
||
PERMISSION_TO_SHENFEN,
|
||
SHENFEN_PROFILE_MAP,
|
||
has_fadan_view_permission,
|
||
has_merchant_order_permission,
|
||
check_fadan_permission,
|
||
pick_order_id,
|
||
pick_penalty_create_params,
|
||
write_xiugai_log,
|
||
XIUGAI_LEIXING_DASHOU,
|
||
XIUGAI_LEIXING_GUANSHI,
|
||
XIUGAI_LEIXING_SHANGJIA,
|
||
XIUGAI_LEIXING_ZUZHANG,
|
||
XIUGAI_LEIXING_LABEL,
|
||
)
|
||
|
||
# models 集中导入
|
||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||
## backend
|
||
from backend.models import WithdrawalDailyStats
|
||
|
||
## users
|
||
from users.models import (
|
||
UserGuanshi, UserBoss, UserZuzhang,
|
||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||
)
|
||
from users.business_models import User
|
||
|
||
## orders
|
||
from orders.models import (
|
||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||
OrderPlayerHistory, Order, RefundRecord,
|
||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||
)
|
||
|
||
## shop
|
||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||
|
||
## products
|
||
from products.models import (
|
||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||
)
|
||
|
||
## config
|
||
from config.models import (
|
||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||
TixianQuotaDefault,
|
||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||
)
|
||
|
||
## rank
|
||
from rank.models import (
|
||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||
)
|
||
|
||
# 序列化器
|
||
from ..serializers import PopupPageSerializer
|
||
|
||
# 全局常量、日志对象
|
||
logger = logging.getLogger('houtai')
|
||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||
|
||
class ZxkfghdsView(APIView):
|
||
"""
|
||
客服更换打手接口
|
||
路径:POST /houtai/zxkfghds
|
||
功能:
|
||
1. 将当前订单状态改为已退款(5),释放接单打手(若有)
|
||
2. 如需跨平台通知,则调用对方平台退款通知接口
|
||
3. 基于原订单信息生成全新订单(状态直接为 1,已替老板填写)
|
||
4. 生成新的派单链接,并标记为已使用
|
||
5. 新链接记录上一个链接ID和上一个订单ID
|
||
6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款
|
||
权限:002ac(商家订单管理)/ 002ab(平台订单管理)/ 003aa(跨平台管理)
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = [JSONParser]
|
||
|
||
def post(self, request):
|
||
# ========== 1. 参数提取 ==========
|
||
username = request.data.get('username', '').strip() or request.data.get('phone', '').strip()
|
||
OrderID = pick_order_id(request.data)
|
||
if not username or not OrderID:
|
||
logger.warning("更换打手参数不完整")
|
||
return Response({'code': 400, 'msg': '参数不完整'})
|
||
|
||
# ========== 2. 权限校验(商家链接页客服需 002ac,非仅跨平台 003aa) ==========
|
||
kefu, permissions = verify_kefu_permission(request, username)
|
||
if kefu is None:
|
||
return permissions # 返回 error response
|
||
if not has_merchant_order_permission(permissions):
|
||
return Response({'code': 403, 'msg': '您无权处理订单'})
|
||
|
||
current_user = request.user
|
||
|
||
# ========== 3. 查询原订单及关联信息 ==========
|
||
try:
|
||
old_order = Order.query.select_related('shangjia_kuozhan').get(OrderID=OrderID)
|
||
except Order.DoesNotExist:
|
||
logger.warning(f"订单不存在: {OrderID}")
|
||
return Response({'code': 404, 'msg': '订单不存在'})
|
||
|
||
# 仅允许状态为 2(进行中)或 8(结算中)更换打手
|
||
if old_order.Status not in [1,7,2, 8]:
|
||
return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'})
|
||
|
||
# 本接口仅处理商家发单(Platform=2)
|
||
if old_order.Platform != 2:
|
||
return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'})
|
||
|
||
# 获取原链接(用于模板ID等)
|
||
try:
|
||
old_lianjie = ShangjiaLianjie.query.get(OrderID=OrderID)
|
||
except ShangjiaLianjie.DoesNotExist:
|
||
logger.error(f"原链接记录不存在,订单: {OrderID}")
|
||
return Response({'code': 500, 'msg': '链接数据异常'})
|
||
|
||
# 商家信息(仅用于记录关联,不操作余额)
|
||
try:
|
||
shangjia_ext = old_order.shangjia_kuozhan
|
||
shangjia_id = shangjia_ext.MerchantID
|
||
# 获取商家昵称用于新订单扩展表
|
||
shangjia_user = User.query.select_related('ShopProfile').get(UserUID=shangjia_id)
|
||
shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}"
|
||
except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
|
||
logger.error(f"商家信息缺失,订单: {OrderID}")
|
||
return Response({'code': 500, 'msg': '商家数据异常'})
|
||
|
||
# ========== 4. 退款处理(仅打手侧,不碰商家) ==========
|
||
with transaction.atomic():
|
||
# 5.1 订单状态改为已退款
|
||
old_order.Status = 5
|
||
old_order.AssignedCS = current_user.Phone
|
||
old_order.save()
|
||
|
||
# 5.2 处理接单打手(若有)
|
||
dashou_id = old_order.PlayerID
|
||
if dashou_id:
|
||
try:
|
||
dashou_user = User.query.get(UserUID=dashou_id)
|
||
dashou_profile = dashou_user.DashouProfile
|
||
dashou_profile.tuikuanliang += 1 # 退款次数+1
|
||
dashou_profile.zhuangtai = 1 # 设为空闲
|
||
dashou_profile.save()
|
||
except (User.DoesNotExist, ObjectDoesNotExist):
|
||
logger.warning(f"打手 {dashou_id} 不存在,跳过更新")
|
||
|
||
# 5.3 更新退款记录表
|
||
RefundRecord.query.update_or_create(
|
||
OrderID=OrderID,
|
||
defaults={
|
||
'PlayerID': dashou_id or '',
|
||
'ApplicantID': '',
|
||
'ProcessorID': current_user.Phone,
|
||
'Reason': '客服更换打手退款',
|
||
'ApplyStatus': 1,
|
||
'Amount': old_order.Amount or 0,
|
||
'PlayerCommission': old_order.PlayerCommission or 0,
|
||
'Description': old_order.Description or '',
|
||
'Remark': '更换打手自动退款',
|
||
'Nickname': old_order.Nickname or '',
|
||
}
|
||
)
|
||
|
||
# 5.4 打手每日统计(退款)
|
||
if dashou_id:
|
||
try:
|
||
update_dashou_daily_by_action(
|
||
yonghuid=dashou_id,
|
||
amount=old_order.PlayerCommission,
|
||
action=3 # 3=退款
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"打手每日统计更新失败: {e}")
|
||
|
||
# 5.5 客服处理统计(管理员无 KefuProfile,跳过)
|
||
if hasattr(kefu, 'jinrichuli'):
|
||
kefu.jinrichuli += 1
|
||
kefu.jinyuechuli += 1
|
||
kefu.zongchuli += 1
|
||
kefu.save()
|
||
|
||
# 至此,原订单已退款,事务提交
|
||
|
||
# ========== 6. 生成新订单和链接(不扣商家余额) ==========
|
||
try:
|
||
with transaction.atomic():
|
||
# ---- 6.1 生成新的订单ID和Token ----
|
||
def generate_OrderID():
|
||
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}"
|
||
|
||
def generate_secure_token(dd_id, uid):
|
||
timestamp = int(time.time() * 1_000_000)
|
||
salt = secrets.token_hex(8)
|
||
raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
|
||
token = hashlib.sha256(raw.encode()).hexdigest()[:24]
|
||
if ShangjiaLianjie.query.filter(LinkToken=token).exists():
|
||
return generate_secure_token(dd_id, uid)
|
||
return token
|
||
|
||
def generate_link_url(token):
|
||
base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
|
||
return f"{base}/order/{token}"
|
||
|
||
new_OrderID = generate_OrderID()
|
||
secure_token = generate_secure_token(new_OrderID, shangjia_id)
|
||
|
||
# ---- 6.2 创建新订单(复制核心数据,状态直接为1) ----
|
||
new_order = Order.query.create(
|
||
OrderID=new_OrderID,
|
||
Status=1, # 下单中
|
||
Platform=2,
|
||
Amount=old_order.Amount,
|
||
PlayerCommission=old_order.PlayerCommission,
|
||
ProductTypeID=old_order.ProductTypeID,
|
||
GrabRequirement=old_order.GrabRequirement,
|
||
MembershipID=old_order.MembershipID,
|
||
CommissionReq=old_order.CommissionReq,
|
||
Description=old_order.Description,
|
||
Nickname=old_order.Nickname, # 继承游戏昵称
|
||
Remark=old_order.Remark,
|
||
User1ID=old_order.User1ID, # 商家标识
|
||
ImageURL=old_order.ImageURL,
|
||
ExternalOrderID=old_order.ExternalOrderID,
|
||
ClubID=getattr(old_order, 'ClubID', None) or 'xq',
|
||
# 注意:不继承 AssignedID、PlayerID
|
||
)
|
||
|
||
# ---- 6.3 创建商家扩展表 ----
|
||
MerchantOrderExt.query.create(
|
||
Order=new_order,
|
||
MerchantID=shangjia_id,
|
||
MerchantNickname=shangjia_nicheng,
|
||
)
|
||
|
||
# ---- 6.4 生成新链接并关联旧链接信息 ----
|
||
link_url = generate_link_url(secure_token)
|
||
new_lianjie = ShangjiaLianjie.query.create(
|
||
UserID=shangjia_id,
|
||
ProductTypeID=old_lianjie.ProductTypeID,
|
||
TemplateID=old_lianjie.TemplateID,
|
||
LinkURL=link_url,
|
||
LinkToken=secure_token,
|
||
OrderID=new_OrderID,
|
||
is_used=True, # 直接标记已使用
|
||
ExpireTime=timezone.now() + timezone.timedelta(days=1),
|
||
PreviousLinkID=old_lianjie.id, # 记录旧链接ID
|
||
PreviousOrderID=old_order.OrderID, # 记录旧订单ID
|
||
)
|
||
|
||
# ---- 6.5 异步通知(广播新订单、跨平台同步) ----
|
||
try:
|
||
order_info = {
|
||
'OrderID': new_OrderID,
|
||
'game_type': self._get_game_type_name(new_order.ProductTypeID),
|
||
'amount': str(new_order.Amount),
|
||
'order_desc': (new_order.Description or '')[:50],
|
||
}
|
||
dingdan_guangbo.delay(order_info)
|
||
except Exception as e:
|
||
logger.error(f"广播通知失败: {e}")
|
||
|
||
# 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单
|
||
|
||
logger.info(f"更换打手成功:旧订单{OrderID}→退款,新订单{new_OrderID}已生成")
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '更换成功',
|
||
'data': {
|
||
'new_OrderID': new_OrderID,
|
||
'new_dingdan_id': new_OrderID,
|
||
'new_lianjie': link_url,
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 500, 'msg': '生成新订单失败'})
|
||
|
||
def _get_game_type_name(self, ProductTypeID):
|
||
"""根据类型ID获取游戏类型名称"""
|
||
try:
|
||
gt = ShangpinLeixing.query.filter(id=ProductTypeID).first()
|
||
return gt.jieshao if gt else '游戏订单'
|
||
except Exception:
|
||
return '游戏订单'
|
||
|
||
|