简介:面向高校计算机、电子信息工程、数学专业学生的智能水表系统课程设计与毕业设计参考项目,资源包含完整工程源码,覆盖从传感器数据采集、无线通信到Web管理界面的实现链路,适合需要完成相关课题设计或学习物联网系统开发的学生参考。压缩包共80个文件,以Java源码、JSP页面和Class文件为主,辅以JS/CSS前端资源、XML配置及字体文件等,整体约532KB,目录结构清晰,便于按模块阅读和复用。目前已有68人学习使用。通过该源码,可掌握智能水表远程自动抄表、流量数据采集与上传、后台数据展示等核心环节的编码实现,理解GPRS、NB-IoT等通信方式的选型逻辑,同时为异常检测、水费管理等扩展功能开发提供可复用的基础框架。项目代码与配置完整,适合作为课题设计、毕业论文撰写的技术参考。
1. 智能水表系统毕设资源:先搞清楚它到底是个什么项目
不少同学拿到"智能水表系统.zip"第一反应是打开来找硬件电路图,结果发现里面是一堆.java、.jsp、pom.xml,心里咯噔一下:这跟我以为的智能水表不一样。其实这套资源里的智能水表系统,落点是一个 Java Web 管理平台——硬件端的抄表数据通过无线网络传到服务器之后,由这套系统负责接收、存储、展示和统计,也就是整个智能抄表架构里的"软件大脑"。它的核心价值不是教你怎么焊电路板,而是让你把"远程抄表 + 水务管理"这条完整业务链在电脑上跑通,从登录、数据看板到测试模块都齐活。适合计算机、电子信息工程、物联网专业拿来当课设或毕设底座,替换成自己的数据库表名和前端样式,就是一份能答辩、能演示、能讲清楚原理的作品。
2. 架构与抄表链路:从传感器到 JSP 页面的数据流向
2.1 一个完整智能水表系统的三段式结构
智能水表系统从来不是单机软件,它至少分成三层:感知层、传输层、平台层。感知层是水表终端,里头有流量传感器(常见是霍尔元件或干簧管),水流推动叶轮转动时产生脉冲信号,微控制器(STM32、51 系列都有人用)负责统计脉冲次数换算成流量,再把数据暂存在本地。传输层负责把数据送出去,这也是毕设答辩时老师最爱问的点——你用的是什么通信方案,为什么不用别的。平台层就是本项目——Java Web 后台,负责接收、解析、存储、展示数据,还要支撑管理员登录、用户管理、账单统计这些业务动作。
这套资源结构里能看到典型的 Java Web 工程痕迹:src/main/java存放后台逻辑,webapp下首页是index.jsp,登录页是login.jsp,另外还有一个TestCommunity.jsp。也就是说,它虽然是"智能水表系统",但交付的是平台层代码,感知层和传输层以业务背景和数据模拟的方式存在,这一点先想清楚,后面很多疑惑就解开了。
2.2 传输层选型:四种通信方案怎么选
传输层的通信协议选择直接决定了系统的覆盖范围、功耗和运营成本,这个知识点在论文里通常要占一节。梳理下来,课设阶段最常被比较的四类方案是这样:
| 通信方式 | 覆盖范围 | 典型速率 | 功耗 | 适用场景 |
|---|---|---|---|---|
| GPRS/4G | 广,依托运营商基站 | 高 | 高 | 城区分散表计、实时性要求高的场景 |
| LoRa | 中远,需要自建网关 | 低 | 低 | 小区/园区集中部署,网关覆盖 |
| NB-IoT | 广,蜂窝网络 | 中 | 低 | 目前智能抄表的主流,海量连接 |
| 有线 RS-485 | 短,需布线 | 中 | 低 | 楼栋内部的集中器到主站 |
如果是毕设论文,我一般建议你选 NB-IoT 作为"推荐方案",理由很充分:覆盖广、穿透强、单表功耗低,而且运营商在推,跟你"未来智慧城市"的展望能自然衔接。但如果只是课设演示,实际操作中完全可以把传输层抽象成一个 HTTP POST 请求,模拟水表终端往后台塞数据,这也是第 6 章会做的事。论文里写 NB-IoT,代码里用模拟请求,不冲突,反而是完整闭环。
2.3 平台层内部的分层结构与对应文件
回到这套源码本身,把它拆开看,平台层内部走的是很标准的 Web 分层:
表现层是 JSP 页面,webapp/login.jsp负责登录入口,webapp/index.jsp是登录成功后的主工作台,TestCommunity.jsp看起来是独立测试或社区展示页。控制层在src/main/java里用 Servlet 实现,负责接收 HTTP 请求、调用业务层、决定跳转哪个页面。业务层封装抄表记录的增删改查逻辑。数据访问层用 JDBC 或 MyBatis 操作 MySQL。最外面的pom.xml声明了所有依赖,.classpath、.settings、.project这些是 Eclipse 的工程配置,说明原项目是在 Eclipse 里开发的 Maven 工程,导入时最好也用 Eclipse 或 IDEA 的 Maven 导入方式。
需要特别注意的是,src/main/webapp/WEB-INF目录下面一般放着web.xml和 JSP 的配置文件,resources/META-INF里可能放了数据库配置或上下文配置。拿到项目后第一步不是急着打开 JSP 改页面,而是先把pom.xml里的依赖列表看一遍,确认里面有没有数据库驱动、JSTL 标签库、Servlet API,因为这几个是后面最容易翻车的点。
2.4 数据链路对照:一帧抄表数据从哪来到哪去
把这套系统的数据流串一遍,大致是这样:水表终端的传感器产生脉冲,微控制器换算成用水量并按设定周期打包,通过 NB-IoT 模块以 CoAP/UDP 方式发到运营商平台,运营商平台再通过 HTTP 回调把 JSON 数据推送到你的后台接口。后台 Servlet 收到 JSON 后解析出表号、读数、时间戳,插入数据库的抄表记录表,然后index.jsp从数据库查出来渲染成表格或图表。
如果你答辩时被问到"系统延迟怎么办",可以回答:实时性要求不高的场景,抄表数据每小时上报一次就够,后台用定时任务批量处理;如果要求实时告警,后台接口得支持并发接收,数据库连接也要换成连接池。这套课设代码里未必实现了连接池,但你把这个思路讲出来,老师会觉得你考虑过生产环境的问题。
3. 登录模块落地:从 login.jsp 到数据库权限校验的完整实现
3.1 登录页的表单结构与提交方式
打开login.jsp,里面核心是一个表单,用户名密码通过 POST 提交到LoginServlet。常见的写法是这样:
<form action="${pageContext.request.contextPath}/login" method="post"> <input type="text" name="username" placeholder="请输入用户名" /> <input type="password" name="password" placeholder="请输入密码" /> <button type="submit">登 录</button> </form>这段代码里最关键的是${pageContext.request.contextPath},它动态获取项目部署路径,避免了上下文路径写死导致请求 404 的问题。很多同学在本地跑通、换一台电脑部署就登录失败,往往是这里写死了路径。action 指向的/login要和web.xml里 Servlet 的<url-pattern>完全匹配,大小写也得一致。
3.2 LoginServlet:用 PreparedStatement 挡住 SQL 注入
后台接收登录请求的 Servlet,核心逻辑是查库比对。常见的课设写法是这样:
@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); try (Connection conn = DBUtil.getConnection()) { String sql = "SELECT id, username, role FROM sys_user WHERE username=? AND password=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs = ps.executeQuery(); if (rs.next()) { HttpSession session = request.getSession(); session.setAttribute("userId", rs.getInt("id")); session.setAttribute("role", rs.getString("role")); response.sendRedirect("index.jsp"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } } catch (Exception e) { e.printStackTrace(); } } }这里有两个细节我想强调。第一,密码比对直接在 SQL 里用WHERE username=? AND password=?完成,这种写法胜在简单,课设够用,但严格来说生产环境肯定要存哈希值。答辩时如果老师问密码安全,你就说"当前为了演示采用明文比对,生产环境会换成 BCrypt 加密存储",这一句话就能体现你懂安全边界。第二,用了PreparedStatement而不是拼接字符串,这是刻意为之——?占位符让数据库引擎把参数当成值而不是 SQL 片段,能有效挡住' or '1'='1这类注入。
3.3 DBUtil:连接参数集中管理,别把密码写死在 JSP 里
登录必须要有一个数据库连接工具类,常见做法是写一个DBUtil,把驱动加载、连接获取、资源关闭都封装起来:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/smart_meter?useUnicode=true&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; 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); } }URL 里那串参数值得注意:useUnicode=true&characterEncoding=utf8是解决中文乱码的第一道关卡。如果不加,数据库连接默认用的字符集可能和表结构不一致,读出来的用户名变成问号,登录永远失败。驱动类名有两个版本,MySQL 5.x 用com.mysql.jdbc.Driver,MySQL 8.x 用com.mysql.cj.jdbc.Driver,下载依赖时看清楚 pom.xml 里写的版本,否则会报 ClassNotFoundException。
3.4 登录成功后的 Session 处理和页面跳转
登录成功不能只是跳个页面,至少要做三件事:把用户标识写进 Session、记住用户角色、跳转到主页面。上面代码里session.setAttribute("userId", ...)和setAttribute("role", ...)就是干这个的。后续index.jsp里可以用${sessionScope.role}判断显示哪些菜单,比如管理员能看到"用户管理",普通用户只看到自己的水表读数。
跳转用sendRedirect还是forward也讲究。登录成功用sendRedirect,让浏览器地址栏变成index.jsp,用户按 F5 刷新不会重复提交表单;登录失败用forward回到login.jsp,好处是request域里的errorMsg还能带到页面上显示。如果用sendRedirect带错误消息,就得拼 URL 参数,比较麻烦。这一点的取舍,面试和答辩都有人问。
4. 数据看板与查询:index.jsp 怎么把抄表数据用起来
4.1 抄表数据表设计:先搞清楚你要展示什么
index.jsp是登录后的主工作台,它的数据来源是抄表记录表。课设里这张表的设计直接决定后面查询好不好写。我见过很多项目把用户信息和抄表数据揉在一张表里,结果想按小区统计水量时 SQL 写得非常痛苦。建议至少拆成两张表:sys_user管用户和角色,meter_data管抄表记录。meter_data的常见字段是这样:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键自增 |
| meter_no | VARCHAR(20) | 水表编号,每块表唯一 |
| user_name | VARCHAR(50) | 用户户名 |
| reading | DECIMAL(10,2) | 当前累计读数(吨) |
| flow | DECIMAL(10,2) | 本次周期用水量 |
| status | TINYINT | 0 正常,1 异常(疑似泄漏) |
| collect_time | DATETIME | 抄表时间 |
reading存累计值而不是单次用量,这个和实际智能水表的工作方式一致——表端只记录累计读数,后台用两次读数的差值算周期用水量。你也可以在插入时直接算好flow存进去,省得查询时再做减法。
4.2 DAO 层:按时间范围查抄表记录的典型写法
查询逻辑集中在 DAO 里,一个常见需求是查最近 N 天的抄表记录,并按时间倒序显示在首页表格里:
public List<MeterData> findRecentRecords(int limit) throws SQLException { List<MeterData> list = new ArrayList<>(); String sql = "SELECT meter_no, user_name, reading, flow, status, collect_time " + "FROM meter_data ORDER BY collect_time DESC LIMIT ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, limit); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { MeterData d = new MeterData(); d.setMeterNo(rs.getString("meter_no")); d.setUserName(rs.getString("user_name")); d.setReading(rs.getBigDecimal("reading")); d.setFlow(rs.getBigDecimal("flow")); d.setStatus(rs.getInt("status")); d.setCollectTime(rs.getTimestamp("collect_time")); list.add(d); } } } return list; }这里LIMIT ?的参数也是用setInt传入的,和登录模块的PreparedStatement是同一个套路。要注意 MySQL 的LIMIT参数不能用字符串拼接,有些版本直接拼数字没问题,但统一用占位符最安全。ORDER BY collect_time DESC倒序排列,配合LIMIT就能实现"首页只显示最近 20 条",数据量大了也不会让页面卡死。
4.3 index.jsp 用 JSTL 循环渲染表格
DAO 查出来的数据最终要展示在 JSP 上。Servlet 把 List 塞进 request 域之后,JSP 里用 JSTL 的c:forEach循环输出。常见写法如下:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table> <tr> <th>水表编号</th><th>户名</th><th>累计读数</th> <th>周期用量</th><th>状态</th><th>抄表时间</th> </tr> <c:forEach items="${recentRecords}" var="record"> <tr> <td>${record.meterNo}</td> <td>${record.userName}</td> <td>${record.reading}</td> <td>${record.flow}</td> <td> <c:choose> <c:when test="${record.status == 1}"> <span style="color:red;">异常</span> </c:when> <c:otherwise>正常</c:otherwise> </c:choose> </td> <td>${record.collectTime}</td> </tr> </c:forEach> </table>这一段的items="${recentRecords}"对应 Servlet 里request.setAttribute("recentRecords", list)的名字,必须完全一致。${record.meterNo}用到的是 EL 表达式,它默认调用record对象的 getter 方法,所以实体类MeterData里的属性名和 getter 要符合 JavaBean 规范——属性meterNo对应getMeterNo(),否则页面上就显示空。很多同学改字段名时只改了数据库和实体类,忘了改 getter,结果页面一片空白,这个问题在第 5 章还会再提。
4.4 再加一个趋势查询:按天统计用水量
如果想让首页更丰满一点,可以加一个近 7 天用水量趋势统计,这个功能特别适合答辩演示,因为能直观展示"系统不只是展示明细,还能做数据分析"。SQL 按天分组汇总:
SELECT DATE_FORMAT(collect_time, '%Y-%m-%d') AS day, SUM(flow) AS total_flow FROM meter_data WHERE collect_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE_FORMAT(collect_time, '%Y-%m-%d') ORDER BY day这段 SQL 的逻辑是先用DATE_SUB把时间范围卡在最近 7 天,再用DATE_FORMAT把时间戳转成天并以天分组,最后SUM(flow)汇总出每天的用水总量。%Y-%m-%d的格式串里%Y是四位年份,%m是两位月份,%d是两位日期,大小写不能写反。这个查询结果可以在 JSP 里渲染成柱状图(用隐藏 canvas 或直接输出到表格里都行),或者更简单点,直接放在一个"近 7 日用水"表格里展示,同样用c:forEach循环即可。
5. 避坑指南:JSP+Maven 项目导入、依赖与中文乱码排查
5.1 Eclipse 导入后 JSP 全部报错:少配置了 Tomcat 运行时
- 现象:把
.zip解压后用 Eclipse 的 Import → Existing Maven Projects 导入,src/main/java里的类和webapp下的 JSP 大片标红,HttpServlet、HttpServletRequest全部找不到。 - 原因:项目本来是动态 Web 工程,
Servlet相关 API 由 Tomcat 提供,Maven 的pom.xml里如果用的是<scope>provided</scope>(意思是运行时由容器提供),那么 IDE 里必须手动指定一个 Tomcat 运行时,否则编译环境里根本没有这些类。 - 解决:在 Eclipse 里 Window → Preferences → Server → Runtime Environments 里 Add 一个 Tomcat(版本跟项目匹配,常见是 Tomcat 8 或 9),然后在项目上右键 Properties → Targeted Runtimes,勾上这个 Tomcat,重新 Maven → Update Project,红色报错就会消失。如果在 IDEA 里,确认 Project Structure → SDK 选对 JDK,并且 Facets 里 Web 模块的 Deployment Descriptor 路径指向
src/main/webapp/WEB-INF/web.xml即可。
5.2 运行时报 ClassNotFoundException: com.mysql.cj.jdbc.Driver
- 现象:启动 Tomcat 后访问
login.jsp,点登录按钮,后台报ClassNotFoundException: com.mysql.cj.jdbc.Driver,或者No suitable driver found。 - 原因:数据库驱动 Jar 包只在编译期存在,运行期没有同步到 Tomcat 的 WEB-INF/lib 下。用 Eclipse 直接 Run on Server 时,如果 Maven 依赖没有 deploy 到 webapp,就会出现这种情况。另外 MySQL 8.x 和 5.x 的驱动类名不一样,也可能是因为
DBUtil里写错了驱动类。 - 解决:先确认 pom.xml 里有没有 mysql-connector-java 依赖,然后把 Maven 工程 clean 后再 package,看
target目录下最终生成的 war 里WEB-INF/lib有没有驱动 Jar。如果 IDE 直接运行,可以在项目的 Deployment Assembly 里把 Maven Dependencies 加进去。我一般还会建议直接把 Tomcat 的运行方式改成"使用本地安装的 Tomcat"而不是"使用工作空间元数据",能少踩很多依赖不同步的坑。
5.3 数据库数据变成问号:三层字符集至少统一两处
- 现象:登录时输入的密码含中文,后台查不到用户;或者往
meter_data表插入的户名在 MySQL 客户端看是正常的,但 JSP 页面上显示问号。 - 原因:典型的字符集链路断裂。JSP 文件本身的编码、
request.setCharacterEncoding、JDBC URL 的characterEncoding、MySQL 表结构的字符集,这四个环节只要有任何一个不是 UTF-8,中文就会出乱子。最常见的是 JSP 页面没有声明<%@ page contentType="text/html;charset=UTF-8" %>,Tomcat 按 ISO-8859-1 解析了请求参数。 - 解决:我拿到这类项目第一件事就是全局搜索
charset,确保每个 JSP 头部都有 UTF-8 声明;LoginServlet的doPost第一行写上request.setCharacterEncoding("UTF-8");JDBC URL 带上useUnicode=true&characterEncoding=utf8;最后 MySQL 建库时显式写CREATE DATABASE smart_meter DEFAULT CHARACTER SET utf8mb4。这条链路全走通,乱码基本绝迹。
5.4 8080 端口被占用,Tomcat 起不来
- 现象:启动 Tomcat 时控制台报
Port 8080 required by Tomcat v9.0 Server at localhost is already in use。 - 原因:本机有其他进程占用了 8080 端口,常见的是之前启动过的 Tomcat 实例没被杀掉,或者别的开发工具(如某个前端调试服务)占了端口。这不是项目代码问题,但没处理过的新手很容易卡在这一步。
- 解决:Windows 下用
netstat -ano | findstr 8080找到 PID,再到任务管理器结束对应进程;也可以改 Tomcat 的端口。如果只是想快速跑通项目,改端口最省事——在path-to-tomcat/conf/server.xml里把<Connector port="8080"改成 8081。但注意,如果项目代码里有写死的路径或 Ajax 请求地址指向http://localhost:8080/xxx,改端口后这些请求也会受牵连,所以我更推荐直接杀进程。
5.5 TestCommunity.jsp 单独访问 404:项目上下文路径没弄清楚
- 现象:Tomcat 启动成功,
login.jsp能访问,但直接访问http://localhost:8080/TestCommunity.jsp时返回 404,而在 Eclipse 里明明能看到这个文件。 - 原因:Web 应用的访问路径是
http://localhost:8080/项目上下文路径/资源路径,而不是http://localhost:8080/资源路径。Eclipse 里工程的上下文路径默认是工程名,如果你在浏览器直接敲 JSP 文件名,少了工程名这一段,Tomcat 自然找不到。 - 解决:先看 Tomcat Server 配置里 Modules 那一栏显示的 Path 是什么,假设是
/smart-meter,那么访问首页应该是http://localhost:8080/smart-meter/index.jsp。为了避免手写路径出错,代码里尽量用${pageContext.request.contextPath}拼绝对路径,上面登录表单的例子就是这么处理的。如果你确认路径没问题还是 404,再检查TestCommunity.jsp是否放在了src/main/webapp根目录下,而不是误放进WEB-INF里——WEB-INF下面的资源是禁止直接通过 URL 访问的。
6. 进阶验证:用一条模拟数据打通信道到展示的全链路
代码跑通和逻辑跑通是两回事。这里我给你一个验证套路:在本地往meter_data插入一条模拟抄表数据,然后操作前端页面确认这个数据能显示出来。这个过程看起来简单,实际上把数据库、DAO、Servlet、JSP 整条链路都验证了一遍,比对着页面空谈功能扎实得多。
先写一个模拟数据写入的 Servlet,访问它就往库里塞一条记录。简单实现如下:
@WebServlet("/simulate") public class SimulateDataServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String meterNo = "MTR-1001"; BigDecimal flow = new BigDecimal(Math.round(Math.random() * 50 * 10) / 10.0); String sql = "INSERT INTO meter_data (meter_no, user_name, reading, flow, status, collect_time) " + "VALUES (?, ?, ?, ?, 0, NOW())"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, meterNo); ps.setString(2, "测试住户"); ps.setBigDecimal(4, flow); ps.executeUpdate(); response.getWriter().write("ok"); } catch (Exception e) { e.printStackTrace(); response.getWriter().write("error"); } } }这里Math.round(Math.random() * 50 * 10) / 10.0是生成一位小数的随机用水量,范围控制在 0.0 到 50.0 吨之间。status直接写 0 表示正常。访问一次http://localhost:8080/项目路径/simulate,浏览器显示 ok,然后打开index.jsp,最新的记录应该出现在列表第一行。
你可能会问,为什么要把reading留空?实际上你插入数据时应该补上reading——它表示当前累计读数,我这里是为了演示批量插入时的字段取舍。更严谨的写法是把当前表里该水表最近一条reading查出来,加上flow得到新的累计值。你可以自己改造成"读取上一条 → 累加 → 插入"三步,这其实就是远程抄表后台真实的数据处理逻辑:表端上报的是累计读数,后台计算增量。
从那以后,我每次拿到这类毕设项目,不管什么技术栈,都强制走一遍"模拟数据全链路验证":写一条测试数据、跑一次查询、看一次页面渲染、查一次数据库结果。四步走完,项目能不能用、哪个环节断了,心里一清二楚。做课设最怕的不是代码写不出来,而是数据从哪来、往哪去、页面凭什么显示,这三件事你亲手验证过一遍,答辩时随便老师怎么问都接得住。希望这套资源能帮你把智能水表系统从"能跑"做到"能讲透",少走点弯路。
本文还有配套的精品资源,点击获取