Files
Django/backend/views/zxkf.py
2026-07-04 19:58:19 +08:00

348 lines
15 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 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.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 '游戏订单'