
简介这是一套面向高校计算机相关专业毕业设计的JSP小区水电费管理系统完整项目包采用JSPMySQLB/S架构适合正在准备毕设或需要Java Web实战练手的同学参考。系统分为前台与后台两大模块前台提供站内新闻浏览、在线留言与回复查看、用户注册及小区风景展示后台管理员可管理新闻、审核注册用户、维护用户资料与水电费类别注册用户则能修改个人资料并查询缴费记录功能链路覆盖了典型的信息管理与费用缴纳场景。压缩包共713个文件约10.12MB以gif、jpg等图片素材和jsp页面为主辅以css样式、js脚本、htm静态页及db数据库文件另含jar依赖、sql建库脚本与项目配置文件结构完整可直接导入运行。资源同时附带说明文档与演示视频已有134人学习下载便于快速理解模块划分、数据库设计与页面跳转逻辑是完成同类毕设或课程设计的实用参考。1. 小区水电费管理系统一个 JSP 毕设项目从跑通到讲清楚的完整路径每年毕业季计算机专业的选题里总有一类题目反复出现——基于 JSP 的信息管理系统。小区水电费管理系统就是其中典型的一个它要管住户信息、抄表记录、费用计算、缴费状态还要有管理员和普通用户两种角色。很多同学拿到这个题目时第一反应是去网上找一份源码改改界面就交差。但真正答辩时被问到「你的费用计算逻辑怎么处理的」「JSP 和 Servlet 之间怎么传数据」就答不上来了。这篇内容面向的是正在做 JSP 毕设、或者需要快速理解一个传统 JavaWeb 项目结构的同学。我会从环境搭建讲到核心业务实现再到部署打包和答辩时容易被追问的技术点。不堆概念每一步都落到能跑起来的代码和配置上。如果你手里已经有一份类似的源码也可以对照着看把「能跑」变成「能讲清楚」。2. 环境搭建与项目骨架从零把 JSP 项目跑起来2.1 JDK、Tomcat、MySQL 的版本选择与安装要点传统 JSP 项目对版本比较敏感不是越新越好。我一般推荐这套组合JDK 8或 JDK 11、Tomcat 8.5 或 9.0、MySQL 5.7 或 8.0。原因很直接——网上能找到的 JSP 毕设源码绝大多数是在 JDK 8 Tomcat 8 的环境下写的用 JDK 17 或 Tomcat 10 很容易遇到javax.servlet包找不到的问题因为 Tomcat 10 把包名改成了jakarta.servlet。安装 JDK 后配置JAVA_HOMETomcat 解压即可用MySQL 建议用安装版并记住 root 密码。验证环境是否就绪java -version # 输出应为 java version 1.8.0_xxx 或 11.0.x echo %JAVA_HOME% # Windows 下应输出 JDK 安装路径如 C:\Program Files\Java\jdk1.8.0_301Tomcat 启动验证进入bin目录执行startup.batWindows或startup.shLinux/Mac浏览器访问http://localhost:8080看到 Tomcat 默认页即成功。注意如果 8080 端口被占用修改conf/server.xml中的Connector port8080为其他端口比如 8081。2.2 用 IDEA 新建 JSP 项目的目录结构与关键配置在 IDEA 中新建项目时选 Java Enterprise勾选 Web Application。生成的项目结构里核心目录是web或webapp里面放 JSP 页面和WEB-INF。WEB-INF下的web.xml是部署描述符lib目录放第三方 jar 包。一个典型的小区水电费管理系统目录结构如下src/ com/community/dao/ # 数据库访问层 com/community/entity/ # 实体类 com/community/servlet/ # 控制层 Servlet com/community/util/ # 工具类数据库连接等 web/ css/ js/ images/ login.jsp # 登录页 admin/ # 管理员页面 user/ # 普通用户页面 WEB-INF/ web.xml lib/ mysql-connector-java-8.0.xx.jar关键配置在web.xml中注册 Servlet 和设置欢迎页web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version4.0 welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list servlet servlet-nameLoginServlet/servlet-name servlet-classcom.community.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping /web-app这里url-pattern决定了浏览器访问的路径。比如表单提交到/login就会触发LoginServlet的doPost方法。很多同学在页面里写actionLoginServlet也能跑但那是相对路径匹配项目一改上下文路径就容易 404建议统一用/开头的绝对路径。2.3 数据库连接工具类的封装与连接池配置JSP 项目里最常见的翻车点之一就是数据库连接。新手容易在每个 Servlet 里写一遍DriverManager.getConnection代码重复不说连接用完不关还会导致 MySQL 连接数爆满。正确做法是封装一个工具类有条件的上连接池。先建数据库和核心表CREATE DATABASE community_fee DEFAULT CHARACTER SET utf8mb4; USE community_fee; CREATE TABLE resident ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL COMMENT 房号, name VARCHAR(50) NOT NULL, phone VARCHAR(20), password VARCHAR(50) DEFAULT 123456 ); CREATE TABLE meter_record ( id INT PRIMARY KEY AUTO_INCREMENT, resident_id INT, record_month VARCHAR(7) COMMENT 格式 2024-01, water_usage DECIMAL(10,2) COMMENT 用水量(吨), electric_usage DECIMAL(10,2) COMMENT 用电量(度), water_price DECIMAL(6,2) DEFAULT 3.50, electric_price DECIMAL(6,2) DEFAULT 0.56, total_fee DECIMAL(10,2), pay_status TINYINT DEFAULT 0 COMMENT 0未缴 1已缴, FOREIGN KEY (resident_id) REFERENCES resident(id) );工具类封装package com.community.util; import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/community_fee?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 你的密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }serverTimezoneAsia/Shanghai这个参数在 MySQL 8.0 下必须加否则会报时区错误。characterEncodingutf8保证中文不乱码。如果你的 MySQL 是 5.7驱动类名可以写com.mysql.jdbc.Driver但com.mysql.cj.jdbc.Driver也兼容。提示连接池可以用 Druid 或 C3P0但对于毕设规模直接封装工具类足够。答辩时如果被问到「连接池」能说清楚「减少频繁创建连接的开销」即可。3. 核心业务实现住户管理、抄表录入与费用计算3.1 住户信息 CRUD 的 Servlet JSP 协作模式住户管理是整个系统的数据基础。典型操作包括管理员查看住户列表、新增住户、修改信息、删除住户。这里以「新增住户」为例走一遍完整的数据流。前端addResident.jsp表单form action${pageContext.request.contextPath}/resident?actionadd methodpost 房号input typetext nameroomNo requiredbr 姓名input typetext namename requiredbr 电话input typetext namephonebr input typesubmit value提交 /form${pageContext.request.contextPath}会动态获取项目上下文路径避免部署路径变化导致表单提交 404。这是 JSP 里非常实用的一个写法。后端 Servlet 用action参数区分操作类型package com.community.servlet; import com.community.dao.ResidentDao; import com.community.entity.Resident; import javax.servlet.*; import javax.servlet.http.*; import javax.servlet.annotation.WebServlet; import java.io.IOException; WebServlet(/resident) public class ResidentServlet extends HttpServlet { private ResidentDao residentDao new ResidentDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (list.equals(action)) { req.setAttribute(list, residentDao.findAll()); req.getRequestDispatcher(/admin/residentList.jsp).forward(req, resp); } else if (delete.equals(action)) { residentDao.delete(Integer.parseInt(req.getParameter(id))); resp.sendRedirect(req.getContextPath() /resident?actionlist); } } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String action req.getParameter(action); if (add.equals(action)) { Resident r new Resident(); r.setRoomNo(req.getParameter(roomNo)); r.setName(req.getParameter(name)); r.setPhone(req.getParameter(phone)); residentDao.insert(r); resp.sendRedirect(req.getContextPath() /resident?actionlist); } } }doPost里第一行req.setCharacterEncoding(UTF-8)必须放在读取任何参数之前否则中文姓名会变成乱码。这个坑几乎每个 JSP 新手都会踩一次。sendRedirect和forward的区别也要清楚前者是浏览器重新发起请求地址栏变化后者是服务器内部转发地址栏不变。增删改后用重定向避免刷新页面重复提交。3.2 抄表记录录入与水电费自动计算逻辑抄表录入是业务核心。管理员选择住户、月份输入本月水表和电表读数系统自动算出用量和费用。这里有两种设计一种是直接输入用量一种是输入本次读数并减去上次读数。后者更贴近真实场景但实现稍复杂。费用计算公式水费 用水量 × 水单价 电费 用电量 × 电单价 总费用 水费 电费在 Servlet 中处理protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); int residentId Integer.parseInt(req.getParameter(residentId)); String month req.getParameter(recordMonth); double waterUsage Double.parseDouble(req.getParameter(waterUsage)); double electricUsage Double.parseDouble(req.getParameter(electricUsage)); double waterPrice 3.50; double electricPrice 0.56; double totalFee waterUsage * waterPrice electricUsage * electricPrice; MeterRecord record new MeterRecord(); record.setResidentId(residentId); record.setRecordMonth(month); record.setWaterUsage(waterUsage); record.setElectricUsage(electricUsage); record.setWaterPrice(waterPrice); record.setElectricPrice(electricPrice); record.setTotalFee(totalFee); record.setPayStatus(0); meterDao.insert(record); resp.sendRedirect(req.getContextPath() /meter?actionlist); }单价建议存在数据库或配置文件里不要硬编码在 Java 代码中。答辩时老师如果问「水价调整了怎么办」你可以回答「修改配置表即可不需要重新编译」。这是一个加分项。Double.parseDouble如果传入空字符串会抛NumberFormatException前端加required属性只能防普通用户接口层面最好再做一次判空String waterStr req.getParameter(waterUsage); if (waterStr null || waterStr.trim().isEmpty()) { req.setAttribute(error, 用水量不能为空); req.getRequestDispatcher(/admin/meterAdd.jsp).forward(req, resp); return; }3.3 缴费状态管理与住户端查询页面缴费状态是一个TINYINT字段0 表示未缴1 表示已缴。管理员在列表页点击「标记已缴」触发更新住户登录后只能看到自己的记录。管理员更新缴费状态if (pay.equals(action)) { int id Integer.parseInt(req.getParameter(id)); meterDao.updatePayStatus(id, 1); resp.sendRedirect(req.getContextPath() /meter?actionlist); }住户端查询需要从 session 中取当前登录用户的 id只查自己的数据HttpSession session req.getSession(); Resident current (Resident) session.getAttribute(currentUser); if (current null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } ListMeterRecord myRecords meterDao.findByResidentId(current.getId()); req.setAttribute(records, myRecords); req.getRequestDispatcher(/user/myFee.jsp).forward(req, resp);myFee.jsp里用 JSTL 遍历展示% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 trth月份/thth用水量/thth用电量/thth总费用/thth状态/th/tr c:forEach items${records} varr tr td${r.recordMonth}/td td${r.waterUsage}/td td${r.electricUsage}/td td${r.totalFee}/td td${r.payStatus 1 ? 已缴 : 未缴}/td /tr /c:forEach /tableJSTL 的c:forEach比在 JSP 里写 Java 脚本片段% %更规范也更符合 MVC 分层思想。答辩时如果被问到「为什么不用脚本片段」可以说「JSTL 让页面职责更单一便于维护」。4. 避坑与排查JSP 毕设项目最容易翻车的 5 个地方4.1 中文乱码现象、原因与三层解决现象表单提交后数据库里存的是????或者页面显示乱码。原因JSP 页面编码、请求编码、数据库编码、Tomcat 编码任何一层不一致都会导致乱码。解决三层同时设置。JSP 页面头部加% page contentTypetext/html;charsetUTF-8 languagejava %Servlet 里req.setCharacterEncoding(UTF-8)和resp.setContentType(text/html;charsetUTF-8)数据库连接 URL 加characterEncodingutf8。Tomcat 的conf/server.xml中 Connector 加URIEncodingUTF-8。4.2 404 错误路径匹配的常见误用现象表单提交或页面跳转时报 404。原因action或href写的是相对路径项目上下文路径变化后匹配不到。或者web.xml里url-pattern和表单action不一致。解决统一用${pageContext.request.contextPath}/xxx拼接路径。检查web.xml中 Servlet 映射和WebServlet注解是否重复或冲突。如果用了注解就不需要在web.xml里再配一遍。4.3 数据库连接泄漏连接数爆满的排查思路现象系统运行一段时间后报Too many connections。原因每次getConnection后没有在finally块中关闭或者异常发生时跳过了关闭逻辑。解决所有数据库操作统一用try-finally或try-with-resourcestry (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 执行操作 } catch (SQLException e) { e.printStackTrace(); }try-with-resources会自动关闭实现了AutoCloseable的对象比手动finally更简洁。MySQL 默认最大连接数是 151毕设演示时一般够用但养成好习惯没坏处。4.4 JSTL 标签不生效jar 包缺失与版本冲突现象页面直接显示c:forEach标签原文或者报The absolute uri cannot be resolved。原因WEB-INF/lib下缺少jstl.jar和standard.jar或者只放了一个。解决JSTL 1.2 版本只需要jstl-1.2.jar一个包。如果用的是老版本需要jstl.jarstandard.jar两个。确认 jar 包放在WEB-INF/lib下而不是项目根目录IDEA 中还要在 Project Structure 的 Artifacts 里确认这些 jar 会被打包进WEB-INF/lib。4.5 部署到 Tomcat 后页面样式丢失现象本地 IDEA 里跑正常打包成 war 部署到独立 Tomcat 后 CSS、JS、图片全部 404。原因页面里用了相对路径引用静态资源比如hrefcss/style.css但实际访问路径多了层上下文。解决静态资源引用也统一加${pageContext.request.contextPath}例如href${pageContext.request.contextPath}/css/style.css。或者在 JSP 头部定义base href${pageContext.request.contextPath}/让所有相对路径基于这个基准。5. 打包部署与答辩加分项把项目讲出工程味5.1 传统 JSP 项目打包 war 的完整流程IDEA 中打包 war 的步骤File → Project Structure → Artifacts → 添加 Web Application: Archive → 选择输出目录 → Build → Build Artifacts。生成的 war 包复制到 Tomcat 的webapps目录启动 Tomcat 会自动解压部署。命令行方式# 在项目根目录执行前提是已配置好目录结构 jar -cvf community_fee.war * # 将 war 包复制到 Tomcat webapps cp community_fee.war $TOMCAT_HOME/webapps/ # 启动 Tomcat $TOMCAT_HOME/bin/startup.sh访问http://localhost:8080/community_fee/login.jsp即可。war 包的好处是部署简单答辩时可以直接在老师电脑上跑不依赖 IDEA。5.2 答辩时被追问的技术点与回答思路老师常问的几个问题提前准备好答案追问点回答思路为什么用 JSP 不用前后端分离项目规模小、周期短JSP 开发效率高适合快速交付Session 和 Cookie 的区别Session 存服务端Cookie 存客户端登录状态用 Session 更安全数据库连接怎么管理的封装了工具类统一获取和关闭避免泄漏费用计算逻辑在哪一层在 Servlet 层计算DAO 层只负责存取职责分离如果并发缴费怎么处理可以在更新语句加WHERE pay_status0条件防止重复缴费最后一条是加分项。实际代码UPDATE meter_record SET pay_status1 WHERE id? AND pay_status0;通过executeUpdate返回的影响行数判断是否更新成功如果返回 0 说明已经被缴过了。这个细节能体现你对并发场景的思考。5.3 从毕设到可展示项目三个低成本改进方向如果想让项目在答辩时更出彩不需要大改架构三个小改进就够第一把单价配置抽到数据库表sys_config里管理员可以在后台修改水价电价体现「可配置」思维。第二给列表页加分页。数据量大了不分页会很难看用LIMIT offset, size实现即可前端加「上一页/下一页」按钮。第三加一个简单的费用统计图。用 ECharts 引入一个 CDN 链接后端返回 JSON 数据展示每月总费用趋势。不需要很复杂一个折线图就能让演示效果提升一个档次。// 在 JSP 中引入 ECharts 并渲染折线图 var chart echarts.init(document.getElementById(chart)); chart.setOption({ xAxis: { type: category, data: [2024-01,2024-02,2024-03] }, yAxis: { type: value }, series: [{ type: line, data: [1200, 1350, 1100] }] });数据可以从后端 Servlet 查出来后转成 JSON 塞进 request 域JSP 里用${monthData}取。这样既展示了前后端数据交互又让页面不那么单调。我自己做这类项目最大的教训是不要等到最后一天才打包部署。本地 IDEA 跑通和 war 包部署到独立 Tomcat 是两回事路径问题、jar 包冲突、编码问题都可能在部署阶段集中爆发。提前一周在干净环境里完整走一遍部署流程留出排查时间。希望帮到你。本文还有配套的精品资源点击获取