简介:一套面向海外贷款与信贷业务的完整产品源码包,基于Laravel框架构建,适配Linux CentOS 7.6、宝塔、PHP 7.3与MySQL 5.6环境,根目录为public,采用laravel5伪静态并支持SSL,中英文双语界面,适合需要搭建线上借贷平台、开展二次开发或研究海外信贷系统架构的开发者。压缩包共2000个文件,约181.82MB,前端以1338个JS脚本、218个JSON配置、148个CSS样式表与103个HTML页面为主,且属于编译后的静态产物;后端辅以MD说明文档、SQL数据库脚本等资源,便于理解项目整体结构。数据库入口位于根目录.env文件,修改配置即可对接环境;前端可通过调整默认文档优先访问index.html。目前已有377人学习下载。借助该源码可梳理Laravel项目从public入口到.env配置的部署链路,并参考宝塔环境下伪静态、SSL的落地方式,管理后台界面也能为自建借贷平台提供设计参考,整体适合二次开发或教学研究。
1. 海外借贷平台源码:先想清楚为什么下载下来跑不起来
搜“Home-credit海外贷款信贷产品源码”的人,很多都经历过同一个画面:花一下午把工程跑起来,前端登录正常,后台菜单齐全,真正放一笔款却发现还款计划算不平、入账冲抵顺序写死、逾期罚息只能靠定时脚本硬凑。Home Credit 这类海外借贷产品,不管是 POS 消费分期、现金贷还是循环贷,值钱的源码从来不是页面,而是还款计划引擎、入账分配顺序、账务状态机和幂等交易。这篇文章把这条链路的领域边界讲清楚,给出可运行的还款计划与 APR 计算代码、建表 SQL 和入账分配逻辑,再交代落地最容易翻车的五类场景。适合准备自研信贷系统、正在选型贷款平台软件源码、或者刚接手海外借贷平台项目需要快速对齐核心逻辑的开发与产品。
2. 源码领域的边界:从Home Credit的产品形态拆出核心对象与选型
标题里的“线上贷款产品大全”如果只当成贷款 App 的页面合集,方向就偏了。海外信贷平台真正的产品大全,是产品工厂里一组可配置的参数模板:不同市场、不同客群、不同资金成本,决定了利率口径、还款频率、宽限期和费用结构。动手写代码之前,先把这些边界拆清楚。
2.1 三种产品形态决定三套参数:POS分期、现金贷、循环贷
Home Credit 在海外市场大量铺的是线下商户场景的分期业务,典型就这样三种形态。
- POS 消费分期:客户在商户买手机、家电,当场签约分期还款。金额不高,期限集中在 6 到 24 个月,按月等额本息。
- 现金贷:小额、短期、高频复借。期限短到 7 天、长到 90 天,还款频率可能是周结或月结,利率常用日息表达。
- 循环贷:给客户授信额度,随借随还,按日计息,按月结算,额度可以循环释放。
这三类产品落到参数层,差异非常明显。
| 产品形态 | 计息方式 | 还款频率 | 费用结构 |
|---|---|---|---|
| POS 消费分期 | 年化利率,等额本息 | 按月 | 申请费、商户贴息 |
| 现金贷 | 日利率 / 月利率 | 按周或按月 | 手续费、逾期费 |
| 循环贷 | 按日计息,月内结息 | 按月 | 提现费、账户管理费 |
源码里的产品表,至少要有这些字段:loan_type、repayment_scheme(AMORTIZED / INTEREST_ONLY / REVOLVING)、day_count_basis(ACTUAL/360、ACTUAL/365、30/360)、fee_template、grace_days、timezone。很多团队源码建站成功后第一个坑,就是把利率和费用写死在页面上,产品经理一调息就要发版。正确做法是把这些参数做成配置,一个产品编码对应一条参数记录,线上修改只影响新合同。
2.2 四个核心数据对象与它们的关系
信贷系统的代码量看似庞大,核心数据对象其实只有四个。
- Client(客户):承载 KYC 信息、身份编码、联系方式、信用评级外部编号。
- Product(产品模板):上一节说的利率、期限、费用、还款方式参数。
- LoanAccount(借贷账户):一笔放款对应一个账户,承载合同金额、利率、期限、当前状态。
- Transaction(账务流水):记录本金、利息、费用、罚息的每一次变动。
四个对象的关系是:Client 对 LoanAccount 一对多,Product 对 LoanAccount 一对多,LoanAccount 对 Transaction 一对多。注意一个容易混淆的选择:按客户还是按合同开账户。贷超类的线上平台很多按客户做账户,但信贷核心必须按合同开账户,因为同一客户可以同时持有两笔不同利率、不同期限的分期合同,资金进出要能精确归属到某一笔。
事务表的不可变性特别关键。账务流水只能追加,不能修改删除。一旦允许 UPDATE 某笔本金流水,账务就会变成黑匣子,日终对账永远对不平。正确的习惯是:出错就用红字冲正(负数流水),绝不回头改旧流水。这也意味着主键最好用自增 ID 加业务唯一键,而不是把金额、日期当主键去查重。
2.3 自研、Fineract、混合路线怎么选
海外借贷平台源码选型,常见有三条路。
一是直接上 Apache Fineract。它是 Mifos 生态下开源的信贷核心,天然覆盖贷款、储蓄、客户管理和基础总账,适合中小信贷机构快速起步。但它的问题也明显:数据模型偏传统,自定义利率方案和复杂产品结构时要绕很多弯,前后端耦合老,版本升级往往要跟着做数据迁移。产品团队如果计划频繁调息、多做几个本地化版本,Fineract 的边界很快就会碰到。
二是完全自研。团队有一定规模、要对接多个本地支付网关、要做差异化合规报送时,自研是常态。最常见的技术栈是 Spring Boot + MySQL + Redis + RabbitMQ/Kafka。真正要自己写透的只有四块:还款计划引擎、入账分配、日终批处理、对账。其余客户管理、后台页面、App 接口,都是常规 CRUD。
三是混合路线:拿 Fineract 的 loan_schedule 和 loan_transaction 表结构当参考,重写成自己的模型。既避开它的历史包袱,又不用从零设计账务。这个方案适合有一定 Java 能力的团队,我一般推荐这条路线,因为信贷核心最怕的是自己把状态机设计歪了,参考成熟开源模型能少走很多弯路。
选型考量就看几个参数:上线时间紧不紧、后续产品变更频率高不高、本地支付通道有多少个要接、有没有审计或监管报送的特殊要求。注意,别把风控引擎跟账务核心绑死在同一个服务里,风控规则会频繁调整,账务必须稳定,二者理应在部署上分开。
3. 用Python复现信贷核心:还款计划、APR与借贷账户的源码骨架
做海外借贷平台,先把还款计划引擎和入账逻辑写对,业务才有意义。下面这段是能直接运行的最小骨架,我按从计算到入库的顺序来。
3.1 等额本息还款计划生成器:先跑通最小可用的计算
from datetime import date from decimal import Decimal, ROUND_HALF_UP import calendar def money(value) -> Decimal: # 金额一律转成 Decimal,避免 float 的二进制误差 return Decimal(str(value)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) def add_months(source: date, months: int) -> date: # 跨年、跨月都要正确处理,月底放款不能生成 2 月 31 日 month_index = source.month - 1 + months year = source.year + month_index // 12 month = month_index % 12 + 1 day = min(source.day, calendar.monthrange(year, month)[1]) return date(year, month, day) def monthly_payment(principal: Decimal, annual_rate_pct: Decimal, term: int) -> Decimal: r = annual_rate_pct / Decimal("100") / Decimal("12") n = Decimal(term) if r == 0: return money(principal / n) factor = (1 + r) ** n return money(principal * r * factor / (factor - 1)) def build_amortization_schedule( principal: Decimal, annual_rate_pct: Decimal, term: int, start_date: date, billing_day: int = 15): schedule = [] balance = principal pay = monthly_payment(principal, annual_rate_pct, term) r = annual_rate_pct / Decimal("100") / Decimal("12") for period in range(1, term + 1): interest = money(balance * r) principal_part = pay - interest if period == term: # 尾期把剩余本金结清,月供与前面几期会有一点差异 principal_part = balance interest = money(balance * r) pay_this = principal_part + interest else: pay_this = pay balance -= principal_part due_date = add_months(start_date, period) schedule.append({ "period": period, "due_date": due_date, "payment": pay_this, "interest": interest, "principal": principal_part, "balance_after": balance, }) if balance <= 0: break return schedule这段代码的核心是等额本息公式:月供 = 本金 × 月利率 × (1 + 月利率)^期数 / ((1 + 月利率)^期数 - 1)。每月利息用剩余本金重新计算,月供减去当月利息就是当月还本,最后一期用剩余本金收尾。add_months 手工处理了大小月和跨年,避免 date.replace 直接把月份加到 13 月去。
参数上要注意:月利率是年化利率除以 12 再除以 100;billing_day 是账单日,不是自然月的 1 号,海外很多市场习惯固定到发薪日;尾期月供会与前期不同,这是等额本息的设计行为,不是 bug。
3.2 真实APR:用二分法把名义利率还原成借款成本
很多平台标着年化 18%,客户实际还款成本算下来却远远不止,因为手续费、保险费被排除在宣传利率之外。监管展示口径通常要求真实年化 APR,做法是把放款额和每一期还款当成一组现金流,求内部收益率。
def xnpv(rate: Decimal, amounts, dates) -> Decimal: # amounts[0] 是放款(负数),dates[0] 是放款日 d0 = dates[0] total = Decimal("0") for amount, d in zip(amounts, dates): years = Decimal((d - d0).days) / Decimal("365") total += amount / ((1 + rate) ** years) return total def calc_apr(amounts, dates, low=Decimal("0.0001"), high=Decimal("10")) -> Decimal: f_low = xnpv(low, amounts, dates) f_high = xnpv(high, amounts, dates) if f_low <= 0 or f_high >= 0: raise ValueError("现金流方向不对或区间不含解") for _ in range(100): mid = (low + high) / Decimal("2") f_mid = xnpv(mid, amounts, dates) if f_mid == 0: return mid if f_low * f_mid > 0: # 同号,解在右侧 low = mid else: high = mid return (low + high) / Decimal("2")调用示例:放款 10000,分 12 期每期还款 955,把 500 元手续费摊到放款日扣除。amounts 就是 [-9500, 955, 955, ..., 955],dates 从放款日开始逐月排开,返回的年化利率才是客户真实成本。
这里的关键是现金流口径。放款金额要扣除所有提前收取的费用,每期还款要排除与借贷无关的附加服务费,否则 APR 一样会失真。APR 计算建议单独做成一个服务或工具函数,产品调参和合规报送都要反复用到它。
3.3 借贷账户落库:SQL字段、索引与状态机
还款计划算完,要落到借贷账户表。下面是一份常用的建表语句。
CREATE TABLE loan_account ( id BIGINT AUTO_INCREMENT PRIMARY KEY, external_id VARCHAR(64) NOT NULL, product_id BIGINT NOT NULL, client_id BIGINT NOT NULL, principal_amount DECIMAL(18, 6) NOT NULL, annual_interest_rate DECIMAL(9, 6) NOT NULL, term_months INT NOT NULL, schedule_method VARCHAR(32) NOT NULL, apr_display DECIMAL(9, 6), status VARCHAR(32) NOT NULL, current_principal DECIMAL(18, 6) NOT NULL, current_interest DECIMAL(18, 6) NOT NULL, current_penalty DECIMAL(18, 6) NOT NULL, arrears_days INT NOT NULL DEFAULT 0, disbursement_date DATE, maturity_date DATE, payment_hierarchy VARCHAR(64) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE KEY uk_external_id (external_id), KEY idx_client_status (client_id, status) );字段设计上,external_id 是对接外部支付网关的唯一键,必须唯一索引,否则回调重复时挡不住重复入账。金额类统一 DECIMAL(18,6),展示层再四舍五入到分,避免浮点误差。payment_hierarchy 记录这款产品的入账冲抵顺序,默认就是罚息优先、费用其次、再利息、最后本金。
状态机至少要覆盖这一串:PENDING_APPROVAL → APPROVED → ACTIVE → DELINQUENT → DEFAULT → WRITTEN_OFF,以及 CLAOSED 这个终态。逾期状态要从 ACTIVE 派生出来,不能跟 ACTIVE 平级,否则会出现“账户既正常又逾期”的数据矛盾。状态变更必须走状态机方法,不能直接 UPDATE status 字段绕过校验。
提示:账户建立后,还款计划的每一期都要落成单独子表,不要在代码里实时计算未来期数。合同一旦生效,计划表就是合同事实,后续提前还款、逾期重组都基于这张表来调整。
3.4 入账分配:罚息优先、利息次之、本金最后的Payment Allocation
客户还一笔钱,先冲哪一部分,是信贷系统最容易起争议的地方。国际信贷的常见顺序是罚息、费用、利息、本金,这也是很多监管框架下的默认要求。原因是防止罚息继续滚雪球,也倒逼客户尽快归还本金。
DEFAULT_HIERARCHY = ("penalty", "fee", "interest", "principal") def allocate_payment(payment: Decimal, balances: dict) -> (dict, Decimal): remaining = payment result = {} for bucket in DEFAULT_HIERARCHY: due = balances[bucket] paid = min(due, remaining) result[bucket] = paid remaining -= paid return result, remaining资金进来先看当前账户的罚息余额,有余力再冲费用,然后是利息,最后才触及本金。remaining 如果还有盈余,说明客户在超额还款,这时可以把超额部分冲减未来期次的本金,或者沉淀为预付款,取决于产品参数里的 prepayment 设置。这里不要自己发明顺序,直接做成产品配置项,让产品经理决定每个市场的分配规则。
4. 海外借贷平台核心源码避坑:五类最常见的翻车现场
以下五条都是真实上线和联调里反复出现的问题,不是玄学,是能稳定复现的血泪经验。每条按现象、原因、解决展开,照着排查能省掉大半熬夜时间。
4.1 时区导致的还款到期日错位
现象:东南亚客户看到自己还款日比合同约定早了一天,还款被系统判逾期,催收系统开始发短信。
原因:服务部署在 UTC 时区,还款日按 UTC 计算,而客户所在地是 UTC+7。月份一跨越时区线,本地日期就产生偏移。
解决:产品表强制保存 timezone 字段,还款计划生成时把放款时间转成产品目标时区,再计算到期日。
from zoneinfo import ZoneInfo def due_date_in_product_tz(disbursement_date, product_tz: str, billing_day: int): local_date = disbursement_date.astimezone(ZoneInfo(product_tz)).date() return add_months(local_date, 1).replace(day=billing_day)这里要提醒的是:不要把时区做成全局常量,东南亚、中东、拉美各有各的时区,一个市场一个产品实例时尤其容易漏。还款计划、逾期天数、罚息起算日,全部围绕产品时区计算,界面展示再转客户本地时区。
4.2 金额字段用浮点数,对账总差几分钱
现象:日终报表里总账余额和借贷账户明细差了 0.01 到 0.02,手工核对查不出规律。
原因:代码里用了 float 算利息和罚息。二进制无法精确表示所有十进制小数,0.1 + 0.2 会等于 0.30000000000000004。
解决:金额计算全程 Decimal,并且每步显式圈定精度。
from decimal import Decimal # 错误写法 balance = 1000.0 interest = balance * 0.015 # 可能产生 15.000000000000002 # 正确写法 balance = Decimal("1000.00") interest = (balance * Decimal("0.015")).quantize(Decimal("0.01"))注意 Decimal(str(value)) 是安全的,Decmial(0.1) 不行,因为 0.1 在转成 float 时已经有误差。数据库里 DECIMAL 列不要用 float 类型字段去读,ORM 映射时也要显式声明为对应的小数类型。
4.3 罚息按日累计,日终批处理跑两次就翻倍
现象:同一笔账户,罚息忽然变成应计的 2 倍,恰好发生在重启日终任务那天。
原因:罚息累计逻辑写成“从上一罚息日期到今天全量重算”,但第一次执行成功后没有更新 last_penalty_date,第二次执行等于重复叠加同一区间。
解决:累加函数增加幂等判断,或者给罚息记录加业务日期唯一键。
def apply_daily_penalty(acc, business_date: date): if acc.last_penalty_date == business_date: return days = (business_date - acc.last_penalty_date).days if days <= 0: return daily_rate = acc.annual_penalty_rate / Decimal("360") acc.accrued_penalty += acc.current_principal * daily_rate * days acc.last_penalty_date = business_date批处理任务最重要的素质就是可重入。任何“每日执行一次”的脚本,都必须保证重复执行结果一致。要么用幂等键,要么用业务日期做唯一索引,第二次跑直接跳过。
4.4 等额本息首期或尾期月供失真
现象:客户月初放款和月末放款,第一期月供差异很大,产品经理认为是 bug。
原因:等额本息公式假设每一期长度相等,但实际首期可能只有 15 天,也可能是 45 天。天数差异直接反映在第一期利息。
解决:首期按实际天数计息,剩余期数再按等额本息重算。
def first_period_interest(principal, annual_rate_pct, disbursement_date, first_due_date): days = (first_due_date - disbursement_date).days daily_rate = annual_rate_pct / Decimal("100") / Decimal("360") return money(principal * daily_rate * days)如果产品合同明确规定“每月按 30 天计息”,那就用 30/360 口径,不要混用。关键是一张合同里只能有一种 day count 口径,计算和展示要一致。
4.5 支付回调缺少幂等键,一笔还款记成两笔
现象:客户还款一次,账户却显示两笔入账,贷款直接被提前结清。
原因:支付网关因为超时重发回调,回调处理接口没有按外部流水号去重。
解决:还款事务表加外部流水号唯一索引,入库前先尝试插入。
ALTER TABLE repayment_transaction ADD COLUMN external_ref VARCHAR(64) NULL, ADD UNIQUE KEY uk_ext_ref (external_ref);处理逻辑用“先插后查”代替“先查后插”:直接插入,遇到唯一键冲突就说明这笔回调已经处理过,返回原结果即可。这个套路比 SELECT 再 INSERT 稳妥得多,因为两条并发回调同时打到服务,查询方式依然可能重复记账。
5. 用影子放款验证整条链路:对账公式与三步骤
源码写完,怎么证明它能放款?我的习惯是上影子放款:不让真实资金进场,用历史合同数据把整条链路先跑一遍。具体三步,第一步抽样最近 120 天的历史合同,取 500 到 1000 笔覆盖不同产品、不同利率、不同期限的样本;第二步把这批合同按新系统的还款计划引擎重新生成排期,逐笔模拟每期还款和逾期;第三步每天日终跑一次对账公式。
对账公式本身很简单,核的是本金。
def reconcile_day(book, repayment_principal): expected = book.beginning_principal + book.disbursement - repayment_principal assert abs(expected - book.ending_principal) <= Decimal("0.01")期初本金加当期放款本金,减掉当期归还本金,必须等于期末本金。这个公式不过关,后面利息、罚息一定也过不了。影子放款至少要连续跑满一个完整账期,中间故意插入手工批处理重启、支付回调重发,验证幂等逻辑确实兜得住。
验证通过后再加一个进阶校验:随机挑几笔提前结清和逾期重组的账户,人工用 Excel 重算一遍利息,跟系统输出逐项比对。这一步能抓出大量引擎层面的隐性问题。我现在接手任何信贷项目,第一件事都是让影子放款先跑满一个月,这套验证帮我挡掉了后面大多数线上事故。希望帮到你。
本文还有配套的精品资源,点击获取