
简介这是一套基于Java后端、JavaScript前端与HTML页面构建的水质检测系统完整开发资源面向计算机专业本科生及初级开发者适用于毕业设计、课程设计与小型项目实践聚焦环境监测类Web应用的全流程实现。资源包共297个文件涵盖37个核心Java业务逻辑与Servlet类、50个JavaScript交互脚本、34个CSS样式文件、63个XML配置含Spring与MyBatis配置、41个PNG/GIF/JPG图标与界面素材以及1个SQL数据库脚本和16个JSP页面模板整体压缩包仅3.91MB轻量易部署。内容预览显示已集成Bootstrap、jQuery UI、Font Awesome、ElFinder等主流前端组件具备响应式界面与文件管理能力。已有50人学习下载提供可直接运行的全栈源码、结构清晰的MVC分层目录、完整数据库表结构与初始化数据支持快速二次开发与功能扩展是入门级Web项目实战的优质参考范例。1. 水质检测系统不是“网页Java”拼凑出来的玩具它得真能读传感器、存数据、画趋势图、发告警毕业答辩时老师一连问三个“实时性怎么保证”你得答得上来这个标题里藏着一个被严重低估的工程现实用 Java JavaScript HTML 做水质检测系统本质是构建一个轻量级工业监控前端后端闭环——不是做个带表格的静态页面也不是写个能增删改查的 Spring Boot Demo。它要对接真实传感器比如 RS485 的 pH/DO/浊度模块、要扛住每 10 秒一批数据的持续写入、要在浏览器里秒级刷新折线图、要在超标时弹窗邮件双通道告警、还要让导师在本地双击start.bat就跑起来。我带过 17 届到 24 届共 32 个本科生做这类毕设翻车率超 60%核心问题全出在「技术栈选型没对齐真实需求」有人用纯 JSP 渲染页面结果传感器数据一来就卡死有人把所有逻辑塞进前端 JS数据库连接池爆满还有人用 H2 内存库演示答辩时换 MySQL 就报Unknown system variable sql_mode。这篇笔记不讲概念只拆解从传感器串口读数 → Java 后端解析入库 → WebSocket 推送 → ECharts 动态绘图 → 超标规则引擎触发告警这条链路上每个环节必须亲手调通的命令、参数、配置和血泪坑。适合正在赶毕设 deadline、手头只有 Arduino 模拟器或 USB 转 TTL 传感器模块、需要今天就能跑通 demo 的同学。2. 后端用 Spring Boot 3.x MyBatis-Plus 搭建高吞吐水质数据接收服务拒绝 Tomcat 默认线程池吃掉你的传感器心跳水质数据不是用户点击产生的请求它是按固定周期常见 5s/10s/30s涌进来的字节流。如果用传统 HTTP POST 每次提交一条记录10 个传感器 × 每秒 2 条 20 QPSTomcat 默认 200 线程池瞬间打满。真实项目必须走长连接 批量写入。我们用 Spring Boot 3.2.3JDK 17 Netty 封装串口监听再通过 REST API 提供标准化数据入口。2.1 串口通信层用 jSerialComm 替代 RXTX避开 Windows 10/11 驱动签名玄学RXTX 在 Win10 之后需要手动禁用驱动签名强制学生机基本搞不定。jSerialComm 是纯 Java 实现无需 DLL直接 Maven 引入!-- pom.xml -- dependency groupIdcom.fazecast/groupId artifactIdjSerialComm/artifactId version2.10.4/version /dependency关键代码不是“打开串口读字符串”而是带校验、带缓冲、带重连的健壮读取// SerialDataReceiver.java public class SerialDataReceiver { private SerialPort serialPort; private final ScheduledExecutorService scheduler Executors.newSingleThreadScheduledExecutor(); public void connect(String portName) { try { serialPort SerialPort.getCommPort(portName); // 如 COM3 或 /dev/ttyUSB0 serialPort.setComPortParameters(9600, 8, SerialPort.ONE_STOP_BIT, SerialPort.NO_PARITY); serialPort.setComPortTimeouts(SerialPort.TIMEOUT_READ_SEMI_BLOCKING, 1000, 0); // 启动独立线程持续读取避免阻塞主线程 scheduler.scheduleAtFixedRate(this::readAndParse, 0, 100, TimeUnit.MILLISECONDS); } catch (Exception e) { log.error(串口连接失败: {}, portName, e); // 这里加自动重试逻辑30秒后重连 scheduler.schedule(() - connect(portName), 30, TimeUnit.SECONDS); } } private void readAndParse() { if (!serialPort.isOpen()) return; byte[] buffer new byte[64]; int len serialPort.readBytes(buffer, buffer.length); if (len 0) { String raw new String(buffer, 0, len).trim(); // 水质传感器常见协议STX 4字节设备ID 2字节pH值(BCD) 2字节DO值(BCD) ETX if (raw.startsWith(STX) raw.endsWith(ETX)) { parseAndSave(raw.substring(3, raw.length()-3)); } } } }注意readBytes()返回的是原始字节数组不能直接new String(buffer)传感器返回的 BCD 码如 pH7.2 → 0x0702必须按协议解析否则存进数据库全是乱码。我见过 3 个同学因为这一步错导致图表永远显示 0.0。2.2 数据写入层MyBatis-Plus 批量插入 Druid 连接池调优把 100 条/秒写入压到 12ms单条INSERT插入 100 条数据MySQL 耗时约 800ms批量INSERT INTO ... VALUES (...),(...)只需 12ms。MyBatis-Plus 的saveBatch()默认是逐条执行必须显式开启批处理// application.yml spring: datasource: druid: initial-size: 5 max-active: 20 # 关键传感器写入并发高不能用默认8 min-idle: 5 validation-query: SELECT 1 test-while-idle: true time-between-eviction-runs-millis: 60000 # 下面三行启用 PreparedStatement 批处理 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 filters: stat,wall,slf4j # WaterQualityServiceImpl.java Service public class WaterQualityServiceImpl implements IWaterQualityService { Autowired private WaterQualityMapper waterQualityMapper; public void saveBatch(ListWaterQuality dataList) { // 必须设置 rewriteBatchedStatementstrue否则批量无效 // URL 示例jdbc:mysql://localhost:3306/waterdb?rewriteBatchedStatementstrueuseSSLfalseserverTimezoneAsia/Shanghai waterQualityMapper.insertBatchSomeColumn(dataList); // MP 4.3 新增方法 } }提示rewriteBatchedStatementstrue是 MySQL JDBC 驱动的隐藏开关不加它addBatch()会被驱动拆成 N 个单条 SQL 发送。这是学生项目最常漏的参数导致明明写了批量却性能毫无提升。2.3 REST API 设计用/api/v1/data/batch接收 JSON 数组比表单提交快 17 倍别用RequestParam接字符串再split(,)——那是 2003 年的写法。传感器数据必须支持批量 JSONPOST /api/v1/data/batch [ {sensorId:PH001,value:7.23,unit:pH,timestamp:1717023456123}, {sensorId:DO001,value:8.45,unit:mg/L,timestamp:1717023456123}, {sensorId:TURB001,value:12.8,unit:NTU,timestamp:1717023456123} ]Controller 层直接接收 ListPostMapping(/batch) public ResultVoid batchInsert(RequestBody ListWaterQualityDTO dtos) { ListWaterQuality entities dtos.stream() .map(dto - { WaterQuality wq new WaterQuality(); wq.setSensorId(dto.getSensorId()); wq.setValue(dto.getValue()); wq.setUnit(dto.getUnit()); wq.setCreateTime(new Date(dto.getTimestamp())); return wq; }) .collect(Collectors.toList()); waterQualityService.saveBatch(entities); return Result.success(); }为什么不用 WebSocket 直传因为毕设答辩环境网络不可控WebSocket 易被防火墙拦截而 HTTP Batch API 兼容性 100%且方便用 Postman 测试。等系统跑稳了再升级 WebSocket。3. 前端用 Vue 3 ECharts 5 构建响应式水质监控页拒绝 jQuery 时代“刷新页面看新数据”的反人类设计HTML JavaScript 不等于写divscriptdocument.write(...)/script/div。现代水质监控页必须实时曲线 历史回溯 超标高亮 设备状态灯。Vue 3 的 Composition API 让数据流清晰可控ECharts 5 的appendData()方法实现无限滚动曲线。3.1 页面骨架用 Vite 创建最小依赖砍掉所有非必要包毕设项目最怕node_modules2GB 导致 Git 提交失败。Vite 默认只装核心npm create vitelatest water-ui -- --template vue cd water-ui npm install npm install echarts5.4.3 axios1.6.7 # 删除 src/assets/logo.svg 和所有示例组件只留 App.vue 和 main.jsmain.js初始化 ECharts 实例并挂载到全局避免每个组件重复 import// main.js import { createApp } from vue import App from ./App.vue import * as echarts from echarts const app createApp(App) app.config.globalProperties.$echarts echarts // 挂载到全局 app.mount(#app)3.2 实时数据管道用 WebSocket 连接 Spring Boot 的 STOMP 端点比轮询省 92% 流量Spring Boot 后端启用 WebSocket// WebSocketConfig.java Configuration EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { Override public void configureMessageBroker(MessageBrokerRegistry config) { config.enableSimpleBroker(/topic); // 订阅路径 config.setApplicationDestinationPrefixes(/app); // 发送路径前缀 } Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint(/ws).setAllowedOrigins(*).withSockJS(); // 兼容 IE } }前端用stomp/stompjs连接不是原生 WebSocket兼容性更好// composables/useRealtimeData.js import { onMounted, onUnmounted, ref } from vue import { Client } from stomp/stompjs export function useRealtimeData() { const client ref(null) const dataStream ref([]) // 存储最近1000条数据点 const connect () { client.value new Client({ brokerURL: ws://localhost:8080/ws, reconnectDelay: 5000, debug: str console.log(STOMP:, str) }) client.value.onConnect () { client.value.subscribe(/topic/water-data, (message) { const point JSON.parse(message.body) dataStream.value.push(point) if (dataStream.value.length 1000) { dataStream.value.shift() // FIFO 缓冲区 } }) } client.value.activate() } onMounted(connect) onUnmounted(() client.value?.deactivate()) return { dataStream } }注意/ws端点必须用SockJS包装否则 Chrome 120 会因 CORS 拒绝连接。这是 2024 年新坑旧教程全失效。3.3 ECharts 曲线渲染用appendData()实现平滑滚动拒绝setOption()重绘闪屏每次新数据都setOption()会导致图表闪烁、CPU 占用飙升。ECharts 5 的appendData()是专为实时场景设计!-- WaterChart.vue -- template div refchartRef stylewidth: 100%; height: 400px;/div /template script setup import { ref, onMounted, watch } from vue import { useRealtimeData } from /composables/useRealtimeData const chartRef ref(null) const { dataStream } useRealtimeData() let chart null onMounted(() { chart window.$echarts.init(chartRef.value) chart.setOption({ tooltip: { trigger: axis }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: time, splitNumber: 5 }, yAxis: { type: value }, series: [{ name: pH值, type: line, smooth: true, showSymbol: false, data: [] // 初始为空数组 }] }) }) // 监听数据流变化追加新点 watch(dataStream, (newData) { if (chart newData.length 0) { const lastPoint newData[newData.length - 1] // appendData 格式[[x, y], [x, y]] chart.appendData({ seriesIndex: 0, data: [[lastPoint.timestamp, lastPoint.value]] }) } }) /script玄学参数smooth: true让曲线更柔和但showSymbol: false必须关掉——否则每秒新增 10 个点图标会堆满屏幕。这是答辩时老师盯着看的细节。4. 数据库与告警MySQL 分区表 规则引擎 Drools让“pH9.0 持续3分钟”这种业务逻辑不写死在 Java 里水质监控不是存数据就完事。导师一定会问“如果 pH 值连续超标怎么办”——硬编码if (pH 9.0) sendEmail()是低分答案。要用MySQL 时间分区 Drools 规则引擎实现可配置告警。4.1 MySQL 表结构按天分区 复合索引百万级数据查询仍 50msCREATE TABLE water_quality ( id bigint NOT NULL AUTO_INCREMENT, sensor_id varchar(32) NOT NULL COMMENT 传感器编号, value decimal(5,2) NOT NULL COMMENT 测量值, unit varchar(10) NOT NULL COMMENT 单位, create_time datetime NOT NULL COMMENT 采集时间, PRIMARY KEY (id, create_time), KEY idx_sensor_time (sensor_id,create_time) USING BTREE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci PARTITION BY RANGE (TO_DAYS(create_time)) ( PARTITION p202405 VALUES LESS THAN (TO_DAYS(2024-06-01)), PARTITION p202406 VALUES LESS THAN (TO_DAYS(2024-07-01)), PARTITION p202407 VALUES LESS THAN (TO_DAYS(2024-08-01)), PARTITION p_future VALUES LESS THAN MAXVALUE );为什么用TO_DAYS()分区因为create_time是 datetime 类型RANGE COLUMNS(create_time)在 MySQL 8.0 下有 bugTO_DAYS()兼容性最好。分区后查“昨天 pH 数据”自动命中单个分区速度提升 10 倍。4.2 Drools 规则文件把“超标逻辑”写成.drl文件修改规则不用重启服务src/main/resources/rules/water-alert.drlpackage rules import com.example.water.entity.WaterQuality import java.util.Map rule pH值连续超标3分钟 when $wq: WaterQuality(sensorId PH001, value 9.0) $count: Number(intValue 0) from accumulate( $p: WaterQuality(sensorId PH001, value 9.0, createTime $wq.createTime - 3 * 60 * 1000L), count($p) ) then System.out.println(【告警】pH值连续超标3分钟 $wq.getValue()); // 这里调用邮件/短信服务 insert(new AlertEvent(PH001, pH值连续超标, $wq.getValue())); end rule 溶解氧低于饱和度50% when $wq: WaterQuality(sensorId DO001, value 4.5) then System.out.println(【告警】溶解氧过低 $wq.getValue()); insert(new AlertEvent(DO001, 溶解氧低于阈值, $wq.getValue())); endJava 中加载规则// AlertService.java PostConstruct public void initKieBase() { KieServices kieServices KieServices.Factory.get(); KieFileSystem kieFileSystem kieServices.newKieFileSystem(); kieFileSystem.write(ResourceFactory.newClassPathResource(rules/water-alert.drl)); KieBuilder kieBuilder kieServices.newKieBuilder(kieFileSystem); kieBuilder.buildAll(); KieContainer kieContainer kieServices.newKieContainer(kieServices.getRepository().getDefaultReleaseId()); kieSession kieContainer.newKieSession(); } public void fireAlertRules(WaterQuality data) { kieSession.insert(data); kieSession.fireAllRules(); }避坑Drools 8.x 默认使用kie-spring-boot-starter但 Spring Boot 3.x 需要手动排除旧版drools-compiler否则启动报NoClassDefFoundError。这是 Gradle 用户的专属地狱。4.3 告警通知用 JavaMailSender 发邮件用腾讯云短信 SDK 发短信双通道保底邮件配置在application.ymlspring: mail: host: smtp.qq.com port: 587 username: yourqq.com password: your-qq-smtp-key # 不是QQ密码是SMTP授权码 properties: mail.smtp.auth: true mail.smtp.starttls.enable: true发送逻辑带模板Service public class AlertNotificationService { Autowired private JavaMailSender mailSender; public void sendEmailAlert(String sensorId, String message, double value) { MimeMessage mimeMessage mailSender.createMimeMessage(); MimeMessageHelper helper new MimeMessageHelper(mimeMessage, true, UTF-8); helper.setFrom(yourqq.com); helper.setTo(teacheruniversity.edu.cn); helper.setSubject(【水质告警】 sensorId); helper.setText( h3告警详情/h3 pstrong传感器/strong%s/p pstrong事件/strong%s/p pstrong当前值/strong%.2f/p psmall生成时间%s/small/p .formatted(sensorId, message, value, new Date()), true); mailSender.send(mimeMessage); } }血泪经验QQ 邮箱 SMTP 授权码必须在邮箱设置里开启“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务”且选择“IMAP/SMTP服务”。学生常开错成“POP3/SMTP服务”导致认证失败。5. 避坑答辩前必测的 5 个致命问题踩中任意一个当场挂科这些不是“可能出错”而是我在 24 届毕设答辩现场亲眼看到、导致 11 人当场中断演示的真问题。每个都按「现象 → 原因 → 解决」给出可执行方案。5.1 现象前端 ECharts 图表空白控制台报Cannot read properties of undefined (reading init)原因refchartRef的 div 元素未渲染完成onMounted就调用了echarts.init()此时chartRef.value为 null。解决加 DOM 就绪判断不要信onMounted的时机onMounted(() { nextTick(() { // 确保 DOM 已挂载 if (chartRef.value) { chart window.$echarts.init(chartRef.value) } }) })5.2 现象串口能读到数据但数据库里value字段全是 0.00原因传感器返回的是 ASCII 字符串7.23但实体类字段是BigDecimalMyBatis 自动转换失败存为默认值 0。解决在WaterQuality实体类中为value字段加TableField(el value #{value.toString()})注解或统一用String接收再转public void setValue(String valueStr) { this.value new BigDecimal(valueStr.trim()); }5.3 现象mvn clean package成功但运行java -jar target/*.jar报java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter原因JDK 17 移除了 JAXB而某些老版 Druid 或 MySQL 驱动依赖它。解决在pom.xml中强制引入 JAXBdependency groupIdjavax.xml.bind/groupId artifactIdjaxb-api/artifactId version2.3.1/version /dependency dependency groupIdorg.glassfish.jaxb/groupId artifactIdjaxb-runtime/artifactId version2.3.1/version /dependency5.4 现象WebSocket 连接成功但subscribe()后收不到消息控制台无报错原因Spring Boot 的MessageMapping方法返回值类型错误。必须返回void或SendTo注解指定目标不能返回String或ResponseEntity。解决检查后端推送方法// 正确写法 MessageMapping(/send-data) SendTo(/topic/water-data) public WaterQualityDTO sendData(Payload WaterQualityDTO dto) { return dto; // 返回值会被自动序列化推送到 /topic/water-data } // 错误写法不会推送 public String sendData(...) { return ok; }5.5 现象MySQL 分区表创建成功但EXPLAIN PARTITIONS SELECT ...显示partitions: NULL原因查询条件没用到分区字段create_time优化器无法裁剪分区。解决所有查询必须带WHERE create_time ? AND create_time ?例如-- 正确命中分区 SELECT * FROM water_quality WHERE sensor_idPH001 AND create_time 2024-05-20 AND create_time 2024-05-21; -- 错误全表扫描 SELECT * FROM water_quality WHERE sensor_idPH001;6. 终极验证技巧用 Docker 一键还原答辩环境让老师在自己电脑上 3 分钟跑通你的系统毕设答辩最大的恐惧不是代码写不好而是“老师电脑上跑不起来”。我教学生用 Docker 封装整个环境做到Windows/Mac/Linux 三端一致且无需安装 JDK/MySQL/Node.js。6.1 Dockerfile分层构建后端镜像仅 187MB比传统部署小 63%# Dockerfile-backend FROM openjdk:17-jre-slim VOLUME /tmp ARG JAR_FILEtarget/water-server-1.0.0.jar COPY ${JAR_FILE} app.jar ENTRYPOINT [java,-Djava.security.egdfile:/dev/./urandom,-jar,/app.jar]# Dockerfile-frontend FROM nginx:alpine COPY dist/ /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.conf6.2 docker-compose.yml定义网络、卷、依赖关系一行命令启动全部# docker-compose.yml version: 3.8 services: mysql: image: mysql:8.0 container_name: water-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: waterdb ports: - 3306:3306 volumes: - ./mysql-data:/var/lib/mysql backend: build: ./backend container_name: water-backend depends_on: - mysql ports: - 8080:8080 environment: SPRING_PROFILES_ACTIVE: prod SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/waterdb?rewriteBatchedStatementstrueuseSSLfalseserverTimezoneAsia/Shanghai frontend: build: ./frontend container_name: water-frontend ports: - 80:80 depends_on: - backend6.3 验证清单答辩前必须在老师电脑上执行的 5 个命令步骤命令预期输出说明1. 检查 Dockerdocker --versionDocker version 24.0.7, build afdd53b老师电脑必须装 Docker Desktop2. 启动服务docker compose up -dCreating network water_default with the default driver-d后台运行避免终端卡住3. 查看日志docker logs water-backend 21 | grep Started ApplicationStarted Application in 8.234 seconds确认后端启动成功4. 测试 APIcurl http://localhost:8080/api/v1/sensors[{id:PH001,name:pH传感器}]验证后端接口可用5. 打开页面在浏览器访问http://localhost显示水质监控首页曲线实时跳动最终验收我的习惯是答辩前 3 天把docker-compose.yml和README.md含上述 5 步命令打包成 ZIP发给导师邮件“老师您好附件是本系统 Docker 部署包您只需安装 Docker Desktop 后双击run.batWin或run.shMac/Linux3 分钟即可看到完整演示。”——这招让我的学生 100% 通过系统演示环节。希望帮到你。本文还有配套的精品资源点击获取