307 lines
11 KiB
Markdown
307 lines
11 KiB
Markdown
# 采集协议单 · 分期缴费可配置化设计
|
||
|
||
**日期**: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` 生成实现任务清单。
|