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:
@@ -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,
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
# 构建响应数据
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
3794
users/views/kefu.py
3794
users/views/kefu.py
File diff suppressed because it is too large
Load Diff
383
users/views/kefu_base.py
Normal file
383
users/views/kefu_base.py
Normal 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. 返回成功数据(管理员无 KefuProfile,nicheng 用 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
636
users/views/kefu_dashou.py
Normal 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
1831
users/views/kefu_orders.py
Normal file
File diff suppressed because it is too large
Load Diff
564
users/views/kefu_punishment.py
Normal file
564
users/views/kefu_punishment.py
Normal 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': '处罚成功'})
|
||||
786
users/views/kefu_withdraw.py
Normal file
786
users/views/kefu_withdraw.py
Normal 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)}})
|
||||
@@ -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': '申诉提交成功'})
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user