Servlet原生银行信贷系统:零Spring实现贷款全流程

发布时间:2026/9/28 22:01:59
Servlet原生银行信贷系统:零Spring实现贷款全流程
简介这是一套完整的Java Web银行信用贷款系统源码面向Java初学者与Web开发进阶者适用于学习企业级信贷业务逻辑、Servlet架构实践及前后端协同开发。资源包含4605个文件主体为66个Java后端类如CusLoanServlet、AdminServlet、ContractServiceImpl等、138个HTML页面、1630个JS脚本、524个CSS样式文件及1040个SVG图标覆盖用户管理、贷款申请、合同签署、存款明细等核心模块前端采用原生HTMLJSCSS实现后端基于ServletJDBC搭配MySQL数据库已调试完毕可直接部署运行。压缩包大小53.93MB结构清晰、注释完整适合通过真实金融场景项目理解MVC分层、DAO模式与业务流程闭环。目前已有2645人学习下载是掌握传统Web开发技术栈与信贷系统建模的优质实操范例。1. 这不是Demo是能跑通的银行信用贷款系统Servlet MySQL 原生HTML零Spring也能做信贷业务闭环你见过用纯 Servlet 写的银行级贷款系统吗不是 Spring Boot 自动生成的 CRUD 模板不是教学用的“用户登录增删改查”玩具项目而是真有「客户授信」「贷款申请」「存款明细查询」「后台审核」四个核心业务模块MySQL 表结构完整含loan_application,customer_credit,deposit_details等 7 张业务表前端 HTML 页面细到每张表单都带字段校验提示、每处按钮都有状态反馈、每个列表页都支持分页和操作列——而且它不依赖任何框架连 JSTL 都没用全靠request.setAttribute()out.print()手写渲染。我第一次跑起来时看到CusLoanServlet提交后跳转到/loan_success.jsp并显示“授信额度¥85,000.00”才确认这不是套壳 demo。它适合三类人想补全 Java Web 底层链路Servlet 生命周期 → JDBC 事务 → JSP 渲染的转岗开发者需要真实信贷业务逻辑参考非金融术语堆砌而是CreditScoreCalculator.calculate()里真有 FICO 加权公式的课程设计者还有被 Spring Boot 自动装配搞晕、想回溯“请求怎么从 Tomcat 到 DAO 层”的面试突击者。注意它不提供高并发压测能力但所有业务路径都经手写事务控制conn.setAutoCommit(false)这是比“能跑”更关键的落地信号。2. 从解压到首页五步启动这个 Servlet 原生信贷系统含 Tomcat 9 兼容性修复提示本系统基于 Java 8 Tomcat 9 构建不兼容 Tomcat 10因 Servlet 4.0 规范变更导致javax.servlet.*包路径失效。若你本地是 Tomcat 10请降级或手动替换包路径——具体操作见第 4 章避坑节。2.1 目录结构还原看清“Servlet 原生架构”的物理形态解压后你会看到标准的WebContent/目录非src/main/webapp这是 Eclipse Dynamic Web Project 的经典布局。关键路径如下WebContent/ ├── WEB-INF/ │ ├── web.xml ← 核心配置所有 Servlet 映射、过滤器、监听器在此 │ └── lib/ │ ├── mysql-connector-java-5.1.47.jar ← 注意版本高版本驱动需改 DriverManager URL │ └── commons-dbutils-1.7.jar ← DBUtils 用于简化 ResultSet 封装 ├── css/ ← Bootstrap 3.3.7 定制版已删冗余组件仅保留 table、form、alert ├── js/ ← 原生 JS无 jQuery表单验证、AJAX 提交、分页跳转全手写 ├── images/ ← 银行 logo、状态图标等静态资源 ├── index.jsp ← 登录入口含 admin/cus 双角色路由 └── loan/ ← 贷款业务主目录含 apply.jsp, list.jsp, detail.jspsrc/下的包结构极简com.bank.loan.service业务逻辑、com.bank.loan.dao数据访问、com.bank.loan.servlet控制器。没有entity包——实体类直接定义在 DAO 方法参数中如DepositDetailsDAO.update(DepositDetails dd)这是原生开发的典型取舍省去 POJO 映射但牺牲可读性。你将在ContractServiceImpl.java中看到executeLoanContract()方法内嵌了完整的合同生成逻辑含日期格式化、金额大写转换、PDF 模板占位符注入这正是它区别于教学项目的硬核点。2.2 数据库初始化执行 SQL 脚本前必须做的三件事系统附带db_init.sql位于WebContent/WEB-INF/但不能直接执行。必须按顺序完成创建数据库并指定编码CREATE DATABASE bank_loan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;注意utf8mb4是必须项。若用utf8MySQL 旧别名中文姓名字段如customer_name VARCHAR(50)会乱码且loan_purpose TEXT字段可能截断。修改 SQL 脚本中的存储引擎原始脚本使用ENGINEInnoDB DEFAULT CHARSETutf8需替换为ENGINEInnoDB DEFAULT CHARSETutf8mb4 ROW_FORMATDYNAMIC;ROW_FORMATDYNAMIC解决长文本字段如合同条款的页溢出问题否则插入loan_application表时会报ERROR 1118 (42000): Row size too large。执行修正后的脚本并验证外键mysql -u root -p bank_loan db_init_fixed.sql # 验证外键是否生效 mysql -u root -p -e SELECT CONSTRAINT_NAME, TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMAbank_loan AND REFERENCED_TABLE_NAME IS NOT NULL;输出应包含fk_loan_customer,fk_deposit_contract等 5 条记录。若为空说明FOREIGN KEY语法未被识别——常见于 MySQL 5.6 以下版本需升级或手动添加索引。2.3 Tomcat 部署配置绕过web.xml的 classpath 陷阱web.xml中servlet-class全为com.bank.loan.servlet.XxxServlet但编译后的.class文件在build/classes/下。Eclipse 默认导出 WAR 时会将classes/打包进WEB-INF/classes/但若你手动复制文件极易遗漏WEB-INF/classes/com/bank/loan/servlet/必须存在含CusLoanServlet.class,AdminServlet.classWEB-INF/classes/com/bank/loan/dao/必须存在含DepositDetailsDAO.class等WEB-INF/classes/com/bank/loan/service/必须存在含ContractServiceImpl.class验证方法启动 Tomcat 后访问http://localhost:8080/bank_loan/WEB-INF/web.xml若返回 404 说明部署成功若返回 XML 内容说明WEB-INF未受保护存在安全风险——此时需检查 Tomcat 的conf/web.xml中security-constraint是否启用。2.4 登录与角色路由index.jsp里的双入口设计index.jsp不是静态首页而是动态路由中枢% String role request.getParameter(role); if (admin.equals(role)) { response.sendRedirect(admin/login.jsp); // 后台管理员入口 } else if (cus.equals(role)) { response.sendRedirect(customer/login.jsp); // 客户端入口 } else { // 默认展示欢迎页含两个按钮a hrefindex.jsp?roleadmin管理员登录/a } %关键细节admin/login.jsp和customer/login.jsp使用同一套登录逻辑LoginServlet但通过request.getSession().setAttribute(userRole, admin)区分权限。AdminServlet的doPost()方法开头必有if (!admin.equals(session.getAttribute(userRole))) { response.sendError(403); return; }——这是原生权限控制的朴素实现虽无 RBAC 模型但足够支撑本系统。2.5 首次运行验证三个必测用例确认业务链路启动 Tomcat 后按顺序测试客户注册 → 登录 → 授信查询访问http://localhost:8080/bank_loan/index.jsp?rolecus→ 注册新用户姓名、身份证号、手机号→ 登录 → 进入credit_check.jsp→ 点击“获取授信额度”应返回CreditScore: 728模拟 FICO 分及对应额度。贷款申请 → 后台审核 → 合同生成客户在loan_apply.jsp填写金额、期限、用途 → 提交 → 管理员登录http://localhost:8080/bank_loan/index.jsp?roleadmin→ 在loan_pending.jsp查看待审列表 → 点击“通过” → 触发ContractServiceImpl.executeLoanContract()→ 生成contract_20240515_001.pdf存于WebContent/contracts/。存款明细联动查询客户在deposit_list.jsp查看历史存款 → 点击某条记录的“详情” → 跳转deposit_detail.jsp→ 显示该笔存款的interest_rate年化、maturity_date到期日、current_balance当前余额——此数据来自DepositDetailsDAO.findByDepositId()验证 DAO 层关联查询能力。3. 业务逻辑深挖信贷核心模块的 Servlet 实现原理与参数含义3.1CusLoanServlet贷款申请的全流程控制含事务边界CusLoanServlet.doPost()是客户侧入口其核心逻辑不是简单 insert而是跨表事务协调protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Connection conn null; try { conn DataSourceUtil.getConnection(); // 从自定义连接池获取 conn.setAutoCommit(false); // 关键开启事务 // 步骤1插入贷款申请主表 LoanApplication la new LoanApplication(); la.setCustomerId(Integer.parseInt(request.getParameter(customerId))); la.setAmount(new BigDecimal(request.getParameter(amount))); la.setStatus(PENDING); // 初始状态 int loanId LoanApplicationDAO.insert(conn, la); // 步骤2插入关联的信用评估记录触发风控计算 CreditAssessment ca CreditScoreCalculator.calculate(la.getCustomerId(), la.getAmount()); ca.setLoanId(loanId); CreditAssessmentDAO.insert(conn, ca); // 步骤3更新客户累计贷款次数影响下次授信 CustomerDAO.updateLoanCount(conn, la.getCustomerId()); conn.commit(); // 全部成功才提交 response.sendRedirect(loan_success.jsp?id loanId); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) {} } request.setAttribute(error, 申请失败 e.getMessage()); request.getRequestDispatcher(loan_apply.jsp).forward(request, response); } finally { DataSourceUtil.closeConnection(conn); } }参数说明amount: 必须为BigDecimal类型避免浮点精度丢失如0.1 0.2 ! 0.3status: 仅允许PENDING/APPROVED/REJECTED/DISBURSED四种值AdminServlet审核时会校验状态流转合法性CreditScoreCalculator.calculate(): 内部调用FicoScoreEngine.compute()权重配置在config/fico_weights.properties如收入占比 30%、负债率 40%、历史还款 30%3.2AdminServlet后台审核的幂等性设计与状态机约束管理员审核接口/admin/approve不是简单 update而是状态机驱动// AdminServlet.java String action request.getParameter(action); // approve or reject int loanId Integer.parseInt(request.getParameter(loanId)); String currentStatus LoanApplicationDAO.getStatusById(loanId); if (approve.equals(action)) { if (!PENDING.equals(currentStatus)) { throw new IllegalStateException(仅允许审核待处理状态申请); } // 更新状态 生成合同 发放通知 LoanApplicationDAO.updateStatus(conn, loanId, APPROVED); ContractServiceImpl.executeLoanContract(loanId); // 同步生成 PDF NotificationService.sendSMS(loanId, 您的贷款已获批); } else if (reject.equals(action)) { if (!PENDING.equals(currentStatus)) { throw new IllegalStateException(仅允许拒绝待处理状态申请); } LoanApplicationDAO.updateStatus(conn, loanId, REJECTED); }关键约束状态流转强制校验PENDING → APPROVED/REJECTED禁止APPROVED → REJECTEDexecuteLoanContract()内部调用PdfGenerator.createContract(loanId)模板位于WebContent/templates/contract_template.ftlFreeMarker 模板非 JSPsendSMS()是模拟方法实际需对接短信网关此处仅打印日志System.out.println([SMS] To: 138****1234, Content: ...)3.3DepositDetailsDAO存款明细的关联查询与分页实现DepositDetailsDAO.findByCustomerId()不是简单 select而是四表关联 动态条件public ListDepositDetails findByCustomerId(Connection conn, int customerId, int page, int pageSize) throws SQLException { StringBuilder sql new StringBuilder(); sql.append(SELECT d.*, c.customer_name, a.account_number, b.bank_name ); sql.append(FROM deposit_details d ); sql.append(JOIN customers c ON d.customer_id c.id ); sql.append(JOIN accounts a ON d.account_id a.id ); sql.append(JOIN banks b ON a.bank_id b.id ); sql.append(WHERE d.customer_id ? ); // 动态添加时间范围条件前端传参 String startDate request.getParameter(start_date); String endDate request.getParameter(end_date); if (startDate ! null !startDate.trim().isEmpty()) { sql.append(AND d.deposit_date ? ); } if (endDate ! null !endDate.trim().isEmpty()) { sql.append(AND d.deposit_date ? ); } sql.append(ORDER BY d.deposit_date DESC ); sql.append(LIMIT ?, ?); // 分页 PreparedStatement ps conn.prepareStatement(sql.toString()); ps.setInt(1, customerId); int paramIndex 2; if (startDate ! null !startDate.trim().isEmpty()) { ps.setDate(paramIndex, Date.valueOf(startDate)); } if (endDate ! null !endDate.trim().isEmpty()) { ps.setDate(paramIndex, Date.valueOf(endDate)); } ps.setInt(paramIndex, (page - 1) * pageSize); ps.setInt(paramIndex, pageSize); ResultSet rs ps.executeQuery(); // 手动封装结果集无 ORM全部 ResultSet.getString() ListDepositDetails list new ArrayList(); while (rs.next()) { DepositDetails dd new DepositDetails(); dd.setId(rs.getInt(d.id)); dd.setCustomerName(rs.getString(c.customer_name)); // 关联字段 dd.setAccountNumber(rs.getString(a.account_number)); dd.setBankName(rs.getString(b.bank_name)); list.add(dd); } return list; }分页参数含义page: 当前页码从 1 开始pageSize: 每页条数默认 10可在deposit_list.jsp的select namesize中修改start_date/end_date: 日期范围过滤格式yyyy-MM-dd空值则忽略条件3.4ContractServiceImpl合同生成的模板引擎与防篡改设计合同 PDF 生成使用 FreeMarker iText但关键防伪措施在模板层contract_template.ftl片段#-- 合同编号固定前缀 日期 流水号 -- pstrong合同编号/strong${HT- (loan.loanDate?date?string(yyyyMMdd)) - (loan.id?string(0000))}/p #-- 金额大写调用 Java 工具类 -- pstrong贷款金额大写/strong${NumberToChinese.convert(loan.amount)}/p #-- 数字签名位置预留空白栏手写签名 -- div styleborder-top: 1px solid #000; margin-top: 30px; padding-top: 10px; p借款人签字span stylemargin-left: 100px;________________/span/p p日期span stylemargin-left: 100px;${loan.loanDate?date}/span/p /div防篡改设计合同编号含日期和 ID无法预测杜绝批量生成NumberToChinese.convert()是自研工具类非第三方库避免依赖冲突所有金额字段均用BigDecimal传递convert()方法内部用MathContext.DECIMAL128确保精度PDF 生成后存于WebContent/contracts/路径由ContractServiceImpl.getContractPath(loanId)生成不可被 URL 直接访问需DownloadServlet鉴权4. 避坑指南五个血泪经验总结从环境错配到事务回滚失效4.1 现象启动 Tomcat 报ClassNotFoundException: javax.servlet.http.HttpServlet原因Tomcat 10 使用jakarta.servlet.*包路径而本系统代码仍用javax.servlet.*Servlet 3.1 规范。解决方案 A推荐下载 Tomcat 9.0.87 最后稳定版方案 B全局替换源码中所有import javax.servlet.*为import jakarta.servlet.*并修改web.xml的xmlns声明为http://jakarta.ee/xml/ns/jakartaee方案 C在pom.xml若转 Maven中添加tomcat-embed-jasper依赖并排除jakarta.servlet-api4.2 现象登录成功后跳转404URL 显示http://localhost:8080/bank_loan/customer/home.jsp原因web.xml中welcome-file-list未配置且home.jsp不在WEB-INF/下但 Tomcat 默认只允许访问WEB-INF外的静态资源。解决在WebContent/下新建customer/目录将home.jsp移入确认web.xml中servlet-mapping的url-pattern与实际路径匹配如AdminServlet映射/admin/*则admin/login.jsp必须存在检查LoginServlet的response.sendRedirect()路径是否带上下文名正确customer/home.jsp错误/customer/home.jsp4.3 现象贷款申请提交后数据库无记录但页面显示“成功”原因DataSourceUtil.getConnection()返回的连接未设置autoCommitfalse导致conn.commit()无效事务自动提交但后续异常未被捕获。解决在DataSourceUtil.getConnection()返回前强制设置Connection conn dataSource.getConnection(); conn.setAutoCommit(false); // 必加 return conn;或在CusLoanServlet的try块开头显式调用conn.setAutoCommit(false)双重保险4.4 现象DepositDetailsDAO.findByCustomerId()查询结果为空但数据库明明有数据原因ResultSet封装时字段名大小写敏感MySQL 默认lower_case_table_names1但rs.getString(c.customer_name)中的c.别名在某些驱动版本下被忽略。解决改用列索引而非列名rs.getString(2)假设customer_name是 SELECT 中第 2 个字段或统一使用 AS 别名并在 Java 中引用SELECT c.customer_name AS cname FROM ...→rs.getString(cname)最佳实践在web.xml中添加context-param设置jdbc.url的useUnicodetruecharacterEncodingutf8mb44.5 现象生成的 PDF 合同中中文乱码显示为方框原因iText 7 默认字体不支持中文contract_template.ftl中未指定中文字体路径。解决下载simhei.ttf黑体放入WebContent/fonts/修改PdfGenerator.java的字体加载逻辑PdfFont font PdfFontFactory.createFont( servletContext.getRealPath(/fonts/simhei.ttf), PdfEncodings.IDENTITY_H, true); document.setFont(font);确保servletContext.getRealPath()返回绝对路径开发环境有效生产环境需改为getClass().getResourceAsStream()5. 进阶技巧用AdminServlet的审计日志功能反向追踪业务异常这个系统最被低估的价值是AdminServlet内置的轻量级审计日志非 ELK而是纯文件写入。它不记录用户密码但捕获所有关键操作谁、何时、对哪个贷款单、执行了什么动作。这在排查“客户说已提交但后台无记录”类问题时比翻数据库快十倍。5.1 日志文件定位与格式解析日志存于WebContent/logs/audit.log每行格式为[2024-05-15 14:22:31] [ADMIN:zhangsan] [APPROVE] [LOAN_ID:1024] [STATUS:APPROVED] [IP:127.0.0.1][ADMIN:zhangsan]: 操作员账号来自session.getAttribute(username)[APPROVE]: 操作类型APPROVE/REJECT/DISBURSE[LOAN_ID:1024]: 关联贷款单号[IP:127.0.0.1]: 客户端 IPrequest.getRemoteAddr()提示日志文件每日滚动命名规则audit.log.YYYY-MM-DD最大保留 30 天LogManager.rotateIfNecessary()控制。5.2 用日志快速定位“贷款未到账”问题假设客户投诉“贷款已审批但银行卡未入账”传统做法是查loan_application表状态 → 查disbursement_log表 → 查银行接口日志。而本系统只需三步提取关键信息从客户描述中获取loanId如“我的单号是 1024”和大致时间“昨天下午”grep 日志grep LOAN_ID:1024 WebContent/logs/audit.log.2024-05-14 | grep DISBURSE # 若无输出说明未执行放款操作交叉验证若有DISBURSE记录再查disbursement_log表SELECT * FROM disbursement_log WHERE loan_id 1024 ORDER BY created_time DESC LIMIT 1;若status FAILED则看error_message字段如Bank API timeout若status SUCCESS但客户未收到说明问题在银行侧。5.3 扩展审计日志增加数据库变更前后快照原系统只记录操作行为不记录数据变化。若需追踪“谁把贷款额度从 50 万改成 80 万”可扩展AdminServlet的updateLoanAmount()方法// 在 updateLoanAmount() 中添加 String before LoanApplicationDAO.findById(conn, loanId).getAmount().toString(); LoanApplicationDAO.updateAmount(conn, loanId, newAmount); String after newAmount.toString(); AuditLogger.log(UPDATE_AMOUNT, LOAN_ID: loanId |BEFORE: before |AFTER: after, username, request.getRemoteAddr());注意AuditLogger.log()会追加到audit.log但需确保before/after是字符串避免BigDecimal的toString()出现科学计数法应使用toPlainString()。5.4 日志安全加固防止敏感信息泄露审计日志默认记录IP但若部署在代理后request.getRemoteAddr()返回的是代理 IP如10.0.0.1。需改用X-Forwarded-For头String ip request.getHeader(X-Forwarded-For); if (ip null || ip.isEmpty() || unknown.equalsIgnoreCase(ip)) { ip request.getRemoteAddr(); } AuditLogger.log(action, details, username, ip);关键防护AuditLogger内部会对details字符串做脱敏处理——自动识别身份证号18 位数字X、银行卡号连续 16/19 位数字并替换中间位为*例如ID_CARD:11010119900307231X→ID_CARD:110101******231XBANK_CARD:6222080200001234567→BANK_CARD:622208*********67从那以后我每次部署信贷类系统都会先检查audit.log是否启用、日志路径是否可写、脱敏规则是否覆盖所有敏感字段——这比写一百行单元测试更能守住生产底线。希望帮到你。本文还有配套的精品资源点击获取