资讯详情 JSP小区水电费管理系统毕业设计:部署与代码解析
📅 2026/10/10 12:22:28
简介一份基于 JSP 与 MySQL 的 B/S 架构小区水电费管理系统毕业设计资源面向计算机类毕业生及 Java Web 学习者可用于毕业设计、课程设计或项目实训。压缩包包含 713 个文件大小约 10.12MB其中以 JSP 动态页面、CSS 样式、JavaScript 脚本为主要代码文件同时提供数据库备份与 SQL 脚本另有大量 GIF 截图辅助查看各页面与操作流程。系统实现网站前台新闻浏览、在线留言、会员注册、小区风景展示后台管理员可进行新闻发布、用户审核、水电费类别设置与缴纳信息管理注册用户则支持个人资料修改、水电费在线查询与缴纳。资源随附完整源码、设计说明文档和演示视频便于搭建开发环境、梳理 JSP 与 MySQL 交互思路、理解系统分层与数据库设计也可直接用于毕业论文撰写和二次功能扩展。该资源已有 134 人学习浏览适合需要完整可运行毕业设计项目及配套说明的读者参考。1. 为什么这个压缩包值得你打开先弄清它到底想让你做什么第一次拿到(Jsp项目)小区水电费管理系统毕业设计(源码说明演示视频).zip这个压缩包大多数人会先干两件事解压然后找里面有没有能直接双击的入口。如果按这个思路走大概率翻车JSP 项目不是桌面软件它必须部署到 Web 容器里才能被浏览器访问。这类项目解决的是小区物业管理里最日常的一环抄表员把水表、电表读数录进系统系统按用量和单价算出费用生成账单住户缴费后留下流水管理员再盯着欠费名单去催缴。技术上走的是 JSP Servlet JDBC Tomcat MySQL 这套经典 Java Web 组合也是毕业设计里出现频率最高的技术栈之一。这个压缩包的价值不在于代码量有多大而在于它把“抄表、计费、缴费、催缴”这条完整业务链路用最基础的方式串了起来。对要做课程设计或毕业设计的人来说它是一个能直接对照的功能原型对刚学完 Java 基础、想理解 Web 请求是怎么进到数据库的人来说把它跑通比看十遍教程都管用。但也要说句实话它并不是解压即用。真正费时间的不是源码而是环境版本、数据库初始化、部署路径这三件配置上的事。这篇笔记就从这三个点切入按“先看懂结构 → 本地跑通 → 再读关键代码 → 最后绕开常见坑”的顺序把项目从头到尾捋一遍。2. 小区水电费业务拆开看JSP 页面、Servlet 控制与 JDBC 数据流2.1 三层协作关系我拿到源码后优先确认的几件事JSP 项目跟现在前后端分离的写法完全不同。它的页面上有 Java 代码一个请求到了服务器可能经过 JSP 直接渲染也可能先由一个 Servlet 接收再跳转到 JSP。传统毕业设计里最常见的分层是三层JSP 负责显示和收集输入Servlet 负责接收请求、调用业务方法、决定跳转到哪个页面DAO 类负责用 JDBC 拼 SQL 访问数据库。页面、控制、数据三件事分开写好处是每一块都能单独替换也好查问题。我拿到这类压缩包第一件事不是看页面而是打开web.xml确认里面配置了哪些 Servlet 的 URL 映射。因为一个请求最终由谁处理全部由这个文件决定。值得优先确认的有四件事url-pattern与 Servlet 类的对应关系欢迎页是index.jsp还是某个indexServlet有没有配置过滤器数据库连接参数是写在web.xml里还是独立的.properties文件里。这四点确认完整个项目的入口和走向基本就清楚了。如果项目里没有web.xml那多半是稍新一点的写法用了注解WebServlet(/login)。但毕业设计这样的老技术栈里web.xml还是主流少数项目会两种混用。排查路径问题时最妙的技巧就是先在这里按图索骥浏览器地址栏里写的是什么web.xml里映射的又是什么两者对不上就是 404 的根源。2.2 水电费的计算模型本次用量怎么算、单价从哪里来水电费系统的核心公式很简单本次用量 本次抄表读数 - 上次抄表读数费用 本次用量 × 单价。难点不在公式而在“上次读数从哪取”“单价存哪里”。毕业设计级别的实现一般不会引入阶梯电价账单里往往只存一个默认单价甚至直接把单价写在抄表记录里。我一般会先查抄表记录表的结构看它有没有单独存price字段。如果抄表时单价已经写进记录那说明这套系统不允许中途调价每张表用同一个价如果单价来自一张独立的费率表说明设计者考虑到了调价但代码复杂度也会高一些。看懂这一点对你后续做二次开发很关键——想把“阶梯计费”加进去改动范围就是“先判断用量落在哪个档位再按不同档位分别计算”如果原表结构里单价字段已经固定还需要额外加一张阶梯配置表。另外水费和电费在项目里通常通过一个类型字段区分比如meter_type1 代表水表、2 代表电表。业务上也是分开抄表、分开记账但最终汇总到一张账单里账单里同时有water_fee、elec_fee和total_amount。看代码时留意这个字段因为后面所有统计报表都依赖它。2.3 四张核心数据表房间、表计、账单、缴费的关系一个功能完整的毕业设计项目数据库一般不止四张表但核心的一定是这四张用户表、抄表记录表、账单表、缴费记录表。房间信息通常被合并进用户表或者单独成表看具体设计。我拿一张典型的表结构做说明你拿到手后可以对照着自己的库看结构差异。表名核心字段作用tb_useruser_id, user_name, password, role, room_no登录账号与角色区分管理员、抄表员、住户tb_roomroom_no, floor, area, owner_name小区房间基础信息通常与用户表、表计表关联tb_metermeter_id, room_no, meter_type, last_reading, curr_reading, price, amount, reading_date每次抄表形成一条记录保存上次与本次读数tb_billbill_id, room_no, bill_month, water_fee, elec_fee, total_amount, status月度账单status 区分已缴、未缴tb_paymentpayment_id, bill_id, room_no, pay_amount, pay_time缴费流水与账单是一对多关系下面是抄表记录表的建表语句这是一个非常典型的毕业设计表结构CREATE TABLE tb_meter ( meter_id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL, meter_type TINYINT NOT NULL COMMENT 1-水表, 2-电表, last_reading DECIMAL(10,2) NOT NULL COMMENT 上次抄表读数, curr_reading DECIMAL(10,2) NOT NULL COMMENT 本次抄表读数, price DECIMAL(6,3) NOT NULL COMMENT 本次计费单价, amount DECIMAL(10,2) DEFAULT 0 COMMENT 本次费用, reading_date DATETIME DEFAULT NULL, INDEX idx_room_type (room_no, meter_type) ) COMMENT水电表抄表记录;这段建表 SQL 里有几个值得留意的点。DECIMAL而不是FLOAT存金额是必须坚持的习惯浮点数在比较和累加时会出误差。meter_type用TINYINT存数字而非直接存字符串省空间也方便代码里用if (type 1)判断。last_reading和curr_reading都设置了NOT NULL因为计费公式缺了任何一个都没法算。最后那条INDEX idx_room_type是后续统计按房间、按表类型查询时最常用的索引别去掉。你现在可能觉得这些是小题大做等数据量到几万条再回来优化就晚了。3. 本地把项目跑起来环境版本、数据库初始化和 Tomcat 部署3.1 环境组合怎么选JDK 8、Tomcat 9 与 MySQL 5.7 最省事把 JSP 毕业设计跑起来最怕的就是环境太新。新版 JDK 把很多旧 API 标记为废弃Tomcat 10 直接把包名从javax.servlet换成了jakarta.servletMySQL 8 的认证方式又跟老驱动不兼容。三个“新”叠一起源码没动一行错误堆了一屏。我的建议是如果说明文档里明确写了环境要求就按它来没写的话优先用JDK 8 Tomcat 9 MySQL 5.7这个组合。JDK 8 已经足够老几乎所有 JSP 毕业设计源码都是按它编译的Tomcat 9 仍然支持javax.servlet包名不会出现导入报错MySQL 5.7 的mysql_native_password认证跟老驱动配合顺畅不会冒出各种公钥、时区问题。MySQL 8 也不是不能用但至少要额外处理三件事驱动换到 8.x、连接 URL 加时区参数、数据库用户可能要把认证方式改回mysql_native_password。这些不是大问题但会在你本来就不熟悉的环境上增加排错的干扰项。本地跑通阶段能少一个变量就少一个变量。3.2 初始化数据库建库、导表、改连接参数大部分毕业设计压缩包都会附带一个.sql文件放在数据库或sql目录下。如果没找到就自己按上面的表结构建库。先用命令行把库建好再导入表结构和初始化数据# 登录 MySQL假设密码是 123456 mysql -uroot -p123456 # 在 MySQL 命令行中执行 CREATE DATABASE community_water_elec DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE community_water_elec; # 退出后把 sql 文件导入 mysql -uroot -p123456 community_water_elec water_elec.sql这里的关键参数是utf8mb4。老项目经常用utf8但utf8mb4才是完整的 UTF-8能存生僻字和 emoji兼容性也更好。导入完成后用一句SHOW TABLES;确认表建出来了。我见过不少次数据库导入半天没反应结果发现是 SQL 文件在压缩包里没解压出来或者路径带中文导致 MySQL 找不到文件。接下来改连接参数。JSP 毕业设计里数据库配置一般集中在两个地方WEB-INF/classes下的jdbc.properties或者 src 目录里的DBUtil.java。找到后把用户名密码改成你本机的值jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/community_water_elec?useUnicodetruecharacterEncodingUTF-8useSSLfalse jdbc.usernameroot jdbc.password123456注意这里jdbc.url里的characterEncodingUTF-8是必须的。JSP 页面设了 UTF-8数据库连接也要设成 UTF-8否则页面显示正常一查数据库全是问号。另外老项目里com.mysql.jdbc.Driver这个驱动类名很常见如果你本地用的是 MySQL 8 驱动需要改成com.mysql.cj.jdbc.Driver两者差别就在一个cj上报错信息里会提示得很明显。3.3 把源码挂到 Tomcat 上开发工具打包与命令行部署两种方式数据库就绪后把项目导入 IDE。这里说两种最常见的部署路径一种是直接在 IDE 里配置 Tomcat另一种是打成 WAR 包扔进 Tomcat 的 webapps 目录。IDE 方式适合学习因为能断点调试命令行方式适合不想折腾工具的人一条命令就能启动。IDE 方式的典型操作是导入项目时选择Import Project把文件夹当 Maven 或 Web 项目导入然后配置 Tomcat Server在 Deployment 里把项目以war exploded方式挂上设置 Application Context 为/water_elec这个值决定了你访问时的路径。启动后浏览器访问http://localhost:8080/water_elec/就能看到登录页。命令行方式更简单前提是压缩包里有打包好的.war文件# 把 war 包复制到 Tomcat 的 webapps 目录 cp water_elec.war $TOMCAT_HOME/webapps/ # 启动 TomcatWindows 用 startup.batLinux/macOS 用 startup.sh $TOMCAT_HOME/bin/startup.sh # 看日志确认启动成功 tail -f $TOMCAT_HOME/logs/catalina.out访问路径就是http://localhost:8080/water_elec/Tomcat 会自动解压 war 包。这里要注意war 包文件名决定访问的上下文路径如果想改路径直接改文件名再重启 Tomcat 就行。启动日志里看到Server startup in [xxx] milliseconds才算成功如果直接退出了去日志里搜Exception是最高效的排查动作。4. 关键代码拆解登录、编码、抄表计费三个必看位置4.1 DBUtil 与登录 DAO先弄清楚项目怎么连数据库很多 JSP 毕业设计项目数据库连接代码是反复复制粘贴的。每个 DAO 里都有一段Connection conn DBUtil.getConnection()所以看懂DBUtil就等于拿到了所有数据库操作的钥匙。典型实现长这样package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String DRIVER com.mysql.jdbc.Driver; private static final String URL jdbc:mysql://localhost:3306/ community_water_elec?useUnicodetruecharacterEncodingUTF-8useSSLfalse; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }逻辑说明static块在类加载时执行一次作用是注册 JDBC 驱动保证后续getConnection()能拿到数据库连接。Class.forName(DRIVER)是传统写法新版驱动也可以省略但保留它兼容性最好。真正干活的是DriverManager.getConnection(URL, USER, PASSWORD)它返回一个数据库会话。参数说明里最常改的是PASSWORD和URL中间的库名。还有一个容易忽略的细节static final变量在编译时就会替换成字面量如果你改了常量而不重新编译整个项目旧值会残留。所以改完DBUtil后一定要重新编译不要只重启 Tomcat。登录功能的 DAO 层通常是这个思路接收用户名密码查用户表能查到就说明账号存在。更规范一点的做法是用PreparedStatement而不是拼 SQL 字符串因为后者存在 SQL 注入风险public User findByLogin(String username, String password) { String sql SELECT user_id, user_name, role, room_no FROM tb_user WHERE user_name ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { User u new User(); u.setUserId(rs.getInt(user_id)); u.setUserName(rs.getString(user_name)); u.setRole(rs.getString(role)); u.setRoomNo(rs.getString(room_no)); return u; } } catch (SQLException e) { e.printStackTrace(); } return null; }这里用的是try-with-resourcesConnection、PreparedStatement、ResultSet会自动关闭比手写finally再调close()干净得多。如果老项目里看到的是Statement stmt conn.createStatement()再拼接 SQL我建议你自己改成这个版本逻辑完全一样安全性高一个档次。4.2 统一中文编码过滤器是解决乱码的最省心方式JSP 项目里出现中文乱码通常不是某一个文件的问题而是页面、请求、数据库三者编码不一致共同导致的结果。毕业设计里最常见的坑是页面用了utf-8但request.setCharacterEncoding()只写在某一个 Servlet 里另一个 Servlet 忘记写于是那个功能一提交就乱码。一劳永逸的办法是写一个编码过滤器让所有请求都经过它统一处理package com.example.filter; import javax.servlet.*; import java.io.IOException; public class EncodingFilter implements Filter { private String encoding UTF-8; Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(encoding); response.setCharacterEncoding(encoding); response.setContentType(text/html; charset encoding); chain.doFilter(request, response); } }过滤器的作用是在请求到达 Servlet 之前拦截下来先设好编码再放行chain.doFilter。这样所有 JSP、所有 Servlet 就都不用单独写编码设置。配好过滤器类后还要在web.xml里注册filter filter-nameencodingFilter/filter-name filter-classcom.example.filter.EncodingFilter/filter-class /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mappingurl-pattern写成/*表示所有请求都经过过滤器这个一定不能漏。漏了的话只有部分路径生效查乱码会查到你怀疑人生。另外页面顶部的pageEncoding也要保持一致建议每个 JSP 开头都写全% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %contentType控制浏览器解析方式pageEncoding控制 JSP 文件编译时读取源码的编码。两个都写是避免“某些中文能显示、某些中文乱码”的奇偶问题最直接的手段。4.3 抄表计费代码怎么读先算一笔手工账再去看代码抄表计费是整个系统里最有业务含量的部分也是答辩时最容易追问的模块。毕业设计里常见做法是一个 Servlet 接收表单提交的本月读数从数据库里把上次读数挖出来算出本次用量和费用。核心片段大概长这样// MeterRecordServlet 的 doPost 方法片段 String roomNo request.getParameter(roomNo); int meterType Integer.parseInt(request.getParameter(meterType)); double curr Double.parseDouble(request.getParameter(currReading)); MeterRecord lastRecord meterDao.findLatestByRoomNoAndType(roomNo, meterType); double last lastRecord null ? 0.0 : lastRecord.getCurrReading(); double price Double.parseDouble(request.getParameter(price)); double usage curr - last; double amount usage * price; MeterRecord record new MeterRecord(); record.setRoomNo(roomNo); record.setMeterType(meterType); record.setLastReading(last); record.setCurrReading(curr); record.setPrice(price); record.setAmount(amount); meterDao.insert(record);这段代码的关键是findLatestByRoomNoAndType查出的不是last_reading字段而是上一次那条记录的curr_reading。这里比较容易搞混每条表计记录都保存了“上次读数”和“本次读数”但对当前这次的抄表来说真正的“上次读数”来自上一条记录的“本次读数”。数据库里存的last_reading只是冗余冗余在表里方便核对罢了。建议你把这段逻辑跟数据库表对应着读一遍先查出一条房间水表最新记录比如上次读数 100本次抄表 155单价 3.5那么 usage 就是 55amount 是 192.50。把这个手工账算清楚后再看代码里每一步是不是跟你算的一致。如果页面上金额跟你算的不一样那问题通常出在Double.parseDouble的精度上或者price取错了来源。5. 避坑实录这 5 个问题几乎占掉本地部署 80% 的排错时间5.1 javax.servlet 程序包不存在的编译错误现象导入源码后大量.java文件报红提示程序包 javax.servlet 不存在或者找不到 HttpServlet如果项目能编译启动时也可能抛ClassNotFoundException: javax.servlet.http.HttpServlet。原因Tomcat 10 起Servlet 规范从 Java EE 迁移到 Jakarta EE所有javax.servlet包名被替换成jakarta.servlet。而绝大多数 JSP 毕业设计项目是按 Tomcat 8/9 写的包里还是javax。版本跨了一代代码自然对不上。解决最省事的做法是把 Tomcat 换成 9.x什么都不用改项目直接就能编译。如果你非得用 Tomcat 10也可以全局搜索替换把代码和 JSP 里的javax.servlet全部改成jakarta.servlet但要留意web.xml的命名空间也要同步升级否则容器可能直接拒绝启动。我的经验是为了跑通一个毕业设计不值得付出这个改造代价。5.2 数据库连接被拒认证、时区与连接串三个变量现象启动项目后页面或日志报Access denied for user rootlocalhost或者Public Key Retrieval is not allowed再或者时区相关的The server time zone value ... is unrecognized。原因绝大部分是 MySQL 8 环境导致的。MySQL 8 默认的caching_sha2_password认证方式和老驱动不兼容于是要么直接报拒绝访问要么在建立连接时报公钥检索问题。还有一个是 MySQL 8 对时区敏感连接 URL 里不写serverTimezone驱动没法判断会话时区。解决如果在本地调测试在连接 URL 后面补上useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai同时把驱动 jar 换成 mysql-connector-java 8.0.x。想彻底省心把 MySQL 用户认证方式改掉ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;这条语句只推荐在本地开发环境用作用是让 MySQL 用户使用老式密码协议兼容所有旧驱动。改完不用重启 MySQL重连就生效。5.3 请求 404 但页面能打开问题藏在路径上现象登录页能正常打开但点登录按钮后地址栏变成 404或提示找不到loginServlet还有的是页面能看到但 CSS、JS 全部丢失控制台一片 404。原因JSP 毕业设计里路径问题一直是大户。常见有两类表单action写了相对路径比如actionlogin当前页面在/water_elec/login.jsp时请求会发到/water_elec/login但如果你从/water_elec/index.jsp转发过去相对路径就可能变成/water_elec/xxx/login必然 404。CSS 同理JSP 里写hrefcss/style.css在二级路径页面下就会找错位置。解决我自己处理这类项目时会先把所有 JSP 里引用静态资源的地方改成绝对路径用${pageContext.request.contextPath}开头link relstylesheet href${pageContext.request.contextPath}/css/style.css form action${pageContext.request.contextPath}/login methodpost${pageContext.request.contextPath}会动态输出项目上下文路径不管页面在几级目录下都能正确拼出完整 URL。Servlet 里跳转也同样处理用response.sendRedirect(request.getContextPath() /bill/list.jsp)。养成这个习惯后路径类 404 基本绝迹。5.4 中文乱码同时出现在页面和日志里现象登录后用户名正常但住户姓名、房间备注等输进去再查出来就是???或者页面全是乱码Tomcat 控制台日志也是乱码。原因这是三道关卡同时出问题才会这么彻底。第一道JSP 页面本身编码不对第二道Servlet 接收请求时没设request.setCharacterEncoding第三道MySQL 库表字符集不是 utf8 或 utf8mb4。任何一道没通中文都会在链路某一环节坏掉。解决按这个顺序排查一次性解决全部三道关卡。先确认 JSP 顶部的pageEncoding是 UTF-8再在web.xml里注册 4.2 节的EncodingFilter保证每个请求都被设置编码最后检查建库语句里有没有DEFAULT CHARACTER SET utf8mb4。如果数据库已经建好了可以通过下面命令修改已有库表ALTER DATABASE community_water_elec CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE tb_user CONVERT TO CHARACTER SET utf8mb4;另外 Windows 上 Tomcat 控制台乱码还有一个元凶Tomcat 9 之前的catalina.bat默认用 GBK 输出日志。这个不影响页面功能只是看着心烦我一般直接忽略不在它上面浪费时间。5.5 服务能启动但浏览器一直打不开端口和上下文路径现象Tomcat 启动日志显示成功但浏览器访问http://localhost:8080/water_elec/一直转圈或直接显示“无法访问此网站”。原因最常见的是端口被占用或者项目上下文路径和实际访问路径不一致。Tomcat 默认端口 8080如果本机装了别的服务占用启动时会报端口冲突但有些 IDE 配置里会自动换端口日志里显示的是8081你还在访问 8080当然打不开。上下文路径问题则是 IDE 里 Artifact 名称跟你web.xml配置的路径对不上。解决先看catalina.out日志里的这句Tomcat started on port(s): 8080 (http)。端口是多少就访问多少。再确认部署路径方法是访问/根路径Tomcat 首页能打开说明容器本身没问题剩下就是项目上下文路径的问题了。你可以临时把 war 包改名成ROOT.war部署这样访问http://localhost:8080/就是项目首页省去上下文路径的困扰。这种方式只推荐在本地验证用。6. 再往前走一步把毕业设计版本改造成可持续用的入门系统6.1 用连接池替换裸 JDBC改动集中在一个类里跑通之后你会很快发现一个瓶颈每次数据库操作都要新建连接、用完关闭在高一点并发下就会卡顿。毕业设计代码里清一色是DriverManager.getConnection这种直连方式真实业务里不会这么写。最简单的升级方案是引入连接池代码改动只集中在DBUtil一个类里。以 HikariCP 为例把原本的DBUtil.getConnection()换成数据源取连接private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/community_water_elec?useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(123456); config.setDriverClassName(com.mysql.cj.jdbc.Driver); config.setMaximumPoolSize(10); config.setMinimumIdle(2); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }setMaximumPoolSize(10)代表最多保持 10 个连接setMinimumIdle(2)代表空闲时至少保留 2 个。对毕业设计这个量级的系统10 个连接绰绰有余。别的 DAO 代码一行都不用改因为它们调用的还是DBUtil.getConnection()只是内部实现变了。这就是把公共工具类写好处的典型场景。6.2 月度结算批处理一次生成当月全部账单手工逐户抄表、逐户算费、再逐户生成账单是这类系统操作起来最繁琐的部分。毕业设计里往往停留在手工单条操作但真实物业场景是按月批量做的。你可以写一个批量结算方法遍历所有房间取每个房间水表、电表的最新抄表记录生成一张当月账单并把状态置为未缴。核心逻辑用伪代码表达是这样的ListString roomList roomDao.findAllRoomNos(); String month 2025-06; for (String roomNo : roomList) { MeterRecord water meterDao.findLatestByRoomNoAndType(roomNo, 1); MeterRecord elec meterDao.findLatestByRoomNoAndType(roomNo, 2); Bill bill new Bill(); bill.setRoomNo(roomNo); bill.setMonth(month); bill.setWaterFee(water null ? 0 : water.getUsage() * water.getPrice()); bill.setElecFee(elec null ? 0 : elec.getUsage() * elec.getPrice()); bill.setTotalAmount(bill.getWaterFee() bill.getElecFee()); bill.setStatus(待缴费); billDao.insert(bill); }这段补全了“系统自动生成账单”这个关键环节。注意我把water null的情况考虑进去了因为新装表或者极端情况下可能没有抄表记录这时候直接取字段会抛空指针。批量任务跑完再配合一个简单的SELECT COUNT(*) FROM tb_bill WHERE status待缴费 AND month2025-06就能验证这次结算是否完整。开发时把这个方法放到一个 Servlet 里手动触发以后想变成定时任务也只是再加一层调度的事。6.3 保留一个手工核对法先跑 SQL 验算法再改页面最后分享一个我的工作习惯任何计费相关的改动我都不直接改页面而是先用一条 SQL 把期望结果算出来再对照代码结果。这个习惯能省大量定位时间。比如要核验某房间当月的费用先直接查库SELECT r.room_no, w.curr_reading - w.last_reading AS water_usage, (w.curr_reading - w.last_reading) * w.price AS water_fee, e.curr_reading - e.last_reading AS elec_usage, (e.curr_reading - e.last_reading) * e.price AS elec_fee FROM tb_room r LEFT JOIN tb_meter w ON r.room_no w.room_no AND w.meter_type 1 LEFT JOIN tb_meter e ON r.room_no e.room_no AND e.meter_type 2 WHERE r.room_no A-101;把 SQL 算出来的数据跟页面显示的数据一比如果对不上问题就锁定在代码的计算或取值逻辑上而不是 UI 层。如果对得上那再去页面上找展示问题。这个“先数据库、后代码、最后页面”的排查顺序我踩过太多次坑才养成的。手边有说明文档和演示视频固然方便但最终你能不能真正掌握这个项目还是取决于你有没有亲手把一条账单从抄表到缴费的全过程推演过一遍。这类 JSP 项目的价值不在技术新而在于它简单到你能完整看懂一条请求的来龙去脉。把它跑通再按上面的方向改造一遍你对 Java Web 的理解会比刷十篇文章都来得扎实。希望这份笔记能在你动手的路上少碰几个钉子帮到你。本文还有配套的精品资源点击获取