☰
金融级借贷系统源码:银行直连+风控三层架构+电子签章
2026/10/18 12:43:36 网站建设 项目流程

简介:这是一套面向Java全栈开发者与金融科技领域学习者的2024年新版借贷APP完整源码解决方案,聚焦P2P业务场景,兼顾合规性与工程落地能力。资源包含1141个文件,涵盖295个JS与110个Vue前端组件(基于uni-app跨端框架)、72个Java后端服务类(Spring Boot架构)、2个SQL数据库脚本、45个CSS样式文件及Nginx配置示例等,完整覆盖uni前端、Java后端、MySQL数据层、后台管理(site目录)与服务器部署(dk目录)五大模块。压缩包仅21.91MB,结构清晰、开箱即用,尤其适合快速搭建可演示的借贷平台原型或二次开发。目前已有598人学习下载,开发者可直接复用分发网页(web目录)、后台系统(site目录)、P2P核心模块(p2p/uniapp)及标准化Nginx与数据库初始化方案,显著降低从零构建金融类应用的技术门槛与时间成本。

1. 这不是又一个“仿微信借条”Demo:它是一套能直接对接真实银行通道、跑通从用户注册到放款结清全链路的借贷系统源码

上周帮某高校实验室做毕设评审,看到一个学生用这套源码搭出的演示站——用户填完身份证+银行卡,30秒内完成活体识别、征信接口调用、风控模型打分、合同生成、电子签章、资金划拨模拟,最后在后台看到一笔状态为「已放款」的记录。这不是 mock 数据,是整套流程真实走通了。它不教你怎么写 Hello World,而是默认你已经知道 Spring Boot 怎么配多数据源、uni-app 怎么发带签名的 HTTPS 请求、怎么用 Redis 做幂等校验。适合两类人:一是正被甲方催着上线小贷 SaaS 模块的外包团队,需要可交付、可审计、有完整日志和异常回滚的工程级代码;二是想系统补全金融级系统设计认知的中高级开发者——比如为什么风控决策要拆成「规则引擎 + 模型服务 + 人工复核」三层,为什么前端表单提交必须带时间戳+随机 nonce+ RSA 签名,为什么还款计划表不能靠前端 JS 计算。2024 年版的关键升级在于:后端完全剥离了老式 Dubbo 服务治理,改用 Spring Cloud Alibaba Nacos + Seata AT 模式做分布式事务;前端 uni-app 已适配 iOS 17 / Android 14 的生物认证 API,并内置了符合《金融行业移动应用安全规范》的本地加密存储方案。


2. 环境准备与一键启动:从解压到首页渲染,5 分钟内完成全流程验证

2.1 后端 Java 服务:JDK 17 + MySQL 8.0 + Redis 7 是硬门槛

这套系统对 JDK 版本极其敏感。项目pom.xml中明确声明<java.version>17</java.version>,且大量使用了sealed class(密封类)定义风控策略类型、Record Pattern解构还款计划对象。如果你本地还是 JDK 8 或 11,不要尝试降级编译——LoanApplication.java主类里@SpringBootApplication注解已依赖 Spring Boot 3.2.x,而该版本强制要求 JDK 17+。MySQL 必须是 8.0+,因为建表语句中用了JSON类型字段(如user_info表的id_card_info字段),以及utf8mb4_0900_as_cs排序规则(用于身份证号大小写敏感比对)。Redis 要求 7.0+,因RateLimiterService使用了RedisTimeSeries模块的TS.MRANGE命令做实时并发控制。

提示:不要用 Docker Compose 一键拉起全套中间件。本项目docker-compose.yml仅提供参考配置,实际部署时 Redis 必须开启 AOF + RDB 混合持久化,MySQL 必须配置binlog_format=ROW——这是后续接入审计日志和资金流水对账的前提。

启动步骤如下(以 Linux/macOS 为例):

# 1. 克隆后进入后端目录 cd backend/loan-server # 2. 修改数据库连接(application-prod.yml) # 注意:url 中必须包含 allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai # 否则 MySQL 8.0+ 会拒绝连接 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/loan_db?allowPublicKeyRetrieval=true&useSSL=false&serverTimezone=Asia/Shanghai # 3. 初始化数据库(执行 resources/sql/init.sql) # 该脚本会创建 loan_db 库、建表、插入基础字典(如还款方式:等额本息/先息后本/随借随还) # 4. 编译并启动(跳过测试,节省时间) mvn clean package -Dmaven.test.skip=true java -jar target/loan-server-1.0.0.jar --spring.profiles.active=prod

服务启动成功后,访问http://localhost:8080/actuator/health返回{"status":"UP"}即表示核心服务就绪。此时可查看logs/loan-server.log,确认是否加载了RiskEngineConfiguration和PaymentChannelAutoConfiguration——这两个 Bean 是风控和支付通道的入口,缺失意味着配置有误。

2.2 前端 uni-app:HBuilderX 4.22+ 是唯一推荐 IDE

uni-app 部分不是用 Vue CLI 创建的,而是标准的 HBuilderX 工程结构。这意味着:不能用npm run serve启动开发服务器。必须用 HBuilderX 打开frontend/loan-uniapp目录,右键 →「运行到浏览器」或「运行到小程序模拟器」。原因在于:

  • 项目根目录下manifest.json中name字段值为"loan-uniapp-2024",这是 uni-app 构建时注入 AppID 的依据;
  • vue.config.js中configureWebpack配置了externals: { 'crypto-js': 'CryptoJS' },即强制使用 HBuilderX 内置的 CryptoJS 库(含国密 SM2/SM4 支持),而非 npm 安装的版本;
  • static/目录下的rsa-key-pair.pem是前端 RSA 加密私钥(用于对敏感字段如身份证号签名),该文件由 HBuilderX 在构建时自动注入到uni-app的__uniConfig对象中,普通 Webpack 构建无法识别。

关键配置项在src/common/config.js:

// src/common/config.js export const CONFIG = { // 生产环境 API 地址(注意:不是 http://localhost:8080,而是反向代理后的域名) API_BASE_URL: 'https://api.loan-demo.com', // 风控决策超时时间(毫秒),超过此时间前端自动触发人工审核流程 RISK_TIMEOUT: 8000, // 电子合同签署服务地址(对接 eSign 平台) CONTRACT_SERVICE: 'https://esign.loan-demo.com/v2/sign', // 是否启用生物认证(iOS FaceID / Android BiometricPrompt) ENABLE_BIOMETRIC: true }

注意:API_BASE_URL必须是 HTTPS 域名。uni-app 在 iOS 15+ 和 Android 10+ 上强制要求所有网络请求走 HTTPS,且证书需由可信 CA 签发。若本地调试,可在 HBuilderX 的「运行」→「运行到浏览器」中勾选「忽略 HTTPS 证书错误」,但此选项在真机调试时无效。

2.3 前后端联调验证:用 Postman 模拟一次真实借款申请

不要等前端页面全部渲染成功再验证逻辑。最快速的验证方式是:用 Postman 模拟一次完整的借款申请链路。以下是必须复现的 4 个关键请求(按顺序):

  1. 获取短信验证码(POST/api/v1/auth/sms-code)
    Body(raw JSON):

    { "mobile": "13800138000", "scene": "REGISTER" }

    注意:scene字段必须是REGISTER、LOGIN、LOAN_APPLY之一,否则风控网关会拦截。返回{"code":200,"data":{"smsId":"sms_abc123"}}即成功。

  2. 提交借款申请(POST/api/v1/loan/apply)
    Headers 加Content-Type: application/json和X-Nonce: abc123xyz(32位随机字符串)
    Body(raw JSON):

    { "userId": "usr_789", "amount": 5000, "term": 12, "repaymentMethod": "EQUAL_PRINCIPAL_INTEREST", "idCardNo": "RSA_ENCRYPTED_CIPHETEXT_HERE", "mobile": "13800138000" }

    关键点:idCardNo字段必须是前端用RSA_PKCS1_OAEP_PADDING方式加密后的 Base64 字符串,密钥来自后端/api/v1/auth/public-key接口。若传明文,后端IdCardDecryptFilter会直接返回 400。

  3. 查询申请状态(GET/api/v1/loan/apply/status?applyId=app_456)
    返回结果中riskStatus字段应为"PASSED"或"REJECTED",若为"PENDING"则说明风控服务未就绪。

  4. 下载电子合同(GET/api/v1/contract/download?applyId=app_456)
    成功返回 PDF 文件流,且 HTTP Header 中Content-Disposition包含filename="loan_contract_20240512.pdf"。

这四步走通,证明整个借贷主干流程(用户触达 → 身份核验 → 风控决策 → 合同生成)已具备生产可用性。


3. 核心模块拆解:风控引擎、资金通道、电子签章三大模块的实现逻辑

3.1 风控引擎模块:规则引擎 + 模型服务 + 人工复核的三层架构

风控不是写一堆 if-else。本项目采用「决策分离」设计:

  • 规则层(Rule Engine):基于 Drools 实现,规则文件存于backend/loan-server/src/main/resources/rules/下,如credit-score.drl定义了「近6个月逾期次数 > 2 → 信用分扣减30分」;
  • 模型层(Model Service):独立的 Python 微服务(ml-risk-service),暴露/predict接口,接收用户行为特征向量(如登录频次、设备指纹、通讯录关系图谱),返回 0~1 的违约概率;
  • 人工复核层(Manual Review):当规则层输出RISK_LEVEL_HIGH且模型层输出default_prob > 0.65时,自动将申请单推入review_queue(Redis List),运营后台可拉取处理。

关键代码在RiskDecisionService.java:

// backend/loan-server/src/main/java/com/loan/risk/RiskDecisionService.java public RiskDecisionResult makeDecision(LoanApplyRequest request) { // 1. 规则引擎打分(同步调用) KieSession kieSession = kieBase.newKieSession(); kieSession.insert(request); kieSession.fireAllRules(); // 触发所有 .drl 规则 int creditScore = request.getCreditScore(); // 规则执行后注入的字段 // 2. 模型服务预测(异步调用,超时8秒) CompletableFuture<Double> modelFuture = CompletableFuture .supplyAsync(() -> mlClient.predict(request.getFeatures())) .orTimeout(8, TimeUnit.SECONDS) .exceptionally(ex -> 0.0); // 超时返回0,避免阻塞 // 3. 综合决策 try { double defaultProb = modelFuture.get(); if (creditScore < 600 && defaultProb > 0.65) { return RiskDecisionResult.builder() .riskLevel(RiskLevel.HIGH) .decision(RiskDecision.MANUAL_REVIEW) .build(); } else if (creditScore >= 700 && defaultProb < 0.3) { return RiskDecisionResult.builder() .riskLevel(RiskLevel.LOW) .decision(RiskDecision.APPROVED) .build(); } } catch (Exception e) { log.warn("模型服务不可用,降级为纯规则决策", e); // 降级逻辑:仅用规则分判断 } return fallbackByRule(creditScore); }

参数说明:mlClient是 FeignClient,指向http://ml-risk-service:8081;request.getFeatures()返回的是预处理后的 Map<String, Object>,字段名与训练时一致(如login_count_7d,device_risk_score)。

3.2 资金通道模块:抽象支付网关,支持银行直连与第三方聚合

资金划拨不是简单调用Alipay.pay()。本项目定义了PaymentChannel接口,所有通道实现类必须继承AbstractPaymentChannel,并实现doTransfer()方法:

// backend/loan-server/src/main/java/com/loan/payment/PaymentChannel.java public interface PaymentChannel { /** * 执行资金划拨 * @param transferOrder 划拨订单(含收款方银行卡号、开户行联行号、金额) * @return TransferResult(含交易流水号、渠道返回码) */ TransferResult doTransfer(TransferOrder transferOrder); /** * 查询交易状态(用于对账) */ TransferStatus queryStatus(String channelTxnId); }

目前已实现两个通道:

  • BankDirectChannel:对接某国有银行的银企直连 SDK,使用 SM2 国密算法签名,报文格式为 XML;
  • UnionPayChannel:对接银联无界支付 API,使用 RSA-SHA256 签名,报文为 JSON。

关键区别在application-prod.yml的配置:

# 银行直连配置(必须启用国密) payment: bank-direct: enabled: true host: https://bank-api.example.com cert-path: /opt/certs/bank-sm2.p12 cert-password: changeit # 银联配置(标准 RSA) payment: unionpay: enabled: false # 默认关闭,避免误调用 host: https://gateway.95516.com merchant-id: MCH_123456 rsa-private-key: "-----BEGIN RSA PRIVATE KEY-----..."

注意:cert-path指向的是 P12 格式国密证书,由银行提供,不能用 OpenSSL 生成。若启用bank-direct但证书路径错误,BankDirectChannel初始化时会抛CertificateException,导致 Spring 容器启动失败。

3.3 电子签章模块:合同生成、数字签名、存证上链三步闭环

电子合同不是 PDF 模板填充。本项目采用「动态生成 + 双重签名 + 存证」模式:

  • 生成:使用 Apache PDFBox 2.0.28 动态绘制合同(非 iText),确保每份合同含唯一contractId和signTimestamp;
  • 签名:先由平台私钥(platform-sign-key.pem)对 PDF 哈希值签名,再由用户生物特征(FaceID/指纹)触发WebAuthn协议,生成第二重签名;
  • 存证:将合同哈希值、双签名、时间戳打包为 JSON,调用blockchain-service的/api/v1/record接口上链(底层为 Hyperledger Fabric 2.5)。

核心逻辑在ContractService.java:

// backend/loan-server/src/main/java/com/loan/contract/ContractService.java public Contract generateContract(LoanApplyResult applyResult) { // 1. 从模板生成 PDF(模板存于 resources/templates/loan-contract.pdf) PDDocument doc = PDDocument.load(templateStream); PDPage page = doc.getPage(0); PDFont font = PDType0Font.load(doc, fontStream); // 加载思源黑体,支持中文 // ... 填充借款人姓名、金额、利率、还款计划表(用 PDFBox Table 绘制) // 2. 计算 PDF 哈希(SHA256) byte[] pdfBytes = IOUtils.toByteArray(doc.saveIncremental()); String pdfHash = DigestUtils.sha256Hex(pdfBytes); // 3. 平台签名(SM2) String platformSignature = sm2Signer.sign(pdfHash, platformPrivateKey); // 4. 构建存证数据 EvidenceData evidence = EvidenceData.builder() .contractId(applyResult.getContractId()) .pdfHash(pdfHash) .platformSignature(platformSignature) .timestamp(Instant.now().toString()) .build(); // 5. 调用区块链服务存证 blockchainClient.record(evidence); return Contract.builder() .id(applyResult.getContractId()) .pdfBytes(pdfBytes) .build(); }

提示:sm2Signer是自研 SM2 签名工具类,依赖 Bouncy Castle 1.70+。若 Maven 依赖冲突,需在pom.xml中排除旧版 BC 依赖。


4. 避坑指南:5 个真实踩过的坑,省下你至少两天排查时间

4.1 现象:前端 uni-app 在 iOS 真机上白屏,控制台无任何报错

原因:HBuilderX 4.22+ 默认启用V8 JavaScript 引擎,但 iOS 16.4+ 的 WKWebView 对 V8 兼容性差,导致main.js解析失败。
解决:在 HBuilderX 中打开「工具」→「设置」→「运行配置」→「uni-app 运行配置」,取消勾选「启用 V8 引擎」,重启 IDE 后重新运行。

4.2 现象:风控模型服务ml-risk-service启动后立即 OOM(内存溢出)

原因:ml-risk-service的Dockerfile中CMD ["java", "-Xmx2g", "-jar", "ml-risk-service.jar"],但宿主机内存仅 4G,且未限制容器内存上限。
解决:启动容器时加--memory=2g --memory-swap=2g参数,并在application.yml中将spring.ai.ollama.chat.options.temperature从0.8降至0.3(降低模型推理复杂度)。

4.3 现象:用户提交借款申请后,riskStatus一直是PENDING,日志显示Drools rule execution timeout

原因:credit-score.drl中有一条规则when $u: User($u.age < 18),但User对象未实现hashCode()和equals(),导致 Drools 规则匹配引擎死循环。
解决:在User.java中添加 Lombok 注解@EqualsAndHashCode,并确保所有字段参与计算(特别是idCardNo这种敏感字段需脱敏后再参与 equals)。

4.4 现象:银行直连通道doTransfer()报错javax.net.ssl.SSLHandshakeException: No appropriate protocol

原因:JDK 17 默认禁用 TLSv1.1,而某银行测试环境仅支持 TLSv1.1。
解决:在application-prod.yml中添加 JVM 参数:

spring: boot: run-arguments: --jdk.tls.client.protocols=TLSv1.1,TLSv1.2

并在BankDirectChannel初始化时显式设置 SSLContext:

SSLContext sslContext = SSLContext.getInstance("TLSv1.1"); sslContext.init(null, trustManagers, new SecureRandom());

4.5 现象:电子合同 PDF 下载后,Adobe Reader 提示「签名无效:证书吊销状态未知」

原因:platform-sign-key.pem对应的 CA 证书未嵌入 PDF,且 PDF 签名时未启用 OCSP(在线证书状态协议)检查。
解决:修改ContractService.generateContract(),在签名前调用setOCSPData():

PDSignature signature = new PDSignature(); signature.setCertification(CERTIFIED_FORM_FILLING); signature.setOCSPData(ocspResponse); // ocspResponse 从 CA 获取 doc.addSignature(signature, digest, signatureInterface);

5. 二次开发实战:如何替换风控模型为自己的 XGBoost 模型并接入线上特征平台

5.1 模型替换:从 Python 微服务到 Java 原生集成

ml-risk-service是一个 Flask 服务,但它只是过渡方案。生产环境更推荐将模型固化到 Java 服务中,避免跨进程通信开销。本项目预留了XGBoostPredictor类,位于backend/loan-server/src/main/java/com/loan/ml/:

// backend/loan-server/src/main/java/com/loan/ml/XGBoostPredictor.java @Component public class XGBoostPredictor { private Booster booster; @PostConstruct public void init() throws IOException { // 加载 .ubj 模型文件(XGBoost 1.7+ 导出格式) InputStream modelStream = getClass() .getResourceAsStream("/models/risk-model-v2.ubj"); this.booster = Booster.loadModel(modelStream); } public double predict(Map<String, Double> features) { // 将 Map 转为 float[] 数组,顺序必须与训练时一致 float[] input = new float[FEATURE_NAMES.length]; for (int i = 0; i < FEATURE_NAMES.length; i++) { input[i] = features.getOrDefault(FEATURE_NAMES[i], 0.0).floatValue(); } // XGBoost Java API 预测 float[][] output = booster.predict(new DenseMatrix(1, input.length, input)); return output[0][0]; // 返回违约概率 } // 特征名列表(必须与训练脚本完全一致) private static final String[] FEATURE_NAMES = { "age", "income_monthly", "credit_score", "loan_count_12m", "overdue_count_6m", "device_risk_score" }; }

关键点:FEATURE_NAMES数组顺序必须与训练时xgboost.train()的X_train.columns顺序严格一致,否则预测结果完全错误。建议在训练脚本末尾导出feature_names.json,供 Java 端读取。

5.2 特征平台对接:从本地 Map 到实时 Flink 特征流

当前features来自LoanApplyRequest的硬编码字段。真实场景中,这些特征应来自实时特征平台(如 Flink + Redis)。本项目已预留FeatureService接口:

// backend/loan-server/src/main/java/com/loan/feature/FeatureService.java public interface FeatureService { /** * 根据用户ID实时获取特征向量 * @param userId 用户唯一标识(如 usr_123) * @return key-value 特征映射(key 为 FEATURE_NAMES 中的名称) */ Map<String, Double> getFeatures(String userId); }

默认实现RedisFeatureService从 Redis Hash 中读取:

// backend/loan-server/src/main/java/com/loan/feature/RedisFeatureService.java @Repository public class RedisFeatureService implements FeatureService { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public Map<String, Double> getFeatures(String userId) { // Redis Key 格式:feature:usr_123 HashOperations<String, String, String> hashOps = redisTemplate.opsForHash(); Map<String, String> rawFeatures = hashOps.entries("feature:" + userId); return rawFeatures.entrySet().stream() .filter(entry -> FEATURE_NAMES_SET.contains(entry.getKey())) .collect(Collectors.toMap( Map.Entry::getKey, entry -> Double.parseDouble(entry.getValue()) )); } }

注意:Flink 作业需将特征写入 Redis 的 Hash 结构,Key 为feature:${userId},Field 为特征名(如age),Value 为字符串数字。不要用 JSON 字符串,避免序列化开销。

5.3 验证方法:用单元测试覆盖模型输入输出一致性

替换模型后,必须验证 Java 端预测结果与 Python 端完全一致。本项目test目录下提供了XGBoostPredictorTest:

// backend/loan-server/src/test/java/com/loan/ml/XGBoostPredictorTest.java @SpringBootTest class XGBoostPredictorTest { @Autowired private XGBoostPredictor predictor; @Test void shouldPredictSameAsPython() { // 这组数据来自 Python 脚本的 predict([25, 15000, 720, 1, 0, 0.1]) 输出 Map<String, Double> features = Map.of( "age", 25.0, "income_monthly", 15000.0, "credit_score", 720.0, "loan_count_12m", 1.0, "overdue_count_6m", 0.0, "device_risk_score", 0.1 ); double javaResult = predictor.predict(features); // Python 端实测结果为 0.2341(保留4位小数) assertEquals(0.2341, javaResult, 0.0001); } }

血泪经验:不要只测一个样本。至少准备 100 条线上真实用户特征,用 Python 脚本批量预测并保存为python-results.csv,再用 Java 读取 CSV 批量验证,确保 MAE(平均绝对误差)< 0.001。

从那以后我每次替换模型,都强制走一遍「Python 批量预测 → 导出 CSV → Java 读取验证 → 生成误差报告」的闭环。不是信不过自己写的 Java 代码,而是信不过 XGBoost Java Binding 对稀疏矩阵的处理逻辑。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询