尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot环境监测系统实践:从数据采集到告警可视化

Spring Boot环境监测系统实践:从数据采集到告警可视化 简介一份基于Java的环境监测系统源码面向毕业设计、Java Web课程项目与初中级开发者覆盖空气质量、温湿度、CO2浓度等环境数据的采集、处理、存储与可视化展示并包含用户登录、管理员和普通用户权限区分等完整流程能够帮助读者从工程角度理解环境监测业务闭环。压缩包共46个文件大小约29KB以35个Java源文件为主同时包含DAO数据访问层、Controller控制层、数据库设计及更新脚本、README说明等代码按用户管理、监控管理、各环境数据模块如CO2、温度、湿度拆分分层清晰便于定位和修改。目前已有701人浏览学习适合用于毕业设计参考、快速二次开发或Java Web面试练手。研读源码可以掌握MVC分层、Servlet/JSP交互、JDBC数据库操作、接口设计等常见技能配合内置数据库脚本能较快搭建本地运行环境省去从零构建项目的精力。1. 环境监测系统的核心从来不是传感器一套环境监测系统放上生产环境大家最先关心的往往是硬件端的数据准不准、传输稳不稳但切换到毕业设计或者课程设计的视角题目写的是“JAVA实现”评审老师真正想看到的是一条从数据采集到落库、再到展示和告警的完整业务链路。换句话说难点不在模拟一个温度传感器而在你用 Java 生态把“数据怎么来、怎么存、怎么用”讲清楚。这篇文章按一套可复现的架构来讲采集端可以是真实串口设备、也可以是模拟数据源服务端用 Spring Boot 负责接收、清洗和存储Web 端提供实时曲线和告警记录。适合两类人一是拿这个题目做毕业设计的学生需要的是能跑通、能讲解、能应付答辩的完整代码组织方式二是想快速搭一套轻量级监控后台的 Java 工程师可以参考它的分层和表结构设计。下面所有代码都按最小可用原则给出直接复制就能本地跑再根据你自己的硬件或业务去扩展。2. 数据模型设计先让指标字典稳定再谈页面展示2.1 环境监测系统需要几张表很多初写这个题目的同学一上来就建一张“监测数据大表”把所有字段堆进去结果后期加指标、加监测点的时候改表改到怀疑人生。我一般会把表拆成四类监测点、指标字典、实时/历史监测数据、告警记录。监测点表monitor_point描述“在哪里测”比如一号厂房东侧、污水处理站入口指标字典表indicator_info描述“测什么”比如温度、湿度、PM2.5、二氧化硫浓度监测数据表存实际数值告警记录表存超限事件。这样拆的好处是新增一个监测点时不需要改表结构只要往监测点表插一行新增一种污染物指标时同理。一个容易忽略的设计点是指标的单位。温度用摄氏度、PM2.5 用 μg/m³、气压用 hPa如果不统一存入数据库前端展示和告警阈值比较时会经常出错。我的做法是在 indicator_info 表里直接冗余一个 unit 字段。CREATE TABLE monitor_point ( id BIGINT PRIMARY KEY AUTO_INCREMENT, point_code VARCHAR(32) NOT NULL UNIQUE COMMENT 监测点编码, point_name VARCHAR(64) NOT NULL COMMENT 监测点名称, location_desc VARCHAR(255) COMMENT 位置描述, status TINYINT DEFAULT 1 COMMENT 1启用 0停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT监测点表; CREATE TABLE indicator_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, indicator_code VARCHAR(32) NOT NULL UNIQUE COMMENT 指标编码, indicator_name VARCHAR(64) NOT NULL COMMENT 指标名称, unit VARCHAR(16) NOT NULL COMMENT 单位, upper_limit DECIMAL(10,2) COMMENT 上限阈值, lower_limit DECIMAL(10,2) COMMENT 下限阈值, sort_no INT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT指标字典表; CREATE TABLE monitor_data ( id BIGINT PRIMARY KEY AUTO_INCREMENT, point_id BIGINT NOT NULL COMMENT 监测点ID, indicator_id BIGINT NOT NULL COMMENT 指标ID, data_value DECIMAL(10,2) NOT NULL COMMENT 监测数值, collect_time DATETIME NOT NULL COMMENT 采集时间, KEY idx_point_time (point_id, collect_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT监测数据表;表结构里有两个地方值得展开。第一monitor_data 只用 point_id 和 indicator_id 做外键关联但建表时不加物理外键约束这是互联网开发的常见习惯——外键影响插入性能关联关系交给应用层去保证。第二查询条件基本都带 point_id 和 collect_time所以联合索引建在 (point_id, collect_time) 上而不是单独给每个字段建普通索引。数据量上来以后这个索引能明显压住按时间范围查询的扫描行数。2.2 为什么不用时序数据库听到“环境监测”四个字有经验的工程师会想到 InfluxDB、TimescaleDB 这类时序数据库。但如果这是你的毕业设计我建议先用 MySQL 把业务跑通理由有两个。其一MySQL 对于每分钟一条、一天 1440 条的数据量毫无压力一台普通笔记本就能扛住几百万行其二答辩时老师问“你的数据存在哪、怎么取的”MySQL 的存储过程和 SQL 是课程里学过的东西讲起来顺畅引入时序库反而容易把答辩重点带偏。真要用 MySQL 存时序数据有一个点要提前处理数据膨胀。一小时一条数据看不出问题但如果监测点有 50 个、采集频率压到 5 秒一次一天就是 86 万行。对这种场景我会在 monitor_data 表上按月建分区或者定时把一个月前的数据归档到 summary 表。分区语法比较简单建表时加上PARTITION BY RANGE (TO_DAYS(collect_time))每个月提前把下一个月的分区建好。考虑到毕业设计的数据量基本到不了这个量级分区可以作为加分项写进文档但代码里不建议过度设计。3. 数据接入层模拟采集器与 Spring Boot 接收链路3.1 采集端为什么从模拟开始真实硬件采集需要串口通信、Modbus 协议、或者 LoRa/ESP32 网关这些内容一展开就是另一个题目。作为 JAVA 实现的环境监测系统采集端先用一个定时任务模拟传感器数据把服务端的接收、解析、存储链路走通是性价比最高的做法。等后期有硬件了只需要把模拟器替换成串口读取或 HTTP 上报的客户端服务端完全不用改。模拟器的核心逻辑是按照配置的监测点和指标每隔固定时间生成一条符合取值范围的数据。为了让它看起来真实数值不能是完全随机数我会用“基准值 随机波动”的方式生成温度在 20 到 35 度之间波动PM2.5 在 30 到 120 之间波动。Service public class MockDataGenerator { Resource private MonitorPointMapper pointMapper; Resource private IndicatorInfoMapper indicatorMapper; Resource private MonitorDataMapper dataMapper; Scheduled(fixedRate 5000) public void generate() { ListMonitorPoint points pointMapper.selectList( new LambdaQueryWrapperMonitorPoint().eq(MonitorPoint::getStatus, 1)); ListIndicatorInfo indicators indicatorMapper.selectList(null); for (MonitorPoint point : points) { for (IndicatorInfo indicator : indicators) { MonitorData data new MonitorData(); data.setPointId(point.getId()); data.setIndicatorId(indicator.getId()); data.setDataValue(randomValue(indicator)); data.setCollectTime(LocalDateTime.now()); dataMapper.insert(data); } } } private BigDecimal randomValue(IndicatorInfo indicator) { double base (indicator.getUpperLimit().doubleValue() indicator.getLowerLimit().doubleValue()) / 2; double range indicator.getUpperLimit().doubleValue() - indicator.getLowerLimit().doubleValue(); double value base (Math.random() - 0.5) * range * 0.4; return BigDecimal.valueOf(value).setScale(2, RoundingMode.HALF_UP); } }这段代码里fixedRate 5000表示每 5 秒执行一次从系统启动后固定节奏触发不管上次任务是否执行完毕。如果采集逻辑耗时较长应该改成fixedDelay它的含义是上一次任务跑完后再等 5 秒。环境监测的采集任务通常很轻一次生成几十条记录毫秒级完成用 fixedRate 没问题。随机值生成时我取上下限的中间值作为基准再偏移 ±20% 的范围这样数据会围绕正常值波动偶尔才会触达告警边界演示时既能看到曲线变化又不会告警刷屏。3.2 接收真实设备的 HTTP 上报接口模拟器是服务端主动拉数据真实设备往往是设备端主动推数据所以系统还要提供一个上报接口。硬件网关把 JSON 数据 POST 到/api/v1/upload服务端解析后批量落库。批量插入非常关键千万不能一条一条 insert否则设备一分钟上报一次、每次几百条记录时数据库连接会被频繁占用。PostMapping(/upload) public ResultVoid upload(RequestBody UploadRequest request) { ListMonitorData dataList request.getDataList().stream().map(item - { MonitorData data new MonitorData(); data.setPointId(item.getPointId()); data.setIndicatorId(item.getIndicatorId()); data.setDataValue(item.getValue()); data.setCollectTime(item.getCollectTime()); return data; }).collect(Collectors.toList()); dataMapper.insertBatchSomeColumn(dataList); return Result.success(); }insertBatchSomeColumn是 MyBatis-Plus 内置的批量插入方法需要在实体类对应的 Mapper 里继承BaseMapper并额外配置一个注入方法具体配置方式 MyBatis-Plus 官方文档有写。批量大小建议控制在 500 条以内太大容易超过 MySQL max_allowed_packet 限制。还有一点上报接口要校验collectTime是否为 null真实设备经常有时区配置错误导致时间字段丢失服务端可以设定“当前时间偏差超过 5 分钟就拒绝入库并记录日志”的规则避免脏数据污染曲线。4. 查询与告警让数据产生业务价值4.1 实时曲线接口的分页与聚合前端页面一般有两个诉求进入监测点详情页时展示最近 1 小时或 24 小时的曲线页面轮询请求最新数据点。这里有一个需要和前端约定好的查询参数granularity聚合粒度。在 MySQL 里做时间聚合最常用的手段是DATE_FORMAT配合GROUP BY但直接在 SQL 里写死格式不灵活。我会让前端传来granularityminute或granularityhour后端据此动态拼 SQL。GetMapping(/history) public ResultListMonitorDataVO history(Long pointId, Long indicatorId, String startTime, String endTime, String granularity) { String pattern minute.equals(granularity) ? %Y-%m-%d %H:%i : %Y-%m-%d %H; LambdaQueryWrapperMonitorData wrapper new LambdaQueryWrapper(); wrapper.eq(MonitorData::getPointId, pointId) .eq(MonitorData::getIndicatorId, indicatorId) .ge(MonitorData::getCollectTime, startTime) .le(MonitorData::getCollectTime, endTime) .last(GROUP BY DATE_FORMAT(collect_time, pattern )); // 实际查询需要配合 select 聚合字段这里示意分组条件 ListMonitorData list dataMapper.selectList(wrapper); return Result.success(list); }这段代码在实际运行时还需要在 select 中指定 AVG(data_value) 和 DATE_FORMAT 的别名否则 MyBatis-Plus 会返回整行字段。这里的重点是讲清楚设计思路分钟级粒度用于展示最近 1 小时曲线小时级粒度用于 7 天趋势。如果需要更高性能可以把聚合查询的结果缓存到 Rediskey 设计成history:{pointId}:{indicatorId}:{granularity}:{startTime}缓存时间 60 秒因为历史数据本身不会频繁变化只有最新数据点需要实时查询。4.2 告警判定不要在页面里做判断告警逻辑有一个常见误区和生产环境的做法差距很大前端拿到数据后自己判断是否超限然后弹窗提示。这种方式导致告警规则分散在多个页面运维人员改阈值要改前端代码重新发版。合理的做法是告警判定放在服务端前端只负责展示。我的实现方式是在模拟器生成数据或上报接口入库时同步执行一次规则校验超限就插入告警记录表并使用 Async 发送站内通知或邮件。Async public void checkAlert(MonitorData data, IndicatorInfo indicator) { BigDecimal value data.getDataValue(); boolean overUpper indicator.getUpperLimit() ! null value.compareTo(indicator.getUpperLimit()) 0; boolean underLower indicator.getLowerLimit() ! null value.compareTo(indicator.getLowerLimit()) 0; if (overUpper || underLower) { AlertRecord record new AlertRecord(); record.setPointId(data.getPointId()); record.setIndicatorId(data.getIndicatorId()); record.setDataValue(value); record.setAlertType(overUpper ? upper : lower); record.setAlertTime(LocalDateTime.now()); alertRecordMapper.insert(record); } }阈值比较一定要用compareTo不要用或。因为 data_value 在数据库里是 DECIMAL 类型映射到 Java 后是 BigDecimal包装类型之间用比较的是地址而不是数值。这个坑在答辩演示时最容易暴露测数据明明超标了告警记录就是不出来一整晚查不到原因。另外告警记录表要加一个handle_status字段0 表示未处理、1 表示已确认这是答辩时被问得最多的地方之一——“你的系统告警之后怎么闭环”。后续可以接一个简单的处理页面点击确认后记录处理人和处理时间。4.3 告警风暴怎么避免如果某个监测点持续超标超过半小时每个采集周期都生成一条告警用户的手机就会一直响这在生产环境叫告警风暴。毕设里不用做得太复杂我用的策略是“同点位同指标 10 分钟内只生成一条告警”。实现方式有两种查询最近一条告警记录看时间差或者用 Redis 的 SETNX 带上过期时间。代码如下Boolean first redisTemplate.opsForValue() .setIfAbsent(alert: pointId : indicatorId, 1, Duration.ofMinutes(10)); if (Boolean.TRUE.equals(first)) { // 生成告警 }这个方案比查数据库优雅得多不需要额外写 SELECT 语句Redis 自动过期相当于重置窗口。如果项目里还没集成 Redis用数据库查询最近一条告警的时间也可以只是并发高时要加唯一索引防重。告警的去重逻辑放在服务端可以做单元测试这一点在文档里写清楚答辩时是加分项。5. 可视化与演示技巧从能跑到能讲5.1 选择 ECharts 还是自绘表格Web 端可视化有两种主流选型集成 ECharts 画折线图、面积图、仪表盘或者用表格加上简单的 CSS 状态灯。ECharts 是更稳妥的选择因为环境监测系统的核心展示就是“时间-数值”曲线这是折线图的典型场景而且 ECharts 的 tooltip 自带十字准星和数据点信息不需要额外开发。数据刷新推荐用 setInterval 每隔 5 秒拉一次最新数据接口而不是 WebSocket。理由很简单轮询的代码量少、逻辑直观、不容易出连接异常5 秒一次的数据量对接入端和服务端的压力都极小。前端页面结构上我会做成一个监测点列表页加一个详情页。列表页展示所有监测点的最新一条数据和状态正常/离线/超标详情页展示选定监测点的多指标曲线和告警记录表格。列表页的离线判断逻辑要注意如果当前时间减去该监测点最新数据的 collectTime 大于 60 秒就认为离线。这个阈值要说明离线判定和告警一样应该放在后端接口里返回而不是前端拿着时间戳自己算——否则用户改了时区页面状态就全乱了。5.2 演示前的三个数据准备工作答辩现场最怕的是打开页面发现曲线是空的。因为模拟数据生成器要等系统启动后才开始攒数据如果启动完立刻演示只有一两个数据点曲线根本看不出趋势。我的做法是在系统启动时加一个DataInitializer如果当天数据表记录数小于某个阈值就自动回填过去 24 小时的数据每 5 分钟一条这样打开页面就有平滑的历史曲线可看。第二个准备工作是准备一个“超标场景”。随机波动的数据很难在演示时正好碰到超限告警所以我手动往监测点插入几条超标数据例如把某个指标的值设为上限的 1.5 倍然后演示告警记录页面的变化。这个操作可以做成一个测试接口/api/v1/simulate/alert只在开发环境开启生产环境要关闭。最后是时间校准。如果你的电脑时区和数据库时区不一致查询出来的 collectTime 会偏移 8 小时曲线高峰对不上。在 JDBC 连接串上加上serverTimezoneAsia/Shanghai并且数据库连接和 Java 应用都用同一时区这个问题就能避免。答辩前把这几项检查完现场演示一般不会出幺蛾子。本文还有配套的精品资源点击获取
返回列表