实现提现资金冻结:手动/自动打款共用冻结拆分,驳回仅退 shenqing_jine。

新增公共/用户冻结配置、流水表与后台解冻接口;修复冻结与扣款重复扣余额问题。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-10 04:06:08 +08:00
parent 146a64b710
commit 8678f54459
13 changed files with 1447 additions and 20 deletions

View 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 个确认点,回复「对/不对」或补充后,我再按此文档写代码。**