Skip to content

还款系统设计 ​

定位:钱放出去只是开始,收得回来、收得分毫不差才是信贷的完整周期。还款系统的三个硬问题:三种还款入口怎么收敛到一条出口管线。一笔钱进来先冲谁。批量代扣几千笔里有一笔失败怎么办。冲抵与借据的业务语义见信贷业务全景与领域模型。记账与计息规则见账务系统设计-复式记账与日切。资金渠道与内部户见清结算与资方对接。

一、还款方式矩阵:三个入口,一条出口 ​

维度主动还款自动代扣(批扣)提前结清
触发方用户发起我方日终批量发起用户发起
金额确定用户输入(≥ 当期最低)系统按应还清单生成系统试算(试算单有时效)
渠道收银台/余额/银行卡代收接口/批扣文件收银台一次性支付
失败语义用户自行重试进重试批次 + 触达提醒试算单过期重算
幂等键还款单号批次号 + 明细序号还款单号
特殊约束—扣款时间窗(避开夜间)利息算头不算尾、违约金可减免

三种入口全部收敛到同一条出口管线——入口多元化、出口唯一化:

主动还款 ─┐
自动代扣 ─┼─▶ 还款单 ─▶ 渠道扣款/入账确认 ─▶ 账务分录 ─▶ 冲抵引擎 ─▶ 借据/额度事件
提前结清 ─┘   (唯一幂等键)   (以渠道终态为准)   (账务先行)   (顺序配置化)

出口唯一的价值:冲抵、记账、事件、对账只实现一次。任何新还款方式(余额、他人代还、新支付渠道)只是新增入口适配器,资金正确性核心零改动。

二、冲抵顺序引擎:配置化的费用→利息→本金 ​

1)应还的拆分模型 ​

"欠多少钱"不是一个数,是四类科目余额的组合,冲抵的对象是拆分行而不是借据总余额:

sql
CREATE TABLE iou_receivable (          -- 借据应还拆分: 冲抵的对象
  id            BIGINT PRIMARY KEY,
  iou_id        BIGINT NOT NULL,
  item_type     VARCHAR(16) NOT NULL,  -- PRINCIPAL/INTEREST/PENALTY/FEE
  period_no     INT NOT NULL,          -- 期号
  receivable    DECIMAL(16,2) NOT NULL,  -- 应还
  settled_amt   DECIMAL(16,2) NOT NULL DEFAULT 0,  -- 已冲抵
  UNIQUE KEY uk_iou_item (iou_id, item_type, period_no)
);

CREATE TABLE offset_rule_config (      -- 冲抵顺序: 配置化, 按产品 + 口径版本
  id            BIGINT PRIMARY KEY,
  product_code  VARCHAR(32) NOT NULL,
  rule_version  VARCHAR(16) NOT NULL,  -- 监管/合同口径版本
  seq           INT NOT NULL,          -- 1,2,3… 先冲谁
  item_type     VARCHAR(16) NOT NULL,
  effective_at  DATETIME NOT NULL,     -- 存量借据按立据时点版本执行
  UNIQUE KEY uk_prod_ver_seq (product_code, rule_version, seq)
);

2)冲抵算法 ​

java
// 还款金额按配置顺序逐项冲抵; 每笔冲抵 = 一条账务分录 + 一行 settled_amt 条件更新
BigDecimal rest = repayAmount;
for (OffsetRule rule : rules) {                 // seq 升序: FEE→PENALTY→INTEREST→PRINCIPAL
    BigDecimal owe = r.getReceivable().subtract(r.getSettledAmt());
    BigDecimal hit = rest.min(owe);
    if (hit.signum() > 0) { offsetItem(r, hit); rest = rest.subtract(hit); }
    if (rest.signum() == 0) break;
}
if (rest.signum() > 0) depositToSuspense(rest); // 溢缴款挂暂收, 不冲"还没发生的账"
  • 顺序为什么配置化:监管口径会调整(费用/罚息/利息/本金的先后有政策窗口)。产品间有差异、合同有约定。写死代码意味着每次口径调整都是发版,且存量借据必须按立据时点的口径执行。所以顺序必须带版本号(与清结算与资方对接里清分规则按政策版本回放是同一问题);
  • 本金最后冲不是商务选择而是风险常识。本金在,利息罚息才有计息基数。本金先冲完,后续计息成了无源之水;
  • 尾差规则写进配置:最后一期应还 = 剩余全部,凑整误差落在指定期次。禁止"差一分钱结不了清"。

三、部分还款时序 ​

用户            还款域              渠道            账务域            借据/额度
 │ ①还款 3000    │                  │               │               │
 ├──────────────▶│ 立还款单 PROCESSING│               │               │
 │ ◀──待支付──────┤                  │               │               │
 │ ②收银台支付     ├─────────────────▶│ 代收           │               │
 │               │ ◀──支付成功回调─────┤ (验签+三层幂等)  │               │
 │               │ ③记账: 借 备付金 / 贷 应收还款        │               │
 │               ├─────────────────────────────────▶│ 凭证落库(借贷平衡)│
 │               │ ④冲抵: FEE 0 → PENALTY 0 → INTEREST 300 → PRINCIPAL 2700
 │               │ ⑤事件: 还款成功 ──▶ 借据应还更新 ──▶ 额度释放 2700     │
 │ ◀──还款成功────┤ ⑥剩余应还在: 借据仍 ACTIVE, 计息照常推进               │

纪律:账务先行——渠道终态确认后先记账再冲抵;冲抵失败不回滚分录,进补偿队列异步重试。资金已收是渠道终态背书的事实,冲抵只是业务视图更新。先账务后业务的取舍见信贷业务全景与领域模型追问 Q2。

四、全额结清时序 ​

结清 = 一笔把应还清零的交易,难点在试算口径的时效性:

①试算: 结清额 = 本金余额 + 当期利息 + Σ罚息 + 提前结清违约金(减免需审批留痕)
       口径: 利息算到 T 日(算头不算尾, 见账务篇计息规则); 试算单当日有效, 过期重算
②支付: 用户按试算额支付(金额锁定, 多退少不补)
③冲抵: 金额恰好清零 → 借据 CAS: ACTIVE→SETTLED(伴生 settled_at 原子回填)
④联动: 额度释放(循环额度 available 回补) / 结清证明异步生成 / 推送通知
⑤兜底: 渠道延迟到账跨日 → 重算利息出现差额 → 差额挂暂收人工核, 不自动"凑整结清"

提前结清的两个资金敏感点:利息只算到结清日(多收一天都是客诉加监管风险)。违约金减免是产品配置且必须留审批痕迹。减免没有留痕,稽核时就是"资金去向不明"。

五、批量代扣设计:批扣文件 → 执行 → 结果回写 ​

T日 22:00 计息/应还批完成 ──▶ 生成批扣清单(到期未还, 剔除当日已主动还款者)
                            按渠道/银行拆批, 生成批次清单三要素(笔数/金额/校验和)
T+1日 08:30 批次状态机 CREATED → EXECUTING(限速执行, N 笔/秒)
09:00 逐笔代扣指令 ──┬─ 成功 ──▶ 走统一冲抵出口(第三节同一条管线)
                    └─ 失败 ──▶ 记录渠道失败码, 绝不立即盲重
11:00 结果回写: 渠道结果文件/回调 ──▶ 逐笔勾对(指令 vs 回执, 差异进对账)
13:00 失败分类: 余额不足→傍晚二扣 / 卡异常→短信引导主动还 / 渠道故障→熔断顺延
sql
CREATE TABLE batch_deduct_batch (      -- 批次: 状态机 + 清单三要素
  id            BIGINT PRIMARY KEY,
  biz_date      DATE NOT NULL,
  channel_code  VARCHAR(32) NOT NULL,
  total_cnt     INT NOT NULL,
  total_amt     DECIMAL(16,2) NOT NULL,  -- 与明细 SUM 逐日勾对
  status        VARCHAR(16) NOT NULL,  -- CREATED/EXECUTING/PARTIAL/DONE/FAILED
  UNIQUE KEY uk_date_channel (biz_date, channel_code)
);
CREATE TABLE batch_deduct_item (       -- 明细: 幂等键 = 批次 + 明细
  id            BIGINT PRIMARY KEY,
  batch_id      BIGINT NOT NULL,
  iou_id        BIGINT NOT NULL,
  amount        DECIMAL(16,2) NOT NULL,
  status        VARCHAR(16) NOT NULL,  -- INIT/SUCCESS/FAIL/RETRIED
  fail_code     VARCHAR(32),
  UNIQUE KEY uk_batch_item (batch_id, id)
);

批扣三条纪律:

  • 时间窗:银行代收有窗口约束,夜间扣款既是投诉源失败率也高。
  • 限速:渠道合同 QPS 上限,失败率突升自动熔断顺延。
  • 部分成功是常态:批次状态 PARTIAL,只重推失败行,成功行绝不再扣。批次协议同清结算与资方对接的文件批次三要素。

六、失败重试与补偿 ​

失败类型判定处理
余额不足明确失败码傍晚二次批扣(可配置)→ 次日提醒,不无限重试
渠道超时无终态先查单后重试:查单确认失败才允许再扣;查不到挂"在途"进对账
卡状态异常(冻结/销户)明确失败码不重试,转短信/推送引导主动还款
渠道批量故障失败率 > 阈值熔断整批顺延;已扣成功的不回滚

两条铁律:

  • 重复扣款防御:任何重试前必须拿到前次指令的终态。拿不到就查单,查单也超时就挂起人工。"超时盲重试"是还款域第一大资损来源。幂等三层与查单补单的同构见幂等与对账-资金安全底座;
  • 多扣必退:主动还款与批扣并发导致超额冲抵时,溢缴款挂暂收并原路退回。T+1 对账核"暂收余额 = 待退款清单"。

七、异常与降级 ​

故障预案
冲抵引擎不可用入账照常(资金事实优先),冲抵事件积压异步补,用户侧展示"处理中"
账务不可用停收新还款(拒绝进钱比记错钱安全),在途资金挂暂收,恢复后补记
批量任务中途失败批次状态机断点续跑(按明细状态扫描),不整批重来
渠道结果文件未到超时告警 + 逐笔查单兜底;差异累积进对账,绝不按"全失败"处理

八、追问链 ​

Q1:先冲抵还是先记账? 先记账。资金到账是渠道终态背书的事实,冲抵是业务视图。反过来先冲抵后记账,记账失败就会出现"借据说还了、账上说没收钱"。四层核对里的账证核对立刻抓出来。账务是唯一资金事实源(见账务系统设计-复式记账与日切)。

Q2:还款金额大于全部应还,多出来的钱怎么办? 溢缴款挂暂收科目,原路退回或按产品配置抵充未来账单。绝不能把多还的钱冲进"还没发生的利息"——没发生的账不存在,冲了就是预收,计息口径全乱。

Q3:主动还款和批扣同一秒并发同一借据? 两层防御。入口层:批扣清单生成时剔除当日已主动还款的借据(事前规避)。冲抵层:每笔冲抵按应还余额条件更新,第二笔到时应还已被冲掉,自然滑向暂收/退款(事中收敛)。并发窗口内的多扣靠退款兜底,不追求分布式锁互斥。

Q4:为什么冲抵顺序把费用和罚息排在利息本金前面? 监管通常要求先收已产生的费用与罚息(防止"先还本"让罚息基数长期挂账)。这也是出借人利益保护的一致口径。但关键是"通常"——不同时期口径有差异。所以它必须是带版本号的配置而不是代码常量。配置化的成本就是为存量借据保留口径版本。

Q5:结清试算额和实际到账差了一分钱/晚了一小时怎么办? 金额差一分:差额挂暂收,人工核后红冲或补收,不允许"凑整结清"。时间差过夜:利息按实际到账日重算(算头不算尾),多收部分自动退。试算单必须有时效——无限有效的试算单等于把计息口径冻结在过去。

Q6:批扣为什么不全量并发打渠道? 渠道合同有 QPS 上限、批量接口有窗口约束,且失败率随并发毛刺上升。限速执行加失败率熔断,既是"对渠道的礼貌"也是对自己的保护。一次打挂渠道,当天所有还款都进不了账。

九、面试官视角小结 ​

面试官在还款设计里检查四件事:

  • 入口出口观:能不能把多入口收敛到唯一资金管线——每个入口各写一套冲抵逻辑是事故温床;
  • 冲抵顺序:说得出"费用→罚息→利息→本金",更要说得出"为什么配置化、为什么带版本";
  • 时序纪律:账务先行、先查单后重试、部分成功不回滚——三句都是资金安全的一线纪律;
  • 批量的工程感:批次状态机、限速、熔断、断点续跑——说明真跑过日终批量,不是只画过在线链路。

一句话总结:入口多元出口一,账务先行冲抵配,先查终态再重试,批扣限速留退路。

2026-10 新增(金融信贷域 · 系统设计层)。