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
1421 lines
60 KiB
Python
1421 lines
60 KiB
Python
"""users.views.tixian - 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 TixianXinxiHuoquView(APIView):
|
||
"""
|
||
获取用户提现收款信息接口
|
||
返回用户已设置的手机号、收款码、收款账号
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理获取收款信息的POST请求
|
||
直接返回用户当前的收款设置
|
||
"""
|
||
try:
|
||
# 从request.user获取当前用户
|
||
user_main = request.user
|
||
|
||
from jituan.services.club_user import get_user_club_id
|
||
from jituan.services.withdraw_config import get_tixian_feilv
|
||
|
||
club_id = get_user_club_id(user_main)
|
||
dashou_rate = get_tixian_feilv(club_id, 1)
|
||
guanshi_rate = get_tixian_feilv(club_id, 2)
|
||
|
||
# 构建返回数据
|
||
response_data = {
|
||
'txdianhua': user_main.Phone if user_main.Phone else '',
|
||
'txtupian': user_main.PaymentQRCode if user_main.PaymentQRCode else '',
|
||
'txzh': user_main.PaymentAccount if user_main.PaymentAccount else '',
|
||
'dashou_rate': dashou_rate,
|
||
'guanshi_rate': guanshi_rate,
|
||
}
|
||
|
||
# 返回成功响应
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except Exception as e:
|
||
return Response({
|
||
'code': 99,
|
||
'msg': f'系统错误: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class ShoukuanXinxiShangchuanView(APIView):
|
||
"""
|
||
上传用户收款信息接口
|
||
支持同时上传手机号、收款账号、收款码图片
|
||
使用multipart/form-data格式
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
parser_classes = (MultiPartParser,FormParser,JSONParser)
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理上传收款信息的POST请求
|
||
验证并保存用户的收款设置
|
||
"""
|
||
try:
|
||
# 获取当前用户
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
|
||
# 准备响应数据
|
||
response_data = {}
|
||
|
||
# 1. 验证必填字段:手机号必须要有
|
||
txdianhua = request.data.get('txdianhua', '').strip()
|
||
if not txdianhua:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '收款人电话不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证手机号格式(11位数字)
|
||
if not txdianhua.isdigit() or len(txdianhua) != 11:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '请输入11位有效的手机号码',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 2. 获取收款账号(可能为空)
|
||
txzh = request.data.get('txzh', '').strip()
|
||
|
||
# 3. 处理收款码图片上传(如果有)
|
||
txtupian_file = request.FILES.get('file') # 注意:前端upload.js使用'file'作为字段名
|
||
txtupian_url = ''
|
||
|
||
if txtupian_file:
|
||
# 验证图片
|
||
is_valid, error_msg = validate_image(txtupian_file)
|
||
if not is_valid:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': f'图片验证失败: {error_msg}',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 上传收款码图片
|
||
txtupian_url = self.handle_shoukuanma_upload(user_main, txtupian_file)
|
||
if txtupian_url:
|
||
response_data['txtupian'] = txtupian_url
|
||
else:
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '收款码上传失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
else:
|
||
response_data['txtupian'] = ''
|
||
|
||
# 4. 验证收款账号和收款码至少有一个
|
||
if not txzh and not txtupian_url:
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '请填写收款账号或上传收款码',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 保存所有信息到数据库
|
||
with transaction.atomic():
|
||
user_main.Phone = txdianhua
|
||
user_main.PaymentAccount = txzh if txzh else ''
|
||
|
||
# 只有上传了图片才更新zhifu字段
|
||
if txtupian_url:
|
||
user_main.PaymentQRCode = txtupian_url
|
||
|
||
user_main.save()
|
||
|
||
# 6. 构建完整的返回数据
|
||
response_data['txdianhua'] = txdianhua
|
||
response_data['txzh'] = txzh if txzh else ''
|
||
|
||
# 7. 返回成功响应
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '收款信息设置成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except Exception as e:
|
||
traceback.print_exc() # 打印详细错误信息到控制台
|
||
return Response({
|
||
'code': 99,
|
||
'msg': f'系统错误: {str(e)}',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def handle_shoukuanma_upload(self, user_main, image_file):
|
||
"""
|
||
处理收款码图片上传到腾讯云COS
|
||
存储路径: tixian/{yonghuid}/{yonghuid}.扩展名
|
||
"""
|
||
try:
|
||
yonghuid = user_main.UserUID
|
||
if not yonghuid:
|
||
return None
|
||
|
||
# 获取旧收款码URL
|
||
old_shoukuanma_url = user_main.PaymentQRCode
|
||
|
||
# 生成文件扩展名
|
||
file_name = image_file.name
|
||
file_extension = os.path.splitext(file_name)[1].lower()
|
||
if not file_extension:
|
||
file_extension = '.jpg'
|
||
|
||
# 构建OSS文件路径: tixian/{yonghuid}/{yonghuid}{扩展名}
|
||
# 使用用户ID作为文件名,确保同一用户多次上传会覆盖
|
||
oss_file_name = f"{yonghuid}{file_extension}"
|
||
oss_file_path = f"tixian/{yonghuid}/{oss_file_name}"
|
||
|
||
# 删除旧收款码(如果存在)
|
||
if old_shoukuanma_url and old_shoukuanma_url.strip():
|
||
delete_from_oss(old_shoukuanma_url)
|
||
|
||
# 上传新收款码到OSS
|
||
new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
|
||
|
||
if new_shoukuanma_url:
|
||
# 返回相对路径(从tixian文件夹开始)
|
||
return oss_file_path
|
||
else:
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"处理收款码上传失败: {e}")
|
||
return None
|
||
|
||
|
||
# ========== 订单状态常量(请根据您的实际业务修改)==========
|
||
# 假设状态值:已完成=9,已退款=10(示例,实际以您的项目为准)
|
||
ORDER_STATUS_COMPLETED = 3 # 订单已完成
|
||
ORDER_STATUS_REFUNDED = 5 # 订单已退款
|
||
ORDER_STATUS_FAIL = 6
|
||
# 进行中状态(不可提现)例如:待接单、已接单、进行中等,您需要定义排除列表
|
||
FORBIDDEN_STATUS_FOR_WITHDRAW = [1, 2, 3, 4, 5, 6, 7, 8] # 除已完成和已退款外的所有状态
|
||
|
||
|
||
class TixianShenqingView(APIView):
|
||
"""
|
||
用户提现申请接口(完整版)
|
||
支持类型:
|
||
1-打手佣金提现
|
||
2-管事分红提现
|
||
3-组长分红提现
|
||
4-审核官分佣提现
|
||
5-打手押金提现(退出平台)
|
||
6-商家余额提现(退出平台)
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
|
||
# 1. 获取并验证参数
|
||
leixing = request.data.get('leixing')
|
||
jine = request.data.get('jine')
|
||
fangshi = request.data.get('fangshi')
|
||
txskfs = request.data.get('txskfs', 0)
|
||
|
||
# 罚单检查(共用)
|
||
fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
|
||
if not fadan_ok:
|
||
return Response({'code': 400, 'msg': fadan_msg})
|
||
|
||
# 参数完整性
|
||
missing = []
|
||
if leixing is None: missing.append('leixing')
|
||
if jine is None: missing.append('jine')
|
||
if fangshi is None: missing.append('fangshi')
|
||
if missing:
|
||
return Response({'code': 1, 'msg': f'缺少参数: {", ".join(missing)}'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
leixing = int(leixing)
|
||
jine = decimal.Decimal(str(jine))
|
||
fangshi = int(fangshi)
|
||
txskfs = int(txskfs)
|
||
except (ValueError, TypeError, decimal.InvalidOperation) as e:
|
||
return Response({'code': 2, 'msg': f'参数格式错误: {str(e)}'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if jine <= decimal.Decimal('0'):
|
||
return Response({'code': 3, 'msg': '提现金额必须大于0'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 支持的提现类型(1-6)
|
||
if leixing not in [1, 2, 3, 4, 5, 6]:
|
||
return Response({'code': 4, 'msg': '无效的提现类型'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if fangshi not in [1, 2]:
|
||
return Response({'code': 5, 'msg': '无效的收款方式,只能是1(微信)或2(支付宝)'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
from jituan.services.club_user import get_user_club_id
|
||
from jituan.services.withdraw_config import get_tixian_feilv
|
||
|
||
club_id = get_user_club_id(user_main)
|
||
current_rate = decimal.Decimal('0.00')
|
||
rate_key = {
|
||
1: '5', # 打手佣金
|
||
2: '6', # 管事分红
|
||
3: '8', # 组长分红
|
||
4: '9', # 审核官分佣
|
||
5: '11', # 打手押金提现
|
||
6: '10', # 商家余额提现
|
||
}.get(leixing)
|
||
|
||
if rate_key:
|
||
try:
|
||
current_rate = get_tixian_feilv(club_id, leixing)
|
||
except ValueError:
|
||
return Response({'code': 41, 'msg': f'提现费率未配置(类型{leixing})'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
shouxufei = jine * current_rate
|
||
shijidaozhang = jine - shouxufei
|
||
if shijidaozhang <= decimal.Decimal('0'):
|
||
return Response({'code': 6, 'msg': '提现金额过低,扣除手续费后无实际到账'},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 根据提现类型执行校验和扣款
|
||
nicheng = ''
|
||
if leixing == 1:
|
||
result = self._check_dashou_commission_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
nicheng = result['data']['nicheng']
|
||
# 扣款(佣金)
|
||
with transaction.atomic():
|
||
profile = UserDashou.objects.select_for_update().get(user=user_main)
|
||
if profile.yue < jine:
|
||
return Response({'code': 13, 'msg': '余额不足'})
|
||
profile.yue = F('yue') - jine
|
||
profile.save()
|
||
elif leixing == 2:
|
||
result = self._check_guanshi_dividend_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
with transaction.atomic():
|
||
profile = UserGuanshi.objects.select_for_update().get(user=user_main)
|
||
if profile.yue < jine:
|
||
return Response({'code': 22, 'msg': '余额不足'})
|
||
profile.yue = F('yue') - jine
|
||
profile.save()
|
||
nicheng = "管事用户"
|
||
elif leixing == 3:
|
||
result = self._check_zuzhang_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
with transaction.atomic():
|
||
profile = UserZuzhang.objects.select_for_update().get(user=user_main)
|
||
if profile.ketixian_jine < jine:
|
||
return Response({'code': 32, 'msg': '可提现金额不足'})
|
||
profile.ketixian_jine = F('ketixian_jine') - jine
|
||
profile.save()
|
||
nicheng = user_main.nicheng if hasattr(user_main, 'nicheng') else ''
|
||
elif leixing == 4:
|
||
result = self._check_shenheguan_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
with transaction.atomic():
|
||
profile = UserShenheguan.objects.select_for_update().get(user=user_main)
|
||
if profile.yue < jine:
|
||
return Response({'code': 42, 'msg': '可提现金额不足'})
|
||
profile.yue = F('yue') - jine
|
||
profile.save()
|
||
nicheng = "审核官"
|
||
elif leixing == 5:
|
||
# 打手押金提现(退出)
|
||
result = self._check_dashou_yajin_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
with transaction.atomic():
|
||
profile = UserDashou.objects.select_for_update().get(user=user_main)
|
||
if profile.yajin < jine:
|
||
return Response({'code': 53, 'msg': '押金余额不足'})
|
||
profile.yajin = F('yajin') - jine
|
||
# 押金提现后封禁打手账号
|
||
profile.zhanghaozhuangtai = 0 # 0表示禁用
|
||
profile.save()
|
||
nicheng = profile.nicheng or ''
|
||
elif leixing == 6:
|
||
# 商家余额提现(退出)
|
||
result = self._check_shangjia_withdraw(user_main, jine)
|
||
if result['code'] != 0:
|
||
return Response(result, status=status.HTTP_400_BAD_REQUEST)
|
||
with transaction.atomic():
|
||
profile = UserShangjia.objects.select_for_update().get(user=user_main)
|
||
if profile.yue < jine:
|
||
return Response({'code': 63, 'msg': '商家余额不足'})
|
||
profile.yue = F('yue') - jine
|
||
# 注意:是否封禁商家账号?根据需求,用户未明确要求封禁,暂时只扣款,可后续扩展。
|
||
profile.save()
|
||
nicheng = profile.nicheng or ''
|
||
|
||
# 4. 创建提现记录(先扣款后创建,确保资金安全)
|
||
try:
|
||
avatar = user_main.Avatar or ''
|
||
phone = user_main.Phone or ''
|
||
zhifu = user_main.PaymentQRCode or ''
|
||
skzhanghao = user_main.PaymentAccount or ''
|
||
from jituan.services.club_user import get_user_club_id
|
||
tixian_record = Tixianjilu.query.create(
|
||
club_id=get_user_club_id(user_main),
|
||
yonghuid=yonghuid,
|
||
avatar=avatar,
|
||
phone=phone,
|
||
nicheng=nicheng,
|
||
leixing=leixing,
|
||
zhifu=zhifu,
|
||
skzhanghao=skzhanghao,
|
||
jine=shijidaozhang, # 记录实际到账金额
|
||
zhuangtai=1, # 审核中
|
||
fangshi=fangshi,
|
||
shenheid=None,
|
||
bhliyou='',
|
||
)
|
||
except Exception as e:
|
||
return Response({'code': 99, 'msg': f'创建提现记录失败: {str(e)}'},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# 5. 返回成功响应
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '提现申请提交成功',
|
||
'data': {
|
||
'tixian_id': tixian_record.id,
|
||
'leixing': leixing,
|
||
'jine': str(jine.quantize(decimal.Decimal('0.01'))),
|
||
'fangshi': fangshi,
|
||
'zhuangtai': 1,
|
||
'CreateTime': tixian_record.CreateTime.strftime('%Y-%m-%d %H:%M:%S'),
|
||
'current_rate': str(current_rate.quantize(decimal.Decimal('0.0000'))),
|
||
'shouxufei': str(shouxufei.quantize(decimal.Decimal('0.01'))),
|
||
'shijidaozhang': str(shijidaozhang.quantize(decimal.Decimal('0.01'))),
|
||
'tip': f'本次提现手续费率{current_rate * 100}%,手续费{shouxufei}元,实际到账{shijidaozhang}元'
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
traceback.print_exc()
|
||
return Response({'code': 99, 'msg': f'系统错误: {str(e)}'},
|
||
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# ==================== 校验函数 ====================
|
||
|
||
def _check_dashou_commission_withdraw(self, user_main, jine):
|
||
"""打手佣金提现校验"""
|
||
try:
|
||
dashou = UserDashou.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 10, 'msg': '您不是打手身份,无法提现佣金'}
|
||
|
||
if dashou.zhanghaozhuangtai != 1:
|
||
return {'code': 11, 'msg': '打手账号已被封禁,无法提现'}
|
||
if dashou.zhuangtai != 1:
|
||
return {'code': 12, 'msg': '您有订单进行中,请完成后提现'}
|
||
if dashou.jifen != 10:
|
||
return {'code': 15, 'msg': '积分不足10分,请补充积分后提现'}
|
||
if dashou.yue < jine:
|
||
return {'code': 13, 'msg': f'余额不足,当前余额: {dashou.yue}'}
|
||
|
||
trial_ok, trial_msg = check_dashou_trial_blocks_commission_withdraw(user_main.UserUID)
|
||
if not trial_ok:
|
||
return {'code': 16, 'msg': trial_msg}
|
||
|
||
return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
|
||
|
||
def _check_guanshi_dividend_withdraw(self, user_main, jine):
|
||
"""管事分红提现校验"""
|
||
try:
|
||
guanshi = UserGuanshi.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 20, 'msg': '您不是管事身份,无法提现分红'}
|
||
if guanshi.zhuangtai != 1:
|
||
return {'code': 21, 'msg': '管事账号已被封禁,无法提现'}
|
||
if guanshi.yue < jine:
|
||
return {'code': 22, 'msg': f'余额不足,当前余额: {guanshi.yue}'}
|
||
return {'code': 0, 'msg': 'ok', 'data': {}}
|
||
|
||
def _check_zuzhang_withdraw(self, user_main, jine):
|
||
"""组长分红提现校验"""
|
||
try:
|
||
zuzhang = UserZuzhang.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 30, 'msg': '您不是组长身份,无法提现组长分红'}
|
||
if zuzhang.zhuangtai != 1:
|
||
return {'code': 31, 'msg': '组长账号已被封禁,无法提现'}
|
||
if zuzhang.ketixian_jine < jine:
|
||
return {'code': 32, 'msg': f'可提现金额不足,当前可提: {zuzhang.ketixian_jine}'}
|
||
return {'code': 0, 'msg': 'ok', 'data': {}}
|
||
|
||
def _check_shenheguan_withdraw(self, user_main, jine):
|
||
"""审核官分佣提现校验"""
|
||
try:
|
||
shenheguan = UserShenheguan.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 40, 'msg': '您不是审核官身份,无法提现分佣'}
|
||
if shenheguan.zhuangtai != 1:
|
||
return {'code': 41, 'msg': '审核官账号已被封禁,无法提现'}
|
||
if shenheguan.yue < jine:
|
||
return {'code': 42, 'msg': f'可提现金额不足,当前可提: {shenheguan.yue}'}
|
||
return {'code': 0, 'msg': 'ok', 'data': {}}
|
||
|
||
|
||
def _check_shangjia_withdraw(self, user_main, jine):
|
||
"""
|
||
商家余额提现校验(退出平台)
|
||
包含:订单、自身罚单、申请的罚单检查
|
||
"""
|
||
try:
|
||
shangjia = UserShangjia.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 60, 'msg': '您不是商家身份,无法提现商家余额'}
|
||
|
||
if shangjia.zhuangtai != 1:
|
||
return {'code': 61, 'msg': '商家账号已被禁用,无法提现'}
|
||
if shangjia.yue < jine:
|
||
return {'code': 62, 'msg': f'商家余额不足,当前余额: {shangjia.yue}'}
|
||
|
||
# 订单检查(使用 Q 取反)
|
||
unfinished_orders = MerchantOrderExt.query.filter(
|
||
MerchantID=user_main.UserUID,
|
||
).exclude(
|
||
Order__Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED,ORDER_STATUS_FAIL]
|
||
).exists()
|
||
if unfinished_orders:
|
||
return {'code': 63, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提现余额'}
|
||
|
||
# 自身被处罚的罚单检查(状态不是已缴纳(2)和已驳回(4))
|
||
self_fadan = Penalty.query.filter(
|
||
PenalizedUserID=user_main.UserUID,
|
||
).exclude(
|
||
Status__in=[2, 4]
|
||
).exists()
|
||
if self_fadan:
|
||
return {'code': 64, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
|
||
|
||
# 申请的对他人处罚罚单检查(状态不是已缴纳(2)和已驳回(4))
|
||
applied_fadan = Penalty.query.filter(
|
||
ApplicantID=user_main.UserUID,
|
||
).exclude(
|
||
Status__in=[2, 4]
|
||
).exists()
|
||
if applied_fadan:
|
||
return {'code': 65, 'msg': '您发起的对其他用户的处罚尚未处理完毕(待缴纳/申诉中),请等待完结'}
|
||
|
||
return {'code': 0, 'msg': 'ok', 'data': {'nicheng': shangjia.nicheng or ''}}
|
||
|
||
def _check_dashou_yajin_withdraw(self, user_main, jine):
|
||
"""
|
||
打手押金提现校验(退出平台)
|
||
要求:无进行中的订单,无未结算订单,自身罚单需已处理
|
||
"""
|
||
try:
|
||
dashou = UserDashou.query.get(user=user_main)
|
||
except ObjectDoesNotExist:
|
||
return {'code': 50, 'msg': '您不是打手身份,无法提取押金'}
|
||
|
||
if dashou.zhanghaozhuangtai != 1:
|
||
return {'code': 51, 'msg': '打手账号已禁用,无法提取押金'}
|
||
if dashou.zhuangtai != 1:
|
||
return {'code': 52, 'msg': '您有订单进行中,请完成后提取押金'}
|
||
if dashou.yajin < jine:
|
||
return {'code': 53, 'msg': f'押金余额不足,当前押金: {dashou.yajin}'}
|
||
|
||
# 订单检查(使用 exclude + __in)
|
||
unfinished_orders = Order.query.filter(
|
||
PlayerID=user_main.UserUID,
|
||
).exclude(
|
||
Status__in=[ORDER_STATUS_COMPLETED, ORDER_STATUS_REFUNDED]
|
||
).exists()
|
||
if unfinished_orders:
|
||
return {'code': 54, 'msg': '您还有未完成或未退款的订单,请先处理完毕再提取押金'}
|
||
|
||
# 自身罚单检查
|
||
self_fadan = Penalty.query.filter(
|
||
PenalizedUserID=user_main.UserUID,
|
||
).exclude(
|
||
Status__in=[2, 4]
|
||
).exists()
|
||
if self_fadan:
|
||
return {'code': 55, 'msg': '您有未处理完毕的自身罚单(待缴纳/申诉中),请先处理'}
|
||
|
||
return {'code': 0, 'msg': 'ok', 'data': {'nicheng': dashou.nicheng or ''}}
|
||
|
||
|
||
class TixianJiluHuoquViewV2(APIView):
|
||
"""
|
||
提现记录获取接口
|
||
mode=1:手动提现记录(默认,兼容旧版)
|
||
mode=2:自动提现审核记录(含 shenhe_jilu_id / shenhe_danhao,支持 zhuangtai 筛选)
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
user_main = request.user
|
||
yonghuid = user_main.UserUID
|
||
page = request.data.get('page', 1)
|
||
limit = request.data.get('limit', 50)
|
||
mode = int(request.data.get('mode', 1) or 1)
|
||
zhuangtai_filter = int(request.data.get('zhuangtai', 0) or 0)
|
||
|
||
try:
|
||
page = int(page)
|
||
limit = int(limit)
|
||
if page < 1 or limit < 1:
|
||
return Response({'code': 1, 'msg': '参数错误', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
except (TypeError, ValueError):
|
||
return Response({'code': 1, 'msg': '参数格式错误', 'data': None},
|
||
status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if limit > 100:
|
||
limit = 100
|
||
offset = (page - 1) * limit
|
||
|
||
qs = Tixianjilu.query.filter(yonghuid=yonghuid)
|
||
if mode == 2:
|
||
# 自动提现:含新审核关联记录;zhuangtai>0 时按状态筛选
|
||
if zhuangtai_filter > 0:
|
||
qs = qs.filter(zhuangtai=zhuangtai_filter)
|
||
qs = qs.order_by('-CreateTime')
|
||
|
||
records_queryset = qs.values(
|
||
'id', 'leixing', 'jine', 'zhuangtai', 'CreateTime', 'fangshi',
|
||
'phone', 'skzhanghao', 'zhifu', 'bhliyou',
|
||
'shenhe_jilu_id', 'shenhe_danhao',
|
||
)[offset:offset + limit + 1]
|
||
|
||
records_list = list(records_queryset)
|
||
has_more = len(records_list) > limit
|
||
records_to_return = records_list[:limit] if has_more else records_list
|
||
|
||
# 批量取审核表 fail_reason
|
||
audit_ids = [r['shenhe_jilu_id'] for r in records_to_return if r.get('shenhe_jilu_id')]
|
||
audit_map = {}
|
||
if audit_ids:
|
||
for a in TixianShenheJilu.query.filter(id__in=audit_ids).values(
|
||
'id', 'fail_reason', 'shenqing_jine', 'shenhe_danhao'
|
||
):
|
||
audit_map[a['id']] = a
|
||
|
||
# 批量取自动打款失败原因
|
||
danhao_list = [r['shenhe_danhao'] for r in records_to_return if r.get('shenhe_danhao')]
|
||
auto_fail_map = {}
|
||
if danhao_list:
|
||
for ar in TixianAutoRecord.query.filter(
|
||
shenhe_danhao__in=danhao_list, zhuangtai=2
|
||
).order_by('-CreateTime').values('shenhe_danhao', 'fail_reason'):
|
||
if ar['shenhe_danhao'] not in auto_fail_map:
|
||
auto_fail_map[ar['shenhe_danhao']] = ar['fail_reason']
|
||
|
||
formatted_records = []
|
||
for record in records_to_return:
|
||
audit = audit_map.get(record.get('shenhe_jilu_id')) if record.get('shenhe_jilu_id') else None
|
||
fail_reason = ''
|
||
if audit and audit.get('fail_reason'):
|
||
fail_reason = audit['fail_reason']
|
||
elif record.get('shenhe_danhao'):
|
||
fail_reason = auto_fail_map.get(record['shenhe_danhao'], '') or ''
|
||
|
||
jine_val = float(record['jine']) if record['jine'] else 0.00
|
||
if mode == 2 and audit and audit.get('shenqing_jine') is not None:
|
||
jine_val = float(audit['shenqing_jine'])
|
||
|
||
shenhe_danhao_val = record.get('shenhe_danhao') or (audit.get('shenhe_danhao') if audit else '')
|
||
formatted_records.append({
|
||
'id': record['id'],
|
||
'tixianjilu_id': record['id'],
|
||
'tixian_id': record['id'],
|
||
'leixing': record['leixing'],
|
||
'jine': jine_val,
|
||
'zhuangtai': record['zhuangtai'],
|
||
'create': record['CreateTime'].strftime('%Y-%m-%d %H:%M:%S'),
|
||
'fangshi': record['fangshi'],
|
||
'txdianhua': record['phone'] or '',
|
||
'txzh': record['skzhanghao'] or '',
|
||
'txtupian': record['zhifu'] or '',
|
||
'shibaiyuanyin': record['bhliyou'] or '',
|
||
'bhliyou': record['bhliyou'] or '',
|
||
'fail_reason': fail_reason,
|
||
'shenhe_jilu_id': record.get('shenhe_jilu_id'),
|
||
'shenhe_danhao': shenhe_danhao_val,
|
||
})
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '获取成功',
|
||
'data': {
|
||
'list': formatted_records,
|
||
'has_more': has_more,
|
||
'page': page,
|
||
'limit': limit,
|
||
'current_count': len(formatted_records),
|
||
},
|
||
})
|
||
|
||
except Exception as e:
|
||
return Response({
|
||
'code': 99,
|
||
'msg': f'系统错误: {str(e)}',
|
||
'data': None,
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
class YonghuTixianShenheXiugaiView(APIView):
|
||
"""
|
||
用户修改审核中提现申请接口
|
||
访问路径: /api/yonghu/yonghutxshxg
|
||
请求方法: POST
|
||
权限要求: JWT Token认证通过的用户
|
||
请求体: multipart/form-data 格式
|
||
支持字段: tixian_id, fangshi, txdianhua, txzh, file (图片文件)
|
||
|
||
功能: 允许用户修改审核中提现申请的收款方式、电话、账号、收款码等
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
parser_classes = (MultiPartParser, FormParser, JSONParser)
|
||
|
||
def post(self, request):
|
||
"""
|
||
处理用户修改审核中提现申请的POST请求
|
||
"""
|
||
try:
|
||
# 1. 获取当前用户
|
||
user_main = request.user
|
||
current_yonghuid = user_main.UserUID
|
||
|
||
# 2. 验证必填字段:提现记录ID
|
||
tixian_id = request.data.get('tixian_id')
|
||
if not tixian_id:
|
||
return Response({
|
||
'code': 1,
|
||
'msg': '提现记录ID不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 查询提现记录(使用select_for_update确保事务安全)
|
||
with transaction.atomic():
|
||
try:
|
||
tixian_record = Tixianjilu.objects.select_for_update().get(
|
||
id=tixian_id,
|
||
yonghuid=current_yonghuid # 确保只能修改自己的记录
|
||
)
|
||
except Tixianjilu.DoesNotExist:
|
||
return Response({
|
||
'code': 2,
|
||
'msg': '提现记录不存在或无权修改',
|
||
'data': None
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# 4. 验证提现记录状态(必须是审核中)
|
||
if tixian_record.zhuangtai != 1:
|
||
return Response({
|
||
'code': 3,
|
||
'msg': '只有审核中的提现申请可以修改',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 5. 获取并验证提现方式
|
||
fangshi = request.data.get('fangshi')
|
||
if fangshi:
|
||
fangshi = int(fangshi)
|
||
if fangshi not in [1, 2]:
|
||
return Response({
|
||
'code': 4,
|
||
'msg': '提现方式无效,请选择1(微信)或2(支付宝)',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 检查提现类型与方式的匹配(如果前端提供了leixing)
|
||
leixing = request.data.get('leixing')
|
||
if leixing:
|
||
leixing = int(leixing)
|
||
# 这里可以根据业务逻辑添加类型与方式的验证
|
||
# 例如:某些类型只能使用某些方式
|
||
|
||
# 6. 验证手机号(必填)
|
||
txdianhua = request.data.get('txdianhua', '').strip()
|
||
if not txdianhua:
|
||
return Response({
|
||
'code': 5,
|
||
'msg': '收款人电话不能为空',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 验证手机号格式
|
||
if not txdianhua.isdigit() or len(txdianhua) != 11:
|
||
return Response({
|
||
'code': 6,
|
||
'msg': '请输入11位有效的手机号码',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 7. 获取收款账号(可选)
|
||
txzh = request.data.get('txzh', '').strip()
|
||
|
||
# 8. 处理收款码图片上传(如果有)
|
||
txtupian_file = request.FILES.get('file')
|
||
new_zhifu_url = None
|
||
old_zhifu_url = tixian_record.zhifu
|
||
|
||
if txtupian_file:
|
||
# 验证图片
|
||
is_valid, error_msg = validate_image(txtupian_file)
|
||
if not is_valid:
|
||
return Response({
|
||
'code': 7,
|
||
'msg': f'图片验证失败: {error_msg}',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 上传收款码图片
|
||
new_zhifu_url = self.handle_shoukuanma_upload(
|
||
user_main,
|
||
txtupian_file,
|
||
old_zhifu_url,
|
||
tixian_record.leixing
|
||
)
|
||
|
||
if not new_zhifu_url:
|
||
return Response({
|
||
'code': 8,
|
||
'msg': '收款码上传失败',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
# 9. 验证收款信息(账号和图片至少有一个)
|
||
if not txzh and not (new_zhifu_url or old_zhifu_url):
|
||
return Response({
|
||
'code': 9,
|
||
'msg': '请填写收款账号或上传收款码',
|
||
'data': None
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 10. 更新提现记录
|
||
update_fields = []
|
||
|
||
# 更新提现方式(如果有)
|
||
if fangshi:
|
||
tixian_record.fangshi = fangshi
|
||
update_fields.append('fangshi')
|
||
|
||
# 更新手机号(必填)
|
||
tixian_record.phone = txdianhua
|
||
update_fields.append('phone')
|
||
|
||
# 更新收款账号(如果有)
|
||
if txzh is not None: # 注意:空字符串也要更新
|
||
tixian_record.skzhanghao = txzh
|
||
update_fields.append('skzhanghao')
|
||
|
||
# 更新收款码图片(如果有新图片)
|
||
if new_zhifu_url:
|
||
tixian_record.zhifu = new_zhifu_url
|
||
update_fields.append('zhifu')
|
||
|
||
# 保存更新
|
||
if update_fields:
|
||
tixian_record.save(update_fields=update_fields)
|
||
|
||
# 11. 构建返回数据
|
||
response_data = {
|
||
'tixian_id': tixian_record.id,
|
||
'fangshi': tixian_record.fangshi,
|
||
'txdianhua': tixian_record.phone,
|
||
'txzh': tixian_record.skzhanghao or '',
|
||
'txtupian': tixian_record.zhifu or '' # 返回相对URL
|
||
}
|
||
|
||
# 12. 返回成功响应
|
||
return Response({
|
||
'code': 0,
|
||
'msg': '提现申请修改成功',
|
||
'data': response_data
|
||
})
|
||
|
||
except Exception as e:
|
||
# 记录详细错误日志
|
||
logger.error(
|
||
f"修改提现申请异常 - 用户ID: {getattr(user_main, 'yonghuid', 'N/A')}, "
|
||
f"提现ID: {tixian_id}, "
|
||
f"错误类型: {type(e).__name__}, "
|
||
f"错误详情: {str(e)}",
|
||
exc_info=True
|
||
)
|
||
|
||
# 返回友好错误信息
|
||
return Response({
|
||
'code': 99,
|
||
'msg': '系统繁忙,请稍后重试',
|
||
'data': None
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
def handle_shoukuanma_upload(self, user_main, image_file, old_zhifu_url, leixing=None):
|
||
"""
|
||
处理收款码图片上传到腾讯云COS
|
||
存储路径: tixian/{yonghuid}/{timestamp_uuid}.扩展名
|
||
|
||
参数:
|
||
- user_main: 当前用户对象
|
||
- image_file: 图片文件对象
|
||
- old_zhifu_url: 旧收款码URL(用于删除)
|
||
- leixing: 提现类型(可选,用于文件夹分类)
|
||
|
||
返回: 新的图片相对路径
|
||
"""
|
||
try:
|
||
yonghuid = user_main.UserUID
|
||
if not yonghuid:
|
||
return None
|
||
|
||
# 删除旧收款码(如果存在且不是空字符串)
|
||
if old_zhifu_url and old_zhifu_url.strip():
|
||
delete_from_oss(old_zhifu_url)
|
||
|
||
# 获取文件扩展名
|
||
file_name = image_file.name
|
||
file_extension = os.path.splitext(file_name)[1].lower()
|
||
if not file_extension:
|
||
file_extension = '.jpg'
|
||
|
||
# 生成唯一文件名(使用时间戳+UUID确保不重复)
|
||
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
|
||
unique_id = str(uuid.uuid4())[:8] # 取UUID前8位
|
||
unique_file_name = f"{timestamp}_{unique_id}{file_extension}"
|
||
|
||
# 根据提现类型构建文件夹路径(如果提供了leixing)
|
||
if leixing:
|
||
# leixing=1:打手佣金, leixing=2:管事分红
|
||
type_folder = 'dashou' if leixing == 1 else 'guanshi'
|
||
oss_file_path = f"tixian/{type_folder}/{yonghuid}/{unique_file_name}"
|
||
else:
|
||
# 默认路径
|
||
oss_file_path = f"tixian/{yonghuid}/{unique_file_name}"
|
||
|
||
# 上传新收款码到OSS
|
||
new_shoukuanma_url = upload_to_oss(image_file, oss_file_path)
|
||
|
||
if new_shoukuanma_url:
|
||
# 返回相对路径(从tixian文件夹开始)
|
||
return oss_file_path
|
||
else:
|
||
return None
|
||
|
||
except Exception as e:
|
||
print(f"处理收款码上传失败: {e}")
|
||
return None
|
||
|
||
|
||
class TixianAssetView(APIView):
|
||
"""
|
||
获取用户所有可提现资产及费率
|
||
请求方式:POST
|
||
认证:JWT Token
|
||
返回:
|
||
{
|
||
"code": 200,
|
||
"msg": "success",
|
||
"data": {
|
||
"dashou_yue": "123.45",
|
||
"dashou_yajin": "500.00",
|
||
"shangjia_yue": "0.00",
|
||
"guanshi_fenyong": "67.89",
|
||
"zuzhang_fenyong": "0.00",
|
||
"kaoheguan_fenyong": "0.00",
|
||
"dashou_rate": 0.08,
|
||
"dashou_yajin_rate": 0.05,
|
||
"shangjia_rate": 0.06,
|
||
"guanshi_rate": 0.08,
|
||
"zuzhang_rate": 0.05,
|
||
"kaoheguan_rate": 0.07
|
||
}
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
user = User.query.select_related(
|
||
'DashouProfile', 'ShopProfile', 'GuanshiProfile', 'ZuzhangProfile', 'ShenheguanProfile'
|
||
).get(UserUID=request.user.UserUID)
|
||
# 初始化返回数据,默认全部为0
|
||
data = {
|
||
'dashou_yue': '0.00',
|
||
'dashou_yajin': '0.00',
|
||
'shangjia_yue': '0.00',
|
||
'guanshi_fenyong': '0.00',
|
||
'zuzhang_fenyong': '0.00',
|
||
'kaoheguan_fenyong': '0.00',
|
||
'dashou_rate': 0.00,
|
||
'dashou_yajin_rate': 0.00,
|
||
'shangjia_rate': 0.00,
|
||
'guanshi_rate': 0.00,
|
||
'zuzhang_rate': 0.00,
|
||
'kaoheguan_rate': 0.00,
|
||
}
|
||
|
||
# ------------------- 打手资产 -------------------
|
||
try:
|
||
dashou = user.DashouProfile
|
||
data['dashou_yue'] = str(dashou.yue)
|
||
data['dashou_yajin'] = str(dashou.yajin)
|
||
except ObjectDoesNotExist:
|
||
pass
|
||
|
||
# ------------------- 商家资产 -------------------
|
||
try:
|
||
shangjia = user.ShopProfile
|
||
data['shangjia_yue'] = str(shangjia.yue)
|
||
except ObjectDoesNotExist:
|
||
pass
|
||
|
||
# ------------------- 管事资产 -------------------
|
||
try:
|
||
guanshi = user.GuanshiProfile
|
||
data['guanshi_fenyong'] = str(guanshi.yue)
|
||
except ObjectDoesNotExist:
|
||
pass
|
||
|
||
# ------------------- 组长资产 -------------------
|
||
try:
|
||
zuzhang = user.ZuzhangProfile
|
||
data['zuzhang_fenyong'] = str(zuzhang.ketixian_jine)
|
||
except ObjectDoesNotExist:
|
||
pass
|
||
|
||
# ------------------- 审核官(考核官)资产 -------------------
|
||
try:
|
||
kaoheguan = user.ShenheguanProfile
|
||
data['kaoheguan_fenyong'] = str(kaoheguan.yue)
|
||
except ObjectDoesNotExist:
|
||
pass
|
||
|
||
from jituan.services.club_user import get_user_club_id
|
||
from jituan.services.withdraw_config import build_tixian_asset_rates
|
||
|
||
club_id = get_user_club_id(user)
|
||
data.update(build_tixian_asset_rates(club_id))
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '获取成功',
|
||
'data': data
|
||
}, status=status.HTTP_200_OK)
|
||
|
||
|
||
class GuanshiContactView(APIView):
|
||
"""
|
||
管事获取自己的邀请人(组长)信息
|
||
路径: /api/yonghu/guanshi_contact
|
||
方法: POST
|
||
权限: JWT认证
|
||
请求体: 无(仅需携带合法token)
|
||
成功返回:
|
||
code: 200,
|
||
data: {
|
||
uid: "1234567", // 组长用户ID
|
||
nicheng: "张三", // 组长昵称
|
||
touxiang: "avatar/xxx.jpg" // 组长头像相对路径(前端拼接完整URL)
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
# 1. 获取当前登录用户(必须已通过JWT认证)
|
||
current_user = request.user # 类型: User
|
||
|
||
# 2. 校验是否为管事身份(存在管事扩展表记录)
|
||
try:
|
||
guanshi_profile = current_user.GuanshiProfile
|
||
except ObjectDoesNotExist:
|
||
return Response({
|
||
'code': 403,
|
||
'msg': '您还不是管事,无法获取组长信息',
|
||
'data': None
|
||
})
|
||
|
||
# 3. 获取邀请人ID(管事扩展表中的 yaoqingren 字段存储的是邀请人的 yonghuid)
|
||
inviter_yonghuid = guanshi_profile.yaoqingren
|
||
if not inviter_yonghuid:
|
||
return Response({
|
||
'code': 404,
|
||
'msg': '未找到邀请人信息',
|
||
'data': None
|
||
})
|
||
|
||
# 4. 根据邀请人用户ID查询用户主表
|
||
try:
|
||
inviter_user = User.query.get(UserUID=inviter_yonghuid)
|
||
except User.DoesNotExist:
|
||
logger.error(f"邀请人用户不存在: yonghuid={inviter_yonghuid}")
|
||
return Response({
|
||
'code': 404,
|
||
'msg': '邀请人账号异常',
|
||
'data': None
|
||
})
|
||
|
||
# 5. 获取邀请人昵称(从老板扩展表取,若不存在则给默认值)
|
||
try:
|
||
inviter_nicheng = inviter_user.BossProfile.nickname
|
||
except ObjectDoesNotExist:
|
||
inviter_nicheng = '组长' # 兜底昵称
|
||
|
||
# 6. 构造返回数据
|
||
response_data = {
|
||
'uid': inviter_user.UserUID,
|
||
'nicheng': inviter_nicheng,
|
||
'touxiang': inviter_user.Avatar or '' # 头像相对路径
|
||
}
|
||
|
||
return Response({
|
||
'code': 200,
|
||
'msg': '获取成功',
|
||
'data': response_data
|
||
})
|
||
|
||
|
||
def generate_tixian_id():
|
||
"""生成唯一提现订单号:TX + 时间戳(13位) + 4位随机数"""
|
||
timestamp = str(int(time.time() * 1000))
|
||
rand = ''.join(random.choices(string.digits, k=4))
|
||
return f'TX{timestamp}{rand}'
|
||
|
||
|
||
class TixianShenqingV3View(APIView):
|
||
"""
|
||
自动提现收款接口(新流程第二步)
|
||
POST /yonghu/tixiansq
|
||
必须传 shenhe_danhao;审核状态须为 6=待收款;申请时已扣款,本接口不再扣款
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip()
|
||
if shenhe_danhao:
|
||
return process_audit_collect(request)
|
||
return Response({
|
||
'code': 7,
|
||
'msg': '缺少审核单号,请先提交提现申请(/yonghu/zddksh)并等待审核通过后再收款',
|
||
})
|
||
|
||
|
||
class TixianCallbackV3View(APIView):
|
||
"""
|
||
微信转账结果回调接口(最终状态通知)
|
||
处理微信异步发送的转账成功/失败结果
|
||
"""
|
||
permission_classes = [] # 无需认证,签名验证保证安全
|
||
|
||
def post(self, request):
|
||
headers = request.headers
|
||
body = request.body.decode('utf-8')
|
||
|
||
# 1. 验证签名(多商户依次验签)
|
||
from utils.wechat_v3 import resolve_callback_club_id, decrypt_callback_ciphertext
|
||
callback_club_id = resolve_callback_club_id(headers, body)
|
||
if not callback_club_id:
|
||
return Response({'code': 'FAIL', 'message': '签名验证失败'}, status=status.HTTP_401_UNAUTHORIZED)
|
||
|
||
# 2. 解密数据
|
||
try:
|
||
data = json.loads(body)
|
||
resource = data.get('resource')
|
||
if not resource:
|
||
return Response({'code': 'FAIL', 'message': 'resource不存在'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
associated_data = resource.get('associated_data', '')
|
||
nonce = resource.get('nonce')
|
||
ciphertext = resource.get('ciphertext')
|
||
if not nonce or not ciphertext:
|
||
return Response({'code': 'FAIL', 'message': '解密字段缺失'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
plaintext = decrypt_callback_ciphertext(associated_data, nonce, ciphertext, club_id=callback_club_id)
|
||
callback_data = json.loads(plaintext)
|
||
except Exception as e:
|
||
logger.error(f"回调解密失败: {str(e)}", exc_info=True)
|
||
return Response({'code': 'FAIL', 'message': f'解密失败: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 3. 获取关键信息
|
||
out_bill_no = callback_data.get('out_bill_no') # 商户提现单号(即 tixian_id)
|
||
transfer_status = callback_data.get('transfer_status') # SUCCESS / FAIL
|
||
wechat_transfer_no = callback_data.get('transfer_no') # 微信转账单号
|
||
fail_reason = callback_data.get('fail_reason', '')
|
||
|
||
if not out_bill_no:
|
||
return Response({'code': 'FAIL', 'message': 'out_bill_no缺失'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# 4. 幂等处理:只更新状态为0(处理中)的记录,已最终状态不再处理
|
||
with transaction.atomic():
|
||
try:
|
||
auto_record = TixianAutoRecord.objects.select_for_update().get(tixian_id=out_bill_no)
|
||
except TixianAutoRecord.DoesNotExist:
|
||
# 记录不存在,视为已处理,返回成功
|
||
return Response({'code': 'SUCCESS', 'message': 'ok'})
|
||
|
||
# 如果已经是最新状态,直接返回成功(幂等)
|
||
if auto_record.zhuangtai != 0:
|
||
return Response({'code': 'SUCCESS', 'message': 'already processed'})
|
||
|
||
if transfer_status == 'SUCCESS':
|
||
mark_transfer_success(
|
||
auto_record,
|
||
wechat_transfer_no=wechat_transfer_no,
|
||
with_accounting=True,
|
||
)
|
||
|
||
else: # transfer_status == 'FAIL'
|
||
auto_record.zhuangtai = 2
|
||
auto_record.fail_reason = fail_reason or '微信转账失败'
|
||
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
|
||
release_collect_quota_for_record(auto_record)
|
||
|
||
if not auto_record.shenhe_danhao:
|
||
logger.error('回调失败但打款记录无审核单号 bill=%s', out_bill_no)
|
||
else:
|
||
try:
|
||
audit = TixianShenheJilu.objects.select_for_update().get(
|
||
shenhe_danhao=auto_record.shenhe_danhao
|
||
)
|
||
close_audit_wechat_failed(
|
||
audit, None, auto_record.fail_reason,
|
||
)
|
||
except TixianShenheJilu.DoesNotExist:
|
||
logger.warning(f'回调失败但审核记录不存在: {auto_record.shenhe_danhao}')
|
||
|
||
return Response({'code': 'SUCCESS', 'message': 'ok'})
|
||
|
||
|
||
class TixianQueRenAutoView(APIView):
|
||
"""
|
||
用户确认提现结果
|
||
POST /yonghu/tixianqr
|
||
以微信官方查单状态为准:SUCCESS 落库成功;FAIL/CANCELLED 关闭审核单+退款;进行中则提示等待
|
||
"""
|
||
permission_classes = [permissions.IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
tixian_id = request.data.get('tixian_id')
|
||
result = request.data.get('result') # 1成功 0失败/取消
|
||
|
||
if not tixian_id or result is None:
|
||
return Response({'code': 1, 'msg': '缺少参数'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
try:
|
||
result = int(result)
|
||
except ValueError:
|
||
return Response({'code': 2, 'msg': '参数格式错误'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if result not in [0, 1]:
|
||
return Response({'code': 3, 'msg': 'result值无效'}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
user_main = request.user
|
||
|
||
with transaction.atomic():
|
||
try:
|
||
auto_record = TixianAutoRecord.objects.select_for_update().get(
|
||
tixian_id=tixian_id,
|
||
yonghuid=user_main.UserUID,
|
||
)
|
||
except TixianAutoRecord.DoesNotExist:
|
||
return Response({'code': 4, 'msg': '提现记录不存在'}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
if result == 1:
|
||
if auto_record.zhuangtai == 1:
|
||
return Response({'code': 0, 'msg': '提现成功', 'data': None})
|
||
|
||
wx_data = query_wechat_transfer_bill(tixian_id)
|
||
if wx_data is None:
|
||
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
|
||
|
||
if wx_data.get('_not_found'):
|
||
return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
|
||
|
||
state = (wx_data.get('state') or '').upper()
|
||
if state in WX_STATE_SUCCESS:
|
||
mark_transfer_success(
|
||
auto_record,
|
||
wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'),
|
||
with_accounting=True,
|
||
)
|
||
return Response({'code': 0, 'msg': '提现成功', 'data': None})
|
||
|
||
if state in WX_STATE_FAIL:
|
||
fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or state
|
||
auto_record.zhuangtai = 2
|
||
auto_record.fail_reason = str(fail_reason)[:500]
|
||
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
|
||
release_collect_quota_for_record(auto_record)
|
||
if auto_record.shenhe_danhao:
|
||
audit = TixianShenheJilu.objects.select_for_update().get(
|
||
shenhe_danhao=auto_record.shenhe_danhao,
|
||
)
|
||
close_audit_wechat_failed(audit, None, auto_record.fail_reason)
|
||
return Response({'code': 400, 'msg': '微信已确认转账失败,该笔提现已关闭,请联系客服处理'})
|
||
|
||
if state in WX_STATE_WAIT or state in WX_STATE_CANCELING:
|
||
return Response({'code': 12, 'msg': '收款处理中,请在微信中完成或稍后再试'})
|
||
|
||
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
|
||
|
||
# 用户报告取消/失败:以微信查单为准,不本地盲目标记
|
||
if auto_record.zhuangtai == 2:
|
||
return Response({'code': 0, 'msg': '提现已取消', 'data': None})
|
||
if auto_record.zhuangtai == 1:
|
||
return Response({'code': 0, 'msg': '提现成功', 'data': None})
|
||
|
||
wx_data = query_wechat_transfer_bill(tixian_id)
|
||
if wx_data is None:
|
||
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试,切勿重复操作'})
|
||
|
||
if wx_data.get('_not_found'):
|
||
return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
|
||
|
||
state = (wx_data.get('state') or '').upper()
|
||
if state in WX_STATE_SUCCESS:
|
||
mark_transfer_success(
|
||
auto_record,
|
||
wechat_transfer_no=wx_data.get('transfer_bill_no') or wx_data.get('transfer_no'),
|
||
with_accounting=True,
|
||
)
|
||
return Response({'code': 0, 'msg': '提现成功', 'data': None})
|
||
|
||
if state in WX_STATE_FAIL:
|
||
fail_reason = wx_data.get('fail_reason') or wx_data.get('close_reason') or '用户取消收款'
|
||
auto_record.zhuangtai = 2
|
||
auto_record.fail_reason = str(fail_reason)[:500]
|
||
auto_record.save(update_fields=['zhuangtai', 'fail_reason'])
|
||
release_collect_quota_for_record(auto_record)
|
||
if auto_record.shenhe_danhao:
|
||
audit = TixianShenheJilu.objects.select_for_update().get(
|
||
shenhe_danhao=auto_record.shenhe_danhao,
|
||
)
|
||
close_audit_wechat_failed(audit, None, auto_record.fail_reason)
|
||
return Response({'code': 0, 'msg': '提现已关闭,请联系客服处理'})
|
||
|
||
if state in WX_STATE_WAIT or state in WX_STATE_CANCELING:
|
||
return Response({'code': 12, 'msg': '收款仍在进行中,请在微信中操作或稍后再试'})
|
||
|
||
return Response({'code': 12, 'msg': '系统正在核对微信状态,请稍后再试'})
|
||
|
||
except Exception as e:
|
||
logger.error(f'提现确认异常: {str(e)}', exc_info=True)
|
||
return Response({'code': 99, 'msg': '系统错误'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|