尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot贷款系统:风控校验+状态机+Docker生产交付

SpringBoot贷款系统:风控校验+状态机+Docker生产交付 简介本资源是一套基于SpringBoot的微服务架构贷款申请系统完整源码面向计算机专业本科生、毕业设计与课程设计学习者解决分布式金融业务系统开发实践难题。项目采用典型微服务拆分模式包含loan-application-service、credit-score-service、config-server、Eureka注册中心等核心模块并通过Maven统一管理依赖与构建流程适合深入理解SpringCloud生态与企业级信贷系统设计逻辑。压缩包共361个文件以75个Java业务类、216个XML配置及YML/Properties环境配置文件为主辅以Dockerfile容器化部署脚本、README项目文档和.gitignore版本控制规范整体829KB结构清晰、开箱即用。目前已有51人学习下载读者可直接复现完整微服务调用链路掌握服务注册发现、配置中心集成、信用评分模块解耦及前后端分离含Vue前端线索等关键能力。1. 这不是又一个“用户注册表单提交”的 SpringBoot 演示项目它是一套可落地的贷款申请业务骨架覆盖风控前置校验、多状态流转、异步审批通知与 Docker 容器化交付闭环当你在 GitHub 或毕设仓库里看到基于SpringBoot的贷款申请系统.zip别急着解压——先问自己三个问题这个系统是否能真实模拟银行/消金机构中「客户提交→初审拦截→人工复核→放款确认」的完整链路它的数据模型是否包含授信额度、利率阶梯、逾期罚息规则等金融级字段部署包里有没有真正可用的Dockerfile和.gitignore而不是空文件或复制粘贴的模板很多所谓“贷款系统”只做到前端表单提交加 MyBatis 插入一条loan_apply记录连最基本的身份证号校验、手机号三要素验证、征信接口占位符都没有。而本标题指向的系统核心价值在于它把 SpringBoot 作为业务编排引擎而非单纯 Web 框架来使用用EventListener响应状态变更事件触发风控调用用Scheduled扫描超时待审核任务用RestTemplate封装标准 HTTP 接口对接外部征信服务并通过Dockerfile实现从mvn clean package到docker run -p 8080:8080 loan-app的一键交付。适合需要快速搭建合规性原型的金融科技团队、正在准备 SpringBoot 面试题的中级后端工程师以及要接手真实贷款模块二次开发的外包人员——你拿到的不是玩具是带生产级约束的最小可行业务骨架。2. 用 SpringBoot 3.x Lombok MyBatis-Plus 构建贷款领域模型为什么放弃 JPA 而选择手动控制 SQL 与事务边界2.1 选型依据金融场景下对 SQL 可控性、审计追溯与性能压测的刚性需求在贷款系统中loan_apply表绝非简单 CRUD。它需承载复合主键设计apply_idversion支持乐观锁状态机字段status枚举值SUBMITTED,REJECTED,APPROVED,DISBURSED,OVERDUE风控结果嵌套结构risk_result_jsonTEXT 字段存 JSON含score,level,reasons敏感字段加密存储id_card_encrypted,mobile_encrypted使用 AES-256-GCMJPA 的Entity自动生成 SQL 在此处成为负担无法精确控制UPDATE loan_apply SET status ?, risk_result_json ? WHERE apply_id ? AND version ?中的AND version ?条件审计日志需记录每次状态变更前后的完整字段快照JPA 的PreUpdate无法获取旧值压测时发现findAll()默认 N1 查询导致数据库连接池耗尽。因此本系统采用 MyBatis-Plus 3.5.3.1兼容 SpringBoot 3.2保留 XML 显式 SQL 控制权同时利用其LambdaQueryWrapper防止硬编码字段名。提示MyBatis-Plus 的TableField(fill FieldFill.INSERT)用于自动填充create_time但update_time必须在UPDATE语句中显式写update_time NOW()否则并发更新时时间戳会丢失。2.2 核心实体类与 Mapper 层实现带版本控制的状态变更与加密字段处理// src/main/java/com/example/loan/entity/LoanApply.java Data TableName(loan_apply) public class LoanApply { TableId(type IdType.ASSIGN_UUID) private String applyId; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; private Integer version; // 乐观锁版本号 private String idCardEncrypted; // 加密后身份证号 private String mobileEncrypted; // 加密后手机号 private BigDecimal amount; // 申请金额 private Integer termMonths; // 期数 private BigDecimal annualRate; // 年化利率 TableField(status) private LoanStatus status; // 枚举类型 TableField(risk_result_json) private String riskResultJson; // JSON 字符串 // 敏感字段 setter调用加密工具类 public void setIdCard(String plainIdCard) { this.idCardEncrypted AesUtil.encrypt(plainIdCard); } public String getIdCard() { return AesUtil.decrypt(this.idCardEncrypted); } }对应 Mapper XML 片段src/main/resources/mapper/LoanApplyMapper.xml!-- 更新状态并校验版本号 -- update idupdateStatusByVersion UPDATE loan_apply SET status #{status}, risk_result_json #{riskResultJson}, update_time NOW(), version version 1 WHERE apply_id #{applyId} AND version #{version} !-- 关键确保并发安全 -- /update该 SQL 的WHERE apply_id ? AND version ?是状态变更原子性的基石。若返回影响行数为 0说明版本号已被其他线程更新业务层需抛出OptimisticLockException并重试。2.3 依赖配置与关键参数SpringBoot 3.2.x 下的 JDK 17 兼容性与数据库连接池调优pom.xml中必须声明以下依赖省略版本号以 SpringBoot 3.2.4 为准dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.3.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scopetest/scope /dependency /dependenciesapplication.yml中数据库连接池关键参数HikariCP参数推荐值说明spring.datasource.hikari.maximum-pool-size20贷款系统读多写少20 连接足够支撑 50 QPSspring.datasource.hikari.connection-timeout30003 秒超时避免风控接口慢拖垮整个连接池spring.datasource.hikari.leak-detection-threshold60000启用连接泄漏检测60 秒未归还即告警mybatis-plus.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl开发环境开启 SQL 日志生产环境关闭注意SpringBoot 3.x 强制要求 JDK 17若本地 IDEA 仍用 JDK 8 创建项目会报Unsupported class file major version 61错误。务必在File Project Structure Project SDK中切换为 JDK 17并在pom.xml中确认java.version17/java.version。3. 实现贷款申请全生命周期状态机从提交拦截到异步通知的事件驱动架构3.1 状态流转图与事件定义为什么不用 StateMachine 而用 Spring Event 自定义事件总线贷款申请状态并非线性流程SUBMITTED→REJECTED初审失败SUBMITTED→APPROVED初审通过进入人工复核APPROVED→DISBURSED放款成功DISBURSED→OVERDUE还款日未还每日凌晨批处理扫描Spring Statemachine 在此场景过于重量级需定义状态机配置类、事件枚举、状态转换条件且难以与 MyBatis-Plus 的updateStatusByVersion原子操作耦合。本系统采用轻量级ApplicationEventPublisher 自定义事件// 事件定义 public class LoanStatusChangedEvent extends ApplicationEvent { private final String applyId; private final LoanStatus oldStatus; private final LoanStatus newStatus; public LoanStatusChangedEvent(Object source, String applyId, LoanStatus oldStatus, LoanStatus newStatus) { super(source); this.applyId applyId; this.oldStatus oldStatus; this.newStatus newStatus; } // getter... } // 发布事件在 Service 层 public void approveApplication(String applyId) { LoanApply oldApply loanApplyMapper.selectById(applyId); if (!oldApply.getStatus().equals(LoanStatus.SUBMITTED)) { throw new IllegalStateException(Only SUBMITTED can be approved); } boolean updated loanApplyMapper.updateStatusByVersion( applyId, oldApply.getVersion(), LoanStatus.APPROVED, {} ); if (!updated) { throw new OptimisticLockException(Version conflict on approve); } // 发布事件 applicationEventPublisher.publishEvent( new LoanStatusChangedEvent(this, applyId, oldApply.getStatus(), LoanStatus.APPROVED) ); }3.2 事件监听器实现风控校验、短信通知与逾期扫描的解耦落地Component Slf4j public class LoanStatusEventListener { EventListener public void handleStatusChange(LoanStatusChangedEvent event) { switch (event.getNewStatus()) { case SUBMITTED: // 触发初审调用风控服务模拟 HTTP 请求 invokeRiskEngine(event.getApplyId()); break; case APPROVED: // 异步发送短信通知客户复核通过 CompletableFuture.runAsync(() - sendSmsNotification(event.getApplyId())); break; case DISBURSED: // 记录放款流水更新客户授信额度 updateCreditLimit(event.getApplyId()); break; case OVERDUE: // 记录逾期日志触发催收任务 log.warn(Loan {} overdue since {}, event.getApplyId(), LocalDate.now()); break; } } private void invokeRiskEngine(String applyId) { // 实际调用RestTemplate.postForObject(http://risk-service/v1/evaluate, request, RiskResult.class) log.info(Risk engine invoked for {}, applyId); } private void sendSmsNotification(String applyId) { // 实际调用阿里云 SMS SDK log.info(SMS sent for loan {}, applyId); } }该设计将业务逻辑拆分为独立方法每个方法可单独单元测试且CompletableFuture.runAsync确保短信发送不阻塞主流程。3.3 定时任务扫描逾期用Scheduled实现每日凌晨 2 点的批量状态更新Component Slf4j public class OverdueLoanScheduler { Scheduled(cron 0 0 0 2 * ?) // 每日凌晨 2 点执行 public void scanOverdueLoans() { LocalDateTime now LocalDateTime.now(); // 查找已放款但未还款且还款日早于今天的贷款 ListLoanApply overdueList loanApplyMapper.selectList( new LambdaQueryWrapperLoanApply() .eq(LoanApply::getStatus, LoanStatus.DISBURSED) .lt(LoanApply::getRepayDate, now.toLocalDate()) ); for (LoanApply loan : overdueList) { // 原子更新状态为 OVERDUE boolean updated loanApplyMapper.updateStatusByVersion( loan.getApplyId(), loan.getVersion(), LoanStatus.OVERDUE, null ); if (updated) { log.info(Marked loan {} as OVERDUE, loan.getApplyId()); // 同时发布事件触发后续催收动作 applicationEventPublisher.publishEvent( new LoanStatusChangedEvent(this, loan.getApplyId(), LoanStatus.DISBURSED, LoanStatus.OVERDUE) ); } } } }提示Scheduled默认单线程执行若逾期贷款量极大10万条需在application.yml中配置线程池spring: task: scheduling: pool: size: 54. Docker 容器化交付从Dockerfile多阶段构建到.gitignore的生产级过滤策略4.1Dockerfile编写为什么必须用多阶段构建而非FROM openjdk:17-jre-slim直接运行直接FROM openjdk:17-jre-slim会导致镜像体积过大300MB且包含不必要的编译工具javac,jar。本系统采用 Maven 多阶段构建# 第一阶段构建 FROM maven:3.9.6-openjdk-17-slim AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM openjdk:17-jre-slim VOLUME [/tmp] ARG DEPENDENCY/app/target/dependency # 从构建阶段复制依赖和 jar COPY --frombuild /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java,-Djava.security.egdfile:/dev/./urandom,-jar,/app.jar]该写法将构建环境Maven与运行环境JRE彻底分离最终镜像仅含 JRE 和打包好的app.jar体积压缩至 85MB 左右。-Djava.security.egdfile:/dev/./urandom解决容器内熵池不足导致启动慢的问题。4.2.gitignore生产级配置排除敏感文件、IDE 缓存与构建产物.gitignore不是简单复制模板而是按实际项目结构精准过滤# Maven 构建产物 target/ *.jar *.war *.ear # IDE 配置IntelliJ .idea/ *.iml *.iws *.ipr # IDE 配置Eclipse .project .classpath .settings/ # SpringBoot 配置文件敏感信息 application-prod.yml application-secret.yml config/application-prod.yml # 日志文件 logs/ *.log # 临时文件 *.swp *.swo .DS_Store # Docker 构建缓存 Dockerfile .dockerignore # 本地数据库文件H2 *.h2.db *.trace.db注意application-prod.yml必须被忽略其内容应包含数据库密码、Redis 密钥等由 CI/CD 流水线注入或挂载 ConfigMap。若误提交需立即轮换所有密钥并执行git filter-repo清除历史记录。4.3 容器运行与健康检查docker-compose.yml中的生产就绪配置version: 3.8 services: loan-app: image: loan-app:1.0.0 ports: - 8080:8080 environment: - SPRING_PROFILES_ACTIVEprod - SPRING_DATASOURCE_URLjdbc:mysql://mysql:3306/loan_db?useSSLfalseserverTimezoneAsia/Shanghai - SPRING_DATASOURCE_USERNAMEroot - SPRING_DATASOURCE_PASSWORDyour_secure_password depends_on: - mysql # 健康检查HTTP GET /actuator/health healthcheck: test: [CMD, curl, -f, http://localhost:8080/actuator/health] interval: 30s timeout: 10s retries: 3 start_period: 40s mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: your_secure_password MYSQL_DATABASE: loan_db volumes: - mysql-data:/var/lib/mysql command: --default-authentication-pluginmysql_native_password volumes: mysql-data:健康检查curl -f http://localhost:8080/actuator/health依赖 SpringBoot Actuator需在pom.xml中添加dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency并在application.yml中暴露端点management: endpoint: health: show-details: always endpoints: web: exposure: include: health,info,metrics,prometheus5. 验证系统可用性用curl和docker logs快速定位三大高频故障点5.1 数据库连接失败docker logs loan-app中的关键错误模式与修复路径当容器启动后立即退出执行docker logs loan-app最常见错误Caused by: com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.这表示应用无法连接 MySQL 容器。不要直接修改application.yml中的localhost—— 在 Docker Compose 网络中服务名mysql即 DNS 名称。正确排查步骤确认 MySQL 容器是否正常运行docker ps | grep mysql进入 MySQL 容器验证端口docker exec -it mysql-container-id mysql -uroot -pyour_secure_password -e SELECT 1检查docker-compose.yml中loan-app的depends_on是否生效注意depends_on不保证 MySQL 完全就绪需配合健康检查在loan-app容器内测试网络连通性docker exec -it loan-app-container-id sh -c telnet mysql 3306若telnet失败说明 Docker 网络未打通需检查docker-compose.yml的network_mode是否被意外覆盖。5.2 状态更新失败updateStatusByVersion返回 false 的三种真实场景与日志定位当调用approveApplication接口返回 500日志中出现WARN c.e.l.s.LoanApplyService - Version conflict on approve for applyId: abc123这不是代码 Bug而是并发冲突的正常现象。需检查场景日志特征解决方案双提交同一applyId在 100ms 内收到两个approve请求前端按钮点击后置灰后端加 Redis 分布式锁SET loan:abc123:lock 1 NX EX 10状态非法日志出现IllegalStateException: Only SUBMITTED can be approved检查前端是否绕过状态校验直接调用/api/loan/approve应在 Controller 层加Valid注解SQL 未匹配UPDATE loan_apply ... WHERE apply_id ? AND version ?影响行数为 0在updateStatusByVersion方法中添加log.debug(Update SQL affected {} rows, result)确认传入version是否正确5.3 Docker 镜像启动慢java -jar app.jar卡在Starting Servlet Engine的根因与优化若docker run -p 8080:8080 loan-app:1.0.0启动耗时超过 90 秒检查熵池不足容器内随机数生成慢。已在Dockerfile中添加-Djava.security.egdfile:/dev/./urandom确认生效docker exec -it container jps -l查看 JVM 参数DNS 解析阻塞应用启动时调用外部风控服务但docker-compose.yml未配置 DNSservices: loan-app: dns: 8.8.8.8Actuator 端点加载过重/actuator/health默认检查所有组件DB、Redis、Mail。精简配置management: health: db: show-details: never redis: show-details: never最终验证命令链# 构建镜像 docker build -t loan-app:1.0.0 . # 启动服务 docker-compose up -d # 检查健康状态 curl http://localhost:8080/actuator/health # 提交一笔测试申请替换为实际手机号/身份证 curl -X POST http://localhost:8080/api/loan/submit \ -H Content-Type: application/json \ -d {mobile:13800138000,idCard:110101199003072712,amount:10000,termMonths:12}只要返回{code:200,data:{applyId:xxx}}即证明贷款申请系统的核心链路已跑通。本文还有配套的精品资源点击获取
返回列表