ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSP小区水电费管理系统毕业设计:从源码部署到答辩实战

JSP小区水电费管理系统毕业设计:从源码部署到答辩实战 简介面向毕业设计或Java Web初学者的完整项目资料是一套基于JSPMySQLB/S架构的小区水电费管理系统。系统分为前台展示与后台管理两大模块游客可浏览站内新闻、小区风景、在线留言并注册管理员能维护新闻、审核用户、分类管理水电费账单注册用户可修改个人资料、查询并缴纳水电费用覆盖了典型管理系统的核心业务闭环。压缩包共713个文件以JSP页面、Java类、SQL脚本为主同时包含463个GIF演示截图、CSS/JS前端样式、JAR依赖库及说明文档整体仅10.12MB结构清晰便于直接导入运行与二次开发。除源码外还附带了毕业设计说明文档和演示视频可辅助理解实现思路、数据库表关系及操作流程。该资源已有134人学习查看适合需要快速完成课程设计、毕业设计或入门JSP开发的学习者参考。1. 这一个 zip装着毕业设计最容易被答辩老师追问的三样东西Jsp小区水电费管理系统这类毕业设计源码包几乎是 Java Web 方向每年毕业季的“硬通货”。打开 zip 之后你通常能看到三样东西前端用 JSP 写的页面后端用 Servlet 处理请求再加一套 MySQL 的建表脚本。说白了它就是让你把“水电费抄表—计费—缴费—统计”这条业务链在网页上完整跑起来顺便把 JSP、Servlet、JDBC、数据库设计这些课程考点全部串进去做成一个能演示、能截图、能写进论文的功能闭环。这套项目的价值不在于页面多好看而在于它非常典型用户的增删改查、按抄表数自动算费、缴费状态管理、月度汇总覆盖了大多数毕业设计需要演示的“业务完整性”。本文直接按“拆包 → 环境匹配 → 建库建表 → 核心链路改造 → 避坑 → 答辩升级”这条线往下走每一步都给能直接复用的命令和代码目标是让你在一周内从 zip 变成一个能上台演示、能回答老师追问的系统。2. 先拆包再动手源码目录结构和运行逻辑避免第 3 天才发现货不对版拿到 zip 的第一反应别是双击解压然后往 Tomcat 里扔得先看清楚里面的目录结构。毕业设计项目的源码目录和互联网公司的工程不太一样它通常不是一个完整的 Maven 工程而是一个 Eclipse / MyEclipse 风格的 Web 项目。认清这个结构后面所有配置才有依据。2.1 标准目录长什么样WebRoot、src、数据库脚本各司其职常见的毕业设计 JSP 项目分两类。老式的是 WebRoot 根目录型新一点的会带 .mymetadata 或 .project 文件标识出这是 MyEclipse 工程。不管哪种核心组件是一致的src 下面放 Java 代码包括 Servlet 类、工具类 DBHelper、实体类User、Bill、Meter 等WebRoot 下放 JSP 页面和 WEB-INF 的 web.xml 配置文件。JspShuiDianFei/ ├── src/ │ ├── com/dorm/model/ // 实体类 │ ├── com/dorm/servlet/ // 控制器层 Servlet │ ├── com/dorm/util/ // DBHelper 数据库连接工具 │ └── com/dorm/dao/ // 数据访问层 ├── WebRoot/ │ ├── login.jsp │ ├── index.jsp │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ // JDBC 驱动 jar 通常扔这里 │ ├── css/ │ ├── img/ │ └── js/ ├── 数据库脚本/ │ ├── shuidian.sql │ └── 说明.txt ├── 演示视频/ │ └── demo.mp4 └── 说明文档/ ├── 开题报告.doc └── 答辩PPT.ppt拿到这种结构后我一般先做三件事看 web.xml 里配置了哪些 Servlet 和欢迎页看 DBHelper 里连的是哪个库、账号密码是多少看 sql 脚本里建了哪几张表。这三件事做完项目能不能跑心里基本就有数了。如果打开发现是 Maven 工程的 pom.xml 结构那说明这个交付物可能被二次打包过反而要留意是不是和演示视频里的项目对得上。2.2 先看 web.xml再看 DBHelper3 分钟定位这个项目的配置命脉毕业设计 JSP 项目的配置命脉就一个文件web.xml。Servlet 的映射、欢迎页面、监听器全在这里面定义。很多同学拿到项目上来就找 login.jsp结果直接访问 404就是因为没有走 /login 这个 Servlet 映射而是直接访问 JSP 文件Session 里的验证信息根本没初始化。web-app xmlnshttp://java.sun.com/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd version3.0 display-name小区水电费管理系统/display-name welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list servlet servlet-nameLoginServlet/servlet-name servlet-classcom.dorm.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping /web-app这个配置说明项目的入口是 login.jsp表单提交后跳转到 /login 这个 Servlet 做验证。注意看这里 url-pattern如果源码里写的是 /*那么所有请求都会被这个 Servlet 截走静态资源如 CSS、JS 也会被拦截这属于交付源码里的常见毛病后面避坑章会细讲。接着打开 DBHelper.java 看连接串。package com.dorm.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBHelper { private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/shuidian?useUnicodetruecharacterEncodingutf-8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这段代码里有几个典型问题值得注意。第一URL 里没有 serverTimezone 参数如果是 MySQL 8.x连接会直接报时区错误必须改成jdbc:mysql://localhost:3306/shuidian?serverTimezoneAsia/ShanghaicharacterEncodingutf-8。第二驱动类是 com.mysql.jdbc.Driver这个只在 MySQL 5.x 驱动里存在换了 MySQL 8 就要改成 com.mysql.cj.jdbc.Driver。这两处不改项目跑不起来而且报错信息对新手极不友好后面避坑章会展开。2.3 导出源码和演示视频的对应关系先看演示视频里的地址栏拿到 zip 里的演示视频不要只看界面效果要盯着浏览器的地址栏看。视频里如果访问的是 localhost:8080/项目名/login那说明部署路径是项目名如果访问的是 localhost:8080/login那说明配置了 ROOT 路径这两种方式对应不同部署方法。看视频还有一个重要作用是判断这个项目的正常操作流是什么是先登录管理员再进去录入抄表数还是先注册用户再缴费。操作流决定你验证系统时的测试路径。看完地址栏还不够要把视频里输入的数据记下来。视频里如果录入了某个用户本月表数 1234、上月表数 1100算出电费 67.2 元那你就拿同样的数据在本地跑一遍看输出的结果是否一致。这个验证步骤很多人忽略但对毕业设计来说非常关键——答辩老师如果拿视频里的截图问你“这个数据怎么来的”你能现场复现比任何口头解释都有说服力。到这一步为止项目算是摸清了可以开始搭环境。3. 环境匹配是第一道门槛JDK、Tomcat、MySQL 版本怎么配才能一次跑通JSP 项目对版本极其敏感这和前后端分离的工程完全不同。新版 JDK 不兼容老 TomcatMySQL 8 连不上老驱动JSP 编译报错又不像 IDE 里那样给明确提示——这些坑我见得太多了。整一章专门讲环境匹配是因为它能过滤掉一半下载了这个 zip 却跑不起来的人而他们的问题不在代码在版本错配。3.1 版本选型JDK 8 配 Tomcat 8.5 配 MySQL 5.7 是最稳的组合JDK 17 别碰这类老工程做毕业设计的 JSP 项目最稳的组合是 JDK 1.8 Tomcat 8.5 MySQL 5.7。这个组合和绝大多数老源码的编译级别、驱动方式、JSTL 标签库版本都能对齐。如果你电脑上只装了 JDK 17项目引用的是 javax.servlet 包Tomcat 10 把 javax 改成 jakarta 了直接编译失败强行用 Tomcat 9 跑 JDK 17JSP 编译时又可能因为反射访问限制报 IllegalAccessError。所以结论很简单这类源码别追新越新越容易翻车。MySQL 版本则要注意 5.7 和 8.0 的差异。源码里如果用的驱动是 com.mysql.jdbc.Driver那这是 5.x 时代的驱动MySQL 8.0 也能勉强带动但必须连一个 MySQL 8 的实例时驱动包最好替换为 mysql-connector-java-8.0.33.jar并把连接串改成下面对比表里的写法。如果你完全按照下面这张表配置基本能避开 80% 的环境启动问题。组件推荐版本必改配置常见翻车点JDK1.8jdk-8u202JAVA_HOME 指向 JDK 而不是 JRE高版本 JDK 不兼容 Tomcat 8 的反射Tomcat8.5.87server.xml 端口不冲突Tomcat 10 用 jakarta.servlet老代码编不过MySQL5.7 或 8.0驱动类与 URL 参数对应8.0 报 jdbc 驱动时区错误连接驱动5.1.49 / 8.0.33版本要和 MySQL 大版本一致驱动 jar 扔 WEB-INF/lib 后要重启你可能会问项目说明文档里写的一定是某个版本那怎么办我一般的原则是说明文档是作者在自己机器上跑的记录你只需要保证三个兼容维度——JDK 能编译 Java 代码、Tomcat 能编译 JSP、JDBC 驱动能连上数据库。至于作者用的 Eclipse 版本、MyEclipse 版本和最终运行没有直接关系。3.2 部署两种姿势Eclipse/IDEA 里跑和直接扔 Tomcat webapps 的区别部署方式看起来是个操作问题实际上决定你会不会遇到“代码改了但页面还是旧的”这种玄学问题。先用最简单的方式在 Tomcat 里跑通再去折腾 IDE 热部署是我反复验证过的建议顺序。下载 Tomcat 8.5 解压后改两个配置。第一个是 conf/server.xml 里的端口Tomcat 默认 8080如果被占用就改成 8088第二个是修改 conf/ 下的 context.xml加上一行数据库连接池的参数但大多数毕业设计源码没有配连接池这步跳过。然后把整个项目文件夹复制到 Tomcat 的 webapps 目录下双击 bin/startup.bat 启动浏览器访问http://localhost:8080/项目文件夹名/login这种方式的逻辑很简单Tomcat 启动时会自动解析 webapps 下的每个文件夹把带有 WEB-INF 和 web.xml 的目录当做一个 Web 应用发布。改动 JSP 页面后不需要重启 Tomcat因为 JSP 每次请求时编译一次但改动 Java 类后必须重启因为类已经加载进 JVM 内存了。这块很多人搞混——改 Servlet 不重启结果页面报错或行为不变就把锅甩给源码。如果要在 IDEA 里跑注意一个细节Artifacts 的 Output Layout 要保证 WEB-INF/lib 下的驱动 jar 被打进包。IDEA 默认不会自动带上外部引入的 jar很多新手在 IDEA 里部署后报 ClassNotFoundException: com.mysql.jdbc.Driver就是这个原因。而直接扔 Tomcat webapps 的方式天然没有这个问题因为 WebRoot/WEB-INF/lib 里的 jar 就是最终要用的。顺序应该是先在 webapps 下跑通再进 IDEA 做二次开发。3.3 首次启动日志怎么看异常堆栈的 5 个高频关键词启动 Tomcat 后控制台输出一堆日志。新手看到 ERROR 就慌实际上 Tomcat 启动日志里有几条固定信息是正常现象比如 org.apache.catalina.startup. Catalina.start 和 Server startup in [xxx] milliseconds。真正要看的关键词只有几个这里列一个速查表后面避坑章也会配合展开。日志关键词含义处理方向ClassNotFoundException找不到某个类通常是驱动 jar 没进 lib检查 WEB-INF/libNoClassDefFoundError类在编译期存在运行时加载失败依赖顺序问题重启 Tomcat 清理缓存Port 8080 already in use端口被占用改端口或杀掉占用进程IllegalArgumentException: Invalid character访问路径里有中文或空格项目文件夹名用英文Unsupported major.minor version 52.0class 文件编译版本高于 JVMJDK 版本不一致class 是高版本 JDK 编译的看日志有个技巧不要从上往下逐行读要从第一行 Exception 开始往下找 Caused by。Java 的异常链里 Caused by 才是根本原因。比如前面一大段部署错误最后 Caused by 写着 cannot be cast to javax.servlet.Servlet那就说明 Servlet 类没有正确继承 HttpServlet 类或 web.xml 里 class 路径写错。直接处理 Caused by 后面的内容比盯着整屏日志更有效。一旦 Tomcat 能启动、首页能打开、数据库脚本导入成功这个项目就进入了“能跑”状态。但“能跑”不等于“能用”真正的核心是业务链路是不是完整的下一章直接拆解最核心的水电费计算与缴费闭环。4. 数据库设计与核心业务链路从抄表到出账单一个 Servlet 事务解决脏数据水电费管理系统的核心不只是 CRUD。答辩老师最爱问两个问题一是“电费阶梯价怎么算的”二是“用户缴了费之后状态怎么变更的”。这两个问题如果答不上来项目演示再流畅也会被问住。本章把数据库表结构和核心计算链路的写法展开照着这个思路去改源码你的项目就比原始 zip 高一个档次。4.1 核心表和字段设计费率表、抄表记录表、缴费记录表如何关联绝大多数毕业设计的水电费系统里有这四张表用户表、费率表、抄表记录表、缴费记录表。用户表和费率表是基础数据抄表和缴费是业务流水。关键点在于电费不是直接录入金额而是录入本月的表底数用本月表底数减去上月表底数得到用量再乘以费率。这个“算”的过程是你需要在数据库里搞清楚的业务本质。-- 用户表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL, room_no VARCHAR(20) COMMENT 房间号, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 费率表 CREATE TABLE t_rate ( id INT PRIMARY KEY AUTO_INCREMENT, rate_type VARCHAR(20) COMMENT water/electricity, unit_price DECIMAL(10,2) COMMENT 单价元/度或元/吨, ladder_one DECIMAL(10,2) COMMENT 阶梯一上限, ladder_price_one DECIMAL(10,2), ladder_two DECIMAL(10,2), ladder_price_two DECIMAL(10,2), update_time DATETIME ); -- 抄表记录表 CREATE TABLE t_meter ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, meter_type VARCHAR(20) COMMENT water/electricity, current_num DECIMAL(10,2) COMMENT 本次表底, last_num DECIMAL(10,2) COMMENT 上次表底, usage_num DECIMAL(10,2) COMMENT 本次用量, meter_date DATE, operator VARCHAR(20), FOREIGN KEY (user_id) REFERENCES t_user(id) ); -- 缴费记录表 CREATE TABLE t_payment ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, meter_id INT NOT NULL, amount DECIMAL(10,2), status TINYINT COMMENT 0-未缴 1-已缴, pay_time DATETIME, FOREIGN KEY (user_id) REFERENCES t_user(id) ); -- 初始化费率第一阶梯 0.55 元/度超过 200 度为 0.85 元/度 INSERT INTO t_rate(rate_type, unit_price, ladder_one, ladder_price_one, ladder_two, ladder_price_two) VALUES(electricity, 0.55, 200, 0.55, 1000, 0.85);这套表结构和原 zip 里给的脚本可能有差异比如表名前缀、字段命名风格但业务关系是一样的。注意 DECIMAL 类型而不是 FLOAT是专门为了金额精度——用 FLOAT 算水电费会在小数点后出现 0.30000000000000004 这种丑数答辩时演示出来很尴尬。DECIMAL(10,2) 能保证金额精确到分。4.2 抄表 → 计费 → 缴费的执行逻辑Service 层做事务Servlet 只做转发JSP 项目最容易犯的结构性错误是“所有业务逻辑都写在 Servlet 里”一个类几百行。这里给出的方案是把计算逻辑放到独立的 Service 类里Servlet 只处理请求参数和页面跳转。这样虽然看起来多写了一层但答辩时能讲清楚“分层设计”是一个很加分的点。下面代码是抄表计费的核心逻辑我把它叫做“抄表事务三连”插入抄表记录 → 计算金额 → 生成缴费单三层操作包在同一个 Connection 事务里任何一步失败都整体回滚保证不会出现“表抄了但缴费单没了”或反过来的情况。package com.dorm.service; import com.dorm.util.DBHelper; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.math.BigDecimal; public class MeterService { /** * 抄表并生成缴费单全程一个事务 * param userId 用户ID * param meterType 类型 water/electricity * param currentNum 本次表底数 */ public boolean meterAndBill(int userId, String meterType, double currentNum) { Connection conn null; try { conn DBHelper.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查询该用户上次表底数 double lastNum 0; String sqlLast SELECT current_num FROM t_meter WHERE user_id? AND meter_type? ORDER BY meter_date DESC LIMIT 1; PreparedStatement psLast conn.prepareStatement(sqlLast); psLast.setInt(1, userId); psLast.setString(2, meterType); ResultSet rs psLast.executeQuery(); if (rs.next()) { lastNum rs.getDouble(current_num); } double usageNum currentNum - lastNum; if (usageNum 0) { throw new RuntimeException(本次抄表数小于上次表底请核对); } // 2. 插入抄表记录 String sqlInsert INSERT INTO t_meter(user_id, meter_type, current_num, last_num, usage_num, meter_date, operator) VALUES(?,?,?,?,?,NOW(),admin); PreparedStatement psInsert conn.prepareStatement(sqlInsert); psInsert.setInt(1, userId); psInsert.setString(2, meterType); psInsert.setDouble(3, currentNum); psInsert.setDouble(4, lastNum); psInsert.setDouble(5, usageNum); psInsert.executeUpdate(); // 3. 按费率计算金额支持阶梯价 BigDecimal money calc(meterType, usageNum, conn); // 4. 生成缴费单 String sqlPay INSERT INTO t_payment(user_id, meter_id, amount, status) VALUES(?,?,?,0); int meterId getLastId(conn); // 也可以使用 SELECT LAST_INSERT_ID() PreparedStatement psPay conn.prepareStatement(sqlPay); psPay.setInt(1, userId); psPay.setInt(2, meterId); psPay.setBigDecimal(3, money); psPay.executeUpdate(); conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } /** 阶梯计费以电费为例超过一阶梯自动切换到二阶梯价 */ private BigDecimal calc(String meterType, double usageNum, Connection conn) throws SQLException { BigDecimal usage BigDecimal.valueOf(usageNum); String sql SELECT * FROM t_rate WHERE rate_type? LIMIT 1; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, meterType); ResultSet rs ps.executeQuery(); BigDecimal fee BigDecimal.ZERO; if (rs.next()) { BigDecimal ladderOne rs.getBigDecimal(ladder_one); BigDecimal priceOne rs.getBigDecimal(ladder_price_one); BigDecimal priceTwo rs.getBigDecimal(ladder_price_two); if (usage.compareTo(ladderOne) 0) { fee usage.multiply(priceOne); } else { BigDecimal part1 ladderOne.multiply(priceOne); BigDecimal part2 usage.subtract(ladderOne).multiply(priceTwo); fee part1.add(part2); } } return fee.setScale(2, BigDecimal.ROUND_HALF_UP); } private int getLastId(Connection conn) throws SQLException { PreparedStatement ps conn.prepareStatement(SELECT LAST_INSERT_ID() AS id); ResultSet rs ps.executeQuery(); if (rs.next()) return rs.getInt(id); return 0; } }这段代码里最值得讲的是 BigDecimal 的引入。用 double 计算 0.1 * 3 在 Java 里会得到 0.30000000000000004这在缴费金额上完全不可接受。BigDecimal 配合 setScale(2, BigDecimal.ROUND_HALF_UP) 能精确到分。另外事务的写法也值得注意getConnection() 拿到连接后先 setAutoCommit(false)然后所有操作都走这个连接最后 commitcatch 里 rollback这比每个 DAO 方法各开一个连接安全得多。原 zip 里如果每个表各开连接改成这个写法需要小重构但对答辩帮助很大。4.3 JSP 页面如何与 Servlet 协作EL 表达式和 JSTL 渲染缴费列表后端逻辑写完了前端页面不能只是纯 Java 脚本片段% %那是老掉牙的写法。这里用 EL 表达式加 JSTL 标签来渲染缴费列表这也是答辩时能明确说出的技术点。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html headtitle缴费管理/title/head body table border1 tr th房间号/thth类型/thth用量/thth金额/thth状态/thth操作/th /tr c:forEach items${paymentList} varp tr td${p.roomNo}/td td${p.meterType}/td td${p.usageNum}/td tdfmt:formatNumber value${p.amount} pattern0.00/ 元/td td c:choose c:when test${p.status 1}font colorgreen已缴/font/c:when c:otherwisefont colorred未缴/font/c:otherwise /c:choose /td td c:if test${p.status 0} a hrefpay?id${p.id} onclickreturn confirm(确认收到该笔费用?)确认缴费/a /c:if /td /tr /c:forEach /table /body /html这段 JSP 展示了几个关键点。JSTL 的 c:forEach 遍历 Servlet 设置到 request 里的 paymentList 集合c:choose 做状态判断fmt:formatNumber 控制金额显示格式。注意 JSP 顶部必须声明 JSTL 标签库的 uri如果 Tomcat 的 lib 里没有 jstl.jar 和 standard.jar这个页面会直接报 500 错误——这个问题在避坑章节会再次提到因为它是 JSP 项目最常见故障之一所在位置是 WEB-INF/lib。使用 EL 表达式 ${} 而不是 % % 的好处是页面代码更干净也更容易维护老师看了会认为你有工程化意识。页面渲染这条路走通之后系统的核心闭环就打通了录入表底数时自动算费生成缴费单缴费操作更新状态列表页通过状态字段控制可操作按钮。这样一个完整的“抄表 → 计费 → 缴费”演示流程就足以撑起整个毕业答辩的主线。5. 避坑手册JSP 项目从解压到答辩的 8 个高频翻车点这部分内容全部来自实际做这类项目时踩过的坑按“现象 → 原因 → 解决”的格式整理。每一个都对应一个来自真实调试的故障你提前看一眼胜过后天晚上熬夜排查。5.1 中文乱码所有的字都变成问号最典型的现象是JSP 页面上的中文能正常显示但从数据库查出来的中文是问号或者提交表单后存进数据库的是问号。原因是三层编码没有对齐JSP 文件本身的编码、Tomcat 接收请求的编码、数据库连接的编码。解决方法是把三层全部固定为 UTF-8。// 方案一写一个编码过滤器拦截所有请求设置请求与响应编码 public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }在 web.xml 注册这个过滤器并放在所有 Servlet 映射之前filter filter-nameencoding/filter-name filter-classcom.dorm.util.EncodingFilter/filter-class /filter filter-mapping filter-nameencoding/filter-name url-pattern/*/url-pattern /filter-mapping然后检查 JSP 页面第一行 pageEncoding 是否等于 UTF-8数据库连接串里是否带了 characterEncodingutf-8以及 MySQL 建库时是否指定 utf8mb4 字符集。三层对齐之后乱码可以完全避免。这条是先把脏数据挡在入口的设计也是以后工作中统一编码规范的基本功。5.2 Tomcat 能启动但页面 404地址栏没输入对所访问的路径和应用部署路径不一致是最常见的 404 原因不是源码坏了。部署到 webapps 下的文件夹名是 JspShuiDianFei那么访问路径必须是 http://localhost:8080/JspShuiDianFei/少了斜杠或者拼错大小写都会 404。Tomcat 的路径匹配对大小写并不敏感但文件夹里的 JSP 文件名是敏感的login.jsp 和 Login.jsp 是两个文件。另外检查 web.xml 里的欢迎页如果 login.jsp 写的是 login.htm而项目里只有 login.jsp访问根路径时也会 404。解决方法是先在 webapps 目录下确认文件名和 web.xml 完全一致。5.3 mysql 驱动 ClassNotFoundException但明明把 jar 放进了 lib这是一道经典的 IDE 部署问题。直接把 jar 扔在 WEB-INF/lib 目录下Tomcat 启动时应该能扫描到但如果是在 IDEA/Eclipse 里跑构建路径Build Path没有加载这个 jar运行时根本找不到类。此时需要右键项目属性 → Java Build Path → Libraries → Add JARs 手动引入。另外还要确认 MySQL 驱动的版本和 MySQL 服务版本兼容MySQL 8 请用 mysql-connector-java-8.0.33.jarMySQL 5.7 用 5.1.49 即可。驱动 jar 放好之后必须重启 Tomcat不是重新编译就完事。5.4 SQL 语法错误字段名带上了莫名其妙的引号MySQL 报 syntax error 时认真看它标注的位置一般是字段名或表名拼错。但 JSP 项目里还常有另一种情况写 SQL 时表名用的是 mysql 保留字比如 order、desc、group。如果项目里的表恰好叫 t_orderSQL 应该写成SELECT * FROM t_order这里的关键不是让你把所有表名都加反引号而是遇到无法解释的语法错误时先怀疑字段名是不是保留了字。把 MySQL 保留字列表打开对照表名查一遍比盯着 SQL 看半小时有效。这个坑在生成报表的时候容易出现比如按月份统计时 SQL 里用了 month 做列名。5.5 JSP 页面报错The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved这个报错说明 Tomcat 没找到 JSTL 标签库的 jar。JSP 标准里 JSTL 不是内置的必须把 jstl.jar 和 standard.jar 放到 WEB-INF/lib 下。Tomcat 10 用户会遇到的额外问题是 JSTL 得用 jakarta.servlet.jsp.jstl 版本因为是 javax 改为 jakarta 命名空间。但如果是按照推荐版本 Tomcat 8.5 运行只需要确认 WEB-INF/lib 里有这两个 jar 且版本匹配JSTL 1.2 可以直接用不用装 standard.jar。5.6 控制台报 Address already in use: JVM_Bind端口被占用。先用 netstat 查一下谁占用了 8080再决定是杀掉进程还是改端口。# 查看 PID 占用 netstat -ano | findstr :8080 # 杀掉指定进程Windows taskkill /PID 你的PID /F # 或者改 Tomcat/conf/server.xml 里的 Connector port改端口的方式适合你机器上已经跑着别的 Web 服务的情况把 8080 改成 8081同时记住访问路径也要跟着改不然地址栏还是老端口就又成 404 了。5.7 页面样式全丢了CSS 和 JS 加载不到JSP 页面往往通过相对路径引用 css比如link hrefcss/style.css。当你在 /login 页面时浏览器解析这个相对路径是相对当前路径的可能会解析成 /css/style.css如果当前 URL 是 /admin/login.do则解析成 /admin/css/style.css自然 404。解决方案是使用 JSP 内置对象获取当前项目根路径% String basePath request.getContextPath(); % link href%basePath%/css/style.css relstylesheet这行代码以后会用得非常多。讲到更规范的做法可以直接在 JSP 顶部计算并引入保证无论在哪个 Servlet 下转发css js 的路径都不会错。5.8 增加了数据却删不掉提示外键约束失败水电费系统两张最重要的业务表 t_meter 和 t_payment 都带了 FOREIGN KEY 关联到 t_user这保证数据完整性。但如果要删除一个用户而抄表记录或缴费记录里还有这个用户的 idMySQL 默认 RESTRICT 限制下会拒绝删除。这也是一个可以主动在答辩时讲出来的细节或者改进方案。-- 方案一先删子表再删父表 DELETE FROM t_payment WHERE user_id1; DELETE FROM t_meter WHERE user_id1; DELETE FROM t_user WHERE id1; -- 方案二删除外键约束后重建为级联删除 ALTER TABLE t_payment DROP FOREIGN KEY fk_payment_user; ALTER TABLE t_payment ADD CONSTRAINT fk_payment_user FOREIGN KEY (user_id) REFERENCES t_user(id) ON DELETE CASCADE;实际开发经验里尽量别无脑用 ON DELETE CASCADE因为误删用户会把缴费历史全删了缴费记录是审计数据应该保留。所以更合理的处理方式是用户表加一个 status 字段做逻辑删除而不是物理删除。这种方式放在答辩里也是职业素养的证明。6. 给答辩加分的四个小改造月度汇总、导出 Excel、SQL 注入防护和演示开关这一章做的是锦上添花的事核心功能已经跑通接下来用几个小改造让项目在答辩时更耐看。改造的原则是“少动核心结构、多增加可演示场景”每一处都能在一页 PPT 上讲清楚绝不引入复杂框架避免把毕业设计做成一个没法收尾的大工程。6.1 月度账单汇总一条 SQL 实现按房间号的当月用量汇总答辩时老师很喜欢问“能不能统计一下这个月每个房间用了多少水电”。原项目如果没做这个功能你可以加一个简单的汇总报表页面SQL 也容易理解SELECT u.room_no, SUM(CASE WHEN m.meter_typewater THEN m.usage_num ELSE 0 END) AS total_water, SUM(CASE WHEN m.meter_typeelectricity THEN m.usage_num ELSE 0 END) AS total_electricity FROM t_user u LEFT JOIN t_meter m ON u.id m.user_id AND DATE_FORMAT(m.meter_date, %Y-%m) DATE_FORMAT(NOW(), %Y-%m) GROUP BY u.id, u.room_no;这条 SQL 利用 CASE WHEN 做行转列把水表和电表的两条记录转成一行展示。实际改动时写一个 QueryServlet 执行这条 SQL把结果放到 request 里转发到一个新 JSP 页面即可。这页放答辩 ppt 里标题就写“月度消耗汇总报表”老师立刻知道你有数据库聚合查询的能力。6.2 换一个思路做导出JSP 页面直接输出 Excel不引入第三方库很多同学想给系统加“导出 Excel”功能一上来就找 POI 依赖但毕业设计时间里引入不熟悉的库反而是负担。其实有一个超轻量的方案HTML 表格加一个 header浏览器就能把它另存为 Excel 格式。这和真正使用 Apache POI 导出的效果差异在于文件格式但用作答辩功能演示完全够用。% page contentTypeapplication/vnd.ms-excel;charsetUTF-8 % % response.setHeader(Content-Disposition, attachment; filenamebill.xls); % html headmeta http-equivContent-Type contenttext/html; charsetUTF-8/head body table border1 trth房间号/thth类型/thth金额/thth状态/th/tr !-- 这里循环输出已有数据结构与上一章列表相同 -- /table /body /html关键点只有两行contentType 设置成 application/vnd.ms-excel以及 Content-Disposition 里的 attachment 触发下载。这个方法生成的 .xls 文件用 WPS 或 Excel 打开都没有问题。如果要真正意义上的 .xlsx那还是要走 POI但那个复杂度就不适合塞进这个环节了。这个技巧是典型的“一个 Response 头解决一种业务需求”的实战思路。6.3 顺手补上 SQL 注入防护PreparedStatement 不只是“建议”原 zip 里的登录验证如果用的是字符串拼接 SQL那是典型的注入漏洞。老师只要在用户名框里输入一个 or 11就能直接登进去不用密码演示出来就是灾难。好在 Java 的 PreparedStatement 能根本性解决这个问题。把拼接形式改写成参数化形式// 错误写法拼接 SQL String sql SELECT * FROM t_user WHERE username name AND password pwd ; // 正确写法参数化查询 String sql SELECT * FROM t_user WHERE username? AND password?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, name); ps.setString(2, pwd); ResultSet rs ps.executeQuery();这也是把一个常见的毕业设计源码和“有安全意识”区分开的最关键一步。这个修改只有几行代码但它可以在答辩时专门做成一个小环节说说为什么用 PreparedStatement而不是 Statement本身就是一个很有说服力的加分点。6.4 演示模式开关一键填充示例数据避免现场冷场这是我最想分享的小技巧。答辩最怕发生的情况是现场数据库里没有数据或者录入的数据太乱页面看起来非常空旷。写一个 demo.jsp点击之后执行下面逻辑插入 5 个测试用户给每个用户插入最近三个月的抄表记录和对应的缴费单其中部分设为已缴。当你把项目部署到答辩机器上时先登录进去点一下“生成演示数据”系统就变成一个漂亮丰满的状态。这个功能实际是服务端一次性执行多条 SQL。在代码里用 JDBC 的 Statement 执行一批 insert或者直接调用几个已有的 Service 方法都可以。我自己的经验是每次答辩前必做两件事一是准备一个数据重置脚本来清库并重新生成演示数据二是提前在演示环境跑一遍全流程保证抄表录入的数据和缴费状态都是连贯的。毕业设计翻车最惨的一次就是现场数据是个不完整的半成品缴费状态乱七八糟最后老师问“为什么这个人缴了费还是未缴”场面一度很尴尬。后来我每次都把这几件事提前做完重置数据、生成演示流水、把缴费状态调整到“大部分已缴 个别待缴”这样答辩时可以自然地说“我们看一个还没有缴费的用户走一遍流程”而不是手忙脚乱地找数据。对于一个 Jsp 小区水电费管理系统来说能走到这一步说明你已经拿到了一个源码包并且真的把它变成了自己能讲解、能修改、能演示的东西。答辩能不能过看的不是项目多宏大而是你对项目里代码的掌控程度——希望你比我当年顺利每次演示都能像彩排一样稳希望帮到你。本文还有配套的精品资源点击获取
返回列表