fix: 冻结条件可评估+分期规范化,定时扫描更及时

后台条件真正按罚款率/单量等评估;选项式配置所需字段规范化;Celery 改为每5分钟;小程序说明文案接口补齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-26 19:36:10 +08:00
parent a505a1ab2d
commit 6c96eb3bcb
6 changed files with 485 additions and 41 deletions

View File

@@ -89,7 +89,7 @@ app.conf.beat_schedule = {
# 5. 资金冻结到期解冻(默认无冻结单时几乎空跑)
'scan_fund_freeze_due': {
'task': 'jituan.tasks.scan_fund_freeze_due',
'schedule': crontab(minute='*/15'),
'schedule': crontab(minute='*/5'),
'options': {
'queue': 'periodic_tasks',
'priority': 4,

View File

@@ -118,6 +118,8 @@ def _config_snapshot(cfg: ClubFundFreezeConfig) -> Dict[str, Any]:
'installment_json': cfg.installment_json,
'condition_json': cfg.condition_json,
'freeze_min_days': cfg.freeze_min_days,
'display_reason': (cfg.remark or '')[:255],
'remark': (cfg.remark or '')[:255],
}
@@ -233,7 +235,11 @@ def credit_role_balance(
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],
freeze_reason=(
freeze_reason
or (getattr(cfg, 'remark', None) if cfg else None)
or f'{source_type}冻结'
)[:255],
config_snapshot=_config_snapshot(cfg),
current_step=1,
next_eligible_at=next_at,
@@ -381,12 +387,14 @@ def apply_unfreeze_step(
force: bool = False,
) -> Dict[str, Any]:
"""
执行一笔冻结单的当前分期步(骨架:先支持到期/手动;条件解冻 P4 再接指标)
force=True 时忽略 next_eligible_at后台手动
执行一笔冻结单的当前分期步。
force=True:后台手动,跳过时间/条件门槛
条件未达标时不推进步数、不改 next_eligible_at供定时任务下次重试。
"""
from django.db import transaction
from users.models import UserDashou, UserGuanshi, UserZuzhang, UserShenheguan
from jituan.models import FundUnfreezeEvent
from jituan.services.fund_freeze_conditions import evaluate_unfreeze_conditions
with transaction.atomic():
ledger = (
@@ -400,29 +408,37 @@ def apply_unfreeze_step(
return {'ok': False, 'msg': '已完结或已作废'}
now = timezone.now()
if not force and ledger.next_eligible_at and ledger.next_eligible_at > now:
return {'ok': False, 'msg': '未到解冻时间', 'next_eligible_at': ledger.next_eligible_at}
snap = ledger.config_snapshot or {}
trigger_mode = (snap.get('unfreeze_trigger') or 'time_only')
min_days = max(0, int(snap.get('freeze_min_days') or 0))
frozen_at = ledger.frozen_at or now
min_days_ok = force or (now >= frozen_at + timedelta(days=min_days))
time_ok = force or (
not ledger.next_eligible_at or ledger.next_eligible_at <= now
)
if trigger_mode == 'manual_only' and not force:
return {'ok': False, 'msg': '仅允许手动解冻'}
# condition_*自动任务暂不评估指标P4手动 force 可解
if trigger_mode == 'condition_only' and not force:
return {'ok': False, 'msg': '条件解冻尚未启用自动评估'}
steps = _plan_steps(snap)
step_idx = max(0, int(ledger.current_step or 1) - 1)
if step_idx >= len(steps):
step = {'seq': ledger.current_step, 'type': 'remain', 'value': 1}
step = {'seq': ledger.current_step, 'type': 'remain', 'value': 1, 'need_condition': False}
else:
step = steps[step_idx]
need_step_cond = bool(step.get('need_condition'))
amount = _step_unfreeze_amount(ledger, step)
if amount <= 0:
ledger.status = FundFreezeLedger.STATUS_DONE
ledger.save(update_fields=['status', 'UpdateTime'])
return {'ok': True, 'amount': Decimal('0.00'), 'done': True}
cond_ok = True
cond_snap: Dict[str, Any] = {}
need_eval = (
not force
and (
need_step_cond
or trigger_mode in (
'condition_only', 'time_and_condition', 'time_or_condition',
)
)
)
profile_map = {
ROLE_DASHOU: UserDashou,
@@ -442,6 +458,63 @@ def apply_unfreeze_step(
if not profile:
return {'ok': False, 'msg': '角色扩展表不存在'}
if need_eval:
cond_ok, cond_snap = evaluate_unfreeze_conditions(
ledger.club_id,
ledger.user_id,
ledger.role,
snap.get('condition_json') or {},
profile=profile,
)
if not force:
if trigger_mode == 'time_only':
if not time_ok:
return {'ok': False, 'msg': '未到解冻时间', 'next_eligible_at': ledger.next_eligible_at}
elif trigger_mode == 'condition_only':
if not min_days_ok:
return {'ok': False, 'msg': f'未满最短冻结天数({min_days}天)'}
if not cond_ok:
return {
'ok': False,
'msg': '条件未达标',
'condition': cond_snap,
'retry': True,
}
elif trigger_mode == 'time_and_condition':
if not time_ok:
return {'ok': False, 'msg': '未到解冻时间', 'next_eligible_at': ledger.next_eligible_at}
if not cond_ok:
return {
'ok': False,
'msg': '已到期但条件未达标,等待达标后自动解',
'condition': cond_snap,
'retry': True,
}
elif trigger_mode == 'time_or_condition':
if not (time_ok or cond_ok):
return {
'ok': False,
'msg': '未到期且条件未达标',
'condition': cond_snap,
'retry': True,
}
# 分期单步要求达标时,即使全局是 time_only / time_or 已到期,也仍需条件
if need_step_cond and not cond_ok:
return {
'ok': False,
'msg': '本分期步要求条件达标',
'condition': cond_snap,
'retry': True,
}
amount = _step_unfreeze_amount(ledger, step)
if amount <= 0:
ledger.status = FundFreezeLedger.STATUS_DONE
ledger.save(update_fields=['status', 'UpdateTime'])
return {'ok': True, 'amount': Decimal('0.00'), 'done': True}
# 冻结池不足则按实际可解
cur_frozen = _money(getattr(profile, 'dongjie_yue', 0) or 0)
if cur_frozen < amount:
@@ -472,19 +545,22 @@ def apply_unfreeze_step(
else:
ledger.status = FundFreezeLedger.STATUS_PARTIAL
ledger.current_step = next_step
# 下一步到期时间
if next_step - 1 < len(steps):
nxt = steps[next_step - 1]
days = int(nxt.get('after_days') or 0)
# after_days 相对冻结日
ledger.next_eligible_at = ledger.frozen_at + timedelta(days=days)
ledger.next_eligible_at = frozen_at + timedelta(days=days)
else:
# 无更多配置步:剩余立即进入可扫状态,下一步按 remain 吃完
ledger.next_eligible_at = now
ledger.save(update_fields=[
'amount_unfrozen', 'status', 'current_step', 'next_eligible_at', 'UpdateTime',
])
event_trigger = trigger
if not force and trigger_mode in ('condition_only', 'time_or_condition') and cond_ok and not time_ok:
event_trigger = 'condition'
FundUnfreezeEvent.query.create(
ledger_id=ledger.id,
club_id=ledger.club_id,
@@ -492,10 +568,10 @@ def apply_unfreeze_step(
role=ledger.role,
step_seq=int(step.get('seq') or ledger.current_step),
amount=amount,
trigger=trigger,
reason=(reason or f'{trigger}解冻')[:255],
trigger=event_trigger,
reason=(reason or f'{event_trigger}解冻')[:255],
operator_id=(operator_id or '')[:32],
condition_snapshot={},
condition_snapshot=cond_snap or {},
)
return {
@@ -504,29 +580,65 @@ def apply_unfreeze_step(
'remain': max(remain, Decimal('0.00')),
'done': remain <= 0,
'ledger_id': ledger.id,
'condition': cond_snap,
}
def scan_due_freeze_ledgers(limit: int = 200) -> Dict[str, Any]:
"""定时任务入口:扫到期冻结单并尝试解冻一步。"""
"""
定时任务入口。
1) 到期next_eligible_at<=now的冻结中/部分解冻单
2) 条件触发类condition_only / time_or_condition —— 即使未到期也扫描,避免「条件先达标却要干等到期」
临界:条件未达标只记 fail+retry不改账本下次 beat 再试。
"""
from django.db.models import Q
now = timezone.now()
qs = (
FundFreezeLedger.query.filter(
status__in=[FundFreezeLedger.STATUS_FROZEN, FundFreezeLedger.STATUS_PARTIAL],
next_eligible_at__lte=now,
)
.order_by('next_eligible_at', 'id')[:limit]
base = FundFreezeLedger.query.filter(
status__in=[FundFreezeLedger.STATUS_FROZEN, FundFreezeLedger.STATUS_PARTIAL],
)
ok_n = fail_n = 0
for row in qs:
due_ids = list(
base.filter(next_eligible_at__lte=now)
.order_by('next_eligible_at', 'id')
.values_list('id', flat=True)[:limit]
)
# JSON 字段路径MySQL/Postgres Django JSONField 支持 key 查询
cond_ids = []
remain = max(0, limit - len(due_ids))
if remain > 0:
try:
r = apply_unfreeze_step(row.id, trigger='schedule')
cond_ids = list(
base.filter(
Q(config_snapshot__unfreeze_trigger='condition_only')
| Q(config_snapshot__unfreeze_trigger='time_or_condition')
)
.exclude(id__in=due_ids)
.order_by('id')
.values_list('id', flat=True)[:remain]
)
except Exception as e:
logger.warning('scan condition branch skipped: %s', e)
ids = due_ids + cond_ids
ok_n = fail_n = retry_n = 0
for lid in ids:
try:
r = apply_unfreeze_step(lid, trigger='schedule')
if r.get('ok'):
ok_n += 1
else:
fail_n += 1
logger.info('unfreeze skip id=%s msg=%s', row.id, r.get('msg'))
if r.get('retry'):
retry_n += 1
logger.info('unfreeze skip id=%s msg=%s', lid, r.get('msg'))
except Exception as e:
fail_n += 1
logger.exception('unfreeze error id=%s: %s', row.id, e)
return {'ok': ok_n, 'fail': fail_n, 'scanned': ok_n + fail_n}
logger.exception('unfreeze error id=%s: %s', lid, e)
return {
'ok': ok_n,
'fail': fail_n,
'retry': retry_n,
'scanned': len(ids),
'due': len(due_ids),
'cond_scan': len(cond_ids),
}

View File

@@ -72,6 +72,7 @@ def _cfg_to_dict(row: Optional[ClubFundFreezeConfig], club_id: str, role: str) -
},
'freeze_min_days': 0,
'remark': '',
'display_reason': '',
'exists': False,
}
return {
@@ -90,6 +91,7 @@ def _cfg_to_dict(row: Optional[ClubFundFreezeConfig], club_id: str, role: str) -
'condition_json': row.condition_json or {},
'freeze_min_days': int(row.freeze_min_days or 0),
'remark': row.remark or '',
'display_reason': row.remark or '',
'updated_by': row.updated_by or '',
'exists': True,
}
@@ -158,8 +160,40 @@ def save_config(request, data: dict, operator: str = '') -> Tuple[Optional[dict]
installment_json = json.loads(installment_json)
except Exception:
return None, 'installment_json 不是合法 JSON'
if schedule_mode == 'installment' and not (installment_json.get('steps') or []):
installment_json = DEFAULT_INSTALLMENT
if schedule_mode == 'installment':
steps = installment_json.get('steps') if isinstance(installment_json, dict) else None
if not steps:
installment_json = dict(DEFAULT_INSTALLMENT)
steps = installment_json['steps']
# 规范化:自由添加的分期重新编号,校验类型
normalized = []
for i, s in enumerate(steps):
if not isinstance(s, dict):
return None, f'{i + 1}期格式错误'
typ = (s.get('type') or 'ratio').strip().lower()
if typ not in ('ratio', 'fixed', 'remain'):
return None, f'{i + 1}期类型无效ratio/fixed/remain'
try:
after_days = max(0, int(s.get('after_days') or 0))
except (TypeError, ValueError):
return None, f'{i + 1}期天数无效'
try:
value = float(s.get('value') or 0)
except (TypeError, ValueError):
return None, f'{i + 1}期数值无效'
if typ == 'ratio' and (value < 0 or value > 1):
return None, f'{i + 1}期比例须在 0~1'
normalized.append({
'seq': i + 1,
'after_days': after_days,
'type': typ,
'value': value if typ != 'remain' else 1,
'need_condition': bool(s.get('need_condition')),
})
if not normalized:
return None, '请至少添加一期分期'
# 最后一期建议 remain若全是 ratio 且合计偏离太大仅告警不拦截——尾差由 remain 或末步吃完
installment_json = {'steps': normalized}
condition_json = data.get('condition_json') or {}
if isinstance(condition_json, str):
@@ -172,6 +206,9 @@ def save_config(request, data: dict, operator: str = '') -> Tuple[Optional[dict]
if freeze_min_days < 0:
return None, '最短冻结天数不能为负'
# 用户可见冻结说明:优先 display_reason兼容 remark
display_reason = (data.get('display_reason') or data.get('remark') or '')[:255]
row = ClubFundFreezeConfig.query.filter(club_id=club_id, role=role).first()
if not row:
row = ClubFundFreezeConfig(
@@ -190,7 +227,7 @@ def save_config(request, data: dict, operator: str = '') -> Tuple[Optional[dict]
row.installment_json = installment_json
row.condition_json = condition_json
row.freeze_min_days = freeze_min_days
row.remark = (data.get('remark') or '')[:255]
row.remark = display_reason
row.updated_by = (operator or '')[:32]
row.save()
invalidate_freeze_config_cache(club_id, role)

View File

@@ -0,0 +1,250 @@
"""资金冻结:解冻条件评估(实时算窗口指标,并回写角色 risk_* 快照)。"""
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.utils import timezone
logger = logging.getLogger(__name__)
def _dec(v, default='0') -> Decimal:
try:
return Decimal(str(v if v is not None else default)).quantize(
Decimal('0.000001'), rounding=ROUND_HALF_UP
)
except Exception:
return Decimal(default)
def _int(v, default=0) -> int:
try:
return int(v)
except (TypeError, ValueError):
return default
def _flag(cfg: dict, enable_key: str, legacy_active: bool) -> bool:
"""新配置用 enable_*;旧配置无 enable 时按 legacy_active 推断。"""
if enable_key in cfg:
return bool(cfg.get(enable_key))
return legacy_active
def compute_risk_metrics(
club_id: str,
user_id: str,
role: str,
condition_json: Optional[dict] = None,
) -> Dict[str, Any]:
"""按窗口统计完成单量 / 罚款率 / 投诉率 / 有效邀请。失败时返回安全默认。"""
cfg = condition_json or {}
fine_days = max(1, _int(cfg.get('fine_rate_window_days'), 30))
complaint_days = max(1, _int(cfg.get('complaint_rate_window_days'), 30))
order_days = max(fine_days, complaint_days, _int(cfg.get('order_window_days'), 30))
now = timezone.now()
since_order = now - timedelta(days=order_days)
since_fine = now - timedelta(days=fine_days)
since_complaint = now - timedelta(days=complaint_days)
completed = 0
fine_count = 0
complaint_count = 0
valid_invites = 0
try:
from orders.models import Order, Penalty
if role == 'dashou':
completed = Order.query.filter(
ClubID=club_id,
PlayerID=user_id,
Status=3,
CreateTime__gte=since_order,
).count()
fine_count = Penalty.query.filter(
PenalizedUserID=user_id,
Identity=1,
Status__in=[1, 2, 3, 5],
CreateTime__gte=since_fine,
).count()
# 投诉:窗口内该打手完成单关联的工单数
try:
from gongdan.models import Gongdan
order_ids = list(
Order.query.filter(
ClubID=club_id,
PlayerID=user_id,
Status=3,
CreateTime__gte=since_complaint,
).values_list('OrderID', flat=True)[:800]
)
if order_ids:
complaint_count = Gongdan.query.filter(
order_id__in=order_ids,
).count()
except Exception as e:
logger.warning('complaint metrics skip: %s', e)
valid_invites = 0
elif role == 'guanshi':
from users.models import User, UserGuanshi, UserDashou
u = User.query.filter(UserUID=user_id).first()
gs = UserGuanshi.query.filter(user=u).first() if u else None
if gs is not None:
valid_invites = int(getattr(gs, 'yaogingshuliang', 0) or 0)
# 管事「完成单」近似:窗口内其邀请打手的成交单量
dashou_uids = list(
UserDashou.query.filter(yaoqingren=user_id).values_list(
'user__UserUID', flat=True
)[:500]
)
if dashou_uids:
completed = Order.query.filter(
ClubID=club_id,
PlayerID__in=dashou_uids,
Status=3,
CreateTime__gte=since_order,
).count()
fine_count = Penalty.query.filter(
PenalizedUserID=user_id,
Identity=2,
Status__in=[1, 2, 3, 5],
CreateTime__gte=since_fine,
).count()
try:
from gongdan.models import Gongdan
complaint_count = Gongdan.query.filter(
guanshi_id=user_id,
CreateTime__gte=since_complaint,
).count()
except Exception:
pass
elif role == 'zuzhang':
from users.models import User, UserGuanshi, UserZuzhang
u = User.query.filter(UserUID=user_id).first()
zz = UserZuzhang.query.filter(user=u).first() if u else None
if zz is not None:
valid_invites = UserGuanshi.query.filter(yaoqingren=user_id).count()
fine_count = Penalty.query.filter(
PenalizedUserID=user_id,
Identity=4,
Status__in=[1, 2, 3, 5],
CreateTime__gte=since_fine,
).count()
completed = valid_invites # 组长无直接结单,用邀请管事数作「完成」近似时可被 min_orders 覆盖
else: # shenheguan
fine_count = Penalty.query.filter(
PenalizedUserID=user_id,
Status__in=[1, 2, 3, 5],
CreateTime__gte=since_fine,
).count()
except Exception as e:
logger.exception('compute_risk_metrics failed user=%s role=%s: %s', user_id, role, e)
denom = max(completed, 1)
fine_rate = _dec(fine_count) / _dec(denom)
complaint_rate = _dec(complaint_count) / _dec(denom)
return {
'completed_orders': completed,
'fine_count': fine_count,
'complaint_count': complaint_count,
'fine_rate': str(fine_rate),
'complaint_rate': str(complaint_rate),
'valid_invites': valid_invites,
'order_window_days': order_days,
'fine_rate_window_days': fine_days,
'complaint_rate_window_days': complaint_days,
}
def write_risk_snapshot(profile, metrics: dict) -> None:
if profile is None:
return
try:
profile.__class__.query.filter(pk=profile.pk).update(
risk_fine_rate=_dec(metrics.get('fine_rate')),
risk_complaint_rate=_dec(metrics.get('complaint_rate')),
risk_completed_orders=_int(metrics.get('completed_orders')),
risk_valid_invites=_int(metrics.get('valid_invites')),
risk_stat_at=timezone.now(),
)
except Exception as e:
logger.warning('write_risk_snapshot failed: %s', e)
def evaluate_unfreeze_conditions(
club_id: str,
user_id: str,
role: str,
condition_json: Optional[dict],
profile=None,
) -> Tuple[bool, Dict[str, Any]]:
"""
返回 (是否达标, 指标快照)。
未勾选的条件不参与;全部未勾选视为「无条件限制」→ 达标。
"""
cfg = condition_json or {}
metrics = compute_risk_metrics(club_id, user_id, role, cfg)
write_risk_snapshot(profile, metrics)
checks = []
reasons = []
# 最低完成单量
min_orders = _int(cfg.get('min_completed_orders'), 0)
if _flag(cfg, 'enable_min_completed_orders', min_orders > 0):
ok = metrics['completed_orders'] >= min_orders
checks.append(ok)
if not ok:
reasons.append(
f"完成单量 {metrics['completed_orders']}/{min_orders}"
)
# 最高罚款率
max_fine = _dec(cfg.get('max_fine_rate'), '1')
if _flag(cfg, 'enable_max_fine_rate', 0 <= max_fine < 1):
ok = _dec(metrics['fine_rate']) <= max_fine
checks.append(ok)
if not ok:
reasons.append(
f"罚款率 {metrics['fine_rate']} > {max_fine}"
)
# 最高投诉率
max_comp = _dec(cfg.get('max_complaint_rate'), '1')
if _flag(cfg, 'enable_max_complaint_rate', 0 <= max_comp < 1):
ok = _dec(metrics['complaint_rate']) <= max_comp
checks.append(ok)
if not ok:
reasons.append(
f"投诉率 {metrics['complaint_rate']} > {max_comp}"
)
# 最低有效邀请
min_inv = _int(cfg.get('min_valid_invites'), 0)
if _flag(cfg, 'enable_min_valid_invites', min_inv > 0):
ok = metrics['valid_invites'] >= min_inv
checks.append(ok)
if not ok:
reasons.append(
f"有效邀请 {metrics['valid_invites']}/{min_inv}"
)
if not checks:
passed = True
else:
passed = all(checks)
snap = {
**metrics,
'passed': passed,
'fail_reasons': reasons,
}
return passed, snap

View File

@@ -13,7 +13,7 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from jituan.models import FundFreezeLedger, FundUnfreezeEvent
from jituan.models import ClubFundFreezeConfig, FundFreezeLedger, FundUnfreezeEvent
from jituan.services.club_user import get_user_club_id
from jituan.services.fund_freeze import (
ROLE_DASHOU,
@@ -109,6 +109,19 @@ class FundFreezeMineView(APIView):
]
total_dongjie = sum((Decimal(r['dongjie_yue']) for r in roles), Decimal('0.00'))
total_withdrawable = sum((Decimal(r['withdrawable']) for r in roles), Decimal('0.00'))
# 用户可见说明:优先打手规则,否则取任意已配置角色
display_reason = ''
if club_id:
cfgs = list(ClubFundFreezeConfig.query.filter(club_id=club_id, enabled=True))
prefer = next((c for c in cfgs if c.role == ROLE_DASHOU), None) or (cfgs[0] if cfgs else None)
if prefer and prefer.remark:
display_reason = prefer.remark
if not display_reason:
any_cfg = ClubFundFreezeConfig.query.filter(club_id=club_id).exclude(remark='').first()
if any_cfg:
display_reason = any_cfg.remark or ''
return Response({
'code': 200,
'msg': '获取成功',
@@ -119,6 +132,10 @@ class FundFreezeMineView(APIView):
'total_dongjie': _money(total_dongjie),
'total_withdrawable': _money(total_withdrawable),
'has_freeze': total_dongjie > 0,
'display_reason': display_reason or (
'部分佣金暂存为冻结保障金,用于订单售后与纠纷保障;'
'按俱乐部规则到期或达标后自动转入可提现余额,冻结中金额不可提现。'
),
},
})
@@ -173,7 +190,11 @@ class FundFreezeMyLedgerView(APIView):
'source_type': r.source_type,
'source_label': SOURCE_LABEL.get(r.source_type, r.source_type or '入账'),
'order_id': r.order_id or '',
'freeze_reason': r.freeze_reason or '',
'freeze_reason': r.freeze_reason or (
(r.config_snapshot or {}).get('display_reason')
or (r.config_snapshot or {}).get('remark')
or ''
),
'current_step': r.current_step,
})
@@ -246,7 +267,11 @@ class FundFreezeMyLedgerDetailView(APIView):
'source_type': ledger.source_type,
'source_label': SOURCE_LABEL.get(ledger.source_type, ledger.source_type or '入账'),
'order_id': ledger.order_id or '',
'freeze_reason': ledger.freeze_reason or '',
'freeze_reason': ledger.freeze_reason or (
(ledger.config_snapshot or {}).get('display_reason')
or (ledger.config_snapshot or {}).get('remark')
or ''
),
'current_step': ledger.current_step,
'events': event_list,
},

View File

@@ -1112,10 +1112,30 @@ class TixianAssetView(APIView):
from jituan.services.club_user import get_user_club_id
from jituan.services.withdraw_config import build_tixian_asset_rates
from jituan.models import ClubFundFreezeConfig
club_id = get_user_club_id(user)
data.update(build_tixian_asset_rates(club_id))
display_reason = ''
try:
if club_id:
prefer = ClubFundFreezeConfig.query.filter(
club_id=club_id, role='dashou', enabled=True,
).first()
if not prefer:
prefer = ClubFundFreezeConfig.query.filter(
club_id=club_id, enabled=True,
).exclude(remark='').first()
if prefer:
display_reason = prefer.remark or ''
except Exception:
pass
data['freeze_display_reason'] = display_reason or (
'部分佣金暂存为冻结保障金,用于订单售后与纠纷保障;'
'按规则解冻后自动转入可提现,冻结中不可提现。'
)
return Response({
'code': 200,
'msg': '获取成功',