Files
Django/users/views/kefu_punishment.py
TermiNexus f1c8633345 perf: P1 性能优化批量修复 + kefu 视图拆分
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
2026-07-05 23:17:38 +08:00

565 lines
22 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.
"""users.views.kefu_punishment - auto-generated by split script."""
"""users.views.kefu - auto-generated by split script."""
import os
import sys
import json
import re
import time
import uuid
import random
import string
import requests
import hashlib
import traceback
import decimal
import jwt
import ipaddress
import xmltodict
from datetime import datetime, timedelta
from decimal import Decimal
import defusedxml.ElementTree as ET
from django.conf import settings
from django.db import models, transaction, IntegrityError, DatabaseError, connection
from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch
from django.core.cache import cache
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.http import HttpResponse
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.hashers import make_password
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 MultiPartParser, FormParser, JSONParser
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework_simplejwt.authentication import JWTAuthentication
from gvsdsdk.fluent import db, func, FQ
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
from utils.money import yuan_to_fen
from utils.fadan_utils import check_fadan_qiangdan_eligible
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from users.fadan_fenhong_utils import process_fadan_fenhong
from orders.utils import (
update_daily_payout,
settle_shangjia_order_guanshi_fenhong
)
from backend.utils import (
update_dashou_daily_by_action, update_guanshi_daily_by_action,
update_shangjia_daily, update_zuzhang_daily_by_action,
verify_kefu_permission
)
from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan
from ..models import (
UserBoss, UserDashou, UserShangjia, UserGuanshi,
UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser,
TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu,
RankingRecord
)
from users.business_models import User
from ..tixian_shenhe_services import (
load_audit_meta_map, refund_balance, sync_audit_and_jilu_status,
mark_transfer_success, release_collect_quota_for_record,
close_audit_wechat_failed, query_wechat_transfer_bill,
check_shijidaozhang_within_limit,
check_dashou_trial_blocks_commission_withdraw,
WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING,
)
from ..tixian_shenhe_views import process_audit_collect
from orders.models import (
Order, PlatformOrderExt, MerchantOrderExt, CommissionRate,
PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage,
Penalty, PenaltyAppealImage
)
from products.models import (
ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu
)
from config.models import Qunpeizhi
from backend.models import MerchantDailyStats
from rank.models import (
KaohePayTemp, Chenghao, ShenheJilu,
KaoheCishuFeiyong, YonghuChenghao
)
from users.models import UserShenheguan
from products.utils import update_shangpin_daily_stat
from shop.utils import update_dianpu_daily_stat
from rank.utils import create_shenhe_jilu_from_temp
import logging
logger = logging.getLogger(__name__)
class KefuPunishmentListView(APIView):
"""
客服获取处罚记录列表接口
请求POST /yonghu/kefu_cfgl
参数:{
"phone": "客服手机号",
"status": [0,3] 或 [1,2], # 状态数组
"dingdan_id": "订单ID可选",
"dashouid": "打手ID可选",
"page": 1,
"page_size": 20
}
认证JWT
返回:{
"code": 0,
"data": {
"list": [处罚记录],
"total": 总数,
"stats": {
"zongshu": 总记录数,
"daichuli": 待处理数,
"yichuli": 已处理数
}
}
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
@staticmethod
def _parse_status_list(raw):
"""兼容 status=[0,3]、\"0,3\"、单数字;空/all 表示不过滤。"""
if raw is None:
return None
if isinstance(raw, str):
s = raw.strip().lower()
if not s or s in ('all', 'null', 'none'):
return None
if s.startswith('['):
import json
try:
raw = json.loads(s)
except (TypeError, ValueError):
return None
else:
raw = [x.strip() for x in s.split(',') if x.strip()]
if isinstance(raw, (int, float)):
return [int(raw)]
if isinstance(raw, list):
out = []
for x in raw:
try:
out.append(int(x))
except (TypeError, ValueError):
continue
return out or None
return None
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
status_list = self._parse_status_list(request.data.get('status'))
dingdan_id = request.data.get('dingdan_id', '').strip()
dashouid = request.data.get('dashouid', '').strip()
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
except ValueError:
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
if not phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 客服身份验证
current_user = request.user
# 1. 获取前端参数
username = request.data.get('phone', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
# 2. 公共权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 3. 检查平台订单管理权限002ab
if '005aa' not in permissions:
return Response({'code': 403, 'msg': '您无权查看平台订单列表'})
# 3. 获取统计信息(不分页)
from jituan.services.club_penalty import filter_penalty_record_qs
from jituan.services.club_user_access import list_response_meta
base_qs = filter_penalty_record_qs(PenaltyRecord.query.all(), request)
agg = base_qs.aggregate(
zongshu=Count('id'),
daichuli=Count('id', filter=Q(ApplyStatus__in=[0, 3])),
yichuli=Count('id', filter=Q(ApplyStatus__in=[1, 2])),
)
stats = {
'zongshu': agg['zongshu'],
'daichuli': agg['daichuli'],
'yichuli': agg['yichuli'],
}
queryset = base_qs
if status_list and isinstance(status_list, list):
queryset = queryset.filter(ApplyStatus__in=status_list)
if dingdan_id:
queryset = queryset.filter(OrderID=dingdan_id)
if dashouid:
queryset = queryset.filter(PlayerID=dashouid)
# 5. 分页
page_obj = queryset.order_by('-CreateTime').paginate(page=page, per_page=page_size)
total = page_obj.total
records = page_obj.items
# 6. 构建返回列表
list_data = []
for item in records:
ct = item.CreateTime
ut = item.UpdateTime
list_data.append({
'dingdan_id': item.OrderID,
'dashouid': item.PlayerID,
'qingqiuid': item.ApplicantID,
'cfliyou': item.PenaltyReason,
'sqzhuangtai': item.ApplyStatus,
'jifen': item.DeductedPoints,
'CreateTime': ct,
'UpdateTime': ut,
'create_time': ct.isoformat() if ct else '',
'update_time': ut.isoformat() if ut else '',
})
return Response({
'code': 0,
'data': {
'list': list_data,
'total': total,
'stats': stats,
**list_response_meta(request),
}
})
class KefuPunishmentDetailView(APIView):
"""
客服获取处罚详情接口
请求POST /yonghu/kefu_cf_detail
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID"
}
认证JWT
返回code=0 + 处罚记录详情(含图片)
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 查询处罚记录(按创建时间倒序取最新一条)
try:
record = PenaltyRecord.query.filter(OrderID=dingdan_id).order_by('-CreateTime').first()
if not record:
return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"查询处罚记录失败: {e}")
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 查询商家证据图片
zhengju_images = []
shensu_images = []
try:
# 商家证据图片:上传者为请求人 (ApplicantID)
zhengju_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.ApplicantID)
zhengju_images = [item.ImageURL for item in zhengju_qs if item.ImageURL]
# 打手申诉图片:上传者为打手 (PlayerID)
shensu_qs = PenaltyEvidenceImage.query.filter(OrderID=dingdan_id, UserID=record.PlayerID)
shensu_images = [item.ImageURL for item in shensu_qs if item.ImageURL]
except Exception as e:
logger.error(f"查询图片失败: {e}")
# 构建返回数据
data = {
'dingdan_id': record.OrderID,
'dashouid': record.PlayerID,
'qingqiuid': record.ApplicantID,
'chuliid': record.ProcessorID,
'cfliyou': record.PenaltyReason,
'sqzhuangtai': record.ApplyStatus,
'bhliyou': record.RejectReason,
'jifen': record.DeductedPoints,
'ssliyou': record.AppealReason or '',
'zhengju_tupian': zhengju_images,
'shensu_tupian': shensu_images,
'CreateTime': record.CreateTime,
'UpdateTime': record.UpdateTime,
}
return Response({'code': 0, 'data': data})
class KefuPunishmentActionView(APIView):
"""
客服同意/驳回处罚接口
请求POST /yonghu/kefu_cf_action
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"action": 1(同意) / 2(驳回),
"reject_reason": "驳回理由(驳回时必填)"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
action = request.data.get('action')
reject_reason = request.data.get('reject_reason', '').strip()
if not phone or not dingdan_id or action not in [1, 2]:
return Response({'code': 401, 'msg': '参数错误'}, status=status.HTTP_400_BAD_REQUEST)
if action == 2 and not reject_reason:
return Response({'code': 400, 'msg': '驳回理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
# 客服身份验证(完整验证,此处略)
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
if kefu.zhuangtai != 1:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
except ObjectDoesNotExist:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 查询待处理的处罚记录状态0或3
try:
record = PenaltyRecord.query.filter(
OrderID=dingdan_id,
ApplyStatus__in=[0, 3]
).order_by('-CreateTime').first()
if not record:
exists = PenaltyRecord.query.filter(OrderID=dingdan_id).exists()
if exists:
return Response({'code': 400, 'msg': '该处罚记录已处理'}, status=status.HTTP_400_BAD_REQUEST)
else:
return Response({'code': 404, 'msg': '处罚记录不存在'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e:
logger.error(f"查询处罚记录失败: {e}")
return Response({'code': 500, 'msg': '服务器内部错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
with transaction.atomic():
# 处理重复记录:同一订单的其他待处理/申诉中记录标记为驳回
duplicate_records = PenaltyRecord.query.filter(
OrderID=dingdan_id,
ApplyStatus__in=[0, 3]
).exclude(id=record.id)
for dup in duplicate_records:
dup.ApplyStatus = 2
dup.ProcessorID = phone
dup.RejectReason = "系统自动驳回:存在重复处罚记录"
dup.save()
logger.info(f"自动驳回重复处罚记录 ID={dup.id}")
if action == 1: # 同意
# 可选验证打手存在
if record.PlayerID:
try:
dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
except UserDashou.DoesNotExist:
return Response({'code': 400, 'msg': '被处罚打手不存在'}, status=status.HTTP_400_BAD_REQUEST)
record.ApplyStatus = 1
record.ProcessorID = phone
record.save()
# 更新商家订单扩展表
try:
dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
dingdan.shangjia_kuozhan.PenaltyApplyStatus = 1
dingdan.shangjia_kuozhan.save()
except Exception as e:
logger.warning(f"更新商家扩展表失败: {e}")
elif action == 2: # 驳回
# 返还积分
if record.DeductedPoints > 0 and record.PlayerID:
try:
dashou = UserDashou.query.get(user__UserUID=record.PlayerID)
if dashou.jifen <= 1000: # 安全限制
dashou.jifen += record.DeductedPoints
dashou.save()
logger.info(f"驳回处罚,打手{record.PlayerID}返还积分{record.DeductedPoints}")
else:
logger.warning(f"打手积分异常({dashou.jifen}),不再返还")
except UserDashou.DoesNotExist:
logger.warning(f"打手{record.PlayerID}不存在,无法返还积分")
record.ApplyStatus = 2
record.ProcessorID = phone
record.RejectReason = reject_reason
record.save()
try:
dingdan = Order.query.select_related('shangjia_kuozhan').get(OrderID=dingdan_id)
if hasattr(dingdan, 'shangjia_kuozhan'):
dingdan.shangjia_kuozhan.PenaltyApplyStatus = 2
dingdan.shangjia_kuozhan.RejectReason = reject_reason
dingdan.shangjia_kuozhan.save()
except Exception as e:
logger.warning(f"更新商家扩展表失败: {e}")
return Response({'code': 0, 'msg': '处理成功'})
class KefuPunishView(APIView):
"""
客服处罚打手接口
请求POST /yonghu/kefuchufa
参数:{
"phone": "客服手机号",
"dingdan_id": "订单ID",
"reason": "处罚原因(可选)"
}
认证JWT
返回code=0 成功
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
dingdan_id = request.data.get('dingdan_id', '').strip()
reason = request.data.get('reason', '').strip()
if not phone or not dingdan_id:
return Response({'code': 401, 'msg': '参数不完整'}, status=status.HTTP_400_BAD_REQUEST)
# 2. 客服身份验证
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: {phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
logger.warning(f"用户类型非客服: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
kefu = None
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
logger.warning(f"客服账号已禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 查询订单
try:
order = Order.query.get(OrderID=dingdan_id)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'}, status=status.HTTP_404_NOT_FOUND)
# 4. 获取打手ID
dashou_id = order.PlayerID
if not dashou_id:
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'}, status=status.HTTP_400_BAD_REQUEST)
# 5. 查询打手
try:
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou = dashou_user.DashouProfile
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手用户不存在'}, status=status.HTTP_404_NOT_FOUND)
except ObjectDoesNotExist:
return Response({'code': 400, 'msg': '该用户不是打手'}, status=status.HTTP_400_BAD_REQUEST)
# 6. 使用事务
from jituan.services.club_penalty import resolve_penalty_record_club_id
with transaction.atomic():
# 创建处罚记录积分扣除5分
jifen = 5
chufa = PenaltyRecord.query.create(
PlayerID=dashou_id,
ApplicantID=phone, # 客服手机号作为请求人
PenaltyReason=reason or '',
ApplyStatus=0, # 待处理
DeductedPoints=jifen,
OrderID=dingdan_id,
ClubID=resolve_penalty_record_club_id(
order_id=dingdan_id,
player_id=dashou_id,
request=request,
user=request.user,
),
)
# 扣除打手积分
if dashou.jifen >= jifen:
dashou.jifen -= jifen
else:
dashou.jifen = 0
dashou.save()
# 可选:更新订单的 clkf 字段记录处理客服
order.AssignedCS = phone
order.save()
return Response({'code': 0, 'msg': '处罚成功'})