feat: 打手押金冻结规则独立于佣金(dashou_yajin)

This commit is contained in:
XingQue committed 2026-08-12 08:30:48 +08:00
1 parent bc4e41f54a
commit 181bf3aaaa
4 files changed
+136 -18

No files matched your search

@@ -0,0 +1,27 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('jituan', '0029_club_wecom_kefu_config'),
]
operations = [
migrations.AlterField(
model_name='clubfundfreezeconfig',
name='role',
field=models.CharField(
choices=[
('dashou', '打手佣金'),
('dashou_yajin', '打手押金'),
('guanshi', '管事'),
('zuzhang', '组长'),
('shenheguan', '考核官'),
],
db_index=True,
max_length=16,
verbose_name='角色',
),
),
]
+2 -1
View File
@@ -485,7 +485,8 @@ class ClubOrderGrabLink(QModel):
class ClubFundFreezeConfig(QModel):
"""俱乐部×角色冻结规则。enabled=False 时入账行为与现网完全一致。"""
ROLE_CHOICES = (
('dashou', '打手'),
('dashou', '打手佣金'),
('dashou_yajin', '打手押金'),
('guanshi', '管事'),
('zuzhang', '组长'),
('shenheguan', '考核官'),
+20 -12
View File
@@ -22,6 +22,7 @@ from jituan.models import ClubFundFreezeConfig, FundFreezeLedger
logger = logging.getLogger(__name__)
ROLE_DASHOU = 'dashou'
ROLE_DASHOU_YAJIN = 'dashou_yajin' # 仅配置维度:押金独立冻结规则;ledger.role 仍写 dashou
ROLE_GUANSHI = 'guanshi'
ROLE_ZUZHANG = 'zuzhang'
ROLE_SHENHEGUAN = 'shenheguan'
@@ -287,6 +288,7 @@ def credit_role_balance(
also_credit_zonge: bool = False,
ignore_source_scope: bool = False,
credit_target: str = CREDIT_TARGET_YUE,
config_role: Optional[str] = None,
) -> Dict[str, Any]:
"""
统一入账。返回 {available, frozen, ledger_id, skipped_freeze}。
@@ -295,8 +297,10 @@ def credit_role_balance(
profile: 已加载的角色扩展实例(UserDashou 等);若空则按 role+user_id 查。
ignore_source_scope: True 时跳过 source_scope 检查(仍受 enabled / 金额规则约束)。
credit_target: yue=可提现佣金(默认);yajin=押金可提部分(冻结进同一 dongjie_yue)。
config_role: 查冻结配置用的角色键;默认=role。押金请传 dashou_yajin,ledger.role 仍写 dashou。
"""
role = (role or '').strip().lower()
cfg_role = (config_role or role).strip().lower() or role
cid = _club(club_id)
credit = _money(amount)
biz_id = (biz_id or '').strip()
@@ -315,8 +319,8 @@ def credit_role_balance(
if profile is None:
raise ValueError(f'找不到角色资料 role={role} user_id={user_id}')
cfg = get_freeze_config(cid, role)
source_ok = True if ignore_source_scope else _source_allowed(cfg, source_type, role=role) if cfg else False
cfg = get_freeze_config(cid, cfg_role)
source_ok = True if ignore_source_scope else _source_allowed(cfg, source_type, role=cfg_role) if cfg else False
freeze_on = bool(cfg and cfg.enabled and source_ok and biz_id)
skip_reason = ''
if not freeze_on:
@@ -331,8 +335,8 @@ def credit_role_balance(
else:
skip_reason = 'unknown'
logger.info(
'fund_freeze skip club=%s role=%s source=%s biz=%s user=%s amount=%s reason=%s',
cid, role, source_type, biz_id, user_id, credit, skip_reason,
'fund_freeze skip club=%s role=%s cfg_role=%s source=%s biz=%s user=%s amount=%s reason=%s',
cid, role, cfg_role, source_type, biz_id, user_id, credit, skip_reason,
)
frozen = calc_freeze_amount(credit, cfg) if freeze_on else Decimal('0.00')
@@ -340,8 +344,8 @@ def credit_role_balance(
if freeze_on and frozen <= 0:
skip_reason = 'calc_zero'
logger.info(
'fund_freeze calc_zero club=%s role=%s source=%s biz=%s amount=%s mode=%s rate=%s',
cid, role, source_type, biz_id, credit,
'fund_freeze calc_zero club=%s role=%s cfg_role=%s source=%s biz=%s amount=%s mode=%s rate=%s',
cid, role, cfg_role, source_type, biz_id, credit,
getattr(cfg, 'freeze_mode', None), getattr(cfg, 'freeze_rate', None),
)
@@ -374,6 +378,7 @@ def credit_role_balance(
next_at = timezone.now() + timedelta(days=delay) if delay else timezone.now()
snap = _config_snapshot(cfg)
snap['credit_target'] = credit_target
snap['config_role'] = cfg_role
ledger = FundFreezeLedger.query.create(
club_id=cid,
user_id=str(user_id),
@@ -396,9 +401,10 @@ def credit_role_balance(
)
ledger_id = ledger.id
logger.info(
'fund_freeze applied club=%s role=%s source=%s target=%s biz=%s user=%s '
'fund_freeze applied club=%s role=%s cfg_role=%s source=%s target=%s biz=%s user=%s '
'credit=%s available=%s frozen=%s ledger=%s',
cid, role, source_type, credit_target, biz_id, user_id, credit, available, frozen, ledger_id,
cid, role, cfg_role, source_type, credit_target, biz_id, user_id,
credit, available, frozen, ledger_id,
)
return {
@@ -409,6 +415,7 @@ def credit_role_balance(
'skip_reason': skip_reason or None,
'club_id': cid,
'credit_target': credit_target,
'config_role': cfg_role,
}
@@ -607,11 +614,11 @@ def credit_dashou_yajin(
freeze_reason: str = '',
) -> Dict[str, Any]:
"""
打手押金入账网关。
打手押金入账网关(独立配置键 dashou_yajin,与佣金冻结规则分离)。
- 未开启冻结 / 未勾选押金来源:全额进 yajin(与现网一致)
- 开启且勾选:按规则拆「可提押金 yajin + 冻结池 dongjie_yue」,ledger.source=yajin_*
- 解冻时回 yajin,绝不进佣金 yue
- 未开启押金冻结 / 未勾选押金来源:全额进 yajin(与现网一致)
- 开启且勾选:按「打手押金」规则拆「可提押金 yajin + 冻结池 dongjie_yue」
- 解冻时回 yajin,绝不进佣金 yue;有效押金仍可抢单
调用方须在 transaction.atomic() 内。
"""
uid = str(user_id or '').strip()
@@ -631,6 +638,7 @@ def credit_dashou_yajin(
freeze_reason=freeze_reason or '押金充值冻结',
also_credit_zonge=False,
credit_target=CREDIT_TARGET_YAJIN,
config_role=ROLE_DASHOU_YAJIN,
)
+87 -5
View File
@@ -12,21 +12,29 @@ from jituan.models import ClubFundFreezeConfig, FundFreezeLedger, FundUnfreezeEv
from jituan.services.club_context import filter_user_related_by_club, resolve_club_id_from_request
from jituan.services.fund_freeze import (
ROLE_DASHOU,
ROLE_DASHOU_YAJIN,
ROLE_GUANSHI,
ROLE_SHENHEGUAN,
ROLE_ZUZHANG,
SOURCE_YAJIN_EXCHANGE,
SOURCE_YAJIN_RECHARGE,
YAJIN_SOURCE_TYPES,
apply_unfreeze_step,
invalidate_freeze_config_cache,
split_preview,
)
ROLES = (
(ROLE_DASHOU, '打手'),
(ROLE_DASHOU, '打手佣金'),
(ROLE_DASHOU_YAJIN, '打手押金'),
(ROLE_GUANSHI, '管事'),
(ROLE_ZUZHANG, '组长'),
(ROLE_SHENHEGUAN, '考核官'),
)
YAJIN_DEFAULT_SCOPE = [SOURCE_YAJIN_RECHARGE, SOURCE_YAJIN_EXCHANGE]
COMMISSION_DEFAULT_SCOPE = ['order_settle', 'fenhong', 'withdraw_reject', 'club_migrate']
DEFAULT_INSTALLMENT = {
'steps': [
{'seq': 1, 'after_days': 7, 'type': 'ratio', 'value': 0.3, 'need_condition': False},
@@ -48,6 +56,9 @@ def _dec(v, default='0') -> str:
def _cfg_to_dict(row: Optional[ClubFundFreezeConfig], club_id: str, role: str) -> Dict[str, Any]:
default_scope = (
list(YAJIN_DEFAULT_SCOPE) if role == ROLE_DASHOU_YAJIN else ['order_settle']
)
if not row:
return {
'club_id': club_id,
@@ -58,7 +69,7 @@ def _cfg_to_dict(row: Optional[ClubFundFreezeConfig], club_id: str, role: str) -
'freeze_fixed': '0',
'freeze_cap': '0',
'min_credit_to_freeze': '0',
'source_scope': ['order_settle'],
'source_scope': default_scope,
'unfreeze_trigger': 'time_only',
'schedule_mode': 'all_at_once',
'installment_json': {'steps': [{'seq': 1, 'after_days': 15, 'type': 'remain', 'value': 1, 'need_condition': False}]},
@@ -74,6 +85,7 @@ def _cfg_to_dict(row: Optional[ClubFundFreezeConfig], club_id: str, role: str) -
'remark': '',
'display_reason': '',
'exists': False,
'is_yajin_config': role == ROLE_DASHOU_YAJIN,
}
return {
'club_id': row.club_id,
@@ -94,20 +106,69 @@ def _cfg_to_dict(row: Optional[ClubFundFreezeConfig], club_id: str, role: str) -
'display_reason': row.remark or '',
'updated_by': row.updated_by or '',
'exists': True,
'is_yajin_config': role == ROLE_DASHOU_YAJIN,
}
def _migrate_legacy_yajin_scope(club_id: str, rows: Dict[str, ClubFundFreezeConfig]) -> None:
"""
一次性兼容:旧「打手」配置里勾了押金来源时,拆到独立 dashou_yajin 行,
并从打手佣金 scope 去掉押金项,避免双轨。
"""
dashou = rows.get(ROLE_DASHOU)
if not dashou:
return
if rows.get(ROLE_DASHOU_YAJIN):
return
scope = list(dashou.source_scope or [])
yajin_items = [s for s in scope if s in YAJIN_SOURCE_TYPES]
if not yajin_items:
return
new_row = ClubFundFreezeConfig(
club_id=club_id,
role=ROLE_DASHOU_YAJIN,
enabled=bool(dashou.enabled),
freeze_mode=dashou.freeze_mode,
freeze_rate=dashou.freeze_rate,
freeze_fixed=dashou.freeze_fixed,
freeze_cap=dashou.freeze_cap,
min_credit_to_freeze=dashou.min_credit_to_freeze,
source_scope=yajin_items,
unfreeze_trigger=dashou.unfreeze_trigger,
schedule_mode=dashou.schedule_mode,
installment_json=dashou.installment_json or {},
condition_json=dashou.condition_json or {},
freeze_min_days=dashou.freeze_min_days or 0,
remark=(dashou.remark or '')[:255],
updated_by='migrate_yajin_split',
)
new_row.save()
dashou.source_scope = [s for s in scope if s not in YAJIN_SOURCE_TYPES]
dashou.save(update_fields=['source_scope', 'UpdateTime'])
invalidate_freeze_config_cache(club_id, ROLE_DASHOU)
invalidate_freeze_config_cache(club_id, ROLE_DASHOU_YAJIN)
rows[ROLE_DASHOU_YAJIN] = new_row
def build_config_payload(request) -> Dict[str, Any]:
club_id = _club(request)
rows = {
r.role: r
for r in ClubFundFreezeConfig.query.filter(club_id=club_id)
}
try:
_migrate_legacy_yajin_scope(club_id, rows)
except Exception:
# 不阻断后台读取
pass
items = []
for role, name in ROLES:
d = _cfg_to_dict(rows.get(role), club_id, role)
d['role_name'] = name
avail, frozen = split_preview(club_id, role, Decimal('100'), 'order_settle')
preview_src = (
SOURCE_YAJIN_RECHARGE if role == ROLE_DASHOU_YAJIN else 'order_settle'
)
avail, frozen = split_preview(club_id, role, Decimal('100'), preview_src)
d['preview_100'] = {'available': str(avail), 'frozen': str(frozen)}
items.append(d)
return {'club_id': club_id, 'roles': items}
@@ -137,13 +198,34 @@ def save_config(request, data: dict, operator: str = '') -> Tuple[Optional[dict]
source_scope = data.get('source_scope')
if source_scope is None:
# 默认同时覆盖订单结算与分红(管事/组长主要靠分红入账)
source_scope = ['order_settle', 'fenhong', 'withdraw_reject', 'club_migrate']
if role == ROLE_DASHOU_YAJIN:
source_scope = list(YAJIN_DEFAULT_SCOPE)
else:
# 默认同时覆盖订单结算与分红(管事/组长主要靠分红入账)
source_scope = list(COMMISSION_DEFAULT_SCOPE)
if isinstance(source_scope, str):
try:
source_scope = json.loads(source_scope)
except Exception:
source_scope = [source_scope]
if not isinstance(source_scope, list):
return None, 'source_scope 格式错误'
source_scope = [str(s).strip() for s in source_scope if str(s).strip()]
# 押金配置与佣金配置严格分离:互不污染 source_scope
if role == ROLE_DASHOU_YAJIN:
source_scope = [s for s in source_scope if s in YAJIN_SOURCE_TYPES]
if not source_scope:
source_scope = list(YAJIN_DEFAULT_SCOPE)
elif role == ROLE_DASHOU:
source_scope = [s for s in source_scope if s not in YAJIN_SOURCE_TYPES]
if not source_scope:
return None, '打手佣金请至少选择一个非押金入账来源(押金请到「打手押金」页配置)'
else:
# 其他角色也不允许勾押金来源
source_scope = [s for s in source_scope if s not in YAJIN_SOURCE_TYPES]
if not source_scope:
return None, '请至少选择一个入账来源'
unfreeze_trigger = (data.get('unfreeze_trigger') or 'time_only').strip()
if unfreeze_trigger not in (