ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

SpringBoot+Vue+WebSocket森林火灾预警系统:实时推送与部署实践

SpringBoot+Vue+WebSocket森林火灾预警系统:实时推送与部署实践 简介面向计算机相关专业毕业设计需求整合SpringBoot、Vue、MySQL及WebSocket技术的森林火灾预警系统源码包。系统内置园区数据管理、温度实时监测、园区监控等模块从前端页面到后端接口形成完整闭环适合作为前后端分离项目的设计参考或二次开发基础。压缩包共396个文件体积约7.39MB涵盖Java编译后的class文件、前端JS与CSS样式、SQL数据库脚本、服务配置以及启动/停止批处理脚本目录组织贴近实际工程方便快速部署运行。随包附有说明材料与完整源代码可协助理解WebSocket长连接实时推送、SpringBoot分层接口开发、Vue组件化页面构建等核心知识点也能直接用于演示或毕业答辩。已有267人学习适合正在准备毕业设计、需要可运行项目源码或希望系统梳理SpringBootVue技术栈的开发者。1. 森林火灾预警为什么必须用 WebSocket 而不是轮询园区防火最怕的不是发现火情而是发现得太晚。传统 HTTP 轮询每 5 秒拉一次温度接口数据量不大时没问题但监控点一多请求堆积、服务器连接数暴涨、前端大屏刷新错位问题全来了。这个基于 SpringBoot Vue MySQL 的森林火灾预警系统把园区数据、温度采集和监控视频整合成一张实时大屏底层用 WebSocket 做服务端主动推送温度变化和烟雾告警可以在几百毫秒内到达浏览器。对于做毕业设计或者接手园区监控改造的人来说它的价值在于把「前后端分离 长连接实时通信 关系型数据落库」这条链路完整跑通而且项目里自带启动脚本和说明材料省掉了很多环境配置上的折腾。下面从数据模型、后端消息推送、前端实时渲染、部署排错四条线拆开讲。2. SpringBoot WebSocket MySQL 的数据模型与消息设计2.1 园区监控场景下的数据库表结构预警系统的核心不是「存数据」而是「存完数据之后还能快速查出趋势和触发告警」。参考这个项目的常见组织方式MySQL 里一般会建四张核心表园区表、监测点表、预警记录表、监控设备表。监测点表是中心它同时关联园区和设备。建表脚本通常是这样的毕业设计答辩时也经常被问到字段设计原因CREATE TABLE park ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 园区ID, park_name VARCHAR(64) NOT NULL COMMENT 园区名称, area DOUBLE COMMENT 园区面积(亩), address VARCHAR(128) COMMENT 园区地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE monitor_point ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 监测点ID, park_id BIGINT NOT NULL COMMENT 所属园区ID, point_name VARCHAR(64) NOT NULL COMMENT 监测点名称, temperature FLOAT COMMENT 当前温度(℃), humidity FLOAT COMMENT 当前湿度(%), smoke_value INT COMMENT 烟雾浓度(ppb), warn_threshold FLOAT DEFAULT 60.0 COMMENT 温度报警阈值(℃), status TINYINT DEFAULT 0 COMMENT 0正常 1预警 2告警, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_park_id (park_id), CONSTRAINT fk_monitor_park FOREIGN KEY (park_id) REFERENCES park(id) ); CREATE TABLE alarm_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, point_id BIGINT NOT NULL, alarm_type VARCHAR(16) COMMENT temp/smoke, alarm_level TINYINT COMMENT 1预警 2告警, alarm_value FLOAT, alarm_time DATETIME DEFAULT CURRENT_TIMESTAMP, handler VARCHAR(32) COMMENT 处理人, status TINYINT DEFAULT 0 COMMENT 0未处理 1已处理 ); CREATE TABLE camera ( id BIGINT PRIMARY KEY AUTO_INCREMENT, point_id BIGINT NOT NULL, camera_name VARCHAR(64), stream_url VARCHAR(255) COMMENT HLS/RTSP流地址, enabled TINYINT DEFAULT 1 );这段 SQL 要注意几个点monitor_point里直接冗余了temperature和humidity字段这是为了大屏展示时减少一次实时查询warn_threshold放在监测点上而不是全局统一因为不同区域对温度敏感度不同alarm_record用alarm_type区分是温度触发还是烟雾触发后续扩展气体传感器时不用改表结构。2.2 实时监测数据的消息协议设计数据库解决的是「历史查询」实时推送需要单独设计 WebSocket 消息格式。常见的做法是统一使用 JSON 消息用type字段区分业务动作避免前端收到一条数据还要猜含义。我一般会这样定义{ type: MONITOR_DATA, data: { pointId: 1, parkId: 1, temperature: 58.3, humidity: 21.5, smokeValue: 320, status: 1, timestamp: 2026-05-18T10:23:45 } }如果触发报警消息结构扩一个alarm对象携带报警级别和建议动作。这样设计的好处是 WebSocket 只管透传业务判断放在后端 Service 层前端拿到数据后只做渲染不参与「要不要报警」的决策。对于像 Vue 这样的前端框架来说解析 JSON 然后更新响应式数据非常直接不会因为消息格式不统一而出现undefined报错。2.3 消息类型枚举与 Spring 层的映射后端如果用枚举来管理消息类型后面扩展会省很多事。项目里常见的是在entity模块里建一个MessageType枚举public enum MessageType { MONITOR_DATA(MONITOR_DATA, 实时监测数据), ALARM_PUSH(ALARM_PUSH, 报警推送), HEART_BEAT(HEART_BEAT, 心跳), CONNECT_CONFIRM(CONNECT_CONFIRM, 连接确认); private final String code; private final String desc; MessageType(String code, String desc) { this.code code; this.desc desc; } public String getCode() { return code; } public String getDesc() { return desc; } }注意枚举的code必须和前端约定的字符串完全一致否则前端switch会落到 default 分支。这个项目里所有实体和控制器都被拆成了独立模块entity、controller、mpController各司其职改消息协议时只需要动枚举和对应的 DTO不会牵连到前端页面。3. 后端实现SpringBoot WebSocket 端点与报警推送3.1 基于 ServerEndpoint 的 WebSocket 服务端SpringBoot 集成 WebSocket 有几种方式最直接的是用ServerEndpoint配合ServerEndpointExporter。这个项目里的实现思路就是先注册端点再写一个处理类。常见代码如下import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; Configuration public class WebSocketConfig { Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; ServerEndpoint(/ws/monitor/{parkId}) Component public class MonitorWebSocketServer { private static final ConcurrentHashMapString, Session SESSION_MAP new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(parkId) String parkId) { String key parkId _ session.getId(); SESSION_MAP.put(key, session); session.getAsyncRemote().sendText({\type\:\CONNECT_CONFIRM\,\data\:\连接成功\}); } OnClose public void onClose(Session session, PathParam(parkId) String parkId) { String key parkId _ session.getId(); SESSION_MAP.remove(key); } OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } public static void sendToPark(String parkId, String message) { SESSION_MAP.forEach((key, session) - { if (key.startsWith(parkId _)) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } }); } }这段代码有两点需要解释。第一SESSION_MAP用parkId sessionId作为 key这样同一个园区可以挂多个浏览器页面比如监控大屏和管理员后台同时在看推送时按园区维度广播第二onOpen里发送的连接确认消息很重要前端只有在收到CONNECT_CONFIRM后才认为连接可用否则就要重连。如果只调用异步发送而不检查返回值断线情况发生后前端不会第一时间感知到。3.2 温度检测与报警判断的业务逻辑WebSocket 端点只是「管道」真正的业务判断要放到 Service 层。项目的mpController和entity模块拆得很细这种分层方式可以保证报警逻辑单独写不污染控制器。常用实现是定时任务或模拟数据线程每秒更新一次监测点温度然后判断是否超过阈值Service public class MonitorService { Autowired private MonitorPointMapper monitorPointMapper; Autowired private AlarmRecordMapper alarmRecordMapper; public void handleSensorData(MonitorPoint point) { // 更新监测点当前温度 monitorPointMapper.updateTemp(point.getId(), point.getTemperature()); if (point.getTemperature() point.getWarnThreshold()) { AlarmRecord alarm new AlarmRecord(); alarm.setPointId(point.getId()); alarm.setAlarmType(temp); alarm.setAlarmValue(point.getTemperature()); alarm.setAlarmLevel(point.getTemperature() point.getWarnThreshold() 10 ? 2 : 1); alarmRecordMapper.insert(alarm); // 构造 WebSocket 推送消息 String message String.format( {\type\:\ALARM_PUSH\,\data\:{\pointId\:%d,\temperature\:%.1f,\warnThreshold\:%.1f}}, point.getId(), point.getTemperature(), point.getWarnThreshold() ); MonitorWebSocketServer.sendToPark(String.valueOf(point.getParkId()), message); } } }这里有一个容易出现理解偏差的地方连续推送。假设温度一直超过阈值如果每次都插入预警记录并推送前端会收到大量重复报警。常见的做法是「只在状态从正常变为预警时推送一次」后续推送频率降低到每 30 秒一次或者在前端加一个「报警静默时间」。项目里monitor_point表设计了status字段就是用来记录当前状态的判断逻辑应该是status ! 1且温度超阈值时才插入预警记录然后把status置为 1。3.3 REST 接口与 WebSocket 如何互补WebSocket 负责「推」但历史数据、园区列表、监控视频地址这些低频数据还是要走 SpringBoot 的 REST 接口。这个项目里mpController就是专门负责管理后台接口的。常用接口划分如下接口路径方法作用返回数据/api/park/listGET获取园区列表park 表全部字段/api/monitor/currentGET查询所有监测点最新数据温度、湿度、烟雾、状态/api/monitor/history?pointId1GET查询指定监测点历史温度按时间升序的温度数组/api/alarm/listGET查看预警记录报警时间、级别、处理状态这个划分的依据是实时性等级。WebSocket 只承载秒级更新的监测数据和报警推送其余全部走 HTTP这样即便 WebSocket 偶发断线重连历史记录查询和园区信息展示也不会受影响。前端拿到 REST 数据后做首屏渲染再通过 WebSocket 增量更新当前温度这个模式在监控类系统里非常稳定。4. Vue 前端实时告警大屏与 WebSocket 连接管理4.1 WebSocket 客户端封装与断线重连Vue 项目里直接在每个组件里new WebSocket()是灾难页面切换一次就多一条连接服务端SESSION_MAP里就会积累一堆僵尸连接。常见的做法是把 WebSocket 封装成单例模块项目中的前端一般是src/utils/websocket.jsclass WebSocketClient { constructor() { this.ws null; this.heartBeatTimer null; this.reconnectTimer null; this.isManualClose false; } connect(parkId) { this.isManualClose false; const protocol location.protocol https: ? wss : ws; const url ${protocol}://${location.host}/ws/monitor/${parkId}; this.ws new WebSocket(url); this.ws.onopen () { console.log(websocket connected); this.startHeartBeat(); }; this.ws.onmessage (event) { const msg JSON.parse(event.data); if (msg.type MONITOR_DATA || msg.type ALARM_PUSH) { window.dispatchEvent(new CustomEvent(ws-message, { detail: msg })); } }; this.ws.onclose () { if (!this.isManualClose) { this.reconnect(parkId); } }; this.ws.onerror (err) { // 这里不要立刻重连等 onclose 触发 console.error(websocket error, err); }; } startHeartBeat() { this.heartBeatTimer setInterval(() { if (this.ws.readyState WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: HEART_BEAT, timestamp: Date.now() })); } }, 20000); } reconnect(parkId) { clearTimeout(this.reconnectTimer); this.reconnectTimer setTimeout(() { console.log(websocket reconnect); this.connect(parkId); }, 3000); } close() { this.isManualClose true; clearInterval(this.heartBeatTimer); this.ws.close(); } } export const wsClient new WebSocketClient();这段封装做了三件关键事用CustomEvent把消息抛给全局组件里只需要监听ws-message事件心跳间隔 20 秒防止 Nginx 或云平台把空闲连接切掉断线后延迟 3 秒重连并配合指数退避改造比如连续失败时把间隔拉长到 10 秒。很多同学写前端 WebSocket 只处理 onmessage不处理 onerror 和 onclose连上几分钟就断了基本都是因为没有心跳和重连。4.2 大屏数据渲染与动态温度曲线园区大屏通常包括三块左侧园区温度列表、中间视频监控、底部烟雾浓度趋势。Vue 组件里监听ws-message后更新数据即可。核心代码参考如下template div classmonitor-dashboard el-row :gutter12 el-col :span8 div v-forpoint in points :keypoint.id classtemp-card span{{ point.pointName }}/span span :classpoint.status 2 ? temp-danger : temp-normal {{ point.temperature }}℃ /span /div /el-col el-col :span16 div refchart styleheight: 300px;/div /el-col /el-row /div /template script import * as echarts from echarts; import { wsClient } from /utils/websocket; export default { data() { return { points: [], chart: null, tempHistory: [] }; }, mounted() { this.initChart(); wsClient.connect(1); window.addEventListener(ws-message, this.handleWsMessage); }, beforeDestroy() { wsClient.close(); window.removeEventListener(ws-message, this.handleWsMessage); }, methods: { initChart() { this.chart echarts.init(this.$refs.chart); this.chart.setOption({ xAxis: { type: time }, yAxis: { type: value, name: 温度(℃) }, series: [{ type: line, data: [] }] }); }, handleWsMessage(event) { const msg event.detail; if (msg.type MONITOR_DATA) { const d msg.data; const idx this.points.findIndex(p p.id d.pointId); if (idx ! -1) { this.$set(this.points, idx, { ...this.points[idx], temperature: d.temperature, status: d.status }); } // 更新折线图 this.tempHistory.push([d.timestamp, d.temperature]); if (this.tempHistory.length 100) this.tempHistory.shift(); this.chart.setOption({ series: [{ data: this.tempHistory }] }); } } } }; /script注意this.$set的使用。Vue 2 里直接通过下标修改数组项不会触发视图更新这是新手最容易踩的坑。如果用 Vue 3改成reactive数组并用splice替换就没这个问题。温度曲线如果把数据全部塞进图表内存会越涨越高所以这里限制了最多展示最近 100 个点实际项目里可以改成按时间窗口滑动。4.3 Vue 路由与监控模块页面划分这个预警系统的前端是多页面结构路由可以按模块拆成园区总览、实时监控、预警管理、系统设置四块。Vue Router 配置如下const routes [ { path: /, redirect: /overview }, { path: /overview, component: () import(/views/Overview.vue), meta: { title: 园区总览 } }, { path: /monitor, component: () import(/views/Monitor.vue), meta: { title: 实时监控 } }, { path: /alarm, component: () import(/views/Alarm.vue), meta: { title: 预警管理 } }, { path: /setting, component: () import(/views/Setting.vue), meta: { title: 系统设置 } } ];路由做懒加载是为了让首屏更快。需要注意的是WebSocket 连接不应该放在App.vue里一启动就连因为如果用户没有进入监控页面会出现大量无效连接。常见的做法是在Monitor.vue的mounted里连接beforeDestroy里关闭这样只有打开监控页才会占用连接。5. 部署启动、数据库初始化和 QL1006 排查5.1 利用启动脚本跑通前后端项目压缩包里自带了启动.bat和停止.bat这是给本地开发环境准备的。常见的启动.bat内容类似echo off chcp 65001 nul echo 正在启动 MySQL 服务... net start mysql || echo MySQL 已启动 echo 启动后端 SpringBoot 服务... start fire-warning-backend java -jar backend.jar --spring.profiles.activeprod echo 启动前端 Nginx 服务... start nginx nginx.exe -p .\nginx-1.24.0 echo 系统启动完成 pause这里不推荐用echo off后就什么都不管至少需要检查端口占用和启动日志。脚本里最容易被忽略的是 MySQL 的字符集问题。如果tree.btl和table.btl里包含了建库语句通常需要使用source或直接导入 MySQL 客户端执行下面是标准初始化步骤mysql -uroot -p --default-character-setutf8mb4 init.sql也可以用命令行进入 MySQL 后执行source C:/fire-warning/sql/init.sql。初始化完成后打开浏览器访问http://localhost:8080后端端口如果是 8080前端静态资源由 Nginx 托管需要反向代理/api和/ws到后端。5.2 Nginx 反向代理 WebSocket 的 60 秒超时问题部署到云服务器后最常见的现象是页面能打开温度也能显示但过一会儿 WebSocket 就断开浏览器控制台里出现[websocket] onclose, code: 1006甚至显示stream disconnected before completion: failed to send websocket request。其中 1006 表示连接被异常关闭没有收到关闭帧绝大多数原因是 Nginx 默认在 60 秒无活动后切断空闲连接。解决方案是在 Nginx 的 location 配置里显式调整超时location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }这里proxy_set_header Connection upgrade必须写成固定字符串不能用$connection_upgrade映射变量否则部分 Nginx 版本下握手会失败。proxy_read_timeout调大到 3600 秒后服务端再配合心跳包连接基本能保持一整天。如果调整后仍然频繁 1006检查一下是不是云服务商的安全组把 8080 端口做了限制或者防火墙对长连接做了空闲回收。最后一个排查技巧是后端onClose里打印 session id 和 parkId再用netstat -an | findstr 8080看 ESTABLISHED 状态连接数是否异常如果大量 TIME_WAIT 说明不是超时问题而是服务端主动关闭了连接。本文还有配套的精品资源点击获取
返回列表