
简介这是一套面向Java Web初学者与信贷业务开发者的银行信用贷款系统完整源码采用Servlet作为Web层实现配合MySQL数据库与HTML前端适合用于课程设计、毕业设计参考或信贷业务流程学习。压缩包共4605个文件约53.93MB其中前端资源占比突出包含1630个js、524个css、138个html及大量svg、less、ts等说明界面层做了较细致的拆分后端有66个java源文件与130个class涵盖贷款、存款、客户管理等业务模块另有36个jar依赖、2个sql脚本及jsp页面整体结构完整。目前已有2645人学习下载。读者可从中获取一套可直接运行的信贷系统实例理解Servlet控制层、DAO数据访问与前端页面的协作方式并参考其目录组织与业务分层思路用于二次开发或功能扩展。1. 银行信用贷款系统源码拆解一套 Java Web 信贷系统到底由哪些模块撑起来很多人搜「java web项目 银行信用贷款系统源代码」心里想的其实是同一件事手上有一套能跑起来的信贷系统想搞清楚它怎么分层、怎么算利息、怎么控额度然后改成自己能用的版本。银行信用贷款系统和普通电商、管理系统最大的区别在于它每一步操作都要留痕、每一笔金额都要能对上账所以源码里真正值钱的不是页面而是授信、放款、还款、逾期这四条业务链的代码组织方式。这套系统通常面向三类人一是要做课程设计或毕业设计的学生需要一套结构完整、能讲清楚业务闭环的 Java Web 项目二是刚转进金融方向的开发者想通过读源码理解信贷业务怎么落到数据库表上三是小团队要快速搭一个贷款类产品的原型拿现成结构改。它解决的核心问题是把「申请—审批—签约—放款—还款—逾期」这条链路用代码固化下来而不是每次重写。下面按模块拆开讲重点放在能复现的结构和参数上。2. 信贷系统分层结构与技术选型为什么这套源码大多长一个样2.1 典型分层Controller 薄、Service 厚、DAO 只做数据搬运银行信用贷款系统的源码十套里有八套是 Spring Boot MyBatis 或 Spring Boot JPA 的组合前端要么是 Thymeleaf 服务端渲染要么是 Vue 前后端分离。分层逻辑基本固定Controller 只负责接参数、做基础校验、返回统一结果Service 承担全部业务规则比如额度计算、利率匹配、还款计划生成DAO 层只做单表或简单关联查询复杂统计走 XML 或自定义 SQL。为什么 Service 必须厚因为信贷业务里同一个动作在不同状态下结果完全不同。比如「放款」这个操作合同状态是「待放款」才能执行执行后要同时改合同状态、生成还款计划、写资金流水、记操作日志这四件事必须在一个事务里。如果把这些逻辑散在 Controller 里后面加一个「放款失败回滚」的需求就会到处改。Service public class LoanDisbursementService { Transactional(rollbackFor Exception.class) public void disburse(Long contractId, BigDecimal amount) { // 1. 校验合同状态只有待放款才能执行 LoanContract contract contractMapper.selectById(contractId); if (!PENDING_DISBURSE.equals(contract.getStatus())) { throw new BizException(合同状态不允许放款); } // 2. 生成还款计划等额本息按月拆分 ListRepaymentPlan plans planGenerator.generate(contract); planMapper.batchInsert(plans); // 3. 写资金流水金额、时间、方向都要落库 FundFlow flow new FundFlow(contractId, amount, DISBURSE, new Date()); flowMapper.insert(flow); // 4. 更新合同状态乐观锁防止并发重复放款 int rows contractMapper.updateStatus(contractId, REPAYING, contract.getVersion()); if (rows 0) { throw new BizException(并发冲突请重试); } } }这段代码的关键点有三个Transactional保证四步操作原子性状态机校验放在最前面避免脏数据更新时带version字段做乐观锁防止两个操作员同时点放款。参数上rollbackFor Exception.class是必须的默认只回滚运行时异常业务异常如果不配就不会回滚这是血泪经验。2.2 技术选型对比MyBatis 和 JPA 在信贷场景下的取舍信贷系统的表结构复杂合同表、还款计划表、流水表之间关联多而且经常要做统计查询。MyBatis 的优势是 SQL 可控复杂查询写 XML 更直观JPA 的优势是单表 CRUD 快但多表关联和动态条件查询容易写成玄学。对比项MyBatisJPA/Hibernate复杂统计查询XML 手写 SQL可控Criteria API 冗长易出错单表增删改查需写 Mapper 方法继承 Repository 即可分页PageHelper 插件成熟Pageable 原生支持学习成本需懂 SQL需懂实体关系映射信贷场景推荐推荐尤其是还款计划批量插入适合简单管理后台我一般会选 MyBatis原因是还款计划生成后要批量插入几百条记录MyBatis 的batchInsert配合rewriteBatchedStatementstrue连接参数性能比 JPA 逐条 persist 高一个数量级。连接串里加这个参数jdbc:mysql://localhost:3306/loan_db?rewriteBatchedStatementstrueuseUnicodetruecharacterEncodingutf8rewriteBatchedStatementstrue会把多条 insert 合并成一条批量插入 500 条还款计划从 2 秒降到 200 毫秒左右。注意这个参数只对 MySQL 有效且要求 SQL 是insert into ... values (...),(...),(...)形式MyBatis 的foreach写法正好满足。2.3 数据库表设计合同、计划、流水三张表撑起整个信贷链路信贷系统的表不用多但核心三张表必须设计对。合同表存借款金额、利率、期限、状态还款计划表存每期应还本金、利息、到期日、实还状态资金流水表存每一笔钱的进出。三张表通过合同 ID 关联流水表还要区分「放款」「还款」「逾期罚息」三种方向。CREATE TABLE loan_contract ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, amount DECIMAL(15,2) NOT NULL, rate DECIMAL(8,6) NOT NULL COMMENT 年化利率, term INT NOT NULL COMMENT 期限月数, status VARCHAR(20) NOT NULL DEFAULT PENDING_REVIEW, version INT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE repayment_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, contract_id BIGINT NOT NULL, period INT NOT NULL COMMENT 第几期, principal DECIMAL(15,2) NOT NULL, interest DECIMAL(15,2) NOT NULL, due_date DATE NOT NULL, status VARCHAR(20) DEFAULT UNPAID, INDEX idx_contract (contract_id) );金额字段一律用DECIMAL不要用FLOAT或DOUBLE浮点数算利息会出现 0.01 的误差对账时就是灾难。version字段用于乐观锁contract_no加唯一索引防止重复生成合同。还款计划表的idx_contract索引必须有否则查某合同的所有期数会全表扫描。3. 授信审批与额度计算从申请到批复的代码落地3.1 授信申请流程的状态机设计授信申请不是一条直线而是一个状态机。典型状态包括DRAFT草稿、SUBMITTED已提交、REVIEWING审核中、APPROVED已批复、REJECTED已拒绝、CANCELLED已取消。每个状态只能由特定角色触发特定动作才能流转比如只有审核员能把REVIEWING改成APPROVED。源码里通常用一个CreditApplicationService来管状态流转核心方法是transition(applicationId, action)。action 不是状态本身而是「提交」「通过」「拒绝」这类动作由 Service 内部映射到目标状态。这样做的好处是前端只传动作不传状态避免前端乱传状态导致数据错乱。public enum ApplicationAction { SUBMIT, APPROVE, REJECT, CANCEL } public void transition(Long appId, ApplicationAction action, String operator) { CreditApplication app mapper.selectById(appId); String current app.getStatus(); String target; switch (action) { case SUBMIT: if (!DRAFT.equals(current)) throw new BizException(只有草稿可提交); target SUBMITTED; break; case APPROVE: if (!REVIEWING.equals(current)) throw new BizException(只有审核中可批复); target APPROVED; break; case REJECT: if (!REVIEWING.equals(current)) throw new BizException(只有审核中可拒绝); target REJECTED; break; default: throw new BizException(未知动作); } mapper.updateStatus(appId, target, operator); }这段代码把状态校验集中在 Service 里Controller 只传appId和action。参数上operator必须记录信贷系统所有状态变更都要留操作人这是合规要求。如果后面要加「退回修改」动作只需在 switch 里加一个 case不用改前端。3.2 额度计算收入、负债、征信三个因子的加权模型额度计算是信贷系统的核心算法。常见做法是基础额度 月收入 × 倍数再减去已有负债的月供最后乘以征信系数。倍数一般取 12 到 24征信系数根据征信等级取 0.6 到 1.0。public BigDecimal calculateLimit(BigDecimal monthlyIncome, BigDecimal monthlyDebt, String creditLevel) { // 基础倍数普通客户 12 倍优质客户 24 倍 BigDecimal multiplier A.equals(creditLevel) ? new BigDecimal(24) : new BigDecimal(12); BigDecimal base monthlyIncome.multiply(multiplier); // 减去已有负债的 12 倍月供 BigDecimal debtDeduction monthlyDebt.multiply(new BigDecimal(12)); // 征信系数 BigDecimal creditFactor getCreditFactor(creditLevel); BigDecimal limit base.subtract(debtDeduction).multiply(creditFactor); // 最低 5000最高 500000 if (limit.compareTo(new BigDecimal(5000)) 0) return BigDecimal.ZERO; return limit.min(new BigDecimal(500000)); } private BigDecimal getCreditFactor(String level) { switch (level) { case A: return new BigDecimal(1.0); case B: return new BigDecimal(0.8); case C: return new BigDecimal(0.6); default: return new BigDecimal(0.5); } }参数说明monthlyIncome是税后月收入monthlyDebt是现有贷款月供总和creditLevel来自征信查询结果。倍数和系数都是可配置的实际项目里会放到数据库或配置中心不要硬编码在代码里。注意limit.min()做上限截断compareTo做下限判断不要用equals比较 BigDecimal这是新手常翻车的地方。3.3 审批日志与操作留痕每一笔额度调整都要能追溯信贷系统里额度不是算出来就完事任何人工调整都要留日志。常见做法是建一张credit_audit_log表记录申请 ID、操作人、操作类型、调整前额度、调整后额度、原因、时间。CREATE TABLE credit_audit_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, application_id BIGINT NOT NULL, operator VARCHAR(64) NOT NULL, action VARCHAR(32) NOT NULL, before_limit DECIMAL(15,2), after_limit DECIMAL(15,2), remark VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_app (application_id) );写日志的时机是在额度变更的同一个事务里不能异步写否则事务回滚了日志还在对不上账。日志表只增不改不删查询时按application_id查全部历史。如果后面要做额度调整审批流这张表就是基础。4. 放款、还款与逾期资金链路里最容易出错的三个环节4.1 等额本息还款计划生成公式、精度与批量插入等额本息是信用贷款最常用的还款方式每期还款额相同但本金和利息的占比逐期变化。公式是每期还款额 贷款本金 × 月利率 × (1月利率)^期数 / ((1月利率)^期数 - 1)。源码里通常用BigDecimal实现最后一起的利息用减法倒推避免累计误差。public ListRepaymentPlan generate(BigDecimal principal, BigDecimal annualRate, int months) { BigDecimal monthlyRate annualRate.divide(new BigDecimal(12), 10, RoundingMode.HALF_UP); BigDecimal temp BigDecimal.ONE.add(monthlyRate).pow(months); BigDecimal monthlyPay principal.multiply(monthlyRate).multiply(temp) .divide(temp.subtract(BigDecimal.ONE), 2, RoundingMode.HALF_UP); ListRepaymentPlan plans new ArrayList(); BigDecimal remaining principal; for (int i 1; i months; i) { BigDecimal interest remaining.multiply(monthlyRate) .setScale(2, RoundingMode.HALF_UP); BigDecimal principalPart; if (i months) { // 最后一期本金 剩余本金利息 月供 - 本金 principalPart remaining; interest monthlyPay.subtract(principalPart); } else { principalPart monthlyPay.subtract(interest); } plans.add(new RepaymentPlan(i, principalPart, interest, monthlyPay)); remaining remaining.subtract(principalPart); } return plans; }关键参数monthlyRate保留 10 位小数中间计算不截断只在每期利息上setScale(2)。最后一期用减法倒推保证本金合计等于贷款总额。批量插入时用 MyBatis 的foreach配合前面说的rewriteBatchedStatementstrue。4.2 还款冲正与流水对账钱和账必须同时改还款操作要同时做三件事更新还款计划状态、写资金流水、更新合同剩余本金。这三件事必须在一个事务里而且要考虑「冲正」——如果还款入账后发现错误不能删记录要写一笔反向流水冲掉。Transactional(rollbackFor Exception.class) public void repay(Long planId, BigDecimal amount, String channel) { RepaymentPlan plan planMapper.selectById(planId); if (PAID.equals(plan.getStatus())) { throw new BizException(该期已还清); } // 写还款流水 FundFlow flow new FundFlow(plan.getContractId(), amount, REPAY, new Date()); flow.setChannel(channel); flowMapper.insert(flow); // 更新计划状态 planMapper.updateStatus(planId, PAID); // 更新合同剩余本金 contractMapper.reducePrincipal(plan.getContractId(), plan.getPrincipal()); }冲正的做法是再写一笔REPAY_REVERSE流水金额为负同时把计划状态改回UNPAID。不要用DELETE删流水信贷系统的流水表只允许 insert。参数上channel记录还款渠道对账时按渠道分组核对。4.3 逾期判定与罚息计算定时任务怎么写才不重不漏逾期判定一般用定时任务每天凌晨跑一次把到期日小于今天且状态为UNPAID的计划标记为OVERDUE同时计算罚息。罚息利率通常是正常利率的 1.5 倍按天计算。Scheduled(cron 0 0 1 * * ?) public void markOverdue() { ListRepaymentPlan overduePlans planMapper.selectOverdue(new Date()); for (RepaymentPlan plan : overduePlans) { BigDecimal penaltyRate plan.getRate() .multiply(new BigDecimal(1.5)) .divide(new BigDecimal(365), 10, RoundingMode.HALF_UP); long days ChronoUnit.DAYS.between(plan.getDueDate(), LocalDate.now()); BigDecimal penalty plan.getPrincipal() .multiply(penaltyRate) .multiply(new BigDecimal(days)) .setScale(2, RoundingMode.HALF_UP); planMapper.updateOverdue(plan.getId(), penalty); } }注意定时任务要加分布式锁否则多实例部署时会重复执行。cron表达式0 0 1 * * ?是每天凌晨 1 点避开业务高峰期。selectOverdue查询要加索引否则每天全表扫描还款计划表数据量大了会拖垮数据库。5. 避坑与排查信贷系统源码改造时最容易翻车的五个地方5.1 金额用 Double 导致对账差几分钱现象还款计划生成后本金合计和贷款金额差 0.01 到 0.05 元对账时怎么都平不了。原因Double和Float是二进制浮点数无法精确表示十进制小数累加误差会放大。解决所有金额字段用BigDecimal数据库用DECIMAL(15,2)Java 里new BigDecimal(0.1)而不是new BigDecimal(0.1)。中间计算保留 10 位小数最终结果再setScale(2)。5.2 状态流转没加乐观锁导致重复放款现象两个操作员同时点「放款」同一笔合同放款两次资金流水多了一笔。原因更新合同状态时没有并发控制两个线程都读到PENDING_DISBURSE都执行了更新。解决合同表加version字段更新时where id ? and version ?更新影响行数为 0 就抛异常回滚。这是信贷系统最经典的坑没有之一。5.3 还款计划批量插入没开 rewriteBatchedStatements现象生成 360 期还款计划插入耗时 3 秒以上接口超时。原因MySQL 驱动默认逐条发送 insert网络往返次数太多。解决连接串加rewriteBatchedStatementstrueMyBatis 的foreach写法会自动合并成一条多值 insert。注意这个参数对insert ... on duplicate key update也有效但要求 SQL 里没有select子句。5.4 定时任务多实例重复执行现象逾期标记任务在测试环境正常上线后罚息翻倍。原因生产环境部署了两个实例定时任务同时跑同一笔计划被标记两次。解决用 Redis 分布式锁或数据库行锁任务开始时抢锁抢不到就跳过。锁的过期时间要大于任务最长执行时间一般设 10 分钟。5.5 日志表异步写入导致事务回滚后日志还在现象放款失败回滚了但操作日志里有一条「放款成功」的记录审计时对不上。原因日志写入用了Async或消息队列和主事务不在一个事务里。解决信贷系统的关键操作日志必须和业务在同一个事务里同步写宁可慢一点也不能丢一致性。非关键日志比如页面访问日志可以异步但资金相关的绝对不行。6. 从能跑到能用信贷系统源码改造的验证清单与一个实用技巧拿到一套银行信用贷款系统源码改完之后怎么验证它是不是真的能用我一般会跑一个「全链路对账」脚本从授信申请开始走完审批、签约、放款、还款、逾期最后核对三张表的金额是否闭合。具体做法是申请一笔 10 万、12 期、年化 12% 的贷款生成还款计划后用代码校验「每期本金之和 100000」「每期利息之和 总利息」「每期月供之和 总还款额」三个等式必须严格相等差一分钱都算失败。public void verifyPlan(ListRepaymentPlan plans, BigDecimal principal) { BigDecimal sumPrincipal plans.stream() .map(RepaymentPlan::getPrincipal) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal sumInterest plans.stream() .map(RepaymentPlan::getInterest) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal sumPay plans.stream() .map(RepaymentPlan::getMonthlyPay) .reduce(BigDecimal.ZERO, BigDecimal::add); // 本金合计必须等于贷款金额 if (sumPrincipal.compareTo(principal) ! 0) { throw new BizException(本金合计不符: sumPrincipal); } // 月供合计必须等于本金加利息 if (sumPay.compareTo(sumPrincipal.add(sumInterest)) ! 0) { throw new BizException(月供合计不符); } }这个校验脚本我建议放在单元测试里每次改完还款计划生成逻辑都跑一遍。参数上principal传合同金额plans传生成结果三个reduce分别求和。注意compareTo返回 0 才算相等不要用equalsBigDecimal的equals会比较精度100.00和100.0不相等。另一个实用技巧是给所有状态字段加一个「状态流转图」的注释放在枚举类上面。信贷系统的状态多新人接手时最容易搞混哪个状态能转到哪个状态。我习惯在LoanStatus枚举里用注释画一个简单的流转关系比如DRAFT - SUBMITTED - REVIEWING - APPROVED - PENDING_DISBURSE - REPAYING - SETTLED逾期和取消作为分支。这样改代码时一眼就能看出当前状态能不能执行某个动作比翻文档快得多。最后说一个我自己的习惯每次改信贷系统的核心逻辑先在测试环境用真实数据跑一遍全链路把每一步的金额、状态、时间戳都打出来和改之前的日志逐行对比。信贷系统不怕慢就怕账对不上。对账对不平的时候不要猜把三张表的 SQL 拉出来一条条核对问题一定在某个setScale或者某个if分支里。希望帮到你。本文还有配套的精品资源点击获取