Files
along_django/yonghu/freeze_service.py
XingQue c3bab3564a 加固管理员解冻:校验落库并返回余额/冻结池变动明细。
解冻接口返回 unfreeze_result,便于后台核对池→余额是否真正写回。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 13:15:10 +08:00

455 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
提现余额冻结服务 — 手动/自动打款申请共用。
规则摘要:
- 比例模式:按当前余额切出 freeze_jine 入池,申请金额 apply_jine 为实际提现扣款部分(另扣手续费)。
- 固定模式:冻结池未满则优先填池;申请+池仍不足目标则整笔入池freeze_only
- 驳回:仅退 shenqing_jine 到余额,冻结池不回滚。
"""
from __future__ import annotations
import decimal
import logging
from dataclasses import dataclass
from typing import Optional, Tuple
from django.db import transaction
from peizhi.models import WithdrawConfig
from .models import (
TixianDongjieGonggongPeizhi,
TixianDongjieJilu,
UserDashou,
UserGuanshi,
UserMain,
UserShangjia,
)
logger = logging.getLogger('yonghu.freeze')
DONGJIE_MODE_RATIO = 1
DONGJIE_MODE_FIXED = 2
# 支持冻结的提现类型
FREEZE_LEIXING_SET = frozenset({1, 2, 5, 6})
LEIXING_META = {
1: {
'model': UserDashou,
'balance': 'yue',
'pool': 'dongjie_chi',
'solo_enabled': 'dandu_dongjie_enabled',
'mode': 'dongjie_mode',
'bili': 'dongjie_bili',
'guding': 'dongjie_guding_jine',
'role_name': '打手佣金',
},
5: {
'model': UserDashou,
'balance': 'yajin',
'pool': 'dongjie_chi_yajin',
'solo_enabled': 'dandu_dongjie_yajin_enabled',
'mode': 'dongjie_yajin_mode',
'bili': 'dongjie_yajin_bili',
'guding': 'dongjie_yajin_guding_jine',
'role_name': '打手押金',
},
2: {
'model': UserGuanshi,
'balance': 'yue',
'pool': 'dongjie_chi',
'solo_enabled': 'dandu_dongjie_enabled',
'mode': 'dongjie_mode',
'bili': 'dongjie_bili',
'guding': 'dongjie_guding_jine',
'role_name': '管事分红',
},
6: {
'model': UserShangjia,
'balance': 'yue',
'pool': 'dongjie_chi',
'solo_enabled': 'dandu_dongjie_enabled',
'mode': 'dongjie_mode',
'bili': 'dongjie_bili',
'guding': 'dongjie_guding_jine',
'role_name': '商家余额',
},
}
@dataclass
class FreezeConfig:
mode: int
bili: decimal.Decimal
guding_jine: decimal.Decimal
source: str # 'user' | 'public'
@dataclass
class FreezeSplitResult:
withdraw_jine: decimal.Decimal
freeze_jine: decimal.Decimal
freeze_only: bool
message: str = ''
@dataclass
class ApplyFreezeOutcome:
freeze_only: bool
withdraw_jine: decimal.Decimal
benchi_dongjie_jine: decimal.Decimal
message: str = ''
pool_after: decimal.Decimal = decimal.Decimal('0')
balance_after: decimal.Decimal = decimal.Decimal('0')
def _q(value) -> decimal.Decimal:
return decimal.Decimal(str(value or 0)).quantize(
decimal.Decimal('0.01'), rounding=decimal.ROUND_HALF_UP,
)
def is_freeze_leixing(leixing: int) -> bool:
return int(leixing) in FREEZE_LEIXING_SET
def get_leixing_meta(leixing: int) -> dict:
meta = LEIXING_META.get(int(leixing))
if not meta:
raise ValueError(f'不支持的冻结提现类型: {leixing}')
return meta
def _build_config(mode, bili, guding, source) -> FreezeConfig:
return FreezeConfig(
mode=int(mode or DONGJIE_MODE_RATIO),
bili=_q(bili),
guding_jine=_q(guding),
source=source,
)
def resolve_freeze_config(profile, leixing: int) -> Optional[FreezeConfig]:
"""返回生效冻结配置None 表示本笔不冻结。"""
leixing = int(leixing)
if leixing not in FREEZE_LEIXING_SET:
return None
meta = get_leixing_meta(leixing)
if getattr(profile, meta['solo_enabled'], False):
return _build_config(
getattr(profile, meta['mode']),
getattr(profile, meta['bili']),
getattr(profile, meta['guding']),
'user',
)
wc = WithdrawConfig.objects.filter(id=1).only('dongjie_quanju_enabled').first()
if not wc or not wc.dongjie_quanju_enabled:
return None
pub = TixianDongjieGonggongPeizhi.objects.filter(leixing=leixing).first()
if not pub or not pub.role_enabled:
return None
return _build_config(pub.dongjie_mode, pub.dongjie_bili, pub.dongjie_guding_jine, 'public')
def compute_freeze_split(
balance: decimal.Decimal,
pool: decimal.Decimal,
config: Optional[FreezeConfig],
apply_jine: decimal.Decimal,
) -> FreezeSplitResult:
"""纯计算:不修改数据库。"""
balance = _q(balance)
pool = _q(pool)
apply_jine = _q(apply_jine)
if apply_jine <= 0:
raise ValueError('提现金额必须大于0')
if apply_jine > balance:
raise ValueError(f'余额不足,当前余额: {balance}')
if config is None:
return FreezeSplitResult(apply_jine, decimal.Decimal('0'), False)
if config.mode == DONGJIE_MODE_RATIO:
if config.bili <= 0:
return FreezeSplitResult(apply_jine, decimal.Decimal('0'), False)
if config.bili >= 1:
return FreezeSplitResult(
decimal.Decimal('0'), apply_jine, True,
message=f'已冻结 {apply_jine} 元进入资金池比例100%',
)
freeze_jine = _q(balance * config.bili)
max_withdraw = balance - freeze_jine
if apply_jine > max_withdraw:
raise ValueError(
f'最多可提现 {max_withdraw} 元(需冻结 {freeze_jine} 元,占余额 {config.bili * 100:.2f}%'
)
return FreezeSplitResult(apply_jine, freeze_jine, False)
if config.mode == DONGJIE_MODE_FIXED:
target = _q(config.guding_jine)
if target <= 0:
return FreezeSplitResult(apply_jine, decimal.Decimal('0'), False)
if pool >= target:
return FreezeSplitResult(apply_jine, decimal.Decimal('0'), False)
if apply_jine + pool < target:
return FreezeSplitResult(
decimal.Decimal('0'), apply_jine, True,
message=f'已冻结 {apply_jine} 元,冻结池 {pool + apply_jine}/{target} 元,满额后可正常提现',
)
gap = target - pool
withdraw_jine = apply_jine - gap
if withdraw_jine <= 0:
return FreezeSplitResult(
decimal.Decimal('0'), apply_jine, True,
message=f'已冻结 {apply_jine} 元,冻结池 {pool + apply_jine}/{target}',
)
return FreezeSplitResult(withdraw_jine, gap, False)
return FreezeSplitResult(apply_jine, decimal.Decimal('0'), False)
def get_locked_profile(user_main: UserMain, leixing: int):
meta = get_leixing_meta(leixing)
Model = meta['model']
try:
return Model.objects.select_for_update().get(user=user_main), meta
except Model.DoesNotExist:
raise ValueError(f'用户{user_main.yonghuid}缺少{meta["role_name"]}身份账户')
def _write_freeze_log(yonghuid, leixing, jine, pool_before, pool_after, caozuo, operator_id='', beizhu=''):
TixianDongjieJilu.objects.create(
yonghuid=yonghuid,
leixing=leixing,
jine=_q(jine),
dongjie_chi_before=_q(pool_before),
dongjie_chi_after=_q(pool_after),
caozuo=caozuo,
operator_id=operator_id or '',
beizhu=beizhu or '',
)
def apply_balance_freeze(user_main: UserMain, leixing: int, apply_jine) -> ApplyFreezeOutcome:
"""
提现申请前执行冻结拆分(须在 transaction.atomic 内调用)。
- freeze_only=True仅入池不进入提现扣款流程
- 否则返回 withdraw_jine实际提现扣款额与 benchi_dongjie_jine
"""
leixing = int(leixing)
apply_jine = _q(apply_jine)
if not is_freeze_leixing(leixing):
return ApplyFreezeOutcome(False, apply_jine, decimal.Decimal('0'))
profile, meta = get_locked_profile(user_main, leixing)
balance = _q(getattr(profile, meta['balance']))
pool = _q(getattr(profile, meta['pool']))
config = resolve_freeze_config(profile, leixing)
split = compute_freeze_split(balance, pool, config, apply_jine)
# 仅将冻结部分划入冻结池;实际提现扣款由 deduct_balance 完成,避免重复扣款
freeze_deduct = split.freeze_jine
if freeze_deduct <= 0:
if split.freeze_only:
freeze_deduct = apply_jine
else:
return ApplyFreezeOutcome(
freeze_only=False,
withdraw_jine=split.withdraw_jine,
benchi_dongjie_jine=decimal.Decimal('0'),
pool_after=pool,
balance_after=balance,
)
if freeze_deduct > balance:
raise ValueError('余额不足,无法完成冻结拆分')
if not split.freeze_only and freeze_deduct + split.withdraw_jine > balance:
raise ValueError('余额不足,无法完成冻结与提现拆分')
new_balance = balance - freeze_deduct
new_pool = pool + freeze_deduct
setattr(profile, meta['balance'], new_balance)
setattr(profile, meta['pool'], new_pool)
update_fields = [meta['balance'], meta['pool']]
if hasattr(profile, 'update_time'):
update_fields.append('update_time')
profile.save(update_fields=update_fields)
if split.freeze_jine > 0:
_write_freeze_log(
user_main.yonghuid, leixing, freeze_deduct, pool, new_pool,
TixianDongjieJilu.CAOZUO_APPLY,
)
logger.info(
'提现申请冻结 yonghuid=%s leixing=%s apply=%s freeze=%s withdraw=%s pool=%s balance=%s',
user_main.yonghuid, leixing, apply_jine, freeze_deduct,
split.withdraw_jine, new_pool, new_balance,
)
if split.freeze_only:
return ApplyFreezeOutcome(
freeze_only=True,
withdraw_jine=decimal.Decimal('0'),
benchi_dongjie_jine=freeze_deduct,
message=split.message or f'已冻结 {freeze_deduct} 元进入资金池',
pool_after=new_pool,
balance_after=new_balance,
)
return ApplyFreezeOutcome(
freeze_only=False,
withdraw_jine=split.withdraw_jine,
benchi_dongjie_jine=freeze_deduct,
pool_after=new_pool,
balance_after=new_balance,
)
def admin_unfreeze_balance(user_main: UserMain, leixing: int, jine, operator_id='', beizhu='') -> dict:
"""管理员解冻:冻结池 → 可提现余额。返回变动前后快照。"""
leixing = int(leixing)
jine = _q(jine)
if jine <= 0:
raise ValueError('解冻金额必须大于0')
profile, meta = get_locked_profile(user_main, leixing)
pool = _q(getattr(profile, meta['pool']))
if jine > pool:
raise ValueError(f'解冻金额不能超过冻结池余额 {pool}')
balance = _q(getattr(profile, meta['balance']))
new_pool = pool - jine
new_balance = balance + jine
setattr(profile, meta['pool'], new_pool)
setattr(profile, meta['balance'], new_balance)
update_fields = [meta['balance'], meta['pool']]
if hasattr(profile, 'update_time'):
update_fields.append('update_time')
profile.save(update_fields=update_fields)
profile.refresh_from_db()
actual_balance = _q(getattr(profile, meta['balance']))
actual_pool = _q(getattr(profile, meta['pool']))
if actual_balance != new_balance or actual_pool != new_pool:
logger.error(
'解冻落库不一致 yonghuid=%s leixing=%s expect_balance=%s actual=%s expect_pool=%s actual=%s',
user_main.yonghuid, leixing, new_balance, actual_balance, new_pool, actual_pool,
)
raise ValueError('解冻写入失败,请重试或联系技术')
_write_freeze_log(
user_main.yonghuid, leixing, jine, pool, actual_pool,
TixianDongjieJilu.CAOZUO_UNFREEZE, operator_id=operator_id, beizhu=beizhu,
)
logger.info(
'管理员解冻 yonghuid=%s leixing=%s jine=%s balance %s%s pool %s%s operator=%s',
user_main.yonghuid, leixing, jine, balance, actual_balance, pool, actual_pool, operator_id,
)
return {
'balance_before': balance,
'balance_after': actual_balance,
'dongjie_chi_before': pool,
'dongjie_chi_after': actual_pool,
'unfreeze_jine': jine,
}
def get_user_freeze_info(user_main: UserMain, leixing: int) -> dict:
"""后台/详情页展示用。"""
leixing = int(leixing)
meta = get_leixing_meta(leixing)
Model = meta['model']
try:
profile = Model.objects.get(user=user_main)
except Model.DoesNotExist:
return {'exists': False}
balance = _q(getattr(profile, meta['balance']))
pool = _q(getattr(profile, meta['pool']))
config = resolve_freeze_config(profile, leixing)
max_apply = balance
if config and config.mode == DONGJIE_MODE_RATIO and config.bili > 0 and config.bili < 1:
freeze_part = _q(balance * config.bili)
max_apply = balance - freeze_part
return {
'exists': True,
'leixing': leixing,
'role_name': meta['role_name'],
'balance': str(balance),
'dongjie_chi': str(pool),
'dandu_dongjie_enabled': bool(getattr(profile, meta['solo_enabled'], False)),
'dongjie_mode': int(getattr(profile, meta['mode'], DONGJIE_MODE_RATIO)),
'dongjie_bili': str(_q(getattr(profile, meta['bili'], 0))),
'dongjie_guding_jine': str(_q(getattr(profile, meta['guding'], 0))),
'max_apply_jine': str(max_apply),
'freeze_active': config is not None,
'freeze_source': config.source if config else '',
}
def get_public_freeze_settings() -> dict:
wc = WithdrawConfig.objects.filter(id=1).first()
quanju = bool(wc.dongjie_quanju_enabled) if wc else False
roles = []
for leixing in (1, 2, 5, 6):
pub = TixianDongjieGonggongPeizhi.objects.filter(leixing=leixing).first()
meta = get_leixing_meta(leixing)
roles.append({
'leixing': leixing,
'role_name': meta['role_name'],
'role_enabled': bool(pub.role_enabled) if pub else False,
'dongjie_mode': int(pub.dongjie_mode) if pub else DONGJIE_MODE_RATIO,
'dongjie_bili': str(_q(pub.dongjie_bili)) if pub else '0.00',
'dongjie_guding_jine': str(_q(pub.dongjie_guding_jine)) if pub else '0.00',
})
return {'dongjie_quanju_enabled': quanju, 'dongjie_roles': roles}
def save_public_freeze_settings(quanju_enabled: bool, roles: list) -> None:
WithdrawConfig.objects.update_or_create(id=1, defaults={'dongjie_quanju_enabled': bool(quanju_enabled)})
for row in roles:
leixing = int(row['leixing'])
if leixing not in FREEZE_LEIXING_SET:
continue
TixianDongjieGonggongPeizhi.objects.update_or_create(
leixing=leixing,
defaults={
'role_enabled': bool(row.get('role_enabled', False)),
'dongjie_mode': int(row.get('dongjie_mode', DONGJIE_MODE_RATIO)),
'dongjie_bili': _q(row.get('dongjie_bili', 0)),
'dongjie_guding_jine': _q(row.get('dongjie_guding_jine', 0)),
},
)
def save_user_freeze_settings(user_main: UserMain, leixing: int, data: dict) -> dict:
leixing = int(leixing)
profile, meta = get_locked_profile(user_main, leixing)
if 'dandu_dongjie_enabled' in data:
setattr(profile, meta['solo_enabled'], bool(data['dandu_dongjie_enabled']))
if 'dongjie_mode' in data:
setattr(profile, meta['mode'], int(data['dongjie_mode']))
if 'dongjie_bili' in data:
setattr(profile, meta['bili'], _q(data['dongjie_bili']))
if 'dongjie_guding_jine' in data:
setattr(profile, meta['guding'], _q(data['dongjie_guding_jine']))
update_fields = []
for key in (meta['solo_enabled'], meta['mode'], meta['bili'], meta['guding']):
if key in [f.name for f in profile._meta.fields]:
update_fields.append(key)
if hasattr(profile, 'update_time'):
update_fields.append('update_time')
profile.save(update_fields=update_fields)
return get_user_freeze_info(user_main, leixing)