
简介这是一套面向农业信息化开发者的智慧农业集成管理系统源码基于 Java Web 技术实现。系统覆盖农田环境监测、设备远程控制、灌溉施肥计划、用户认证等核心场景把物联网数据采集与后台管理有机结合适合需要学习 Java 项目分层设计、实体建模、数据持久化及前后端交互的开发者参考。压缩包共 224 个文件以 142 个 Java 源文件为主另有 30 个 HTML 页面、25 个 XML 配置、20 个 JavaScript 脚本及 SQL 数据库脚本等整体仅 476KB结构紧凑便于快速导入项目运行。系统前端涵盖地块、传感器、设备、方案等管理界面后端处理业务逻辑与数据存取配合数据库脚本可搭建一套完整原型可用于课程设计、项目实训或技术原型搭建。目前已有 174 人学习浏览对希望了解智慧农业系统模块划分与 JavaWeb 工程结构的读者具有较好的参考价值。1. 智慧农业集成管理系统从“半夜被电话吵醒”到“大棚自己会决策”凌晨三点值班室连续弹出十几条“温度超限”告警你却不敢把手机调成静音——因为告警只负责通知不负责处理棚里的卷帘和风机还是得自己摸黑去按。这套智慧农业集成管理系统解决的正是这个矛盾它把环境感知、阈值判断、设备联动放在同一条链路上让大棚在没人盯着的时候也能自己做出反应。它不是单一功能的小插件而是一个从设备接入到管理后台的完整工程包含环境数据采集、告警规则引擎、自动化控制、可视化看板等模块。适合正在做农业物联网平台化、想把硬件数据和业务逻辑真正打通的技术团队参考。下面按架构设计、核心代码、运行步骤和二次开发的顺序来拆。2. 架构设计一次环境上报如何变成一组合法操作2.1 技术栈选型为什么用 EMQX 而不是自研长连接很多农业项目上来就自己封装 Socket设备端连服务器、控制端也连服务器最后连接管理、断线重连、消息去重全要自己写折腾半年还不敢保证稳定。这套系统直接采用 MQTT Broker 做消息中枢服务端不维护设备长连接只作为消息的订阅者设备的上下线、心跳保活都交给 Broker 处理。服务端用的是 Spring Boot 2.x MyBatis Plus MySQL Redis前端是 Vue3 Element Plus规则引擎和数据接入都是独立模块便于替换和扩展。这种选型对一个以“管理”为核心的农业系统来说比较务实硬件采集端只需要支持 MQTT 协议后端通过订阅主题拿到原始数据存入 MySQL 做持久化和报表Redis 做告警冷却和设备令牌缓存。实时推送用 WebSocket不需要引入额外的流计算框架单机支撑几百个设备绰绰有余。2.2 工程目录结构四块物理隔离按部署单元划分解压后整体分为四个部分后端服务、前端管理台、数据库脚本、说明文档。看目录之前先有个心理预期——这属于典型的单体应用但模块边界清晰不会出现“业务都堆在 controller 里”的情况。smart-agriculture/ ├── backend/ # Spring Boot 后端服务 │ ├── src/main/java/ │ │ ├── controller/ # 管理端 REST 接口 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis Plus 数据访问层 │ │ ├── mqtt/ # MQTT 订阅与消息处理 │ │ ├── rule/ # 告警规则引擎 │ │ ├── websocket/ # 实时推送 │ │ └── config/ # 数据源、Redis、MQTT 配置 │ └── src/main/resources/ │ └── application.yml ├── frontend/ # Vue3 管理后台 │ ├── src/ │ │ ├── views/ # 大屏监控、设备管理、告警记录 │ │ ├── api/ # 接口封装 │ │ └── router/ │ └── package.json ├── sql/ │ └── init.sql # 建库建表脚本与初始数据 └── docs/ └── 部署说明.md后端里的mqtt和rule是两个独立包这是这套系统的精华区域。mqtt负责把协议层的脏活隔离在业务之外rule负责把告警和联动做成可配置项。实际改造时更换硬件协议只需要动mqtt包调整控制策略只需要动rule包互不干扰这就是模块划分带来的收益。2.3 核心数据链路从传感器到风机的完整路径把一次设备上报到最终动作执行串起来整条链路是传感器定时上报 → EMQX Broker 接收到主题smart-agriculture/{deviceId}/telemetry的消息 → 后端 MQTT 回调解析 JSON 数据 → 落库至device_data表 → 规则引擎拉取该设备绑定的告警规则逐条比对 → 命中规则后生成告警记录并推送到 WebSocket → 若规则配置了联动动作则调用设备控制接口下发指令 → 设备端收到指令执行如打开风机卷帘。设备上报的消息体长这样{ deviceId: G001, ts: 1720000000, type: telemetry, data: { temperature: 28.6, humidity: 75.2, soilMoisture: 32, light: 1800 } }ts字段是 Unix 时间戳代表设备端采集时间而不是服务器接收时间。规则引擎在做阈值判断时会拿这个设备时间戳作为依据避免网络延迟导致的数据乱序。这也是我在类似项目里吃过亏的地方——如果不区分“采集时间”和“到达时间”告警排序和联动判断会出现错位。3. 核心模块拆解告警引擎与自动控制的真实代码3.1 MQTT 接入层从主题解析到数据分发先看mqtt包里的消息回调处理器。它负责订阅主题、解析负载、分发数据。这里有一个关键设计用 Topic 携带设备 ID消息体只放业务数据这样 MQTT 端的通配符订阅能够天然按设备维度路由后端不需要在消息体里额外解析设备路由信息。Component public class MqttInboundHandler implements MqttCallback { private final DeviceDataService deviceDataService; private final RuleEngine ruleEngine; private final MqttClient mqttClient; EventListener(ApplicationReadyEvent.class) public void connectAndSubscribe() throws MqttException { mqttClient.setCallback(this); // QoS1 保证消息至少一次送达避免关键采集数据丢失 mqttClient.subscribe(smart-agriculture//telemetry, 1); } Override public void messageArrived(String topic, MqttMessage message) { // topic 形如 smart-agriculture/G001/telemetry String[] segments topic.split(/); String deviceId segments[1]; JSONObject payload JSON.parseObject(new String(message.getPayload())); JSONObject data payload.getJSONObject(data); DeviceData entity new DeviceData(); entity.setDeviceId(deviceId); entity.setTemperature(data.getDouble(temperature)); entity.setHumidity(data.getDouble(humidity)); entity.setSoilMoisture(data.getDouble(soilMoisture)); entity.setLight(data.getDouble(light)); entity.setCollectTime(new Date(payload.getLong(ts) * 1000)); // 先落库再进规则引擎保证告警可追溯 deviceDataService.save(entity); ruleEngine.evaluate(entity); } }这段代码有几个值得注意的参数细节。订阅时指定 QoS 为 1意味着消息在 Broker 和客户端之间至少送达一次代价是可能出现重复消息因此规则引擎里要做幂等控制。collectTime用的是设备端时间戳如果设备本地时钟不准这里会出现时间偏差部署时要用 NTP 统一设备时钟否则后续的告警时长统计会失真。数据落库和规则判断是串行的如果设备量大导致save变慢常见做法是把save改为异步批量写或者引入消息队列解耦。3.2 告警规则引擎阈值比对、持续时间与去重冷却规则引擎是这套系统的核心决定了告警链路的可靠性。它做的事并不复杂——从数据库查出该设备绑定的规则逐条判断当前值是否触发触发后再验证是否满足“持续时间”要求最后通过 Redis 做冷却去重。Service public class RuleEngine { private final AlarmRuleMapper ruleMapper; private final AlarmRecordMapper alarmRecordMapper; private final StringRedisTemplate redisTemplate; private final DeviceControlClient controlClient; public void evaluate(DeviceData data) { ListAlarmRule rules ruleMapper.selectByDeviceId(data.getDeviceId()); for (AlarmRule rule : rules) { double actual extractMetricValue(data, rule.getMetric()); if (!compare(actual, rule.getOperator(), rule.getThreshold())) { continue; } // 持续时间检查只有当指标连续超过阈值一定秒数才告警 String key rule:trigger: rule.getId() : data.getDeviceId(); Long startTime redisTemplate.opsForValue().increment(key); if (startTime 1) { redisTemplate.expire(key, Duration.ofSeconds(rule.getDurationSec())); } Long elapsed redisTemplate.getExpire(key, TimeUnit.SECONDS); if (rule.getDurationSec() - elapsed rule.getDurationSec()) { // 触发时长未达到设定值继续累积 continue; } // 冷却去重同一规则同一设备在 cooldown 内只推一次告警 Boolean acquired redisTemplate.opsForValue() .setIfAbsent(alarm:cooldown: rule.getId() : data.getDeviceId(), 1, Duration.ofSeconds(rule.getCooldownSec())); if (Boolean.TRUE.equals(acquired)) { AlarmRecord record new AlarmRecord(); record.setRuleId(rule.getId()); record.setDeviceId(data.getDeviceId()); record.setActualValue(actual); record.setLevel(rule.getLevel()); alarmRecordMapper.insert(record); // 如果规则配置了联动动作触发设备控制 if (rule.getAction() ! null) { controlClient.execute(rule.getAction(), data.getDeviceId()); } } } } private double extractMetricValue(DeviceData data, String metric) { switch (metric) { case temperature: return data.getTemperature(); case humidity: return data.getHumidity(); case soilMoisture: return data.getSoilMoisture(); case light: return data.getLight(); default: throw new UnsupportedOperationException(不支持指标: metric); } } private boolean compare(double actual, String op, double threshold) { switch (op) { case : return actual threshold; case : return actual threshold; case : return actual threshold; case : return actual threshold; default: return false; } } }许多初版项目死在“告警风暴”上——温度刚越过阈值就疯狂推告警几分钟刷几百条运营直接把通知关掉。这个引擎用 Redis 做了两层控制第一层是“持续时间”指标必须持续越界指定秒数才产生告警避免单次毛刺导致误报第二层是“冷却时间”同一设备同一规则在冷却期内只推一条避免重复打扰。metric和operator字段都是可配置的新增一种传感器指标只需要在extractMetricValue里加一个分支这也是扩展成本最低的设计方式。3.3 设备控制链路告警触发后指令如何安全下发规则命中后执行联动动作例如温度超过 32 度持续 60 秒就打开风机。指令下发不能直接裸调设备接口中间要经过一个控制服务统一处理指令格式、反馈确认和失败补偿。Service public class DeviceControlClient { private final MqttPublisher mqttPublisher; private final StringRedisTemplate redisTemplate; public void execute(JSONObject action, String deviceId) { String commandTopic smart-agriculture/ deviceId /command; String requestId UUID.randomUUID().toString(); JSONObject payload new JSONObject(); payload.put(requestId, requestId); payload.put(action, action.getString(action)); payload.put(param, action.getJSONObject(param)); // 发送前记录待确认状态Redis key 10秒过期用于超时重试 mqttPublisher.publish(commandTopic, payload.toJSONString()); redisTemplate.opsForValue().set( device:ack: requestId, deviceId, Duration.ofSeconds(10)); } }这里用了“请求 ID 待确认状态”的异步确认模式。设备执行指令后会上报一条包含requestId的 ack 消息后端确认后删除 Redis 里的待确认 key。10 秒内没有确认则判定下发失败触发补偿逻辑重发指令或记录异常工单。payload中的action是动作名如open_fan、roll_curtainparam是动作参数由后台规则配置。这种设计把“下发”和“成功”解耦不会出现设备离线时指令凭空消失、操作记录里却显示已执行的情况。4. 本地运行与避坑记录从建库到告警联动跑通4.1 环境准备与数据库初始化运行前需要准备 MySQL 5.7、Redis 5.0、EMQX 4.x/5.x以及 JDK 1.8 和 Node.js 16。把sql/init.sql导入 MySQL脚本里包含了设备表、告警规则表、告警记录表、用户表和初始数据。建库后建议先登录 MySQL 确认alarm_rule表里是否插入了示例规则SELECT * FROM alarm_rule; -- 预期结果至少包含一条 temperature 32.00 的规则并配置了 open_fan 动作还没有示例规则就手动插入一条测试用规则INSERT INTO alarm_rule (rule_name, device_id, metric, operator, threshold, duration_sec, action, cooldown_sec) VALUES (温度过高自动开风机, G001, temperature, , 32.00, 60, {action:open_fan,param:{level:1}}, 300);duration_sec表示连续越界 60 秒才触发cooldown_sec表示触发后 300 秒内不再重复告警。这里的参数直接影响系统灵敏度测试阶段建议把duration_sec调小到 5 秒便于快速验证联动效果。4.2 后端配置与启动后端配置集中在application.yml。需要改三处MySQL 地址、Redis 地址、EMQX Broker 地址。其他保持默认即可。spring: datasource: url: jdbc:mysql://localhost:3306/smart_agri?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 redis: host: localhost port: 6379 mqtt: broker: tcp://localhost:1883 client-id: server-agri-${random.value} username: emqx_user password: emqx_passclient-id必须全局唯一且不能与设备端共用同一个 ID否则 Broker 会互相踢下线。serverTimezoneAsia/Shanghai是为了保证数据库时间字段和采集时间一致这里省略会导致时间差 8 小时的经典问题。启动后端后看日志出现Connected to MQTT broker和Subscribed to smart-agriculture//telemetry说明连接和订阅都正常。4.3 前端启动前端启动前先安装依赖cd frontend npm install npm run dev启动后访问本地开发服务器地址默认端口在vite.config.js里配置。登录页账号密码在init.sql的sys_user表里。进入系统后先打开“设备管理”页面确认能查看到导入的设备列表再打开“大屏监控”这个页面会通过 WebSocket 实时刷新数据。4.4 避坑记录5 个真实踩过的坑坑 1时间戳差了 8 个小时告警统计对不上现象设备上报的采集时间在管理后台显示比当前时间晚 8 小时按小时统计的温湿度曲线整体右移。原因MySQL 连接串没加serverTimezone而程序里存的是带时区的 JavaDate驱动默认用了服务器本地时区做转换。解决在 JDBC URL 中显式指定serverTimezoneAsia/Shanghai并在设备端确认上报的时间戳是 Unix 时间戳不要传带时区的字符串。坑 2偶发出现“有告警通知但没执行联动”复查日志发现消息根本没到服务端现象规则引擎输出日志时有时无Mqtt 订阅看似正常但告警直接漏掉。原因MQTT 消息的 QoS 设置为 0即“最多一次”Broker 不保证送达。当后端短暂阻塞或网络抖动时消息直接丢弃。解决订阅和发布统一使用 QoS 1同时在后端消费逻辑里做幂等防止 QoS 1 带来的重复送达。坑 3生产环境上温度告警刷屏几分钟推了几百条现象单台设备温度越界后告警记录和微信通知瞬间被刷满。原因规则表里没有冷却机制每次上报数据命中规则都立即生成告警。解决在规则表增加cooldown_sec字段用 RedisSETNX实现固定时间窗口内的去重同一个设备和规则在冷却窗口内只允许产生一条告警。坑 4WebSocket 推送在大屏页面偶发断开刷新页面就好现象监控大屏连续运行几小时后推送停止控制台提示 WebSocket 连接关闭。原因有些前端代码里对onbeforeunload做了清理操作但没有妥善管理重连后端也没有检测心跳连接被中间层静默回收后无人感知。解决前端封装一个带指数退避的自动重连逻辑后端每 30 秒发送一次 ping 帧保活超过三次未收到 pong 就主动关闭该连接。坑 5前端表格显示“undefined”接口数据明明有现象设备列表里的温度、湿度字段全显示成 undefined但网络请求返回值正常。原因后端返回的是驼峰命名如collectTime数据库表字段是下划线命名如collect_timeMyBatis Plus 的映射配置没打开。解决在application.yml里配置map-underscore-to-camel-case: true或保证实体类字段与数据库列名严格对应。5. 二次开发把一套固定系统改成可配置的交付物把这个系统从“只管一套大棚”变成“能复制给多个基地用”重点改造方向有两个设备接入的通用化和规则配置的可视化。先看设备接入层。现在MQTT主题里写死了smart-agriculture前缀不同基地的租户如果共用一套服务主题前缀就得变成tenantId/deviceId/telemetry这种形式。消息回调里解析出tenantId后再根据租户去查规则。这样不用改核心引擎就能支持多租户隔离。如果新接入的设备不是 MQTT 协议比如通过 Modbus RTU 采集可以单独写一个协议适配器把 Modbus 数据封装成标准DeviceData对象丢给RuleEngine复用告警和联动链路。另一个值得改的地方是规则配置。当前alarm_rule表的action字段是 JSON 字符串要改动作只能写 SQL。把后台“告警规则”页面扩成一个表单界面操作符、阈值、持续时间、动作类型都做成下拉选择动作参数动态渲染成输入框。这样运营人员能在界面上调整联动策略不用再请开发改数据。改造时注意规则表的变更要有版本号字段一旦规则被修改旧告警记录仍能通过版本号追溯当时使用的规则内容。分享一个我的教训有一次给某基地做演示我把设备 ID 直接写死在 Service 里现场换了一套设备就全盘翻车。从那以后我每次接入新设备都强制走一遍配置文件注册流程设备 ID、短信通知开关、阈值参数一律从数据库读取再也不敢在代码里埋固定值。希望这套系统的拆分思路能帮到你尤其是别让告警引擎和控制链路耦合在一起——改起来确实天差地别。本文还有配套的精品资源点击获取