简介:这是一套面向农业信息化开发者及物联网应用方向的智慧农业集成管理系统源码,涵盖物联网监测、大数据分析、设备控制、地块管理、农事计划等典型业务。项目基于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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='告警记录表';告警表是一个典型的宽表设计,把触发值、阈值、级别都冗余进来,历史告警查询就不需要回查设备快照。metric_code 与阈值配置表联动,实现同一指标对不同基地采用不同阈值的灵活度。
2.4 前端结构:管理后台与大屏的页面划分
前端工程主要目录及职责如下。
| 路径 | 职责 | 关键依赖 |
|---|---|---|
| views/login | 登录页 | 表单校验、验证码 |
| views/device | 设备管理 | 表格、状态切换、点位配置 |
| views/monitor | 实时监控 | 地图打点、点位卡片 |
| views/alarm | 告警中心 | 列表筛选、详情抽屉 |
| views/screen | 数据大屏 | ECharts 图表、WebSocket |
| views/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 Result<Void> 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 分钟一次上报配 windowCount=3,意味着持续超限 15 分钟才会告警。对大棚夜间的温度波动来说,这个设置能滤掉绝大多数瞬间尖峰,而不是靠降低阈值灵敏度来避免误报。alarmService.create 内部做了两件事:先插一条 alarm_record,再调用告警推送组件广播消息。
3.3 WebSocket 实时推送:让告警秒上大屏
告警产生后需要立即推给前端,这里用的是标准 WebSocket 端点。
@ServerEndpoint("/ws/alarm") @Component public class AlarmWebSocket { private static final CopyOnWriteArraySet<Session> 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 会下载依赖,时间取决于网络,中途不要 Ctrl+C,容易留下残缺的本地仓库。
mvn clean package -DskipTests java -jar agri-admin/target/agri-admin.jar配置文件在 agri-admin 的 application.yml 中,改以下四个位置就能跑通本机环境。
spring: datasource: url: jdbc:mysql://localhost:3306/agri?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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 里的两个参数值得解释:characterEncoding=utf8 保证中文不乱码,serverTimezone=Asia/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 allowed,MySQL 8 驱动在非 SSL 连接下偶发。
原因:MySQL 8 默认使用 caching_sha2_password 认证,JDBC 驱动首次连接需要取公钥,url 里少了允许获取公钥的参数。
解决:在 jdbc url 末尾追加 allowPublicKeyRetrieval=true&useSSL=false。这个参数不影响原有逻辑,属于驱动握手阶段的配置,加了就通畅。
5.2 联动阶段:告警不推送与数据错位
现象:告警记录在后端日志里有,列表接口也能查到,但浏览器迟迟不弹通知。
原因:前端 WebSocket 建的地址是 ws://localhost:8080,而后端接口要走 vite 代理。直连后端会碰到端口跨域,连接建立失败,前端只是静默记录错误,不会主动告诉你握手失败。
解决:前端统一使用 location.host 拼接 WebSocket 地址,开发环境代理里务必写ws: true。注意 vite 的 /ws 代理 target 要写 ws://localhost:8080 而不是 http://,否则握手协议不对。
现象:数据库中告警时间正确,前端展示却慢 8 小时。
原因:JDBC 连接串缺了 serverTimezone 参数,驱动按服务器默认时区读取时间,而前后端又各自做了一次解析,时间就累计偏移了。
解决:url 里补 serverTimezone=Asia/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 地址泄露后被人乱刷。
我给某基地上线这套系统时,曾遇到凌晨设备离线、短信通道欠费没人察觉的情况,农户第二天早上才看到异常。从那次以后,我每次交付都会强制走一遍告警静默演练:停掉前端页面,拔掉模拟数据源,看消息能否通过备用通道送达。系统可以不上云,告警链路不能断。希望这段拆解帮到你。
本文还有配套的精品资源,点击获取