Files
zkxw/docs/superpowers/specs/2026-06-10-protocol-installment-config-design.md
T
2026-06-11 15:42:58 +08:00

307 lines
11 KiB
Markdown
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.
# 采集协议单 · 分期缴费可配置化设计
**日期**:2026-06-10
**范围**:`web-admin` 采集协议单新增/编辑弹窗(`ProtocolFormDialog.vue`)、查看弹窗、后端缴费主表/明细保存与计算
**前置 spec**:[2026-06-04-protocol-pay-mode-switch-design.md](./2026-06-04-protocol-pay-mode-switch-design.md)
**状态**:已实现(2026-06-10)
---
## 1. 背景与目标
分期模式(`pay_mode=fq`)下,当前应缴费时间与应缴费金额均为**硬编码规则**:
| 维度 | 现状 | 目标 |
|------|------|------|
| 应缴费提前天数 | 第 2 期起固定提前 **10** 天 | 可配置,默认 **30** 天 |
| 每期缴费比例 | 各期**均分**合同金额 | 每期比例可配置,默认 `50/10/10/10/10/10` |
| 比例合计 | — | **必须 = 100%**;最后一期自动补齐 |
| 期数变化 | 明细条数联动 | 比例输入框数量同步;保留已有比例,新增框默认 0 |
本设计在既有「缴费模式切换 / 分期参数变更 / 已缴锁定」规则之上扩展,**不改动**趸交逻辑与明细状态机。
---
## 2. 决策记录
| 议题 | 决定 |
|------|------|
| 提前天数默认值 | **30** 天(替代原 10 天;更新 2026-06-04 spec 对应条目) |
| 提前天数适用范围 | 仅第 **2** 期起;第 1 期 = 分期开始时间 |
| 比例默认值(6 期) | `50 / 10 / 10 / 10 / 10 / 10` |
| 比例合计 | **必须 = 100%** |
| 最后一期比例 | **只读**,自动 = `100 − Σ(前 N−1 期)` |
| 期数减少(如 6→3) | **保留前 N 个**比例,最后一期自动补齐 |
| 期数增加(如 3→6) | **保留已有**比例,新增框默认 **0**,最后一期自动补齐 |
| 首次切分期 / 新增 | 期数 6、间隔 6 月、提前 30 天、比例默认 50/10/10/10/10/10 |
| 小数月间隔换算 | 仍 30 天/月(不变) |
| 已缴行(`yjf`) | 保留原 `dueAmount`,不参与比例/日期重算 |
| 部分已缴后重建未缴 | 剩余金额按**未缴期次比例归一化**分配 |
---
## 3. 数据模型
### 3.1 缴费主表新增字段
表 `biz_protocol_payment`:
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `installment_advance_days` | INT | 30 | 第 2 期起应缴费提前天数 |
| `installment_ratios` | VARCHAR(256) | NULL | 各期缴费比例,JSON 数组,如 `[50,10,10,10,10,10]`,长度 = `installment_count` |
迁移脚本:`docs/sql/migrations/2026-06-10-protocol-installment-config.sql`
同步更新 `docs/sql/zkxw.sql`。
### 3.2 前后端字段映射
| 后端 snake | 前端 camel |
|------------|------------|
| `installment_advance_days` | `installmentAdvanceDays` |
| `installment_ratios` | `installmentRatios` |
`installmentRatios` 在前端为 `number[]`,长度始终等于 `installmentCount`。
### 3.3 历史数据兼容
- 旧记录无新字段:`installmentAdvanceDays` 读默认 **30**;`installmentRatios` 为空时按**均分**反推或保存时按均分写入(编辑打开时若为空则 `Array(count).fill(100/count)` 末位补尾差到 100)。
- 只读查看:无比例快照时展示「均分(历史)」或按明细反算比例(可选,P1)。
---
## 4. 计算规则
### 4.1 应缴费时间
```typescript
function calcFqInstallmentDueDate(
startDate: string,
index: number, // 0-based 期次索引
intervalMonths: number,
advanceDays: number // 新增参数,默认 30
): string
```
- `index === 0`:返回 `startDate`
- `index >= 1`:`addIntervalMonthsYmd(startDate, index * intervalMonths)` 再 **`− advanceDays` 天**
前后端共用同一规则;后端 `protocol_payment_installment.py` 删除常量 `FQ_ADVANCE_DAYS = 10`,改为读主表字段。
### 4.2 应缴费金额(全额、无已缴)
设合同金额 `C`,比例数组 `R[0..N−1]`,ΣR = 100:
```
amount[i] = floor(C × R[i] / 100 × 100) / 100 // 前 N−1 期,分向下取整
amount[N−1] = C − Σ amount[0..N−2] // 最后一期补尾差
```
### 4.3 应缴费金额(部分已缴)
已有 `yjf` 行时,修改比例/期数/合同金额触发未缴重建:
```
remaining = contractAmount − Σ(dueAmount WHERE pay_status = 'yjf')
unpaidIndices = 未缴期次索引集合
unpaidRatioSum = Σ R[i] for i in unpaidIndices
amount[i] = remaining × R[i] / unpaidRatioSum // 前若干未缴期向下取整
最后一期未缴 = remaining − Σ(其他未缴期 amount)
```
已缴行 `dueAmount`、`installmentNo`、转账信息**不变**。
### 4.4 比例数组维护(期数联动 · 策略 1)
```typescript
function resizeInstallmentRatios(
oldRatios: number[],
newCount: number
): number[]
```
| 操作 | 行为 |
|------|------|
| `newCount > old.length` | 保留原数组,末尾补 `(newCount - old.length)` 个 **0** |
| `newCount < old.length` | 截取前 `newCount` 个 |
| `newCount === old.length` | 不变 |
| 最后一项 | 始终重算为 `100 − sum(前 N−1)`,**不可编辑** |
**示例**
- 6→3:`[50,10,10,10,10,10]` → `[50,10,40]`(末位自动)
- 3→6:`[50,10,40]` → `[50,10,0,0,0,40]`(末位自动)
校验:`sum(前 N−1) <= 100`;若 `> 100` 表单报错,禁止保存。
---
## 5. UI 设计
### 5.1 布局位置
`ProtocolFormDialog.vue` → 缴费信息 → 缴费主表 → `payMode === 'fq'` 区块。
在现有「分期期数 / 每期间隔(月) / 分期开始时间」下方新增一行(或两行):
```
应缴费提前(天) [ 30 ] (整数,≥ 0,建议 min=0 max=365)
缴费比例(%) [50] [10] [10] [10] [10] [10]
第1期 … 第5期 第6期(自动)
合计:100% (前 N−1 可编辑,最后一期 disabled + 自动值)
```
- 每个比例框:`el-input type="number"`,宽度紧凑(约 64–72px),带期次序号 label(第 1 期…第 N 期)。
- 最后一期 label 加 `form-label-tip`:「自动补齐」。
- 比例变更 → 防抖或 `@change` 后调用 `syncPaymentDueAmounts()`(跳过锁定行)。
- 提前天数变更 → 纳入分期参数变更流程(见 §6)。
### 5.2 查看弹窗
`ProtocolViewDialog.vue` 分期区块只读展示:
- 应缴费提前(天)
- 缴费比例:「50% / 10% / … / 10%」
### 5.3 默认值
| 场景 | installmentAdvanceDays | installmentRatios |
|------|------------------------|-------------------|
| 新增协议,首次选分期 | 30 | `[50,10,10,10,10,10]` |
| 编辑旧数据,字段为空 | 30 | 均分生成并末位补齐 |
| dj→fq 切换 | 30 | 默认 6 期比例(若 resetCountInterval) |
---
## 6. 交互与联动
### 6.1 纳入「分期参数变更」的参数
扩展 `FqParams`:
```typescript
interface FqParams {
installmentCount: number | null
installmentIntervalMonths: number | null
installmentStartDate: string
installmentAdvanceDays: number | null // 新增
installmentRatios: number[] // 新增
}
```
以下变更走现有 `handleFqParamsChange` 流程(校验 → 确认 → 重建):
- 分期期数
- 每期间隔
- 分期开始时间
- **应缴费提前天数**
- **缴费比例**(前 N−1 期)
`isSameFqParams` 同步比较新字段。
### 6.2 期数变更
1. `resizeInstallmentRatios` 调整数组(策略 1)
2. 重算最后一期比例
3. 触发 `handleFqParamsChange`(与改期数同一链路)
### 6.3 合同金额变更
`syncContractAmount` → `syncPaymentDueAmounts`:按当前比例重算**未锁定**明细金额(逻辑 §4.2 / §4.3)。
### 6.4 缴费模式切换
- **dj → fq**:生成 N 条明细时使用当前 `installmentRatios` + `installmentAdvanceDays`
- **fq → dj**:比例/提前天数字段保留主表快照(与现有分期参数一致,只读历史)
### 6.5 已缴 / 锁定规则
与 [2026-06-04 spec](./2026-06-04-protocol-pay-mode-switch-design.md) 一致:
- `yjf` / `jfqrz` / `tfqrz` / `ytf` 行锁定
- 含 `jfqrz` 等时禁止修改分期参数(含比例、提前天数)
- 期数不得小于已缴最大期次
---
## 7. 后端改造
### 7.1 CRUD / Schema
- `_row_to_dict` 增加两字段
- Create/Update body 可写(分期模式下必填校验见下)
- 读取 bundle 时返回数组
### 7.2 保存校验(`pay_mode=fq`)
1. `installment_advance_days` ≥ 0(建议整数)
2. `len(installment_ratios) === installment_count`
3. `sum(installment_ratios) === 100`(允许 ±0.01 浮点误差)
4. 各期 `due_amount` 之和 = `contract_amount`(±0.015,已有)
5. 明细条数 = 期数(已有)
### 7.3 计算服务
- `calc_fq_installment_due_date(..., advance_days)` 参数化
- 可选:抽取 `calc_installment_due_amounts_by_ratios(contract, ratios)` 与前端 mirror
---
## 8. 影响文件
| 层级 | 路径 |
|------|------|
| SQL | `docs/sql/migrations/2026-06-10-protocol-installment-config.sql`、`docs/sql/zkxw.sql` |
| Model | `backend/app/models/models.py` |
| CRUD/Schema/API | `backend/app/crud/collection_protocol.py`、相关 schema |
| 计算 | `backend/app/services/protocol_payment_installment.py` |
| 校验 | `backend/app/services/protocol_pay_mode_switch.py` |
| 前端工具 | `protocol-payment-installment.ts` |
| 前端工具 | `protocol-fq-params-change.ts` |
| 前端工具 | `protocol-pay-mode-switch.ts` |
| 表单 | `ProtocolFormDialog.vue` |
| 查看 | `ProtocolViewDialog.vue` |
| 文档 | 更新 `2026-06-04-protocol-pay-mode-switch-design.md` §2 决策表 |
---
## 9. 测试用例
| # | 场景 | 期望 |
|---|------|------|
| T1 | 新增 fq,默认 6 期 | 提前 30 天;比例 50/10/10/10/10/10;金额按比分配 |
| T2 | 改第 1 期比例为 60 | 末位自动变 0;前 5 期 10 各;金额重算 |
| T3 | 前 5 期各 25,合计 >100 | 表单报错,禁止保存 |
| T4 | 6→3 期 | 比例 `[50,10,40]`;3 条明细 |
| T5 | 3→6 期 | 比例 `[50,10,0,0,0,40]`;6 条明细 |
| T6 | 改提前天数 30→15 | 未缴应缴日重算(确认后) |
| T7 | 2 期 yjf + 改比例 | yjf 金额不变;未缴按归一化比例分 remaining |
| T8 | 编辑旧数据(无新字段) | 默认 30 天;比例均分展示 |
| T9 | 保存 | 后端校验比例和=100、金额和=合同 |
| T10 | 查看弹窗 | 展示提前天数与比例 |
---
## 10. 不在本次范围
- 缴费确认 / 退费列表页的筛选默认值(仍为「前 10 天」业务文案,与协议提前天数无关)
- 小程序端
- 按比例的历史数据批量迁移脚本(仅读兼容)
---
## 11. 实现顺序建议
1. DB 迁移 + Model/Schema/CRUD 字段
2. 前端 `protocol-payment-installment.ts`:比例算金额、提前天数参数
3. `protocol-fq-params-change.ts` / `protocol-pay-mode-switch.ts` 扩展
4. `ProtocolFormDialog.vue` UI + 联动
5. 后端校验 + 计算参数化
6. `ProtocolViewDialog.vue` 只读展示
7. 联调测试 T1–T10
---
**审阅**:请确认本 spec 无遗漏后,进入 `writing-plans` 生成实现任务清单。