228 lines
8.2 KiB
Markdown
228 lines
8.2 KiB
Markdown
# 采集协议单 · 缴费模式切换设计
|
||
|
||
**日期**:2026-06-04
|
||
**范围**:`web-admin` 采集协议单新增/编辑弹窗(`ProtocolFormDialog.vue`)及后端缴费主表/明细保存
|
||
**状态**:已实现(P0)
|
||
|
||
---
|
||
|
||
## 1. 背景
|
||
|
||
采集协议单缴费主表 `pay_mode` 有两种取值:
|
||
|
||
| 值 | 含义 | 明细约定 |
|
||
|----|------|----------|
|
||
| `dj` | 趸交(一次性) | **1 条**有效明细 |
|
||
| `fq` | 分期 | **N 条**明细,N = `installment_count` |
|
||
|
||
用户在**编辑已保存协议**时可能切换缴费模式。当前实现仅做字段联动(金额同步、日期重算),**不区分明细缴费状态**,会导致:
|
||
|
||
- 分期改趸交后仍保留多条明细且每条金额 = 合同全额;
|
||
- 已缴明细被重算应缴日/期次;
|
||
- 趸交已缴后仍可改分期,账实不符。
|
||
|
||
本设计定义切换规则与实现边界。
|
||
|
||
---
|
||
|
||
## 2. 决策记录
|
||
|
||
| 议题 | 决定 |
|
||
|------|------|
|
||
| 小数月间隔换算 | 30 天/月(已实现,见 `protocol-payment-installment.ts`) |
|
||
| 第 2 期起提前天数 | 可配置,默认 **30** 天(见 [2026-06-10-protocol-installment-config-design.md](./2026-06-10-protocol-installment-config-design.md)) |
|
||
| 分期部分已缴后改趸交,未缴期次 | **A:作废**(软删/不再展示,不进分期预警) |
|
||
| 已缴/确认中明细 | **保留**,只读,不参与重算 |
|
||
| 趸交已缴后改分期 | **禁止** |
|
||
| 趸交确认中改分期 | **禁止**(提示先完成或撤回财务确认) |
|
||
|
||
---
|
||
|
||
## 3. 明细状态与「锁定」定义
|
||
|
||
| pay_status | 含义 | 切换时 |
|
||
|------------|------|--------|
|
||
| `dfj` | 待缴费 | 可作废、可重建 |
|
||
| `jfqrz` | 缴费确认中 | **锁定**(视为已占用,禁止模式切换) |
|
||
| `yjf` | 已缴费 | **锁定**(保留,只读) |
|
||
| `tfqrz` | 退费确认中 | **锁定**(禁止切换) |
|
||
| `ytf` | 已退费 | **锁定**(禁止切换) |
|
||
|
||
**剩余应缴金额**:
|
||
|
||
```
|
||
remaining = contractAmount − Σ(dueAmount WHERE pay_status IN ('yjf'))
|
||
```
|
||
|
||
`jfqrz` 阶段切换一律阻断;若业务后续允许「确认中算占用」,再单独开变更。
|
||
|
||
---
|
||
|
||
## 4. 场景规则
|
||
|
||
### 4.1 趸交 → 分期(dj → fq)
|
||
|
||
| 前置条件 | 处理 |
|
||
|----------|------|
|
||
| 无明细或全部 `dfj` | **允许**。作废全部现有明细;按分期参数生成 N 条新 `dfj` 明细;主表 `pay_mode=fq`。 |
|
||
| 存在 `jfqrz` / `yjf` / `tfqrz` / `ytf` | **禁止**。提示原因。 |
|
||
|
||
切换后:第 i 期 `installmentNo=i`,应缴日按 `calcFqInstallmentDueDate`,应缴金额按合同均分(末期待尾差)。
|
||
|
||
### 4.2 分期 → 趸交(fq → dj),一期未缴
|
||
|
||
| 前置条件 | 处理 |
|
||
|----------|------|
|
||
| 全部 `dfj` | **允许**。作废全部 N 条分期明细;**新建 1 条**趸交明细:`dueAmount=contractAmount`,`installmentNo=null`,应缴日=`lumpSumDeadline`。 |
|
||
| 存在任何非 `dfj` | 走 4.3 |
|
||
|
||
### 4.3 分期 → 趸交(fq → dj),部分已缴
|
||
|
||
| 前置条件 | 处理 |
|
||
|----------|------|
|
||
| 有 `yjf`,无 `jfqrz`/`tfqrz`/`ytf` | **允许**。 |
|
||
| 存在 `jfqrz`/`tfqrz`/`ytf` | **禁止**。 |
|
||
|
||
**允许时步骤**:
|
||
|
||
1. **保留**所有 `yjf` 明细(只读展示,保留原 `installmentNo`、转账信息)。
|
||
2. **作废**所有 `dfj` 分期明细(不进入表单列表;保存时 `del_flag=1`)。
|
||
3. **新建 1 条**趸交明细:`dueAmount=remaining`,`payStatus=dfj`,`installmentNo=null`,应缴日=`lumpSumDeadline`。
|
||
4. 主表 `pay_mode=dj`;分期参数字段可保留只读快照(便于查看历史计划)。
|
||
|
||
若 `remaining ≤ 0`:禁止切换,提示「已缴清,无需改趸交」。
|
||
|
||
### 4.4 趸交 → 分期,趸交已缴
|
||
|
||
**禁止**。提示「趸交已结清,无法改为分期」。
|
||
|
||
---
|
||
|
||
## 5. 交互设计
|
||
|
||
### 5.1 切换前校验
|
||
|
||
用户点击「分期 / 趸交」radio 时:
|
||
|
||
1. 若目标模式与当前相同 → 无操作。
|
||
2. 调用 `evaluatePayModeSwitch(currentMode, targetMode, details, contractAmount)`。
|
||
3. 返回 `blocked` → `ElMessageBox.alert` 说明原因,**回滚 radio**。
|
||
4. 返回 `allowed` → `ElMessageBox.confirm` 展示影响摘要,确认后执行变换。
|
||
|
||
**确认文案示例(fq→dj,部分已缴)**:
|
||
|
||
> 将作废 4 期未缴计划,保留 2 期已缴记录,并新增 1 条趸交明细(剩余 ¥X,XXX.XX)。是否继续?
|
||
|
||
### 5.2 编辑态保护
|
||
|
||
- 锁定明细:禁用删除、应缴金额、缴费状态、期次、应缴日编辑。
|
||
- 趸交模式下隐藏「添加明细」(或禁用,保证仅 1 条有效未缴行 + 已缴历史行)。
|
||
- 切到分期时**不**强制 `resetCountInterval`;优先保留主表已保存的期数/间隔/开始时间。
|
||
|
||
### 5.3 表单数据结构(过渡期)
|
||
|
||
```typescript
|
||
interface PaymentDetailRow {
|
||
// ...existing fields
|
||
_payModeLocked?: boolean // yjf 等锁定行
|
||
_voided?: boolean // 前端待作废,保存时不提交或提交 void 标记
|
||
}
|
||
```
|
||
|
||
保存 payload:仅提交**未作废**明细 + 锁定保留行;作废行由后端按 id 软删(P1)或前端不提交、后端 diff(P2)。
|
||
|
||
---
|
||
|
||
## 6. 后端改造(分阶段)
|
||
|
||
### P0(与前端同步上线)
|
||
|
||
在 `save_payment_details` / 协议 bundle 保存前增加校验:
|
||
|
||
- `pay_mode=dj`:有效明细中「未锁定 + 未作废」的**待缴**行 ≤ 1;`Σ dueAmount(dfj 行)` = `remaining`(无已缴时 = 合同金额)。
|
||
- `pay_mode=fq`:有效 `dfj` 行数 = `installment_count`(无已缴切换场景);或允许「已缴 k 期 + 剩余 N−k 期 dfj」(dj→fq 不适用,仅 fq 内改参数时需另议)。
|
||
- 禁止客户端把 `yjf` 行 `pay_status` 改回 `dfj`。
|
||
|
||
### P1
|
||
|
||
- 作废明细:`del_flag=1`,分期预警自然排除。
|
||
- 保存时带 `detail.id` upsert,避免每次编辑换 UUID。
|
||
|
||
### P2
|
||
|
||
- 专用 API:`POST .../payment/change-mode`
|
||
- 操作日志:记录模式变更、作废条数、剩余金额
|
||
|
||
---
|
||
|
||
## 7. 核心算法(前端纯函数)
|
||
|
||
建议新增 `web-admin/src/utils/protocol-pay-mode-switch.ts`:
|
||
|
||
```typescript
|
||
type SwitchResult =
|
||
| { ok: true; details: DetailLike[]; summary: string }
|
||
| { ok: false; reason: string }
|
||
|
||
function evaluatePayModeSwitch(...): SwitchResult | 'need_confirm'
|
||
function applyPayModeSwitch(...): SwitchResult
|
||
```
|
||
|
||
**禁止**在 `onPayModeChange` 中直接 `forEach` 重写全部明细;改为 `applyPayModeSwitch` 返回新数组。
|
||
|
||
`tryGenerateInstallmentDetails` / `syncPaymentDueAmounts`:
|
||
|
||
- 跳过 `_payModeLocked` 或 `payStatus !== 'dfj'` 的行;
|
||
- 仅对未锁定 `dfj` 行重算金额/日期。
|
||
|
||
---
|
||
|
||
## 8. 测试用例
|
||
|
||
| # | 场景 | 期望 |
|
||
|---|------|------|
|
||
| T1 | dj dfj×1 → fq 6期 | 1 条作废,6 条新 dfj,金额均分 |
|
||
| T2 | fq 6×dfj → dj | 6 条作废,1 条 dj dfj 全额 |
|
||
| T3 | fq 2×yjf + 4×dfj → dj | 2 条 yjf 保留,4 条作废,1 条 dj remaining |
|
||
| T4 | dj yjf×1 → fq | blocked |
|
||
| T5 | fq 含 jfqrz → dj | blocked |
|
||
| T6 | fq 全部 yjf → dj | blocked(remaining=0) |
|
||
| T7 | 切换取消 | radio 回滚,明细不变 |
|
||
|
||
---
|
||
|
||
## 9. 不在本次范围
|
||
|
||
- 退费后再改模式的专流程 UI
|
||
- 小程序端缴费模式切换
|
||
|
||
---
|
||
|
||
## 10. 实现顺序建议
|
||
|
||
1. `protocol-pay-mode-switch.ts` + 单元测试(Node/vitest 或 backend pytest 镜像逻辑)
|
||
2. 改造 `onPayModeChange` + 确认弹窗
|
||
3. 改造 `tryGenerateInstallmentDetails` / `syncPaymentDueAmounts` 跳过锁定行
|
||
4. 后端 P0 校验
|
||
5. 联调四个场景 + 分期预警回归
|
||
|
||
---
|
||
|
||
## 11. 分期参数变更(非模式切换)
|
||
|
||
编辑已保存的**分期**协议时,用户可能修改「分期期数 / 每期间隔 / 分期开始时间」,规则如下:
|
||
|
||
| 情况 | 处理 |
|
||
|------|------|
|
||
| 全部 `dfj` | 确认后按新参数**整表重建** N 条明细 |
|
||
| 部分 `yjf` | 保留已缴行;**作废**全部 `dfj`;按 **剩余金额** 均分生成 `(新期数 − 已缴期数)` 条未缴明细;应缴日按已缴期数续算 index |
|
||
| 期数小于已缴最大期次 | **禁止** |
|
||
| 含 `jfqrz`/`tfqrz`/`ytf` | **禁止** |
|
||
| 仅改间隔/开始时间(期数不变) | 确认后保留 yjf,重建未缴行(金额+日期) |
|
||
|
||
实现:`web-admin/src/utils/protocol-fq-params-change.ts`,表单 `@change` → `handleFqParamsChange()`。
|
||
|
||
---
|
||
|
||
**审阅**:请确认本 spec 无遗漏后,进入 `writing-plans` 生成实现任务清单。
|