组合贷由两笔独立贷款构成:商业贷款和公积金贷款。它们的本金、利率、放款规则和可能的重定价方式都不相同。
因此,组合贷不能把两笔贷款加总后,套一个“平均利率”算月供。正确做法是分别生成两套还款计划,再把同一期的本金、利息、月供和剩余本金逐项相加。
房贷计算器 采用的就是这个口径。
为什么平均利率不可靠
假设:
- 商贷本金
P_c,年利率R_c - 公积金贷款本金
P_g,年利率R_g - 贷款期限均为
n期
很多实现会先算一个加权平均利率:
[
R_{avg} =
\frac{P_c \times R_c + P_g \times R_g}
{P_c + P_g}
]
再把 P_c + P_g 与 R_avg 代进等额本息公式。
这个方法看起来合理,但不是精确计算。
等额本息月供公式中包含幂函数:
[
A =
\frac{P \times r \times (1+r)^n}
{(1+r)^n - 1}
]
利率与本金之间不是线性关系。两个不同利率的还款计划相加,不等于先平均利率,再生成一份计划。期限越长、两种利率差距越大,误差越明显。
正确的数据模型:两笔贷款,两套计划
先定义单笔贷款输入:
type LoanInput = {
principal: number;
annualRate: number;
months: number;
method: "annuity" | "principal";
};
还款计划的每一行都保留本金、利息和余额:
type ScheduleRow = {
period: number;
payment: number;
principal: number;
interest: number;
balance: number;
};
组合贷不需要另一套复杂公式。它只需要复用单笔贷款的计算器两次:
const commercial = calculateLoan({
principal: commercialAmount * 10_000,
annualRate: commercialRate / 100,
months,
method,
});
const provident = calculateLoan({
principal: providentAmount * 10_000,
annualRate: providentRate / 100,
months,
method,
});
这里的 months 在基础版本中相同。若商贷和公积金贷款期限不同,后面需要按日期或期数补零合并,不能直接按数组下标相加。
逐月合并才是组合贷计算的主体
两份计划生成后,第 i 期组合月供等于两笔贷款第 i 期月供之和:
[
paymenti =
payment{c,i} + payment_{g,i}
]
同样地:
[
principali =
principal{c,i} + principal_{g,i}
]
[
interesti =
interest{c,i} + interest_{g,i}
]
[
balancei =
balance{c,i} + balance_{g,i}
]
实现可以保持很直接:
const schedule = commercial.schedule.map((row, index) => {
const providentRow = provident.schedule[index];
return {
period: row.period,
payment: row.payment + providentRow.payment,
principal: row.principal + providentRow.principal,
interest: row.interest + providentRow.interest,
balance: row.balance + providentRow.balance,
};
});
这样导出的还款计划可以回答用户真正关心的问题:
- 第 1 期总共需要准备多少钱;
- 已还 60 期后还剩多少本金;
- 某一期利息中,商贷和公积金贷款各占多少;
- 提前还款时到底该对哪一部分本金操作。
一个可复核的算例
设:
| 项目 | 商贷 | 公积金贷款 |
|---|---|---|
| 本金 | 70 万元 | 30 万元 |
| 年利率 | 3.5% | 2.6% |
| 期限 | 30 年 | 30 年 |
| 还款方式 | 等额本息 | 等额本息 |
单独计算后:
| 指标 | 商贷 | 公积金贷款 | 合并后 |
|---|---|---|---|
| 首期月供 | 3143.31 元 | 1201.02 元 | 4344.33 元 |
| 首期本金 | 1101.65 元 | 551.02 元 | 1652.67 元 |
| 首期利息 | 2041.67 元 | 650.00 元 | 2691.67 元 |
| 总利息 | 43.16 万元 | 13.24 万元 | 56.40 万元左右 |
组合后的首期月供不是“100 万元按某个平均利率”的月供,而是 3143.31 元与 1201.02 元相加的结果。
这也是为什么组合贷计算器应保存两套底层计划。只展示一个汇总结果,后续很难支持查余额、提前还款或利率调整。
不同期数时,不能继续按数组下标合并
现实中,商贷和公积金贷款的放款日、还款日或期限可能不同。
例如:
- 商贷剩余 240 期;
- 公积金贷款剩余 180 期。
此时第 181 期开始,公积金贷款应视为 0,不是访问不存在的数组项:
const length = Math.max(
commercial.schedule.length,
provident.schedule.length,
);
const schedule = Array.from({
length }, (_, index) => {
const c = commercial.schedule[index];
const p = provident.schedule[index];
return {
period: index + 1,
payment: (c?.payment ?? 0) + (p?.payment ?? 0),
principal: (c?.principal ?? 0) + (p?.principal ?? 0),
interest: (c?.interest ?? 0) + (p?.interest ?? 0),
balance: (c?.balance ?? 0) + (p?.balance ?? 0),
};
});
这只是按“期数对齐”的简化版本。
如果两笔贷款实际还款日不同,例如一笔每月 5 日扣款,另一笔每月 20 日扣款,最稳妥的模型应以真实还款日期为键生成现金流。否则“第 12 期”只是数组序号,不一定对应同一个自然月的资金压力。
因此,基础工具适合在两部分期限和还款节奏一致时合并;实际放款日、扣款日或期限不一致时,应分开试算,再按实际日期汇总。
等额本金也能用同一套合并逻辑
若两部分都选择等额本金,单笔计划的月供会逐月递减。组合计划不需要改公式,仍然按期相加。
const payment =
commercialRow.payment +
providentRow.payment;
区别仅在于每一行的来源数据变了:
- 等额本息:两部分各自月供固定;
- 等额本金:两部分各自月供递减;
- 合并后:总月供遵循两部分月供的叠加结果。
不要因为组合贷存在两种利率,就强制使用等额本息。还款方式是每笔贷款计划的属性,而不是组合公式的一部分。
舍入位置会影响尾差
金额显示需要保留两位小数,但不应该在每一步都拿显示值继续计算。
更稳妥的方式是:
- 单笔计划内部保留未舍入金额;
- 合并后再对展示字段保留两位小数;
- 最后一期用剩余本金兜底归零;
- 汇总时优先从内部原始值累计。
const money = (value: number) =>
Math.round((value + Number.EPSILON) * 100) / 100;
const merged = {
payment: money(c.payment + p.payment),
principal: money(c.principal + p.principal),
interest: money(c.interest + p.interest),
balance: money(c.balance + p.balance),
};
否则可能出现一个让用户困惑的现象:逐期月供合计、本金合计、利息合计之间差几角钱甚至几元。
组合贷计算需要锁住的测试
test("组合首期月供等于两部分首期月供之和", () => {
const commercial = calculateLoan({
principal: 700_000,
annualRate: 0.035,
months: 360,
method: "annuity",
});
const provident = calculateLoan({
principal: 300_000,
annualRate: 0.026,
months: 360,
method: "annuity",
});
const merged = mergeSchedules(
commercial.schedule,
provident.schedule,
);
assert.equal(
merged[0].payment,
commercial.schedule[0].payment +
provident.schedule[0].payment,
);
});
test("组合计划的最终剩余本金为零", () => {
const merged = mergeSchedules(
commercialSchedule,
providentSchedule,
);
assert.equal(merged.at(-1).balance, 0);
});
test("期限不一致时,已结清部分不再产生月供", () => {
const merged = mergeSchedules(
scheduleWith240Months,
scheduleWith180Months,
);
assert.equal(merged[200].payment, scheduleWith240Months[200].payment);
});
组合贷的正确实现并不在于找到一个“平均利率”。商贷和公积金贷款各自算清楚,再逐月合并,才能让月供、利息、剩余本金和后续操作都经得起核对。