feat: 打手押金冻结规则独立于佣金(dashou_yajin)
This commit is contained in:
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
@@ -485,7 +485,8 @@ class ClubOrderGrabLink(QModel):
|
||||
class ClubFundFreezeConfig(QModel):
|
||||
"""俱乐部×角色冻结规则。enabled=False 时入账行为与现网完全一致。"""
|
||||
ROLE_CHOICES = (
|
||||
('dashou', '打手'),
|
||||
('dashou', '打手佣金'),
|
||||
('dashou_yajin', '打手押金'),
|
||||
('guanshi', '管事'),
|
||||
('zuzhang', '组长'),
|
||||
('shenheguan', '考核官'),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in new issue
Block a user