资讯详情

资讯详情

智慧农业集成管理系统实践:设备接入、阈值告警与可视化大屏

简介这是一套面向农业信息化开发者及物联网应用方向的智慧农业集成管理系统源码涵盖物联网监测、大数据分析、设备控制、地块管理、农事计划等典型业务。项目基于Java Web技术栈采用分层开发模式实体、接口、控制层与前端页面配合清晰适合作为课程设计、毕业设计或企业二次开发的参考底座。资源共224个文件核心为142个Java类、30个HTML页面、20个JavaScript文件并辅以XML配置、CSS样式、SQL数据库脚本及工程化配置文件压缩包仅476KB轻量易部署便于快速导入主流IDE。目前已有174人学习下载。从内容预览可看到登录认证、传感器档案、设备管理、计划管理、地块管理等页面配合后端Java逻辑与数据库脚本可完整展示业务数据的流转过程。对于希望梳理智慧农业系统架构、学习MVC分层和前后端交互的开发者这份工程提供了一条简洁的学习路径。1. 智慧农业集成管理系统给基地装一套会告警的数字底座接手农业信息化项目时我一开始以为工作重心是那张数据大屏真正做完才意识到稳定接入几百个传感器点位、把告警准确送达农户手里才是硬骨头。这套智慧农业集成管理系统把设备接入、数据管理、阈值告警、可视化大屏和农事任务串成一个开箱即用的闭环后端基于 Spring Boot前端 Vue自带初始化数据库与接口文档。适合两类人一是想直接部署到基地或园区的项目负责人二是想拿一套干净工程做二次开发的开发者。接下来我按“架构 → 核心代码 → 跑通 → 改造”的顺序拆给你看本地跑通大约半小时。2. 技术架构与工程结构从传感器点位到前端大屏的完整链路先看整体逻辑。这套系统的数据流可以分成五层感知层的温湿度、光照和土壤墒情传感器负责采集现场指标接入层接收传感器上报的数据并做格式校验服务层的规则引擎对每一个指标做阈值判断命中后生成告警存储层用 MySQL 保存业务数据Redis 承担实时点位缓存最上面是 Vue 管理后台和可视化大屏。2.1 整体分层与数据流五层结构我用一条链路串起来顺序如下感知层大棚里的空气温湿度、二氧化碳、光照度传感器按固定频率上报。接入层统一暴露一个写入接口兼容 HTTP JSON 和 MQTT 两种上报方式先落 Redis 再异步落库。服务层设备状态维护、指标归档、规则引擎判定、告警生成与通知。存储层业务表走 MySQL实时点位走 Redis历史趋势可以平滑替换为时序库。应用层后台管理加大屏展示外加移动端适配的 H5 页面。常见做法是给规则引擎单独留一个模块方便后面扩展阈值、策略和通知渠道。这套工程把设备接入和业务服务拆在不同的包路径下边界拖得比较清楚。后面加新设备类型时不用碰告警逻辑这是我当时拆包最看重的一点。2.2 目录结构下载包里每个模块负责什么拆开工程后是典型的 Maven 多模块结构。对以前只写过单模块工程的人来说这套骨架需要先认路否则容易把代码塞错地方。agri-platform/ ├── agri-common/ # 公共模块统一返回值、异常、工具类 ├── agri-admin/ # 后台管理登录、用户、角色、菜单权限 ├── agri-iot/ # 设备接入上报接口、协议解析、设备状态 ├── agri-service/ # 业务核心告警规则、告警记录、农事任务、报表 ├── agri-web/ # 前端工程Vue3 Element Plus ├── sql/ # 初始化脚本建库、建表、种子数据 └── docs/ # 设计文档与接口说明我一般拿到这种结构先看 agri-iot 里的 controller 和 agri-service 里的 rule 包这两个包是整套系统的业务咽喉。agri-common 里的返回结构则决定了前端 axios 拦截器怎么写。顺手提一句前端里并没有把大屏单独拆成微应用而是放在 views/screen 目录下复用同一套接口和权限。对中小型农业项目来说这个取舍合理省去跨应用鉴权的问题。2.3 数据库模型设备、点位与告警的关联设计初始化脚本里影响全局的是 device 和 alarm_record 两张表业务字段设计大致如下。CREATE TABLE device ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, farm_id BIGINT NOT NULL COMMENT 所属基地, device_code VARCHAR(32) NOT NULL COMMENT 设备编号对接时用, device_name VARCHAR(64) DEFAULT NULL COMMENT 设备名称, longitude DECIMAL(10,6) DEFAULT NULL COMMENT 经度, latitude DECIMAL(10,6) DEFAULT NULL COMMENT 纬度, status TINYINT DEFAULT 0 COMMENT 0离线 1在线, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_device_code (device_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT设备表;设备表的关键点是 device_code上报接口只认它不认自增 id这样设备被重新登记后不需要改上报固件。farm_id 则是权限隔离的边界后台用户只能看到自己所属基地的设备。CREATE TABLE alarm_record ( id BIGINT NOT NULL AUTO_INCREMENT, device_id BIGINT NOT NULL COMMENT 设备id, metric_code VARCHAR(16) NOT NULL COMMENT 指标编码如 temp/humi, metric_value DECIMAL(10,2) DEFAULT NULL COMMENT 触发时监测值, threshold_value DECIMAL(10,2) DEFAULT NULL COMMENT 对应阈值, alarm_level TINYINT DEFAULT 2 COMMENT 1提示 2紧急, status TINYINT DEFAULT 0 COMMENT 0未处理 1已确认, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_device_created (device_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT告警记录表;告警表是一个典型的宽表设计把触发值、阈值、级别都冗余进来历史告警查询就不需要回查设备快照。metric_code 与阈值配置表联动实现同一指标对不同基地采用不同阈值的灵活度。2.4 前端结构管理后台与大屏的页面划分前端工程主要目录及职责如下。路径职责关键依赖views/login登录页表单校验、验证码views/device设备管理表格、状态切换、点位配置views/monitor实时监控地图打点、点位卡片views/alarm告警中心列表筛选、详情抽屉views/screen数据大屏ECharts 图表、WebSocketviews/task农事任务待办、日历路由的默认排序是先看到监控再看到告警实际使用中告警中心的打开频率最高。我在改造时把告警菜单提到了第二位纯属产品取舍不影响功能。前端和后端接口走 /api 前缀生产环境交给 nginx 反代本地开发则用 vite proxy 转发。3. 核心模块实战一个告警事件从采集到上屏的完整链路这一章是全文的重头戏我拧出告警这条主线来走读。流程是设备上报数据 → 服务层防抖去重 → 规则引擎判定 → 写告警记录 → WebSocket 推给前端 → 前端弹通知并更新列表。每一步我都贴出关键代码和参数含义。3.1 设备数据上报统一入口与防抖先看接入层。这套系统把上报接口设计成单入口所有设备的数据都走同一个 POST 接口。RestController RequestMapping(/api/v1/device) public class DeviceDataController { Autowired private DeviceDataService dataService; PostMapping(/report) public ResultVoid report(RequestBody DeviceReportDTO dto) { dataService.processReport(dto); return Result.ok(); } }DeviceReportDTO 的典型字段是deviceCode设备编号、reportTime上报时间、metrics指标数组。指标数组里每个元素包含 metricCode如 temp、value当前读数。接口只做接收和校验真正的业务逻辑全在 processReport 里。public void processReport(DeviceReportDTO dto) { Device device deviceMapper.findByCode(dto.getDeviceCode()); if (device null || device.getStatus() 0) { log.warn(设备不可用: {}, dto.getDeviceCode()); return; } for (MetricValue mv : dto.getMetrics()) { // 指标数据先放入 Redis 实时缓存再异步写入历史表 metricService.saveRealtime(device.getId(), mv); // 立即触发规则判定不等待批量任务 ruleMatcher.evaluate(device, mv); } }这段代码里 saveRealtime 和 evaluate 是顺序执行的先更新缓存点位再走规则引擎。这样如果规则引擎判定出告警前端大屏拉取实时值时拿到的已经是最新数据不会出现大屏上温度 35 度、告警却还在判断 34 度的情况。设备离线时直接丢弃本次上报避免脏数据污染告警统计。3.2 规则引擎阈值判定与窗口消抖告警模块最见功力的地方是“不误报”。默认实现用的是阈值加连续窗口计数也就是连续 N 次超阈值才真正触发告警单次抖动不上报。Component public class RuleMatcher { Resource private ThresholdConfigCache configCache; Resource private AlarmService alarmService; Resource private MetricCounter counter; public void evaluate(Device device, MetricValue mv) { // 按基地 指标编码拿阈值配置 ThresholdConfig config configCache.get(device.getFarmId(), mv.getMetricCode()); if (config null) { return; } boolean hit mv.getValue() config.getMaxValue() || mv.getValue() config.getMinValue(); int times counter.incrementAndGet(device.getId(), mv.getMetricCode()); if (hit times config.getWindowCount()) { alarmService.create(device, mv, config); counter.reset(device.getId(), mv.getMetricCode()); } else if (!hit) { // 恢复正常后及时归零避免误累计 counter.reset(device.getId(), mv.getMetricCode()); } } }几个参数值得展开。maxValue 和 minValue 是上下限阈值windowCount 是连续触发次数比如配置成 3表示上报 3 次都超阈值才告警间隔默认取上报频率。实际项目中可以将上报频率和 windowCount 联动比如 5 分钟一次上报配 windowCount3意味着持续超限 15 分钟才会告警。对大棚夜间的温度波动来说这个设置能滤掉绝大多数瞬间尖峰而不是靠降低阈值灵敏度来避免误报。alarmService.create 内部做了两件事先插一条 alarm_record再调用告警推送组件广播消息。3.3 WebSocket 实时推送让告警秒上大屏告警产生后需要立即推给前端这里用的是标准 WebSocket 端点。ServerEndpoint(/ws/alarm) Component public class AlarmWebSocket { private static final CopyOnWriteArraySetSession SESSIONS new CopyOnWriteArraySet(); OnOpen public void onOpen(Session session) { SESSIONS.add(session); } OnClose public void onClose(Session session) { SESSIONS.remove(session); } public static void push(String json) { for (Session session : SESSIONS) { try { session.getBasicRemote().sendText(json); } catch (IOException e) { SESSIONS.remove(session); } } } }这是最直接的全量广播实现适合中小型项目。每个浏览器标签页连上来都注册一个 session后端有告警就遍历广播。要注意的是这个端点没有做浏览器端用户鉴权接入层需要校验请求头里的 token。实际生产我一般会在 WebSocket 握手拦截器里校验用户身份并给 session 绑定 userId推送时只推给对应用户所在的基地。心跳是另一个容易翻车的点。浏览器与后端之间有 nginx 或负载均衡时空闲连接会被自动断开。常见做法是前端每隔 30 秒发一个 ping后端收到后原样回 pong。这个工程里没有默认实现我在改造时补上了后面避坑章节会细说。3.4 前端消费弹窗通知与告警列表更新前端接告警流比较简单下面是一段实际页面里使用的逻辑。// 拼接 WebSocket 地址兼容 http/https const protocol location.protocol https: ? wss : ws const ws new WebSocket(${protocol}://${location.host}/ws/alarm) ws.onmessage (event) { const alarm JSON.parse(event.data) // 紧急告警弹提示框普通告警只更新角标 if (alarm.level 2) { ElNotification({ title: 紧急告警, message: ${alarm.deviceName} ${alarm.metricName} 当前值 ${alarm.value}, type: error, duration: 0 }) } alarmStore.refresh() } ws.onclose () { // 断线后 5 秒重连同时开启轮询兜底 setTimeout(initWs, 5000) }这里有两个细节。第一location.host 直接复用当前域名端口后端和前端在同一域名下部署时不需要硬编码地址换环境不用改代码。第二onclose 后只靠重连是不够的因为断线期间产生的告警会丢我在断线检测时同时调一次后端列表接口用增量拉取补齐缺失告警。这套组合在弱网环境下表现比纯 WebSocket 可靠得多。4. 部署运行从本地起服务到发布上线的完整步骤前面代码读得差不多了这一章直接落到实际操作。我按“装环境 → 初始化数据库 → 起后端 → 起前端”的顺序写每步附带常见参数解释。4.1 环境准备与初始化工程依赖不算复杂MySQL 5.7 或 8.0、Redis 6 以上、JDK 8 或 17、Node 16 以上基本够用。如果本机不想装 MySQL用 docker 起一个实例也快但是要注意时区和字符集见后面避坑章节。数据库初始化用项目自带的脚本mysql -uroot -p sql/init.sql这个脚本会建库建表并写入默认用户、默认基地和默认阈值配置。建议执行前先浏览一遍脚本里的密码和字符集设置。遇到报错时看具体行号多半是数据表已经存在可以手动删除旧库再执行。4.2 后端启动与关键配置后端工程根目录执行打包和启动命令。第一次跑 Maven 会下载依赖时间取决于网络中途不要 CtrlC容易留下残缺的本地仓库。mvn clean package -DskipTests java -jar agri-admin/target/agri-admin.jar配置文件在 agri-admin 的 application.yml 中改以下四个位置就能跑通本机环境。spring: datasource: url: jdbc:mysql://localhost:3306/agri?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver data: redis: host: localhost port: 6379 server: port: 8080如果 MySQL 和 Redis 都在本机配置基本不用动只改数据库密码。url 里的两个参数值得解释characterEncodingutf8 保证中文不乱码serverTimezoneAsia/Shanghai 避免 JDBC 驱动把时间错位 8 小时。这两项不写告警记录的时间戳会在前后端各偏差一次排查时非常玄学。4.3 前端启动与代理配置前端是标准的 npm 工程安装依赖后启动开发服务器。cd agri-web npm install npm run devnpm install 在旧版本 Node 下会报 engine 警告不用管它。前端开发环境访问接口需要经 vite 代理转发到后端配置文件 vite.config.js 中核心配置如下。server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true }, /ws: { target: ws://localhost:8080, ws: true } } }注意 /ws 也需要代理且要设置 ws: true。不少人后端起了、前端页面开了但告警就是不弹窗顺着这个代理看一圈就能发现原因。生产环境不依赖 vite 代理直接把前端构建产物放到 nginx 的 html 目录并配置 /api 和 /ws 反向代理即可。4.4 账号与默认数据验证初始化脚本里预置了一个管理员账号admin / admin123。登录后能看到一个默认基地和一批虚拟设备这些虚拟设备带定时上报的埋点页面上的温度曲线会自己动。如果大屏没有数据优先看 Redis 里有没有实时点位 key而不是直接怀疑 SQL 写错。redis-cli keys *realtime*出现带设备 id 的 hash key 说明数据在正常写入。如果 key 存在但大屏仍空白再看前端请求是否被跨域或者代理拦截抓一下浏览器 Network 标签就能定位。提示后端启动日志里如果刷 Redis 连接错误进程通常不会退出但第一个请求来临时会抛异常。养成启动后立刻调用一次健康检查接口的习惯比盯着启动日志更可靠。5. 避坑与排查启动、联动、告警丢失的踩坑记录这套系统我在本地和服务器上各跑过一遍踩坑基本集中启动阶段和告警联动阶段。下面按现象、原因、解决的顺序记录看完能少走不少弯路。5.1 启动阶段数据库连接失败的两种典型情况现象后端启动时 Maven 打包成功但 java -jar 后几秒抛异常提示 Access denied for user。很多人第一反应是密码写错了改完密码还报错。原因初始化脚本里的默认用户名可能是 agri_admin而 application.yml 里写的是 root。两边不一致驱动连接时自然被拒绝。解决优先用脚本里创建的业务账号而不是改 root 密码。在 mysql 命令行里执行select user, host from mysql.user确认账号存在再回到 yml 配置。如果用的是云数据库还要确认目标库 IP 白名单是否放行了本机出口 IP。现象另一类报错是 Public Key Retrieval is not allowedMySQL 8 驱动在非 SSL 连接下偶发。原因MySQL 8 默认使用 caching_sha2_password 认证JDBC 驱动首次连接需要取公钥url 里少了允许获取公钥的参数。解决在 jdbc url 末尾追加 allowPublicKeyRetrievaltrueuseSSLfalse。这个参数不影响原有逻辑属于驱动握手阶段的配置加了就通畅。5.2 联动阶段告警不推送与数据错位现象告警记录在后端日志里有列表接口也能查到但浏览器迟迟不弹通知。原因前端 WebSocket 建的地址是 ws://localhost:8080而后端接口要走 vite 代理。直连后端会碰到端口跨域连接建立失败前端只是静默记录错误不会主动告诉你握手失败。解决前端统一使用 location.host 拼接 WebSocket 地址开发环境代理里务必写ws: true。注意 vite 的 /ws 代理 target 要写 ws://localhost:8080 而不是 http://否则握手协议不对。现象数据库中告警时间正确前端展示却慢 8 小时。原因JDBC 连接串缺了 serverTimezone 参数驱动按服务器默认时区读取时间而前后端又各自做了一次解析时间就累计偏移了。解决url 里补 serverTimezoneAsia/Shanghai并且后端统一返回带时区的字符串前端不要再 new Date() 二次转换。这个坑的隐蔽点在于日志里时间是对的只有页面时间错。5.3 告警丢失断线重连时产生的空窗期现象页面开着但长时间切后台回到页面发现一段时间内的告警没有弹窗只有刷新列表之后才看到记录。原因WebSocket 断线后前端虽然做了重连但断线期间后端广播的消息是没有地方投放的。页面处于后台时浏览器可能收紧 websocket 心跳连接被静默断开而不触发 onclose。解决前端在 onclose 和 visibilitychange 触发时统一调一次增量拉取接口以本地最大告警时间为准补拉。后端实现一个带 since 参数的告警查询只返回大于该时间的记录数据量小、响应快。从那以后我把重连、补拉、轮询兜底三件事写成了一个统一函数任何渠道的告警消费都走同一套逻辑。现象部分告警出现重复推送列表里同一条告警出现两次。原因网络抖动导致上报请求重发后端没有做去重规则引擎对同一条数据判定两次。解决在上报入口按 deviceCode metricCode reportTime 做唯一索引重复上报时忽略即可。这个方案比 Redis 分布式锁便宜也能从源头遏制重复。5.4 排查习惯先看日志还是先看网络我在排这套系统的告警链路时有一个固定的排查顺序先看后端日志确认告警是否生成再看 WebSocket 连接是否存活最后看页面有没有收到 onmessage。这个顺序避免了很多无效调试。生产环境建议把告警模块的日志单独输出到独立文件用 grep 一条命令就能过滤当天的告警记录不用在庞大的请求日志里捞。grep alarm logs/agri-admin.log | tail -100日志里出现 create alarm 后广告推送失败的问题就锁定在传输层不再怀疑规则配置。6. 二次开发如何把它改造成自己的监控中台工程跑通只是开始。这套系统真正的价值在于边界清晰我顺着三个高频改造点分享思路。6.1 把模拟数据源换成真实物联网设备默认工程用定时任务模拟上报数据替换真实设备时不要动 report 接口而是写一个协议适配器。比如传感器走 MQTT 上云适配器做两件事订阅对应 topic把 payload 翻译成 DeviceReportDTO然后调用现有的 DeviceDataService。业务层完全无感告警和缓存逻辑不用改一行。Component public class MqttDeviceAdapter { Autowired private DeviceDataService dataService; public void onMessage(String topic, String payload) { // 解析 payload 为 DeviceReportDTO DeviceReportDTO dto parser.parse(topic, payload); dataService.processReport(dto); } }6.2 加新告警规则有两种路径如果只是新增一个指标编码在阈值配置表加一行即可后台界面可以直接操作。如果要加的是持续性判定比如“连续升温 20 分钟”可以在 RuleMatcher 里抽出一组 RuleHandler每个 handler 负责一种规则类型由配置驱动。这样既保住默认行为又给复杂策略留了扩展位。6.3 把通知扩展到钉钉和短信只依赖 WebSocket 推送有个盲区前端页面没打开的人收不到。我一般会做一个 NotificationChannel 接口默认实现是 WebSocket 推送扩展实现调钉钉机器人或短信网关告警创建后遍历所有 channel 异步发送。短信通道要保留一个开关防止费用不可控钉钉机器人则注意加签名校验避免 webhook 地址泄露后被人乱刷。我给某基地上线这套系统时曾遇到凌晨设备离线、短信通道欠费没人察觉的情况农户第二天早上才看到异常。从那次以后我每次交付都会强制走一遍告警静默演练停掉前端页面拔掉模拟数据源看消息能否通过备用通道送达。系统可以不上云告警链路不能断。希望这段拆解帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →