修复了管理员无法进后台的问题

This commit is contained in:
2026-06-19 01:58:34 +08:00
parent 5b9436b994
commit d71a81f279
22 changed files with 4396 additions and 4295 deletions

View File

@@ -0,0 +1,56 @@
# Generated by Django 4.2.27 on 2026-06-19 01:08
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('users', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='adminprofile',
name='user',
field=models.OneToOneField(db_column='UserUUID', on_delete=django.db.models.deletion.CASCADE, related_name='AdminProfile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户'),
),
migrations.AlterField(
model_name='userboss',
name='user',
field=models.OneToOneField(db_column='UserUUID', on_delete=django.db.models.deletion.CASCADE, related_name='BossProfile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户'),
),
migrations.AlterField(
model_name='userdashou',
name='user',
field=models.OneToOneField(db_column='UserUUID', on_delete=django.db.models.deletion.CASCADE, related_name='DashouProfile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户'),
),
migrations.AlterField(
model_name='userguanshi',
name='user',
field=models.OneToOneField(db_column='UserUUID', on_delete=django.db.models.deletion.CASCADE, related_name='GuanshiProfile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户'),
),
migrations.AlterField(
model_name='userkefu',
name='user',
field=models.OneToOneField(db_column='UserUUID', on_delete=django.db.models.deletion.CASCADE, related_name='KefuProfile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户'),
),
migrations.AlterField(
model_name='usershangjia',
name='user',
field=models.OneToOneField(db_column='UserUUID', on_delete=django.db.models.deletion.CASCADE, related_name='ShopProfile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户'),
),
migrations.AlterField(
model_name='usershenheguan',
name='user',
field=models.OneToOneField(db_column='UserUUID', on_delete=django.db.models.deletion.CASCADE, related_name='ShenheguanProfile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户'),
),
migrations.AlterField(
model_name='userzuzhang',
name='user',
field=models.OneToOneField(db_column='UserUUID', on_delete=django.db.models.deletion.CASCADE, related_name='ZuzhangProfile', to=settings.AUTH_USER_MODEL, verbose_name='关联用户'),
),
]

File diff suppressed because it is too large Load Diff

View File

@@ -209,17 +209,17 @@ class PhbHqsjView(APIView):
return {}, {}
users = User.objects.filter(UserUID__in=yonghuids)
avatar_map = {u.yonghuid: u.avatar or '' for u in users}
avatar_map = {u.UserUID: u.Avatar or '' for u in users}
nick_map = {}
if shenfen == 'dashou':
for p in UserDashou.objects.filter(user__UserUID__in=yonghuids).select_related('user'):
nick_map[p.user.yonghuid] = p.nicheng or '用户'
nick_map[p.user.UserUID] = p.nicheng or '用户'
elif shenfen == 'shangjia':
for p in UserShangjia.objects.filter(user__UserUID__in=yonghuids).select_related('user'):
nick_map[p.user.yonghuid] = p.nicheng or '用户'
nick_map[p.user.UserUID] = p.nicheng or '用户'
else:
for b in UserBoss.objects.filter(user__UserUID__in=yonghuids).select_related('user'):
nick_map[b.user.yonghuid] = b.nickname or '用户'
nick_map[b.user.UserUID] = b.nickname or '用户'
return nick_map, avatar_map

View File

@@ -1,281 +1,281 @@
# yonghu/ranking_tasks.py
"""
排行榜数据转移任务 - 生产环境专用
在清空前将数据转移到排行榜表
"""
from celery import shared_task
from django.db import transaction
from django.utils import timezone
from datetime import datetime, timedelta
import logging
from decimal import Decimal
from users.models import (
UserDashou, UserShangjia, UserGuanshi, UserBoss,
RankingRecord
)
from gvsdsdk.models import User
logger = logging.getLogger(__name__)
def get_user_nicheng(user, shenfen):
"""
获取用户昵称
规则打手用打手nicheng其他用老板nickname
"""
try:
if shenfen == 2: # 打手
dashou_profile = getattr(user, 'dashou_profile', None)
if dashou_profile and dashou_profile.nicheng:
return dashou_profile.nicheng
else: # 管事或商家
boss_profile = getattr(user, 'boss_profile', None)
if boss_profile and boss_profile.nickname:
return boss_profile.nickname
# 默认返回用户ID
return f"用户{user.yonghuid}"
except Exception:
return f"用户{user.yonghuid}"
@shared_task
def zhuanyi_ribang():
"""
转移日榜数据 - 每天23:55执行
在清零前将今日数据转移到排行榜表
"""
try:
# 统计日期(今天)
today = timezone.now().date()
logger.info(f"开始转移日榜数据,日期: {today}")
records_to_create = []
# 1. 转移打手日榜(今日收益 > 0
dashou_list = UserDashou.objects.filter(
jinrishouyi__gt=0 # 只转移收益大于0的
).select_related('user').order_by('-jinrishouyi')
for i, dashou in enumerate(dashou_list, start=1):
if i > 100: # 最多100条
break
user = dashou.user
record = RankingRecord(
zhouqi=1, # 日榜
tongji_date=today,
shenfen=2, # 打手
yonghuid=user.yonghuid,
paiming=i,
zhizhi=dashou.jinrishouyi,
nicheng=get_user_nicheng(user, 2),
avatar=user.avatar or ''
)
records_to_create.append(record)
# 2. 转移商家日榜(今日流水 > 0
shangjia_list = UserShangjia.objects.filter(
jinriliushui__gt=0 # 只转移流水大于0的
).select_related('user').order_by('-jinriliushui')
for i, shangjia in enumerate(shangjia_list, start=1):
if i > 100: # 最多100条
break
user = shangjia.user
record = RankingRecord(
zhouqi=1,
tongji_date=today,
shenfen=4, # 商家
yonghuid=user.yonghuid,
paiming=i,
zhizhi=shangjia.jinriliushui,
nicheng=get_user_nicheng(user, 4),
avatar=user.avatar or ''
)
records_to_create.append(record)
# 3. 转移管事日榜(今日充值人数 > 0
guanshi_list = UserGuanshi.objects.filter(
jinrichongzhi__gt=0 # 只转移充值人数大于0的
).select_related('user').order_by('-jinrichongzhi')
for i, guanshi in enumerate(guanshi_list, start=1):
if i > 100: # 最多100条
break
user = guanshi.user
record = RankingRecord(
zhouqi=1,
tongji_date=today,
shenfen=3, # 管事
yonghuid=user.yonghuid,
paiming=i,
zhizhi=Decimal(guanshi.jinrichongzhi),
nicheng=get_user_nicheng(user, 3),
avatar=user.avatar or ''
)
records_to_create.append(record)
# 批量保存
if records_to_create:
with transaction.atomic():
RankingRecord.objects.bulk_create(records_to_create)
logger.info(f"日榜数据转移完成,保存{len(records_to_create)}条记录")
result = {
'success': True,
'date': today.strftime('%Y-%m-%d'),
'total_count': len(records_to_create),
}
return result
except Exception as e:
logger.error(f"日榜数据转移失败: {str(e)}", exc_info=True)
raise
@shared_task
def zhuanyi_yuebang():
"""
转移月榜数据 - 每月最后一天23:50执行
在清零前将本月数据转移到排行榜表
"""
try:
# 🔥 获取当前时间
now = timezone.now()
today = now.date()
# 🔥 判断今天是否是当月的最后一天
# 方法:计算明天的日期,如果明天的月份和今天不同,那么今天就是最后一天
tomorrow = today + timedelta(days=1)
if tomorrow.month == today.month:
# 今天不是最后一天,直接返回
logger.info(f"今天不是当月最后一天({today}),跳过月榜转移")
return {
'success': True,
'message': f'今天不是当月最后一天,跳过执行',
'is_last_day': False,
'today': today.strftime('%Y-%m-%d'),
}
logger.info(f"今天是当月最后一天({today}),开始转移月榜数据")
# 🔥 统计日期为当月第一天如2024-01-01
tongji_date = datetime(today.year, today.month, 1).date()
records_to_create = []
# 1. 转移打手月榜(今月收益 > 0
dashou_list = UserDashou.objects.filter(
jinyueshouyi__gt=0 # 只转移收益大于0的
).select_related('user').order_by('-jinyueshouyi')
for i, dashou in enumerate(dashou_list, start=1):
if i > 100: # 最多100条
break
user = dashou.user
record = RankingRecord(
zhouqi=2, # 月榜
tongji_date=tongji_date,
shenfen=2, # 打手
yonghuid=user.yonghuid,
paiming=i,
zhizhi=dashou.jinyueshouyi,
nicheng=get_user_nicheng(user, 2),
avatar=user.avatar or ''
)
records_to_create.append(record)
# 2. 转移商家月榜(今月流水 > 0
shangjia_list = UserShangjia.objects.filter(
jinyueliushui__gt=0 # 只转移流水大于0的
).select_related('user').order_by('-jinyueliushui')
for i, shangjia in enumerate(shangjia_list, start=1):
if i > 100: # 最多100条
break
user = shangjia.user
record = RankingRecord(
zhouqi=2,
tongji_date=tongji_date,
shenfen=4, # 商家
yonghuid=user.yonghuid,
paiming=i,
zhizhi=shangjia.jinyueliushui,
nicheng=get_user_nicheng(user, 4),
avatar=user.avatar or ''
)
records_to_create.append(record)
# 3. 转移管事月榜(今月充值人数 > 0
guanshi_list = UserGuanshi.objects.filter(
jinyuechongzhi__gt=0 # 只转移充值人数大于0的
).select_related('user').order_by('-jinyuechongzhi')
for i, guanshi in enumerate(guanshi_list, start=1):
if i > 100: # 最多100条
break
user = guanshi.user
record = RankingRecord(
zhouqi=2,
tongji_date=tongji_date,
shenfen=3, # 管事
yonghuid=user.yonghuid,
paiming=i,
zhizhi=Decimal(guanshi.jinyuechongzhi),
nicheng=get_user_nicheng(user, 3),
avatar=user.avatar or ''
)
records_to_create.append(record)
# 批量保存
if records_to_create:
with transaction.atomic():
RankingRecord.objects.bulk_create(records_to_create)
logger.info(f"月榜数据转移完成,保存{len(records_to_create)}条记录")
result = {
'success': True,
'month': tongji_date.strftime('%Y-%m'),
'total_count': len(records_to_create),
'is_last_day': True,
'today': today.strftime('%Y-%m-%d'),
}
return result
except Exception as e:
logger.error(f"月榜数据转移失败: {str(e)}", exc_info=True)
raise
@shared_task
def qingli_jiulishuju():
"""
清理旧历史数据 - 每月1号凌晨1点执行
清理3个月前的排行榜数据保留最近3个月的数据
"""
try:
# 计算3个月前的日期
three_months_ago = timezone.now().date() - timedelta(days=90)
# 删除旧数据
deleted_count, _ = RankingRecord.objects.filter(
tongji_date__lt=three_months_ago
).delete()
logger.info(f"清理旧历史数据完成,删除{deleted_count}条记录")
return {
'success': True,
'deleted_count': deleted_count,
'before_date': three_months_ago.strftime('%Y-%m-%d')
}
except Exception as e:
logger.error(f"清理旧历史数据失败: {str(e)}")
# yonghu/ranking_tasks.py
"""
排行榜数据转移任务 - 生产环境专用
在清空前将数据转移到排行榜表
"""
from celery import shared_task
from django.db import transaction
from django.utils import timezone
from datetime import datetime, timedelta
import logging
from decimal import Decimal
from users.models import (
UserDashou, UserShangjia, UserGuanshi, UserBoss,
RankingRecord
)
from gvsdsdk.models import User
logger = logging.getLogger(__name__)
def get_user_nicheng(user, shenfen):
"""
获取用户昵称
规则打手用打手nicheng其他用老板nickname
"""
try:
if shenfen == 2: # 打手
dashou_profile = getattr(user, 'DashouProfile', None)
if dashou_profile and dashou_profile.nicheng:
return dashou_profile.nicheng
else: # 管事或商家
boss_profile = getattr(user, 'BossProfile', None)
if boss_profile and boss_profile.nickname:
return boss_profile.nickname
# 默认返回用户ID
return f"用户{user.UserUID}"
except Exception:
return f"用户{user.UserUID}"
@shared_task
def zhuanyi_ribang():
"""
转移日榜数据 - 每天23:55执行
在清零前将今日数据转移到排行榜表
"""
try:
# 统计日期(今天)
today = timezone.now().date()
logger.info(f"开始转移日榜数据,日期: {today}")
records_to_create = []
# 1. 转移打手日榜(今日收益 > 0
dashou_list = UserDashou.objects.filter(
jinrishouyi__gt=0 # 只转移收益大于0的
).select_related('user').order_by('-jinrishouyi')
for i, dashou in enumerate(dashou_list, start=1):
if i > 100: # 最多100条
break
user = dashou.user
record = RankingRecord(
zhouqi=1, # 日榜
tongji_date=today,
shenfen=2, # 打手
yonghuid=user.UserUID,
paiming=i,
zhizhi=dashou.jinrishouyi,
nicheng=get_user_nicheng(user, 2),
avatar=user.Avatar or ''
)
records_to_create.append(record)
# 2. 转移商家日榜(今日流水 > 0
shangjia_list = UserShangjia.objects.filter(
jinriliushui__gt=0 # 只转移流水大于0的
).select_related('user').order_by('-jinriliushui')
for i, shangjia in enumerate(shangjia_list, start=1):
if i > 100: # 最多100条
break
user = shangjia.user
record = RankingRecord(
zhouqi=1,
tongji_date=today,
shenfen=4, # 商家
yonghuid=user.UserUID,
paiming=i,
zhizhi=shangjia.jinriliushui,
nicheng=get_user_nicheng(user, 4),
avatar=user.Avatar or ''
)
records_to_create.append(record)
# 3. 转移管事日榜(今日充值人数 > 0
guanshi_list = UserGuanshi.objects.filter(
jinrichongzhi__gt=0 # 只转移充值人数大于0的
).select_related('user').order_by('-jinrichongzhi')
for i, guanshi in enumerate(guanshi_list, start=1):
if i > 100: # 最多100条
break
user = guanshi.user
record = RankingRecord(
zhouqi=1,
tongji_date=today,
shenfen=3, # 管事
yonghuid=user.UserUID,
paiming=i,
zhizhi=Decimal(guanshi.jinrichongzhi),
nicheng=get_user_nicheng(user, 3),
avatar=user.Avatar or ''
)
records_to_create.append(record)
# 批量保存
if records_to_create:
with transaction.atomic():
RankingRecord.objects.bulk_create(records_to_create)
logger.info(f"日榜数据转移完成,保存{len(records_to_create)}条记录")
result = {
'success': True,
'date': today.strftime('%Y-%m-%d'),
'total_count': len(records_to_create),
}
return result
except Exception as e:
logger.error(f"日榜数据转移失败: {str(e)}", exc_info=True)
raise
@shared_task
def zhuanyi_yuebang():
"""
转移月榜数据 - 每月最后一天23:50执行
在清零前将本月数据转移到排行榜表
"""
try:
# 🔥 获取当前时间
now = timezone.now()
today = now.date()
# 🔥 判断今天是否是当月的最后一天
# 方法:计算明天的日期,如果明天的月份和今天不同,那么今天就是最后一天
tomorrow = today + timedelta(days=1)
if tomorrow.month == today.month:
# 今天不是最后一天,直接返回
logger.info(f"今天不是当月最后一天({today}),跳过月榜转移")
return {
'success': True,
'message': f'今天不是当月最后一天,跳过执行',
'is_last_day': False,
'today': today.strftime('%Y-%m-%d'),
}
logger.info(f"今天是当月最后一天({today}),开始转移月榜数据")
# 🔥 统计日期为当月第一天如2024-01-01
tongji_date = datetime(today.year, today.month, 1).date()
records_to_create = []
# 1. 转移打手月榜(今月收益 > 0
dashou_list = UserDashou.objects.filter(
jinyueshouyi__gt=0 # 只转移收益大于0的
).select_related('user').order_by('-jinyueshouyi')
for i, dashou in enumerate(dashou_list, start=1):
if i > 100: # 最多100条
break
user = dashou.user
record = RankingRecord(
zhouqi=2, # 月榜
tongji_date=tongji_date,
shenfen=2, # 打手
yonghuid=user.UserUID,
paiming=i,
zhizhi=dashou.jinyueshouyi,
nicheng=get_user_nicheng(user, 2),
avatar=user.Avatar or ''
)
records_to_create.append(record)
# 2. 转移商家月榜(今月流水 > 0
shangjia_list = UserShangjia.objects.filter(
jinyueliushui__gt=0 # 只转移流水大于0的
).select_related('user').order_by('-jinyueliushui')
for i, shangjia in enumerate(shangjia_list, start=1):
if i > 100: # 最多100条
break
user = shangjia.user
record = RankingRecord(
zhouqi=2,
tongji_date=tongji_date,
shenfen=4, # 商家
yonghuid=user.UserUID,
paiming=i,
zhizhi=shangjia.jinyueliushui,
nicheng=get_user_nicheng(user, 4),
avatar=user.Avatar or ''
)
records_to_create.append(record)
# 3. 转移管事月榜(今月充值人数 > 0
guanshi_list = UserGuanshi.objects.filter(
jinyuechongzhi__gt=0 # 只转移充值人数大于0的
).select_related('user').order_by('-jinyuechongzhi')
for i, guanshi in enumerate(guanshi_list, start=1):
if i > 100: # 最多100条
break
user = guanshi.user
record = RankingRecord(
zhouqi=2,
tongji_date=tongji_date,
shenfen=3, # 管事
yonghuid=user.UserUID,
paiming=i,
zhizhi=Decimal(guanshi.jinyuechongzhi),
nicheng=get_user_nicheng(user, 3),
avatar=user.Avatar or ''
)
records_to_create.append(record)
# 批量保存
if records_to_create:
with transaction.atomic():
RankingRecord.objects.bulk_create(records_to_create)
logger.info(f"月榜数据转移完成,保存{len(records_to_create)}条记录")
result = {
'success': True,
'month': tongji_date.strftime('%Y-%m'),
'total_count': len(records_to_create),
'is_last_day': True,
'today': today.strftime('%Y-%m-%d'),
}
return result
except Exception as e:
logger.error(f"月榜数据转移失败: {str(e)}", exc_info=True)
raise
@shared_task
def qingli_jiulishuju():
"""
清理旧历史数据 - 每月1号凌晨1点执行
清理3个月前的排行榜数据保留最近3个月的数据
"""
try:
# 计算3个月前的日期
three_months_ago = timezone.now().date() - timedelta(days=90)
# 删除旧数据
deleted_count, _ = RankingRecord.objects.filter(
tongji_date__lt=three_months_ago
).delete()
logger.info(f"清理旧历史数据完成,删除{deleted_count}条记录")
return {
'success': True,
'deleted_count': deleted_count,
'before_date': three_months_ago.strftime('%Y-%m-%d')
}
except Exception as e:
logger.error(f"清理旧历史数据失败: {str(e)}")
raise

View File

@@ -188,7 +188,7 @@ def validate_withdraw_eligibility(user_main, leixing, jine):
提现申请资格校验(完整基本条件;仅放宽「不限制并行申请笔数」)。
与手动提现 TixianShenqingView 对齐。
"""
yonghuid = user_main.yonghuid
yonghuid = user_main.UserUID
fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
if not fadan_ok:
@@ -314,7 +314,7 @@ def validate_collect_eligibility(user_main, leixing):
打手佣金/押金:罚单、封禁、接单中、积分、会员、订单等;
其他身份:账号状态、罚单/订单等。
"""
yonghuid = user_main.yonghuid
yonghuid = user_main.UserUID
fadan_ok, fadan_msg = check_fadan_qiangdan_eligible(yonghuid, 2)
if not fadan_ok:
@@ -650,7 +650,7 @@ def reserve_collect_quota_limits(user_main, leixing, jine, shijidaozhang, shenhe
logger.info(
'收款限额预占成功 yonghuid=%s leixing=%s shenhe_danhao=%s jine=%s shijidaozhang=%s platform_total=%s',
user_main.yonghuid, leixing, shenhe_danhao, jine, shijidaozhang, platform_stat.total_amount,
user_main.UserUID, leixing, shenhe_danhao, jine, shijidaozhang, platform_stat.total_amount,
)
return True, '', ''
@@ -691,13 +691,13 @@ def release_collect_quota_reservation(user_main, leixing, jine, shijidaozhang):
except Exception as e:
logger.error(
'回滚收款限额预占失败 yonghuid=%s leixing=%s err=%s',
user_main.yonghuid, leixing, e, exc_info=True,
user_main.UserUID, leixing, e, exc_info=True,
)
return
logger.info(
'收款限额预占已回滚 yonghuid=%s leixing=%s jine=%s shijidaozhang=%s',
user_main.yonghuid, leixing, jine, shijidaozhang,
user_main.UserUID, leixing, jine, shijidaozhang,
)
@@ -1371,7 +1371,7 @@ def create_audit_application(user_main, leixing, jine):
不创建打款记录表TixianAutoRecord 仅在收款时创建)
允许多笔并行申请(不阻塞待收款/审核中单)
"""
yonghuid = user_main.yonghuid
yonghuid = user_main.UserUID
ok, msg, extra = validate_withdraw_eligibility(user_main, leixing, jine)
if not ok:
@@ -1408,12 +1408,12 @@ def create_audit_application(user_main, leixing, jine):
tixian_record = Tixianjilu.query.create(
yonghuid=yonghuid,
avatar=user_main.avatar or '',
phone=user_main.phone or '',
avatar=user_main.Avatar or '',
phone=user_main.Phone or '',
nicheng=nicheng,
leixing=leixing,
zhifu=user_main.zhifu or '',
skzhanghao=user_main.skzhanghao or '',
zhifu=user_main.PaymentQRCode or '',
skzhanghao=user_main.PaymentAccount or '',
jine=shijidaozhang,
zhuangtai=1,
fangshi=1,

View File

@@ -1,406 +1,406 @@
# yonghu/tixian_shenhe_views.py
"""
自动提现审核 — 用户侧接口:
① TixianZddkshApplyView → POST /yonghu/zddksh 提交审核(扣款)
② process_audit_collect → POST /yonghu/tixiansq 收款P0一单到底先查微信再决定是否新建
"""
import decimal
import json
import logging
import random
import string
import time
import requests
from django.conf import settings
from django.db import transaction
from django.utils import timezone
from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from utils.redis_lock import acquire_lock, release_lock
from utils.wechat_v3 import build_authorization
from gvsdsdk.fluent import db, func, FQ
from .models import TixianAutoRecord
from .tixian_shenhe_services import (
RECONCILE_ALLOW_NEW,
RECONCILE_AUDIT_CLOSED,
RECONCILE_COMPLETED,
RECONCILE_PENDING,
RECONCILE_WAIT_CONFIRM,
check_collect_quota_limits,
create_audit_application,
close_audit_over_limit_refund,
close_audit_wechat_failed,
get_audit_for_collect,
handle_post_transfer_failure,
reconcile_shenhe_wechat_bills,
release_collect_quota_for_record,
reserve_collect_quota_limits,
resolve_collect_context,
validate_collect_amount,
validate_collect_eligibility,
)
logger = logging.getLogger('yonghu.tixian_shenhe')
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}'
def _build_collect_success_response(payload):
return Response({
'code': 0,
'msg': '请确认收款',
'data': payload,
})
def _is_wx_balance_insufficient(err_code='', err_msg=''):
blob = f'{err_code} {err_msg}'.upper()
return any(k in blob for k in ('NOT_ENOUGH', 'INSUFFICIENT', '余额不足', '资金不足'))
def _verify_collect_quota_or_response(user_main, audit, shenhe_danhao):
"""收款限额校验;不通过时直接返回 Response"""
ok, msg, limit_kind = check_collect_quota_limits(
user_main,
audit.leixing,
audit.shenqing_jine,
audit.shijidaozhang,
shenhe_danhao,
)
if not ok:
limit_code = 33 if limit_kind == 'platform' else 400
logger.warning(
'收款限额校验未通过 yonghuid=%s shenhe_danhao=%s leixing=%s kind=%s msg=%s',
user_main.yonghuid, shenhe_danhao, audit.leixing, limit_kind, msg,
)
return Response({'code': limit_code, 'msg': msg})
return None
def _mark_auto_record_failed(tixian_id, fail_reason):
"""微信明确失败时标记打款记录终态,避免后续一直被当成「处理中」"""
TixianAutoRecord.query.filter(tixian_id=tixian_id, zhuangtai=0).update(
zhuangtai=2,
fail_reason=(fail_reason or '打款失败')[:500],
update_time=timezone.now(),
)
def _build_wx_transfer_body(wx_cfg, tixian_id, openid, yonghuid, leixing, shijidaozhang):
transfer_scene_id = wx_cfg['TRANSFER_SCENE_ID']
role_map = {1: '打手', 2: '管事', 3: '组长', 4: '考核官', 5: '打手', 6: '商家'}
desc_map = {1: '佣金提现', 2: '分红提现', 3: '分红提现', 4: '分佣提现', 5: '押金提现', 6: '余额提现'}
transfer_scene_report_infos = []
if transfer_scene_id == '1005':
transfer_scene_report_infos = [
{'info_type': '岗位类型', 'info_content': role_map.get(leixing, '用户')},
{'info_type': '报酬说明', 'info_content': desc_map.get(leixing, '提现')},
]
elif transfer_scene_id == '1009':
transfer_scene_report_infos = [
{'info_type': '采购商品名称', 'info_content': '平台服务'},
]
return {
'appid': wx_cfg['APPID'],
'out_bill_no': tixian_id,
'transfer_scene_id': transfer_scene_id,
'openid': openid,
'transfer_amount': int(shijidaozhang * 100),
'transfer_remark': f'平台提现-{yonghuid}',
'transfer_scene_report_infos': transfer_scene_report_infos,
}
def _call_wechat_transfer(body, wx_cfg):
url = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills'
body_json = json.dumps(body, separators=(',', ':'))
auth = build_authorization('POST', url, body_json)
headers = {
'Authorization': auth,
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0',
}
resp = requests.post(url, data=body_json, headers=headers, timeout=15)
try:
wx_result = resp.json()
except ValueError:
wx_result = {}
return resp, wx_result
class TixianZddkshApplyView(APIView):
"""POST /yonghu/zddksh 提交审核申请(扣款,不创建 TixianAutoRecord"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
user_main = request.user
lock_key = f'tixian_audit_apply:{user_main.yonghuid}'
identifier = acquire_lock(lock_key, timeout=10)
if not identifier:
return Response({'code': 429, 'msg': '操作频繁,请稍后重试'})
try:
leixing = request.data.get('leixing')
jine_str = request.data.get('jine')
if leixing is None or jine_str is None:
return Response({'code': 1, 'msg': '缺少参数: leixing 或 jine'})
try:
leixing = int(leixing)
jine = decimal.Decimal(str(jine_str))
except (ValueError, decimal.InvalidOperation):
return Response({'code': 2, 'msg': '参数格式错误'})
if leixing not in [1, 2, 3, 4, 5, 6]:
return Response({'code': 3, 'msg': '提现类型无效'})
if jine <= decimal.Decimal('0.1'):
return Response({'code': 4, 'msg': '提现金额必须大于0.1'})
try:
data = create_audit_application(user_main, leixing, jine)
except ValueError as e:
return Response({'code': 400, 'msg': str(e)})
except Exception as e:
logger.error(f'提现审核申请异常: {e}', exc_info=True)
return Response({'code': 99, 'msg': '提现申请失败,请稍后重试'})
return Response({'code': 0, 'msg': '提现申请已提交,请等待审核', 'data': data})
finally:
release_lock(lock_key, identifier)
def process_audit_collect(request):
"""
POST /yonghu/tixiansq 用户收款P0 防多提)
主路径tixianjilu_id → Tixianjilu → TixianShenheJilu(状态6)
若 TixianAutoRecord 已有 tixian_id → 只问微信,非终态绝不新建
仅微信 FAIL/CANCELLED 终态后才允许新建打款记录
"""
user_main = request.user
yonghuid = user_main.yonghuid
tixianjilu_id = request.data.get('tixianjilu_id') or request.data.get('id')
shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip()
shenhe_jilu_id = request.data.get('shenhe_jilu_id')
logger.info(
'收款请求 yonghuid=%s tixianjilu_id=%s shenhe_jilu_id=%s shenhe_danhao=%s',
yonghuid, tixianjilu_id, shenhe_jilu_id, shenhe_danhao,
)
try:
if not user_main.openid:
return Response({'code': 10, 'msg': '用户未绑定微信,无法收款'})
wx_cfg = settings.WECHAT_PAY_V3_CONFIG
if not wx_cfg.get('APPID') or not wx_cfg.get('MCHID') or not wx_cfg.get('TRANSFER_SCENE_ID'):
logger.error('微信打款配置不完整 APPID=%s MCHID=%s', wx_cfg.get('APPID'), wx_cfg.get('MCHID'))
return Response({'code': 11, 'msg': '系统配置异常,请联系客服'})
ctx, err = resolve_collect_context(
yonghuid,
tixianjilu_id=tixianjilu_id,
shenhe_danhao=shenhe_danhao,
shenhe_jilu_id=shenhe_jilu_id,
)
if not ctx:
return Response({'code': 400, 'msg': err})
audit = ctx['audit']
shenhe_danhao = ctx['shenhe_danhao']
tixianjilu_id_val = ctx['jilu'].id if ctx.get('jilu') else audit.tixianjilu_id
leixing = audit.leixing
amount_ok, amount_msg = validate_collect_amount(audit)
if not amount_ok:
with transaction.atomic():
close_audit_over_limit_refund(audit, None, amount_msg)
return Response({
'code': 400,
'msg': f'{amount_msg},可到账金额已退回余额(手续费不退),请重新申请',
})
# 收款前严格资格校验:不通过仅返回错误,审核单保持待收款(6),不退款
collect_ok, collect_msg = validate_collect_eligibility(user_main, leixing)
if not collect_ok:
logger.warning(
'收款校验未通过 yonghuid=%s shenhe_danhao=%s leixing=%s msg=%s',
yonghuid, shenhe_danhao, leixing, collect_msg,
)
return Response({'code': 400, 'msg': collect_msg})
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
if quota_resp:
return quota_resp
lock_key = f'tixian_collect:{shenhe_danhao}'
identifier = acquire_lock(lock_key, timeout=15)
if not identifier:
return Response({'code': 429, 'msg': '操作频繁,请稍后重试'})
tixian_id = None
quota_reserved = False
try:
action, payload = reconcile_shenhe_wechat_bills(shenhe_danhao)
if action == RECONCILE_COMPLETED:
return Response({'code': 8, 'msg': payload.get('msg', '该提现已完成,请勿重复操作')})
if action == RECONCILE_AUDIT_CLOSED:
return Response({
'code': 400,
'msg': payload.get('msg', '该笔提现已关闭,请重新申请'),
})
if action == RECONCILE_WAIT_CONFIRM:
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
if quota_resp:
return quota_resp
return _build_collect_success_response(payload)
if action == RECONCILE_PENDING:
msg = payload.get('msg') if isinstance(payload, dict) else payload
return Response({'code': 12, 'msg': msg or '有收款处理中,请稍后再试'})
if action != RECONCILE_ALLOW_NEW:
return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
with transaction.atomic():
audit, err = get_audit_for_collect(
shenhe_danhao, yonghuid, audit.id,
)
if not audit:
return Response({'code': 400, 'msg': err})
# 金额一律用审核表已落库数据(申请时扣款+算费),收款不再重算
jine = audit.shenqing_jine
shouxufei = audit.shouxufei
shijidaozhang = audit.shijidaozhang
feilv = audit.feilv
ok, msg, limit_kind = reserve_collect_quota_limits(
user_main, leixing, jine, shijidaozhang, shenhe_danhao,
)
if not ok:
limit_code = 33 if limit_kind == 'platform' else 400
return Response({'code': limit_code, 'msg': msg})
quota_reserved = True
tixian_id = generate_tixian_id()
TixianAutoRecord.query.create(
tixian_id=tixian_id,
yonghuid=yonghuid,
leixing=leixing,
jine=jine,
shouxufei=shouxufei,
shijidaozhang=shijidaozhang,
feilv=feilv,
zhuangtai=0,
shenhe_danhao=shenhe_danhao,
)
audit.tixian_auto_id = tixian_id
audit.save(update_fields=['tixian_auto_id', 'update_time'])
body = _build_wx_transfer_body(
wx_cfg, tixian_id, user_main.openid, yonghuid, leixing, shijidaozhang,
)
try:
resp, wx_result = _call_wechat_transfer(body, wx_cfg)
if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM':
package_info = wx_result.get('package_info')
auto_record = TixianAutoRecord.query.get(tixian_id=tixian_id)
auto_record.wechat_package = json.dumps(package_info, ensure_ascii=False)
auto_record.save(update_fields=['wechat_package'])
return _build_collect_success_response({
'tixian_id': tixian_id,
'package_info': package_info,
'shouxufei': str(shouxufei),
'shijidaozhang': str(shijidaozhang),
'current_rate': str(feilv),
'mch_id': wx_cfg['MCHID'],
'shenhe_danhao': shenhe_danhao,
'tixianjilu_id': tixianjilu_id_val,
})
err_code = wx_result.get('code', '')
err_msg = wx_result.get('message', wx_result.get('detail', '微信接口调用失败'))
logger.error(
'微信发起转账未进入待确认: bill=%s code=%s msg=%s HTTP%s',
tixian_id, err_code, err_msg, resp.status_code,
)
is_balance_issue = _is_wx_balance_insufficient(err_code, err_msg)
if is_balance_issue:
_mark_auto_record_failed(tixian_id, err_msg)
if quota_reserved:
release_collect_quota_for_record(
TixianAutoRecord.query.get(tixian_id=tixian_id),
)
quota_reserved = False
return Response({
'code': 99,
'msg': '运营账户资金不足,需等管理员充值后才能提现',
})
post_action, post_payload = handle_post_transfer_failure(
tixian_id, shenhe_danhao, err_code, err_msg,
)
if post_action == RECONCILE_WAIT_CONFIRM:
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
if quota_resp:
return quota_resp
return _build_collect_success_response(post_payload)
if post_action == RECONCILE_COMPLETED:
return Response({'code': 8, 'msg': '该提现已完成,请勿重复操作'})
if post_action == RECONCILE_AUDIT_CLOSED:
return Response({
'code': 400,
'msg': post_payload.get('msg', '该笔提现已关闭,请重新申请'),
})
if post_action == RECONCILE_PENDING:
pending_msg = (
post_payload.get('msg', post_payload)
if isinstance(post_payload, dict) else post_payload
)
return Response({'code': 12, 'msg': pending_msg or '有收款处理中,请稍后再试'})
user_msg = err_msg or '微信收款发起失败,请稍后重试'
_mark_auto_record_failed(tixian_id, user_msg)
if quota_reserved:
release_collect_quota_for_record(
TixianAutoRecord.query.get(tixian_id=tixian_id),
)
quota_reserved = False
return Response({'code': 99, 'msg': user_msg})
except requests.RequestException as e:
logger.error(
'微信打款网络异常(保持待查单): bill=%s err=%s',
tixian_id, e, exc_info=True,
)
return Response({
'code': 12,
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
})
finally:
release_lock(lock_key, identifier)
except Exception as e:
logger.error(
'收款接口未捕获异常 yonghuid=%s tixianjilu_id=%s shenhe_danhao=%s err=%s',
yonghuid, tixianjilu_id, shenhe_danhao, e,
exc_info=True,
)
return Response({'code': 99, 'msg': '系统繁忙,请稍后重试'})
# yonghu/tixian_shenhe_views.py
"""
自动提现审核 — 用户侧接口:
① TixianZddkshApplyView → POST /yonghu/zddksh 提交审核(扣款)
② process_audit_collect → POST /yonghu/tixiansq 收款P0一单到底先查微信再决定是否新建
"""
import decimal
import json
import logging
import random
import string
import time
import requests
from django.conf import settings
from django.db import transaction
from django.utils import timezone
from rest_framework import permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from utils.redis_lock import acquire_lock, release_lock
from utils.wechat_v3 import build_authorization
from gvsdsdk.fluent import db, func, FQ
from .models import TixianAutoRecord
from .tixian_shenhe_services import (
RECONCILE_ALLOW_NEW,
RECONCILE_AUDIT_CLOSED,
RECONCILE_COMPLETED,
RECONCILE_PENDING,
RECONCILE_WAIT_CONFIRM,
check_collect_quota_limits,
create_audit_application,
close_audit_over_limit_refund,
close_audit_wechat_failed,
get_audit_for_collect,
handle_post_transfer_failure,
reconcile_shenhe_wechat_bills,
release_collect_quota_for_record,
reserve_collect_quota_limits,
resolve_collect_context,
validate_collect_amount,
validate_collect_eligibility,
)
logger = logging.getLogger('yonghu.tixian_shenhe')
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}'
def _build_collect_success_response(payload):
return Response({
'code': 0,
'msg': '请确认收款',
'data': payload,
})
def _is_wx_balance_insufficient(err_code='', err_msg=''):
blob = f'{err_code} {err_msg}'.upper()
return any(k in blob for k in ('NOT_ENOUGH', 'INSUFFICIENT', '余额不足', '资金不足'))
def _verify_collect_quota_or_response(user_main, audit, shenhe_danhao):
"""收款限额校验;不通过时直接返回 Response"""
ok, msg, limit_kind = check_collect_quota_limits(
user_main,
audit.leixing,
audit.shenqing_jine,
audit.shijidaozhang,
shenhe_danhao,
)
if not ok:
limit_code = 33 if limit_kind == 'platform' else 400
logger.warning(
'收款限额校验未通过 yonghuid=%s shenhe_danhao=%s leixing=%s kind=%s msg=%s',
user_main.UserUID, shenhe_danhao, audit.leixing, limit_kind, msg,
)
return Response({'code': limit_code, 'msg': msg})
return None
def _mark_auto_record_failed(tixian_id, fail_reason):
"""微信明确失败时标记打款记录终态,避免后续一直被当成「处理中」"""
TixianAutoRecord.query.filter(tixian_id=tixian_id, zhuangtai=0).update(
zhuangtai=2,
fail_reason=(fail_reason or '打款失败')[:500],
update_time=timezone.now(),
)
def _build_wx_transfer_body(wx_cfg, tixian_id, openid, yonghuid, leixing, shijidaozhang):
transfer_scene_id = wx_cfg['TRANSFER_SCENE_ID']
role_map = {1: '打手', 2: '管事', 3: '组长', 4: '考核官', 5: '打手', 6: '商家'}
desc_map = {1: '佣金提现', 2: '分红提现', 3: '分红提现', 4: '分佣提现', 5: '押金提现', 6: '余额提现'}
transfer_scene_report_infos = []
if transfer_scene_id == '1005':
transfer_scene_report_infos = [
{'info_type': '岗位类型', 'info_content': role_map.get(leixing, '用户')},
{'info_type': '报酬说明', 'info_content': desc_map.get(leixing, '提现')},
]
elif transfer_scene_id == '1009':
transfer_scene_report_infos = [
{'info_type': '采购商品名称', 'info_content': '平台服务'},
]
return {
'appid': wx_cfg['APPID'],
'out_bill_no': tixian_id,
'transfer_scene_id': transfer_scene_id,
'openid': openid,
'transfer_amount': int(shijidaozhang * 100),
'transfer_remark': f'平台提现-{yonghuid}',
'transfer_scene_report_infos': transfer_scene_report_infos,
}
def _call_wechat_transfer(body, wx_cfg):
url = 'https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills'
body_json = json.dumps(body, separators=(',', ':'))
auth = build_authorization('POST', url, body_json)
headers = {
'Authorization': auth,
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0',
}
resp = requests.post(url, data=body_json, headers=headers, timeout=15)
try:
wx_result = resp.json()
except ValueError:
wx_result = {}
return resp, wx_result
class TixianZddkshApplyView(APIView):
"""POST /yonghu/zddksh 提交审核申请(扣款,不创建 TixianAutoRecord"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
user_main = request.user
lock_key = f'tixian_audit_apply:{user_main.UserUID}'
identifier = acquire_lock(lock_key, timeout=10)
if not identifier:
return Response({'code': 429, 'msg': '操作频繁,请稍后重试'})
try:
leixing = request.data.get('leixing')
jine_str = request.data.get('jine')
if leixing is None or jine_str is None:
return Response({'code': 1, 'msg': '缺少参数: leixing 或 jine'})
try:
leixing = int(leixing)
jine = decimal.Decimal(str(jine_str))
except (ValueError, decimal.InvalidOperation):
return Response({'code': 2, 'msg': '参数格式错误'})
if leixing not in [1, 2, 3, 4, 5, 6]:
return Response({'code': 3, 'msg': '提现类型无效'})
if jine <= decimal.Decimal('0.1'):
return Response({'code': 4, 'msg': '提现金额必须大于0.1'})
try:
data = create_audit_application(user_main, leixing, jine)
except ValueError as e:
return Response({'code': 400, 'msg': str(e)})
except Exception as e:
logger.error(f'提现审核申请异常: {e}', exc_info=True)
return Response({'code': 99, 'msg': '提现申请失败,请稍后重试'})
return Response({'code': 0, 'msg': '提现申请已提交,请等待审核', 'data': data})
finally:
release_lock(lock_key, identifier)
def process_audit_collect(request):
"""
POST /yonghu/tixiansq 用户收款P0 防多提)
主路径tixianjilu_id → Tixianjilu → TixianShenheJilu(状态6)
若 TixianAutoRecord 已有 tixian_id → 只问微信,非终态绝不新建
仅微信 FAIL/CANCELLED 终态后才允许新建打款记录
"""
user_main = request.user
yonghuid = user_main.UserUID
tixianjilu_id = request.data.get('tixianjilu_id') or request.data.get('id')
shenhe_danhao = (request.data.get('shenhe_danhao') or '').strip()
shenhe_jilu_id = request.data.get('shenhe_jilu_id')
logger.info(
'收款请求 yonghuid=%s tixianjilu_id=%s shenhe_jilu_id=%s shenhe_danhao=%s',
yonghuid, tixianjilu_id, shenhe_jilu_id, shenhe_danhao,
)
try:
if not user_main.OpenID:
return Response({'code': 10, 'msg': '用户未绑定微信,无法收款'})
wx_cfg = settings.WECHAT_PAY_V3_CONFIG
if not wx_cfg.get('APPID') or not wx_cfg.get('MCHID') or not wx_cfg.get('TRANSFER_SCENE_ID'):
logger.error('微信打款配置不完整 APPID=%s MCHID=%s', wx_cfg.get('APPID'), wx_cfg.get('MCHID'))
return Response({'code': 11, 'msg': '系统配置异常,请联系客服'})
ctx, err = resolve_collect_context(
yonghuid,
tixianjilu_id=tixianjilu_id,
shenhe_danhao=shenhe_danhao,
shenhe_jilu_id=shenhe_jilu_id,
)
if not ctx:
return Response({'code': 400, 'msg': err})
audit = ctx['audit']
shenhe_danhao = ctx['shenhe_danhao']
tixianjilu_id_val = ctx['jilu'].id if ctx.get('jilu') else audit.tixianjilu_id
leixing = audit.leixing
amount_ok, amount_msg = validate_collect_amount(audit)
if not amount_ok:
with transaction.atomic():
close_audit_over_limit_refund(audit, None, amount_msg)
return Response({
'code': 400,
'msg': f'{amount_msg},可到账金额已退回余额(手续费不退),请重新申请',
})
# 收款前严格资格校验:不通过仅返回错误,审核单保持待收款(6),不退款
collect_ok, collect_msg = validate_collect_eligibility(user_main, leixing)
if not collect_ok:
logger.warning(
'收款校验未通过 yonghuid=%s shenhe_danhao=%s leixing=%s msg=%s',
yonghuid, shenhe_danhao, leixing, collect_msg,
)
return Response({'code': 400, 'msg': collect_msg})
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
if quota_resp:
return quota_resp
lock_key = f'tixian_collect:{shenhe_danhao}'
identifier = acquire_lock(lock_key, timeout=15)
if not identifier:
return Response({'code': 429, 'msg': '操作频繁,请稍后重试'})
tixian_id = None
quota_reserved = False
try:
action, payload = reconcile_shenhe_wechat_bills(shenhe_danhao)
if action == RECONCILE_COMPLETED:
return Response({'code': 8, 'msg': payload.get('msg', '该提现已完成,请勿重复操作')})
if action == RECONCILE_AUDIT_CLOSED:
return Response({
'code': 400,
'msg': payload.get('msg', '该笔提现已关闭,请重新申请'),
})
if action == RECONCILE_WAIT_CONFIRM:
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
if quota_resp:
return quota_resp
return _build_collect_success_response(payload)
if action == RECONCILE_PENDING:
msg = payload.get('msg') if isinstance(payload, dict) else payload
return Response({'code': 12, 'msg': msg or '有收款处理中,请稍后再试'})
if action != RECONCILE_ALLOW_NEW:
return Response({'code': 12, 'msg': '收款处理中,请稍后再试'})
with transaction.atomic():
audit, err = get_audit_for_collect(
shenhe_danhao, yonghuid, audit.id,
)
if not audit:
return Response({'code': 400, 'msg': err})
# 金额一律用审核表已落库数据(申请时扣款+算费),收款不再重算
jine = audit.shenqing_jine
shouxufei = audit.shouxufei
shijidaozhang = audit.shijidaozhang
feilv = audit.feilv
ok, msg, limit_kind = reserve_collect_quota_limits(
user_main, leixing, jine, shijidaozhang, shenhe_danhao,
)
if not ok:
limit_code = 33 if limit_kind == 'platform' else 400
return Response({'code': limit_code, 'msg': msg})
quota_reserved = True
tixian_id = generate_tixian_id()
TixianAutoRecord.query.create(
tixian_id=tixian_id,
yonghuid=yonghuid,
leixing=leixing,
jine=jine,
shouxufei=shouxufei,
shijidaozhang=shijidaozhang,
feilv=feilv,
zhuangtai=0,
shenhe_danhao=shenhe_danhao,
)
audit.tixian_auto_id = tixian_id
audit.save(update_fields=['tixian_auto_id', 'update_time'])
body = _build_wx_transfer_body(
wx_cfg, tixian_id, user_main.OpenID, yonghuid, leixing, shijidaozhang,
)
try:
resp, wx_result = _call_wechat_transfer(body, wx_cfg)
if resp.status_code == 200 and wx_result.get('state') == 'WAIT_USER_CONFIRM':
package_info = wx_result.get('package_info')
auto_record = TixianAutoRecord.query.get(tixian_id=tixian_id)
auto_record.wechat_package = json.dumps(package_info, ensure_ascii=False)
auto_record.save(update_fields=['wechat_package'])
return _build_collect_success_response({
'tixian_id': tixian_id,
'package_info': package_info,
'shouxufei': str(shouxufei),
'shijidaozhang': str(shijidaozhang),
'current_rate': str(feilv),
'mch_id': wx_cfg['MCHID'],
'shenhe_danhao': shenhe_danhao,
'tixianjilu_id': tixianjilu_id_val,
})
err_code = wx_result.get('code', '')
err_msg = wx_result.get('message', wx_result.get('detail', '微信接口调用失败'))
logger.error(
'微信发起转账未进入待确认: bill=%s code=%s msg=%s HTTP%s',
tixian_id, err_code, err_msg, resp.status_code,
)
is_balance_issue = _is_wx_balance_insufficient(err_code, err_msg)
if is_balance_issue:
_mark_auto_record_failed(tixian_id, err_msg)
if quota_reserved:
release_collect_quota_for_record(
TixianAutoRecord.query.get(tixian_id=tixian_id),
)
quota_reserved = False
return Response({
'code': 99,
'msg': '运营账户资金不足,需等管理员充值后才能提现',
})
post_action, post_payload = handle_post_transfer_failure(
tixian_id, shenhe_danhao, err_code, err_msg,
)
if post_action == RECONCILE_WAIT_CONFIRM:
quota_resp = _verify_collect_quota_or_response(user_main, audit, shenhe_danhao)
if quota_resp:
return quota_resp
return _build_collect_success_response(post_payload)
if post_action == RECONCILE_COMPLETED:
return Response({'code': 8, 'msg': '该提现已完成,请勿重复操作'})
if post_action == RECONCILE_AUDIT_CLOSED:
return Response({
'code': 400,
'msg': post_payload.get('msg', '该笔提现已关闭,请重新申请'),
})
if post_action == RECONCILE_PENDING:
pending_msg = (
post_payload.get('msg', post_payload)
if isinstance(post_payload, dict) else post_payload
)
return Response({'code': 12, 'msg': pending_msg or '有收款处理中,请稍后再试'})
user_msg = err_msg or '微信收款发起失败,请稍后重试'
_mark_auto_record_failed(tixian_id, user_msg)
if quota_reserved:
release_collect_quota_for_record(
TixianAutoRecord.query.get(tixian_id=tixian_id),
)
quota_reserved = False
return Response({'code': 99, 'msg': user_msg})
except requests.RequestException as e:
logger.error(
'微信打款网络异常(保持待查单): bill=%s err=%s',
tixian_id, e, exc_info=True,
)
return Response({
'code': 12,
'msg': '收款请求已提交,系统正在核对状态,请稍后再试,切勿重复点击',
})
finally:
release_lock(lock_key, identifier)
except Exception as e:
logger.error(
'收款接口未捕获异常 yonghuid=%s tixianjilu_id=%s shenhe_danhao=%s err=%s',
yonghuid, tixianjilu_id, shenhe_danhao, e,
exc_info=True,
)
return Response({'code': 99, 'msg': '系统繁忙,请稍后重试'})

File diff suppressed because it is too large Load Diff