首页 / 资讯中心 / 文章详情

金融级借贷系统源码:银行直连+风控三层架构+电子签章

金融级借贷系统源码:银行直连+风控三层架构+电子签章 ★ FEATURED ARTICLE
简介这是一套面向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.version17/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_formatROW——这是后续接入审计日志和资金流水对账的前提。启动步骤如下以 Linux/macOS 为例# 1. 克隆后进入后端目录 cd backend/loan-server # 2. 修改数据库连接application-prod.yml # 注意url 中必须包含 allowPublicKeyRetrievaltrueuseSSLfalseserverTimezoneAsia/Shanghai # 否则 MySQL 8.0 会拒绝连接 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/loan_db?allowPublicKeyRetrievaltrueuseSSLfalseserverTimezoneAsia/Shanghai # 3. 初始化数据库执行 resources/sql/init.sql # 该脚本会创建 loan_db 库、建表、插入基础字典如还款方式等额本息/先息后本/随借随还 # 4. 编译并启动跳过测试节省时间 mvn clean package -Dmaven.test.skiptrue java -jar target/loan-server-1.0.0.jar --spring.profiles.activeprod服务启动成功后访问http://localhost:8080/actuator/health返回{status:UP}即表示核心服务就绪。此时可查看logs/loan-server.log确认是否加载了RiskEngineConfiguration和PaymentChannelAutoConfiguration——这两个 Bean 是风控和支付通道的入口缺失意味着配置有误。2.2 前端 uni-appHBuilderX 4.22 是唯一推荐 IDEuni-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 个关键请求按顺序获取短信验证码POST/api/v1/auth/sms-codeBodyraw JSON{ mobile: 13800138000, scene: REGISTER }注意scene字段必须是REGISTER、LOGIN、LOAN_APPLY之一否则风控网关会拦截。返回{code:200,data:{smsId:sms_abc123}}即成功。提交借款申请POST/api/v1/loan/applyHeaders 加Content-Type: application/json和X-Nonce: abc123xyz32位随机字符串Bodyraw 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。查询申请状态GET/api/v1/loan/apply/status?applyIdapp_456返回结果中riskStatus字段应为PASSED或REJECTED若为PENDING则说明风控服务未就绪。下载电子合同GET/api/v1/contract/download?applyIdapp_456成功返回 PDF 文件流且 HTTP Header 中Content-Disposition包含filenameloan_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_queueRedis 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秒 CompletableFutureDouble 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:8081request.getFeatures()返回的是预处理后的 MapString, 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 国密算法签名报文格式为 XMLUnionPayChannel对接银联无界支付 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且未限制容器内存上限。解决启动容器时加--memory2g --memory-swap2g参数并在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.protocolsTLSv1.1,TLSv1.2并在BankDirectChannel初始化时显式设置 SSLContextSSLContext 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(MapString, 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 中的名称 */ MapString, 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 RedisTemplateString, Object redisTemplate; Override public MapString, Double getFeatures(String userId) { // Redis Key 格式feature:usr_123 HashOperationsString, String, String hashOps redisTemplate.opsForHash(); MapString, 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 为特征名如ageValue 为字符串数字。不要用 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]) 输出 MapString, 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 对稀疏矩阵的处理逻辑。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站