基于Java与阿里云RDS的水质检测系统设计与实现解析

发布时间:2026/10/27 6:05:00
基于Java与阿里云RDS的水质检测系统设计与实现解析 简介一款基于Java与阿里云数据库的水质检测系统完整设计源码面向物联网、环境监测及Java Web开发者可用于快速搭建从数据采集、云端存储到展示控制的全流程水质监测应用。包体共97个文件、约1.71MB涵盖27个Java源文件实现JDBC数据库连接、用户注册登录、传感器数据读取及电机控制、35个XML配置定义数据库与系统参数、19张PNG界面/状态图、3个Gradle构建脚本、3个JAR依赖包及2个属性文件结构清晰易于导入改造。该资源已有329人学习下载源码不仅实现MySQL与阿里云数据库双路数据接入还支持下位机通过NB开发板远程上传数据并可在移动端显示温度与水质传感器数值、远程操控换水电机是学习Java物联网实战与云数据库集成的优质案例。1. 水质检测系统这件事Java 和阿里云 RDS 把监测数据真正用起来搞水质监测的人应该都有这个经历检测仪的数据堆在 Excel 里周报靠人肉汇总领导临时想查某个站点某天下午的 pH 曲线得翻半天原始记录。这套基于 Java 和阿里云数据库的水质检测系统设计源码就是把这一摊子事收进一个工程里——设备数据上报、阈值预警、站点查询、统计图表全部走数据库落库和接口输出。它解决的不是“怎么测水质”而是“测出来的数据怎么存、怎么查、怎么变成能用的信息”。适合两类人一是刚接手监测类项目的 Java 后端开发想看看一个真实的业务体系怎么组织二是已经在用阿里云 RDS MySQL、但只把它当本地 MySQL 使的工程师这份源码里的连接配置、表设计和定时任务写法值得直接抄。2. 项目骨架与选型为什么是 Spring Boot MyBatis Plus RDS MySQL2.1 技术栈判断这套源码的选型逻辑水质检测系统本质上是一个“设备数据采集 数据存储 查询预警”的物联网后端它和普通 CRUD 系统的区别在于写入频率高、数据带时间戳、查询经常按站点和时间段聚合。如果用原生 JDBC 硬写连接管理、分页、事务都要自己兜底项目维护成本会失控如果引入太重的工作流引擎或大数据组件对单机部署又明显过剩。这套源码选的是 Spring Boot MyBatis Plus MySQL 这条路上最常见的一组组合。Spring Boot 负责把 HTTP 接口、定时任务、连接池装配好MyBatis Plus 把单表 CRUD 和分页从手写 SQL 里解放出来但复杂聚合查询仍然留了 XML 自定义 SQL 的口子数据库落到阿里云 RDS MySQL是因为它和自建 MySQL 在 SQL 层面完全兼容迁移成本几乎为零同时白名单、备份、监控这些运维动作都可以在控制台点掉。换句话说这套选型没有追求新奇而是把“能稳定跑起来、能快速改需求”放在了第一位对大多数中小规模监测项目是合理的。2.2 模块划分与目录结构拿到源码先别急着跑先把目录吃透。它没有把所有类都堆在 controller 里而是按业务边界拆成了几块我拆解下来大概是这个结构包名/目录职责说明controller对外 HTTP 接口负责参数接收和响应封装service / service.impl业务逻辑层数据校验、预警判断、事务控制都在这里mapperMyBatis Plus 的 Mapper 接口继承 BaseMapper 获得基础 CRUDentity数据库表对应的实体类字段名与表结构一一对应dto / vo接口入参和出参对象避免实体类直接暴露给前端configMyBatis Plus 分页插件、Cors 跨域、定时任务等配置job定时任务比如定期清理过期数据、重新计算超标记录新手容易犯的错是直接把 entity 当返回对象用字段一多接口文档就乱。这套源码里 entity 只负责映射表对外传输用 dto/vo两个端互不污染这个习惯建议保留。另外注意 config 包里单独拆了分页插件配置这和后面图表查询能不能顺利跑起来直接相关。2.3 阿里云 RDS MySQL 的初始化数据库连接配置与建表脚本无论你是新建 RDS 实例还是复用已有的先确认三件事实例版本选了 MySQL 8.x兼容性最好窗口函数和 JSON 能力都有、账号权限足够、白名单里加了自己的出口 IP。这一步做完再把连接串换成 RDS 提供的内网地址或公网地址写进 application.ymlspring: datasource: url: jdbc:mysql://rm-xxxxxxx.mysql.rds.aliyuncs.com:3306/water_quality?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueconnectTimeout5000socketTimeout60000 username: water_app password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 3000 idle-timeout: 600000 max-lifetime: 1800000这里有几个参数值得单独拎出来说。serverTimezoneAsia/Shanghai不写你查出来的时间大概率比实际慢 8 小时这和 RDS 默认时区有关useSSLfalse在公网连接时能省掉一层握手开销但如果你所在企业强制要求加密传输可以改成 true 并配 CA 证书allowPublicKeyRetrievaltrue是 MySQL 8 用 caching_sha2_password 认证时必需的不加会报Public Key Retrieval is not allowed。连接池方面maximum-pool-size设 20 对中小系统够用调太大反而会让 RDS 的连接数被打满因为云数据库实例有连接数上限超了直接报Too many connections。然后是建表脚本。这套源码里最核心的是三张表设备表、水质监测记录表、预警记录表。监测记录表是数据量增长最快的一张它的设计决定了查询性能上限CREATE TABLE t_water_quality_record ( id bigint NOT NULL AUTO_INCREMENT, device_id varchar(64) NOT NULL COMMENT 设备编号, station_id varchar(64) NOT NULL COMMENT 站点编号, ph decimal(5,2) DEFAULT NULL COMMENT pH值, turbidity decimal(8,3) DEFAULT NULL COMMENT 浊度 NTU, dissolved_oxygen decimal(6,3) DEFAULT NULL COMMENT 溶解氧 mg/L, temperature decimal(5,2) DEFAULT NULL COMMENT 水温 ℃, collect_time datetime NOT NULL COMMENT 采集时间, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 入库时间, PRIMARY KEY (id), UNIQUE KEY uk_device_collect (device_id, collect_time), KEY idx_station_time (station_id, collect_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT水质监测记录表;这个表设计有几个要点。第一device_id和collect_time建了唯一索引这是防重的基础——设备重复上报同一时间点的数据第二次直接插入失败应用层捕获这个冲突即可实现幂等。第二idx_station_time复合索引专门服务“查某站点某时间段数据”这个高频查询注意字段顺序是 station_id 在前、collect_time 在后这匹配查询条件的等值范围模式。第三指标字段全部用 decimal 而不是 float水质数据对精度敏感float 在累加平均时会出现偏差虽然界面看不出来但报表对不上的时候很折磨人。3. 核心业务实现从设备上报到水质预警3.1 设备数据上报接口接收检测仪的 JSON 数据并幂等入库检测仪或采集网关把数据以 JSON 形式 POST 到这个接口Controller 只做参数接收真正的校验和落库放在 Service 层。上报接口的第一个坑是重复数据设备断网重连后会把缓存的数据重推一遍如果不做幂等记录表里会塞满重复行统计平均值时直接翻车。源码里的做法是“唯一索引 捕获 DuplicateKeyException”简单有效PostMapping(/api/device/report) public ResultVoid report(RequestBody Validated WaterQualityReportDTO dto) { WaterQualityRecord record new WaterQualityRecord(); record.setDeviceId(dto.getDeviceId()); record.setStationId(dto.getStationId()); record.setPh(dto.getPh()); record.setTurbidity(dto.getTurbidity()); record.setDissolvedOxygen(dto.getDissolvedOxygen()); record.setTemperature(dto.getTemperature()); record.setCollectTime(dto.getCollectTime()); return waterQualityService.saveRecordWithIdempotency(record); }Service public class WaterQualityServiceImpl implements WaterQualityService { Override Transactional(rollbackFor Exception.class) public ResultVoid saveRecordWithIdempotency(WaterQualityRecord record) { try { waterQualityRecordMapper.insert(record); } catch (DuplicateKeyException e) { // 同一设备同一采集时间的数据已存在直接忽略重复上报 log.warn(duplicate report ignored, device{}, time{}, record.getDeviceId(), record.getCollectTime()); return Result.ok(重复数据已忽略); } return Result.ok(); } }service 层用Transactional保证单次插入要么成功要么抛异常重复数据依赖数据库的唯一索引挡住比先 select 再 insert 的方式更可靠——因为 select 和 insert 之间存在时间窗并发上报时仍然可能插重。DuplicateKeyException是 Spring 对数据库唯一键冲突的统一封装不要自己去抓SQLIntegrityConstraintViolationException那样就把 MyBatis 的异常实现细节泄漏到了业务层。这里还有一层参数校验collect_time不能是未来时间、ph必须在 0 到 14 之间。前端传来的数据不一定靠谱检测仪偶尔会出负值或 null这种脏数据入库后会把整段曲线拉歪。建议在 DTO 上用NotNull、DecimalMin、DecimalMax做基础校验业务规则再在 service 里补一道。3.2 预警逻辑数据入库后立刻判断是否超标水质监测系统光存数据没有意义核心价值在于“指标超了能马上知道”。预警判断放在上报流程里同步做还是异步做这套源码选择同步做因为上报频率不算高一般几十秒一条在事务里顺带插入预警记录可以保证数据一致性。阈值配置不是硬编码在代码里而是读数据库配置表这样调整标准不用重新发版Override Transactional(rollbackFor Exception.class) public ResultVoid saveRecordWithIdempotency(WaterQualityRecord record) { try { waterQualityRecordMapper.insert(record); } catch (DuplicateKeyException e) { log.warn(duplicate report ignored, device{}, time{}, record.getDeviceId(), record.getCollectTime()); return Result.ok(重复数据已忽略); } // 判断是否触发预警阈值从配置表读取 checkAndSaveAlarm(record); return Result.ok(); } private void checkAndSaveAlarm(WaterQualityRecord record) { QualityThreshold threshold qualityThresholdMapper.selectById(record.getStationId()); if (threshold null) { return; // 该站点未配置阈值不预警 } ListAlarmRecord alarms new ArrayList(); if (record.getPh() ! null record.getPh().compareTo(threshold.getPhMin()) 0 || record.getPh().compareTo(threshold.getPhMax()) 0) { alarms.add(buildAlarm(record, ph, pH超标, record.getPh().toString())); } if (record.getDissolvedOxygen() ! null record.getDissolvedOxygen().compareTo(threshold.getDoMin()) 0) { alarms.add(buildAlarm(record, dissolved_oxygen, 溶解氧偏低, record.getDissolvedOxygen().toString())); } if (!alarms.isEmpty()) { alarmRecordMapper.insertBatch(alarms); } }这段判断逻辑看着简单但有一个隐蔽的坑decimal字段的compareTo而不是。因为ph、dissolved_oxygen都是 BigDecimal 类型用比较的是对象引用而不是数值编译不会报错但结果永远不对这个坑我见过不止一次。另外注意insertBatch是 MyBatis Plus 的批量插入方法一次拼多条 SQL减少与数据库的交互次数。预警记录不直接推送短信或钉钉而是落库由定时任务去消费。这个设计的理由是预警是重要事件如果推送服务挂了或网络抖动了数据不能丢先落库再推送才有重试的余地。源码里 jeecg 风格的通知逻辑我没去细追但“先写库、后通知”这个顺序是所有监测系统都应该遵守的。3.3 历史查询与图表统计分页查询和聚合 SQL前端大屏要展示“某站点最近 24 小时的 pH 变化曲线”底层就是一条聚合 SQL。这里如果用 MyBatis Plus 的 QueryWrapper 去按时间分组SQL 写不出来所以源码里在 XML 中手写了统计查询select idselectAvgIndicatorByHour resultTypemap SELECT DATE_FORMAT(collect_time, %Y-%m-%d %H:00:00) AS time_point, ROUND(AVG(ph), 2) AS avg_ph, ROUND(AVG(turbidity), 2) AS avg_turbidity, ROUND(AVG(dissolved_oxygen), 2) AS avg_do FROM t_water_quality_record WHERE station_id #{stationId} AND collect_time BETWEEN #{startTime} AND #{endTime} GROUP BY DATE_FORMAT(collect_time, %Y-%m-%d %H:00:00) ORDER BY time_point ASC /selectGROUP BY 里用DATE_FORMAT把时间截断到小时这是时间序列聚合最常用的做法。但要注意如果这张表数据量已经上百万这个查询会走全表扫描因为DATE_FORMAT(collect_time)导致索引失效。正确做法是利用idx_station_time先按station_id collect_time范围过滤再对过滤后的结果做格式化聚合上面的 SQL 已经这么写了——WHERE 条件里用的是原始字段所以索引能命中GROUP BY 的表达式只作用于中间结果集。分页查询这块源码配置了 MyBatis Plus 的分页插件Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); pagination.setOverflow(false); interceptor.addInnerInterceptor(pagination); return interceptor; } }PaginationInnerInterceptor是 MyBatis Plus 3.x 里新版分页插件旧版的PaginationInterceptor已经废弃了。setMaxLimit(100L)是硬性保护前端传pageSize9999也不会一次性拉全表setOverflow(false)表示页数越界时直接返回空而不是自动跳回第一页这个行为更符合 API 的直觉。用的时候注意不要和 PageHelper 同时引入两个分页插件会在 SQL 上加两套 limit查出来的数据是错的。4. 阿里云 RDS 避坑五个让人半夜爬起来查日志的问题4.1 连接失败明明密码没错为什么报 Communications link failure现象本地跑得好好的部署到服务器上就连不上 RDS报Communications link failure或Connection timed out。原因RDS 控制了访问白名单你的服务器出口 IP 不在白名单里。本地能连是因为本地 IP 加过了换台机器就漏了。解决在阿里云 RDS 控制台找到“数据安全性 白名单设置”把应用服务器的公网 IP 或内网 IP 加进去。如果应用部署在阿里云 ECS 上优先用内网地址连接不走公网延迟和稳定性都更好。还有一个低频原因VPC 和交换机的网络类型不匹配ECS 和 RDS 必须处在同一个 VPC 下才能用内网地址。4.2 时间差八小时hibernate 和 mybatis 查出来的时间都不对现象数据库里存的时间是 14:00应用查出来变成 06:00用日期做统计时每天的数据都对不上。原因RDS 实例的默认时区是 UTC而中国在东八区。JDBC 连接串里没指定serverTimezone驱动就用服务器默认时区去解释数据库返回的时间。解决连接串加serverTimezoneAsia/Shanghai同时把 JVM 的时区也对齐启动参数加-Duser.timezoneAsia/Shanghai。另外可以在 RDS 控制台的“参数设置”里把default_time_zone改成08:00双保险。这个问题最坑的地方在于某些环境下代码里能看到的部分是对的只有报表和定时任务对不上排查起来很费劲。4.3 MySQL 8 认证插件导致的连接报错现象本地 MySQL 5.7 连接正常换到 RDS MySQL 8 后启动报Public Key Retrieval is not allowed。原因MySQL 8 默认的认证插件caching_sha2_password在非 SSL 连接时需要先获取 RSA 公钥而 JDBC 驱动出于安全考虑默认不允许自动获取。解决连接串加allowPublicKeyRetrievaltrueuseSSLfalse两个参数成对出现。如果企业网络强制要求 SSL那就useSSLtrue并配置证书。这是把本地库迁到 RDS 时最常撞见的第一个报错提前写进配置里能省一晚上的折腾时间。4.4 批量插入报 PacketTooBigException或连接数被占满现象往上爬历史数据时一次插入几千条报Packet for query is too large或者系统运行几天后日志频繁出现Too many connections。原因max_allowed_packet参数限制单次 SQL 包大小默认值偏小一次性批量插入超出限制就会中断。连接数打满则是连接池没控制住HikariCP 的maximum-pool-size配到 100但 RDS 实例规格可能只允许 50 个连接。解决在 RDS 控制台把max_allowed_packet调大到 64M这可以按需设置连接池参数要结合 RDS 实例规格来定应用侧maximum-pool-size控制在 20 以内同时确认没有别的地方在用手动创建的数据库连接比如某些老代码用DriverManager.getConnection用完不还这是连接池打满的主要原因。4.5 监控告警缺失数据库磁盘满了才反应过来现象某天写入突然报错The table is full查监控发现磁盘使用率已经 100%。原因RDS 磁盘是按量付费的默认不会自动扩容。监测数据每天都在涨如果没人盯磁盘阈值满了就写不进去数据直接断档。解决在 RDS 控制台打开“自动扩容”功能设定触发阈值比如 80%空间不够会自动扩展。同时给实例配好基础告警CPU、内存、磁盘使用率、慢查询数告警渠道至少接短信或钉钉机器人避免全靠人肉巡检。数据备份也确认一下自动备份策略是开启的不然扩容之后再想恢复到误删前的状态就难了。5. 把断点数据找回来SQL 回放核对与补采脚本数据入库后工作还没有结束。水质监测系统跑久了总有那么几次设备离线、网络抖动、上报接口超时导致某个时间段的数据缺了一块。曲线图上的一段空缺看的人不会太在意但做平均值统计、算达标率时这段空缺就是整套报表的对不上的来源。我的习惯是定期用 SQL 做一次“数据回放核对”把设备一周的理论采集点数按上报频率估算和实际入库条数做对比差距超过阈值就把缺失清单导出来走补采。第一步先算每个设备的理论应采点和实际落库数SELECT device_id, COUNT(*) AS actual_count, TIMESTAMPDIFF(MINUTE, MIN(collect_time), MAX(collect_time)) / 30.0 AS expected_count FROM t_water_quality_record WHERE collect_time #{startTime} AND collect_time #{endTime} GROUP BY device_id HAVING actual_count expected_count * 0.95;这个查询假设检测仪每 30 分钟上报一次如果你那边上报频率是 10 分钟把30.0换成10.0即可。expected_count是一个估算值用最大时间减最小时间除以上报间隔算出来再乘 0.95 的容忍系数——允许丢 5% 以内的数据因为部分时段设备可能是正常停机的。HAVING 子句的作用是在分组计算后过滤出“缺得比较多”的设备这些就是需要人工看一眼的。第二步对那些确实有缺失的设备生成一份缺失时间段清单写到临时表或直接导出。第三步才是补采我一般在服务里写一个定时任务每天凌晨拉一遍近 24 小时的数据凡是发现缺失的调用设备侧的 HTTP 接口重新拉取拉到的数据走正常的上报接口入库——幂等机制保证了就算补采的数据和已存在的数据重复了也只是被忽略不会污染记录表。补采逻辑里有一个细节拉数据时要用设备侧的时间作为collect_time而不是补采时的系统时间。不然这段数据虽然补上了但时间戳全变成补采时刻曲线直接错位。从那以后我每次部署完这套上线流程都会强制跑一遍上面的核对 SQL顺手检查一下磁盘容量和备份状态。这样比任何监控告警都让人安心至少数据断档不会等到报表端发现时才暴露。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询