简介:这份源码面向Java初学者与物联网开发爱好者,提供一套基于Java与阿里云数据库的水质检测系统完整实现,可用于课程设计、毕业设计或IoT环境监测练手项目。压缩包共97个文件、约1.71MB,以35个XML配置、27个Java源文件为主,另有19张PNG界面素材、3个Gradle构建文件、3个JAR依赖包及2个properties属性文件,结构清晰、便于二次开发。系统实现了MySQL数据库连接取数、用户注册登录修改注销、温度与水质传感器数据展示、发送命令操控换水电机,并支持下位机经NB开发板连接阿里云数据库上传保存数据,覆盖采集、处理、展示与控制全流程。目前已有329人学习下载,适合想了解Java JDBC、Android界面与云数据库协同的读者参考借鉴。
1. 水质检测系统为什么值得用 Java 和阿里云数据库重写一遍
很多做环境监测的团队,早期系统都是单机版:一台工控机跑个 C# 或 Delphi 程序,数据存本地 Access 或 SQLite,检测员手动导出 Excel 再上报。这套东西在只有一两个监测点时能用,一旦点位扩到几十个、要按小时上报、还要给环保平台推数据,立刻崩盘——数据对不上、并发写冲突、历史曲线查不动。基于 Java 和阿里云数据库的水质检测系统设计源码,解决的正是这个阶段的替换问题:用 Java 做服务端保证跨平台和生态成熟,用阿里云数据库(RDS MySQL 或 PolarDB)扛住多站点并发写入和长期存储,源码层面把采集、存储、告警、报表四条链路拆清楚。这套方案适合两类人:一是要交课程设计或毕设、需要一套能跑通的水质检测系统的学生;二是中小型监测站里被老系统折磨、想自己搭一套可控后端的工程师。下面按「先立住架构、再动手复现、最后讲坑」的顺序拆开讲。
2. 水质检测系统的数据模型与阿里云数据库选型
2.1 监测点位、指标、读数三张核心表怎么设计
水质检测系统的数据模型不复杂,但设计错了后期查询会非常难受。核心是三张表:监测点位表(site)、指标定义表(indicator)、读数表(reading)。点位表存站点编号、名称、经纬度、所属流域;指标表存指标编码(如 PH、DO、NH3N)、单位、上下限;读数表是事实表,存点位 ID、指标 ID、采集时间、数值。
关键决策在读数表。新手常犯的错是把每个指标做成一列(pH、do、nh3n 各一列),这样加一个指标就要改表结构,而且大量 NULL。正确做法是窄表:一行一个「点位 + 指标 + 时间 + 值」。代价是查询要按指标过滤,但换来的是无限扩展指标。
-- 监测点位表 CREATE TABLE site ( id BIGINT PRIMARY KEY AUTO_INCREMENT, site_code VARCHAR(32) NOT NULL UNIQUE COMMENT '点位编号,如 SZ-001', site_name VARCHAR(64) NOT NULL, longitude DECIMAL(10,6), latitude DECIMAL(10,6), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 指标定义表 CREATE TABLE indicator ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(16) NOT NULL UNIQUE COMMENT '指标编码 PH/DO/NH3N', name VARCHAR(32) NOT NULL, unit VARCHAR(16), lower_limit DECIMAL(10,3) COMMENT '合格下限', upper_limit DECIMAL(10,3) COMMENT '合格上限' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 读数事实表(窄表设计) CREATE TABLE reading ( id BIGINT PRIMARY KEY AUTO_INCREMENT, site_id BIGINT NOT NULL, indicator_id BIGINT NOT NULL, collect_time DATETIME NOT NULL COMMENT '采集时间', value DECIMAL(10,3) NOT NULL, quality_flag TINYINT DEFAULT 0 COMMENT '0正常 1可疑 2超标', KEY idx_site_time (site_id, collect_time), KEY idx_indicator_time (indicator_id, collect_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:reading 表建了两个联合索引,idx_site_time 服务「某点位某时间段的所有指标」这类查询,idx_indicator_time 服务「某指标全站点趋势」这类查询。参数上,value 用 DECIMAL(10,3) 而不是 FLOAT,因为水质数据要参与超标判定,浮点误差会导致边界值误判。quality_flag 是预留字段,采集端发现异常(如探头离线返回的默认值)时打标,避免脏数据污染统计。
2.2 为什么选阿里云 RDS 而不是自建 MySQL
自建 MySQL 的坑在于运维:备份、主从、慢查询、磁盘扩容,每一样都要人盯。阿里云 RDS 把这些托管掉,对中小团队最实际的价值是三点:一是自动备份和按时间点恢复,水质数据往往有上报合规要求,误删能回滚是刚需;二是读写分离,采集端高频写入走主库,报表查询走只读实例,不用自己搭中间件;三是 DTS 可以做跨地域同步,多站点分布在不同区域时不用自己写同步逻辑。
选型上,如果点位在 50 个以内、采集频率 5 分钟一次,入门级 RDS MySQL 8.0(2 核 4G)足够;如果要做实时曲线和大量聚合查询,考虑 PolarDB MySQL 版,它的共享存储架构在只读扩展上更省心。连接层用 HikariCP,这是 Java 生态里最稳的连接池,配置上把 maximumPoolSize 设成 CPU 核数的 2 到 4 倍即可,别盲目调大。
// HikariCP 数据源配置,对接阿里云 RDS HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://rm-xxxx.mysql.rds.aliyuncs.com:3306/water?useSSL=true&serverTimezone=Asia/Shanghai"); config.setUsername("water_app"); config.setPassword("******"); config.setMaximumPoolSize(16); // 按 4 核 * 4 估算 config.setMinimumIdle(4); config.setConnectionTimeout(3000); // 3 秒拿不到连接就失败,避免线程堆积 config.setIdleTimeout(600000); config.setMaxLifetime(1800000); // 小于 RDS 侧 wait_timeout,防止用到被服务端断开的连接 HikariDataSource ds = new HikariDataSource(config);参数说明:maxLifetime 必须小于阿里云 RDS 的 wait_timeout(默认 28800 秒,但很多实例调小了),否则会出现「连接已被服务端关闭但池里还认为可用」的经典报错。connectionTimeout 设短一点,让采集端快速失败重试,比线程全卡死强。
3. 用 Java 把采集、入库、告警三条链路跑通
3.1 采集端数据接入的最小实现
采集端来源通常两种:一是现场设备通过 Modbus/MQTT 上报,二是第三方平台推 JSON。Java 侧统一收口到一个接入接口,做校验后入库。最小实现用一个 Spring Boot 的 Controller 接收批量读数,核心是幂等——同一批次重复推送不能产生重复行。
@PostMapping("/api/reading/batch") public Result batchInsert(@RequestBody @Valid ReadingBatchReq req) { // 1. 按 siteCode + collectTime + indicatorCode 去重 List<Reading> list = req.getItems().stream() .map(i -> convert(req.getSiteCode(), i)) .collect(Collectors.toList()); // 2. 批量插入,利用唯一索引兜底幂等 try { readingMapper.batchInsertIgnore(list); } catch (DuplicateKeyException e) { log.warn("批次存在重复读数,已忽略: {}", req.getBatchNo()); } // 3. 异步触发超标判定,不阻塞接入 alertService.checkAsync(list); return Result.ok(); }逻辑说明:batchInsertIgnore 对应INSERT IGNORE或ON DUPLICATE KEY UPDATE,前提是 reading 表上要有 (site_id, indicator_id, collect_time) 的唯一索引,否则幂等无从谈起。alertService.checkAsync 用线程池异步执行,接入接口的响应时间不能被告警逻辑拖慢。参数上,批量大小建议控制在 500 到 1000 行一次,太大容易触发 RDS 的 max_allowed_packet 限制,太小则网络往返开销高。
3.2 超标判定与告警落库
超标判定逻辑本身简单:拿读数和 indicator 表的上下限比。但工程上有两个细节:一是判定要区分「瞬时超标」和「持续超标」,后者才值得告警;二是告警要落库并去重,不能每来一条超标数据就发一次通知。
public void checkAsync(List<Reading> list) { for (Reading r : list) { Indicator ind = indicatorCache.get(r.getIndicatorId()); if (ind == null) continue; boolean over = r.getValue().compareTo(ind.getUpperLimit()) > 0 || r.getValue().compareTo(ind.getLowerLimit()) < 0; if (!over) continue; // 持续超标判定:查最近 N 条是否都超标 int recentOver = readingMapper.countRecentOver( r.getSiteId(), r.getIndicatorId(), r.getCollectTime(), 3); if (recentOver >= 3) { alertMapper.insertIfAbsent(r.getSiteId(), r.getIndicatorId(), r.getCollectTime()); } } }参数说明:countRecentOver 里的 3 表示「连续 3 次超标才告警」,这个阈值要按采集频率调——5 分钟一次的话 3 次是 15 分钟,比较合理;如果是 1 分钟一次,3 次太敏感,容易误报。insertIfAbsent 依赖告警表上的唯一索引(site_id, indicator_id, 时间窗口)做去重,避免同一超标事件反复入库。
3.3 报表查询怎么避免拖垮主库
报表是水质检测系统里最容易出性能问题的地方。历史曲线、月度统计、超标率,这些查询动辄扫几十万行。做法是:报表查询全部走只读实例,且对高频统计做预聚合。预聚合表按「点位 + 指标 + 天」存日均值、最大值、超标次数,报表直接查这张小表。
-- 预聚合表,由定时任务每天凌晨生成 CREATE TABLE reading_daily_stat ( id BIGINT PRIMARY KEY AUTO_INCREMENT, site_id BIGINT NOT NULL, indicator_id BIGINT NOT NULL, stat_date DATE NOT NULL, avg_value DECIMAL(10,3), max_value DECIMAL(10,3), over_count INT DEFAULT 0, UNIQUE KEY uk_site_ind_date (site_id, indicator_id, stat_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:预聚合把「查一个月曲线」从扫 30 万行降到扫 30 行。代价是数据有延迟(当天数据要等次日凌晨才聚合),所以实时曲线仍查原始表但限制时间范围,历史报表查聚合表。这个取舍在监测场景里完全可接受,因为上报通常按天或按小时,不需要秒级实时。
4. 部署到阿里云时的避坑清单
4.1 连接池与 RDS 参数不匹配导致间歇性断连
现象:系统跑几小时后开始报Communications link failure,重启又正常。原因:HikariCP 的 maxLifetime 大于 RDS 的 wait_timeout,池里持有的是服务端已关闭的连接。解决:把 maxLifetime 设为比 wait_timeout 小 30 秒以上,并在 RDS 控制台确认 wait_timeout 实际值,别用默认假设。
4.2 批量插入触发 max_allowed_packet 限制
现象:大批量上报时抛Packet for query is too large。原因:一次 INSERT 的数据量超过 RDS 的 max_allowed_packet(默认常见 1MB 或 4MB)。解决:把批量拆成每批 500 行,或在 RDS 参数里调大 max_allowed_packet,但更推荐前者,因为单条大 SQL 还会长时间持锁。
4.3 时区不一致导致采集时间错位
现象:报表里数据时间比实际早或晚 8 小时。原因:Java 侧 serverTimezone 没设,或 RDS 实例时区是 UTC 而应用按东八区写入。解决:JDBC URL 显式写serverTimezone=Asia/Shanghai,RDS 参数 time_zone 也设为+08:00,两边对齐。这个坑很隐蔽,因为单看一边都正常。
4.4 超标告警风暴
现象:某个探头故障持续返回极值,告警系统短时间内发出几百条通知。原因:只做了单条超标判定,没做持续判定和频率限制。解决:加连续 N 次判定,并在告警发送层加「同一站点同一指标 X 分钟内只发一次」的限流,用 Redis 的 SETNX 加过期时间实现最简单。
4.5 只读实例延迟导致报表数据「缺一块」
现象:刚写入的数据在报表里查不到,过几秒又出现。原因:报表走了只读实例,主从复制有延迟。解决:对实时性要求高的查询强制走主库(用注解或独立数据源),对延迟不敏感的报表才走只读。别指望只读实例零延迟。
5. 让这套源码真正可用的两个进阶技巧
第一个技巧是把指标上下限做成可配置而非硬编码。水质标准会随政策调整,如果上下限写死在代码里,每次改都要重新发版。做法是把 indicator 表的 lower_limit/upper_limit 做成后台可维护,判定逻辑每次从缓存读,缓存用定时刷新或配置中心推送。这样运维改一个阈值不用惊动开发。
第二个技巧是给采集数据加「质量标记」并在报表里体现。真实场景里探头会漂移、会离线、会返回默认值,这些数据如果直接进统计,均值会被污染。做法是采集端对异常值打 quality_flag,报表统计时默认只算 flag=0 的数据,同时单独展示「可疑数据占比」。这个字段前期不加,后期补数据迁移会很痛苦。
验证这套系统是否跑通,我一般按这个顺序:先灌 10 万条模拟读数,看批量插入耗时和索引是否生效(用 EXPLAIN 确认走索引);再模拟一个探头持续超标,确认告警只发一次;最后跑一次月度报表,对比预聚合表和原始表算出来的均值是否一致。三步都过,基本可以上生产。
我自己踩过最深的坑是时区那次——排查了大半天,最后发现是 JDBC URL 少写了一个参数。所以现在凡是新环境,第一件事就是把时区、字符集、连接池三个参数对齐再谈业务。希望帮到你。
本文还有配套的精品资源,点击获取