ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

海外贷款信贷系统PHP源码拆解:还款计划与支付回调的工程实现

海外贷款信贷系统PHP源码拆解:还款计划与支付回调的工程实现 简介这是一套基于Laravel框架的海外信贷贷款产品源码面向金融科技开发者、借贷平台运营方及有海外放贷业务需求的技术团队。系统支持中英文界面前端为编译后静态页面后端提供完整PHP业务逻辑可快速搭建线上贷款平台。压缩包共2000个文件约181.82MB其中1338个JS负责前端交互148个CSS负责样式103个HTML构成页面骨架218个JSON存储配置179个MD文档说明另附1个SQL脚本。资源以代码和前端资源为主。部署环境采用CentOS7.6、宝塔、PHP7.3与MySQL5.6根目录为public需开启SSL与Laravel5伪静态。数据库参数在根目录.env中修改前端需将index.html设为优先访问这些要点能减少环境配置时间。已有377人学习适合具备PHP基础、需要部署或二次开发海外借贷产品的开发者参考。1. 海外贷款信贷产品源码能跑的借贷闭环长什么样说到海外贷款信贷产品源码很多人第一反应是“这就是个放款后台”。我拆完这份 Home-credit 海外借贷平台的完整 PHP 源码包之后第一个想纠正的就是这个判断它真正花心思的地方不在放款而在还钱。还款计划怎么生成、逾期怎么判、罚息怎么算、第三方支付回调怎么保证不重复入账这些才是信贷系统里最容易翻车的环节。整套源码覆盖贷前申请、贷中审批、贷后对账的完整闭环既有 C 端 H5/App 接口也有给运营、风控、财务用的独立管理后台。适合想把海外现金贷或分期贷快速搭起来的技术团队、接外包单需要业务底稿的开发者以及想看懂信贷业务建模的转行程序员。下面我按代码包的实际结构把链路、参数和坑逐个拆开。2. 系统架构拆解目录结构、订单状态机与用户生命周期这份源码是典型的 PHP 全家桶工程入口比普通业务系统多因为它同时服务 C 端用户、内部运营和第三方支付回调三方角色。拆包时先别急着看业务代码把入口和状态机理清楚后面所有的审批逻辑、对账逻辑都是围着它们转的。2.1 目录结构与模块归属代码包解压后是一个标准的多入口目录结构我拆的时候第一件事就是确认每个目录对谁开放home-credit/ ├── api/ # C端接口注册、申请、主动还款 │ ├── register.php # 手机号注册登录 │ ├── apply.php # 借款申请入口 │ ├── repay.php # 主动还款 │ └── callback/ # 第三方支付回调目录 ├── admin/ # 管理后台运营、风控、财务 ├── common/ # 公共层DB、Redis、金额计算、签名 ├── config/ # 数据库/短信/支付/环境参数 ├── bin/ # 定时任务逾期扫描、还款扣款 └── database.sql # 初始化脚本这个分层逻辑很清楚api只对 C 端流量开放admin必须做登录鉴权和操作审计callback目录是对第三方支付渠道开放的不能套用用户登录态。common里放的是所有模块共用的东西我重点看了金额精度处理和签名校验工具这两个文件后面排坑时反复被翻到。2.2 订单状态机贷前贷中贷后怎么流转信贷系统不像电商系统那样订单只有“待付款、已付款、已发货”几个状态贷款订单的状态流转是带着动作的。核心订单表长这样CREATE TABLE loan_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT 业务单号展示给用户, user_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, amount_cents BIGINT NOT NULL COMMENT 借款金额单位分, term_months TINYINT NOT NULL COMMENT 期数3/6/12期, status ENUM(pending,approved,rejected,loaned,repaying,overdue,settled,bad_debt) NOT NULL DEFAULT pending, risk_score TINYINT DEFAULT NULL COMMENT 风控评分null表示未出结果, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;amount_cents用分做单位这是金融系统的铁律直接用浮点数存金额后面对账必炸。状态流转是pending → approved → loaned → repaying → settled任何一环出问题都会走rejected或overdue分支。overdue超过 N 天会进入bad_debt核销池这个 N 值在配置文件里不同产品线可以单独调。我建议把状态流转图打印出来贴在工位上。审批通过不等于放款成功loaned状态必须由支付回调触发而不是运营手动点按钮——这一点在源码里体现得很直接后台的“确认放款”按钮实际是触发一次放款申请真正改状态的是回调接口。2.3 用户生命周期与授信边界用户表里有个容易被忽略的credit_limit_cents字段它是授信额度和订单表里的借款金额不是一回事。新用户注册后额度不会直接拉满源码里有 bootstrap 首借逻辑首次借款上限通常只有 1000 到 3000 单位按时还款后才逐步提升。// 提额逻辑每结清一单按表现分加额度 if ($order-status settled $order-paid_days $order-term_months) { $user-credit_limit_cents bcadd( $user-credit_limit_cents, bcmul($order-amount_cents, 0.10, 0), 0 ); $user-level min($user-level 1, 5); $user-save(); }bcadd和bcmul是 PHP 的精度计算函数源码里所有金额操作都走这两个函数没有人用原生 - * /处理钱。这个细节值得学特别是后面排查对账不平的时候你基本可以断定是有人绕过了 common 层的金额工具类自己写了运算。3. 申请与风控模块审批规则、反欺诈与额度计算申请流程是整个系统里链路最长的一段从用户点“借款”到资金到账中间要穿过参数校验、订单创建、风控评分、人工复核、放款回调五个环节。源码把风控做成了可配置的规则引擎不需要改代码就能调策略这是这套系统比较值钱的地方。3.1 申请接口的完整链路与并发保护apply.php不是简单 insert 一条记录它做了两层保护。第一层是用户维度存在 pending 或 approved 状态的订单时直接拒绝新申请避免用户同时申请多笔借款造成超额负债。第二层是数据库维度订单创建放在事务里配合业务单号唯一索引兜底。public function apply($request) { $user auth($request-token); // 校验用户是否已有进行中的订单 $pending LoanOrder::where(user_id, $user-id) -whereIn(status, [pending, approved]) -exists(); if ($pending) { return fail(存在未完成订单请先结清后再申请); } // 创建订单并进入异步风控队列 DB::transaction(function () use ($user, $request) { $order LoanOrder::create([ user_id $user-id, product_id $request[product_id], amount_cents bcmul($request[amount], 100, 0), term_months $request[term], status pending, ]); RiskQueue::push($order-id); // Redis 异步队列不阻塞用户请求 }); return success(申请已提交请等待审批结果); }注意bcmul($request[amount], 100, 0)前端传过来的是元这里转成分第三参数0表示结果保留 0 位小数直接截断而不是四舍五入。RiskQueue::push把风控计算放进 Redis 队列异步消费这样用户提交申请后 2 秒内就能得到回执不用干等评分结果。我见过不少团队把风控计算写在申请接口里同步执行用户请求动辄等 5 秒以上体验极差。3.2 风控规则引擎与反欺诈字段风控规则存放在risk_rule表里每条规则是一个键值对判断运营人员能在后台直接添加或禁用规则不必改代码。源码里的执行逻辑大致是$rules RiskRule::where(status, 1)-orderBy(weight, desc)-get(); $score 0; foreach ($rules as $rule) { $profile $user-profile; // 用户画像手机号、设备、IP、实名信息 $value $profile[$rule-field] ?? null; if ($rule-operator eq $value $rule-threshold) { $score $rule-hits_score; } if ($rule-operator gte $value $rule-threshold) { $score $rule-hits_score; } } // 分数低于阈值直接拒绝进入人工复核区间则转 admin 队列规则表的字段设计值得抄field是画像字段名operator是 eq/gte/ltethreshold是阈值hits_score是命中的加减分weight控制执行顺序。反欺诈的玩法都藏在hits_score为负值的规则里同一设备 24 小时内注册超过 3 个账号扣 30 分同一 IP 关联超过 5 个借款申请直接扣到拒绝线以下手机号命中黑名单强制拒绝。这些规则独立成表的好处是调策略不用发版运营改完立即生效。3.3 利率与还款计划计算利率计算是信贷系统的门面等额本息和先息后本的公式看起来简单落到代码里有几个精度细节必须处理。源码里等额本息月供计算用的是标准金融公式function monthly_payment($principalCents, $annualRate, $months) { $monthRate bcdiv($annualRate, 1200, 6); // 年利率转月利率保留6位 $pow pow(1 $monthRate, $months); $pmt bcdiv( bcmul($principalCents, bcmul($monthRate, $pow, 6), 6), $pow - 1, 0 ); return $pmt; }这里最容易翻车的是bcdiv($annualRate, 1200, 6)年利率是百分数所以除以 100 再除以 12得到月利率。第三参数 6 表示中间计算保留 6 位小数避免浮点误差累积。最后一步bddiv(..., 0)把月供规整到整数分多出来的零头在最后一期补齐。产品配置里还有两种计息方式的选择参数等额本息先息后本月供每期固定前 N-1 期只还利息月供构成本金占比递增末期一次性还全部本金适合场景分期贷短期周转贷源码入口monthly_payment()interest_only_payment()源码里两种函数都存在按产品表的repay_type字段切换。无论选哪种金额都必须走bc*函数否则账目对不上是迟早的事。4. 后台管理与还款对账权限模型、放款回调与罚息计算管理后台是运营、风控、财务三个角色共用的权限模型做不好后面审计的时候会非常被动。这一章的代码量不大但数据敏感度和一致性要求是最高的。4.1 管理后台权限模型后台菜单表和权限表是标准的 RBAC 结构admin_user、admin_role、admin_menu三张表。角色设置上源码默认给了一个相对靠谱的切分角色可操作模块敏感操作运营用户查询、订单查看、公告维护无直接资金操作风控规则配置、订单审批审批通过/拒绝财务放款审核、还款对账确认放款、手工调账财务角色的“确认放款”按钮实际上是发起一笔放款请求真正的状态变更在支付回调里。这样设计的好处是就算财务误点了一百次确认没有支付渠道的回调确认订单状态永远不会变成loaned这个安全边界值得借鉴。4.2 还款计划生成与罚息计算放款回调成功后系统会立即生成一整期的还款计划而不是每天实时计算。repay_schedule表里每一期是一条记录包含due_date、principal_cents、interest_cents、status字段。生成逻辑里有个尾差处理function generate_schedule($order, $pmt) { $balance $order-amount_cents; $monthRate bcdiv($order-annual_rate, 1200, 6); for ($i 1; $i $order-term_months; $i) { $interest bcmul($balance, $monthRate, 0); $principal bcsub($pmt, $interest, 0); if ($i $order-term_months) { $principal $balance; // 最后一期补齐本金尾差 $pmt bcadd($principal, $interest, 0); } // 写入 repay_schedule 表 $balance bcsub($balance, $principal, 0); } }每次计算利息时都按剩余本金算最后一期把剩余本金一次性收干净。如果不做这个尾差处理由于每期bcmul(..., 0)的截断最后一期会出现几厘钱的差额用户还款后订单永远无法变成settled这是对账系统最常见的故障源。4.3 第三方支付回调幂等处理支付回调是所有信贷系统里并发压力最大、也是最容易出问题的接口。渠道方可能会因为网络超时重发回调也可能因为用户重复点击还款触发两笔扣款。源码里幂等处理非常有代表性$tradeNo $_POST[trade_no]; // 先查重 $exists RepayRecord::where(trade_no, $tradeNo)-exists(); if ($exists) { return SUCCESS; // 重复回调直接吞掉不落账 } DB::transaction(function () use ($_POST, $order) { // 落还款记录 变更订单状态 释放授信额度 RepayRecord::create([/* 渠道、金额、时间 */]); $order-status settled; $order-settled_at date(Y-m-d H:i:s); $order-save(); $user-increment(credit_limit_cents); // 额度恢复 });除了业务层先查后插repay_record表的trade_no字段还加了唯一索引。哪怕两个并发请求同时走到$exists判断都返回 false数据库层也会让第二个插入失败事务回滚后不会产生脏数据。这个“业务幂等 数据库唯一索引”的双保险是金融系统处理回调的标准姿势。5. 部署避坑与常见问题排查环境、时区、回调、精度四类坑源码能跑起来只是第一步真正折磨人的是部署和长期运行的问题。我按实际踩过的坑梳理了最常见的四类问题每一条都是“现象→原因→解决”的路径照着排查能省不少时间。5.1 部署路径与环境参数部署到海外服务器上的流程比较固定前提是 PHP 7.4、MySQL 5.7、Redis 6并确认安装了bcmath和redis扩展# 1. 安装基础环境 apt install -y nginx mysql-server php-fpm php-mysql php-redis php-bcmath # 2. 导入数据库 mysql -u root -p database.sql # 3. 复制并修改环境配置 cp .env.example .env vi .env # 填入数据库、Redis、短信与支付参数 # 4. 配置 Nginx 站点指到 api/ 目录 nginx -t nginx -s reload # 5. 启动异步队列消费 php bin/queue.php work.env里最关键的是这几个参数参数说明常见错误DB_HOST/DB_NAME数据库连接用了 localhost 但 PHP-FPM 走 socket 时连不上REDIS_HOST风控队列和限流依赖 Redis忘记启动服务导致申请接口卡死PAYMENT_APP_ID/SECRET支付渠道密钥密钥带特殊字符时未用引号包裹CALLBACK_URL支付回调地址用了 IP 而渠道要求 HTTPS 域名APP_TIMEZONE业务时区默认 UTC 导致还款日偏一天我一般建议部署完成后先跑一遍php bin/queue.php work看日志有没有报错再调接口走通一遍申请流程最后才接真实支付渠道。5.2 还款日计算总是差一天现象用户反馈还款日显示和合同差一天逾期判定也不准。原因php.ini里date.timezoneUTC订单创建和还款计划生成用的都是 UTC 时间而用户在东南亚时区凌晨 2 点前提交的申请会被记成前一天。解决把.env的APP_TIMEZONE设为业务所在地区并在代码入口统一date_default_timezone_set(getenv(APP_TIMEZONE))同时把数据库连接串加上timezone参数保证 MySQL 端和 PHP 端一致。5.3 同一笔还款入账两次现象用户还款后订单变成了settled但财务对账发现还款记录有两条金额翻倍。原因支付渠道重发了回调而repay_record表里没有对渠道交易号建唯一索引业务层的查重被并发请求绕过。解决给repay_record.trade_no加UNIQUE KEY再把回调处理包进事务里违反唯一索引时自动回滚。这是纯数据库层面的兜底这时候别指望业务代码判断万无一失。5.4 短信接口被刷导致费用爆炸现象上线当天短信账单爆了注册接口被脚本轰炸每条验证码都有成本。原因/api/register接口没有限流攻击者用脚本循环请求每次后端都真实调用了短信服务商。解决在注册和登录接口加 Redis 限流同一 IP 每分钟最多 5 次同一手机号每天最多 10 次验证码连续输错 3 次后锁定半小时。代码写起来很简单但要记得把限流逻辑放在签名校验之后、真正调用短信服务的逻辑之前。5.5 分页对账时金额不平现象还款计划表各期本金之和与放款金额差了几毛钱。原因生成还款计划时中间过程用了浮点运算而不是全程bc*函数导致每期截断的误差累积到最后一期。解决所有金额字段一律用BIGINT存分PHP 侧只用bcadd/bcsub/bcmul/bcdiv且中间步骤保留 6 位小数、最终结果截断到 0 位。金额相关的代码尽量收敛到一个公共类里不要散落在各个控制器中。6. 进阶验证用一条脚本跑通“注册→申请→放款→还款→逾期”全流程信贷系统最怕的不是单点故障而是状态流转被改乱。我通常会把端到端的流程自动化成一个脚本每次改完代码先跑一遍再上环境#!/bin/bash BASEhttp://127.0.0.1 PHONE${1:-19900000001} # 1. 注册并登录 TOKEN$(curl -s -X POST $BASE/api/register \ -d phone$PHONEcode123456 | jq -r .data.token // empty) # 2. 提交借款申请 ORDER_NO$(curl -s -X POST $BASE/api/apply \ -H Authorization:$TOKEN \ -d amount1000term3 | jq -r .data.order_no // empty) # 3. 后台审批通过 curl -s -X POST $BASE/admin/approve \ -d order_no$ORDER_NO /dev/null # 4. 模拟放款回调触发状态变更 curl -s -X POST $BASE/api/callback/loan \ -d order_no$ORDER_NOtrade_noTEST$RANDOM # 5. 把还款日改成昨天触发逾期扫描 php bin/cron.php check_overdue --force # 验证订单表状态应从 pending → approved → loaned → overdue mysql -u root -p -e \ select order_no, status, amount_cents from loan_order where order_no $ORDER_NO;这个脚本的每一步都对应一个状态检查点注册后能拿到 token申请后订单状态是pending审批后是approved放款回调触达后是loaned逾期扫描后变成overdue。如果哪一步状态不对说明对应的控制器或定时任务出了问题。我还会把重复执行回调的命令跑一遍确认幂等逻辑生效订单状态不会被二次回调改坏。我第一次独立改逾期任务的时候把overdue和settled的判定顺序写反了结果已结清订单被重新标记成逾期罚息算了一遍还把催收队列给激活了。从那以后我每次动订单状态机都强制跑一遍这个脚本确认状态流转无误后再上环境。希望帮到你。本文还有配套的精品资源点击获取
返回列表