简介:这是一套基于Java、JavaScript与HTML共同实现的水质检测系统完整项目包,面向高校毕业设计、课程设计以及个人项目开发场景,可帮助学习者快速掌握Java Web项目从后端逻辑到前端展示的完整流程。项目涵盖用户管理、水质数据录入与查询、检测结果展示等典型功能,并已通过严格测试,可直接在此基础上二次扩展。包体共含297个文件,压缩包约3.91MB,源码部分以37个Java文件、16个JSP页面和50个JavaScript脚本为主,配套34个CSS样式文件、41个PNG图片及63个XML配置,构建出清晰的前后端界面;同时附带1个SQL数据库脚本、1个properties配置文件和1个Markdown文档,便于直接初始化运行环境与辅助阅读。目前已有50人学习或下载,对于需要完整项目参考的开发者来说,这份资源既能用于理解水质检测业务数据流,也可借鉴前端基于Bootstrap、jQuery UI等成熟组件搭建交互页面的思路,性价比高,适合快速启动课程设计或毕业设计。
1. 水质检测系统的技术栈拆解:java+javaScript+html这套组合到底在做什么
刚接到这类毕设需求时,很多人以为难点在水质检测算法或传感器数据采集,实际上真正卡住人的,是 java、javaScript、html 三条技术线怎么在一个系统里完成数据闭环。水质检测系统本质上是一个典型的全栈 CRUD 项目:java 负责接收检测数据、做超标判定并把结果写进数据库;javaScript 从后端接口取数,在 html 页面上渲染出实时曲线、历史报表和报警列表;数据库则把监测点、检测指标、报警记录全部落盘。对做毕业设计或课程设计的人来说,它最合适的切入点不是研究水质本身,而是把「数据采集 → 存储 → 判定 → 展示」这条链路完整跑通,这也是答辩时最容易讲清楚的价值点。
2. 先立数据库:水质检测系统的五张核心表、字段选型与初始化脚本
2.1 五张核心表怎么设计才不算毕业设计硬凑
水质检测系统里,监测数据本身并不复杂,复杂的是「一个监测点对应多个检测指标」的关联关系。最常见的错误是只建一张大表,把 pH、溶解氧、浊度、温度、电导率全塞进去,字段越加越多,最后每个指标的历史分布根本无法单独统计。我的习惯是拆成五张表,分别是:用户表、监测点表、设备表、水质数据表、报警记录表。
用户表没必要做角色权限那套复杂设计,毕业设计做到登录和角色区分就够了。监测点表保存位置名称、所属区域、经纬度,方便前端做地图点位展示;设备表记录设备的安装位置和使用状态,它和水质数据表通过 device_id 关联;水质数据表是核心,每行记录一条检测结果;报警记录表则是当某个指标超出阈值时由 java 后端写入的数据,前端首页大屏上跳红的那部分数据就从这里查。
字段选型有几个地方容易被忽略。水质数据表中的指标值不要用 float,推荐 DECIMAL(6,2),因为浮点类型在 java 端用 BigDecimal 接收时经常出现精度异常,而 DECIMAL 在 MySQL 里存储的是定点数,页面展示两位小数时不用额外处理。时间字段统一用 DATETIME,不要用 TIMESTAMP,TIMESTAMP 有 2038 年问题,DATETIME 的实际范围更大,而且 TIMESTAMP 会隐式跟随数据库时区转换,排查时容易迷糊。还有一个不是所有教程都会提的点:在 device_id 和 monitor_time 的联合字段上加唯一索引,防止相同的采样时间点重复写入同一条数据,这在后面做数据回放演示时很有用。
2.2 把水质测试数据铺进数据库:初始化SQL脚本与字段参数
表结构定好之后,先别急着写 java 代码,我一般会先把数据库初始化脚本写好并直接执行一遍。脚本的作用不只是建表,还要写入一批能用来演示的测试数据,否则后端接口写完没有数据可查,前端页面永远是空状态,联调就无从下手。
CREATE TABLE monitor_point ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', point_name VARCHAR(64) NOT NULL COMMENT '监测点名称(如:东湖取样点)', area VARCHAR(64) DEFAULT NULL COMMENT '所属区域', longitude DECIMAL(10,6) DEFAULT NULL COMMENT '经度', latitude DECIMAL(10,6) DEFAULT NULL COMMENT '纬度', status TINYINT DEFAULT 1 COMMENT '状态:1启用 0停用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='监测点表'; CREATE TABLE water_quality_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', device_id BIGINT NOT NULL COMMENT '设备ID', monitor_time DATETIME NOT NULL COMMENT '采样时间', ph_value DECIMAL(6,2) DEFAULT NULL COMMENT 'pH值', dissolved_oxygen DECIMAL(6,2) DEFAULT NULL COMMENT '溶解氧 mg/L', turbidity DECIMAL(6,2) DEFAULT NULL COMMENT '浊度 NTU', temperature DECIMAL(6,2) DEFAULT NULL COMMENT '水温 ℃', conductivity DECIMAL(6,2) DEFAULT NULL COMMENT '电导率 uS/cm', UNIQUE KEY uk_device_time (device_id, monitor_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='水质检测数据表'; INSERT INTO monitor_point (point_name, area, longitude, latitude) VALUES ('东湖取样点', '城东片区', 120.152100, 30.256100), ('南河断面', '城南片区', 120.198200, 30.221800), ('第二水厂进水口', '城北片区', 120.169800, 30.284500);这段建表脚本里,唯一索引 uk_device_time 是防止重复插入的关键,如果演示完一轮之后想重新生成数据,只需要调用 INSERT ... ON DUPLICATE KEY UPDATE 就能安全覆盖同一时刻的采样记录。DECIMAL(6,2) 的 6 表示总位数,2 表示小数位,这意味着最大可以存 9999.99,对水质指标来说完全够用,换算成 SQL 里的字段就是整数位不能超过 4 位。
提示:所有字符串字段都按 utf8mb4 建表,不要用 utf8。如果库里已经有旧表,执行 ALTER TABLE table_name DEFAULT CHARACTER SET utf8mb4 补一下,否则插入 emoji 或特殊符号时会报 Incorrect string value 错误。
初始化脚本里还可以加一条内置账号,但用户表的密码字段不要存明文。毕业设计为了好演示,很多人直接存明文,这只是在本地跑无所谓;但如果你是课程设计要上交源码,建议至少用 MD5 或 SHA256 简单处理一下。java 后端校验时把用户输入的密码进行同样的哈希再比对,工作量很小,写在文档里却是个加分项。
3. java后端链路:水质数据接收、超标报警与Maven打包部署
3.1 Spring Boot + MyBatis 的最小水质接收接口
水质检测系统的 java 后端,最常见的组合是 Spring Boot + MyBatis,这也是大多数毕业设计模板的选择。Spring Boot 负责接口暴露与依赖注入,MyBatis 负责 SQL 映射,避免在 Service 层手写大段 JDBC。第一步是把依赖在 pom.xml 里声明好:spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java(版本用 8.x,不要用 5.x,否则后面连接数据库时会遇到驱动类名和时区问题)。配置写在 application.yml 里,数据源参数我用得最多的是下面这一组。
spring: datasource: url: jdbc:mysql://localhost:3306/water_quality?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.waterquality.entityURL 里的 serverTimezone=Asia/Shanghai 是最容易漏的参数。MySQL 8.x 驱动默认要求指定时区,不填启动时会直接报异常“The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone”,这个报错里的乱码是中文时区名在编码转换后出现的,见到它先往时区参数上想。useSSL=false 是因为本地开发环境没有配 SSL 证书,加上它避免控制台刷一堆安全告警。username 和 password 在提交给老师前一定改成你自己本地的值,不要留着什么 root/123456 这种固定组合。
Mapper 接口里我把常用的查询和插入方法列出来:selectRecentRecords 按监测点查询最近 N 条记录、selectByTimeRange 按时间段查询、insertRecord 插入一条检测数据。SQL 写在 Mapper XML 里,用 resultType 映射到实体类。注意实体类属性名和数据库字段名如果采用驼峰命名(比如 monitorTime),在 mybatis 全局配置里要打开 map-underscore-to-camel-case: true,否则 JDBC 查出来的 monitor_time 映射不到 monitorTime 上,页面显示全是 null。
3.2 超标报警判定:阈值放数据库还是写死在Java里
水质检测系统的核心业务逻辑不是增删改查,而是「收到一条数据后判断它是否超标,超标就生成报警记录」。这个判定的阈值放在哪里,很多项目做得比较草率。有直接把阈值写在 Controller 方法里的,也有在 Service 里写一堆 if 硬编码的,这两种做法在你换一个监测点、换一个水质标准的时候,全部要改代码重新打包。我的做法是在数据库里建一张阈值配置表,表里存每个监测点每个指标的上下限,判定逻辑统一从配置表取值。
@Service public class WaterQualityService { private static final BigDecimal PH_LOWER = new BigDecimal("6.50"); private static final BigDecimal PH_UPPER = new BigDecimal("8.50"); public void checkAndAlarm(WaterQualityRecord record) { boolean alarm = false; if (record.getPhValue() != null) { if (record.getPhValue().compareTo(PH_LOWER) < 0 || record.getPhValue().compareTo(PH_UPPER) > 0) { alarm = true; } } // 同样的逻辑判断溶解氧、浊度等指标 if (alarm) { insertAlarmRecord(record, "指标超出阈值"); } } }实际落地时我会把 PH_LOWER、PH_UPPER 这类常量从代码里抽出去,放进 ThresholdConfig 表,让管理员在页面上直接改。这样答辩时展示「把 pH 上限从 8.5 改成 8.0,再插入一条 pH=8.2 的数据,系统立刻生成报警」,效果比写死代码直观得多。判定时用 BigDecimal 的 compareTo 而不是 equals,因为数据库里的 DECIMAL 类型转出来的 BigDecimal 可能带不同的 scale,equals 要求值和精度都相等,compareTo 只看数值大小,用错的话 pH=8.50 和 8.5 会判成不相等,这在 java 后端是常见坑。
插入报警记录的方法要加 @Transactional(rollbackFor = Exception.class)。原因是报警记录和监测数据写入是同一个事务里的两步操作,如果插入报警记录时抛了异常,而前面那步数据已经提交,就会出现数据表里有超标记录但报警列表里查不到的怪象。这个注解加上后,任何一步失败都会回滚整个事务,保证两边数据一致。
3.3 Maven打包与Tomcat部署的参数调整
后端本地跑通之后,最后的部署环节一般是两种:一种是 Spring Boot 内置 Tomcat 直接 java -jar 运行;另一种是打包成 WAR 丢到外置 Tomcat 的 webapps 下。毕业设计演示我建议用第一种,简单省事,但做课程设计老师要求必须放 Tomcat 里另说。
Maven 打包前要检查 pom.xml 的 packaging 标签。默认是 jar,如果要打 WAR,需要把 packaging 改成 war,同时加一段 spring-boot-starter-tomcat 的 provided 依赖。很多人在这一步习惯性漏掉,结果打出来的 WAR 在外置 Tomcat 里起不来。打包命令是 mvn clean package -DskipTests,不要带 test 参数跑一遍全部单元测试,测试代码如果没有配好测试数据源,打包时会被卡在数据库连接上。
启动时如果 8080 端口被别的东西占了,用 java -jar xxx.jar --server.port=8081 换个端口,不要跑去改代码里的 server.port 再重新打包,省一次打包时间。端口改完后前端接口地址要同步改,否则前端请求发到 8080 端口会连接失败,这个问题排查看起来像后端挂了,实际上没到后端。
4. javaScript与html前端:监测大屏、实时曲线和历史查询的落地写法
4.1 原生HTML + javaScript + Canvas 画实时水质曲线
水质检测系统的前端部分,很多教程一上来就让你引入 ECharts,用它画图确实省事,但有个实际问题:答辩提问环节老师很可能会问「你能讲讲曲线是怎么画出来的吗?」,如果只会配 ECharts 的 option 参数,说不清楚内部逻辑,印象分会打折。我的思路是核心的实时曲线用原生 canvas 画,其他辅助图表再考虑用类库,这样既能讲清楚绘图原理,交付时也不显得太过于简单。
实时曲线的基本思路是:用 requestAnimationFrame 循环或者 setInterval 定时器从后端取最近 N 条数据,把时间戳映射成 canvas 的横坐标,把指标值映射成纵坐标,然后用 lineTo 连接各个点。纵坐标的范围不要写死,要根据当前数据集里的最大值和最小值动态计算,否则 pH 值在 6.5 到 8.5 之间波动时,如果纵轴范围固定成 0 到 100,画出来的曲线永远是一条贴在底部的直线,看上去像系统坏了。
async function loadChartData(pointId) { const resp = await fetch(`/api/water/records?pointId=${pointId}&limit=30`); const list = await resp.json(); if (!list || list.length === 0) return; const canvas = document.getElementById('phChart'); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = '#2d8cf0'; ctx.lineWidth = 2; ctx.beginPath(); const chartW = canvas.width - 40; const chartH = canvas.height - 40; const maxVal = list.reduce((m, r) => Math.max(m, r.phValue), 0); const minVal = list.reduce((m, r) => Math.min(m, r.phValue), 0); const gap = Math.max((maxVal - minVal) * 0.2, 0.5); list.forEach((r, i) => { const x = 30 + (i * chartW / (list.length - 1)); const y = 30 + chartH - ((r.phValue - minVal + gap) / (maxVal - minVal + 2 * gap) * chartH); i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); }); ctx.stroke(); document.getElementById('updateTime').innerText = `最近采样时间:${list[list.length - 1].monitorTime}`; } setInterval(() => loadChartData(currentPointId), 5000);这段代码里,fetch 请求返回的 JSON 数组会先计算出当前数据集的上下界,再给上下界各加 20% 的留白(gap),这样曲线不管数值怎么波动,都不至于顶到画布边缘甚至画到可见区域外。给 maxVal - minVal 乘 0.2 是本项目里一个适合大多数水质指标的默认参数,如果你检测的是氨氮这类本身波动就大的指标,可以把 0.2 提到 0.3,让曲线在视觉上更舒展。注意避免除以零的边界情况,如果所有数据点的值完全相同,就必须走 gap 兜底分支。
4.2 Fetch调用后端接口:跨域、中文编码与刷新频率
前端页面放在和 Spring Boot 同一个端口下时,直接请求相对路径 /api/water/records 就行,不存在跨域。但如果你从磁盘上用 file:// 方式直接打开 html,或者把前端单独起在 8081 端口,后端在 8080,就会触发跨域问题。浏览器控制台会看到类似 “CORS policy: No 'Access-Control-Allow-Origin' header” 的报错,这是浏览器安全策略在拦截,不是接口本身挂了。
处理跨域有两个做法,选补一个简单改动方案就行。后端 Controller 上加 @CrossOrigin 注解,或者写一个全局 WebMvcConfigurer 配置 addCorsMappings,允许 /api/** 路径来自任意来源。方案一代码侵入少,方案二更规整,不会逼着每个 Controller 都写一行重复注解。国内环境里用 8080/8081 的本地组合做演示已经够多,也不需要展开更复杂的安全配置。
中文编码问题会和跨域问题同时出现。fetch 拿到 JSON 后,如果接口返回的中文监测点名称在页面上显示成乱码,后端没有设置 produces = "application/json;charset=UTF-8" 是主要原因。Spring Boot 里 @RequestMapping 一般不写 produces 也能正确返回 UTF-8,但如果你用的是外置 Tomcat,就需要检查 Tomcat 的 server.xml 里 Connector 的 URIEncoding 是否设为 UTF-8,以及数据库连接 URL 里的 characterEncoding=utf8 是否到位。这三处只要有一处是默认配置,中文就可能从数据库一路乱到页面。
刷新频率上,setInterval(5000) 是适合课堂演示的值,不会让数据库在演示期间疯狂写入,也不会让曲线看起来迟钝。如果你要展示「实时性」,可以把间隔调成 2000,但要注意数据库在同一联调环境里每 2 秒插一条数据时,一天的记录量就是 43200 条,开发库无所谓,传到云端演示时会拉高存储成本。另外 setInterval 有个弱点:如果网络慢,上一次请求还没返回下一次请求就发出去了,响应乱序之后曲线会来回跳。稳妥的做法是改用 setTimeout 在每次请求完成后重新计时。这不是必须改的,但在演示现场网络不稳时非常值得改成后者。
5. 水质检测系统避坑指南:数据库连接、乱码、跨域等5个典型问题
5.1 数据库连接失败:驱动版本与时区参数不一致
现象:Spring Boot 启动日志报错 “The server time zone value '...' is unrecognized”,或者 “Public Key Retrieval is not allowed”。原因:MySQL 8.x 驱动对时区校验更严格,且默认连接不开放公钥检索。解决:在 jdbc 连接 URL 末尾追加 serverTimezone=Asia/Shanghai 和 allowPublicKeyRetrieval=true 两个参数;驱动依赖确认是 mysql-connector-java 8.0.x 而不要用旧版 5.1.x,旧版驱动连新版 MySQL 会直接报 com.mysql.jdbc.CommunicationsException。这个参数组合也是我在多个项目里反复用到的一组通用配置,基本上只要是 MySQL 5.7 以上版本,加这两个参数之后没有连不上的。
5.2 前端中文乱码:从Tomcat到JDBC的全链路排查
现象:监测点名称、报警说明在页面上显示为「???」或一堆乱码。原因:这套系统里中文要经过数据库连接、后端接口响应、浏览器渲染三个环节,任何一处默认编码不是 UTF-8 都会断链。解决:按顺序排查 MySQL 连接 URL 是否带 characterEncoding=utf8、后端响应是否使用 UTF-8、html 页面 head 里是否有 。这三步都对了,乱码基本不存在。如果页面是用 JSP 做的,还要额外检查 <%@ page pageEncoding="UTF-8" %> 这行有没有写。
5.3 跨域请求被拦截:开发环境与打包部署后的两套处理
现象:前端页面打开后能正常渲染,但一调 /api/water/records 就报 CORS 错误,数据区空白。原因:前端页面源与后端端口不一致。解决:开发模式下给后端加 WebMvcConfigurer 的 CORS 映射,允许 http://localhost:8081 源访问 /api/**;打包部署后如果前后端在同一 Tomcat 下同端口,跨域配置不需要保留。这个过程中最坑的是加完 CORS 配置还是报错,浏览器有缓存,强制刷新 Ctrl+Shift+R 再试。另一个容易忽略的情况:HTML 文件被编辑器插件或浏览器安全策略限制出现 file:// 协议下的跨域,要先起一个本地静态服务器(python -m http.server 这种)再调试,不要直接在浏览器里双击打开。
5.4 日期时间排序错乱:LocalDateTime与DATETIME的精度陷阱
现象:历史记录查询按时间排序,结果最新的数据反而排到后面,或者同一秒的数据顺序不稳定。原因:java 实体里用 LocalDateTime,MySQL 用 DATETIME,前端把字符串显示出来后按字符串排序,这种排出来的结果和真实时间顺序不一致。解决:第一次查询就在 SQL 里按 monitor_time DESC 排好序,不要在 javaScript 里用 sort 做二次排序;如果前端一定要处理,把时间字符串转成 Date 对象或时间戳再比大小。日期时间这个坑比数据库字段不符更隐蔽,因为系统不报错,只是数据看起来不对劲。
5.5 打包后接口404与静态资源路径对不上
现象:java -jar 启动后页面能打开,但所有接口请求都是 404;或者页面图标、js、css 找不到。原因:打包时静态资源没有进入 jar 包,或者接口路径与前端请求路径不一致,classpath:/static 下的资源目录结构写错。解决:检查 Spring Boot 项目里 src/main/resources/static 目录是否存在,前端文件是否全部放进去,html 里引用的 js/css 用相对路径而不是绝对路径(比如 /js/app.js 如果部署在根路径会失效,写成 ./js/app.js 更稳)。接口 404 时,优先看后端 Controller 的 @RequestMapping 和前端 fetch 的 URL 是否完全一致,注意大小写和有没有多写一层项目名。这个问题排查起来眼睛会花,最快的办法是打开浏览器开发者工具看网络请求的实际 URL,一眼就能看出偏差。
6. 答辩演示前的一小时:造数脚本、手动触发报警与调试顺序
越到演示节点越要靠确定性的数据来兜底。系统已经能跑起来后,不要依赖人工在页面上一点一点加数据,写一个造数脚本批量生成过去 7 天的水质记录,每天的采样间隔 10 分钟,数量够画曲线、分页也不卡。关键的一步是让数据中混入几条超标记录,比如把凌晨某几个点的 pH 值拉低到 5.8,让报警记录自动生成,演示时直接切到报警页面就有红字可看。
把报警阈值从阈值配置表里临时调小也是一种模拟超标的手段,调完不用重启,页面刷新后新告警就能正常触发。但要记住演示结束前把阈值调回真实标准值,不然数据库里留下全是不合理阈值配置痕迹,交付源码给老师时会被追问设计依据。我自己的做法是演示前一晚完整跑一遍操作流程:登录 → 打开监测大屏 → 刷新曲线列表 → 查看报警记录 → 修改一次阈值 → 看到新的报警产生,整条链路不超过十分钟,把这一步排进调试顺序里能挡掉大多数现场翻车。
这套系统里最容易被低估的是测试数据的真实感。直接跑 SQL 脚本用 random 函数生成数值,得到的曲线是一根剧烈抖动的毛刺线,视觉上像机器坏了。生成数据时给每个指标各自生成的曲线加一个正弦趋势,加上小幅随机噪声,如下图所示的效果才像真实检测数据。这种造数里写的参数(振幅、周期、噪声范围)因人而异,但每类指标要单独配——pH 慢变,浊度快变,这样演示时几条曲线在页面上有明显的形态差异,比清一色的直线有说服力。
我希望你把「先数据库,再后端,再前端」的顺序作为自己的主思路,因为我见过不下十个项目先写前端页面再倒回去建表,最后前端要什么字段后端表里没有,强行拼接后接口返回的数据画不出图,又回头改表、改 dao、改 sql,前后折腾三周。先跑通一条最小链路再逐步往上加东西,比一次性把所有功能做完再调试要顺得多。希望帮到你。
本文还有配套的精品资源,点击获取