Files
Django/users/views/kefu_withdraw.py
TermiNexus f1c8633345 perf: P1 性能优化批量修复 + kefu 视图拆分
P1 性能优化(避免循环内 N+1 / N 次 DB 查询):
- shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组
- product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断
- roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取
- guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete
- admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update
- jituan 服务层多处合并统计查询、批量预取 map
- rank 多处循环 N+1 改批量预取
- backend 多处循环内 count/create 改批量
- config/orders/merchant_ops 多处循环 N+1 改批量预取

其他改动:
- users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件
- 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
2026-07-05 23:17:38 +08:00

787 lines
34 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""users.views.kefu_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)}})