788 lines
34 KiB
Python
788 lines
34 KiB
Python
"""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.tixian_club import resolve_payout_club_id
|
||
|
||
if action == 1:
|
||
tixian.zhuangtai = 2
|
||
try:
|
||
from jituan.services.szjilu_accounting import apply_szjilu_expense
|
||
payout_club = resolve_payout_club_id(jilu=tixian, user=user)
|
||
update_daily_payout(tixian.jine, payout_club)
|
||
apply_szjilu_expense(tixian.jine, payout_club)
|
||
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)}})
|