11 KiB
采集协议单 · 分期缴费可配置化设计
日期:2026-06-10
范围:web-admin 采集协议单新增/编辑弹窗(ProtocolFormDialog.vue)、查看弹窗、后端缴费主表/明细保存与计算
前置 spec: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 应缴费时间
function calcFqInstallmentDueDate(
startDate: string,
index: number, // 0-based 期次索引
intervalMonths: number,
advanceDays: number // 新增参数,默认 30
): string
index === 0:返回startDateindex >= 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)
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:
interface FqParams {
installmentCount: number | null
installmentIntervalMonths: number | null
installmentStartDate: string
installmentAdvanceDays: number | null // 新增
installmentRatios: number[] // 新增
}
以下变更走现有 handleFqParamsChange 流程(校验 → 确认 → 重建):
- 分期期数
- 每期间隔
- 分期开始时间
- 应缴费提前天数
- 缴费比例(前 N−1 期)
isSameFqParams 同步比较新字段。
6.2 期数变更
resizeInstallmentRatios调整数组(策略 1)- 重算最后一期比例
- 触发
handleFqParamsChange(与改期数同一链路)
6.3 合同金额变更
syncContractAmount → syncPaymentDueAmounts:按当前比例重算未锁定明细金额(逻辑 §4.2 / §4.3)。
6.4 缴费模式切换
- dj → fq:生成 N 条明细时使用当前
installmentRatios+installmentAdvanceDays - fq → dj:比例/提前天数字段保留主表快照(与现有分期参数一致,只读历史)
6.5 已缴 / 锁定规则
与 2026-06-04 spec 一致:
yjf/jfqrz/tfqrz/ytf行锁定- 含
jfqrz等时禁止修改分期参数(含比例、提前天数) - 期数不得小于已缴最大期次
7. 后端改造
7.1 CRUD / Schema
_row_to_dict增加两字段- Create/Update body 可写(分期模式下必填校验见下)
- 读取 bundle 时返回数组
7.2 保存校验(pay_mode=fq)
installment_advance_days≥ 0(建议整数)len(installment_ratios) === installment_countsum(installment_ratios) === 100(允许 ±0.01 浮点误差)- 各期
due_amount之和 =contract_amount(±0.015,已有) - 明细条数 = 期数(已有)
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. 实现顺序建议
- DB 迁移 + Model/Schema/CRUD 字段
- 前端
protocol-payment-installment.ts:比例算金额、提前天数参数 protocol-fq-params-change.ts/protocol-pay-mode-switch.ts扩展ProtocolFormDialog.vueUI + 联动- 后端校验 + 计算参数化
ProtocolViewDialog.vue只读展示- 联调测试 T1–T10
审阅:请确认本 spec 无遗漏后,进入 writing-plans 生成实现任务清单。