实现提现资金冻结:手动/自动打款共用冻结拆分,驳回仅退 shenqing_jine。
新增公共/用户冻结配置、流水表与后台解冻接口;修复冻结与扣款重复扣余额问题。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
381
docs/资金冻结功能设计方案.md
Normal file
381
docs/资金冻结功能设计方案.md
Normal file
@@ -0,0 +1,381 @@
|
||||
# 资金冻结功能 — 设计方案(待确认)
|
||||
|
||||
> 本文档仅描述思路与边界,**确认后再写代码**。
|
||||
> 目标:打手 / 管事 / 商家三类角色的可提现余额,在**自动打款申请**时按配置冻结一部分资金,进入「冻结资金池」,防止重复提现;后台可配置、可单独覆盖、可解冻。
|
||||
|
||||
---
|
||||
|
||||
## 一、需求理解(我的归纳)
|
||||
|
||||
| 要点 | 理解 |
|
||||
|------|------|
|
||||
| 冻结对象 | 打手佣金余额(`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` | |
|
||||
|
||||
便于审计,避免资金纠纷。
|
||||
|
||||
---
|
||||
|
||||
## 四、配置解析规则
|
||||
|
||||
```python
|
||||
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`
|
||||
|
||||
**步骤:**
|
||||
|
||||
1. `freeze_from_balance = quantize(B * R)`
|
||||
2. `max_withdraw = B - freeze_from_balance`
|
||||
3. 若 `A > max_withdraw` → **拒绝**:`最多可提现 {max_withdraw} 元({R*100}% 已冻结)`
|
||||
4. 若 `freeze_from_balance > 0`:
|
||||
- `balance -= freeze_from_balance`
|
||||
- `dongjie_chi += freeze_from_balance`
|
||||
5. 对金额 `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_chi`
|
||||
- `T` = 配置的固定冻结目标
|
||||
- `A` = 用户申请金额
|
||||
- `B` = 当前余额(须 `A <= B`)
|
||||
|
||||
**步骤:**
|
||||
|
||||
1. 若 `P >= T` → **不冻结**,`withdraw_jine = A`,走原逻辑
|
||||
2. 若 `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 - fill`
|
||||
- `balance -= 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`(三个详情页共用):
|
||||
|
||||
```json
|
||||
{
|
||||
"yonghuid": "0000001",
|
||||
"leixing": 1,
|
||||
"jine": "50.00",
|
||||
"beizhu": "人工解冻"
|
||||
}
|
||||
```
|
||||
|
||||
校验:
|
||||
|
||||
- `0 < jine <= dongjie_chi`
|
||||
- `balance += jine`,`dongjie_chi -= jine`
|
||||
- 写解冻流水
|
||||
|
||||
---
|
||||
|
||||
## 七、接口设计
|
||||
|
||||
### 7.1 公共配置(提现设置页)
|
||||
|
||||
| 方法 | 路径 | 权限 |
|
||||
|------|------|------|
|
||||
| GET | `/houtai/hthqtxpz` 扩展返回冻结配置 | 现有提现设置权限 |
|
||||
| POST | `/houtai/htxgtxsz` 扩展保存冻结配置 | 同上 |
|
||||
|
||||
返回/保存结构示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"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` | 保存单独配置 / 解冻 |
|
||||
|
||||
请求体:
|
||||
|
||||
```json
|
||||
{
|
||||
"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` |
|
||||
|
||||
---
|
||||
|
||||
## 十、待你确认的问题
|
||||
|
||||
1. **一期范围**:是否只做 **打手/管事/商家 + 自动打款(zddksh)**?组长、审核官、押金、手动打款是否二期?
|
||||
2. **固定模式「全入池」**:申请金额不足以填满冻结池时,是否**不创建审核单**、仅入池(我按此设计)?
|
||||
3. **比例模式**:是否按上文「先按余额切冻结比例,再对剩余部分提现」?(避免余额残留可再提)
|
||||
4. **驳回**:是否同意「提现扣款 + 本次冻结」**全部回滚到可提现余额**?
|
||||
5. **公共配置**:全局一个总开关 + 每角色独立开关,是否符合你的预期?
|
||||
|
||||
---
|
||||
|
||||
## 十一、实施顺序(确认后)
|
||||
|
||||
1. 迁移 + `freeze_service` 单元测试(用例覆盖第五节所有表格)
|
||||
2. 接入 `zddksh` / 驳回回滚
|
||||
3. 后台 API + 提现设置页 UI
|
||||
4. 三角色详情页 UI + 解冻
|
||||
5. 小程序申请前提示(可选)
|
||||
|
||||
---
|
||||
|
||||
**请先看第十节 5 个确认点,回复「对/不对」或补充后,我再按此文档写代码。**
|
||||
Reference in New Issue
Block a user