Commit 7b10b5954164a70ac9a8eb036aaa686b1ded48bf
1 parent
a4e80d45
feat:【IoT 物联网】优化 MQTT 配置属性,新增 MQTT 消息处理抽象基类
Showing
6 changed files
with
244 additions
and
97 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayProperties.java
| @@ -103,39 +103,65 @@ public class IotGatewayProperties { | @@ -103,39 +103,65 @@ public class IotGatewayProperties { | ||
| 103 | */ | 103 | */ |
| 104 | @NotNull(message = "是否开启不能为空") | 104 | @NotNull(message = "是否开启不能为空") |
| 105 | private Boolean enabled; | 105 | private Boolean enabled; |
| 106 | + | ||
| 106 | /** | 107 | /** |
| 107 | - * HTTP 认证端口 | 108 | + * HTTP 认证端口(默认:8090) |
| 108 | */ | 109 | */ |
| 109 | private Integer httpAuthPort = 8090; | 110 | private Integer httpAuthPort = 8090; |
| 111 | + | ||
| 110 | /** | 112 | /** |
| 111 | * MQTT 服务器地址 | 113 | * MQTT 服务器地址 |
| 112 | */ | 114 | */ |
| 115 | + @NotEmpty(message = "MQTT 服务器地址不能为空") | ||
| 113 | private String mqttHost; | 116 | private String mqttHost; |
| 117 | + | ||
| 114 | /** | 118 | /** |
| 115 | - * MQTT 服务器端口 | 119 | + * MQTT 服务器端口(默认:1883) |
| 116 | */ | 120 | */ |
| 117 | - private Integer mqttPort; | 121 | + @NotNull(message = "MQTT 服务器端口不能为空") |
| 122 | + private Integer mqttPort = 1883; | ||
| 123 | + | ||
| 118 | /** | 124 | /** |
| 119 | * MQTT 用户名 | 125 | * MQTT 用户名 |
| 120 | */ | 126 | */ |
| 127 | + @NotEmpty(message = "MQTT 用户名不能为空") | ||
| 121 | private String mqttUsername; | 128 | private String mqttUsername; |
| 129 | + | ||
| 122 | /** | 130 | /** |
| 123 | * MQTT 密码 | 131 | * MQTT 密码 |
| 124 | */ | 132 | */ |
| 133 | + @NotEmpty(message = "MQTT 密码不能为空") | ||
| 125 | private String mqttPassword; | 134 | private String mqttPassword; |
| 135 | + | ||
| 126 | /** | 136 | /** |
| 127 | - * MQTT 是否开启 SSL | 137 | + * MQTT 是否开启 SSL(默认:false) |
| 128 | */ | 138 | */ |
| 129 | - private Boolean mqttSsl; | 139 | + @NotNull(message = "MQTT 是否开启 SSL 不能为空") |
| 140 | + private Boolean mqttSsl = false; | ||
| 141 | + | ||
| 130 | /** | 142 | /** |
| 131 | - * MQTT客户端 ID | 143 | + * MQTT 客户端 ID(如果为空,系统将自动生成) |
| 132 | */ | 144 | */ |
| 133 | private String mqttClientId; | 145 | private String mqttClientId; |
| 146 | + | ||
| 134 | /** | 147 | /** |
| 135 | - * MQTT 主题 | 148 | + * MQTT 主题列表 |
| 136 | */ | 149 | */ |
| 150 | + @NotEmpty(message = "MQTT 主题不能为空") | ||
| 137 | private List<String> mqttTopics; | 151 | private List<String> mqttTopics; |
| 138 | 152 | ||
| 153 | + /** | ||
| 154 | + * 获取 MQTT 客户端 ID,如果未配置则自动生成 | ||
| 155 | + * | ||
| 156 | + * @return MQTT 客户端 ID | ||
| 157 | + */ | ||
| 158 | + public String getMqttClientId() { | ||
| 159 | + if (cn.hutool.core.util.StrUtil.isBlank(mqttClientId)) { | ||
| 160 | + mqttClientId = "iot-gateway-mqtt-" + System.currentTimeMillis(); | ||
| 161 | + } | ||
| 162 | + return mqttClientId; | ||
| 163 | + } | ||
| 164 | + | ||
| 139 | } | 165 | } |
| 140 | 166 | ||
| 141 | } | 167 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttDownstreamSubscriber.java
| @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; | ||
| 3 | import cn.hutool.core.util.StrUtil; | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | import cn.hutool.json.JSONObject; | 4 | import cn.hutool.json.JSONObject; |
| 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; | 5 | import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; |
| 6 | +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; | 7 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 7 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; | 8 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; |
| 8 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 9 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| @@ -47,19 +48,28 @@ public class IotMqttDownstreamSubscriber implements IotMessageSubscriber<IotDevi | @@ -47,19 +48,28 @@ public class IotMqttDownstreamSubscriber implements IotMessageSubscriber<IotDevi | ||
| 47 | 48 | ||
| 48 | @Override | 49 | @Override |
| 49 | public void onMessage(IotDeviceMessage message) { | 50 | public void onMessage(IotDeviceMessage message) { |
| 50 | - log.info("[onMessage][接收到下行消息:{}]", message); | 51 | + log.info("[onMessage][接收到下行消息][messageId: {}][method: {}][deviceId: {}]", |
| 52 | + message.getId(), message.getMethod(), message.getDeviceId()); | ||
| 51 | try { | 53 | try { |
| 52 | // 根据消息方法处理不同的下行消息 | 54 | // 根据消息方法处理不同的下行消息 |
| 53 | String method = message.getMethod(); | 55 | String method = message.getMethod(); |
| 54 | if (method == null) { | 56 | if (method == null) { |
| 55 | - log.warn("[onMessage][消息({})方法为空]", message); | 57 | + log.warn("[onMessage][消息方法为空][messageId: {}][deviceId: {}]", |
| 58 | + message.getId(), message.getDeviceId()); | ||
| 59 | + return; | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + // 过滤上行消息:下行订阅者只处理下行消息 | ||
| 63 | + if (isUpstreamMessage(method)) { | ||
| 64 | + log.debug("[onMessage][忽略上行消息][method: {}][messageId: {}]", method, message.getId()); | ||
| 56 | return; | 65 | return; |
| 57 | } | 66 | } |
| 58 | 67 | ||
| 59 | // 处理下行消息 | 68 | // 处理下行消息 |
| 60 | handleDownstreamMessage(message); | 69 | handleDownstreamMessage(message); |
| 61 | } catch (Exception e) { | 70 | } catch (Exception e) { |
| 62 | - log.error("[onMessage][处理下行消息失败:{}]", message, e); | 71 | + log.error("[onMessage][处理下行消息失败][messageId: {}][method: {}][deviceId: {}]", |
| 72 | + message.getId(), message.getMethod(), message.getDeviceId(), e); | ||
| 63 | } | 73 | } |
| 64 | } | 74 | } |
| 65 | 75 | ||
| @@ -155,4 +165,15 @@ public class IotMqttDownstreamSubscriber implements IotMessageSubscriber<IotDevi | @@ -155,4 +165,15 @@ public class IotMqttDownstreamSubscriber implements IotMessageSubscriber<IotDevi | ||
| 155 | return payload; | 165 | return payload; |
| 156 | } | 166 | } |
| 157 | 167 | ||
| 168 | + /** | ||
| 169 | + * 判断是否为上行消息 | ||
| 170 | + * | ||
| 171 | + * @param method 消息方法 | ||
| 172 | + * @return 是否为上行消息 | ||
| 173 | + */ | ||
| 174 | + private boolean isUpstreamMessage(String method) { | ||
| 175 | + IotDeviceMessageMethodEnum methodEnum = IotDeviceMessageMethodEnum.of(method); | ||
| 176 | + return methodEnum != null && methodEnum.getUpstream(); | ||
| 177 | + } | ||
| 178 | + | ||
| 158 | } | 179 | } |
| 159 | \ No newline at end of file | 180 | \ No newline at end of file |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttUpstreamProtocol.java
| @@ -37,10 +37,20 @@ import java.util.concurrent.TimeUnit; | @@ -37,10 +37,20 @@ import java.util.concurrent.TimeUnit; | ||
| 37 | public class IotMqttUpstreamProtocol { | 37 | public class IotMqttUpstreamProtocol { |
| 38 | 38 | ||
| 39 | /** | 39 | /** |
| 40 | - * 默认 QoS 级别 | 40 | + * 默认 QoS 级别 - 至少一次 |
| 41 | */ | 41 | */ |
| 42 | private static final MqttQoS DEFAULT_QOS = MqttQoS.AT_LEAST_ONCE; | 42 | private static final MqttQoS DEFAULT_QOS = MqttQoS.AT_LEAST_ONCE; |
| 43 | 43 | ||
| 44 | + /** | ||
| 45 | + * 连接超时时间(秒) | ||
| 46 | + */ | ||
| 47 | + private static final int CONNECT_TIMEOUT_SECONDS = 10; | ||
| 48 | + | ||
| 49 | + /** | ||
| 50 | + * 重连延迟时间(毫秒) | ||
| 51 | + */ | ||
| 52 | + private static final long RECONNECT_DELAY_MS = 5000; | ||
| 53 | + | ||
| 44 | private final IotGatewayProperties.EmqxProperties emqxProperties; | 54 | private final IotGatewayProperties.EmqxProperties emqxProperties; |
| 45 | 55 | ||
| 46 | // 共享资源 | 56 | // 共享资源 |
| @@ -227,25 +237,26 @@ public class IotMqttUpstreamProtocol { | @@ -227,25 +237,26 @@ public class IotMqttUpstreamProtocol { | ||
| 227 | * 连接 MQTT Broker 并订阅主题 | 237 | * 连接 MQTT Broker 并订阅主题 |
| 228 | */ | 238 | */ |
| 229 | private void connectMqtt() { | 239 | private void connectMqtt() { |
| 240 | + // 参数校验 | ||
| 230 | String host = emqxProperties.getMqttHost(); | 241 | String host = emqxProperties.getMqttHost(); |
| 231 | Integer port = emqxProperties.getMqttPort(); | 242 | Integer port = emqxProperties.getMqttPort(); |
| 232 | 243 | ||
| 233 | if (StrUtil.isBlank(host)) { | 244 | if (StrUtil.isBlank(host)) { |
| 234 | - String msg = "[connectMqtt][MQTT Host 为空,无法连接]"; | ||
| 235 | - log.error(msg); | ||
| 236 | - return; | 245 | + log.error("[connectMqtt][MQTT Host 为空,无法连接]"); |
| 246 | + throw new IllegalArgumentException("MQTT Host 不能为空"); | ||
| 237 | } | 247 | } |
| 238 | - if (port == null) { | ||
| 239 | - log.warn("[connectMqtt][MQTT Port 为 null,使用默认端口 1883]"); | ||
| 240 | - port = 1883; | 248 | + if (port == null || port <= 0) { |
| 249 | + log.error("[connectMqtt][MQTT Port 无效:{}]", port); | ||
| 250 | + throw new IllegalArgumentException("MQTT Port 必须为正整数"); | ||
| 241 | } | 251 | } |
| 242 | 252 | ||
| 243 | - final Integer finalPort = port; | ||
| 244 | - CompletableFuture<Void> connectFuture = mqttClient.connect(finalPort, host) | 253 | + log.info("[connectMqtt][开始连接 MQTT Broker][host: {}][port: {}]", host, port); |
| 254 | + | ||
| 255 | + CompletableFuture<Void> connectFuture = mqttClient.connect(port, host) | ||
| 245 | .toCompletionStage() | 256 | .toCompletionStage() |
| 246 | .toCompletableFuture() | 257 | .toCompletableFuture() |
| 247 | .thenAccept(connAck -> { | 258 | .thenAccept(connAck -> { |
| 248 | - log.info("[connectMqtt][MQTT 客户端连接成功]"); | 259 | + log.info("[connectMqtt][MQTT 客户端连接成功][host: {}][port: {}]", host, port); |
| 249 | // 设置断开重连监听器 | 260 | // 设置断开重连监听器 |
| 250 | mqttClient.closeHandler(closeEvent -> { | 261 | mqttClient.closeHandler(closeEvent -> { |
| 251 | log.warn("[closeHandler][MQTT 连接已断开,准备重连]"); | 262 | log.warn("[closeHandler][MQTT 连接已断开,准备重连]"); |
| @@ -257,17 +268,19 @@ public class IotMqttUpstreamProtocol { | @@ -257,17 +268,19 @@ public class IotMqttUpstreamProtocol { | ||
| 257 | subscribeToTopics(); | 268 | subscribeToTopics(); |
| 258 | }) | 269 | }) |
| 259 | .exceptionally(error -> { | 270 | .exceptionally(error -> { |
| 260 | - log.error("[connectMqtt][连接 MQTT Broker 失败]", error); | 271 | + log.error("[connectMqtt][连接 MQTT Broker 失败][host: {}][port: {}]", host, port, error); |
| 272 | + // 连接失败时也要尝试重连 | ||
| 261 | reconnectWithDelay(); | 273 | reconnectWithDelay(); |
| 262 | return null; | 274 | return null; |
| 263 | }); | 275 | }); |
| 264 | 276 | ||
| 265 | // 等待连接完成 | 277 | // 等待连接完成 |
| 266 | try { | 278 | try { |
| 267 | - connectFuture.get(10, TimeUnit.SECONDS); | 279 | + connectFuture.get(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS); |
| 268 | log.info("[connectMqtt][MQTT 客户端启动完成]"); | 280 | log.info("[connectMqtt][MQTT 客户端启动完成]"); |
| 269 | } catch (Exception e) { | 281 | } catch (Exception e) { |
| 270 | log.error("[connectMqtt][MQTT 客户端启动失败]", e); | 282 | log.error("[connectMqtt][MQTT 客户端启动失败]", e); |
| 283 | + throw new RuntimeException("MQTT 客户端启动失败", e); | ||
| 271 | } | 284 | } |
| 272 | } | 285 | } |
| 273 | 286 | ||
| @@ -284,15 +297,19 @@ public class IotMqttUpstreamProtocol { | @@ -284,15 +297,19 @@ public class IotMqttUpstreamProtocol { | ||
| 284 | */ | 297 | */ |
| 285 | private void subscribeToTopics() { | 298 | private void subscribeToTopics() { |
| 286 | List<String> topicList = emqxProperties.getMqttTopics(); | 299 | List<String> topicList = emqxProperties.getMqttTopics(); |
| 287 | - if (CollUtil.isEmpty(topicList)) { | ||
| 288 | - log.warn("[subscribeToTopics][没有配置要订阅的主题]"); | ||
| 289 | - return; | ||
| 290 | - } | 300 | + // @NotEmpty 注解已保证 topicList 不为空,无需重复校验 |
| 301 | + | ||
| 302 | + log.info("[subscribeToTopics][开始订阅主题,共 {} 个]", topicList.size()); | ||
| 291 | 303 | ||
| 292 | for (String topic : topicList) { | 304 | for (String topic : topicList) { |
| 305 | + if (StrUtil.isBlank(topic)) { | ||
| 306 | + log.warn("[subscribeToTopics][跳过空主题]"); | ||
| 307 | + continue; | ||
| 308 | + } | ||
| 309 | + | ||
| 293 | mqttClient.subscribe(topic, DEFAULT_QOS.value(), subscribeResult -> { | 310 | mqttClient.subscribe(topic, DEFAULT_QOS.value(), subscribeResult -> { |
| 294 | if (subscribeResult.succeeded()) { | 311 | if (subscribeResult.succeeded()) { |
| 295 | - log.info("[subscribeToTopics][订阅主题成功: {}]", topic); | 312 | + log.info("[subscribeToTopics][订阅主题成功: {}][QoS: {}]", topic, DEFAULT_QOS.value()); |
| 296 | } else { | 313 | } else { |
| 297 | log.error("[subscribeToTopics][订阅主题失败: {}]", topic, subscribeResult.cause()); | 314 | log.error("[subscribeToTopics][订阅主题失败: {}]", topic, subscribeResult.cause()); |
| 298 | } | 315 | } |
| @@ -304,10 +321,16 @@ public class IotMqttUpstreamProtocol { | @@ -304,10 +321,16 @@ public class IotMqttUpstreamProtocol { | ||
| 304 | * 延迟重连 | 321 | * 延迟重连 |
| 305 | */ | 322 | */ |
| 306 | private void reconnectWithDelay() { | 323 | private void reconnectWithDelay() { |
| 307 | - vertx.setTimer(5000, timerId -> { | 324 | + vertx.setTimer(RECONNECT_DELAY_MS, timerId -> { |
| 308 | if (isRunning && (mqttClient == null || !mqttClient.isConnected())) { | 325 | if (isRunning && (mqttClient == null || !mqttClient.isConnected())) { |
| 309 | - log.info("[reconnectWithDelay][开始重连 MQTT Broker]"); | ||
| 310 | - connectMqtt(); | 326 | + log.info("[reconnectWithDelay][开始重连 MQTT Broker,延迟 {} 毫秒]", RECONNECT_DELAY_MS); |
| 327 | + try { | ||
| 328 | + connectMqtt(); | ||
| 329 | + } catch (Exception e) { | ||
| 330 | + log.error("[reconnectWithDelay][重连失败,将继续尝试重连]", e); | ||
| 331 | + // 重连失败时继续尝试重连 | ||
| 332 | + reconnectWithDelay(); | ||
| 333 | + } | ||
| 311 | } | 334 | } |
| 312 | }); | 335 | }); |
| 313 | } | 336 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttAbstractHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.StrUtil; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * IoT 网关 MQTT 协议的处理器抽象基类 | ||
| 8 | + * <p> | ||
| 9 | + * 提供通用的异常处理、参数校验等功能 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +@Slf4j | ||
| 14 | +public abstract class IotMqttAbstractHandler { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 处理 MQTT 消息的模板方法 | ||
| 18 | + * | ||
| 19 | + * @param topic 主题 | ||
| 20 | + * @param payload 消息内容 | ||
| 21 | + */ | ||
| 22 | + public final void handle(String topic, String payload) { | ||
| 23 | + try { | ||
| 24 | + // 1. 前置校验 | ||
| 25 | + if (!validateInput(topic, payload)) { | ||
| 26 | + return; | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + // 2. 执行具体逻辑 | ||
| 30 | + doHandle(topic, payload); | ||
| 31 | + | ||
| 32 | + } catch (Exception e) { | ||
| 33 | + log.error("[handle][处理 MQTT 消息失败][topic: {}][payload: {}]", topic, payload, e); | ||
| 34 | + handleException(topic, payload, e); | ||
| 35 | + } | ||
| 36 | + } | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 具体的处理逻辑,由子类实现 | ||
| 40 | + * | ||
| 41 | + * @param topic 主题 | ||
| 42 | + * @param payload 消息内容 | ||
| 43 | + */ | ||
| 44 | + protected abstract void doHandle(String topic, String payload); | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 输入参数校验 | ||
| 48 | + * | ||
| 49 | + * @param topic 主题 | ||
| 50 | + * @param payload 消息内容 | ||
| 51 | + * @return 校验是否通过 | ||
| 52 | + */ | ||
| 53 | + protected boolean validateInput(String topic, String payload) { | ||
| 54 | + if (StrUtil.isBlank(topic)) { | ||
| 55 | + log.warn("[validateInput][主题为空,忽略消息]"); | ||
| 56 | + return false; | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + if (StrUtil.isBlank(payload)) { | ||
| 60 | + log.warn("[validateInput][消息内容为空][topic: {}]", topic); | ||
| 61 | + return false; | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + return true; | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + /** | ||
| 68 | + * 异常处理 | ||
| 69 | + * | ||
| 70 | + * @param topic 主题 | ||
| 71 | + * @param payload 消息内容 | ||
| 72 | + * @param e 异常 | ||
| 73 | + */ | ||
| 74 | + protected void handleException(String topic, String payload, Exception e) { | ||
| 75 | + // 默认实现:记录错误日志 | ||
| 76 | + // 子类可以重写此方法,添加特定的异常处理逻辑 | ||
| 77 | + log.error("[handleException][MQTT 消息处理异常][topic: {}]", topic, e); | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + /** | ||
| 81 | + * 解析主题,获取主题各部分 | ||
| 82 | + * | ||
| 83 | + * @param topic 主题 | ||
| 84 | + * @return 主题各部分数组,如果解析失败返回 null | ||
| 85 | + */ | ||
| 86 | + protected String[] parseTopic(String topic) { | ||
| 87 | + String[] topicParts = topic.split("/"); | ||
| 88 | + if (topicParts.length < 7) { | ||
| 89 | + log.warn("[parseTopic][主题格式不正确][topic: {}]", topic); | ||
| 90 | + return null; | ||
| 91 | + } | ||
| 92 | + return topicParts; | ||
| 93 | + } | ||
| 94 | +} | ||
| 0 | \ No newline at end of file | 95 | \ 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
| @@ -13,10 +13,12 @@ import io.vertx.core.json.JsonObject; | @@ -13,10 +13,12 @@ import io.vertx.core.json.JsonObject; | ||
| 13 | import io.vertx.ext.web.RoutingContext; | 13 | import io.vertx.ext.web.RoutingContext; |
| 14 | import lombok.extern.slf4j.Slf4j; | 14 | import lombok.extern.slf4j.Slf4j; |
| 15 | 15 | ||
| 16 | +import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_AUTH_FAIL; | ||
| 17 | + | ||
| 16 | /** | 18 | /** |
| 17 | * IoT 网关 MQTT HTTP 认证处理器 | 19 | * IoT 网关 MQTT HTTP 认证处理器 |
| 18 | * <p> | 20 | * <p> |
| 19 | - * 处理 EMQX 的认证请求和事件钩子 | 21 | + * 处理 EMQX 的认证请求和事件钩子,提供统一的错误处理和参数校验 |
| 20 | * | 22 | * |
| 21 | * @author 芋道源码 | 23 | * @author 芋道源码 |
| 22 | */ | 24 | */ |
| @@ -24,6 +26,26 @@ import lombok.extern.slf4j.Slf4j; | @@ -24,6 +26,26 @@ import lombok.extern.slf4j.Slf4j; | ||
| 24 | public class IotMqttHttpAuthHandler { | 26 | public class IotMqttHttpAuthHandler { |
| 25 | 27 | ||
| 26 | /** | 28 | /** |
| 29 | + * 认证成功状态码 | ||
| 30 | + */ | ||
| 31 | + private static final int SUCCESS_STATUS_CODE = 200; | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 参数错误状态码 | ||
| 35 | + */ | ||
| 36 | + private static final int BAD_REQUEST_STATUS_CODE = 400; | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * 认证失败状态码 | ||
| 40 | + */ | ||
| 41 | + private static final int UNAUTHORIZED_STATUS_CODE = 401; | ||
| 42 | + | ||
| 43 | + /** | ||
| 44 | + * 服务器错误状态码 | ||
| 45 | + */ | ||
| 46 | + private static final int INTERNAL_ERROR_STATUS_CODE = 500; | ||
| 47 | + | ||
| 48 | + /** | ||
| 27 | * EMQX 认证接口 | 49 | * EMQX 认证接口 |
| 28 | */ | 50 | */ |
| 29 | public void authenticate(RoutingContext context) { | 51 | public void authenticate(RoutingContext context) { |
| @@ -44,7 +66,7 @@ public class IotMqttHttpAuthHandler { | @@ -44,7 +66,7 @@ public class IotMqttHttpAuthHandler { | ||
| 44 | // 参数校验 | 66 | // 参数校验 |
| 45 | if (StrUtil.isEmpty(clientid) || StrUtil.isEmpty(username) || StrUtil.isEmpty(password)) { | 67 | if (StrUtil.isEmpty(clientid) || StrUtil.isEmpty(username) || StrUtil.isEmpty(password)) { |
| 46 | log.warn("[authenticate][认证参数不完整][clientId: {}][username: {}]", clientid, username); | 68 | log.warn("[authenticate][认证参数不完整][clientId: {}][username: {}]", clientid, username); |
| 47 | - sendErrorResponse(context, 400, "认证参数不完整"); | 69 | + sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "认证参数不完整"); |
| 48 | return; | 70 | return; |
| 49 | } | 71 | } |
| 50 | 72 | ||
| @@ -58,7 +80,7 @@ public class IotMqttHttpAuthHandler { | @@ -58,7 +80,7 @@ public class IotMqttHttpAuthHandler { | ||
| 58 | result.checkError(); | 80 | result.checkError(); |
| 59 | if (!BooleanUtil.isTrue(result.getData())) { | 81 | if (!BooleanUtil.isTrue(result.getData())) { |
| 60 | log.warn("[authenticate][设备认证失败][clientId: {}][username: {}]", clientid, username); | 82 | log.warn("[authenticate][设备认证失败][clientId: {}][username: {}]", clientid, username); |
| 61 | - sendErrorResponse(context, 401, "设备认证失败"); | 83 | + sendErrorResponse(context, UNAUTHORIZED_STATUS_CODE, DEVICE_AUTH_FAIL.getMsg()); |
| 62 | return; | 84 | return; |
| 63 | } | 85 | } |
| 64 | 86 | ||
| @@ -67,7 +89,7 @@ public class IotMqttHttpAuthHandler { | @@ -67,7 +89,7 @@ public class IotMqttHttpAuthHandler { | ||
| 67 | 89 | ||
| 68 | } catch (Exception e) { | 90 | } catch (Exception e) { |
| 69 | log.error("[authenticate][设备认证异常]", e); | 91 | log.error("[authenticate][设备认证异常]", e); |
| 70 | - sendErrorResponse(context, 500, "认证服务异常"); | 92 | + sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "认证服务异常"); |
| 71 | } | 93 | } |
| 72 | } | 94 | } |
| 73 | 95 | ||
| @@ -79,7 +101,7 @@ public class IotMqttHttpAuthHandler { | @@ -79,7 +101,7 @@ public class IotMqttHttpAuthHandler { | ||
| 79 | // 解析请求体 | 101 | // 解析请求体 |
| 80 | JsonObject body = context.body().asJsonObject(); | 102 | JsonObject body = context.body().asJsonObject(); |
| 81 | if (body == null) { | 103 | if (body == null) { |
| 82 | - sendErrorResponse(context, 400, "请求体不能为空"); | 104 | + sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "请求体不能为空"); |
| 83 | return; | 105 | return; |
| 84 | } | 106 | } |
| 85 | 107 | ||
| @@ -94,7 +116,7 @@ public class IotMqttHttpAuthHandler { | @@ -94,7 +116,7 @@ public class IotMqttHttpAuthHandler { | ||
| 94 | 116 | ||
| 95 | } catch (Exception e) { | 117 | } catch (Exception e) { |
| 96 | log.error("[connected][处理设备连接事件失败]", e); | 118 | log.error("[connected][处理设备连接事件失败]", e); |
| 97 | - sendErrorResponse(context, 500, "处理失败"); | 119 | + sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "处理失败"); |
| 98 | } | 120 | } |
| 99 | } | 121 | } |
| 100 | 122 | ||
| @@ -106,7 +128,7 @@ public class IotMqttHttpAuthHandler { | @@ -106,7 +128,7 @@ public class IotMqttHttpAuthHandler { | ||
| 106 | // 解析请求体 | 128 | // 解析请求体 |
| 107 | JsonObject body = context.body().asJsonObject(); | 129 | JsonObject body = context.body().asJsonObject(); |
| 108 | if (body == null) { | 130 | if (body == null) { |
| 109 | - sendErrorResponse(context, 400, "请求体不能为空"); | 131 | + sendErrorResponse(context, BAD_REQUEST_STATUS_CODE, "请求体不能为空"); |
| 110 | return; | 132 | return; |
| 111 | } | 133 | } |
| 112 | 134 | ||
| @@ -123,7 +145,7 @@ public class IotMqttHttpAuthHandler { | @@ -123,7 +145,7 @@ public class IotMqttHttpAuthHandler { | ||
| 123 | 145 | ||
| 124 | } catch (Exception e) { | 146 | } catch (Exception e) { |
| 125 | log.error("[disconnected][处理设备断开连接事件失败]", e); | 147 | log.error("[disconnected][处理设备断开连接事件失败]", e); |
| 126 | - sendErrorResponse(context, 500, "处理失败"); | 148 | + sendErrorResponse(context, INTERNAL_ERROR_STATUS_CODE, "处理失败"); |
| 127 | } | 149 | } |
| 128 | } | 150 | } |
| 129 | 151 | ||
| @@ -175,7 +197,7 @@ public class IotMqttHttpAuthHandler { | @@ -175,7 +197,7 @@ public class IotMqttHttpAuthHandler { | ||
| 175 | */ | 197 | */ |
| 176 | private void sendSuccessResponse(RoutingContext context, String message) { | 198 | private void sendSuccessResponse(RoutingContext context, String message) { |
| 177 | context.response() | 199 | context.response() |
| 178 | - .setStatusCode(200) | 200 | + .setStatusCode(SUCCESS_STATUS_CODE) |
| 179 | .putHeader("Content-Type", "text/plain; charset=utf-8") | 201 | .putHeader("Content-Type", "text/plain; charset=utf-8") |
| 180 | .end(message); | 202 | .end(message); |
| 181 | } | 203 | } |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
| 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.router; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.util.StrUtil; | ||
| 4 | import cn.hutool.extra.spring.SpringUtil; | 3 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 4 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 6 | -import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; | ||
| 7 | import cn.iocoder.yudao.module.iot.gateway.enums.IotDeviceTopicEnum; | 5 | import cn.iocoder.yudao.module.iot.gateway.enums.IotDeviceTopicEnum; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 6 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 9 | import io.vertx.mqtt.messages.MqttPublishMessage; | 7 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| @@ -19,14 +17,12 @@ import java.nio.charset.StandardCharsets; | @@ -19,14 +17,12 @@ import java.nio.charset.StandardCharsets; | ||
| 19 | * @author 芋道源码 | 17 | * @author 芋道源码 |
| 20 | */ | 18 | */ |
| 21 | @Slf4j | 19 | @Slf4j |
| 22 | -public class IotMqttUpstreamHandler { | 20 | +public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { |
| 23 | 21 | ||
| 24 | - private final IotDeviceMessageProducer deviceMessageProducer; | ||
| 25 | private final IotDeviceMessageService deviceMessageService; | 22 | private final IotDeviceMessageService deviceMessageService; |
| 26 | private final String serverId; | 23 | private final String serverId; |
| 27 | 24 | ||
| 28 | public IotMqttUpstreamHandler(cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol protocol) { | 25 | public IotMqttUpstreamHandler(cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.IotMqttUpstreamProtocol protocol) { |
| 29 | - this.deviceMessageProducer = SpringUtil.getBean(IotDeviceMessageProducer.class); | ||
| 30 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); | 26 | this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); |
| 31 | this.serverId = protocol.getServerId(); | 27 | this.serverId = protocol.getServerId(); |
| 32 | } | 28 | } |
| @@ -38,41 +34,22 @@ public class IotMqttUpstreamHandler { | @@ -38,41 +34,22 @@ public class IotMqttUpstreamHandler { | ||
| 38 | String topic = message.topicName(); | 34 | String topic = message.topicName(); |
| 39 | String payload = message.payload().toString(StandardCharsets.UTF_8); | 35 | String payload = message.payload().toString(StandardCharsets.UTF_8); |
| 40 | 36 | ||
| 41 | - if (StrUtil.isBlank(topic)) { | ||
| 42 | - log.warn("[handle][主题为空,忽略消息]"); | ||
| 43 | - return; | ||
| 44 | - } | ||
| 45 | - | ||
| 46 | - if (StrUtil.isBlank(payload)) { | ||
| 47 | - log.warn("[handle][消息内容为空][topic: {}]", topic); | ||
| 48 | - return; | ||
| 49 | - } | ||
| 50 | - | ||
| 51 | log.debug("[handle][收到 MQTT 消息][topic: {}]", topic); | 37 | log.debug("[handle][收到 MQTT 消息][topic: {}]", topic); |
| 38 | + // 调用父类的 handle 方法,父类会进行参数校验 | ||
| 52 | handle(topic, payload); | 39 | handle(topic, payload); |
| 53 | } | 40 | } |
| 54 | 41 | ||
| 55 | - /** | ||
| 56 | - * 处理 MQTT 消息 | ||
| 57 | - * | ||
| 58 | - * @param topic 主题 | ||
| 59 | - * @param payload 消息内容 | ||
| 60 | - */ | ||
| 61 | - public void handle(String topic, String payload) { | ||
| 62 | - try { | ||
| 63 | - // 1. 识别并验证消息类型 | ||
| 64 | - String messageType = getMessageType(topic); | ||
| 65 | - if (messageType == null) { | ||
| 66 | - log.warn("[handle][未知的消息类型][topic: {}]", topic); | ||
| 67 | - return; | ||
| 68 | - } | ||
| 69 | - | ||
| 70 | - // 2. 处理消息 | ||
| 71 | - processMessage(topic, payload, messageType); | ||
| 72 | - | ||
| 73 | - } catch (Exception e) { | ||
| 74 | - log.error("[handle][处理消息失败][topic: {}][payload: {}]", topic, payload, e); | 42 | + @Override |
| 43 | + protected void doHandle(String topic, String payload) { | ||
| 44 | + // 1. 识别并验证消息类型 | ||
| 45 | + String messageType = getMessageType(topic); | ||
| 46 | + if (messageType == null) { | ||
| 47 | + log.warn("[doHandle][未知的消息类型][topic: {}]", topic); | ||
| 48 | + return; | ||
| 75 | } | 49 | } |
| 50 | + | ||
| 51 | + // 2. 处理消息 | ||
| 52 | + processMessage(topic, payload, messageType); | ||
| 76 | } | 53 | } |
| 77 | 54 | ||
| 78 | /** | 55 | /** |
| @@ -109,39 +86,37 @@ public class IotMqttUpstreamHandler { | @@ -109,39 +86,37 @@ public class IotMqttUpstreamHandler { | ||
| 109 | * @return 消息类型描述,如果不支持返回 null | 86 | * @return 消息类型描述,如果不支持返回 null |
| 110 | */ | 87 | */ |
| 111 | private String getMessageType(String topic) { | 88 | private String getMessageType(String topic) { |
| 112 | - if (StrUtil.isBlank(topic)) { | ||
| 113 | - return null; | ||
| 114 | - } | 89 | + // 此方法由 doHandle 调用,topic 已经在父类中校验过,无需重复校验 |
| 115 | 90 | ||
| 116 | // 按优先级匹配主题类型,避免误匹配 | 91 | // 按优先级匹配主题类型,避免误匹配 |
| 117 | 92 | ||
| 118 | - // 1. 设备属性上报: /sys/{productKey}/{deviceName}/thing/event/property/post | 93 | + // 1. 设备属性上报: /sys/{productKey}/{deviceName}/thing/property/post |
| 119 | if (isPropertyPostTopic(topic)) { | 94 | if (isPropertyPostTopic(topic)) { |
| 120 | return IotDeviceTopicEnum.PROPERTY_POST_TOPIC.getDescription(); | 95 | return IotDeviceTopicEnum.PROPERTY_POST_TOPIC.getDescription(); |
| 121 | } | 96 | } |
| 122 | 97 | ||
| 123 | - // 2. 设备事件上报: /sys/{productKey}/{deviceName}/thing/event/{eventIdentifier}/post | 98 | + // 2. 设备事件上报: /sys/{productKey}/{deviceName}/thing/{eventIdentifier}/post |
| 124 | if (isEventPostTopic(topic)) { | 99 | if (isEventPostTopic(topic)) { |
| 125 | return "设备事件上报"; | 100 | return "设备事件上报"; |
| 126 | } | 101 | } |
| 127 | 102 | ||
| 128 | - // 3. 设备属性设置响应: /sys/{productKey}/{deviceName}/thing/service/property/set_reply | 103 | + // 3. 设备属性设置响应: /sys/{productKey}/{deviceName}/thing/property/set_reply |
| 129 | if (isPropertySetReplyTopic(topic)) { | 104 | if (isPropertySetReplyTopic(topic)) { |
| 130 | return "设备属性设置响应"; | 105 | return "设备属性设置响应"; |
| 131 | } | 106 | } |
| 132 | 107 | ||
| 133 | - // 4. 设备属性获取响应: /sys/{productKey}/{deviceName}/thing/service/property/get_reply | 108 | + // 4. 设备属性获取响应: /sys/{productKey}/{deviceName}/thing/property/get_reply |
| 134 | if (isPropertyGetReplyTopic(topic)) { | 109 | if (isPropertyGetReplyTopic(topic)) { |
| 135 | return "设备属性获取响应"; | 110 | return "设备属性获取响应"; |
| 136 | } | 111 | } |
| 137 | 112 | ||
| 138 | - // 5. 设备配置设置响应: /sys/{productKey}/{deviceName}/thing/service/config/set_reply | 113 | + // 5. 设备配置设置响应: /sys/{productKey}/{deviceName}/thing/config/set_reply |
| 139 | if (isConfigSetReplyTopic(topic)) { | 114 | if (isConfigSetReplyTopic(topic)) { |
| 140 | return IotDeviceTopicEnum.CONFIG_SET_TOPIC.getDescription() + "响应"; | 115 | return IotDeviceTopicEnum.CONFIG_SET_TOPIC.getDescription() + "响应"; |
| 141 | } | 116 | } |
| 142 | 117 | ||
| 143 | // 6. 设备 OTA 升级响应: | 118 | // 6. 设备 OTA 升级响应: |
| 144 | - // /sys/{productKey}/{deviceName}/thing/service/ota/upgrade_reply | 119 | + // /sys/{productKey}/{deviceName}/thing/ota/upgrade_reply |
| 145 | if (isOtaUpgradeReplyTopic(topic)) { | 120 | if (isOtaUpgradeReplyTopic(topic)) { |
| 146 | return IotDeviceTopicEnum.OTA_UPGRADE_TOPIC.getDescription() + "响应"; | 121 | return IotDeviceTopicEnum.OTA_UPGRADE_TOPIC.getDescription() + "响应"; |
| 147 | } | 122 | } |
| @@ -214,18 +189,4 @@ public class IotMqttUpstreamHandler { | @@ -214,18 +189,4 @@ public class IotMqttUpstreamHandler { | ||
| 214 | && !topic.contains("ota"); | 189 | && !topic.contains("ota"); |
| 215 | } | 190 | } |
| 216 | 191 | ||
| 217 | - /** | ||
| 218 | - * 解析主题,获取主题各部分 | ||
| 219 | - * | ||
| 220 | - * @param topic 主题 | ||
| 221 | - * @return 主题各部分数组,如果解析失败返回 null | ||
| 222 | - */ | ||
| 223 | - private String[] parseTopic(String topic) { | ||
| 224 | - String[] topicParts = topic.split("/"); | ||
| 225 | - if (topicParts.length < 7) { | ||
| 226 | - log.warn("[parseTopic][主题格式不正确][topic: {}]", topic); | ||
| 227 | - return null; | ||
| 228 | - } | ||
| 229 | - return topicParts; | ||
| 230 | - } | ||
| 231 | } | 192 | } |
| 232 | \ No newline at end of file | 193 | \ No newline at end of file |