13 KiB
资金冻结功能 — 设计方案(待确认)
本文档仅描述思路与边界,确认后再写代码。
目标:打手 / 管事 / 商家三类角色的可提现余额,在自动打款申请时按配置冻结一部分资金,进入「冻结资金池」,防止重复提现;后台可配置、可单独覆盖、可解冻。
一、需求理解(我的归纳)
| 要点 | 理解 |
|---|---|
| 冻结对象 | 打手佣金余额(user_dashou.yue)、管事余额(user_guanshi.yue)、商家余额(user_shangjia.yue) |
| 冻结资金去向 | 从「可提现余额」扣出 → 进入「冻结资金池」字段,永远不能通过提现提出 |
| 配置层级 | 公共配置(提现设置页) + 用户单独配置(各角色详情页,开启后覆盖公共) |
| 冻结方式 | 比例:按当前余额冻结 X%;固定金额:冻结池累计须达到 Y 元 |
| 生效范围(一期) | 仅自动打款申请 POST /yonghu/zddksh;收款 tixiansq 不再二次冻结 |
| 驳回退款 | 仅退回本次实际进入提现流程的金额;本次划入冻结池的金额一并回滚 |
| 后台解冻 | 管理员在角色详情页,可将冻结池部分/全部划回可提现余额 |
一期不做(除非你确认要加):
- 组长(
leixing=3)、审核官(leixing=4)、打手押金(leixing=5) 的冻结 - 手动打款
POST /yonghu/tixian的冻结(可二期复用同一套freeze_service)
二、现有提现流程(自动打款)
用户 POST /yonghu/zddksh { leixing, jine }
↓
create_audit_application()
├─ validate_withdraw_eligibility() # 校验余额 >= jine
├─ calc_fee_amounts(jine) # 手续费、实际到账
├─ deduct_balance(leixing, jine) # 从余额扣 jine(申请扣款额)
└─ 写 TixianShenheJilu + Tixianjilu
shenqing_jine = jine
jine(记录) = shijidaozhang(到账额)
审核通过 → 状态 6 待收款
用户 POST /yonghu/tixiansq 收款 → 微信打款(不再动余额)
驳回 → refund_balance(shenqing_jine) 退回余额
改造点:在 deduct_balance 之前插入「冻结拆分」逻辑。
三、表模型设计
3.1 公共冻结配置表 tixian_dongjie_gonggong_peizhi
每个角色一行(leixing 唯一):
| 字段 | 类型 | 说明 |
|---|---|---|
leixing |
SmallInt | 1打手佣金 2管事 6商家(唯一) |
role_enabled |
Boolean | 该角色是否启用公共冻结(默认 False) |
dongjie_mode |
SmallInt | 1=按比例 2=固定金额 |
dongjie_bili |
Decimal(5,4) | 比例,如 0.1000 = 10% |
dongjie_guding_jine |
Decimal(12,2) | 固定冻结目标金额 |
update_time |
DateTime |
3.2 全局总开关(挂在现有 withdraw_config 表)
| 字段 | 说明 |
|---|---|
dongjie_quanju_enabled |
全员冻结总开关,默认 False;仅当为 True 时,公共配置才可能生效 |
生效条件(公共):
dongjie_quanju_enabled == True且 该角色role_enabled == True
3.3 三个扩展表新增字段(结构相同)
user_dashou / user_guanshi / user_shangjia:
| 字段 | 类型 | 说明 |
|---|---|---|
dongjie_chi |
Decimal(12,2) | 冻结资金池(累计,默认 0) |
dandu_dongjie_enabled |
Boolean | 是否启用单独冻结配置(默认 False) |
dongjie_mode |
SmallInt | 1比例 / 2固定(仅单独配置用) |
dongjie_bili |
Decimal(5,4) | 单独比例 |
dongjie_guding_jine |
Decimal(12,2) | 单独固定金额 |
配置优先级:
dandu_dongjie_enabled == True→ 用扩展表上的 mode/bili/guding
否则 → 用公共表(且需全局+角色开关均开)
否则 → 不冻结
3.4 审核记录补充字段(驳回回滚用)
tixian_shenhe_jilu 新增:
| 字段 | 说明 |
|---|---|
benchi_dongjie_jine |
本次申请划入冻结池的金额(驳回时须回滚) |
tixianjilu 同步写入(与审核单一致,便于对账)。
3.5 解冻流水表 tixian_dongjie_jilu(建议)
| 字段 | 说明 |
|---|---|
yonghuid, leixing, jine, dongjie_chi_before, dongjie_chi_after |
|
caozuo |
1申请冻结 2驳回回滚 3管理员解冻 |
operator_id, beizhu, create_time |
便于审计,避免资金纠纷。
四、配置解析规则
def resolve_freeze_config(profile, leixing) -> FreezeConfig | None:
"""
返回生效的冻结配置;None 表示本笔不冻结。
"""
if profile.dandu_dongjie_enabled:
return FreezeConfig(mode=..., bili=..., guding=..., source='user')
global_on = WithdrawConfig.dongjie_quanju_enabled
if not global_on:
return None
pub = TixianDongjieGonggongPeizhi.objects.get(leixing=leixing)
if not pub.role_enabled:
return None
return FreezeConfig(mode=pub.dongjie_mode, ..., source='public')
前端展示规则(详情页):
- 选「按比例」→ 只展示比例输入框
- 选「固定金额」→ 只展示固定金额输入框
- 始终展示:
冻结资金池当前余额、是否启用单独配置开关
五、核心算法(自动打款申请时)
统一入口:compute_freeze_split(balance, pool, config, apply_jine) -> FreezeResult
所有金额 Decimal,保留 2 位,ROUND_HALF_UP。
5.1 无配置 / 未开启
freeze_jine = 0
withdraw_jine = apply_jine # 走原逻辑
5.2 按比例冻结(mode=1)
语义:按申请时刻的「可提现余额」先切出冻结部分,剩余部分才能进入提现扣款。
设:
B= 当前可提现余额(申请前)R= 冻结比例(如 0.10)A= 用户申请提现金额jine
步骤:
freeze_from_balance = quantize(B * R)max_withdraw = B - freeze_from_balance- 若
A > max_withdraw→ 拒绝:最多可提现 {max_withdraw} 元({R*100}% 已冻结) - 若
freeze_from_balance > 0:balance -= freeze_from_balancedongjie_chi += freeze_from_balance
- 对金额
A走原流程:deduct_balance(A)→ 手续费基于A计算
示例(10%):
| 余额 B | 申请 A | 划入冻结池 | 实际提现扣款 | 申请后余额 | 冻结池 |
|---|---|---|---|---|---|
| 100 | 90 | 10 | 90 | 0 | 10 |
| 100 | 50 | 10 | 50 | 40 | 10 |
| 100 | 100 | — | 拒绝(最多 90) | — | — |
关键:冻结部分一次性从余额切走,避免「余额还剩 10% 又能再提」的漏洞。
5.3 按固定金额冻结(mode=2)
语义:冻结池累计须达到目标 T;未满则优先用本次申请金额「填池」。
设:
P= 当前dongjie_chiT= 配置的固定冻结目标A= 用户申请金额B= 当前余额(须A <= B)
步骤:
- 若
P >= T→ 不冻结,withdraw_jine = A,走原逻辑 - 若
P < T:gap = T - P- 情况 A:
A + P < T(本次申请填池后仍未达标)- 整笔
A划入冻结池,不创建提现审核单(或创建「仅冻结」记录,状态特殊) balance -= A,dongjie_chi += A- 返回提示:
已冻结 {A} 元,冻结池 {P+A}/{T},满额后可正常提现
- 整笔
- 情况 B:
A + P >= T(本次可填满池并有余款提现)fill = gap(填池金额)withdraw_jine = A - fillbalance -= A(等价:先扣 A,其中 fill 入池、withdraw_jine 进入提现)dongjie_chi += fill- 对
withdraw_jine计算手续费、写审核单
示例(T=100):
| 池 P | 余额 B | 申请 A | 结果 |
|---|---|---|---|
| 30 | 50 | 50 | 50+30<100 → 全部入池,池=80,不提现 |
| 30 | 80 | 80 | 80+30≥100 → 填池70,提现10 |
| 100 | 50 | 50 | 池已满 → 正常提现50 |
5.4 手续费与记录字段(不弄乱)
仅对 withdraw_jine(实际进入提现流程的金额) 计算:
shouxufei = withdraw_jine * feilv
shijidaozhang = withdraw_jine - shouxufei
TixianShenheJilu.shenqing_jine = withdraw_jine # 申请扣款额
TixianShenheJilu.shijidaozhang = shijidaozhang # 到账额
TixianShenheJilu.benchi_dongjie_jine = freeze_jine # 本次入池
收款阶段 tixiansq: 只打 shijidaozhang,不再动余额/冻结池。
六、驳回 / 解冻
6.1 审核驳回(自动 + 手动均适用)
在现有 refund_balance(shenqing_jine) 基础上增加:
balance += shenqing_jine # 退回提现扣款
dongjie_chi -= benchi_dongjie_jine # 回滚本次冻结(若有)
balance += benchi_dongjie_jine # 冻结部分也回到可提现余额
须在同一 transaction.atomic + select_for_update 内完成。
6.2 管理员解冻
新接口 POST /houtai/.../dongjie_jiedong(三个详情页共用):
{
"yonghuid": "0000001",
"leixing": 1,
"jine": "50.00",
"beizhu": "人工解冻"
}
校验:
0 < jine <= dongjie_chibalance += jine,dongjie_chi -= jine- 写解冻流水
七、接口设计
7.1 公共配置(提现设置页)
| 方法 | 路径 | 权限 |
|---|---|---|
| GET | /houtai/hthqtxpz 扩展返回冻结配置 |
现有提现设置权限 |
| POST | /houtai/htxgtxsz 扩展保存冻结配置 |
同上 |
返回/保存结构示例:
{
"dongjie_quanju_enabled": false,
"dongjie_roles": [
{
"leixing": 1,
"role_name": "打手佣金",
"role_enabled": false,
"dongjie_mode": 1,
"dongjie_bili": "0.1000",
"dongjie_guding_jine": "0.00"
},
{ "leixing": 2, "...": "管事" },
{ "leixing": 6, "...": "商家" }
]
}
7.2 用户单独配置(三详情页共用)
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /houtai/hqdjdongjie?yonghuid=&leixing= |
查询 |
| POST | /houtai/xgdjdongjie |
保存单独配置 / 解冻 |
请求体:
{
"yonghuid": "0000001",
"leixing": 1,
"dandu_dongjie_enabled": true,
"dongjie_mode": 2,
"dongjie_guding_jine": "100.00",
"jiedong_jine": "30.00",
"beizhu": "可选,有值则执行解冻"
}
dongjie_mode=1 时只读/写 dongjie_bili;mode=2 时只读/写 dongjie_guding_jine。
7.3 用户端(小程序)
申请前可增加接口 GET /yonghu/tixian_dongjie_info?leixing= 返回:
- 是否启用冻结、模式、比例/固定额
- 当前余额、冻结池、最大可申请金额(前端展示用,后端申请时仍强校验)
八、代码改动范围(确认后)
| 模块 | 改动 |
|---|---|
peizhi/models.py |
WithdrawConfig 增加总开关 |
yonghu/models.py |
公共配置表 + 三扩展表字段 + 审核表 benchi_dongjie_jine + 流水表 |
yonghu/freeze_service.py |
新建 配置解析 + 冻结拆分算法(纯函数,便于单测) |
yonghu/tixian_shenhe_services.py |
create_audit_application 接入冻结;驳回增加回滚 |
houtai/view.py |
提现设置读写、单独配置/解冻接口 |
kefu/.../Settings.vue |
公共冻结配置 UI |
| 打手/管事/商家详情页 | 单独冻结 + 解冻 UI |
不动: tixiansq 收款逻辑(仅打款,不二次冻结)。
九、边界与防错
| 场景 | 处理 |
|---|---|
| 余额 0 | 拒绝申请 |
| 比例 100% | max_withdraw=0,拒绝任何提现申请 |
| 固定池已满 | 与无冻结相同,正常提现 |
| 仅冻结不入提现(固定模式情况A) | 不扣手续费、不写微信打款相关记录 |
| 并发申请 | select_for_update 锁扩展表余额行 |
| 驳回 | 同时回滚 shenqing_jine + benchi_dongjie_jine |
| 金额精度 | 全程 Decimal,禁止 float |
十、待你确认的问题
- 一期范围:是否只做 打手/管事/商家 + 自动打款(zddksh)?组长、审核官、押金、手动打款是否二期?
- 固定模式「全入池」:申请金额不足以填满冻结池时,是否不创建审核单、仅入池(我按此设计)?
- 比例模式:是否按上文「先按余额切冻结比例,再对剩余部分提现」?(避免余额残留可再提)
- 驳回:是否同意「提现扣款 + 本次冻结」全部回滚到可提现余额?
- 公共配置:全局一个总开关 + 每角色独立开关,是否符合你的预期?
十一、实施顺序(确认后)
- 迁移 +
freeze_service单元测试(用例覆盖第五节所有表格) - 接入
zddksh/ 驳回回滚 - 后台 API + 提现设置页 UI
- 三角色详情页 UI + 解冻
- 小程序申请前提示(可选)
请先看第十节 5 个确认点,回复「对/不对」或补充后,我再按此文档写代码。