修复了管理员无法进后台的问题
This commit is contained in:
@@ -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='关联用户'),
|
||||
),
|
||||
]
|
||||
1258
users/models.py
1258
users/models.py
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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': '系统繁忙,请稍后重试'})
|
||||
|
||||
734
users/views.py
734
users/views.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user