2026-06-10-protocol-installment-config-design.md 11 KB

采集协议单 · 分期缴费可配置化设计

日期: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:返回 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)

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 期数变更

  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 一致:

  • 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 生成实现任务清单。