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
This commit is contained in:
2026-07-05 23:17:38 +08:00
parent 76ea5f8255
commit f1c8633345
54 changed files with 5332 additions and 4494 deletions

View File

@@ -88,29 +88,41 @@ from .chufa import (
ShangjiaChufaJiluHuoquView,
)
from .kefu import (
from .kefu_base import (
KefuGetOrderTypesView,
KefuLoginView,
KefuStatsView,
)
from .kefu_orders import (
KefuCancelDesignationView,
KefuChangeDashouView,
KefuForceCompleteView,
KefuGetDashouDetailView,
KefuGetDashouListView,
KefuGetOrderDetailView,
KefuGetOrderListView,
KefuGetOrderTypesView,
KefuGetShangjiaOrderListView,
KefuLoginView,
KefuMerchantRefundView,
KefuPlatformRefundView,
KefuRecoverOrderView,
KefuRejectRefundView,
KefuRejectSettlementView,
KefuTransferHallView,
)
from .kefu_dashou import (
KefuGetDashouDetailView,
KefuGetDashouListView,
KefuUpdateDashouView,
)
from .kefu_punishment import (
KefuPunishView,
KefuPunishmentActionView,
KefuPunishmentDetailView,
KefuPunishmentListView,
KefuRecoverOrderView,
KefuRejectRefundView,
KefuRejectSettlementView,
KefuStatsView,
KefuTransferHallView,
KefuUpdateDashouView,
)
from .kefu_withdraw import (
KefuWithdrawActionView,
KefuWithdrawDetailView,
KefuWithdrawListView,

View File

@@ -532,12 +532,18 @@ class AdGetOrderList(APIView):
# 注意:这里统计所有订单,不考虑筛选条件
try:
# 平台订单统计 (Platform=1)
platform_total = Order.query.filter(Platform=1).count()
platform_completed = Order.query.filter(Platform=1, Status=3).count()
_agg = Order.query.aggregate(
platform_total=Count('id', filter=Q(Platform=1)),
platform_completed=Count('id', filter=Q(Platform=1, Status=3)),
merchant_total=Count('id', filter=Q(Platform=2)),
merchant_completed=Count('id', filter=Q(Platform=2, Status=3)),
)
platform_total = _agg['platform_total']
platform_completed = _agg['platform_completed']
# 商家订单统计 (Platform=2)
merchant_total = Order.query.filter(Platform=2).count()
merchant_completed = Order.query.filter(Platform=2, Status=3).count()
merchant_total = _agg['merchant_total']
merchant_completed = _agg['merchant_completed']
platform_stats = {
'total': platform_total,
@@ -1170,9 +1176,14 @@ class CfGuanLi(APIView):
)
# 7. 获取统计信息(总是需要统计)
zongshu = PenaltyRecord.query.count()
daichuli = PenaltyRecord.query.filter(ApplyStatus__in=[0, 3]).count()
yichuli = PenaltyRecord.query.filter(ApplyStatus__in=[1, 2]).count()
_agg = PenaltyRecord.query.aggregate(
zongshu=Count('id'),
daichuli=Count('id', filter=Q(ApplyStatus__in=[0, 3])),
yichuli=Count('id', filter=Q(ApplyStatus__in=[1, 2])),
)
zongshu = _agg['zongshu']
daichuli = _agg['daichuli']
yichuli = _agg['yichuli']
# 8. 如果只请求统计信息,直接返回
if qingqiu_tongji:
@@ -1687,6 +1698,13 @@ class AdckyhxqView(APIView):
yonghu_id=uid
).order_by('-CreateTime')
# 批量预取会员详情
_huiyuan_ids = [r.huiyuan_id for r in member_records]
_huiyuan_map = {
h.huiyuan_id: h
for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao')
}
member_list = []
for record in member_records:
# 调用方法检查是否过期,并更新状态
@@ -1694,9 +1712,7 @@ class AdckyhxqView(APIView):
if not is_expired: # 只返回未过期的
# 查询会员详情
huiyuan = Huiyuan.query.filter(
huiyuan_id=record.huiyuan_id
).first()
huiyuan = _huiyuan_map.get(record.huiyuan_id)
if huiyuan:
member_list.append({
@@ -2282,15 +2298,13 @@ class AddtxshView(APIView):
has_more = (offset + current_count) < total_count
# 获取状态统计(统计与搜索无关,依然按筛选条件统计)
_agg = Tixianjilu.query.filter(leixing=leixing).aggregate(
awaiting=Count('id', filter=Q(zhuangtai=1)),
processed=Count('id', filter=Q(zhuangtai__in=[2, 3])),
)
status_counts = {
'awaiting': Tixianjilu.query.filter(
zhuangtai=1,
leixing=leixing
).count(),
'processed': Tixianjilu.query.filter(
zhuangtai__in=[2, 3],
leixing=leixing
).count()
'awaiting': _agg['awaiting'],
'processed': _agg['processed'],
}
# 构建响应数据

View File

@@ -526,16 +526,16 @@ class DashouShensuView(APIView):
chufa_record.save()
# 6. 保存申诉图片(如果有)
saved_tupian_urls = []
if shensu_tupian_urls:
for tupian_url in shensu_tupian_urls:
if tupian_url: # 确保URL不为空
chufa_tupian = PenaltyEvidenceImage.query.create(
OrderID=chufa_record.OrderID,
UserID=yonghuid,
ImageURL=tupian_url
)
saved_tupian_urls.append(tupian_url)
saved_tupian_urls = [u for u in shensu_tupian_urls if u] if shensu_tupian_urls else []
if saved_tupian_urls:
PenaltyEvidenceImage.objects.bulk_create([
PenaltyEvidenceImage(
OrderID=chufa_record.OrderID,
UserID=yonghuid,
ImageURL=tupian_url,
)
for tupian_url in saved_tupian_urls
])
# 7. 构建返回数据
response_data = {

File diff suppressed because it is too large Load Diff

383
users/views/kefu_base.py Normal file
View File

@@ -0,0 +1,383 @@
"""users.views.kefu_base - 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 KefuLoginView(APIView):
"""
客服/管理员登录接口(支持手机号重复)
请求POST /yonghu/kefujinru
请求体:{"phone": "13800138000", "password": "xxx", "erjimima": "yyy"}
返回:
code=0 成功data包含token,nicheng,yonghuid
code=1 失败msg区分用户不存在/密码错误/二级密码错误)
code=4 账号被封禁
管理员IsSuperuser 或 UserType='admin')无需 erjimima 和 KefuProfile。
"""
throttle_classes = [AnonRateThrottle] # 限制匿名请求频率
permission_classes = [AllowAny] # 任何人都可访问
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
password = request.data.get('password', '')
erjimima = request.data.get('erjimima', '')
if not phone or not password:
return Response({
'code': 1,
'msg': '请填写手机号和密码',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取客户端真实IP
client_ip = self.get_client_ip(request)
# 3. 查询该手机号下所有账号(客服 + 管理员)
# 先查客服(有 KefuProfile再查管理员IsSuperuser 或 UserType='admin'
user_mains = list(
User.query.filter(
Phone=phone,
KefuProfile__isnull=False,
).select_related('KefuProfile')
)
# 如果没有客服账号,尝试查找管理员账号
if not user_mains:
admin_candidates = User.query.filter(Phone=phone)
for admin_user in admin_candidates:
if admin_user.IsSuperuser or admin_user.UserType == 'admin':
user_mains = [admin_user]
break
if not user_mains:
logger.warning(f"登录失败,用户不存在: {phone}, IP: {client_ip}")
return Response({
'code': 1,
'msg': '用户不存在',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 4. 遍历所有匹配的账号,验证密码和二级密码
target_user = None
target_kefu = None
password_matched = False
for user in user_mains:
# 验证主密码bcrypt 哈希验证)
if not user.CheckPassword(password):
continue
password_matched = True
# 管理员:跳过二级密码和客服扩展表检查
is_admin = user.IsSuperuser or user.UserType == 'admin'
if is_admin:
target_user = user
target_kefu = None
break
# 客服:验证二级密码
kefu = user.KefuProfile
stored_erji = kefu.erjimima or ''
if stored_erji.startswith(('$2b$', '$2a$')) and len(stored_erji) == 60:
import bcrypt as _bcrypt
if not _bcrypt.checkpw(erjimima.encode('utf-8'), stored_erji.encode('utf-8')):
continue
elif stored_erji != erjimima:
continue
# 如果账号被禁用,记录但继续查找
if kefu.zhuangtai != 1:
if target_user is None:
target_user = user
target_kefu = kefu
continue
# 找到完全匹配且状态正常的账号,立即使用
target_user = user
target_kefu = kefu
break
else:
# 循环结束未找到正常账号
if target_user and target_kefu:
# 只有被禁用的账号匹配,返回封禁提示
logger.warning(f"登录失败,客服账号已禁用: {phone}, IP: {client_ip}")
return Response({
'code': 4,
'msg': '账号已被封禁',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
elif password_matched:
# 密码正确但二级密码错误
logger.warning(f"登录失败,二级密码错误: {phone}, IP: {client_ip}")
return Response({
'code': 1,
'msg': '二级密码错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
else:
# 密码错误
logger.warning(f"登录失败,密码错误: {phone}, IP: {client_ip}")
return Response({
'code': 1,
'msg': '密码错误',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 5. 更新IP和最后登录时间
target_user.IP = client_ip
target_user.UserLastLoginDate = timezone.now()
target_user.save(update_fields=['IP', 'UserLastLoginDate'])
# 6. 生成JWT token
refresh = RefreshToken.for_user(target_user)
token = str(refresh.access_token)
# 7. 返回成功数据(管理员无 KefuProfilenicheng 用 UserName 或 Phone 兜底)
nicheng = target_kefu.nicheng if target_kefu else (target_user.UserName or target_user.Phone)
return Response({
'code': 0,
'msg': '登录成功',
'data': {
'token': token,
'nicheng': nicheng,
'yonghuid': target_user.UserUID,
}
})
def get_client_ip(self, request):
"""
获取客户端真实IP
"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
if ip:
return ip
return request.META.get('REMOTE_ADDR', '0.0.0.0')
class KefuStatsView(APIView):
"""
客服统计数据接口
请求POST /yonghu/kfjrhqzl
请求体:{"phone": "13800138000"}
认证:必须携带有效的 JWT token放在 Authorization 头)
返回:
code=0 成功data 包含 jinrichuli, jinyuechuli, zongchuli
验证失败统一返回 401不透露具体原因
"""
permission_classes = [IsAuthenticated] # 必须登录
def post(self, request):
# 1. 获取请求中的 phone
phone = request.data.get('phone', '').strip()
if not phone:
# 缺少参数,但按约定返回 401可以改为 400但为了统一用401
logger.warning(f"统计接口缺少 phone 参数,用户: {request.user.UserUID}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 2. 获取当前登录用户(从 JWT 中解析)
current_user = request.user
# 3. 验证前端传来的 phone 是否与当前用户的 phone 一致
if current_user.Phone != phone:
# 不一致,可能 token 被冒用或参数错误
logger.warning(f"统计接口 phone 不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 4. 验证是否为后台客服账号
from jituan.services.admin_context import is_kefu_backend_account, is_system_super_admin
if not is_kefu_backend_account(current_user):
logger.warning(f"统计接口用户类型非客服: {current_user.UserUID}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 5. 尝试获取客服扩展表数据(管理员跳过)
is_admin = is_system_super_admin(current_user)
if is_admin:
return Response({
'code': 0,
'data': {
'jinrichuli': 0,
'jinyuechuli': 0,
'zongchuli': 0,
}
}, status=status.HTTP_200_OK)
try:
kefu_profile = current_user.KefuProfile # OneToOneField 反向关系
except UserKefu.DoesNotExist:
logger.warning(f"统计接口客服扩展表不存在: {current_user.UserUID}")
return Response({'detail': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 6. 返回统计数据(字段名与前端约定一致)
return Response({
'code': 0,
'data': {
'jinrichuli': kefu_profile.jinrichuli,
'jinyuechuli': kefu_profile.jinyuechuli,
'zongchuli': kefu_profile.zongchuli,
}
}, status=status.HTTP_200_OK)
class KefuGetOrderTypesView(APIView):
"""
客服获取订单类型列表接口(平台订单用)
请求POST /yonghu/kfhqptddlx
参数:{"phone": "13800138000"}
认证JWT
返回code=0 + 类型列表
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
if not phone:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
current_user = request.user
# 2. 验证手机号一致性
if getattr(current_user, 'Phone', '') != phone:
logger.warning(f"手机号不匹配: 请求phone={phone}, 用户phone={current_user.Phone}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 3. 验证用户类型及客服状态
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
if not is_admin:
try:
kefu_profile = current_user.KefuProfile
except ObjectDoesNotExist:
logger.warning(f"客服扩展表不存在: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu_profile.zhuangtai != 1:
logger.warning(f"客服账号已被禁用: {current_user.UserUID}")
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 4. 查询订单类型(商品类型)
try:
# 只查询需要的字段,减少数据传输
types_qs = ShangpinLeixing.query.all().only('id', 'jieshao', 'tupian_url')
type_list = [
{
'id': t.id,
'biaoti': t.jieshao or '', # 假设商品类型的标题字段为 jieshao
'tupian_url': t.tupian_url or ''
}
for t in types_qs
]
return Response({'code': 0, 'data': type_list})
except Exception as e:
logger.error(f"查询商品类型表失败: {str(e)}")
# 表不存在或查询失败,返回空列表
return Response({'code': 0, 'data': []})

636
users/views/kefu_dashou.py Normal file
View File

@@ -0,0 +1,636 @@
"""users.views.kefu_dashou - 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 KefuGetDashouListView(APIView):
"""
客服获取打手列表接口(仅打手类型)
请求POST /yonghu/kefuhqdslb
参数:{
"phone": "客服手机号",
"keyword": "搜索关键词可选搜索用户ID或昵称",
"page": "页码从1开始",
"page_size": "每页数量"
}
认证JWT
返回:{
"code": 0,
"data": {
"list": [打手对象],
"has_more": true/false
}
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('zhanghao', '').strip()
keyword = request.data.get('keyword', '').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
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. 构建查询条件(只查询打手)
# 通过 UserDashou 扩展表关联 User
queryset = UserDashou.query.select_related('user').all()
if keyword:
# 搜索用户ID或打手昵称
queryset = queryset.filter(
Q(user__UserUID__icontains=keyword) |
Q(nicheng__icontains=keyword)
)
# 4. 计算总数并分页
total = queryset.count()
offset = (page - 1) * page_size
dashou_list = queryset.order_by('-CreateTime')[offset:offset + page_size]
# 5. 构建返回数据(只取需要的字段)
result = []
for dashou in dashou_list:
user = dashou.user
result.append({
'yonghuid': user.UserUID,
'avatar': user.Avatar or '',
'zaixianzhuangtai': dashou.zaixianzhuangtai,
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
'nicheng': dashou.nicheng or '',
'zhuangtai': dashou.zhuangtai,
})
# 6. 判断是否有更多数据
has_more = (offset + len(dashou_list)) < total
return Response({
'code': 0,
'data': {
'list': result,
'has_more': has_more
}
})
class KefuGetDashouDetailView(APIView):
"""
客服获取打手详情接口
请求POST /yonghu/kefuhqdsxq
参数:{
"phone": "客服手机号",
"uid": "打手ID (yonghuid)"
}
认证JWT
返回code=0 + 打手信息 + 会员列表
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# 1. 获取参数
phone = request.data.get('phone', '').strip()
uid = request.data.get('uid', '').strip()
if not phone or not uid:
return Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 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:
# 使用 select_related 预加载 dashou_profile减少数据库查询
user_main = User.query.select_related('DashouProfile').get(
UserUID=uid
)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'}, status=status.HTTP_404_NOT_FOUND)
# 验证用户类型是否为打手(可选,但建议验证)
# 如果该用户没有打手扩展表,则返回空信息(但前端会显示未注册)
try:
dashou = user_main.DashouProfile
except ObjectDoesNotExist:
# 用户存在但未注册打手身份,返回基本信息
user_info = {
'yonghuid': user_main.UserUID,
'avatar': user_main.Avatar or '',
'zaixianzhuangtai': 0,
'zhanghaozhuangtai': 0,
'zhuangtai': 0,
'nicheng': '',
'chenghao': '',
'dianhua': '',
'wechat': '',
'phone': user_main.Phone or '',
'CreateTime': user_main.UserCreateTime,
'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
'ip': user_main.IP or '',
'jiedanzongliang': 0,
'chengjiaozongliang': 0,
'tuikuanliang': 0,
'jinrijiedan': 0,
'yue': 0.00,
'zonge': 0.00,
'jifen': 0,
'yajin': 0.00,
'jinrishouyi': 0.00,
'jinyueshouyi': 0.00,
'yaoqingren': '',
'jieshao': '',
}
member_list = []
return Response({
'code': 0,
'data': {
'user_info': user_info,
'member_list': member_list
}
})
# 4. 构建打手信息
user_info = {
'yonghuid': user_main.UserUID,
'avatar': user_main.Avatar or '',
'zaixianzhuangtai': dashou.zaixianzhuangtai,
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
'zhuangtai': dashou.zhuangtai,
'nicheng': dashou.nicheng or '',
'chenghao': dashou.chenghao or '',
'dianhua': dashou.dianhua or '',
'wechat': dashou.wechat or '',
'phone': user_main.Phone or '',
'CreateTime': user_main.UserCreateTime,
'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '',
'ip': user_main.IP or '',
'jiedanzongliang': dashou.jiedanzongliang,
'chengjiaozongliang': dashou.chengjiaozongliang,
'tuikuanliang': dashou.tuikuanliang,
'jinrijiedan': dashou.jinrijiedan,
'yue': float(dashou.yue) if dashou.yue else 0.00,
'zonge': float(dashou.zonge) if dashou.zonge else 0.00,
'jifen': dashou.jifen,
'yajin': float(dashou.yajin) if dashou.yajin else 0.00,
'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00,
'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00,
'yaoqingren': dashou.yaoqingren or '',
'jieshao': dashou.jieshao or '',
}
# 5. 查询打手会员信息(仅未过期)
member_records = list(Huiyuangoumai.query.filter(
yonghu_id=uid
).order_by('-CreateTime'))
huiyuan_ids = {r.huiyuan_id for r in member_records if r.huiyuan_id}
huiyuan_map = {
h.huiyuan_id: h.jieshao
for h in Huiyuan.query.filter(huiyuan_id__in=huiyuan_ids).only('huiyuan_id', 'jieshao')
}
member_list = []
for record in member_records:
# 调用模型方法检查是否过期
if hasattr(record, 'jiance_shifou_daoqi'):
is_expired = record.jiance_shifou_daoqi()
else:
# 如果方法不存在,简单判断
is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True
if not is_expired:
# 获取会员详情
huiyuan_name = huiyuan_map.get(record.huiyuan_id, '未知会员')
member_list.append({
'id': record.id,
'huiyuan_name': huiyuan_name,
'daoqi_time': record.daoqi_time,
'is_active': record.huiyuan_zhuangtai == 1,
})
return Response({
'code': 0,
'data': {
'user_info': user_info,
'member_list': member_list
}
})
class KefuUpdateDashouView(APIView):
permission_classes = [IsAuthenticated]
"""
客服修改打手信息接口
支持操作:
- jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb)
- jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb)
- jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd)
- gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee)
- gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee)
- gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee)
- jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg)
"""
def post(self, request):
# 1. 获取前端参数
username = request.data.get('username', '').strip()
dashou_id = request.data.get('dashou_id', '').strip()
caozuo = request.data.get('caozuo', '').strip()
value = request.data.get('value') # 金额、积分、天数等
huiyuan_id = request.data.get('huiyuan_id', '').strip() # 会员ID加/减会员时使用)
if not username or not dashou_id or not caozuo:
return Response({'code': 400, 'msg': '参数不完整'})
# 2. 权限校验(统一方法)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions # 直接返回错误响应
# 3. 查询打手是否存在
try:
dashou_user = User.query.select_related('DashouProfile').get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手不存在'})
except UserDashou.DoesNotExist:
return Response({'code': 404, 'msg': '打手扩展信息缺失'})
# 4. 根据操作类型执行(事务内)
with transaction.atomic():
try:
# 记录修改前的值(用于日志)
before = {
'yue': dashou_profile.yue,
'yajin': dashou_profile.yajin,
'jifen': dashou_profile.jifen,
'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai,
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
'zhuangtai': dashou_profile.zhuangtai,
}
# ---------- 余额操作 ----------
if caozuo == 'jian_yue':
if '001aa' not in permissions:
return Response({'code': 403, 'msg': '无权限减打手余额'})
amount = Decimal(str(value))
if dashou_profile.yue < amount:
return Response({'code': 400, 'msg': '余额不足'})
dashou_profile.yue -= amount
dashou_profile.save()
after_yue = dashou_profile.yue
# 记录日志
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2, # 2=打手
xiugaitijiao=after_yue,
xiugaitijiaoq=before['yue'],
)
return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after_yue)}})
elif caozuo == 'jia_yue':
if '001bb' not in permissions:
return Response({'code': 403, 'msg': '无权限加打手余额'})
from jituan.services.group_finance_gate import deny_group_finance_exec
deny = deny_group_finance_exec(request.user, request)
if deny:
return deny
amount = Decimal(str(value))
dashou_profile.yue += amount
dashou_profile.save()
after_yue = dashou_profile.yue
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
xiugaitijiao=after_yue,
xiugaitijiaoq=before['yue'],
)
return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after_yue)}})
# ---------- 押金操作 ----------
elif caozuo == 'jian_yajin':
if '001aa' not in permissions:
return Response({'code': 403, 'msg': '无权限减打手押金'})
amount = Decimal(str(value))
if dashou_profile.yajin < amount:
return Response({'code': 400, 'msg': '押金不足'})
dashou_profile.yajin -= amount
dashou_profile.save()
after_yajin = dashou_profile.yajin
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
yajin=after_yajin,
yyajin=before['yajin'],
)
return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after_yajin)}})
elif caozuo == 'jia_yajin':
if '001bb' not in permissions:
return Response({'code': 403, 'msg': '无权限加打手押金'})
from jituan.services.group_finance_gate import deny_group_finance_exec
deny = deny_group_finance_exec(request.user, request)
if deny:
return deny
amount = Decimal(str(value))
dashou_profile.yajin += amount
dashou_profile.save()
after_yajin = dashou_profile.yajin
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
yajin=after_yajin,
yyajin=before['yajin'],
)
return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after_yajin)}})
# ---------- 积分操作 ----------
elif caozuo == 'jian_jifen':
if '001cc' not in permissions:
return Response({'code': 403, 'msg': '无权限减打手积分'})
jifen = int(value)
if dashou_profile.jifen < jifen:
return Response({'code': 400, 'msg': '积分不足'})
dashou_profile.jifen -= jifen
dashou_profile.save()
after_jifen = dashou_profile.jifen
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
jifen=after_jifen,
yjifen=before['jifen'],
)
return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after_jifen}})
elif caozuo == 'jia_jifen':
if '001dd' not in permissions:
return Response({'code': 403, 'msg': '无权限加打手积分'})
jifen = int(value)
dashou_profile.jifen += jifen
dashou_profile.save()
after_jifen = dashou_profile.jifen
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
jifen=after_jifen,
yjifen=before['jifen'],
)
return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after_jifen}})
# ---------- 状态修改 ----------
elif caozuo == 'gai_zhanghaozhuangtai':
if '001ee' not in permissions:
return Response({'code': 403, 'msg': '无权限修改打手账号状态'})
new_status = int(value)
if new_status not in [0, 1]:
return Response({'code': 400, 'msg': '状态值无效0=封禁,1=正常)'})
dashou_profile.zhanghaozhuangtai = new_status
dashou_profile.save()
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
qitashuoming=f'修改账号状态为{new_status}',
)
return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}})
elif caozuo == 'gai_zaixianzhuangtai':
if '001ee' not in permissions:
return Response({'code': 403, 'msg': '无权限修改打手在线状态'})
new_status = int(value)
if new_status not in [0, 1]:
return Response({'code': 400, 'msg': '状态值无效0=离线,1=在线)'})
dashou_profile.zaixianzhuangtai = new_status
dashou_profile.save()
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
qitashuoming=f'修改在线状态为{new_status}',
)
return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}})
elif caozuo == 'gai_zhuangtai':
if '001ee' not in permissions:
return Response({'code': 403, 'msg': '无权限修改打手接单状态'})
new_status = int(value)
if new_status not in [0, 1]:
return Response({'code': 400, 'msg': '状态值无效0=忙碌,1=空闲)'})
dashou_profile.zhuangtai = new_status
dashou_profile.save()
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
qitashuoming=f'修改接单状态为{new_status}',
)
return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}})
# ---------- 会员操作 ----------
elif caozuo == 'jia_huiyuan':
if '001ff' not in permissions:
return Response({'code': 403, 'msg': '无权限给打手加会员'})
if not huiyuan_id:
return Response({'code': 400, 'msg': '缺少会员ID'})
try:
huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id)
except Huiyuan.DoesNotExist:
return Response({'code': 404, 'msg': '会员类型不存在'})
from jituan.services.club_user import get_user_club_id
from jituan.services.member_recharge import extend_member_entitlement_admin, get_club_huiyuan_row
player_club = get_user_club_id(dashou_user)
row = get_club_huiyuan_row(player_club, huiyuan_id, require_enabled=False)
days = int(value) if value else int(row.formal_days or 30) if row else 30
record, err = extend_member_entitlement_admin(
dashou_id, huiyuan_id, days, player_club,
)
if err:
return Response({'code': 400, 'msg': err})
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
huiyuants=days,
huiyuan_id=huiyuan_id,
qitashuoming=f'加正式会员 {huiyuan_id},增加{days}天(无支付单、无分红)',
)
return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}})
elif caozuo == 'jian_huiyuan':
if '001gg' not in permissions:
return Response({'code': 403, 'msg': '无权限给打手减会员'})
if not huiyuan_id:
return Response({'code': 400, 'msg': '缺少会员ID'})
try:
record = Huiyuangoumai.query.get(yonghu_id=dashou_id, huiyuan_id=huiyuan_id)
except Huiyuangoumai.DoesNotExist:
return Response({'code': 404, 'msg': '该打手未购买此会员'})
# 删除会员记录(或设置为过期,根据业务要求)
record.delete()
Xiugaijilu.query.create(
yonghuid=dashou_id,
xiugaiid=kefu.user.Phone,
leixing=2,
huiyuan_id=huiyuan_id,
qitashuoming=f'移除会员 {huiyuan_id}',
)
return Response({'code': 0, 'msg': '减会员成功'})
else:
return Response({'code': 400, 'msg': '未知操作类型'})
except Exception as e:
logger.error(f"修改打手信息异常: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '服务器内部错误'})

1831
users/views/kefu_orders.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,564 @@
"""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': '处罚成功'})

View File

@@ -0,0 +1,786 @@
"""users.views.kefu_withdraw - 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__)
kefu_txsh_list_logger = logging.getLogger('yonghu.kefu_txsh_list')
_TIXIAN_LIST_BASE_FIELDS = (
'id', 'yonghuid', 'avatar', 'phone', 'nicheng', 'leixing',
'zhifu', 'skzhanghao', 'jine', 'zhuangtai', 'fangshi',
'shenheid', 'bhliyou', 'CreateTime', 'UpdateTime',
)
_TIXIAN_LIST_AUDIT_FIELDS = ('shenhe_jilu_id', 'shenhe_danhao')
class KefuWithdrawListView(APIView):
"""
客服获取提现审核列表接口
请求POST /yonghu/kefu_txsh_list
参数:{
"phone": "客服手机号",
"page": 1,
"page_size": 20,
"status": 1(待审核)/2(已处理),
"type": 1(打手佣金)/2(管事分红)/3(组长分红)/4(审核官分佣)/5(打手押金)/6(商家余额), 可选
"payment": 1(微信)/2(支付宝) 可选,
"search_uid": "用户ID搜索" 可选,
"date": "YYYY-MM-DD" 可选,
"min_amount": 0, # 最小提现金额(元)可选
"max_amount": 999999, # 最大提现金额(元)可选
"dakuan_mode": 1 # 打款方式1手动打款 2自动打款可选
}
认证JWT
返回:{
"code": 0,
"data": {
"list": [提现记录],
"total": 总数,
"stats": {
"total": 总记录数,
"awaiting": 待审核数,
"processed": 已处理数,
"total_amount": "总金额(元)"
}
}
}
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def _safe_request_params(self, request):
try:
return dict(request.data)
except Exception:
return {'raw': str(request.data)}
def _shenhe_jilu_field_available(self):
"""探测 shenhe_jilu_id 字段是否已在数据库中就绪"""
try:
Tixianjilu.query.values_list('shenhe_jilu_id', flat=True).first()
return True
except DatabaseError as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list: shenhe_jilu_id 字段不可用,将按旧表结构查询: %s', e,
exc_info=True,
)
return False
def _list_queryset(self, q, shenhe_field_ok, request):
"""字段未迁移时 only 旧字段,避免 SELECT 不存在的列导致 500"""
fields = _TIXIAN_LIST_BASE_FIELDS + (
_TIXIAN_LIST_AUDIT_FIELDS if shenhe_field_ok else ()
)
from jituan.services.tixian_club import filter_tixianjilu_by_request
qs = Tixianjilu.query.filter(q).only(*fields)
return filter_tixianjilu_by_request(qs, request)
def _resolve_row_dakuan_mode(self, item, audit_mode_map, shenhe_field_ok):
"""无关联审核或审核表未标记自动 → 默认手动(1)"""
if not shenhe_field_ok:
return 1
sid = getattr(item, 'shenhe_jilu_id', None)
if not sid:
return 1
if audit_mode_map.get(sid) == 2:
return 2
return 1
def post(self, request):
from jituan.services.group_finance_gate import require_withdraw_audit_access
dakuan_mode_filter = request.data.get('dakuan_mode')
req_params = self._safe_request_params(request)
kefu_txsh_list_logger.info('kefu_txsh_list 请求开始 params=%s', req_params)
try:
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数错误'}, status=status.HTTP_400_BAD_REQUEST)
if page < 1:
page = 1
if page_size < 1 or page_size > 100:
page_size = min(max(page_size, 1), 100)
status_filter = request.data.get('status')
type_filter = request.data.get('type')
payment_filter = request.data.get('payment')
search_uid = (request.data.get('search_uid') or '').strip()
date_filter = (request.data.get('date') or '').strip()
min_amount = request.data.get('min_amount')
max_amount = request.data.get('max_amount')
phone = (request.data.get('phone') or '').strip()
if not phone:
return Response({'code': 401, 'msg': '缺少客服手机号'}, status=status.HTTP_401_UNAUTHORIZED)
kefu, err_resp = require_withdraw_audit_access(request, phone)
if err_resp:
return err_resp
q = Q()
try:
status_val = int(status_filter) if status_filter is not None and str(status_filter).strip() != '' else None
except (TypeError, ValueError):
status_val = None
try:
dakuan_mode_val = int(dakuan_mode_filter) if dakuan_mode_filter is not None and str(dakuan_mode_filter).strip() != '' else None
except (TypeError, ValueError):
dakuan_mode_val = None
# 未传打款方式参数 → 默认手动打款(1)
effective_dakuan = dakuan_mode_val if dakuan_mode_val in (1, 2) else 1
if status_val == 1:
q &= Q(zhuangtai=1)
elif status_val == 2:
if effective_dakuan == 2:
q &= Q(zhuangtai__in=[2, 3, 4, 5, 6])
else:
q &= Q(zhuangtai__in=[2, 3])
if type_filter is not None and str(type_filter).strip() != '':
try:
type_val = int(type_filter)
if type_val in [1, 2, 3, 4, 5, 6]:
q &= Q(leixing=type_val)
except (TypeError, ValueError):
pass
if payment_filter is not None and str(payment_filter).strip() != '':
try:
payment_val = int(payment_filter)
if payment_val in [1, 2]:
q &= Q(fangshi=payment_val)
except (TypeError, ValueError):
pass
if search_uid:
q &= Q(yonghuid=search_uid)
if date_filter:
q &= Q(UpdateTime__date=date_filter)
if min_amount is not None and str(min_amount).strip() != '':
try:
q &= Q(jine__gte=float(min_amount))
except (TypeError, ValueError):
pass
if max_amount is not None and str(max_amount).strip() != '':
try:
q &= Q(jine__lte=float(max_amount))
except (TypeError, ValueError):
pass
shenhe_field_ok = self._shenhe_jilu_field_available()
if shenhe_field_ok:
if effective_dakuan == 2:
q &= Q(shenhe_jilu_id__isnull=False)
else:
q &= Q(shenhe_jilu_id__isnull=True)
elif effective_dakuan == 2:
kefu_txsh_list_logger.warning('kefu_txsh_list: 自动打款筛选不可用,返回空列表')
return Response({
'code': 0,
'data': {
'list': [],
'total': 0,
'stats': {'total': 0, 'awaiting': 0, 'processed': 0, 'total_amount': '0.00'},
},
})
processed_statuses = [2, 3, 4, 5, 6] if effective_dakuan == 2 else [2, 3]
base_qs = self._list_queryset(q, shenhe_field_ok, request)
agg = base_qs.aggregate(
total=Count('id'),
awaiting=Count('id', filter=Q(zhuangtai=1)),
processed=Count('id', filter=Q(zhuangtai__in=processed_statuses)),
total_amount=Sum('jine'),
)
stats = {
'total': agg['total'],
'awaiting': agg['awaiting'],
'processed': agg['processed'],
'total_amount': str(round(float(agg.get('total_amount') or 0), 2)),
}
queryset = base_qs.order_by('CreateTime')
total = queryset.count()
records = list(queryset[(page - 1) * page_size: page * page_size])
audit_ids = [
getattr(item, 'shenhe_jilu_id', None)
for item in records if getattr(item, 'shenhe_jilu_id', None)
]
audit_meta_map = {}
if audit_ids:
try:
audit_meta_map = load_audit_meta_map(audit_ids)
except Exception as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list: 查询审核表信息失败: %s', e,
exc_info=True,
)
audit_mode_map = {
aid: meta.get('dakuan_mode', 2) for aid, meta in audit_meta_map.items()
}
list_data = []
for item in records:
shenhe_jilu_id = getattr(item, 'shenhe_jilu_id', None) if shenhe_field_ok else None
shenhe_danhao = (getattr(item, 'shenhe_danhao', None) or '') if shenhe_field_ok else ''
if shenhe_jilu_id and not shenhe_danhao:
shenhe_danhao = audit_meta_map.get(shenhe_jilu_id, {}).get('shenhe_danhao', '')
list_data.append({
'id': item.id,
'shenhe_jilu_id': shenhe_jilu_id,
'shenhe_danhao': shenhe_danhao,
'yonghuid': item.yonghuid,
'avatar': item.avatar or '',
'phone': item.phone or '',
'nicheng': item.nicheng or '',
'leixing': item.leixing,
'zhifu': item.zhifu or '',
'skzhanghao': item.skzhanghao or '',
'jine': str(item.jine) if item.jine is not None else '0.00',
'zhuangtai': item.zhuangtai,
'fangshi': item.fangshi,
'dakuan_mode': self._resolve_row_dakuan_mode(item, audit_mode_map, shenhe_field_ok),
'shenheid': item.shenheid or '',
'bhliyou': item.bhliyou or '',
'CreateTime': item.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if item.CreateTime else '',
'UpdateTime': item.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if item.UpdateTime else '',
})
from jituan.services.club_user_access import list_response_meta
return Response({
'code': 0,
'data': {
'list': list_data,
'total': total,
'stats': stats,
**list_response_meta(request),
},
})
except DatabaseError as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list 数据库错误 dakuan_mode=%s params=%s err=%s',
dakuan_mode_filter, req_params, e,
exc_info=True,
)
return Response({
'code': 500,
'msg': '数据库查询失败,请确认已执行 python manage.py migrate yonghu',
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
kefu_txsh_list_logger.error(
'kefu_txsh_list 接口异常 dakuan_mode=%s params=%s err=%s',
dakuan_mode_filter, req_params, e,
exc_info=True,
)
return Response({
'code': 500,
'msg': '获取提现列表失败',
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class KefuWithdrawDetailView(APIView):
"""
客服获取提现详情接口
请求POST /yonghu/kefu_txsh_detail
参数:{
"phone": "客服手机号",
"tixian_id": "提现记录ID"
}
认证JWT
返回code=0 + 提现记录详情 + 用户扩展信息
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = request.data.get('phone', '').strip()
tixian_id = request.data.get('tixian_id')
if not phone or not tixian_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)
from jituan.services.group_finance_gate import require_withdraw_audit_access
_, err_resp = require_withdraw_audit_access(request, phone)
if err_resp:
return err_resp
# 查询提现记录
try:
record = Tixianjilu.query.get(id=tixian_id)
except Tixianjilu.DoesNotExist:
return Response({'code': 404, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, record)
if deny:
return deny
# 查询用户主表
try:
user = User.query.get(UserUID=record.yonghuid)
except User.DoesNotExist:
user = None
# 构建基础返回数据(字段必须与前端一致)
data = {
'id': record.id,
'yonghuid': record.yonghuid,
'avatar': record.avatar or '',
'phone': record.phone or '',
'nicheng': record.nicheng or '',
'leixing': record.leixing, # 1打手 2管事
'zhifu': record.zhifu or '',
'skzhanghao': record.skzhanghao or '',
'jine': str(record.jine) if record.jine else '0.00',
'zhuangtai': record.zhuangtai,
'fangshi': record.fangshi,
'shenheid': record.shenheid or '',
'bhliyou': record.bhliyou or '',
'CreateTime': record.CreateTime,
'UpdateTime': record.UpdateTime,
}
# 根据提现类型添加扩展信息
if user:
if record.leixing == 1: # 打手
try:
dashou = UserDashou.query.get(user=user)
# 计算成交率和退款率
total = dashou.jiedanzongliang or 0
completed = dashou.chengjiaozongliang or 0
refund = dashou.tuikuanliang or 0
completion_rate = round((completed / total * 100) if total > 0 else 0)
refund_rate = round((refund / completed * 100) if completed > 0 else 0)
data['dashouInfo'] = {
'zhuangtai': dashou.zhuangtai, # 工作状态
'zaixianzhuangtai': dashou.zaixianzhuangtai, # 在线状态
'zhanghaozhuangtai': dashou.zhanghaozhuangtai, # 账号状态
'yue': float(dashou.yue) if dashou.yue else 0.00, # 可提现余额
'zonge': float(dashou.zonge) if dashou.zonge else 0.00, # 接单总额
'jifen': dashou.jifen, # 积分
'jiedanzongliang': dashou.jiedanzongliang, # 接单总量
'chengjiaozongliang': dashou.chengjiaozongliang, # 成交总量
'tuikuanliang': dashou.tuikuanliang, # 退款总量
'completionRate': completion_rate,
'refundRate': refund_rate,
}
except ObjectDoesNotExist:
data['dashouInfo'] = None
elif record.leixing == 2: # 管事
try:
guanshi = UserGuanshi.query.get(user=user)
data['guanshiInfo'] = {
'zhuangtai': guanshi.zhuangtai, # 账号状态
'yue': float(guanshi.yue) if guanshi.yue else 0.00,
'yaogingshuliang': guanshi.yaogingshuliang, # 邀请打手总数
'jinrichongzhi': guanshi.jinrichongzhi, # 今日充值
'jinyuechongzhi': guanshi.jinyuechongzhi, # 今月充值
'chongzhifenrun': float(guanshi.chongzhifenrun) if guanshi.chongzhifenrun else 0.00,
}
except ObjectDoesNotExist:
data['guanshiInfo'] = None
return Response({'code': 0, 'data': data})
class KefuWithdrawActionView(APIView):
"""
客服处理提现接口(同意/拒绝)—— 仅此接口处理后台审核打款
POST /yonghu/kefu_txsh_action
单条:{ phone, action, tixian_id, yonghuid, leixing, reason? }
批量:{ phone, action, batch_list: [{tixian_id, yonghuid, leixing}, ...], reason? }
自动打款(dakuan_mode=2)同意→6待收款拒绝→5+驳回原因+退还可到账金额(shijidaozhang),手续费不退,不更新平台收支/每日统计
手动打款(dakuan_mode=1):原逻辑不变
状态要求Tixianjilu.zhuangtai=1 且 TixianShenheJilu.zhuangtai=1审核中其他状态一律拒绝
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def _verify_kefu(self, request, phone):
if not phone:
return None, Response({'code': 401, 'msg': '手机号不能为空'}, status=status.HTTP_400_BAD_REQUEST)
current_user = request.user
if getattr(current_user, 'Phone', '') != phone:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if current_user.UserType not in ('kefu', 'admin'):
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
# 管理员跳过客服扩展表检查
is_admin = current_user.UserType == 'admin' or current_user.IsSuperuser
if not is_admin:
try:
kefu = current_user.KefuProfile
except ObjectDoesNotExist:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
if kefu.zhuangtai != 1:
return None, Response({'code': 401, 'msg': '认证失败'}, status=status.HTTP_401_UNAUTHORIZED)
return current_user, None
def _parse_items(self, request):
"""解析单条或 batch_list返回 [{tixian_id, yonghuid, leixing}]"""
batch_list = request.data.get('batch_list')
if batch_list is not None:
if not isinstance(batch_list, list) or len(batch_list) == 0:
return None, Response({'code': 400, 'msg': 'batch_list 不能为空'}, status=status.HTTP_400_BAD_REQUEST)
items = []
for idx, row in enumerate(batch_list):
if not isinstance(row, dict):
return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 格式错误'}, status=status.HTTP_400_BAD_REQUEST)
tid = row.get('tixian_id')
yid = (row.get('yonghuid') or '').strip()
lx = row.get('leixing')
if not tid or not yid or lx is None:
return None, Response(
{'code': 400, 'msg': f'batch_list[{idx}] 缺少 tixian_id / yonghuid / leixing'},
status=status.HTTP_400_BAD_REQUEST,
)
try:
items.append({
'tixian_id': int(tid),
'yonghuid': yid,
'leixing': int(lx),
})
except (TypeError, ValueError):
return None, Response({'code': 400, 'msg': f'batch_list[{idx}] 参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
return items, None
tixian_id = request.data.get('tixian_id')
if not tixian_id:
return None, Response({'code': 400, 'msg': '提现记录ID不能为空'}, status=status.HTTP_400_BAD_REQUEST)
yonghuid = (request.data.get('yonghuid') or '').strip()
leixing = request.data.get('leixing')
try:
item = {'tixian_id': int(tixian_id), 'yonghuid': yonghuid, 'leixing': int(leixing) if leixing is not None else None}
except (TypeError, ValueError):
return None, Response({'code': 400, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
return [item], None
def _process_auto_item(self, request, item, action, reason, kefu_user):
"""自动打款单条处理(须在 transaction.atomic 内调用)"""
tixian_id = item['tixian_id']
req_yonghuid = item['yonghuid']
req_leixing = item['leixing']
if req_leixing not in [1, 2, 3, 4, 5, 6]:
raise ValueError(f'提现记录{tixian_id}:提现类型无效')
try:
tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
except Tixianjilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
if tixian.yonghuid != req_yonghuid:
raise ValueError(f'提现记录{tixian_id}用户ID不匹配')
if tixian.leixing != req_leixing:
raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
if tixian.zhuangtai != 1:
raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
if not tixian.shenhe_jilu_id:
raise ValueError(f'提现记录{tixian_id}:非自动打款审核记录')
try:
audit = TixianShenheJilu.objects.select_for_update().get(id=tixian.shenhe_jilu_id)
except TixianShenheJilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}:审核记录不存在')
if audit.yonghuid != req_yonghuid:
raise ValueError(f'提现记录{tixian_id}审核记录用户ID不匹配')
if audit.leixing != req_leixing:
raise ValueError(f'提现记录{tixian_id}:审核记录提现类型不匹配')
if audit.zhuangtai != 1:
raise ValueError(f'提现记录{tixian_id}:审核记录非审核中状态')
try:
user = User.objects.select_for_update().get(UserUID=req_yonghuid)
except User.DoesNotExist:
raise ValueError(f'用户{req_yonghuid}不存在')
if action == 1:
limit_ok, limit_msg = check_shijidaozhang_within_limit(audit.shijidaozhang)
if not limit_ok:
sync_audit_and_jilu_status(
audit, 5,
bhliyou=limit_msg,
bo_hui_ren_id=kefu_user.UserUID,
)
refund_balance(user, audit.leixing, audit.shijidaozhang)
tixian.shenheid = kefu_user.UserUID
tixian.bhliyou = limit_msg
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
raise ValueError(f'提现记录{tixian_id}{limit_msg},已自动驳回并退款')
sync_audit_and_jilu_status(audit, 6, shenhe_ren_id=kefu_user.UserUID)
tixian.shenheid = kefu_user.UserUID
tixian.save(update_fields=['shenheid', 'UpdateTime'])
else:
sync_audit_and_jilu_status(
audit, 5,
bhliyou=reason,
bo_hui_ren_id=kefu_user.UserUID,
)
# 退还可到账金额,申请时扣的 shenqing_jine 中的手续费(shouxufei)不退
refund_balance(user, audit.leixing, audit.shijidaozhang)
tixian.shenheid = kefu_user.UserUID
tixian.bhliyou = reason
tixian.save(update_fields=['shenheid', 'bhliyou', 'UpdateTime'])
def _process_manual_item(self, request, item, action, reason, phone):
"""手动打款单条处理(原逻辑,须在 transaction.atomic 内调用)"""
tixian_id = item['tixian_id']
try:
tixian = Tixianjilu.objects.select_for_update().get(id=tixian_id)
except Tixianjilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
req_yonghuid = item.get('yonghuid')
req_leixing = item.get('leixing')
if req_yonghuid and tixian.yonghuid != req_yonghuid:
raise ValueError(f'提现记录{tixian_id}用户ID不匹配')
if req_leixing is not None and tixian.leixing != req_leixing:
raise ValueError(f'提现记录{tixian_id}:提现类型不匹配')
if tixian.zhuangtai != 1:
raise ValueError(f'提现记录{tixian_id}:非审核中状态,无法处理')
if getattr(tixian, 'shenhe_jilu_id', None):
raise ValueError(f'提现记录{tixian_id}:自动打款请走自动流程')
try:
user = User.query.get(UserUID=tixian.yonghuid)
except User.DoesNotExist:
raise ValueError(f'用户{tixian.yonghuid}不存在')
from jituan.services.club_user import get_user_club_id
if action == 1:
tixian.zhuangtai = 2
try:
from jituan.services.szjilu_accounting import apply_szjilu_expense
update_daily_payout(tixian.jine, get_user_club_id(user))
apply_szjilu_expense(tixian.jine, get_user_club_id(user))
except Exception as e:
logger.error(f'更新收支记录失败: {e}')
else:
try:
refund_balance(user, tixian.leixing, tixian.jine)
except ValueError as e:
raise ValueError(str(e))
tixian.zhuangtai = 3
tixian.bhliyou = reason
tixian.shenheid = phone
tixian.UpdateTime = timezone.now()
tixian.save()
def post(self, request):
logger.info(f'接收到提现处理请求,数据: {request.data}')
phone = request.data.get('phone', '').strip()
action_raw = request.data.get('action')
reason = request.data.get('reason', '').strip()
try:
action = int(action_raw)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': 'action 参数必须为数字'}, status=status.HTTP_400_BAD_REQUEST)
if action not in [1, 2]:
return Response({'code': 400, 'msg': 'action 必须为1(同意)或2(拒绝)'}, status=status.HTTP_400_BAD_REQUEST)
if action == 2 and not reason:
return Response({'code': 400, 'msg': '拒绝提现时理由不能为空'}, status=status.HTTP_400_BAD_REQUEST)
kefu_user, err_resp = self._verify_kefu(request, phone)
if err_resp:
return err_resp
from jituan.services.group_finance_gate import require_withdraw_audit_access
_, audit_err = require_withdraw_audit_access(request, phone)
if audit_err:
return audit_err
items, err_resp = self._parse_items(request)
if err_resp:
return err_resp
is_batch = isinstance(request.data.get('batch_list'), list) and len(request.data.get('batch_list')) > 0
try:
with transaction.atomic():
for item in items:
tixian_id = item['tixian_id']
try:
tixian_probe = Tixianjilu.objects.select_for_update().get(id=tixian_id)
except Tixianjilu.DoesNotExist:
raise ValueError(f'提现记录{tixian_id}不存在')
from jituan.services.tixian_club import forbid_if_tixian_out_of_scope
deny = forbid_if_tixian_out_of_scope(request, tixian_probe)
if deny:
raise ValueError(deny.data.get('msg', '无权限处理该提现记录'))
# 有关联审核记录(shenhe_jilu_id)即为 zddksh 自动打款,不依赖 dakuan_mode 字段
is_auto = bool(getattr(tixian_probe, 'shenhe_jilu_id', None))
if is_auto:
self._process_auto_item(request, item, action, reason, kefu_user)
else:
if is_batch:
raise ValueError(f'提现记录{tixian_id}:批量操作仅支持自动打款记录')
self._process_manual_item(request, item, action, reason, phone)
except ValueError as e:
return Response({'code': 400, 'msg': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
logger.error(f'处理提现事务失败: {e}', exc_info=True)
return Response({'code': 500, 'msg': f'处理失败: {str(e)}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
msg = '批量处理成功' if is_batch and len(items) > 1 else '处理成功'
return Response({'code': 0, 'msg': msg, 'data': {'count': len(items)}})

View File

@@ -719,13 +719,15 @@ class FaKuanShenSuView(APIView):
PenaltyAppealImage.query.filter(Penalty=fadan, Purpose=2).delete()
# 插入新上传的申诉图片
for url in tupian_urls:
PenaltyAppealImage.query.create(
PenaltyAppealImage.objects.bulk_create([
PenaltyAppealImage(
Penalty=fadan,
PenalizedUserID=yonghuid,
ImageURL=url,
Purpose=2
Purpose=2,
)
for url in tupian_urls
])
return Response({'code': 0, 'msg': '申诉提交成功'})

View File

@@ -1052,7 +1052,9 @@ class TixianAssetView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
user = User.query.select_related(
'DashouProfile', 'ShopProfile', 'GuanshiProfile', 'ZuzhangProfile', 'ShenheguanProfile'
).get(UserUID=request.user.UserUID)
# 初始化返回数据默认全部为0
data = {
'dashou_yue': '0.00',