☰
Servlet+JSP+MySQL贷款管理系统课设:从表设计到事务实现
2026/10/18 1:46:56 网站建设 项目流程

简介:面向数据库课程设计的在校大学生贷款管理系统,提供完整Java源码与配套说明书,适合本科或高职学生作为期末项目参考。系统基于Java+SQL实现,采用MVC模式分离业务、界面与数据访问,核心功能涵盖用户登录注册、贷款申请、管理员审批、还款计划与数据统计。数据库设计包含学生信息、贷款申请、贷款还款、用户权限四类主要表结构,体现典型管理信息系统开发思路。压缩包共46个文件,其中包含18个java源文件、20个class编译文件、1个SQL建库脚本、1个doc格式说明书以及Eclipse工程配置,整体仅1.1MB,可直接导入开发工具运行调试。已有1510人学习下载,实用性得到验证。借助源码与说明书,可快速理解从建表到功能实现的全过程,并可在此基础上扩展或改造,完成自己的课程设计报告与演示。

1. 贷款管理系统不是黑匣子:两个角色、三张表、一条审批链

如果你做过数据库课设,多少会有这种感觉:选题一听就懂,一动手就卡在「数据怎么流、状态怎么翻、页面怎么串」上。在校大学生贷款管理系统就是这样一个典型课程设计选题——学生端能注册、登录、提交贷款申请、查审批结果和还款计划,管理员端能审核申请、管理用户、生成每期应还明细。整个系统从功能上拆开,就是「两个角色 + 三张核心表 + 一条审批链路」,没有高深算法,没有黑科技,但它把数据库表设计、JDBC 操作、Servlet 请求流转、JSP 页面展示全部串了一遍。本文就用一套可复现的源码加说明书来拆解这个系统,适合正在做课设或者想拿一个完整项目练手的开发者照着重写一遍。

2. 技术栈与架构设计:Servlet + JSP + MySQL 的分层分工

2.1 为什么不用 Spring Boot:课设场景下的技术选型

很多同学拿到课设第一反应是「上 Spring Boot」,但仔细想想答辩时被问的问题:Maven 依赖谁下载的、自动配置干了什么、MyBatis 的代理接口怎么生成的——答不上来反而扣分。这个项目走的是 Java Web 最朴素的一条路:JDK 8 + Tomcat 8.5 + Servlet 3.1 + JSP + JSTL + MySQL 5.7,数据库访问用原生 JDBC 封装。这套组合的好处是每一行代码都能解释清楚,整个请求链路从 Tomcat 接收 HTTP 请求到 Servlet 里取参数、调 Service、操作 DAO、返回 JSP,没有任何黑匣子。

同时它也不是完全原始的裸 JDBC。项目里做了基础的 MVC 分层:Servlet 只做参数接收和页面转发,业务校验放在 Service 层,SQL 全部收拢在 DAO 层,实体类单独一个 entity 包。这样分工对一个课设来说恰到好处——前端 JSP 里的<% %>脚本只做循环展示,不写业务;Service 层里只处理状态判断和金额计算,不碰 Connection。前端没有上框架,用 JSP + Bootstrap + jQuery,因为演示场景里用户操作集中在表单提交和列表刷新,原生写法维护成本最低,答辩时也最好讲。

2.2 目录结构与分层职责:源码包拿到手先看什么

拿到源码包第一件事不是双击运行,而是先对着目录结构确认「哪个文件管什么」。这套系统的目录长这样:

loan-system/ ├── sql/ │ └── loan_system.sql # 建库建表脚本 + 演示账号 ├── src/ │ └── com/loan/demo/ │ ├── entity/ # User、LoanApplication、RepaymentPlan │ ├── dao/ # UserDao、LoanDao、RepaymentDao │ ├── service/ # AuthService、LoanService │ ├── servlet/ # 登录、注册、申请、列表、审核 │ ├── filter/ # EncodingFilter、AuthFilter │ └── util/ # DBUtil、Md5Util └── web/ ├── static/ # css、js、images ├── WEB-INF/ │ ├── web.xml │ └── lib/ # mysql-connector-java 等 jar 包 ├── login.jsp、register.jsp ├── student/ # 学生端页面:apply.jsp、myLoans.jsp └── admin/ # 管理端页面:auditList.jsp、userList.jsp

entity 包里三个类对应数据库三张表,字段和表结构一一映射,没有用 ORM 框架,所以字段名必须手写对齐。dao 包里每个类只负责一张表的增删改查,方法粒度很细:findById、findByUsername、insertApplication、updateStatus、listByUserId。service 包负责组合这些 DAO 方法,比如 LoanService 里的apply方法会先校验金额再调 LoanDao 插入申请,audit方法会更新状态再生成还款计划。filter 包解决两个横切问题:字符编码和登录拦截。util 包里的 DBUtil 是全局共用的数据库连接工厂,Md5Util 对用户密码做哈希,避免明文入库。

如果拿到手的源码版本里有额外的包,比如vo/或dto/,通常是作者把「页面展示用的组合字段」单独抽出来了,不影响整体分层理解。课设项目改包名时记得同步改 web.xml 里 servlet-class 的完整类名,这是最容易漏的一步。

2.3 数据库设计:三张表撑起两个状态机

这套系统的核心是数据模型,设计好了业务代码就是一层皮。三张表的关系可以概括成:用户表存「谁在操作」,贷款申请表存「借了多少钱、借多久、批没批」,还款计划表存「批了之后分几期还、每期还多少」。建表脚本大致长这样:

CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 哈希,不存明文 role VARCHAR(16) NOT NULL DEFAULT 'student', real_name VARCHAR(32), student_no VARCHAR(20), major VARCHAR(64), phone VARCHAR(20), status TINYINT DEFAULT 1, -- 1 正常,0 禁用 created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE loan_application ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, -- 金额用 DECIMAL,不用 FLOAT term_months INT NOT NULL, -- 贷款期限,单位:月 purpose VARCHAR(255), -- 贷款用途 annual_rate DECIMAL(5,2) NOT NULL DEFAULT 4.75, status VARCHAR(16) NOT NULL DEFAULT 'PENDING', reject_reason VARCHAR(255), apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME, auditor_id INT, CONSTRAINT fk_loan_user FOREIGN KEY (user_id) REFERENCES user(id) ); CREATE TABLE repayment_plan ( id INT PRIMARY KEY AUTO_INCREMENT, application_id INT NOT NULL, period_no INT NOT NULL, -- 第几期 due_date DATE NOT NULL, principal DECIMAL(10,2), -- 本期本金 interest DECIMAL(10,2), -- 本期利息 total DECIMAL(10,2), -- 本期应还总额 status VARCHAR(16) DEFAULT 'UNPAID', -- UNPAID / PAID CONSTRAINT fk_plan_loan FOREIGN KEY (application_id) REFERENCES loan_application(id) );
字段类型选择为什么这么定
amountDECIMAL(10,2)用 FLOAT 会有精度漂移,答辩时演示 0.1 + 0.2 就翻车
statusVARCHAR比 TINYINT 可读性好,PENDING/APPROVED/REJECTED 直接看库就懂
annual_rateDECIMAL(5,2)利率固定到两位小数,课设不做浮动利率
created_atDATETIMECURRENT_TIMESTAMP 自动填充,减少代码里手动塞时间

这里隐藏了两个状态机。第一个在 loan_application 表上:贷款申请从 PENDING(待审核)出发,只能往 APPROVED(通过)或 REJECTED(驳回)两条路走,驳回时必须同时记录 reject_reason。第二个在 repayment_plan 表上:审核通过时按贷款期限一次性生成 N 期还款记录,状态统一为 UNPAID,后续按期手动标记为 PAID。两个状态机的切换逻辑全部放在 Service 层,DAO 不做状态判断,这样 DBA 看表结构、开发看业务逻辑互不干扰。

默认利率 4.75% 是写死在 application 表里的字段而非代码常量,原因是利率属于贷款合同数据——今天申请的按 4.75%,下周利率调整后新申请应该记 5.2%,后续对账要以申请时记录的利率为准。这个细节在答辩时说出来会让老师觉得你想过业务场景。

3. 核心链路拆解:一笔贷款从提交申请到生成还款计划

3.1 登录拦截与字符编码:两个 Filter 解决 80% 的课设翻车点

课设系统最常见的两个「玄学问题」——中文乱码和未登录却能访问学生页面——都靠 Filter 在请求进入 Servlet 之前统一处理。先看编码过滤器:

public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; request.setCharacterEncoding("UTF-8"); // 处理 POST 表单参数 resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(request, resp); // 放行到后续 Servlet } }

这个过滤器必须在 web.xml 里注册,并且filter-mapping的 url-pattern 写成/*,保证所有请求都过一遍编码设置。注意setCharacterEncoding必须在读取 request 参数之前调用,否则已经按 ISO-8859-1 解析过的中文就回不来了。POST 表单乱码靠它解决,但 GET 请求的 URL 参数乱码得去 Tomcat 的 server.xml 里给 Connector 加URIEncoding="UTF-8",这个后面避坑章节会细说。

登录拦截器是另一个关键 Filter:它检查 session 里有没有登录用户,没有就重定向到 login.jsp。但要注意放行名单里除了登录页和注册接口,还必须把 css、js、图片等静态资源也放出去,否则页面样式全部丢失:

public void doFilter(...) { HttpSession session = request.getSession(false); String path = request.getRequestURI(); boolean isStatic = path.startsWith(request.getContextPath() + "/static/"); boolean isPublic = path.endsWith("/login.jsp") || path.endsWith("/register.jsp") || path.contains("/login") || path.contains("/register"); if (session != null && session.getAttribute("loginUser") != null) { chain.doFilter(request, response); // 已登录,直接放行 } else if (isStatic || isPublic) { chain.doFilter(request, response); // 公共资源,放行 } else { response.sendRedirect(request.getContextPath() + "/login.jsp"); } }

逻辑说明:先判断 session 是否存在且有效,再判断请求路径是否属于放行名单,两者都不满足才踢回登录页。参数上getSession(false)的 false 是关键——不主动创建新 session,避免未登录用户每次请求都产生一个无用的 Session 对象。静态资源路径的放行判断统一走/static/前缀,这也是为什么静态文件不放 WEB-INF 下的原因,因为 WEB-INF 目录天然拒绝外部直接访问,但 Filter 拦截的是所有请求,两种方式结合最稳妥。

3.2 学生提交贷款申请:参数校验、业务判断、数据落库三段式

贷款申请是系统的核心写入操作。从前端表单提交到数据落库,链路是 apply.jsp 表单 → ApplyServlet → LoanService.apply() → LoanDao.insert()。Servlet 只做三件事:取参数、包成对象、调用 Service 后决定跳转页面:

@WebServlet("/student/apply") public class ApplyServlet extends HttpServlet { private LoanService loanService = new LoanService(); protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String amountStr = req.getParameter("amount"); String termStr = req.getParameter("termMonths"); String purpose = req.getParameter("purpose"); User loginUser = (User) req.getSession().getAttribute("loginUser"); try { loanService.apply(loginUser.getId(), new BigDecimal(amountStr), Integer.parseInt(termStr), purpose); resp.sendRedirect(req.getContextPath() + "/student/myLoans.jsp"); } catch (IllegalArgumentException e) { req.setAttribute("error", e.getMessage()); req.getRequestDispatcher("/student/apply.jsp").forward(req, resp); } } }

参数说明:amount前端传来的金额字符串,用 BigDecimal 构造避免 double 精度问题;termMonths是贷款期限,单位月;loginUser从 session 取,绝不信任前端传来的 userId 参数——这是权限安全的基础意识,课设答辩常问到这里。业务异常用 IllegalArgumentException 抛出后转发回申请页并在表单上方显示错误信息,非法数据不会进入下一层。

真正的业务校验在 Service,这也是看这个项目最值得学的一段:

public void apply(int userId, BigDecimal amount, int termMonths, String purpose) { if (amount.compareTo(new BigDecimal("1000")) < 0 || amount.compareTo(new BigDecimal("50000")) > 0) { throw new IllegalArgumentException("贷款金额必须在 1000 ~ 50000 之间"); } if (termMonths != 12 && termMonths != 24 && termMonths != 36 && termMonths != 48) { throw new IllegalArgumentException("贷款期限只支持 12/24/36/48 个月"); } LoanApplication app = new LoanApplication(); app.setUserId(userId); app.setAmount(amount); app.setTermMonths(termMonths); app.setPurpose(purpose); app.setStatus("PENDING"); loanDao.insert(app); }

校验注意点:金额比较用compareTo而不是equals,因为 BigDecimal 的 equals 会连带比较精度——new BigDecimal("10.0")和new BigDecimal("10.00")会被判定为不相等,而 compareTo 只看数值大小。期限判断限制死四档,这样后续生成还款计划的期数可穷举,避免出现 13 期、47 期这类没法画计划表的脏数据。

3.3 管理员审核:状态翻转与还款计划生成的事务边界

审核是整个系统最考验事务功底的地方。管理员在 auditList.jsp 点击「通过」后,后端要做三件必须同时成功的事:把申请状态改成 APPROVED、记录审核时间和审核人、按等额本息生成全部期数的还款计划。任何一步失败都要回滚,否则会出现「状态是通过但还款计划缺失」的脏数据。核心代码:

public void approve(int applicationId, int auditorId) { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 第一步:翻转状态 int rows = loanDao.updateStatus(conn, applicationId, "APPROVED", auditorId); if (rows != 1) { throw new RuntimeException("申请状态已变更,请刷新后重试"); } // 第二步:读取申请信息,用于计算还款计划 LoanApplication app = loanDao.findById(conn, applicationId); // 第三步:删除旧计划(防重复审核产生的脏数据),再重新生成 repaymentDao.deleteByApplicationId(conn, applicationId); List<RepaymentPlan> plans = calcRepaymentPlan(app); for (RepaymentPlan plan : plans) { repaymentDao.insert(conn, plan); } conn.commit(); } catch (Exception e) { conn.rollback(); throw new RuntimeException("审核失败,已回滚事务", e); } finally { DBUtil.close(conn); } }

逻辑说明:conn.setAutoCommit(false)之后,手动控制 commit 和 rollback,DAO 方法全部改为接收 Connection 参数,保证它们使用的是同一个数据库连接。updateStatus返回影响行数,如果不等于 1,说明这条申请的状态在管理员点「通过」之前已经被别人改过(比如另一台浏览器设备刚好点了驳回),此时直接抛异常回滚而不是硬覆盖,这就是最简单的乐观锁思想。spring 里叫 @Transactional 注解,课设里用 try-catch + 手写回滚反而更看得懂。

等额本息计算是这段代码的数学核心,也是金融业务里最容易算错的地方:

private List<RepaymentPlan> calcRepaymentPlan(LoanApplication app) { BigDecimal principal = app.getAmount(); BigDecimal monthlyRate = app.getAnnualRate() .divide(new BigDecimal("12"), 6, RoundingMode.HALF_UP) .divide(new BigDecimal("100")); int months = app.getTermMonths(); // 等额本息:每期还款额 = 本金 * 月利率 * (1+月利率)^期数 / [(1+月利率)^期数 - 1] BigDecimal pow = BigDecimal.valueOf(Math.pow(1 + monthlyRate.doubleValue(), months)); BigDecimal numerator = principal.multiply(monthlyRate).multiply(pow); BigDecimal denominator = pow.subtract(BigDecimal.ONE); BigDecimal monthlyPayment = numerator.divide(denominator, 2, RoundingMode.HALF_UP); // ...逐期拆分本金和利息,最后一期做尾差修正 }

参数说明:年利率 4.75 先除以 12 换算成月利率,除以 100 是把百分比数字变成小数;除法指定 6 位小数精度防止中间计算溢出;Math.pow计算复利系数。这里有个「最后一期尾差修正」的细节——按照等额本息公式每期都四舍五入到分,最后一期会因为之前每期舍入误差累积导致总和差几分钱,所以最后一期的本金用应还总额 - 已还本金反推,保证所有期数的本金加起来严格等于贷款金额。答辩时说出这个细节,老师会高看你一眼。

4. 运行指南:建库、连接、部署与一次完整的演示走位

4.1 初始化数据库:用脚本而不是手动建表

源码包的 sql 目录下有一个完整脚本,包含建库、建表、插入演示数据。优先用命令行导入,比在 Navicat 里一句句复制粘贴更接近真实工作流:

mysql -u root -p < loan_system.sql

脚本里除了三张表的 CREATE 语句,还有两条演示账号:学生账号student01(密码默认为123456)和管理员账号admin01(密码默认为admin123)。导入完成后可以进 MySQL 验证一下:

USE loan_system; SHOW TABLES; -- 应该看到三张表 SELECT * FROM user; -- 检查演示账号是否插入成功

注意脚本开头如果有CREATE DATABASE语句,执行时需要确保当前 MySQL 账号有建库权限。课设环境里 root 肯定没问题,但如果学校机房用的受限账号,可以手动只执行后面的建表部分。脚本里所有表都带DROP TABLE IF EXISTS前缀,所以重复执行不会报错,这点对反复调试很友好。

4.2 改数据库连接:DBUtil 是第一个要动的文件

连接信息集中在 DBUtil.java 里,也是拿到源码后你必然要修改的文件。这是 JDBC 连接 MySQL 的核心代码:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/loan_system" + "?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

三个参数逐个解释:useSSL=false关掉 MySQL 8.x 默认的 SSL 握手,避免本地开发输出大段 SSL 证书警告;characterEncoding=utf8让 JDBC 连接层和数据库字符集保持一致,这是中文数据不乱码的前提之一;serverTimezone=Asia/Shanghai是针对新版驱动和 MySQL 5.7+ 时区差异的修复,不写的话通常报The server time zone value '�й���׼ʱ��' is unrecognized——这个报错信息本身也是乱码,很容易误导排查方向。如果本机 MySQL 的 root 密码不是 123456,改这一处即可。

如果在 IDEA 里用 Maven 或 Gradle 导入,驱动依赖已经由 pom 管理;如果收到的是非 Maven 源码包,驱动 jar 直接放在 web/WEB-INF/lib 目录下,DBUtil 里也不需要手动Class.forName——JDBC 4.0 起驱动类通过 SPI 自动注册,只要 jar 在 classpath 里就行。

4.3 IDEA 部署到 Tomcat:war exploded 是调试最快的模式

这个项目是 Web 应用,不是普通 Java 程序,运行方式是在 IDEA 里配置 Tomcat。先说一个容易被忽略的顺序问题:先装好本地 Tomcat,再在 IDEA 里配置。步骤建议严格按下面的顺序走:

  1. 打开项目后,确认Project Structure里 Project SDK 选的是 JDK 8(Tomcat 8.5 不支持 JDK 11 以下的某些运行参数组合,JDK 8 最稳)。
  2. 如果源码包没带 Maven 配置,需要手动把 src 目录 Mark asSources Root,把 web 目录 Mark asWeb Resources Directory。
  3. 如果 lib 目录下 jar 包没有蓝色小茶壶图标,全选后右键Add as Library。
  4. Run -> Edit Configurations -> + -> Tomcat Server -> Local,Server 选项卡里指向本地 Tomcat 安装目录。
  5. Deployment选项卡里点+ -> Artifact,选择loan-system:war exploded,Application context 填/loan。
  6. 启动前确认下方Before launch里有Build Artifact的步骤,否则修改 Java 代码后热部署不生效。

war exploded 是解压目录形式,相比 war 包它允许 IDEA 增量更新 class 和资源文件。修改 JSP 后刷新浏览器即可,修改 Java 类后点 Tomcat 面板的Update按钮就能重新加载,不用反复重启——课设调试期能省大量时间。如果选择打包 war 再部署,每次都走完整构建周期,JSP 改一处就要重打一次包,血泪教训。

4.4 演示走位:把整个业务流串一遍

系统跑起来后,建议按这条路径做一次完整的「半分钟演示」,它覆盖了除注册外的所有核心功能:

  1. 浏览器访问http://localhost:8080/loan/login.jsp,系统自动跳转到登录页(说明 AuthFilter 正常拦截)。
  2. 用student01登录,进学生端首页,提交一笔金额 12000、期限 24 期的贷款申请,用途写「学费缴纳」。
  3. 退出登录(销毁 session),用admin01登录,进入待审核列表,能看到刚才提交的 PENDING 状态的申请。
  4. 点击「通过」,系统生成 24 条还款计划记录。
  5. 退出管理员,再用student01登录,进入「我的贷款」页面,能看到状态变成 APPROVED,下方出现完整的还款计划表,每期金额相同(等额本息),最后一期可能与前面差几分钱——这是尾差修正后的正常现象。

演示过程中如果某一步跳转异常,优先看 IDEA 控制台的异常堆栈,而不是先怀疑代码逻辑。课设阶段遇到的 80% 问题都能从「哪一行抛异常、SQL 语句和预编译参数是什么、数据库当前数据是什么状态」这三个方向定位出来。

5. 避坑与排查:六个让课设当场翻车的现场

5.1 中文乱码:页面、Tomcat、JDBC 三层编码不一致

现象:登录后学生姓名和贷款用途全是问号或乱码,入库后 Navicat 里看到的是???。

原因:编码在三层各管一摊——JSP 页面声明的编码、Tomcat 解析 HTTP 请求的编码、JDBC 连接 MySQL 时声明的编码,任何一层不一致都会乱码。最常见的是漏配 EncodingFilter,或者 Tomcat 没有配置 URI 编码。

解决:三层统一 UTF-8。JSP 顶部加<%@ page contentType="text/html;charset=UTF-8" %>;Tomcat 的 server.xml 里 Connector 加URIEncoding="UTF-8";JDBC URL 带上characterEncoding=utf8。改完这三处还乱,检查一下 MySQL 建库时的 DEFAULT CHARSET 是不是 utf8mb4,这三层对齐后乱码基本绝迹。

5.2 一启动就报 ClassNotFoundException: com.mysql.jdbc.Driver

现象:Tomcat 启动或首次访问数据库相关页面时抛出找不到驱动类。

原因:分两种,一是驱动 jar 没有添加到模块依赖里,IDEA 里表现为 jar 包没有显示成可展开的库;二是代码里写了Class.forName("com.mysql.jdbc.Driver"),但 MySQL 8.x 驱动已经把类迁移到了com.mysql.cj.jdbc.Driver。

解决:检查 web/WEB-INF/lib 下是否有 mysql-connector-java 的 jar,并且 IDEA 的 Artifacts 输出里包含它。如果是 MySQL 8 的驱动,推荐直接干掉Class.forName那行,JDBC 4.0 后驱动会自动注册。排查完依然报错,去Project Structure -> Artifacts -> 选中项目 -> Output Layout里确认 lib 目录下的 jar 是不是在打包范围内——IDEA 有时不会自动带上手动 add 的库。

5.3 SQLException: Zero date value prohibited

现象:查询还款计划时抛Zero date value prohibited,或者看到日期字段全是0000-00-00。

原因:MySQL 里某些记录的时间字段是零值(常见于被手工改过数据或批量脚本导入),而 JDBC 8 驱动默认把 0000-00-00 当非法日期处理。另外 serverTimezone 配错也会给出误导性的报错信息。

解决:JDBC URL 加参数zeroDateTimeBehavior=convertToNull,把零值日期自动转成 null,加上前面提过的serverTimezone=Asia/Shanghai,两个参数一起加。更彻底的修法是写 SQL 时对允许为空的 DATETIME 字段用IFNULL(字段, NULL)包一层,前端展示时对 null 输出「未填写」。

5.4 外键约束删不掉用户:删除前先看子表

现象:管理员在用户管理里删除一个学生用户,报Cannot delete or update a parent row。

原因:loan_application 表的外键fk_loan_user引用了 user.id,学生如果提交过贷款申请,直接删父表记录就会撞外键约束。这是数据库设计有意为之的完整性保护,不是 bug。

解决:两种方向,要么业务上不允许删有历史记录的用户,改用 status 字段置 0 做逻辑禁用;要么删除时先删掉该用户的申请、还款计划等子表数据,再删 user 记录。课设阶段用逻辑禁用最省事——状态置 0 后登录接口直接拒绝,历史数据还能保留在库里做统计,演示报表时反而有内容可看。

5.5 登录后页面样式全丢:Filter 把静态资源也拦截了

现象:跳到学生首页后,页面是纯 HTML 文字,没有 CSS 样式和 JS 效果,控制台一堆 404。

原因:AuthFilter 判断未登录直接重定向,或者已登录用户访问的路径被误判成未授权,导致 /static 下的 css、js 请求全部被踢回登录页。浏览器里按 F12 看 Network 面板,静态资源请求的响应应该是 302 跳转。

解决:在 Filter 的放行名单里加静态目录前缀判断,代码写法参考 3.1 节中的isStatic判断。注意/static/要带着前缀判断而不是只判断后缀.css,因为你还要给 js、图片一起放行,统一目录前缀最省心。

5.6 Tomcat 控制台报 Address already in use: 8080

现象:启动 Tomcat 时控制台提示端口已占用,几秒后自动退出。

原因:上一次启动的 Tomcat 实例没有完全关闭,或者机器上其他进程占了 8080。IDEA 里表现为 Tomcat 面板的 Stop 按钮是灰的,但端口还活着。

解决:优先找到未关闭的 java 进程结束掉;临时快速解法是改 Tomcat 端口——IDEA 的 Tomcat 配置里 HTTP port 改成 8081,同时把部署后的 Application context 路径和浏览器访问地址同步改掉。治本办法是杀掉残留进程,因为如果同时开着多个 Tomcat 实例互相干扰,后续 session 和日志排查会更乱。

6. 二次开发方向:先把金额改成 BigDecimal,再谈报表

如果你准备拿这个项目做改进版答辩,我建议改造顺序是「先动钱,再动数据,最后动展示」。动钱指的是把可能还残留在代码里的 double 计算全部替换成 BigDecimal,比如有些旧版本源码在计算利息时用的是double变量,直接改成:

BigDecimal interest = principal.multiply(monthlyRate) .setScale(2, RoundingMode.HALF_UP);

这个改造看起来小,但直接决定了系统在金额运算上是否可靠。改完之后跑一遍第 4.4 节的完整演示,核对还款计划表里的总额是否等于本金加利息总和,这就是最直接的验证方法。接着可以加一个「统计报表」功能:用一条 SQL 按状态分组统计申请数量和金额总量,前端用 ECharts 画饼图,管理员一看就知道当前放款结构。SQL 就写在这:

SELECT status, COUNT(*) AS cnt, SUM(amount) AS total FROM loan_application GROUP BY status;

最后加一个真正有用的后端防护:把审核里的乐观锁从「先查询再更新」改成「条件更新」——一条 UPDATE 语句带上 status 条件,影响行数是 0 就说明状态已被别人改过。比如UPDATE loan_application SET status='APPROVED', audit_time=NOW() WHERE id=? AND status='PENDING',这比先查后改更安全也更高效,答辩时可以把这条 SQL 的 WHERE 条件讲给老师听,说明你理解了并发环境下状态一致性的价值。

我从一个课设项目里学到的教训是:拿到别人的源码,第一反应永远是先看 SQL 脚本和数据模型,因为表结构反映了设计者对整个业务的理解;如果表和表之间的关联、状态流转说不通,那代码写再花哨也没意义。从那以后我每次复现项目都强制先把建库脚本和实体类字段逐一核对,再启动编译器。希望帮到你。

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

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

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

立即咨询