8.2 KiB
采集协议单 · 缴费模式切换设计
日期:2026-06-04
范围:web-admin 采集协议单新增/编辑弹窗(ProtocolFormDialog.vue)及后端缴费主表/明细保存
状态:已实现(P0)
1. 背景
采集协议单缴费主表 pay_mode 有两种取值:
| 值 | 含义 | 明细约定 |
|---|---|---|
dj |
趸交(一次性) | 1 条有效明细 |
fq |
分期 | N 条明细,N = installment_count |
用户在编辑已保存协议时可能切换缴费模式。当前实现仅做字段联动(金额同步、日期重算),不区分明细缴费状态,会导致:
- 分期改趸交后仍保留多条明细且每条金额 = 合同全额;
- 已缴明细被重算应缴日/期次;
- 趸交已缴后仍可改分期,账实不符。
本设计定义切换规则与实现边界。
2. 决策记录
| 议题 | 决定 |
|---|---|
| 小数月间隔换算 | 30 天/月(已实现,见 protocol-payment-installment.ts) |
| 第 2 期起提前天数 | 可配置,默认 30 天(见 2026-06-10-protocol-installment-config-design.md) |
| 分期部分已缴后改趸交,未缴期次 | A:作废(软删/不再展示,不进分期预警) |
| 已缴/确认中明细 | 保留,只读,不参与重算 |
| 趸交已缴后改分期 | 禁止 |
| 趸交确认中改分期 | 禁止(提示先完成或撤回财务确认) |
3. 明细状态与「锁定」定义
| pay_status | 含义 | 切换时 |
|---|---|---|
dfj |
待缴费 | 可作废、可重建 |
jfqrz |
缴费确认中 | 锁定(视为已占用,禁止模式切换) |
yjf |
已缴费 | 锁定(保留,只读) |
tfqrz |
退费确认中 | 锁定(禁止切换) |
ytf |
已退费 | 锁定(禁止切换) |
剩余应缴金额:
remaining = contractAmount − Σ(dueAmount WHERE pay_status IN ('yjf'))
jfqrz 阶段切换一律阻断;若业务后续允许「确认中算占用」,再单独开变更。
4. 场景规则
4.1 趸交 → 分期(dj → fq)
| 前置条件 | 处理 |
|---|---|
无明细或全部 dfj |
允许。作废全部现有明细;按分期参数生成 N 条新 dfj 明细;主表 pay_mode=fq。 |
存在 jfqrz / yjf / tfqrz / ytf |
禁止。提示原因。 |
切换后:第 i 期 installmentNo=i,应缴日按 calcFqInstallmentDueDate,应缴金额按合同均分(末期待尾差)。
4.2 分期 → 趸交(fq → dj),一期未缴
| 前置条件 | 处理 |
|---|---|
全部 dfj |
允许。作废全部 N 条分期明细;新建 1 条趸交明细:dueAmount=contractAmount,installmentNo=null,应缴日=lumpSumDeadline。 |
存在任何非 dfj |
走 4.3 |
4.3 分期 → 趸交(fq → dj),部分已缴
| 前置条件 | 处理 |
|---|---|
有 yjf,无 jfqrz/tfqrz/ytf |
允许。 |
存在 jfqrz/tfqrz/ytf |
禁止。 |
允许时步骤:
- 保留所有
yjf明细(只读展示,保留原installmentNo、转账信息)。 - 作废所有
dfj分期明细(不进入表单列表;保存时del_flag=1)。 - 新建 1 条趸交明细:
dueAmount=remaining,payStatus=dfj,installmentNo=null,应缴日=lumpSumDeadline。 - 主表
pay_mode=dj;分期参数字段可保留只读快照(便于查看历史计划)。
若 remaining ≤ 0:禁止切换,提示「已缴清,无需改趸交」。
4.4 趸交 → 分期,趸交已缴
禁止。提示「趸交已结清,无法改为分期」。
5. 交互设计
5.1 切换前校验
用户点击「分期 / 趸交」radio 时:
- 若目标模式与当前相同 → 无操作。
- 调用
evaluatePayModeSwitch(currentMode, targetMode, details, contractAmount)。 - 返回
blocked→ElMessageBox.alert说明原因,回滚 radio。 - 返回
allowed→ElMessageBox.confirm展示影响摘要,确认后执行变换。
确认文案示例(fq→dj,部分已缴):
将作废 4 期未缴计划,保留 2 期已缴记录,并新增 1 条趸交明细(剩余 ¥X,XXX.XX)。是否继续?
5.2 编辑态保护
- 锁定明细:禁用删除、应缴金额、缴费状态、期次、应缴日编辑。
- 趸交模式下隐藏「添加明细」(或禁用,保证仅 1 条有效未缴行 + 已缴历史行)。
- 切到分期时不强制
resetCountInterval;优先保留主表已保存的期数/间隔/开始时间。
5.3 表单数据结构(过渡期)
interface PaymentDetailRow {
// ...existing fields
_payModeLocked?: boolean // yjf 等锁定行
_voided?: boolean // 前端待作废,保存时不提交或提交 void 标记
}
保存 payload:仅提交未作废明细 + 锁定保留行;作废行由后端按 id 软删(P1)或前端不提交、后端 diff(P2)。
6. 后端改造(分阶段)
P0(与前端同步上线)
在 save_payment_details / 协议 bundle 保存前增加校验:
pay_mode=dj:有效明细中「未锁定 + 未作废」的待缴行 ≤ 1;Σ dueAmount(dfj 行)=remaining(无已缴时 = 合同金额)。pay_mode=fq:有效dfj行数 =installment_count(无已缴切换场景);或允许「已缴 k 期 + 剩余 N−k 期 dfj」(dj→fq 不适用,仅 fq 内改参数时需另议)。- 禁止客户端把
yjf行pay_status改回dfj。
P1
- 作废明细:
del_flag=1,分期预警自然排除。 - 保存时带
detail.idupsert,避免每次编辑换 UUID。
P2
- 专用 API:
POST .../payment/change-mode - 操作日志:记录模式变更、作废条数、剩余金额
7. 核心算法(前端纯函数)
建议新增 web-admin/src/utils/protocol-pay-mode-switch.ts:
type SwitchResult =
| { ok: true; details: DetailLike[]; summary: string }
| { ok: false; reason: string }
function evaluatePayModeSwitch(...): SwitchResult | 'need_confirm'
function applyPayModeSwitch(...): SwitchResult
禁止在 onPayModeChange 中直接 forEach 重写全部明细;改为 applyPayModeSwitch 返回新数组。
tryGenerateInstallmentDetails / syncPaymentDueAmounts:
- 跳过
_payModeLocked或payStatus !== 'dfj'的行; - 仅对未锁定
dfj行重算金额/日期。
8. 测试用例
| # | 场景 | 期望 |
|---|---|---|
| T1 | dj dfj×1 → fq 6期 | 1 条作废,6 条新 dfj,金额均分 |
| T2 | fq 6×dfj → dj | 6 条作废,1 条 dj dfj 全额 |
| T3 | fq 2×yjf + 4×dfj → dj | 2 条 yjf 保留,4 条作废,1 条 dj remaining |
| T4 | dj yjf×1 → fq | blocked |
| T5 | fq 含 jfqrz → dj | blocked |
| T6 | fq 全部 yjf → dj | blocked(remaining=0) |
| T7 | 切换取消 | radio 回滚,明细不变 |
9. 不在本次范围
- 退费后再改模式的专流程 UI
- 小程序端缴费模式切换
10. 实现顺序建议
protocol-pay-mode-switch.ts+ 单元测试(Node/vitest 或 backend pytest 镜像逻辑)- 改造
onPayModeChange+ 确认弹窗 - 改造
tryGenerateInstallmentDetails/syncPaymentDueAmounts跳过锁定行 - 后端 P0 校验
- 联调四个场景 + 分期预警回归
11. 分期参数变更(非模式切换)
编辑已保存的分期协议时,用户可能修改「分期期数 / 每期间隔 / 分期开始时间」,规则如下:
| 情况 | 处理 |
|---|---|
全部 dfj |
确认后按新参数整表重建 N 条明细 |
部分 yjf |
保留已缴行;作废全部 dfj;按 剩余金额 均分生成 (新期数 − 已缴期数) 条未缴明细;应缴日按已缴期数续算 index |
| 期数小于已缴最大期次 | 禁止 |
含 jfqrz/tfqrz/ytf |
禁止 |
| 仅改间隔/开始时间(期数不变) | 确认后保留 yjf,重建未缴行(金额+日期) |
实现:web-admin/src/utils/protocol-fq-params-change.ts,表单 @change → handleFqParamsChange()。
审阅:请确认本 spec 无遗漏后,进入 writing-plans 生成实现任务清单。