
简介这是一份基于JSPMySQL的B/S架构小区水电费管理系统毕业设计项目面向计算机相关专业学生或Java Web学习者可用于毕业设计整体参考、课程设计或项目练手重点解决从需求分析、功能实现到部署演示的完整流程。压缩包共713个文件包含83个JSP页面、CSS/JS等前端样式脚本、SQL数据库脚本、工程配置文件及大量GIF演示截图整体仅10.12MB便于快速获取与本地部署。系统按前台、后台管理员和注册用户三个层次设计前台涵盖站内新闻、在线留言、在线注册及小区风景展示后台管理员可进行新闻管理、用户审核与水电费信息维护并管理缴费类别与记录注册用户则能修改个人资料、查看并缴纳水电费形成完整业务闭环。随包提供说明文档和演示视频可结合源码熟悉JSPMySQL项目结构、会话控制与数据库操作帮助读者理解多角色权限设计和日常业务逻辑。目前已有133人学习下载适合作为毕业设计选题或Java Web学习中的对照实现。1. JSP 项目还值得做小区水电费管理系统的真实内容毕业设计拿到这份压缩包的时候很多人第一反应是“这年头还有 JSP 项目” 实际上一线高校的毕设选题里JSP Servlet JDBC 依然是高频组合。这个系统要解决的问题很具体小区物业需要一个水电费抄表、计费、收缴和欠费催缴的线上台账。解压 zip 后你会看到代码目录、一份说明文档和一段演示视频但真正把它跑起来还需要自己处理 Tomcat 版本、数据库初始化、驱动加载和编码问题。这篇文章就按一个干过类似项目的工程师的思路把从建表、连库、跑通页面到答辩演示的路径完整过一遍。2. JSP 与 Servlet 的三层结构从表码到账单的数据流转2.1 表结构设计房间、水表、电表、账单的主键与冗余字段小区水电费系统最核心的实体是房间、用户、表计和账单。我一般会给每张表设一个自增的主键用room_id关联楼层和单元不在用户表里重复存地址。水表和电表分别记录当前表底数、上次抄表数、倍率和表状态。账单表要冗余记录当时的单价和用量这样历史账单不会因为改价而反算错。下面是常见的最小表设计。表名关键字段说明t_roomroom_id, floor_no, unit_no, owner_name小区房间基础信息t_useruser_id, room_id, login_name, mobile登录账户与房子多对一t_water_metermeter_id, room_id, current_reading, last_reading水表读数与倍率字段t_elec_metermeter_id, room_id, current_reading, last_reading电表读数可按峰谷扩展t_billbill_id, room_id, bill_month, fee_type, amount, status账单记录费用类型与支付状态金额不要用float或double计算时会出现0.30000004之类的误差用DECIMAL(12,2)存应收金额比较稳。水电表底数可能带小数用DECIMAL(14,3)记录倍率单独存换互感器后只改倍率不动历史数据。时间字段统一用DATETIME加上create_time和update_time两个公共字段排查账单异常时有据可查。状态字段建议用tinyint而不是字符串比如 0 未缴、1 已缴、2 销账后续配合字典表扩展成本低。2.2 DAO 层用 JDBC 查询PreparedStatement 怎么防注入数据访问层最常见的写法是 JDBC DAO。以“按房间号查未缴账单”为例写一个真正能拷出去用的方法。public ListBill findUnpaidBills(int roomId) { String sql SELECT bill_id, amount, bill_month, status FROM t_bill WHERE room_id ? AND status 0 ORDER BY bill_month DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, roomId); try (ResultSet rs ps.executeQuery()) { ListBill list new ArrayList(); while (rs.next()) { Bill b new Bill(); b.setBillId(rs.getInt(bill_id)); b.setAmount(rs.getBigDecimal(amount)); b.setBillMonth(rs.getString(bill_month)); b.setStatus(rs.getInt(status)); list.add(b); } return list; } } catch (SQLException e) { logger.error(查询未缴账单失败, roomId roomId, e); return Collections.emptyList(); } }这段代码先拼 SQL 骨架再用PreparedStatement的?占位符传参。setInt(1, roomId)代替字符串拼接既避免 SQL 注入也让 MySQL 走预编译缓存。try-with-resources保证了Connection、PreparedStatement、ResultSet自动关闭不用在finally里一层层判空。DBUtil.getConnection()通常从db.properties读驱动类和 URL。如果后续要支持分页把 SQL 换成LIMIT ?, ?再setInt两个参数即可但注意 MySQL 的 LIMIT 偏移量从 0 开始页数转换时容易差一。2.3 Servlet 转发与 JSP 表达式从 request 到 table 的完整链路控制层我习惯按“一个 Servlet 对应一个业务入口”来做。比如BillServlet的doGet里调用 DAO把结果放到 request 作用域再转发到bill_list.jsp。protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int roomId Integer.parseInt(request.getParameter(roomId)); BillDAO dao new BillDAO(); ListBill list dao.findUnpaidBills(roomId); request.setAttribute(billList, list); request.getRequestDispatcher(/bill_list.jsp).forward(request, response); }下面是本项目常见的请求映射表新建模块时可以先仿照这个结构定路径避免后期改 web.xml 时乱成一团。Servlet请求路径JSP 页面LoginServlet/loginlogin.jspRoomServlet/room/listroom_list.jspMeterServlet/meter/readmeter_read.jspBillServlet/bill/unpaidbill_list.jspJSP 页面里用 JSTL 的c:forEach循环输出不要手写out.println。这里有个隐藏问题如果用response.sendRedirect跳转request 域的数据会全部丢失所以只能在前端页面之间跳转时用重定向。如果项目里某个模块需要 AJAX 返回 JSON可以用jsp jsonarray import的方式在 JSP 里用 Jackson 的ObjectMapper把ListBill转成JSONArray输出前端再解析。那种做法比在 Servlet 里手动拼 JSON 字符串更安全也不会把引号转义搞乱。3. 把源码跑起来Tomcat MySQL 的最小部署步骤3.1 解压后先看清这四类文件拿到压缩包别急着双击演示视频。先把 zip 解开里面通常有四样东西源码目录、说明文档、SQL 脚本和演示视频。拿到压缩包别急着双击演示视频。先把 zip 解开里面通常有四样东西源码目录、说明文档、SQL 脚本和演示视频。一个标准的 JSP 项目目录长这样文件/目录作用src/com/.../daoDAO 接口与实现src/com/.../servlet控制器 ServletWebRoot/login.jsp登录页WebRoot/WEB-INF/web.xmlServlet 映射与欢迎页配置db.sql建库建表脚本readme.doc/pdf项目说明说明文档除了抄写数据库结构还会写运行环境但经常漏掉一个信息JDK 版本。JSP 项目最保守的环境组合是 JDK 8 Tomcat 8.5 MySQL 5.7这套组合能省掉很多版本兼容问题。如果你手头是 JDK 17建议不要直接上 Tomcat 8JSP 依赖的javax.servlet和javax.annotation包在 JDK 9 之后已经不在默认模块里了运行时会报ClassNotFoundException。最常见的做法是切换一个 Tomcat 9或者给项目补上对应的依赖 jar。3.2 数据库初始化建库脚本与连接参数对照先创建数据库并导入 SQL 脚本mysql -u root -p -e create database if not exists property_charge default character set utf8mb4; mysql -u root -p property_charge db.sql导入完成后用show tables验证至少出现t_room、t_bill等核心表。不要用latin1建库否则中文数据全变成问号。SQL 脚本里的表名如果带前缀比如t_保持原样不要手工改因为 DAO 层里的 SQL 都是写死表名的。然后找到db.properties一般是下面这个格式jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/property_charge?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456这里最容易被忽略的是serverTimezone。MySQL 8 的驱动不指定时区连接会直接报The server time zone value。如果用的是 MySQL 5.7com.mysql.jdbc.Driver能用换成 8.0 驱动时类名改成com.mysql.cj.jdbc.Driver。另外characterEncodingutf8和useUnicodetrue要成对出现否则页面中文乱码的锅一半在这。3.3 配好 Tomcat 启动Eclipse/IDEA 两种方式Eclipse 用户Import Existing Projects into Workspace选中源码目录然后右键项目 Properties Java Build Path Add Library Server Runtime选你本机的 Tomcat。再把db.properties里的密码改成本机密码最后 Run As Run on Server。IDEA 用户直接用mvn tomcat7:run更省事前提是项目是 Maven 结构。如果不是 Maven需要手动配置 ArtifactsProject Structure Artifacts Web Application: Exploded然后 Use classpath of module 选择项目最后在 Tomcat Run Configuration 里 Deploy 那个 artifact。mvn clean package -DskipTests mvn tomcat7:runmvn tomcat7:run默认端口是 8080占位时可以加-Dmaven.tomcat.port8081。这种启动方式不需要手动部署 war它会把src/main/webapp当 Web 根目录改 JSP 之后浏览器刷新立刻生效能省下不少调试时间。但注意tomcat7插件只适配 Servlet 3.0如果你的项目里用了WebServlet注解没问题用了javax.servlet.jsp新特性就得换 Tomcat 8 的嵌入式插件。3.4 验证登录与页面跳转第一个能跑通的 URL启动完成后浏览器输入http://localhost:8080/项目名/login.jsp。如果看到登录框说明 JSP 编译和项目部署都正常。登录时先用db.sql里预置的管理员账号试。说明文档里一般会给出初始账号但经常会遇到一个情况数据库里确实有这条用户记录但密码是 MD5 存进去的。这个时候先在 MySQL 里查一下用户表看密码字段是不是一长串哈希值是的话说明登录页走了 MD5 后用md5(password)再比对不要把明文密码直接截回去改。这个验证步骤能帮你判断是后端逻辑问题还是数据初始化问题。4. 核心业务逻辑抄表、计费、催缴的状态机设计4.1 抄表录入基数与倍率的处理抄表页面是物业工作人员每天用的入口。录入当前表底数后系统自动计算本次用量本次用量 (当前读数 - 上次抄表数) * 倍率。这个公式本身简单但毕业设计里经常忽略一个问题换表后上次抄表数要清零倍率也可能变化。我一般会在t_water_meter表里加一列meter_type标识是否更换过表抄表时先判断表状态再用不同公式。public BigDecimal calcUsage(BigDecimal currentReading, BigDecimal lastReading, BigDecimal rate) { if (currentReading.compareTo(lastReading) 0) { throw new IllegalArgumentException(当前读数不能小于上次抄表数); } return currentReading.subtract(lastReading).multiply(rate); }注意BigDecimal的compareTo返回值负数表示小于0 等于正数大于。很多同学在这里用currentReading - lastReading得到的是double一旦读数出现小数位数不一致减出来的结果在数据库里对不上。倍率字段允许为 0 吗不允许。换表后倍率可能变成 1但不会变成 0写校验时就按rate 0判断。4.2 计费规则阶梯电价与公摊水费小区电费很少是单一定价常见做法是第一阶梯 180 度内按 0.55 元/度超过部分按 0.85 元/度。水费除了表上读数还要分摊楼层公共用水。这个分摊逻辑用 Java 写很容易但要注意“阶梯”判定基准是当月总用量还是当月剩余额度。阶梯电量区间度单价元/度第一阶梯0 - 1800.55第二阶梯181 - 2800.85第三阶梯281 及以上1.20public BigDecimal calcElecFee(BigDecimal usage) { BigDecimal fee BigDecimal.ZERO; BigDecimal remain usage; remain subtractStep(remain, 180, new BigDecimal(0.55), fee); remain subtractStep(remain, 100, new BigDecimal(0.85), fee); if (remain.compareTo(BigDecimal.ZERO) 0) { fee fee.add(remain.multiply(new BigDecimal(1.20))); } return fee.setScale(2, RoundingMode.HALF_UP); }上面代码里的subtractStep是一个辅助方法作用是“当前用量与阶梯上限比较取较小值乘以单价累加进费用最后返回剩余未计费量”。实际项目里不建议把三个阶梯拆成三个 SQL 子查询那样只能算总费用拿不到每个阶梯的明细。如果需要在账单里展示“第一阶梯用电/第二阶梯用电”就把这个计算结果保存成 JSON 字段例如{tier1:180,tier2:80}。4.3 账单生成与缴费状态流转每个月初生成账单可以放在 Servlet 里手动触发也可以写一个TimerTask定时跑毕业设计用后者更有亮点。生成逻辑是遍历所有房间读取水表和电表上月末的读数和本月初始读数计算费用后插入t_bill。为了不重复生成需要在账单表加唯一索引uk_room_bill_monthroom_id, bill_month, fee_type。插入前先INSERT IGNORE对重复的月份直接跳过。status含义下一步动作0未缴可缴费、可催缴1已缴生成缴费流水更新物业费余额2滞纳金账单超期后自动加收滞纳金3已销账财务核销不参与统计缴费流程里要处理并发问题同一个房间两个人同时点缴费。最简单的方法是给t_bill加update_time支付动作执行UPDATE t_bill SET status1 WHERE bill_id? AND status0影响行数为 1 才代表当前请求抢到了缴费权。此时再插入缴费流水如果插入失败要回滚。这一套“先锁定状态再写流水”的写法不需要SELECT ... FOR UPDATE对中小型小区也够用。5. 排错JSP 编译 class、JDBC 驱动和中文乱码的常见坑5.1 JSP 编译 class 文件保存在哪里很多同学改完 JSP 文件刷新浏览器发现还是旧页面第一反应是清浏览器缓存。其实 JSP 是服务端编译Tomcat 会把编译后的.java和.class存到tomcat/work/Catalina/localhost/应用名/org/apache/jsp/下。修改 JSP 后如果依然旧效果检查这个目录下对应文件的时间戳。调试命令# Linux/mac 下看 jsp 编译产物 find /path/to/tomcat/work -name *bill_list*.java # Windows 下直接进 work 目录搜文件找到以后用文本编辑器打开.java文件能看到 JSP 里的静态标签变成什么 Java 输出语句。这能帮你判断一个经典问题JSP 里写了一个编码为 GBK 的meta标签但浏览器按 UTF-8 解码编译后的java文件中文全乱解决方式是把 JSP 页面顶部的contentType改成text/html; charsetutf-8然后重启 Tomcat 清空 work 目录。5.2 驱动没加载ClassNotFoundException 的根因ClassNotFoundException: com.mysql.jdbc.Driver这类错误十有八九是 mysql 驱动 jar 没有放到运行时的 classpath 里。如果是普通 web 项目把mysql-connector-java-5.1.x.jar放到WebRoot/WEB-INF/lib/下如果是 Maven 项目确认pom.xml里 dependency 的 scope 是compile而不是provided。还有一种隐蔽情况Tomcat 的 lib 目录下已经有旧版本驱动 jar而项目 lib 里有新版本两个同时存在导致类加载顺序诡异。解决办法是删掉 Tomcat lib 里的驱动统一用项目内的版本。5.3 中文乱码三处编码设置缺一不可乱码问题在 JSP 项目里几乎绕不过去查的时候按下面顺序排查。位置设置失败现象JSP 页面pageEncodingUTF-8且 contentType 带 charset页面静态中文乱码Servletrequest.setCharacterEncoding(UTF-8)在 getParameter 之前表单提交中文乱码JDBC URLcharacterEncodingutf8数据库读写中文乱码request.setCharacterEncoding只对POST请求生效GET请求的参数由 Tomcat 连接器的 URIEncoding 决定所以要把server.xml里 Connector 的URIEncodingUTF-8加上。还有一个细节Tomcat 8 以上默认是 UTF-8Tomcat 7 需要手动配置。如果你的项目部署在 Tomcat 7 上检查到乱码先看这个配置。Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /6. 让演示视频不翻车答辩前的几个实测技巧6.1 录制视频的数据准备与操作节奏演示视频不要录完整操作过程只录三个片段登录、抄表生成账单、缴费成功。每个片段之间停顿 2 秒方便后期写字幕。打开浏览器时禁用浏览器翻译插件否则 JSP 页面的开发者工具会多出一堆看不到请求记录干扰录屏。预置数据要追求“能展示业务边界”录抄表之前把其中一个房间的电表读数改成比上次少用来展示录表校验的报错另一个房间故意让账单超期展示滞纳金生成。视频里出现报错不是坏事准备好台词讲解为什么会报错反而比一帆风顺更有说服力。6.2 预演时每次要重置的数据答辩现场演示经常因为上次试验残留数据导致页面异常。准备一个清理 SQL 脚本里面包含三条DELETE和一条ALTER TABLE AUTO_INCREMENT1在演示之前执行。这样能保证每次打开项目都是一张干净的表。如果演示过程中出现“缴费成功但账单页状态没更新”先停一下打开 MySQL 看 status 字段再刷新页面一般不是代码问题而是浏览器缓存。6.3 演示技巧按下 F12 打开 Network 面板再操作答辩时如果网络环境稳定按 F12 打开浏览器的 Network 面板再点抄表按钮。这样评审老师能看到请求 URL 和状态码比单纯看界面多一层技术说服力。讲解顺序建议先讲表关系再讲一次完整缴费流程最后用“如果把小区换成一栋学校宿舍哪些代码不用改”这样的表述收尾。这套系统核心是状态机加常规 CRUD只要把表结构关系和事务边界回答清楚答辩问答环节不会太难过。本文还有配套的精品资源点击获取