Spring Boot+ECharts构建生态环境动态监测系统:设计、实现与答辩避坑
发布时间:2026/9/26 14:34:54来源:尧图网络
简介面向毕业设计场景的柳州市生态环境动态监测系统完整项目包适合环境工程、地理信息、计算机等相关专业本科生参考用于理解环境监测系统的需求分析、模块划分、数据可视化与预警联动等实现思路。资源共2000个文件以1278个JS脚本、558个HTML页面和136个CSS样式为主另有少量配置与说明文档压缩包约32.64MB整体呈现典型的前端可视化应用结构。系统覆盖空气质量、水质、噪声、植被覆盖与环境灾害预警等核心业务包含地图热力图、实时趋势图、异常告警及公众反馈入口等交互形式。阅读源码目录可快速找到各功能模块对应的页面与脚本借助其中的HTML、CSS和JS文件还原图表渲染与数据展示逻辑也可基于现有结构扩展监测指标或调整界面是完成毕业设计文档与答辩演示的有力支撑。目前已有107人学习下载适合需要完整案例参考或希望二次开发环境监测原型的学习者。1. 拿到“柳州市生态环境动态监测系统.zip”之后先弄明白它在解决什么毕业设计清单里“柳州市生态环境动态监测系统”这个名字很讨巧——城市、环境、动态、监测四个词全占了。说穿了它就是一套带时间轴的Web数据系统后端按分钟级定时抓取空气、水质、噪声三类监测值存进MySQL再经ECharts画成大屏曲线和表格供值班人员看趋势、查异常。它的代码结构通常是一个Spring Boot单体应用加一个前端页面适合计算机类毕设、小型智慧城市课设也适合想做完整数据链路的转行者。真正该关心的问题是如何让演示数据产生真实感、如何解释清楚数据从哪来而不是把技术栈吹得多复杂。2. Spring Boot MyBatis-Plus ECharts这套组合为什么是生态环境类毕设的默认答案环境监测系统没有特别复杂的业务规则它的技术选型核心就一句话谁能最短时间把“数据进来、数据展示、异常提醒”这条链路跑通谁就是答案。Spring Boot负责把Web服务、定时任务、数据库访问揉在一起MyBatis-Plus解决单表读写时那些烦人的样板代码ECharts负责把时间序列画成让答辩老师一眼看懂的大屏。这套组合在生态环境、气象、能耗监测里几乎成了标配不是因为它最先进而是因为它最不容易翻车。2.1 采集-存储-展示先立住这条数据链路后面改代码才不会乱无论这个zip里面是三层架构还是更细的分层核心数据流只有四段采集、入库、查询、展示。采集段决定数据从哪来——真实传感器、公开接口、还是模拟生成入库段决定数据怎么存——按站点区分还是按指标区分查询段决定前端能以多快的速度拿到最近一小时或最近一天的曲线展示段决定用户看到的是表格、折线还是大屏。写答辩PPT时建议直接把这四段画成一条横向流程图每一段标注对应到项目里的哪个类、哪张表、哪个接口。这样做的好处是老师问“数据是怎么从采集端到页面的”时你能按顺序讲清楚而不是东扯一句代码、西扯一句配置。“动态”二字的落脚点就是这条链路能不能持续循环定时任务每五分钟写一次数据前端每三十秒拉一次最新数据曲线一直往右延这才叫动态监测。2.2 单体优先别在毕业设计里硬上微服务和前后端分离我在实际带课设时见过不少反面教材压测都没做过就拆成网关、注册中心、三个微服务最后演示时服务起不来在答辩现场对着黑匣子调了半天血泪经验。一个生态环境监测系统数据量顶天了也就是几万条监测记录单体能扛住微服务带来的服务发现、配置中心、链路追踪只会把毕业设计变成毕业事故。前后端分离也一样Vue工程还要装Node依赖、配代理、构建产物稍有版本冲突整个页面就白屏。所以遇到这类zip优先看它是不是Spring Boot单体加Thymeleaf或静态页面。如果是恭喜你这是最稳的组合如果是前后端分离也没关系只要把Vue的dist构建产物放到resources/static下让后端同时服务页面和接口演示时只需要启动一个Java进程即可。这个习惯我沿用至今验证工作量大减。2.3 拿到zip先看这四个位置而不是急着点运行很多同学解压后第一件事就是点启动类结果报错刷了满屏连项目结构都没看过。拿到这类毕业设计包我一般会先看四个位置第一是pom.xml确认Spring Boot版本与JDK版本是否匹配第二是resources/application.yml确认数据库账号密码和端口第三是sql目录或doc目录里的建库脚本确认表结构和初始化数据第四是static或templates目录确认前端资源是否完整。这四个位置看清楚了启动报错时你能立刻判断是环境问题还是代码问题而不是像无头苍蝇一样乱试。3. 建库建表三张核心表撑起整个监测业务数据库是这类系统最容易出彩也最容易露怯的地方。很多人的表是复制来的字段含义说不清楚索引也乱。其实环境监测系统只需要三张核心表站点表、监测数据表、预警记录表。把这三张表讲明白整个系统的地基就稳了后面写Service和Mapper只是体力活。3.1 为什么用一张指标表而不是给每种污染物建一张表见过有的设计把PM2.5一张表、噪声一张表、水质pH一张表看起来分类清晰实际上查询“某站点最近24小时所有指标”时要跨好几张表关联写起来痛苦不说新增一个监测指标就得新加一张表。更合理的设计是只建一张monitor_data表用data_type字段区分指标类型查询时按类型过滤即可。这样做的优势有三个新指标加一条枚举就好不用动表结构批次查询和跨指标对比只需要一条SQLMyBatis-Plus的LambdaQueryWrapper写起来也简洁。牺牲的代价仅仅是存储上一行多一个类型字段对毕业设计这种数据量来说完全可以忽略。3.2 建表SQL站点表、监测数据表、预警记录表CREATE TABLE monitor_station ( id int NOT NULL AUTO_INCREMENT COMMENT 主键, station_name varchar(64) NOT NULL COMMENT 站点名称如河东水厂, station_type varchar(10) NOT NULL COMMENT 监测类型air / water / noise, longitude decimal(10,6) DEFAULT NULL COMMENT 经度, latitude decimal(10,6) DEFAULT NULL COMMENT 纬度, region varchar(32) NOT NULL COMMENT 所属区域, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0停用, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT监测站点表; CREATE TABLE monitor_data ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, station_id int NOT NULL COMMENT 关联monitor_station.id, data_type varchar(10) NOT NULL COMMENT 指标类型AQI / PM2.5 / PH / 噪声分贝, data_value decimal(10,2) NOT NULL COMMENT 监测数值, collect_time datetime NOT NULL COMMENT 采集时间, PRIMARY KEY (id), KEY idx_station_time (station_id, collect_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT环境监测数据表; CREATE TABLE alarm_record ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, station_id int NOT NULL COMMENT 关联monitor_station.id, data_type varchar(10) NOT NULL COMMENT 指标类型, alarm_value decimal(10,2) NOT NULL COMMENT 触发告警时的数值, threshold decimal(10,2) NOT NULL COMMENT 告警阈值, alarm_time datetime NOT NULL COMMENT 告警发生时间, handle_status tinyint NOT NULL DEFAULT 0 COMMENT 0未处理 1已处理, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预警记录表;这三条SQL里最值得说的是monitor_data的联合索引idx_station_time。前端大屏最常见的请求是“查某个站点最近N分钟的数据”恰好命中这个索引的station_id和collect_time前缀查询速度会明显好于没有索引的全表扫描。alarm_record表没有加索引也可以跑但建议在alarm_time上加一个普通索引因为预警列表页通常按时间倒序分页。字段类型方面经纬度用decimal(10,6)而不是float避免地图展示时小数点被截断数值统一用decimal(10,2)兼顾精度和显示长度。3.3 初始化监测站点没有真实传感器也要让大屏有数据真实的环境监测系统靠传感器设备上报数据毕业设计没有这个条件所以大多数项目会提供一个数据初始化脚本往站点表里插入几个固定站点再靠后端的模拟生成器定时往monitor_data里写数。柳州市区的几个典型点位可以这么插INSERT INTO monitor_station (station_name, station_type, longitude, latitude, region, status) VALUES (三门江森林公园, air, 109.438000, 24.326000, 鱼峰区, 1), (阳和工业园, air, 109.421000, 24.361000, 柳东新区, 1), (河东水厂下游, water, 109.412000, 24.297000, 城中区, 1), (柳侯公园, noise, 109.413000, 24.318000, 城中区, 1);station_type字段决定了这个站点产生什么类型的数据air站点只生成AQI和PM2.5这类空气质量指标water站点生成pH和溶解氧noise站点生成分贝值。这样设计让后面定时任务写数据时逻辑非常直白——先查站点再按站点的类型生成对应指标。经纬度也不用太较真只要落在柳州市区范围内即可ECharts散点图只是示意位置不是精确的测绘定位。4. 定时采集与动态刷新把“动态”两个字落到代码上很多毕业设计其实活活做成了“静态监测系统”——页面打开时有一次数据之后一动不动。这显然是没把动态做进去。动态的来源有两个后端定时任务持续写入新数据前端定时轮询把这些新数据画到图上。这一章把这两段代码都写出来照着改就能让大屏活起来。4.1 Spring Task定时生成监测数据先解决数据从哪来Component public class MonitorDataTask { private static final Random RANDOM new Random(); Autowired private MonitorStationService stationService; Autowired private MonitorDataService dataService; Scheduled(cron 0 */1 * * * ?) public void generateAirData() { ListMonitorStation stations stationService.lambdaQuery() .eq(MonitorStation::getStationType, air) .eq(MonitorStation::getStatus, 1) .list(); for (MonitorStation station : stations) { MonitorData d new MonitorData(); d.setStationId(station.getId()); d.setDataType(AQI); d.setDataValue(Math.round((50 RANDOM.nextDouble() * 80) * 100.0) / 100.0); d.setCollectTime(new Date()); dataService.save(d); } } }这段代码解决的是“没有真实传感器时数据怎么持续进来”。Scheduled(cron 0 */1 * * * ?)表示每隔一分钟执行一次六个字段分别是秒、分、时、日、月、星期?表示不指定星期。演示时可以改成0 */30 * * * * ?每30秒跑一次让曲线动得更快答辩结束后改回5分钟一次更像真实轮询。逻辑上先查所有启用中的air站点再逐站生成一个AQI值保存值在50到130之间浮动模拟从优到轻度污染的范围。参数说明里最容易被忽略的是时区。new Date()拿到的是服务器当前时间如果MySQL连接串没指定时区存进datetime字段的数值会与北京时间差8小时。这个问题后面第5章会专门讲。4.2 Controller到Mapper查询最近N分钟监测数据的完整链路RestController RequestMapping(/api/monitor) public class MonitorController { Autowired private MonitorDataService dataService; GetMapping(/latest) public ResultListMonitorData latest( RequestParam Integer stationId, RequestParam(defaultValue 60) Integer minutes) { Date start new Date(System.currentTimeMillis() - minutes * 60 * 1000L); ListMonitorData list dataService.lambdaQuery() .eq(MonitorData::getStationId, stationId) .ge(MonitorData::getCollectTime, start) .orderByAsc(MonitorData::getCollectTime) .list(); return Result.ok(list); } }Controller接收两个参数stationId指定站点minutes指定回溯多少分钟默认60。Service层用MyBatis-Plus的lambdaQuery拼接查询条件逻辑上等价于SQL里的WHERE station_id ? AND collect_time ? ORDER BY collect_time ASC。注意这里的System.currentTimeMillis() - minutes * 60 * 1000L计算的是秒级时间戳用* 1000L而不是* 1000防止int溢出这是一个很容易被忽略的细节。返回值Result封装了code、message、data三件套这是这类Web项目最常见的统一返回结构。前端拿到data数组后直接按collect_time和data_value两个字段画图不需要额外处理。4.3 ECharts轮询刷新setInterval负责追加setOption负责更新let trendChart null; function loadTrend(stationId) { if (trendChart) { trendChart.dispose(); } trendChart echarts.init(document.getElementById(trendChart)); fetch(/api/monitor/latest?stationId${stationId}minutes60) .then(res res.json()) .then(data { const times data.data.map(d d.collectTime.replace(T, ).slice(0, 19)); const values data.data.map(d d.dataValue); trendChart.setOption({ xAxis: { data: times }, series: [{ name: AQI, type: line, data: values }] }); }); } setInterval(() { const stationId document.getElementById(stationSelect).value; fetch(/api/monitor/latest?stationId${stationId}minutes60) .then(res res.json()) .then(data { const times data.data.map(d d.collectTime.replace(T, ).slice(0, 19)); const values data.data.map(d d.dataValue); trendChart.setOption({ xAxis: { data: times }, series: [{ data: values }] }); }); }, 30000);这里的核心是setOption而不是重新init。很多人第一次写动态图表时每三秒调用一次echarts.init创建新实例最后页面上叠了十几个canvas又卡又糊。正确做法是init一次之后每次数据更新只调用setOptionECharts内部会做diff更新。第一次加载和定时刷新确实重复写了一段逻辑但毕业设计阶段优先保证能跑、能讲清楚不用急着抽象成公共函数。setInterval的间隔设成30秒与后端1分钟写一次错开这样每次刷新都能看到一两个新点演示效果刚刚好。如果把前端也改成1秒轮询后端数据没变曲线不动反而显得系统迟钝。4.4 application.yml里5个必调参数server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/liuzhou_environment?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true参数作用与调整建议server.port后端服务端口与前端请求地址保持一致常见8080或8081datasource.url数据库地址必须带characterEncoding和serverTimezone否则中文乱码、时间少8小时datasource.password改成你自己MySQL的密码这也是启动报错最多的一处spring.jackson.time-zone控制接口返回时间的时区设成GMT8避免前端显示UTC时间map-underscore-to-camel-case让collect_time自动映射成collectTime省去一大堆TableField注解5. 常见问题排查毕业设计演示现场最容易翻车的4个坑这一章写的是这几年帮人调项目总结出来的共性坑。每个坑都按“现象、原因、解决”三条写照着排查比漫无目的地搜报错快得多。5.1 JDK版本不对项目起来报javax.xml.bind缺失现象启动类一点运行控制台直接抛java.lang.ClassNotFoundException: javax.xml.bind.JAXBException或者提示模块不可用。原因Spring Boot 2.x内嵌的Tomcat较老默认依赖JDK8里的Java EE模块而JDK11以后这个模块被移除了。很多毕设项目的pom里没锁JDK版本你电脑装的是JDK17就会踩中。解决装一个JDK8并切换项目JDK或手动引入javax.xml.bind的依赖。我更推荐前者因为整个项目的Maven依赖很可能都是按JDK8选版的升JDK容易引发连锁报错。5.2 大屏中间地图空白控制台报geoJSON加载失败现象页面框架、曲线、表格都正常唯独中间的地图区域一片空白F12控制台显示某个geojson文件加载失败的请求记录。原因页面远程引用了CDN上的柳州市地图JSON演示现场没有资源访问环境或CDN域名已失效文件拉不下来。解决把地图JSON下载后放进resources/static/map/liuzhou.json注册时用相对路径访问fetch(/map/liuzhou.json) .then(res res.json()) .then(geoJson { echarts.registerMap(liuzhou, geoJson); chart.setOption({ geo: { map: liuzhou } }); });5.3 MySQL时间是UTC前端看到的趋势图永远慢8小时现象数据库表里明明有数据但页面曲线在图的最左边挤成一团或者最后几个点的时间比实际时间晚8小时。原因MySQL连接串里的serverTimezone没设置驱动默认按服务器时区UTC读取datetime字段。解决把连接串改成带serverTimezoneAsia/Shanghai的版本同时确认spring.jackson.time-zone: GMT8。两条都做后端入库和前端返回就都在东八区了。5.4 MyBatis-Plus分页失效表格多出一堆重复数据现象页面设置了pageSize10实际却把几十条记录全渲染出来了翻页按钮也表现异常。原因MyBatis-Plus的分页插件不是默认开启的需要注册PaginationInnerInterceptor。很多项目里配置类存在但没扫描到或者没写拦截器分页SQL就退化成普通list查询。解决检查是否有如下配置类Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }只要这个Bean生效分页参数立即正常。排查时可以在日志里看打印的SQL是否带LIMIT带上了就说明插件起作用了。6. 三个进阶技巧让大屏更像一套真实运行的环境监测系统6.1 造数函数按日趋势加随机扰动别用纯Random真实环境数据有昼夜规律凌晨颗粒物浓度高、午后略降。用纯随机数生成的曲线像心电图一眼假。改进方式是先算出基准值再加高斯扰动private double buildAqi(int hour) { double base 60 25 * Math.sin(hour * Math.PI / 12); return Math.max(20, base RANDOM.nextGaussian() * 6); }hour取当前时间的小时值Math.sin模拟一天内先升后降的波形nextGaussian()让数据围绕基准波动。这样生成的AQI曲线在视觉上就有“白天低、晚间抬升”的真实感答辩时也更好解释数据生成策略。6.2 空气质量指数联动颜色阈值const LEVEL_COLOR { 优: #00e400, 良: #ffff00, 轻度污染: #ff7e00, 中度污染: #ff0000 };根据AQI值映射等级再把这个颜色赋给折线的lineStyle或散点的itemStyle污染时段一眼就能看出来。这块代码量不大但对大屏的演示冲击力提升非常明显属于投入产出比很高的一步。6.3 切走页面再回来时手动刷新图表演示时常常会遇到切到别的窗口再切回来的场景此时定时器可能被浏览器节流曲线停在切换前的状态。处理办法是监听页面可见性变化回到页面时主动拉一次数据document.addEventListener(visibilitychange, () { if (!document.hidden) { loadTrend(currentStationId); } });这个小细节不一定会被问到但讲师演示时来回切换窗口发现图表依然实时更新会下意识觉得这个系统是“活”的。这些年我带过的毕设里凡是把这三件事做进去的答辩时基本都能多扛住三四个追问。我自己的习惯是把模拟数据生成器放在定时任务包里单独留个开关每次改完代码先手动执行一次造数脚本再刷新页面验证整套自测十分钟内能打完。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网