日期:2026-06-10
范围:web-admin 采集协议单新增/编辑弹窗(ProtocolFormDialog.vue)、查看弹窗、后端缴费主表/明细保存与计算
前置 spec:2026-06-04-protocol-pay-mode-switch-design.md
状态:已实现(2026-06-10)
分期模式(pay_mode=fq)下,当前应缴费时间与应缴费金额均为硬编码规则:
| 维度 | 现状 | 目标 |
|---|---|---|
| 应缴费提前天数 | 第 2 期起固定提前 10 天 | 可配置,默认 30 天 |
| 每期缴费比例 | 各期均分合同金额 | 每期比例可配置,默认 50/10/10/10/10/10 |
| 比例合计 | — | 必须 = 100%;最后一期自动补齐 |
| 期数变化 | 明细条数联动 | 比例输入框数量同步;保留已有比例,新增框默认 0 |
本设计在既有「缴费模式切换 / 分期参数变更 / 已缴锁定」规则之上扩展,不改动趸交逻辑与明细状态机。
| 议题 | 决定 |
|---|---|
| 提前天数默认值 | 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,不参与比例/日期重算 |
| 部分已缴后重建未缴 | 剩余金额按未缴期次比例归一化分配 |
表 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。
| 后端 snake | 前端 camel |
|---|---|
installment_advance_days |
installmentAdvanceDays |
installment_ratios |
installmentRatios |
installmentRatios 在前端为 number[],长度始终等于 installmentCount。
installmentAdvanceDays 读默认 30;installmentRatios 为空时按均分反推或保存时按均分写入(编辑打开时若为空则 Array(count).fill(100/count) 末位补尾差到 100)。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,改为读主表字段。
设合同金额 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] // 最后一期补尾差
已有 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、转账信息不变。
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),不可编辑 |
示例
[50,10,10,10,10,10] → [50,10,40](末位自动)[50,10,40] → [50,10,0,0,0,40](末位自动)校验:sum(前 N−1) <= 100;若 > 100 表单报错,禁止保存。
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 期)。form-label-tip:「自动补齐」。@change 后调用 syncPaymentDueAmounts()(跳过锁定行)。ProtocolViewDialog.vue 分期区块只读展示:
| 场景 | installmentAdvanceDays | installmentRatios |
|---|---|---|
| 新增协议,首次选分期 | 30 | [50,10,10,10,10,10] |
| 编辑旧数据,字段为空 | 30 | 均分生成并末位补齐 |
| dj→fq 切换 | 30 | 默认 6 期比例(若 resetCountInterval) |
扩展 FqParams:
interface FqParams {
installmentCount: number | null
installmentIntervalMonths: number | null
installmentStartDate: string
installmentAdvanceDays: number | null // 新增
installmentRatios: number[] // 新增
}
以下变更走现有 handleFqParamsChange 流程(校验 → 确认 → 重建):
isSameFqParams 同步比较新字段。
resizeInstallmentRatios 调整数组(策略 1)handleFqParamsChange(与改期数同一链路)syncContractAmount → syncPaymentDueAmounts:按当前比例重算未锁定明细金额(逻辑 §4.2 / §4.3)。
installmentRatios + installmentAdvanceDays与 2026-06-04 spec 一致:
yjf / jfqrz / tfqrz / ytf 行锁定jfqrz 等时禁止修改分期参数(含比例、提前天数)_row_to_dict 增加两字段pay_mode=fq)installment_advance_days ≥ 0(建议整数)len(installment_ratios) === installment_countsum(installment_ratios) === 100(允许 ±0.01 浮点误差)due_amount 之和 = contract_amount(±0.015,已有)calc_fq_installment_due_date(..., advance_days) 参数化calc_installment_due_amounts_by_ratios(contract, ratios) 与前端 mirror| 层级 | 路径 |
|---|---|
| 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 决策表 |
| # | 场景 | 期望 |
|---|---|---|
| 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 | 查看弹窗 | 展示提前天数与比例 |
protocol-payment-installment.ts:比例算金额、提前天数参数protocol-fq-params-change.ts / protocol-pay-mode-switch.ts 扩展ProtocolFormDialog.vue UI + 联动ProtocolViewDialog.vue 只读展示审阅:请确认本 spec 无遗漏后,进入 writing-plans 生成实现任务清单。