diff --git a/agri-admin/src/main/resources/application-mqtt.yml b/agri-admin/src/main/resources/application-mqtt.yml
index 6d515cf..2ecd864 100644
--- a/agri-admin/src/main/resources/application-mqtt.yml
+++ b/agri-admin/src/main/resources/application-mqtt.yml
@@ -2,14 +2,11 @@ spring:
# MQTT配置
mqtt:
host: tcp://122.51.109.52:1883 # 设备/后端的MQTT TCP地址
- ws-host: wss://mq.xiaoces.com/mqtt # 前端的WebSocket地址
username: admin # Mosquitto共用账号
password: Admin#12345678 # Mosquitto密码
client-id: springboot-backend # 截取UUID前8位(自动去横线)
- default-topic: dtu/+/up,frontend/+/control/+ # 后端监听的主题
+ default-topic: dtu/+/up,frontend/+/down/+ # 后端监听的主题
qos: 1 # 消息可靠性
timeout: 60 # 连接超时
keep-alive: 60 # 心跳间隔
- # 新增重连配置
- reconnect-interval: 5 # 重连间隔(秒)
- max-reconnect-times: -1 # 最大重连次数(-1=无限重连)
\ No newline at end of file
+
\ No newline at end of file
diff --git a/agri-framework/src/main/java/com/agri/framework/interceptor/MqttMessageHandler.java b/agri-framework/src/main/java/com/agri/framework/interceptor/MqttMessageHandler.java
index 93e45d3..b7d049c 100644
--- a/agri-framework/src/main/java/com/agri/framework/interceptor/MqttMessageHandler.java
+++ b/agri-framework/src/main/java/com/agri/framework/interceptor/MqttMessageHandler.java
@@ -1,6 +1,9 @@
package com.agri.framework.interceptor;
import com.agri.framework.config.MqttConfig;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.alibaba.fastjson2.TypeReference;
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
@@ -25,6 +28,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -37,7 +41,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* 3. 转发设备状态到订阅的前端
* 4. 处理前端控制指令(权限校验+分布式锁+转发)
* 适配JDK 8,无心跳包相关逻辑
- *
+ *
* 【方案A改造说明(最小改动)】
* 1) 不再自己new MqttClient 做重连,避免 mqttMessageSender 持有旧client导致“重连后发不出去”
* 2) 只依赖 MqttConnectOptions#setAutomaticReconnect(true) 的Paho自动重连
@@ -46,20 +50,26 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Component
public class MqttMessageHandler implements SmartLifecycle {
- /** MQTT客户端(由MqttConfig配置类注入) */
+ /**
+ * MQTT客户端(由MqttConfig配置类注入)
+ */
@Resource
private MqttClient mqttClient;
- /** MQTT消息发送工具类(由MqttConfig配置类注入) */
+ /**
+ * MQTT消息发送工具类(由MqttConfig配置类注入)
+ */
@Resource
private MqttConfig.MqttMessageSender mqttMessageSender;
- /** Redis模板,用于存储订阅关系、设备在线状态、分布式锁 */
+ /**
+ * Redis模板,用于存储订阅关系、设备在线状态、分布式锁
+ */
@Resource
private StringRedisTemplate stringRedisTemplate;
// 读取配置文件中的默认订阅主题(移除心跳主题)
- @Value("${spring.mqtt.default-topic:dtu/+/up,frontend/+/control/+}")
+ @Value("${spring.mqtt.default-topic:dtu/+/up,frontend/+/down/+}")
private String defaultTopic;
// 优化:统一使用SLF4J日志(JDK 8兼容)
@@ -68,14 +78,16 @@ public class MqttMessageHandler implements SmartLifecycle {
// 新增:生命周期管理标识,控制MQTT客户端启动/关闭
private final AtomicBoolean isRunning = new AtomicBoolean(false);
- /** MQTT连接配置项(从MqttConfig注入) */
+ /**
+ * MQTT连接配置项(从MqttConfig注入)
+ */
@Resource
private MqttConnectOptions mqttConnectOptions;
/**
* 初始化:订阅主题+设置回调
* (移除@PostConstruct,改为由SmartLifecycle的start()触发)
- *
+ *
* 【方案A】不做自写重连;Paho会在连接断开后自动重连(前提:connectOptions.setAutomaticReconnect(true))
*/
public void subscribeTopics() throws MqttException {
@@ -104,6 +116,7 @@ public class MqttMessageHandler implements SmartLifecycle {
// 按主题类型设置QoS:控制指令/状态用QoS 1
for (int i = 0; i < topics.length; i++) {
qosArray[i] = 1;
+ topics[i] = topics[i].trim();
}
// 设置MQTT消息回调:处理连接断开、消息接收、消息发布完成
@@ -170,7 +183,8 @@ public class MqttMessageHandler implements SmartLifecycle {
/**
* 消息分发处理:根据主题类型路由到不同处理方法\仅处理设备状态、前端控制指令
* 可以监听到设备传过来的业务数据 以及前端传过来的控制设备指令
- * @param topic 消息主题
+ *
+ * @param topic 消息主题
* @param payload 消息内容(JSON字符串)
*/
private void handleMessage(String topic, String payload) {
@@ -182,8 +196,8 @@ public class MqttMessageHandler implements SmartLifecycle {
if (topic.matches("dtu/\\w+/up")) {
handleDeviceStatus(topic, payload);
}
- // 处理前端控制指令主题:frontend/{clientId}/control/{deviceId}
- else if (topic.matches("frontend/\\w+/control/\\w+")) {
+ // 处理前端控制指令主题:frontend/{clientId}/down/{deviceId}
+ else if (topic.matches("frontend/\\w+/down/\\w+")) {
handleFrontendControl(topic, payload);
}
} catch (Exception e) {
@@ -196,8 +210,51 @@ public class MqttMessageHandler implements SmartLifecycle {
* 处理设备状态:转发给订阅的前端
*/
private void handleDeviceStatus(String topic, String payload) throws MqttException {
+ // 第一步:解析JSON,非有效JSON直接return
+ JSONObject payloadObj;
+ try {
+ payloadObj = JSON.parseObject(payload);
+ } catch (Exception e) {
+ log.error("【设备处理】JSON解析失败,payload={}", payload, e);
+ return;
+ }
+ if (payloadObj == null || payloadObj.isEmpty()) {
+ log.warn("【设备处理】JSON解析后为空,payload={}", payload);
+ return;
+ }
+
// 解析设备ID:主题格式为dtu/{deviceId}/up,分割后第2个元素是设备ID
String deviceId = topic.split("/")[1];
+
+ // 第二步:判断是否为设备回执({"suc":true/false,"prop":{"功能码":指令}})
+ String funcType = null;
+ if (payloadObj.containsKey("suc") && payloadObj.containsKey("prop")) {
+ JSONObject propObj = payloadObj.getJSONObject("prop");
+ if (propObj != null && !propObj.isEmpty()) {
+ // 提取prop中的第一个功能码
+ Map.Entry propEntry = propObj.entrySet().iterator().next();
+ funcType = propEntry.getKey();
+ // 释放对应功能的分布式锁
+ String lockKey = "lock:" + deviceId + ":" + funcType;
+ Boolean delete = stringRedisTemplate.delete(lockKey);
+ if (propObj.size() > 1) {
+ log.warn("【设备回执】prop包含多个功能码,仅处理第一个:{}", propObj.keySet());
+ }
+ log.info("【设备回执】设备{}的{}功能执行完成,已释放锁:{},{}", deviceId, funcType, lockKey, delete);
+
+ // 广播回执结果给所有订阅该设备的前端
+ // String broadcastTopic = "frontend/" + clientId + "/dtu/" + deviceId + "/up";;
+ // JSONObject ackPayload = new JSONObject();
+ // ackPayload.put("deviceId", deviceId);
+ // ackPayload.put("funcType", funcType);
+ // ackPayload.put("suc", payloadObj.getBooleanValue("suc"));
+ // ackPayload.put("code", propEntry.getValue());
+ // mqttMessageSender.publish(broadcastTopic, ackPayload.toJSONString());
+ }
+ }
+
+ // 非回执消息:正常转发给订阅前端
+ // if (!isDeviceAck) {
// 查询Redis中订阅该设备的前端列表:sub:{deviceId}
Set subscribedClients = stringRedisTemplate.opsForSet().members("sub:" + deviceId);
@@ -215,6 +272,7 @@ public class MqttMessageHandler implements SmartLifecycle {
// 优化:替换System.out为log.info
log.info("【设备状态转发】设备{}无订阅前端,跳过转发", deviceId);
}
+ // }
}
/**
@@ -232,37 +290,57 @@ public class MqttMessageHandler implements SmartLifecycle {
return;
}
+ // 解析功能码({"功能码":状态码}格式)
+ Map funcCodeMap = null;
+ try {
+ funcCodeMap = JSON.parseObject(payload, new TypeReference