Commit 8b4bee69f2d66e01e6d7abe614da22c5df810a45

Authored by haohao
1 parent d70c6986

feat:【IoT 物联网】优化 MQTT 连接逻辑,更新 HTTP 认证端口,简化消息处理流程

yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
1 1 package cn.iocoder.yudao.module.iot.gateway.config;
2 2  
3   -import cn.hutool.core.util.StrUtil;
4 3 import jakarta.validation.constraints.NotEmpty;
5 4 import jakarta.validation.constraints.NotNull;
6 5 import lombok.Data;
... ... @@ -105,46 +104,53 @@ public class IotGatewayProperties {
105 104 @NotNull(message = "是否开启不能为空")
106 105 private Boolean enabled;
107 106  
108   - // TODO @haohao:是不是改成 httpPort?不只认证,目前看。
109 107 /**
110   - * HTTP 认证端口(默认:8090)
  108 + * HTTP 服务端口(默认:8090)
111 109 */
112   - private Integer httpAuthPort = 8090;
  110 + private Integer httpPort = 8090;
113 111  
114 112 /**
115 113 * MQTT 服务器地址
116 114 */
117 115 @NotEmpty(message = "MQTT 服务器地址不能为空")
118 116 private String mqttHost;
  117 +
119 118 /**
120 119 * MQTT 服务器端口(默认:1883)
121 120 */
122 121 @NotNull(message = "MQTT 服务器端口不能为空")
123 122 private Integer mqttPort = 1883;
  123 +
124 124 /**
125 125 * MQTT 用户名
126 126 */
127 127 @NotEmpty(message = "MQTT 用户名不能为空")
128 128 private String mqttUsername;
  129 +
129 130 /**
130 131 * MQTT 密码
131 132 */
132 133 @NotEmpty(message = "MQTT 密码不能为空")
133 134 private String mqttPassword;
  135 +
134 136 /**
135 137 * MQTT 客户端的 SSL 开关
136 138 */
137 139 @NotNull(message = "MQTT 是否开启 SSL 不能为空")
138 140 private Boolean mqttSsl = false;
  141 +
139 142 /**
140 143 * MQTT 客户端 ID(如果为空,系统将自动生成)
141 144 */
  145 + @NotEmpty(message = "MQTT 客户端 ID 不能为空")
142 146 private String mqttClientId;
  147 +
143 148 /**
144 149 * MQTT 订阅的主题
145 150 */
146 151 @NotEmpty(message = "MQTT 主题不能为空")
147 152 private List<@NotEmpty(message = "MQTT 主题不能为空") String> mqttTopics;
  153 +
148 154 /**
149 155 * 默认 QoS 级别
150 156 * <p>
... ... @@ -158,24 +164,12 @@ public class IotGatewayProperties {
158 164 * 连接超时时间(秒)
159 165 */
160 166 private Integer connectTimeoutSeconds = 10;
  167 +
161 168 /**
162 169 * 重连延迟时间(毫秒)
163 170 */
164 171 private Long reconnectDelayMs = 5000L;
165 172  
166   - // TODO @haohao:貌似可以通过配置文件 + el 表达式;尽量还是配置文件;
167   - /**
168   - * 获取 MQTT 客户端 ID,如果未配置则自动生成
169   - *
170   - * @return MQTT 客户端 ID
171   - */
172   - public String getMqttClientId() {
173   - if (StrUtil.isBlank(mqttClientId)) {
174   - mqttClientId = "iot-gateway-mqtt-" + System.currentTimeMillis();
175   - }
176   - return mqttClientId;
177   - }
178   -
179 173 }
180 174  
181 175 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java
1 1 package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4   -import cn.hutool.core.util.StrUtil;
5 4 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
6 5 import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties;
7 6 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router.IotMqttHttpAuthHandler;
... ... @@ -20,7 +19,10 @@ import jakarta.annotation.PreDestroy;
20 19 import lombok.Getter;
21 20 import lombok.extern.slf4j.Slf4j;
22 21  
  22 +import java.util.HashMap;
23 23 import java.util.List;
  24 +import java.util.Map;
  25 +import java.util.concurrent.atomic.AtomicInteger;
24 26  
25 27 /**
26 28 * IoT 网关 MQTT 协议:接收设备上行消息
... ... @@ -127,7 +129,7 @@ public class IotMqttUpstreamProtocol {
127 129 router.post(IotMqttTopicUtils.MQTT_EVENT_PATH).handler(authHandler::handleEvent);
128 130  
129 131 // 2. 启动 HTTP 服务器
130   - int authPort = emqxProperties.getHttpAuthPort();
  132 + int authPort = emqxProperties.getHttpPort();
131 133 try {
132 134 httpAuthServer = vertx.createHttpServer()
133 135 .requestHandler(router)
... ... @@ -169,10 +171,13 @@ public class IotMqttUpstreamProtocol {
169 171 log.info("[startMqttClient][使用 MQTT 客户端 ID: {}]", emqxProperties.getMqttClientId());
170 172 createMqttClient();
171 173  
172   - // 3. 连接 MQTT Broker(异步连接,不会抛出异常)
173   - connectMqtt(false);
  174 + // 3. 连接 MQTT Broker(同步等待首次连接结果)
  175 + boolean connected = connectMqttSync();
  176 + if (!connected) {
  177 + throw new RuntimeException("首次连接 MQTT Broker 失败");
  178 + }
174 179  
175   - log.info("[startMqttClient][MQTT 客户端启动完成,正在异步连接中...]");
  180 + log.info("[startMqttClient][MQTT 客户端启动完成]");
176 181 } catch (Exception e) {
177 182 log.error("[startMqttClient][MQTT 客户端启动失败]", e);
178 183 throw new RuntimeException("MQTT 客户端启动失败", e);
... ... @@ -180,6 +185,48 @@ public class IotMqttUpstreamProtocol {
180 185 }
181 186  
182 187 /**
  188 + * 同步连接 MQTT Broker
  189 + *
  190 + * @return 是否连接成功
  191 + */
  192 + private boolean connectMqttSync() {
  193 + String host = emqxProperties.getMqttHost();
  194 + Integer port = emqxProperties.getMqttPort();
  195 + log.info("[connectMqttSync][开始连接 MQTT Broker, host: {}, port: {}]", host, port);
  196 +
  197 + // 使用计数器实现同步等待
  198 + java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1);
  199 + java.util.concurrent.atomic.AtomicBoolean success = new java.util.concurrent.atomic.AtomicBoolean(false);
  200 +
  201 + mqttClient.connect(port, host, connectResult -> {
  202 + if (connectResult.succeeded()) {
  203 + log.info("[connectMqttSync][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
  204 + // 设置处理器
  205 + setupMqttHandlers();
  206 + // 订阅主题
  207 + subscribeToTopics();
  208 + success.set(true);
  209 + } else {
  210 + log.error("[connectMqttSync][连接 MQTT Broker 失败, host: {}, port: {}]",
  211 + host, port, connectResult.cause());
  212 + // 首次连接失败,启动重连机制
  213 + reconnectWithDelay();
  214 + }
  215 + latch.countDown();
  216 + });
  217 +
  218 + try {
  219 + // 等待连接结果,最多等待10秒
  220 + latch.await(10, java.util.concurrent.TimeUnit.SECONDS);
  221 + } catch (InterruptedException e) {
  222 + Thread.currentThread().interrupt();
  223 + log.error("[connectMqttSync][等待连接结果被中断]", e);
  224 + }
  225 +
  226 + return success.get();
  227 + }
  228 +
  229 + /**
183 230 * 停止 MQTT 客户端
184 231 */
185 232 private void stopMqttClient() {
... ... @@ -218,15 +265,6 @@ public class IotMqttUpstreamProtocol {
218 265 // 1. 参数校验
219 266 String host = emqxProperties.getMqttHost();
220 267 Integer port = emqxProperties.getMqttPort();
221   - // TODO @haohao:这些参数校验,交给 validator;
222   - if (StrUtil.isBlank(host)) {
223   - log.error("[connectMqtt][MQTT Host 为空, 无法连接]");
224   - throw new IllegalArgumentException("MQTT Host 不能为空");
225   - }
226   - if (port == null || port <= 0) {
227   - log.error("[connectMqtt][MQTT Port({}) 无效]", port);
228   - throw new IllegalArgumentException("MQTT Port 必须为正整数");
229   - }
230 268  
231 269 if (isReconnect) {
232 270 log.info("[connectMqtt][开始重连 MQTT Broker, host: {}, port: {}]", host, port);
... ... @@ -238,32 +276,28 @@ public class IotMqttUpstreamProtocol {
238 276  
239 277 // 2. 异步连接
240 278 mqttClient.connect(port, host, connectResult -> {
241   - // TODO @haohao:if return,减少括号哈;
242   - if (connectResult.succeeded()) {
243   - if (isReconnect) {
244   - log.info("[connectMqtt][MQTT 客户端重连成功, host: {}, port: {}]", host, port);
245   - } else {
246   - log.info("[connectMqtt][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
247   - }
248   -
249   - // 设置处理器
250   - setupMqttHandlers();
251   - // 订阅主题
252   - subscribeToTopics();
253   - } else {
  279 + if (!connectResult.succeeded()) {
254 280 log.error("[connectMqtt][连接 MQTT Broker 失败, host: {}, port: {}, isReconnect: {}]",
255 281 host, port, isReconnect, connectResult.cause());
256 282  
257   - // TODO @haohao:体感上,是不是首次必须连接成功?类似 mysql;首次要连接上,然后后续可以重连;
  283 + // 首次连接失败或重连失败时,尝试重连
258 284 if (!isReconnect) {
259   - // 首次连接失败时,也要尝试重连
260 285 log.warn("[connectMqtt][首次连接失败,将开始重连机制]");
261   - reconnectWithDelay();
262   - } else {
263   - // 重连失败时,继续尝试重连
264   - reconnectWithDelay();
265 286 }
  287 + reconnectWithDelay();
  288 + return;
  289 + }
  290 +
  291 + if (isReconnect) {
  292 + log.info("[connectMqtt][MQTT 客户端重连成功, host: {}, port: {}]", host, port);
  293 + } else {
  294 + log.info("[connectMqtt][MQTT 客户端连接成功, host: {}, port: {}]", host, port);
266 295 }
  296 +
  297 + // 设置处理器
  298 + setupMqttHandlers();
  299 + // 订阅主题
  300 + subscribeToTopics();
267 301 });
268 302 }
269 303  
... ... @@ -283,12 +317,7 @@ public class IotMqttUpstreamProtocol {
283 317 * 设置 MQTT 处理器
284 318 */
285 319 private void setupMqttHandlers() {
286   - // TODO @haohao:mqttClient 一定非空;
287   - if (mqttClient == null) {
288   - log.warn("[setupMqttHandlers][MQTT 客户端为空,跳过处理器设置]");
289   - return;
290   - }
291   -
  320 + // 由于 mqttClient 在 createMqttClient() 方法中已初始化,此处无需检查
292 321 // 设置断开重连监听器
293 322 mqttClient.closeHandler(closeEvent -> {
294 323 log.warn("[closeHandler][MQTT 连接已断开, 准备重连]");
... ... @@ -301,13 +330,9 @@ public class IotMqttUpstreamProtocol {
301 330 });
302 331  
303 332 // 设置消息处理器
304   - // TODO @haohao:upstreamHandler 一定非空;
305   - if (upstreamHandler != null) {
306   - mqttClient.publishHandler(upstreamHandler::handle);
307   - log.debug("[setupMqttHandlers][MQTT 消息处理器设置完成]");
308   - } else {
309   - log.warn("[setupMqttHandlers][上行消息处理器为空,跳过设置]");
310   - }
  333 + // upstreamHandler 在 startMqttClient() 方法中已初始化,此处无需检查
  334 + mqttClient.publishHandler(upstreamHandler::handle);
  335 + log.debug("[setupMqttHandlers][MQTT 消息处理器设置完成]");
311 336 }
312 337  
313 338 /**
... ... @@ -327,35 +352,39 @@ public class IotMqttUpstreamProtocol {
327 352 int qos = emqxProperties.getMqttQos();
328 353 log.info("[subscribeToTopics][开始订阅主题, 共 {} 个, QoS: {}]", topicList.size(), qos);
329 354  
330   - // TODO @haohao:使用 atomicinteger 会更合适;
331   - int[] successCount = { 0 }; // 使用数组以便在 lambda 中修改
332   - int[] failCount = { 0 };
  355 + // 使用 AtomicInteger 替代数组,线程安全且更简洁
  356 + AtomicInteger successCount = new AtomicInteger(0);
  357 + AtomicInteger failCount = new AtomicInteger(0);
333 358  
  359 + // 构建主题-QoS 映射,批量订阅
  360 + Map<String, Integer> topicQosMap = new HashMap<>();
334 361 for (String topic : topicList) {
335   - // TODO @haohao:MqttClient subscribe(Map<String, Integer> topics, 是不是更简洁哈;
336   - mqttClient.subscribe(topic, qos, subscribeResult -> {
337   - if (subscribeResult.succeeded()) {
338   - successCount[0]++;
339   - log.debug("[subscribeToTopics][订阅主题成功, topic: {}, qos: {}]", topic, qos);
  362 + topicQosMap.put(topic, qos);
  363 + }
340 364  
341   - // 当所有主题都处理完成时,记录汇总日志
342   - if (successCount[0] + failCount[0] == topicList.size()) {
343   - log.info("[subscribeToTopics][主题订阅完成, 成功: {}, 失败: {}, 总计: {}]",
344   - successCount[0], failCount[0], topicList.size());
345   - }
346   - } else {
347   - failCount[0]++;
348   - log.error("[subscribeToTopics][订阅主题失败, topic: {}, qos: {}, 原因: {}]",
349   - topic, qos, subscribeResult.cause().getMessage(), subscribeResult.cause());
350   -
351   - // 当所有主题都处理完成时,记录汇总日志
352   - if (successCount[0] + failCount[0] == topicList.size()) {
353   - log.info("[subscribeToTopics][主题订阅完成, 成功: {}, 失败: {}, 总计: {}]",
354   - successCount[0], failCount[0], topicList.size());
355   - }
  365 + // 批量订阅所有主题
  366 + mqttClient.subscribe(topicQosMap, subscribeResult -> {
  367 + if (subscribeResult.succeeded()) {
  368 + // 批量订阅成功,记录所有主题为成功
  369 + int successful = successCount.addAndGet(topicList.size());
  370 + log.info("[subscribeToTopics][批量订阅主题成功, 共 {} 个主题, QoS: {}]", successful, qos);
  371 + for (String topic : topicList) {
  372 + log.debug("[subscribeToTopics][订阅主题成功, topic: {}, qos: {}]", topic, qos);
356 373 }
357   - });
358   - }
  374 + } else {
  375 + // 批量订阅失败,记录所有主题为失败
  376 + int failed = failCount.addAndGet(topicList.size());
  377 + log.error("[subscribeToTopics][批量订阅主题失败, 共 {} 个主题, 原因: {}]",
  378 + failed, subscribeResult.cause().getMessage(), subscribeResult.cause());
  379 + for (String topic : topicList) {
  380 + log.error("[subscribeToTopics][订阅主题失败, topic: {}, qos: {}]", topic, qos);
  381 + }
  382 + }
  383 +
  384 + // 记录汇总日志
  385 + log.info("[subscribeToTopics][主题订阅完成, 成功: {}, 失败: {}, 总计: {}]",
  386 + successCount.get(), failCount.get(), topicList.size());
  387 + });
359 388 }
360 389  
361 390 /**
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttDownstreamHandler.java
... ... @@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router;
2 2  
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.hutool.extra.spring.SpringUtil;
5   -import cn.hutool.json.JSONObject;
6 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
7 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  8 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
9 9 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol;
10 10 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
11 11 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
... ... @@ -48,52 +48,49 @@ public class IotMqttDownstreamHandler {
48 48 }
49 49  
50 50 // 2.1 根据方法构建主题
51   - String topic = buildTopicByMethod(message.getMethod(), deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  51 + String topic = buildTopicByMethod(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
52 52 if (StrUtil.isBlank(topic)) {
53 53 log.warn("[handle][未知的消息方法: {}]", message.getMethod());
54 54 return;
55 55 }
  56 +
56 57 // 2.2 构建载荷
57   - // TODO @haohao:这里是不是 encode 就可以发拉?因为本身就 json 化了。
58   - JSONObject payload = buildDownstreamPayload(message);
  58 + byte[] payload = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), deviceInfo.getDeviceName());
  59 +
59 60 // 2.3 发布消息
60   - protocol.publishMessage(topic, payload.toString());
  61 + protocol.publishMessage(topic, new String(payload));
61 62 }
62 63  
63   - // TODO @haohao:这个是不是也可以计算;IotDeviceMessageUtils 的 isReplyMessage;这样就直接生成了;
64 64 /**
65   - * 根据方法构建主题
  65 + * 根据消息方法和回复状态构建主题
66 66 *
67   - * @param method 消息方法
  67 + * @param message 设备消息
68 68 * @param productKey 产品标识
69 69 * @param deviceName 设备名称
70 70 * @return 构建的主题,如果方法不支持返回 null
71 71 */
72   - private String buildTopicByMethod(String method, String productKey, String deviceName) {
73   - IotDeviceMessageMethodEnum methodEnum = IotDeviceMessageMethodEnum.of(method);
  72 + private String buildTopicByMethod(IotDeviceMessage message, String productKey, String deviceName) {
  73 + // 1. 解析消息方法
  74 + IotDeviceMessageMethodEnum methodEnum = IotDeviceMessageMethodEnum.of(message.getMethod());
74 75 if (methodEnum == null) {
  76 + log.warn("[buildTopicByMethod][未知的消息方法: {}]", message.getMethod());
75 77 return null;
76 78 }
77   - return switch (methodEnum) {
78   - case PROPERTY_POST -> IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName);
79   - case PROPERTY_SET -> IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName);
80   - default -> null;
81   - };
82 79  
83   - }
  80 + // 2. 判断是否回复消息
  81 + boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
84 82  
85   - /**
86   - * 构建下行消息载荷
87   - *
88   - * @param message 设备消息
89   - * @return JSON 载荷
90   - */
91   - private JSONObject buildDownstreamPayload(IotDeviceMessage message) {
92   - // 使用 IotDeviceMessageService 进行消息编码
93   - IotDeviceRespDTO device = deviceService.getDeviceFromCache(message.getDeviceId());
94   - byte[] encodedBytes = deviceMessageService.encodeDeviceMessage(message, device.getProductKey(),
95   - device.getDeviceName());
96   - return new JSONObject(new String(encodedBytes));
  83 + // 3. 根据消息方法和回复状态,构建主题
  84 + if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_POST && isReply) {
  85 + return IotMqttTopicUtils.buildPropertyPostReplyTopic(productKey, deviceName);
  86 + }
  87 + if (methodEnum == IotDeviceMessageMethodEnum.PROPERTY_SET && !isReply) {
  88 + return IotMqttTopicUtils.buildPropertySetTopic(productKey, deviceName);
  89 + }
  90 +
  91 + log.warn("[buildTopicByMethod][暂时不支持的下行消息: method={}, isReply={}]",
  92 + message.getMethod(), isReply);
  93 + return null;
97 94 }
98 95  
99 96 }
100 97 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttHttpAuthHandler.java
... ... @@ -14,8 +14,6 @@ import io.vertx.core.json.JsonObject;
14 14 import io.vertx.ext.web.RoutingContext;
15 15 import lombok.extern.slf4j.Slf4j;
16 16  
17   -import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL;
18   -
19 17 /**
20 18 * IoT 网关 MQTT HTTP 认证处理器
21 19 * <p>
... ... @@ -67,7 +65,7 @@ public class IotMqttHttpAuthHandler {
67 65 */
68 66 public void handleAuth(RoutingContext context) {
69 67 try {
70   - // 参数校验
  68 + // 1. 参数校验
71 69 JsonObject body = parseRequestBody(context);
72 70 if (body == null) {
73 71 return;
... ... @@ -78,23 +76,21 @@ public class IotMqttHttpAuthHandler {
78 76 log.debug("[handleAuth][设备认证请求: clientId={}, username={}]", clientId, username);
79 77 if (StrUtil.hasEmpty(clientId, username, password)) {
80 78 log.info("[handleAuth][认证参数不完整: clientId={}, username={}]", clientId, username);
81   - sendAuthResponse(context, RESULT_DENY, false, "认证参数不完整");
  79 + sendAuthResponse(context, RESULT_DENY);
82 80 return;
83 81 }
84 82  
85   - // 执行设备认证
  83 + // 2. 执行认证
86 84 boolean authResult = performDeviceAuth(clientId, username, password);
  85 + log.info("[handleAuth][设备认证结果: {} -> {}]", username, authResult);
87 86 if (authResult) {
88   - // TODO @haohao:是不是两条 info,直接打认证结果:authResult
89   - log.info("[handleAuth][设备认证成功: {}]", username);
90   - sendAuthResponse(context, RESULT_ALLOW, false, null);
  87 + sendAuthResponse(context, RESULT_ALLOW);
91 88 } else {
92   - log.info("[handleAuth][设备认证失败: {}]", username);
93   - sendAuthResponse(context, RESULT_DENY, false, DEVICE_AUTH_FAIL.getMsg());
  89 + sendAuthResponse(context, RESULT_DENY);
94 90 }
95 91 } catch (Exception e) {
96 92 log.error("[handleAuth][设备认证异常]", e);
97   - sendAuthResponse(context, RESULT_IGNORE, false, "认证服务异常");
  93 + sendAuthResponse(context, RESULT_IGNORE);
98 94 }
99 95 }
100 96  
... ... @@ -104,9 +100,10 @@ public class IotMqttHttpAuthHandler {
104 100 * 支持的事件类型:client.connected、client.disconnected 等
105 101 */
106 102 public void handleEvent(RoutingContext context) {
  103 + JsonObject body = null;
107 104 try {
108   - // 解析请求体
109   - JsonObject body = parseRequestBody(context);
  105 + // 1. 解析请求体
  106 + body = parseRequestBody(context);
110 107 if (body == null) {
111 108 return;
112 109 }
... ... @@ -114,7 +111,7 @@ public class IotMqttHttpAuthHandler {
114 111 String username = body.getString("username");
115 112 log.debug("[handleEvent][收到事件: {} - {}]", event, username);
116 113  
117   - // 根据事件类型进行分发处理
  114 + // 2. 根据事件类型进行分发处理
118 115 switch (event) {
119 116 case EVENT_CLIENT_CONNECTED:
120 117 handleClientConnected(body);
... ... @@ -123,15 +120,13 @@ public class IotMqttHttpAuthHandler {
123 120 handleClientDisconnected(body);
124 121 break;
125 122 default:
126   - log.debug("[handleEvent][忽略事件: {}]", event);
127 123 break;
128 124 }
129 125  
130 126 // EMQX Webhook 只需要 200 状态码,无需响应体
131 127 context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
132 128 } catch (Exception e) {
133   - // TODO @haohao:body 可以打印出来
134   - log.error("[handleEvent][事件处理失败]", e);
  129 + log.error("[handleEvent][事件处理失败][body={}]", body != null ? body.encode() : "null", e);
135 130 // 即使处理失败,也返回 200 避免EMQX重试
136 131 context.response().setStatusCode(SUCCESS_STATUS_CODE).end();
137 132 }
... ... @@ -163,18 +158,19 @@ public class IotMqttHttpAuthHandler {
163 158 * @return 请求体JSON对象,解析失败时返回null
164 159 */
165 160 private JsonObject parseRequestBody(RoutingContext context) {
  161 + String rawBody = null;
166 162 try {
  163 + rawBody = context.body().asString();
167 164 JsonObject body = context.body().asJsonObject();
168 165 if (body == null) {
169   - log.info("[parseRequestBody][请求体为空]");
170   - sendAuthResponse(context, RESULT_IGNORE, false, "请求体不能为空");
  166 + log.info("[parseRequestBody][请求体为空][rawBody={}]", rawBody);
  167 + sendAuthResponse(context, RESULT_IGNORE);
171 168 return null;
172 169 }
173 170 return body;
174 171 } catch (Exception e) {
175   - // TODO @haohao:最好把 body 打印出来;
176   - log.error("[parseRequestBody][解析请求体失败]", e);
177   - sendAuthResponse(context, RESULT_IGNORE, false, "请求体格式错误");
  172 + log.error("[parseRequestBody][解析请求体失败][rawBody={}]", rawBody, e);
  173 + sendAuthResponse(context, RESULT_IGNORE);
178 174 return null;
179 175 }
180 176 }
... ... @@ -203,13 +199,10 @@ public class IotMqttHttpAuthHandler {
203 199 * 处理设备状态变化
204 200 *
205 201 * @param username 用户名
206   - * @param online 是否在线
  202 + * @param online 是否在线 true 在线 false 离线
207 203 */
208 204 private void handleDeviceStateChange(String username, boolean online) {
209   - // 解析设备信息
210   - if (StrUtil.isEmpty(username) || "undefined".equals(username)) {
211   - return;
212   - }
  205 + // 1. 解析设备信息
213 206 IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(username);
214 207 if (deviceInfo == null) {
215 208 log.debug("[handleDeviceStateChange][跳过非设备连接: {}]", username);
... ... @@ -217,24 +210,13 @@ public class IotMqttHttpAuthHandler {
217 210 }
218 211  
219 212 try {
220   - // TODO @haohao:serverId 获取非空,可以忽略掉;
221   - String serverId = protocol.getServerId();
222   - if (StrUtil.isEmpty(serverId)) {
223   - log.error("[handleDeviceStateChange][获取服务器ID失败]");
224   - return;
225   - }
226   -
227   - // 构建设备状态消息
  213 + // 2. 构建设备状态消息
228 214 IotDeviceMessage message = online ? IotDeviceMessage.buildStateOnline()
229 215 : IotDeviceMessage.buildStateOffline();
230   - // 发送消息到消息总线
231   - deviceMessageService.sendDeviceMessage(message,
232   - deviceInfo.getProductKey(), deviceInfo.getDeviceName(), serverId);
233 216  
234   - // TODO @haohao:online 不用翻译
235   - log.info("[handleDeviceStateChange][设备状态更新: {}/{} -> {}]",
236   - deviceInfo.getProductKey(), deviceInfo.getDeviceName(),
237   - online ? "在线" : "离线");
  217 + // 3. 发送设备状态消息
  218 + deviceMessageService.sendDeviceMessage(message,
  219 + deviceInfo.getProductKey(), deviceInfo.getDeviceName(), protocol.getServerId());
238 220 } catch (Exception e) {
239 221 log.error("[handleDeviceStateChange][发送设备状态消息失败: {}]", username, e);
240 222 }
... ... @@ -244,16 +226,14 @@ public class IotMqttHttpAuthHandler {
244 226 * 发送 EMQX 认证响应
245 227 * 根据 EMQX 官方文档要求,必须返回 JSON 格式响应
246 228 *
247   - * @param context 路由上下文
248   - * @param result 认证结果:allow、deny、ignore
249   - * @param isSuperuser 是否超级用户
250   - * @param message 日志消息(仅用于日志记录,不返回给EMQX)
  229 + * @param context 路由上下文
  230 + * @param result 认证结果:allow、deny、ignore
251 231 */
252   - private void sendAuthResponse(RoutingContext context, String result, boolean isSuperuser, String message) {
  232 + private void sendAuthResponse(RoutingContext context, String result) {
253 233 // 构建符合 EMQX 官方规范的响应
254 234 JsonObject response = new JsonObject()
255 235 .put("result", result)
256   - .put("is_superuser", isSuperuser);
  236 + .put("is_superuser", false);
257 237  
258 238 // 可以根据业务需求添加客户端属性
259 239 // response.put("client_attrs", new JsonObject().put("role", "device"));
... ... @@ -261,7 +241,6 @@ public class IotMqttHttpAuthHandler {
261 241 // 可以添加认证过期时间(可选)
262 242 // response.put("expire_at", System.currentTimeMillis() / 1000 + 3600);
263 243  
264   - // 记录详细的响应日志(message仅用于日志,不返回给EMQX)
265 244 context.response()
266 245 .setStatusCode(SUCCESS_STATUS_CODE)
267 246 .putHeader("Content-Type", "application/json; charset=utf-8")
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
... ... @@ -7,9 +7,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol
7 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8 8 import io.vertx.mqtt.messages.MqttPublishMessage;
9 9 import lombok.extern.slf4j.Slf4j;
10   -import org.springframework.util.Assert;
11   -
12   -import java.util.Arrays;
13 10  
14 11 /**
15 12 * IoT 网关 MQTT 上行消息处理器
... ... @@ -32,38 +29,24 @@ public class IotMqttUpstreamHandler {
32 29 * 处理 MQTT 发布消息
33 30 */
34 31 public void handle(MqttPublishMessage mqttMessage) {
  32 + log.info("[handle][收到 MQTT 消息, topic: {}, payload: {}]", mqttMessage.topicName(), mqttMessage.payload());
35 33 String topic = mqttMessage.topicName();
36 34 byte[] payload = mqttMessage.payload().getBytes();
37 35 try {
38   - // 1. 前置校验
39   - if (StrUtil.isBlank(topic)) {
40   - log.warn("[handle][主题为空, 忽略消息]");
41   - return;
42   - }
43   -
44   - // 2.1 识别并验证消息类型
45   - String messageType = getMessageType(topic);
46   - // TODO @haohao:可以使用 hutool 的,它的字符串拼接更简单;
47   - Assert.notNull(messageType, String.format("未知的消息类型, topic(%s)", topic));
48   - // 2.2 解析主题,获取 productKey 和 deviceName
49   - // TODO @haohao:体感 getMessageType 和下面,都 split;是不是一次就 ok 拉;1)split 掉;2)2、3 位置是 productKey、deviceName;3)4 开始还是 method
  36 + // 1. 解析主题,一次性获取所有信息
50 37 String[] topicParts = topic.split("/");
51   - if (topicParts.length < 4) {
52   - log.warn("[handle][topic({}) 格式不正确,无法解析 productKey 和 deviceName]", topic);
  38 + if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) {
  39 + log.warn("[handle][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic);
53 40 return;
54 41 }
  42 +
55 43 String productKey = topicParts[2];
56 44 String deviceName = topicParts[3];
57   - // TODO @haohao:是不是要判断,部分为空,就不行呀;
58   - if (StrUtil.isAllBlank(productKey, deviceName)) {
59   - log.warn("[handle][topic({}) 格式不正确,productKey 和 deviceName 部分为空]", topic);
60   - return;
61   - }
62 45  
63 46 // 3. 解码消息
64 47 IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(payload, productKey, deviceName);
65 48 if (message == null) {
66   - log.warn("[handle][topic({}) payload({}) 消息解码失败", topic, new String(payload));
  49 + log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
67 50 return;
68 51 }
69 52  
... ... @@ -74,22 +57,4 @@ public class IotMqttUpstreamHandler {
74 57 }
75 58 }
76 59  
77   - // TODO @haohao:是不是 getMethodFromTopic?
78   - /**
79   - * 从主题中,获得消息类型
80   - *
81   - * @param topic 主题
82   - * @return 消息类型
83   - */
84   - private String getMessageType(String topic) {
85   - String[] topicParts = topic.split("/");
86   - // 约定:topic 第 4 个部分开始为消息类型
87   - // 例如:/sys/{productKey}/{deviceName}/thing/property/post -> thing/property/post
88   - if (topicParts.length > 4) {
89   - // TODO @haohao:是不是 subString 前 3 个,性能更好;
90   - return String.join("/", Arrays.copyOfRange(topicParts, 4, topicParts.length));
91   - }
92   - return topicParts[topicParts.length - 1];
93   - }
94   -
95 60 }
96 61 \ No newline at end of file
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application-local.yaml
... ... @@ -32,13 +32,13 @@ yudao:
32 32 # ====================================
33 33 emqx:
34 34 enabled: true
35   - http-auth-port: 8090 # MQTT HTTP 认证服务端口
36   - mqtt-host: 127.0.0.1 # MQTT Broker 地址
37   - mqtt-port: 1883 # MQTT Broker 端口
38   - mqtt-username: admin # MQTT 用户名
39   - mqtt-password: public # MQTT 密码
40   - mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID
41   - mqtt-ssl: false # 是否开启 SSL
  35 + http-port: 8090 # MQTT HTTP 服务端口
  36 + mqtt-host: 127.0.0.1 # MQTT Broker 地址
  37 + mqtt-port: 1883 # MQTT Broker 端口
  38 + mqtt-username: admin # MQTT 用户名
  39 + mqtt-password: public # MQTT 密码
  40 + mqtt-client-id: iot-gateway-mqtt # MQTT 客户端 ID
  41 + mqtt-ssl: false # 是否开启 SSL
42 42 mqtt-topics:
43 43 - "/sys/#" # 系统主题
44 44  
... ... @@ -55,4 +55,4 @@ logging:
55 55 cn.iocoder.yudao.module.iot.gateway.protocol.mqtt: DEBUG
56 56 cn.iocoder.yudao.module.iot.gateway.protocol.http: DEBUG
57 57 # MQTT 客户端日志
58   - io.vertx.mqtt: DEBUG
59 58 \ No newline at end of file
  59 +# io.vertx.mqtt: DEBUG
60 60 \ No newline at end of file
... ...