很多人以为提前还款后,直接用“原贷款金额减去提前还的金额”再算一次月供就行。这样算错的概率很高。
等额本息贷款已经还了几年后,月供里本金和利息的比例变了。此时真正需要处理的是“当前剩余本金”,不是原始贷款本金。提前还款计算器的核心流程只有一句话:先复原原还款计划,找到还款当期的余额,再基于新余额生成新的计划。
本文以 提前还款计算器 为例,拆解“降低月供”和“缩短期限”两条路径的算法。
第一步:先算出提前还款时的剩余本金
设:
- 原贷款本金为
P - 月利率为
r - 原总期数为
n - 已正常还款期数为
t - 原固定月供为
A
等额本息月供为:
[
A = \frac{P \times r \times (1+r)^n}{(1+r)^n - 1}
]
第 k 期先用期初余额计算利息:
[
interestk = balance{k-1} \times r
]
再由月供减去利息,得到当期归还本金:
[
principal_k = A - interest_k
]
最后更新余额:
[
balancek = balance{k-1} - principal_k
]
代码里,最可靠的方式是生成完整计划后取第 t 期余额:
const original = calculateLoan({
principal,
annualRate,
months,
method: "annuity",
});
const balance = original.schedule[paidMonths - 1]?.balance ?? principal;
不要用 原贷款金额 - 已还月供 × 期数。月供中大部分并不是本金,尤其是在贷款前几年。
举个统一案例:贷款 100 万元、年利率 3.5%、期限 30 年,正常还款 60 期后,剩余本金约为 89.70 万元,并不是“100 万减去 60 个月供”后的数字。
第二步:从剩余本金中扣除本次提前还款
设本次实际提前归还本金为 H:
[
B' = B - H
]
其中 B 是提前还款时的剩余本金,B' 是提前还款后的新本金。
实现时要做上限保护。用户输入的提前还款金额不能超过剩余本金:
const prepay = Math.min(balance, inputPrepay);
const newBalance = Math.max(0, balance - prepay);
如果 newBalance 已经为 0,就走“提前结清”分支,不再生成后续月供计划。
if (newBalance === 0) {
return {
payoffPrincipal: balance,
penalty,
totalOutlay: balance + penalty,
};
}
这里还有一个容易遗漏的现实因素:违约金。工具按“提前归还本金 × 违约金比例”计算:
[
penalty = H \times penaltyRate
]
是否收取、怎么收取、能否部分提前还款,都应以合同和办理银行的实际规则为准。计算器只能把用户已知的违约金规则带入试算。
路径一:期限不变,降低月供
假设原来还剩:
[
m = n - t
]
期数不变时,只要把新本金 B' 和剩余期数 m 重新代入等额本息公式:
[
A' = \frac{B' \times r \times (1+r)^m}{(1+r)^m - 1}
]
代码很短:
const revised = calculateLoan({
principal: newBalance,
annualRate,
months: originalMonths - paidMonths,
method: "annuity",
});
在前面的案例中,已经还款 60 期后提前归还 20 万元,且不计违约金:
- 提前还款后本金约 69.70 万元
- 剩余期限仍是 300 期
- 新月供约 3489.20 元
- 预计少付利息约 10.04 万元
这条路径适合希望立刻降低每月支出的人。利息会减少,但仍按原来的剩余期限慢慢还。
路径二:月供基本不变,缩短期限
“缩短期限”不能简单地把剩余期数按提前还款比例缩短。
正确做法是保持目标月供 A,反推新本金 B' 需要多少期还完:
[
m' = \frac{-\ln(1 - \frac{B' \times r}{A})}{\ln(1+r)}
]
期数必须是整数,因此实际实现要向上取整:
const targetPayment = original.schedule[0].payment;
const newMonths =
monthlyRate === 0
? Math.ceil(newBalance / targetPayment)
: Math.ceil(
-Math.log(1 - (newBalance * monthlyRate) / targetPayment) /
Math.log(1 + monthlyRate),
);
得到 newMonths 后,还要重新生成一遍计划:
const revised = calculateLoan({
principal: newBalance,
annualRate,
months: newMonths,
method: "annuity",
});
重新生成这一步不能省。因为向上取整后,精确月供通常会比原月供略低;如果只展示反推期数,不校验新计划,最后一期很容易出现余额或金额对不上的问题。
同样是还款 60 期后提前还 20 万元:
- 提前还款后本金约 69.70 万元
- 剩余期限约 207 期
- 新计划首期月供约 4489.85 元
- 预计少付利息约 21.77 万元
和降低月供相比,缩短期限没有把每月压力明显降下来,但节省的利息更多。
节省利息应扣除违约金
提前还款的节省金额,不应只比较两个计划的总利息。若存在违约金,应扣除:
[
saved =
futureInterest{old}
-
interest{new}
-
penalty
]
对应代码:
const oldFutureInterest = original.schedule
.slice(paidMonths)
.reduce((sum, row) => sum + row.interest, 0);
const saved = Math.max(
0,
oldFutureInterest - revised.totalInterest - penalty,
);
Math.max(0, ...) 的意义是避免页面显示负数“节省利息”。如果违约金已经高于利息节省,更适合展示为“本次试算未节省利息”,再提示用户重新核对合同条款。
三类边界条件不能漏
1. 零利率
当月利率为 0 时,等额本息公式会出现除零。此时应退化为平均归还本金:
const payment =
monthlyRate === 0
? principal / months
: calculateAnnuityPayment(principal, monthlyRate, months);
缩短期限时也不能再使用对数公式,而是:
const newMonths = Math.ceil(newBalance / targetPayment);
2. 提前结清
如果提前还款金额大于等于剩余本金,应按结清处理。页面应展示结清本金、违约金和本次总支出,而不是继续给出一个“新月供 0 元、剩余 0 期”的空计划。
3. 提前还款日期
上面的模型假设提前还款发生在某一期正常还款之后。真实业务里,可能发生在两个还款日之间,银行会按日计息;有些产品还会规定预约时间、最低还款金额或每年办理次数。
这部分不该偷偷塞进月供公式。工具应明确说明自己的时间假设,避免把试算结果包装成结算金额。
用测试把两条路径锁住
这类功能至少应覆盖以下测试:
test("降低月供后剩余期数不变", () => {
const result = prepay({
strategy: "reduce-payment",
paidMonths: 60,
prepay: 200_000,
});
assert.equal(result.schedule.length, 300);
assert.ok(result.firstPayment < originalPayment);
});
test("缩短期限后月供接近原月供", () => {
const result = prepay({
strategy: "reduce-term",
paidMonths: 60,
prepay: 200_000,
});
assert.ok(result.schedule.length < 300);
assert.ok(Math.abs(result.firstPayment - originalPayment) < 1);
});
test("结清时不生成后续还款计划", () => {
const result = prepay({
strategy: "payoff",
paidMonths: 60,
});
assert.equal(result.schedule, undefined);
assert.ok(result.totalOutlay > 0);
});
提前还款工具真正需要回答的不是“提前还多少划算”,而是把同一笔剩余本金放进两套规则里:一套换取更低的月供,另一套换取更短的还款时间。算法把这两件事分开,页面上的结果才不会混在一起。