☰
JSP小区水电费管理系统毕设实战:从环境搭建到答辩避坑
2026/9/26 12:26:34 网站建设 项目流程

简介:这是一套面向高校计算机相关专业毕业设计的JSP小区水电费管理系统完整项目包,采用JSP+MySQL+B/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_HOME,Tomcat 解压即可用,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_301

Tomcat 启动验证:进入bin目录执行startup.bat(Windows)或startup.sh(Linux/Mac),浏览器访问http://localhost:8080,看到 Tomcat 默认页即成功。

注意:如果 8080 端口被占用,修改conf/server.xml中的<Connector port="8080"为其他端口,比如 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 xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="4.0"> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.community.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> </web-app>

这里url-pattern决定了浏览器访问的路径。比如表单提交到/login,就会触发LoginServlet的doPost方法。很多同学在页面里写action="LoginServlet"也能跑,但那是相对路径匹配,项目一改上下文路径就容易 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?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; 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(); } } }

serverTimezone=Asia/Shanghai这个参数在 MySQL 8.0 下必须加,否则会报时区错误。characterEncoding=utf8保证中文不乱码。如果你的 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?action=add" method="post"> 房号:<input type="text" name="roomNo" required><br> 姓名:<input type="text" name="name" required><br> 电话:<input type="text" name="phone"><br> <input type="submit" 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?action=list"); } } @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?action=list"); } } }

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?action=list"); }

单价建议存在数据库或配置文件里,不要硬编码在 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?action=list"); }

住户端查询需要从 session 中取当前登录用户的 id,只查自己的数据:

HttpSession session = req.getSession(); Resident current = (Resident) session.getAttribute("currentUser"); if (current == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } List<MeterRecord> myRecords = meterDao.findByResidentId(current.getId()); req.setAttribute("records", myRecords); req.getRequestDispatcher("/user/myFee.jsp").forward(req, resp);

myFee.jsp里用 JSTL 遍历展示:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table border="1"> <tr><th>月份</th><th>用水量</th><th>用电量</th><th>总费用</th><th>状态</th></tr> <c:forEach items="${records}" var="r"> <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> </table>

JSTL 的c:forEach比在 JSP 里写 Java 脚本片段(<% %>)更规范,也更符合 MVC 分层思想。答辩时如果被问到「为什么不用脚本片段」,可以说「JSTL 让页面职责更单一,便于维护」。

4. 避坑与排查:JSP 毕设项目最容易翻车的 5 个地方

4.1 中文乱码:现象、原因与三层解决

现象:表单提交后数据库里存的是????,或者页面显示乱码。

原因:JSP 页面编码、请求编码、数据库编码、Tomcat 编码任何一层不一致都会导致乱码。

解决:三层同时设置。JSP 页面头部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>;Servlet 里req.setCharacterEncoding("UTF-8")和resp.setContentType("text/html;charset=UTF-8");数据库连接 URL 加characterEncoding=utf8。Tomcat 的conf/server.xml中 Connector 加URIEncoding="UTF-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-resources:

try (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.jar+standard.jar两个。确认 jar 包放在WEB-INF/lib下而不是项目根目录,IDEA 中还要在 Project Structure 的 Artifacts 里确认这些 jar 会被打包进WEB-INF/lib。

4.5 部署到 Tomcat 后页面样式丢失

现象:本地 IDEA 里跑正常,打包成 war 部署到独立 Tomcat 后 CSS、JS、图片全部 404。

原因:页面里用了相对路径引用静态资源,比如href="css/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_status=0条件,防止重复缴费

最后一条是加分项。实际代码:

UPDATE meter_record SET pay_status=1 WHERE id=? AND pay_status=0;

通过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 包冲突、编码问题都可能在部署阶段集中爆发。提前一周在干净环境里完整走一遍部署流程,留出排查时间。希望帮到你。

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

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

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

立即咨询