资讯动态

SpringBoot+OneNet+MySQL水质监测系统实战

发布时间:2026/9/14 3:28:09 来源:尧图企业网站定制
简介本资源是一套面向计算机专业本科生的毕业设计实战项目基于SpringBoot框架构建水质监测Web系统整合MySQL数据库与OneNet云平台解决河流水质数据的远程采集、可视化展示与远程指令下发等核心问题适用于物联网Web全栈开发学习与毕设参考。压缩包共21个文件含5个JSP页面覆盖河流选择、实时/历史数据查询、OneNet可视化集成及周期指令下发、7个编译后Class文件、2个配置文件application.yml与XML、1个JS脚本及多张说明图片整体58.03MB结构清晰便于理解MVC分层与云平台对接逻辑。已有2125人学习下载提供完整可运行工程DemoApplication主类、本地与服务器双部署说明、数据库模拟数据截图及典型页面功能说明助读者快速掌握SpringBoot整合云平台、动态Web页面开发与数据库交互全流程。1. 毕设场景真实还原用 SpringBoot MySQL OneNet 实现河流水质 Web 可视化监测系统这不是一个“套模板就能跑通”的演示项目而是高校环境工程、物联网或计算机专业毕设中高频出现的典型闭环系统传感器采集 pH、溶解氧、浊度等水质参数 → 通过 NB-IoT 或串口模组上传至 OneNet 云平台 → 后端服务定时拉取数据并持久化到本地 MySQL → Web 页面实时渲染折线图、仪表盘、阈值告警列表并支持人工标注与历史回溯。它不追求高并发或微服务架构但对数据时序性、平台对接稳定性、前端图表响应速度和数据库查询效率有明确要求。适合已有 Java 基础、接触过 SpringBoot 单体开发、了解 HTTP/HTTPS 和 RESTful 接口逻辑的学生也适合作为中小型环保监测项目的最小可行原型MVP参考。关键难点不在代码量而在 OneNet API 的鉴权方式选择、MySQL 时间分区表设计、以及 ECharts 图表与后端数据结构的精准映射——这些恰恰是答辩时老师最常追问的细节。2. SpringBoot 后端核心模块搭建从依赖配置到 OneNet 数据同步2.1 项目初始化与关键依赖选型使用 Spring Initializr 创建基础项目推荐 JDK 17 SpringBoot 3.2.x必须包含以下 starter!-- pom.xml 关键依赖 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-quartz/artifactId /dependency dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version2.0.42/version /dependency提示SpringBoot 3.x 默认移除spring-boot-starter-jdbc中的 HikariCP 连接池自动配置需显式引入com.zaxxer:HikariCP:5.0.1并在application.yml中配置spring.datasource.hikari.*参数若使用 SpringBoot 2.7.x则无需额外引入。2.2 OneNet API 对接设备数据拉取与鉴权实现OneNet 提供两种主流数据获取方式HTTP API适用于低频轮询与 MQTT适用于高吞吐实时订阅。毕设场景下推荐 HTTP 方式因其调试直观、无额外中间件依赖。核心步骤如下2.2.1 获取 API Key 与设备信息登录 OneNet 控制台 → 进入目标产品 → 设备管理 → 点击具体设备 → 查看「设备详情」页中的device_id如6543210与api_key非控制台登录密码需在「API密钥管理」中新建权限设为「读取设备数据」。2.2.2 构建 OneNet 数据请求工具类// OneNetClient.java Component public class OneNetClient { private static final String BASE_URL https://api.heclouds.com; private final RestTemplate restTemplate; public OneNetClient(RestTemplateBuilder builder) { this.restTemplate builder .setConnectTimeout(Duration.ofSeconds(5)) .setReadTimeout(Duration.ofSeconds(10)) .build(); } // 拉取指定设备最近 N 条历史数据示例pH 传感器 public ListOneNetDataPoint fetchLatestPhData(String deviceId, int limit) { String url BASE_URL /devices/ deviceId /datapoints; HttpHeaders headers new HttpHeaders(); headers.set(api-key, YOUR_API_KEY_HERE); // 生产环境请从配置中心注入 HttpEntityString entity new HttpEntity(headers); // 查询参数datastream_idphlimit20且只取最近 24 小时数据 UriComponentsBuilder builder UriComponentsBuilder.fromHttpUrl(url) .queryParam(datastream_id, ph) .queryParam(limit, limit) .queryParam(start, Instant.now().minus(24, ChronoUnit.HOURS).getEpochSecond()); ResponseEntityString response restTemplate.exchange( builder.toUriString(), HttpMethod.GET, entity, String.class); if (response.getStatusCode().is2xxSuccessful()) { JSONObject json JSON.parseObject(response.getBody()); JSONArray array json.getJSONObject(data).getJSONArray(datastreams); return parsePhDataPoints(array); } else { throw new RuntimeException(OneNet API call failed: response.getStatusCode()); } } private ListOneNetDataPoint parsePhDataPoints(JSONArray datastreams) { ListOneNetDataPoint points new ArrayList(); for (Object obj : datastreams) { JSONObject ds (JSONObject) obj; if (ph.equals(ds.getString(id))) { JSONArray dpArray ds.getJSONArray(datapoints); for (Object dpObj : dpArray) { JSONObject dp (JSONObject) dpObj; points.add(new OneNetDataPoint( dp.getLong(at), // 时间戳秒级 dp.getDouble(value) // pH 值 )); } } } return points; } }参数说明start参数必须传入秒级时间戳非毫秒否则 OneNet 返回400 Bad Requestlimit最大值为 1000但毕设建议设为 50200避免单次请求超时datastream_id必须与 OneNet 设备中创建的数据流 ID 完全一致区分大小写。2.3 MySQL 表结构设计与 SpringBoot JPA 映射水质数据具有强时序特征需兼顾写入性能与查询效率。采用分表策略按月创建物理表如water_quality_202405并通过Table动态注解或 MyBatis XML 实现路由。-- 示例2024年5月水质主表实际需为每个数据流单独建表或使用 JSON 字段 CREATE TABLE water_quality_202405 ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, record_time datetime NOT NULL COMMENT 记录时间精确到秒, ph_value decimal(4,2) DEFAULT NULL COMMENT pH值, do_value decimal(5,2) DEFAULT NULL COMMENT 溶解氧(mg/L), turbidity decimal(6,2) DEFAULT NULL COMMENT 浊度(NTU), temperature decimal(4,1) DEFAULT NULL COMMENT 温度(℃), is_alert tinyint(1) DEFAULT 0 COMMENT 是否触发阈值告警0否1是, PRIMARY KEY (id), KEY idx_record_time (record_time) USING BTREE, KEY idx_alert_time (is_alert, record_time) USING BTREE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT2024年5月水质监测数据;对应实体类JPA 方式Entity Table(name water_quality_202405) public class WaterQualityRecord { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name record_time, nullable false) Temporal(TemporalType.TIMESTAMP) private Date recordTime; // 注意MySQL DATETIME 对应 java.util.Date非 LocalDateTime Column(name ph_value, precision 4, scale 2) private BigDecimal phValue; Column(name do_value, precision 5, scale 2) private BigDecimal doValue; Column(name turbidity, precision 6, scale 2) private BigDecimal turbidity; Column(name temperature, precision 4, scale 1) private BigDecimal temperature; Column(name is_alert, columnDefinition TINYINT(1) DEFAULT 0) private Boolean isAlert false; // getter/setter 省略 }注意SpringBoot 3.x 默认使用 Jakarta EE 命名空间javax.persistence.*需替换为jakarta.persistence.*若使用LocalDateTime需在application.yml中配置spring.jpa.properties.hibernate.jdbc.time_zone: UTC并确保 MySQL server time zone 为00:00否则时区转换易出错。3. Web 前端可视化实现ECharts 折线图与实时数据驱动3.1 前端工程结构与数据接口约定采用 Vue 3 Composition API或纯 HTML ECharts CDN构建轻量前端。关键目录结构如下src/ ├── assets/ │ └── css/ │ └── main.css ├── components/ │ ├── ChartPanel.vue // 主图表容器 │ └── AlertList.vue // 告警列表组件 ├── api/ │ └── waterApi.js // 封装 /api/water/latest、/api/water/history 等接口 └── main.js后端提供两个核心 REST 接口GET /api/water/latest返回最近 60 条数据用于初始化图表GET /api/water/history?start2024-05-01end2024-05-31返回指定日期范围数据用于历史回溯3.2 ECharts 折线图配置与动态更新// ChartPanel.vue 中的 setup() import * as echarts from echarts; export default { setup() { const chartRef ref(null); let chartInstance null; onMounted(() { if (chartRef.value) { chartInstance echarts.init(chartRef.value); initChart(); loadInitialData(); } }); const initChart () { const option { tooltip: { trigger: axis, formatter: {b}br/pH: {c0}br/DO: {c1} mg/Lbr/浊度: {c2} NTU }, legend: { data: [pH, 溶解氧, 浊度] }, grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }, xAxis: { type: time, splitNumber: 8 }, yAxis: [ { type: value, name: pH, min: 6.0, max: 9.0 }, { type: value, name: 溶解氧(mg/L), min: 0, max: 15 }, { type: value, name: 浊度(NTU), min: 0, max: 100 } ], series: [ { name: pH, type: line, yAxisIndex: 0, smooth: true, data: [] }, { name: 溶解氧, type: line, yAxisIndex: 1, smooth: true, data: [] }, { name: 浊度, type: line, yAxisIndex: 2, smooth: true, data: [] } ] }; chartInstance.setOption(option); }; const loadInitialData async () { try { const res await waterApi.getLatest(); const seriesData { ph: [], do: [], turbidity: [] }; res.data.forEach(item { const timestamp new Date(item.recordTime).getTime(); // 转为毫秒时间戳 seriesData.ph.push([timestamp, item.phValue]); seriesData.do.push([timestamp, item.doValue]); seriesData.turbidity.push([timestamp, item.turbidity]); }); chartInstance.setOption({ series: [ { data: seriesData.ph }, { data: seriesData.do }, { data: seriesData.turbidity } ] }); } catch (e) { console.error(加载初始数据失败, e); } }; // 每 30 秒轮询最新数据模拟实时 const startPolling () { setInterval(async () { try { const res await waterApi.getLatest(); const latest res.data[0]; const timestamp new Date(latest.recordTime).getTime(); chartInstance.appendData([ { seriesIndex: 0, data: [timestamp, latest.phValue] }, { seriesIndex: 1, data: [timestamp, latest.doValue] }, { seriesIndex: 2, data: [timestamp, latest.turbidity] } ]); } catch (e) { console.warn(轮询失败跳过本次更新); } }, 30000); }; onMounted(() { startPolling(); }); return { chartRef }; } };关键点说明appendData方法比setOption更高效避免全量重绘xAxis.type: time要求传入毫秒时间戳而 OneNet 返回的是秒级时间戳必须乘以 1000yAxis.min/max强制设定范围防止因异常数据导致图表缩放失真smooth: true启用贝塞尔曲线插值使水质变化趋势更符合物理直觉。3.3 告警状态联动与阈值配置管理水质安全阈值需可配置如 pH 6.5–8.5DO 4.0 mg/L并在前端实时标红。后端提供/api/thresholds接口返回当前规则{ ph: { min: 6.5, max: 8.5 }, do: { min: 4.0, max: null }, turbidity: { max: 20.0 } }前端在loadInitialData后遍历每条数据判断是否越界并在图表上添加markLine或markPoint// 在 loadInitialData 内部追加 res.data.forEach(item { if (item.phValue thresholds.ph.min || item.phValue thresholds.ph.max) { chartInstance.dispatchAction({ type: addMarkPoint, seriesIndex: 0, data: [{ name: pH 超限, coord: [new Date(item.recordTime).getTime(), item.phValue], value: item.phValue, itemStyle: { color: #ff4757 } }] }); } });4. MySQL 性能优化与 OneNet 数据同步稳定性保障4.1 针对水质时序数据的索引与分区策略单纯依靠record_time索引在百万级数据下查询仍会变慢。必须实施分区Partitioning-- 对 water_quality_202405 表按 record_time 进行 RANGE 分区每月一个分区 ALTER TABLE water_quality_202405 PARTITION BY RANGE (TO_DAYS(record_time)) ( PARTITION p20240501 VALUES LESS THAN (TO_DAYS(2024-05-01)), PARTITION p20240502 VALUES LESS THAN (TO_DAYS(2024-05-02)), PARTITION p20240503 VALUES LESS THAN (TO_DAYS(2024-05-03)), -- ... 可按需扩展 PARTITION p_future VALUES LESS THAN MAXVALUE );验证分区效果执行EXPLAIN PARTITIONS SELECT * FROM water_quality_202405 WHERE record_time BETWEEN 2024-05-10 AND 2024-05-15;输出中partitions列应仅显示p20240510,p20240511,p20240512,p20240513,p20240514,p20240515证明查询精准命中分区。4.2 OneNet 数据拉取失败的容错与重试机制网络抖动或 OneNet 限流会导致fetchLatestPhData()失败。不能简单抛异常中断任务需实现指数退避重试Service public class DataSyncService { private static final int MAX_RETRY 3; private static final long INITIAL_DELAY_MS 1000L; Scheduled(fixedDelay 60000) // 每分钟同步一次 public void syncFromOneNet() { for (int attempt 0; attempt MAX_RETRY; attempt) { try { ListOneNetDataPoint points oneNetClient.fetchLatestPhData(6543210, 50); saveToDatabase(points); break; // 成功则退出循环 } catch (Exception e) { if (attempt MAX_RETRY) { log.error(OneNet 同步失败已达最大重试次数, e); sendAlert(OneNet 数据同步异常请检查网络与 API Key); break; } long delay (long) Math.pow(2, attempt) * INITIAL_DELAY_MS; log.warn(OneNet 同步第 {} 次失败{}ms 后重试, attempt 1, delay, e); try { Thread.sleep(delay); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); break; } } } } private void saveToDatabase(ListOneNetDataPoint points) { String sql INSERT INTO water_quality_202405 (record_time, ph_value) VALUES (?, ?); jdbcTemplate.batchUpdate(sql, points, points.size(), (ps, point) - { ps.setTimestamp(1, new Timestamp(point.getAt() * 1000L)); ps.setBigDecimal(2, BigDecimal.valueOf(point.getValue())); }); } }参数说明fixedDelay 60000表示上一次执行结束后 60 秒再启动下一次避免因单次耗时长导致任务堆积batchUpdate批处理比单条update快 510 倍setTimestamp(1, new Timestamp(...))中乘以 1000 是将秒级时间戳转为毫秒级java.sql.Timestamp。4.3 Web 页面加载优化服务端渲染与静态资源缓存ECharts 初始化较慢首屏白屏明显。可通过 Thymeleaf 模板引擎预渲染骨架!-- index.html -- !DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8/ title河流水质监测系统/title link relstylesheet th:href{/css/main.css} /head body div idapp div classloading th:if${not #lists.isEmpty(latestData)} div classchart-container div idmainChart stylewidth:100%;height:500px;/div /div div classalert-list th:replacefragments/alertList :: alertList(${alertList})/div /div div classskeleton th:unless${not #lists.isEmpty(latestData)} div classskeleton-header/div div classskeleton-chart/div div classskeleton-alerts/div /div /div script th:src{/js/echarts.min.js}/script script th:src{/js/main.js}/script /body /html后端 Controller 提前查库填充模型GetMapping(/) public String home(Model model) { ListWaterQualityRecord latest waterQualityRepository.findTop60ByOrderByRecordTimeDesc(); ListAlertRecord alerts alertRepository.findTop10ByOrderByTriggerTimeDesc(); model.addAttribute(latestData, latest); model.addAttribute(alertList, alerts); return index; }效果用户访问/时服务端已查好最近 60 条数据并渲染骨架 DOM浏览器仅需执行 JS 初始化 ECharts首屏时间从 2.3s 降至 0.8s同时nginx配置静态资源缓存location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control public, immutable; }5. 毕设答辩高频问题应对与部署实操技巧5.1 三个必答技术点及标准回答口径问题回答要点精炼版为什么不用 MQTT 而用 HTTP 拉取“MQTT 需维护长连接与心跳在校园网 NAT 环境下易断连HTTP 拉取逻辑简单、调试直观且每分钟同步一次完全满足水质监测的分钟级时效要求若未来扩展为百台设备再引入 Kafka MQTT 消费集群。”MySQL 分区表如何保证跨月查询性能“查询语句中WHERE record_time BETWEEN 2024-04-25 AND 2024-05-05会自动命中p20240425至p20240505共 12 个分区Explain 显示partitions列仅列出这 12 个避免全表扫描同时建立(record_time, is_alert)联合索引加速告警查询。”ECharts 图表卡顿怎么解决“一是禁用动画animation: false二是限制数据点数量前端只保留最近 200 条超出部分自动shift()三是启用 Canvas 渲染器echarts.init(dom, null, { renderer: canvas })比 SVG 在低端设备快 3 倍。”5.2 本地部署与演示环境快速搭建清单步骤命令/操作说明1. MySQL 初始化mysql -u root -p init_db.sqlinit_db.sql包含建库、建表、分区、插入测试数据100 条模拟水质记录2. OneNet 模拟数据注入curl -X POST https://api.heclouds.com/devices/6543210/datapoints -H api-key: YOUR_KEY -d {datastreams:[{id:ph,datapoints:[{value:7.2}]}]}用于验证后端能否成功拉取避免依赖真实传感器3. SpringBoot 启动参数java -jar -Dspring.profiles.activeprod -Dserver.port8080 target/water-monitor-0.0.1-SNAPSHOT.jarprodprofile 启用连接池监控与 SQL 日志logging.level.com.zaxxer.hikariDEBUG4. 前端构建与 Nginx 代理npm run build cp -r dist/* /var/www/html/ Nginx 配置location /api { proxy_pass http://localhost:8080; }实现前后端分离部署避免浏览器 CORS 问题5.3 真实数据接入调试 checklist✅ OneNet 设备在线状态为「在线」且「数据流」中能看到实时上报的ph、do等字段✅ 后端日志中出现Fetched 12 data points from OneNet device 6543210✅ MySQL 表water_quality_202405中SELECT COUNT(*)结果持续增长✅ 访问http://localhost:8080/api/water/latest返回 JSON 数组且phValue字段值在合理范围内6.0–9.0✅ ECharts 图表 X 轴时间刻度自动适配Y 轴数值随新数据平滑移动无跳变或空白注意若遇到Web 视图加载失败error: could not register service worker说明前端service-worker.js文件缺失或路径错误直接删除public目录下该文件并注释main.js中的navigator.serviceWorker.register()调用即可毕设无需 PWA 功能。部署完成后打开浏览器访问http://localhost你会看到一条平滑的 pH 曲线正随着时间推移缓缓向右延伸——这不是 Demo而是你亲手搭起的、真正连接着物理世界水质传感器的数据生命线。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价