feat: 资金冻结 P0(表结构+默认关闭入账网关+权限菜单种子)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
125
jituan/management/commands/seed_fund_freeze_perm.py
Normal file
125
jituan/management/commands/seed_fund_freeze_perm.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""写入资金冻结相关权限码,并可按俱乐部挂到角色(可重复执行)。
|
||||
|
||||
权限码:
|
||||
djcfg001 资金冻结规则配置
|
||||
djled001 资金冻结明细/手动解冻
|
||||
djrisk001 罚款投诉风险指标
|
||||
|
||||
用法:
|
||||
python manage.py seed_fund_freeze_perm
|
||||
python manage.py seed_fund_freeze_perm --club-id lxs --role-name 管理员
|
||||
python manage.py seed_fund_freeze_perm --all-clubs --role-name 管理员 --dry-run
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from gvsdsdk.models import Permission, Role, RolePermission
|
||||
|
||||
from jituan.models import Club
|
||||
|
||||
PERMS = [
|
||||
('djcfg001', '资金冻结规则配置', '配置本俱乐部各角色冻结比例/分期/解冻条件', '资金冻结'),
|
||||
('djled001', '资金冻结明细', '查看冻结流水并手动解冻', '资金冻结'),
|
||||
('djrisk001', '罚款投诉风险指标', '查看罚款率/投诉率等解冻相关指标', '资金冻结'),
|
||||
]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '写入资金冻结权限码,并按俱乐部挂到角色'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--club-id', default='', help='仅处理该俱乐部')
|
||||
parser.add_argument('--all-clubs', action='store_true')
|
||||
parser.add_argument('--role-name', default='', help='角色名;空则只确保权限码')
|
||||
parser.add_argument('--dry-run', action='store_true')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = options['dry_run']
|
||||
club_id = (options.get('club_id') or '').strip()
|
||||
all_clubs = options.get('all_clubs')
|
||||
role_name = (options.get('role_name') or '').strip()
|
||||
|
||||
sample = Permission.objects.filter(PermStatus=1).first()
|
||||
tenant_uuid = sample.TenantUUID if sample else uuid.UUID(
|
||||
'00000000-0000-0000-0000-000000000001'
|
||||
).bytes
|
||||
|
||||
perm_objs = []
|
||||
for code, name, desc, cat in PERMS:
|
||||
perm = Permission.objects.filter(PermCode=code).first()
|
||||
if perm:
|
||||
self.stdout.write(f' 权限码已存在: {code}')
|
||||
if not dry_run:
|
||||
changed = False
|
||||
if perm.PermName != name:
|
||||
perm.PermName = name
|
||||
changed = True
|
||||
if (perm.PermDesc or '') != desc:
|
||||
perm.PermDesc = desc
|
||||
changed = True
|
||||
if (perm.PermCategory or '') != cat:
|
||||
perm.PermCategory = cat
|
||||
changed = True
|
||||
if changed:
|
||||
perm.save(update_fields=['PermName', 'PermDesc', 'PermCategory'])
|
||||
else:
|
||||
if dry_run:
|
||||
self.stdout.write(f'would create perm {code}')
|
||||
else:
|
||||
perm = Permission.objects.create(
|
||||
PermUUID=uuid.uuid4().bytes,
|
||||
TenantUUID=tenant_uuid,
|
||||
PermCode=code,
|
||||
PermName=name,
|
||||
PermDesc=desc,
|
||||
PermCategory=cat,
|
||||
PermStatus=1,
|
||||
CreateTime=timezone.now(),
|
||||
UpdateTime=timezone.now(),
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(f' 已创建权限码: {code}'))
|
||||
if perm:
|
||||
perm_objs.append(perm)
|
||||
|
||||
if not role_name:
|
||||
self.stdout.write('未指定 --role-name,跳过挂角色')
|
||||
return
|
||||
|
||||
clubs = []
|
||||
if all_clubs:
|
||||
clubs = list(Club.query.filter(status=1).values_list('club_id', flat=True))
|
||||
elif club_id:
|
||||
clubs = [club_id]
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING('请指定 --club-id 或 --all-clubs'))
|
||||
return
|
||||
|
||||
with transaction.atomic():
|
||||
for cid in clubs:
|
||||
roles = Role.objects.filter(RoleName=role_name, ClubID=cid, RoleStatus=1)
|
||||
if not roles.exists():
|
||||
# 兼容无 ClubID 字段旧数据:仅精确 club 时再试全局同名
|
||||
roles = Role.objects.filter(RoleName=role_name, RoleStatus=1)
|
||||
roles = [r for r in roles if (getattr(r, 'ClubID', None) or '') == cid]
|
||||
for role in roles:
|
||||
for perm in perm_objs:
|
||||
exists = RolePermission.objects.filter(
|
||||
RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID,
|
||||
).exists()
|
||||
if exists:
|
||||
self.stdout.write(f' 已挂: {cid} {role_name} <- {perm.PermCode}')
|
||||
continue
|
||||
if dry_run:
|
||||
self.stdout.write(f'would link {cid} {role_name} <- {perm.PermCode}')
|
||||
else:
|
||||
RolePermission.objects.create(
|
||||
RoleUUID=role.RoleUUID,
|
||||
PermUUID=perm.PermUUID,
|
||||
CreateTime=timezone.now(),
|
||||
)
|
||||
self.stdout.write(self.style.SUCCESS(
|
||||
f' 已挂: {cid} {role_name} <- {perm.PermCode}'
|
||||
))
|
||||
@@ -50,6 +50,24 @@ EXTRA_PERMISSIONS = [
|
||||
'审核本俱乐部提现(同意/拒绝/自动打款)。跨俱乐部仅集团高管;须角色 ClubID + 数据范围任职。',
|
||||
'提现管理',
|
||||
),
|
||||
(
|
||||
'djcfg001',
|
||||
'资金冻结规则配置',
|
||||
'配置本俱乐部各角色冻结比例/分期/解冻条件',
|
||||
'资金冻结',
|
||||
),
|
||||
(
|
||||
'djled001',
|
||||
'资金冻结明细',
|
||||
'查看冻结流水并手动解冻',
|
||||
'资金冻结',
|
||||
),
|
||||
(
|
||||
'djrisk001',
|
||||
'罚款投诉风险指标',
|
||||
'查看罚款率/投诉率等解冻相关指标',
|
||||
'资金冻结',
|
||||
),
|
||||
# ---------- 店铺管理(999 系列:列表/详情/复制/提现等) ----------
|
||||
('999a', '店铺状态管理', '修改店铺封禁/正常状态', '店铺管理'),
|
||||
('999b', '店铺余额与创建', '修改可提现余额、添加新店铺', '店铺管理'),
|
||||
|
||||
@@ -37,3 +37,8 @@ class Command(BaseCommand):
|
||||
call_command('seed_gvsdsdk_permissions')
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.WARNING(f'seed_gvsdsdk_permissions 跳过: {e}'))
|
||||
try:
|
||||
from django.core.management import call_command
|
||||
call_command('seed_fund_freeze_perm')
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.WARNING(f'seed_fund_freeze_perm 跳过: {e}'))
|
||||
|
||||
131
jituan/migrations/0017_fund_freeze.py
Normal file
131
jituan/migrations/0017_fund_freeze.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# Generated manually for fund freeze P0 tables
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('jituan', '0016_club_order_grab_link'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ClubFundFreezeConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(db_index=True, max_length=16, verbose_name='俱乐部')),
|
||||
('role', models.CharField(choices=[('dashou', '打手'), ('guanshi', '管事'), ('zuzhang', '组长'), ('shenheguan', '考核官')], db_index=True, max_length=16, verbose_name='角色')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='是否启用冻结')),
|
||||
('freeze_mode', models.SmallIntegerField(choices=[(1, '按比例'), (2, '固定额'), (3, '比例+单笔上限')], default=1)),
|
||||
('freeze_rate', models.DecimalField(decimal_places=6, default=Decimal('0'), help_text='0.2=20%', max_digits=8, verbose_name='冻结比例')),
|
||||
('freeze_fixed', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12)),
|
||||
('freeze_cap', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12, verbose_name='单笔冻结上限(0=不限)')),
|
||||
('min_credit_to_freeze', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12, verbose_name='低于此入账不冻')),
|
||||
('source_scope', models.JSONField(blank=True, default=list, help_text='空=全部来源;如 ["order_settle"]', verbose_name='入账来源白名单')),
|
||||
('unfreeze_trigger', models.CharField(choices=[('time_only', '仅到期'), ('condition_only', '仅条件达标'), ('time_and_condition', '到期且达标'), ('time_or_condition', '到期或达标'), ('manual_only', '仅手动')], default='time_only', max_length=32)),
|
||||
('schedule_mode', models.CharField(choices=[('all_at_once', '一次全解'), ('installment', '分期解冻')], default='all_at_once', max_length=16)),
|
||||
('installment_json', models.JSONField(blank=True, default=dict, help_text='{"steps":[...]}', verbose_name='分期计划')),
|
||||
('condition_json', models.JSONField(blank=True, default=dict, verbose_name='解冻条件')),
|
||||
('freeze_min_days', models.IntegerField(default=0, verbose_name='最短冻结天数')),
|
||||
('remark', models.CharField(blank=True, default='', max_length=255)),
|
||||
('updated_by', models.CharField(blank=True, default='', max_length=32)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '资金冻结规则',
|
||||
'db_table': 'club_fund_freeze_config',
|
||||
'unique_together': {('club_id', 'role')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FundFreezeLedger',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(db_index=True, max_length=16)),
|
||||
('user_id', models.CharField(db_index=True, max_length=16, verbose_name='UserUID')),
|
||||
('role', models.CharField(db_index=True, max_length=16)),
|
||||
('amount_total', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('amount_unfrozen', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=12)),
|
||||
('status', models.SmallIntegerField(db_index=True, default=1)),
|
||||
('frozen_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('source_type', models.CharField(db_index=True, max_length=32)),
|
||||
('order_id', models.CharField(blank=True, db_index=True, default='', max_length=64)),
|
||||
('czjilu_id', models.CharField(blank=True, db_index=True, default='', max_length=64)),
|
||||
('biz_id', models.CharField(max_length=64, verbose_name='幂等业务键')),
|
||||
('freeze_reason', models.CharField(blank=True, default='', max_length=255)),
|
||||
('config_snapshot', models.JSONField(blank=True, default=dict)),
|
||||
('current_step', models.IntegerField(default=1)),
|
||||
('next_eligible_at', models.DateTimeField(blank=True, db_index=True, null=True)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '资金冻结主单',
|
||||
'db_table': 'fund_freeze_ledger',
|
||||
'unique_together': {('club_id', 'role', 'source_type', 'biz_id')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FundUnfreezeEvent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ledger_id', models.BigIntegerField(db_index=True)),
|
||||
('club_id', models.CharField(db_index=True, max_length=16)),
|
||||
('user_id', models.CharField(db_index=True, max_length=16)),
|
||||
('role', models.CharField(max_length=16)),
|
||||
('step_seq', models.IntegerField(default=1)),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('unfrozen_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('trigger', models.CharField(default='schedule', max_length=16)),
|
||||
('reason', models.CharField(blank=True, default='', max_length=255)),
|
||||
('operator_id', models.CharField(blank=True, default='', max_length=32)),
|
||||
('condition_snapshot', models.JSONField(blank=True, default=dict)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '资金解冻事件',
|
||||
'db_table': 'fund_unfreeze_event',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FundRiskStatDaily',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('club_id', models.CharField(db_index=True, max_length=16)),
|
||||
('user_id', models.CharField(db_index=True, max_length=16)),
|
||||
('role', models.CharField(db_index=True, max_length=16)),
|
||||
('day', models.DateField(db_index=True)),
|
||||
('completed_orders', models.IntegerField(default=0)),
|
||||
('fine_count', models.IntegerField(default=0)),
|
||||
('complaint_count', models.IntegerField(default=0)),
|
||||
('valid_invite_count', models.IntegerField(default=0)),
|
||||
('meta_json', models.JSONField(blank=True, default=dict)),
|
||||
('CreateTime', models.DateTimeField(auto_now_add=True)),
|
||||
('UpdateTime', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '资金风险日汇总',
|
||||
'db_table': 'fund_risk_stat_daily',
|
||||
'unique_together': {('club_id', 'user_id', 'role', 'day')},
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='fundfreezeledger',
|
||||
index=models.Index(fields=['status', 'next_eligible_at'], name='idx_ffl_status_next'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='fundfreezeledger',
|
||||
index=models.Index(fields=['user_id', 'role', 'status'], name='idx_ffl_user_role'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='fundfreezeledger',
|
||||
index=models.Index(fields=['club_id', 'status', 'frozen_at'], name='idx_ffl_club_st'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='fundriskstatdaily',
|
||||
index=models.Index(fields=['club_id', 'role', 'day'], name='idx_frsd_club_role_day'),
|
||||
),
|
||||
]
|
||||
151
jituan/models.py
151
jituan/models.py
@@ -464,3 +464,154 @@ class ClubOrderGrabLink(QModel):
|
||||
models.Index(fields=['grabber_club_id'], name='idx_cogl_grabber'),
|
||||
models.Index(fields=['order_club_id'], name='idx_cogl_order'),
|
||||
]
|
||||
|
||||
|
||||
# ==================== 资金冻结(默认关闭,按俱乐部×角色开启)====================
|
||||
|
||||
class ClubFundFreezeConfig(QModel):
|
||||
"""俱乐部×角色冻结规则。enabled=False 时入账行为与现网完全一致。"""
|
||||
ROLE_CHOICES = (
|
||||
('dashou', '打手'),
|
||||
('guanshi', '管事'),
|
||||
('zuzhang', '组长'),
|
||||
('shenheguan', '考核官'),
|
||||
)
|
||||
FREEZE_MODE_CHOICES = (
|
||||
(1, '按比例'),
|
||||
(2, '固定额'),
|
||||
(3, '比例+单笔上限'),
|
||||
)
|
||||
UNFREEZE_TRIGGER_CHOICES = (
|
||||
('time_only', '仅到期'),
|
||||
('condition_only', '仅条件达标'),
|
||||
('time_and_condition', '到期且达标'),
|
||||
('time_or_condition', '到期或达标'),
|
||||
('manual_only', '仅手动'),
|
||||
)
|
||||
SCHEDULE_MODE_CHOICES = (
|
||||
('all_at_once', '一次全解'),
|
||||
('installment', '分期解冻'),
|
||||
)
|
||||
|
||||
club_id = models.CharField(max_length=16, db_index=True, verbose_name='俱乐部')
|
||||
role = models.CharField(max_length=16, choices=ROLE_CHOICES, db_index=True, verbose_name='角色')
|
||||
enabled = models.BooleanField(default=False, verbose_name='是否启用冻结')
|
||||
freeze_mode = models.SmallIntegerField(default=1, choices=FREEZE_MODE_CHOICES)
|
||||
freeze_rate = models.DecimalField(
|
||||
max_digits=8, decimal_places=6, default=Decimal('0'),
|
||||
verbose_name='冻结比例', help_text='0.2=20%',
|
||||
)
|
||||
freeze_fixed = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0'))
|
||||
freeze_cap = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, default=Decimal('0'),
|
||||
verbose_name='单笔冻结上限(0=不限)',
|
||||
)
|
||||
min_credit_to_freeze = models.DecimalField(
|
||||
max_digits=12, decimal_places=2, default=Decimal('0'),
|
||||
verbose_name='低于此入账不冻',
|
||||
)
|
||||
source_scope = models.JSONField(
|
||||
default=list, blank=True,
|
||||
verbose_name='入账来源白名单',
|
||||
help_text='空=全部来源;如 ["order_settle"]',
|
||||
)
|
||||
unfreeze_trigger = models.CharField(
|
||||
max_length=32, default='time_only', choices=UNFREEZE_TRIGGER_CHOICES,
|
||||
)
|
||||
schedule_mode = models.CharField(
|
||||
max_length=16, default='all_at_once', choices=SCHEDULE_MODE_CHOICES,
|
||||
)
|
||||
installment_json = models.JSONField(
|
||||
default=dict, blank=True,
|
||||
verbose_name='分期计划',
|
||||
help_text='{"steps":[{"seq":1,"after_days":7,"type":"ratio","value":0.3,"need_condition":false}]}',
|
||||
)
|
||||
condition_json = models.JSONField(default=dict, blank=True, verbose_name='解冻条件')
|
||||
freeze_min_days = models.IntegerField(default=0, verbose_name='最短冻结天数')
|
||||
remark = models.CharField(max_length=255, blank=True, default='')
|
||||
updated_by = models.CharField(max_length=32, blank=True, default='')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'club_fund_freeze_config'
|
||||
verbose_name = '资金冻结规则'
|
||||
unique_together = [['club_id', 'role']]
|
||||
|
||||
|
||||
class FundFreezeLedger(QModel):
|
||||
"""单笔入账冻结主单。"""
|
||||
STATUS_FROZEN = 1
|
||||
STATUS_PARTIAL = 2
|
||||
STATUS_DONE = 3
|
||||
STATUS_VOID = 4
|
||||
|
||||
club_id = models.CharField(max_length=16, db_index=True)
|
||||
user_id = models.CharField(max_length=16, db_index=True, verbose_name='UserUID')
|
||||
role = models.CharField(max_length=16, db_index=True)
|
||||
amount_total = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
amount_unfrozen = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0'))
|
||||
status = models.SmallIntegerField(default=1, db_index=True)
|
||||
frozen_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
source_type = models.CharField(max_length=32, db_index=True)
|
||||
order_id = models.CharField(max_length=64, blank=True, default='', db_index=True)
|
||||
czjilu_id = models.CharField(max_length=64, blank=True, default='', db_index=True)
|
||||
biz_id = models.CharField(max_length=64, verbose_name='幂等业务键')
|
||||
freeze_reason = models.CharField(max_length=255, blank=True, default='')
|
||||
config_snapshot = models.JSONField(default=dict, blank=True)
|
||||
current_step = models.IntegerField(default=1)
|
||||
next_eligible_at = models.DateTimeField(null=True, blank=True, db_index=True)
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'fund_freeze_ledger'
|
||||
verbose_name = '资金冻结主单'
|
||||
unique_together = [['club_id', 'role', 'source_type', 'biz_id']]
|
||||
indexes = [
|
||||
models.Index(fields=['status', 'next_eligible_at'], name='idx_ffl_status_next'),
|
||||
models.Index(fields=['user_id', 'role', 'status'], name='idx_ffl_user_role'),
|
||||
models.Index(fields=['club_id', 'status', 'frozen_at'], name='idx_ffl_club_st'),
|
||||
]
|
||||
|
||||
|
||||
class FundUnfreezeEvent(QModel):
|
||||
"""每一次解冻执行记录。"""
|
||||
ledger_id = models.BigIntegerField(db_index=True)
|
||||
club_id = models.CharField(max_length=16, db_index=True)
|
||||
user_id = models.CharField(max_length=16, db_index=True)
|
||||
role = models.CharField(max_length=16)
|
||||
step_seq = models.IntegerField(default=1)
|
||||
amount = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
unfrozen_at = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
trigger = models.CharField(max_length=16, default='schedule')
|
||||
reason = models.CharField(max_length=255, blank=True, default='')
|
||||
operator_id = models.CharField(max_length=32, blank=True, default='')
|
||||
condition_snapshot = models.JSONField(default=dict, blank=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'fund_unfreeze_event'
|
||||
verbose_name = '资金解冻事件'
|
||||
|
||||
|
||||
class FundRiskStatDaily(QModel):
|
||||
"""日汇总:罚款/投诉/完成单等,供解冻条件与后台展示。"""
|
||||
club_id = models.CharField(max_length=16, db_index=True)
|
||||
user_id = models.CharField(max_length=16, db_index=True)
|
||||
role = models.CharField(max_length=16, db_index=True)
|
||||
day = models.DateField(db_index=True)
|
||||
completed_orders = models.IntegerField(default=0)
|
||||
fine_count = models.IntegerField(default=0)
|
||||
complaint_count = models.IntegerField(default=0)
|
||||
valid_invite_count = models.IntegerField(default=0)
|
||||
meta_json = models.JSONField(default=dict, blank=True)
|
||||
CreateTime = models.DateTimeField(auto_now_add=True)
|
||||
UpdateTime = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'fund_risk_stat_daily'
|
||||
verbose_name = '资金风险日汇总'
|
||||
unique_together = [['club_id', 'user_id', 'role', 'day']]
|
||||
indexes = [
|
||||
models.Index(fields=['club_id', 'role', 'day'], name='idx_frsd_club_role_day'),
|
||||
]
|
||||
|
||||
275
jituan/services/fund_freeze.py
Normal file
275
jituan/services/fund_freeze.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""资金冻结入账网关(P0)。
|
||||
|
||||
默认:无配置或 enabled=False → 全额进可提现,与现网一致,不写流水。
|
||||
开启后:拆「可提现 + 冻结池」并写 FundFreezeLedger。
|
||||
|
||||
注意:P0 仅提供网关;结算调用点在 P1 收口,避免一次改太多接口。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.models import F
|
||||
from django.utils import timezone
|
||||
|
||||
from jituan.constants import CLUB_ID_DEFAULT
|
||||
from jituan.models import ClubFundFreezeConfig, FundFreezeLedger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ROLE_DASHOU = 'dashou'
|
||||
ROLE_GUANSHI = 'guanshi'
|
||||
ROLE_ZUZHANG = 'zuzhang'
|
||||
ROLE_SHENHEGUAN = 'shenheguan'
|
||||
|
||||
SOURCE_ORDER_SETTLE = 'order_settle'
|
||||
SOURCE_FENHONG = 'fenhong'
|
||||
SOURCE_KAOHE_FEE = 'kaohe_fee'
|
||||
|
||||
_CFG_CACHE_TTL = 60
|
||||
|
||||
|
||||
def _money(v) -> Decimal:
|
||||
return Decimal(str(v or 0)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
|
||||
|
||||
|
||||
def _club(club_id: Optional[str]) -> str:
|
||||
return (club_id or '').strip() or CLUB_ID_DEFAULT
|
||||
|
||||
|
||||
def get_freeze_config(club_id: Optional[str], role: str) -> Optional[ClubFundFreezeConfig]:
|
||||
cid = _club(club_id)
|
||||
role = (role or '').strip().lower()
|
||||
key = f'fund_freeze_cfg:{cid}:{role}'
|
||||
cached = cache.get(key)
|
||||
if cached is not None:
|
||||
return cached or None
|
||||
row = ClubFundFreezeConfig.query.filter(club_id=cid, role=role).first()
|
||||
cache.set(key, row or False, _CFG_CACHE_TTL)
|
||||
return row
|
||||
|
||||
|
||||
def invalidate_freeze_config_cache(club_id: Optional[str] = None, role: Optional[str] = None):
|
||||
if club_id and role:
|
||||
cache.delete(f'fund_freeze_cfg:{_club(club_id)}:{(role or "").strip().lower()}')
|
||||
return
|
||||
# 粗粒度:调用方改配置后建议带 club+role 清;此处留空避免误扫
|
||||
|
||||
|
||||
def calc_freeze_amount(credit: Decimal, cfg: ClubFundFreezeConfig) -> Decimal:
|
||||
credit = _money(credit)
|
||||
if credit <= 0:
|
||||
return Decimal('0.00')
|
||||
min_c = _money(cfg.min_credit_to_freeze)
|
||||
if min_c > 0 and credit < min_c:
|
||||
return Decimal('0.00')
|
||||
|
||||
mode = int(cfg.freeze_mode or 1)
|
||||
frozen = Decimal('0.00')
|
||||
if mode == 2:
|
||||
frozen = _money(cfg.freeze_fixed)
|
||||
elif mode == 3:
|
||||
frozen = _money(credit * Decimal(str(cfg.freeze_rate or 0)))
|
||||
cap = _money(cfg.freeze_cap)
|
||||
if cap > 0:
|
||||
frozen = min(frozen, cap)
|
||||
else:
|
||||
frozen = _money(credit * Decimal(str(cfg.freeze_rate or 0)))
|
||||
cap = _money(cfg.freeze_cap)
|
||||
if cap > 0:
|
||||
frozen = min(frozen, cap)
|
||||
|
||||
if frozen < 0:
|
||||
frozen = Decimal('0.00')
|
||||
if frozen > credit:
|
||||
frozen = credit
|
||||
return frozen
|
||||
|
||||
|
||||
def _source_allowed(cfg: ClubFundFreezeConfig, source_type: str) -> bool:
|
||||
scope = cfg.source_scope
|
||||
if not scope:
|
||||
return True
|
||||
if isinstance(scope, str):
|
||||
return source_type == scope
|
||||
try:
|
||||
return source_type in list(scope)
|
||||
except TypeError:
|
||||
return True
|
||||
|
||||
|
||||
def _config_snapshot(cfg: ClubFundFreezeConfig) -> Dict[str, Any]:
|
||||
return {
|
||||
'club_id': cfg.club_id,
|
||||
'role': cfg.role,
|
||||
'enabled': cfg.enabled,
|
||||
'freeze_mode': cfg.freeze_mode,
|
||||
'freeze_rate': str(cfg.freeze_rate),
|
||||
'freeze_fixed': str(cfg.freeze_fixed),
|
||||
'freeze_cap': str(cfg.freeze_cap),
|
||||
'min_credit_to_freeze': str(cfg.min_credit_to_freeze),
|
||||
'source_scope': cfg.source_scope,
|
||||
'unfreeze_trigger': cfg.unfreeze_trigger,
|
||||
'schedule_mode': cfg.schedule_mode,
|
||||
'installment_json': cfg.installment_json,
|
||||
'condition_json': cfg.condition_json,
|
||||
'freeze_min_days': cfg.freeze_min_days,
|
||||
}
|
||||
|
||||
|
||||
def _first_step_delay_days(cfg: ClubFundFreezeConfig) -> int:
|
||||
if (cfg.schedule_mode or '') == 'installment':
|
||||
steps = (cfg.installment_json or {}).get('steps') or []
|
||||
if steps:
|
||||
try:
|
||||
return max(0, int(steps[0].get('after_days') or 0))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return max(0, int(cfg.freeze_min_days or 0))
|
||||
|
||||
|
||||
def _add_withdrawable(role: str, profile, available: Decimal, also_zonge: bool = False):
|
||||
"""原子增加可提现;组长字段为 ketixian_jine。"""
|
||||
available = _money(available)
|
||||
if available == 0:
|
||||
return
|
||||
if role == ROLE_ZUZHANG:
|
||||
type(profile).query.filter(pk=profile.pk).update(
|
||||
ketixian_jine=F('ketixian_jine') + available,
|
||||
)
|
||||
else:
|
||||
updates = {'yue': F('yue') + available}
|
||||
if also_zonge and hasattr(profile, 'zonge'):
|
||||
updates['zonge'] = F('zonge') + available
|
||||
type(profile).query.filter(pk=profile.pk).update(**updates)
|
||||
|
||||
|
||||
def _add_dongjie(profile, frozen: Decimal):
|
||||
frozen = _money(frozen)
|
||||
if frozen == 0:
|
||||
return
|
||||
type(profile).query.filter(pk=profile.pk).update(
|
||||
dongjie_yue=F('dongjie_yue') + frozen,
|
||||
)
|
||||
|
||||
|
||||
def credit_role_balance(
|
||||
*,
|
||||
club_id: Optional[str],
|
||||
user_id: str,
|
||||
role: str,
|
||||
amount,
|
||||
source_type: str,
|
||||
biz_id: str,
|
||||
profile=None,
|
||||
order_id: str = '',
|
||||
czjilu_id: str = '',
|
||||
freeze_reason: str = '',
|
||||
also_credit_zonge: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
统一入账。返回 {available, frozen, ledger_id, skipped_freeze}。
|
||||
|
||||
调用方须在外层 transaction.atomic() 内,并已持有/即将更新的业务一致性。
|
||||
profile: 已加载的角色扩展实例(UserDashou 等);若空则按 role+user_id 查。
|
||||
"""
|
||||
role = (role or '').strip().lower()
|
||||
cid = _club(club_id)
|
||||
credit = _money(amount)
|
||||
biz_id = (biz_id or '').strip()
|
||||
source_type = (source_type or '').strip() or 'unknown'
|
||||
|
||||
if credit <= 0:
|
||||
return {'available': Decimal('0.00'), 'frozen': Decimal('0.00'), 'ledger_id': None, 'skipped_freeze': True}
|
||||
|
||||
if profile is None:
|
||||
profile = _load_profile(role, user_id)
|
||||
if profile is None:
|
||||
raise ValueError(f'找不到角色资料 role={role} user_id={user_id}')
|
||||
|
||||
cfg = get_freeze_config(cid, role)
|
||||
freeze_on = bool(
|
||||
cfg and cfg.enabled and _source_allowed(cfg, source_type) and biz_id
|
||||
)
|
||||
frozen = calc_freeze_amount(credit, cfg) if freeze_on else Decimal('0.00')
|
||||
available = _money(credit - frozen)
|
||||
|
||||
# 幂等:已存在同 biz 冻结单则只补可提现差额不重复冻(极端重入)
|
||||
if freeze_on and frozen > 0:
|
||||
existed = FundFreezeLedger.query.filter(
|
||||
club_id=cid, role=role, source_type=source_type, biz_id=biz_id,
|
||||
).first()
|
||||
if existed:
|
||||
logger.warning(
|
||||
'fund_freeze idempotent hit club=%s role=%s source=%s biz=%s — 不再入账',
|
||||
cid, role, source_type, biz_id,
|
||||
)
|
||||
return {
|
||||
'available': Decimal('0.00'),
|
||||
'frozen': Decimal('0.00'),
|
||||
'ledger_id': existed.id,
|
||||
'skipped_freeze': True,
|
||||
'idempotent': True,
|
||||
}
|
||||
|
||||
_add_withdrawable(role, profile, available, also_zonge=also_credit_zonge)
|
||||
ledger_id = None
|
||||
if frozen > 0:
|
||||
_add_dongjie(profile, frozen)
|
||||
delay = _first_step_delay_days(cfg)
|
||||
next_at = timezone.now() + timedelta(days=delay) if delay else timezone.now()
|
||||
ledger = FundFreezeLedger.query.create(
|
||||
club_id=cid,
|
||||
user_id=str(user_id),
|
||||
role=role,
|
||||
amount_total=frozen,
|
||||
amount_unfrozen=Decimal('0.00'),
|
||||
status=FundFreezeLedger.STATUS_FROZEN,
|
||||
source_type=source_type,
|
||||
order_id=(order_id or '')[:64],
|
||||
czjilu_id=(czjilu_id or '')[:64],
|
||||
biz_id=biz_id[:64],
|
||||
freeze_reason=(freeze_reason or f'{source_type}冻结')[:255],
|
||||
config_snapshot=_config_snapshot(cfg),
|
||||
current_step=1,
|
||||
next_eligible_at=next_at,
|
||||
)
|
||||
ledger_id = ledger.id
|
||||
|
||||
return {
|
||||
'available': available,
|
||||
'frozen': frozen,
|
||||
'ledger_id': ledger_id,
|
||||
'skipped_freeze': frozen <= 0,
|
||||
}
|
||||
|
||||
|
||||
def _load_profile(role: str, user_id: str):
|
||||
from users.models import User, UserDashou, UserGuanshi, UserZuzhang, UserShenheguan
|
||||
user = User.query.filter(UserUID=user_id).first()
|
||||
if not user:
|
||||
return None
|
||||
mapping = {
|
||||
ROLE_DASHOU: UserDashou,
|
||||
ROLE_GUANSHI: UserGuanshi,
|
||||
ROLE_ZUZHANG: UserZuzhang,
|
||||
ROLE_SHENHEGUAN: UserShenheguan,
|
||||
}
|
||||
model = mapping.get(role)
|
||||
if not model:
|
||||
return None
|
||||
return model.query.filter(user=user).first()
|
||||
|
||||
|
||||
def split_preview(club_id: Optional[str], role: str, amount, source_type: str = SOURCE_ORDER_SETTLE) -> Tuple[Decimal, Decimal]:
|
||||
"""只算不写库:后台试算用。"""
|
||||
credit = _money(amount)
|
||||
cfg = get_freeze_config(club_id, role)
|
||||
if not cfg or not cfg.enabled or not _source_allowed(cfg, source_type):
|
||||
return credit, Decimal('0.00')
|
||||
frozen = calc_freeze_amount(credit, cfg)
|
||||
return _money(credit - frozen), frozen
|
||||
@@ -6,6 +6,12 @@ KEFU_MENU_ROW_DEFS = [
|
||||
'perm_codes': ['bankuai']},
|
||||
{'page_id': 'finance.data', 'name': '财务数据', 'path': '/finance/data', 'parent_id': 'finance', 'sort_order': 12,
|
||||
'perm_codes': ['caiwu']},
|
||||
{'page_id': 'finance.fund-freeze-config', 'name': '资金冻结规则', 'path': '/finance/fund-freeze-config',
|
||||
'parent_id': 'finance', 'sort_order': 13, 'perm_codes': ['djcfg001']},
|
||||
{'page_id': 'finance.fund-freeze-ledger', 'name': '资金冻结明细', 'path': '/finance/fund-freeze-ledger',
|
||||
'parent_id': 'finance', 'sort_order': 14, 'perm_codes': ['djled001']},
|
||||
{'page_id': 'finance.fund-risk-stats', 'name': '罚款投诉指标', 'path': '/finance/fund-risk-stats',
|
||||
'parent_id': 'finance', 'sort_order': 15, 'perm_codes': ['djrisk001']},
|
||||
{'page_id': 'assessment', 'name': '考核管理', 'path': '', 'parent_id': '', 'sort_order': 20},
|
||||
{'page_id': 'assessment.examiner', 'name': '考核官管理', 'path': '/assessment/examiner', 'parent_id': 'assessment', 'sort_order': 21,
|
||||
'perm_codes': ['kaohepeizhi']},
|
||||
|
||||
133
users/migrations/0007_role_fund_freeze_fields.py
Normal file
133
users/migrations/0007_role_fund_freeze_fields.py
Normal file
@@ -0,0 +1,133 @@
|
||||
# Generated manually for fund freeze P0 fields on role profiles
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0006_yonghu_c_yaoqing'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userdashou',
|
||||
name='dongjie_yue',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='冻结资金池'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userdashou',
|
||||
name='risk_fine_rate',
|
||||
field=models.DecimalField(decimal_places=6, default=0, max_digits=8, verbose_name='窗口罚款率'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userdashou',
|
||||
name='risk_complaint_rate',
|
||||
field=models.DecimalField(decimal_places=6, default=0, max_digits=8, verbose_name='窗口投诉率'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userdashou',
|
||||
name='risk_completed_orders',
|
||||
field=models.IntegerField(default=0, verbose_name='窗口完成单量'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userdashou',
|
||||
name='risk_valid_invites',
|
||||
field=models.IntegerField(default=0, verbose_name='窗口有效邀请数'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userdashou',
|
||||
name='risk_stat_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='风险快照时间'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userguanshi',
|
||||
name='dongjie_yue',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='冻结资金池'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userguanshi',
|
||||
name='risk_fine_rate',
|
||||
field=models.DecimalField(decimal_places=6, default=0, max_digits=8, verbose_name='窗口罚款率'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userguanshi',
|
||||
name='risk_complaint_rate',
|
||||
field=models.DecimalField(decimal_places=6, default=0, max_digits=8, verbose_name='窗口投诉率'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userguanshi',
|
||||
name='risk_completed_orders',
|
||||
field=models.IntegerField(default=0, verbose_name='窗口完成单量'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userguanshi',
|
||||
name='risk_valid_invites',
|
||||
field=models.IntegerField(default=0, verbose_name='窗口有效邀请数'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userguanshi',
|
||||
name='risk_stat_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='风险快照时间'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userzuzhang',
|
||||
name='dongjie_yue',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='冻结资金池'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userzuzhang',
|
||||
name='risk_fine_rate',
|
||||
field=models.DecimalField(decimal_places=6, default=0, max_digits=8, verbose_name='窗口罚款率'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userzuzhang',
|
||||
name='risk_complaint_rate',
|
||||
field=models.DecimalField(decimal_places=6, default=0, max_digits=8, verbose_name='窗口投诉率'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userzuzhang',
|
||||
name='risk_completed_orders',
|
||||
field=models.IntegerField(default=0, verbose_name='窗口完成单量'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userzuzhang',
|
||||
name='risk_valid_invites',
|
||||
field=models.IntegerField(default=0, verbose_name='窗口有效邀请数'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userzuzhang',
|
||||
name='risk_stat_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='风险快照时间'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usershenheguan',
|
||||
name='dongjie_yue',
|
||||
field=models.DecimalField(decimal_places=2, default=0.0, max_digits=12, verbose_name='冻结资金池'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usershenheguan',
|
||||
name='risk_fine_rate',
|
||||
field=models.DecimalField(decimal_places=6, default=0, max_digits=8, verbose_name='窗口罚款率'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usershenheguan',
|
||||
name='risk_complaint_rate',
|
||||
field=models.DecimalField(decimal_places=6, default=0, max_digits=8, verbose_name='窗口投诉率'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usershenheguan',
|
||||
name='risk_completed_orders',
|
||||
field=models.IntegerField(default=0, verbose_name='窗口完成单量'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usershenheguan',
|
||||
name='risk_valid_invites',
|
||||
field=models.IntegerField(default=0, verbose_name='窗口有效邀请数'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='usershenheguan',
|
||||
name='risk_stat_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='风险快照时间'),
|
||||
),
|
||||
]
|
||||
@@ -158,6 +158,13 @@ class UserDashou(QModel):
|
||||
jinyueshouyi = models.DecimalField(max_digits=10, decimal_places=2, default=0.00, verbose_name='今月收益金额')
|
||||
jifen = models.IntegerField(default=0, verbose_name='打手积分')
|
||||
yajin= models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='打手押金')
|
||||
# 资金冻结池 + 风险快照(冻结功能;未开启俱乐部保持 0,不影响现网)
|
||||
dongjie_yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='冻结资金池')
|
||||
risk_fine_rate = models.DecimalField(max_digits=8, decimal_places=6, default=0, verbose_name='窗口罚款率')
|
||||
risk_complaint_rate = models.DecimalField(max_digits=8, decimal_places=6, default=0, verbose_name='窗口投诉率')
|
||||
risk_completed_orders = models.IntegerField(default=0, verbose_name='窗口完成单量')
|
||||
risk_valid_invites = models.IntegerField(default=0, verbose_name='窗口有效邀请数')
|
||||
risk_stat_at = models.DateTimeField(null=True, blank=True, verbose_name='风险快照时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
|
||||
last_tixian_date = models.DateField(null=True, blank=True, verbose_name='最近提现日期') # 用于重置今日提现金额
|
||||
@@ -223,6 +230,12 @@ class UserGuanshi(QModel):
|
||||
jinyuechongzhi = models.IntegerField(default=0, verbose_name='今月充值打手数量')
|
||||
chongzhifenrun = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='充值分佣总额')
|
||||
yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='可提现余额')
|
||||
dongjie_yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='冻结资金池')
|
||||
risk_fine_rate = models.DecimalField(max_digits=8, decimal_places=6, default=0, verbose_name='窗口罚款率')
|
||||
risk_complaint_rate = models.DecimalField(max_digits=8, decimal_places=6, default=0, verbose_name='窗口投诉率')
|
||||
risk_completed_orders = models.IntegerField(default=0, verbose_name='窗口完成单量')
|
||||
risk_valid_invites = models.IntegerField(default=0, verbose_name='窗口有效邀请数')
|
||||
risk_stat_at = models.DateTimeField(null=True, blank=True, verbose_name='风险快照时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
# 新增:首次分红金额(针对特殊用户)
|
||||
fenhong_yici = models.DecimalField(max_digits=10,decimal_places=2,default=0.00,verbose_name='首次分红金额')
|
||||
@@ -266,6 +279,12 @@ class UserZuzhang(QModel):
|
||||
fenyong_zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='分佣总额(元)')
|
||||
# 可提现金额
|
||||
ketixian_jine = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='可提现金额(元)')
|
||||
dongjie_yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='冻结资金池')
|
||||
risk_fine_rate = models.DecimalField(max_digits=8, decimal_places=6, default=0, verbose_name='窗口罚款率')
|
||||
risk_complaint_rate = models.DecimalField(max_digits=8, decimal_places=6, default=0, verbose_name='窗口投诉率')
|
||||
risk_completed_orders = models.IntegerField(default=0, verbose_name='窗口完成单量')
|
||||
risk_valid_invites = models.IntegerField(default=0, verbose_name='窗口有效邀请数')
|
||||
risk_stat_at = models.DateTimeField(null=True, blank=True, verbose_name='风险快照时间')
|
||||
# 邀请总数
|
||||
yaoqing_zongshu = models.IntegerField(default=0, verbose_name='邀请总数')
|
||||
# 今日分佣
|
||||
@@ -315,6 +334,12 @@ class UserShenheguan(QModel):
|
||||
tongguo_zongshu = models.IntegerField(default=0, verbose_name='通过总数')
|
||||
yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='可提现余额')
|
||||
zonge = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='赚取总额')
|
||||
dongjie_yue = models.DecimalField(max_digits=12, decimal_places=2, default=0.00, verbose_name='冻结资金池')
|
||||
risk_fine_rate = models.DecimalField(max_digits=8, decimal_places=6, default=0, verbose_name='窗口罚款率')
|
||||
risk_complaint_rate = models.DecimalField(max_digits=8, decimal_places=6, default=0, verbose_name='窗口投诉率')
|
||||
risk_completed_orders = models.IntegerField(default=0, verbose_name='窗口完成单量')
|
||||
risk_valid_invites = models.IntegerField(default=0, verbose_name='窗口有效邀请数')
|
||||
risk_stat_at = models.DateTimeField(null=True, blank=True, verbose_name='风险快照时间')
|
||||
CreateTime = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user