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