Commit 4f6ee4c5eb49238cbe5e049a4740df0cd4609ff2
1 parent
83990086
feat(iot):【协议改造】优化各种 code review 的代码
Showing
12 changed files
with
97 additions
and
66 deletions
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/http/IotHttpProtocol.java
| ... | ... | @@ -125,7 +125,11 @@ public class IotHttpProtocol implements IotProtocol { |
| 125 | 125 | this.downstreamSubscriber.start(); |
| 126 | 126 | } catch (Exception e) { |
| 127 | 127 | log.error("[start][IoT HTTP 协议 {} 启动失败]", getId(), e); |
| 128 | - // 启动失败时关闭 Vertx | |
| 128 | + // 启动失败时关闭资源 | |
| 129 | + if (httpServer != null) { | |
| 130 | + httpServer.close(); | |
| 131 | + httpServer = null; | |
| 132 | + } | |
| 129 | 133 | if (vertx != null) { |
| 130 | 134 | vertx.close(); |
| 131 | 135 | vertx = null; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttConfig.java
| ... | ... | @@ -27,13 +27,6 @@ public class IotMqttConfig { |
| 27 | 27 | private Integer connectTimeoutSeconds = 60; |
| 28 | 28 | |
| 29 | 29 | /** |
| 30 | - * 保持连接超时时间(秒) | |
| 31 | - */ | |
| 32 | - @NotNull(message = "保持连接超时时间不能为空") | |
| 33 | - @Min(value = 1, message = "保持连接超时时间不能小于 1 秒") | |
| 34 | - private Integer keepAliveTimeoutSeconds = 300; | |
| 35 | - | |
| 36 | - /** | |
| 37 | 30 | * 是否启用 SSL |
| 38 | 31 | */ |
| 39 | 32 | @NotNull(message = "是否启用 SSL 不能为空") | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 4 | 6 | import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; |
| 5 | 7 | import cn.iocoder.yudao.module.iot.core.enums.IotProtocolTypeEnum; |
| 6 | 8 | import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; |
| 9 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 7 | 10 | import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; |
| 8 | 11 | import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties.ProtocolInstanceProperties; |
| 9 | 12 | import cn.iocoder.yudao.module.iot.gateway.protocol.IotProtocol; |
| 10 | -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 11 | 13 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamHandler; |
| 12 | 14 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.downstream.IotMqttDownstreamSubscriber; |
| 13 | 15 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttAuthHandler; |
| ... | ... | @@ -15,6 +17,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqt |
| 15 | 17 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; |
| 16 | 18 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 17 | 19 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 20 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 18 | 21 | import io.netty.handler.codec.mqtt.MqttConnectReturnCode; |
| 19 | 22 | import io.netty.handler.codec.mqtt.MqttQoS; |
| 20 | 23 | import io.vertx.core.Vertx; |
| ... | ... | @@ -26,13 +29,10 @@ import io.vertx.mqtt.MqttTopicSubscription; |
| 26 | 29 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| 27 | 30 | import lombok.Getter; |
| 28 | 31 | import lombok.extern.slf4j.Slf4j; |
| 29 | -import cn.hutool.core.lang.Assert; | |
| 30 | -import cn.hutool.core.util.StrUtil; | |
| 31 | 32 | |
| 33 | +import java.util.ArrayList; | |
| 32 | 34 | import java.util.List; |
| 33 | 35 | |
| 34 | -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | |
| 35 | - | |
| 36 | 36 | /** |
| 37 | 37 | * IoT 网关 MQTT 协议:接收设备上行消息 |
| 38 | 38 | * |
| ... | ... | @@ -249,11 +249,22 @@ public class IotMqttProtocol implements IotProtocol { |
| 249 | 249 | // 3.2 设置 QoS 2 消息的 PUBREL 处理器 |
| 250 | 250 | endpoint.publishReleaseHandler(endpoint::publishComplete); |
| 251 | 251 | |
| 252 | - // 4.1 设置订阅处理器 | |
| 252 | + // 4.1 设置订阅处理器(带 ACL 校验) | |
| 253 | 253 | endpoint.subscribeHandler(subscribe -> { |
| 254 | - List<String> topicNames = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::topicName); | |
| 255 | - log.debug("[handleEndpoint][设备订阅,客户端 ID: {},主题: {}]", clientId, topicNames); | |
| 256 | - List<MqttQoS> grantedQoSLevels = convertList(subscribe.topicSubscriptions(), MqttTopicSubscription::qualityOfService); | |
| 254 | + IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); | |
| 255 | + List<MqttQoS> grantedQoSLevels = new ArrayList<>(); | |
| 256 | + for (MqttTopicSubscription sub : subscribe.topicSubscriptions()) { | |
| 257 | + String topicName = sub.topicName(); | |
| 258 | + // 校验主题是否属于当前设备 | |
| 259 | + if (connectionInfo != null && IotMqttTopicUtils.isTopicSubscribeAllowed( | |
| 260 | + topicName, connectionInfo.getProductKey(), connectionInfo.getDeviceName())) { | |
| 261 | + grantedQoSLevels.add(sub.qualityOfService()); | |
| 262 | + log.debug("[handleEndpoint][订阅成功,客户端 ID: {},主题: {}]", clientId, topicName); | |
| 263 | + } else { | |
| 264 | + log.warn("[handleEndpoint][订阅被拒绝,客户端 ID: {},主题: {}]", clientId, topicName); | |
| 265 | + grantedQoSLevels.add(MqttQoS.FAILURE); | |
| 266 | + } | |
| 267 | + } | |
| 257 | 268 | endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); |
| 258 | 269 | }); |
| 259 | 270 | // 4.2 设置取消订阅处理器 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java
| ... | ... | @@ -30,7 +30,6 @@ public abstract class IotMqttAbstractHandler { |
| 30 | 30 | protected final IotMqttConnectionManager connectionManager; |
| 31 | 31 | protected final IotDeviceMessageService deviceMessageService; |
| 32 | 32 | |
| 33 | - // done @AI:基于 method 通过 IotMqttTopicUtils.buildTopicByMethod 计算 reply topic | |
| 34 | 33 | /** |
| 35 | 34 | * 发送成功响应到设备 |
| 36 | 35 | * |
| ... | ... | @@ -43,20 +42,8 @@ public abstract class IotMqttAbstractHandler { |
| 43 | 42 | */ |
| 44 | 43 | protected void sendSuccessResponse(MqttEndpoint endpoint, String productKey, String deviceName, |
| 45 | 44 | String requestId, String method, Object data) { |
| 46 | - try { | |
| 47 | - // 1. 构建响应消息 | |
| 48 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); | |
| 49 | - | |
| 50 | - // 2. 编码消息(使用默认编解码器) | |
| 51 | - byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); | |
| 52 | - | |
| 53 | - // 3. 构建响应主题并发送 | |
| 54 | - String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); | |
| 55 | - endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); | |
| 56 | - log.debug("[sendSuccessResponse][发送成功响应,主题: {}]", replyTopic); | |
| 57 | - } catch (Exception e) { | |
| 58 | - log.error("[sendSuccessResponse][发送成功响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); | |
| 59 | - } | |
| 45 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, data, 0, null); | |
| 46 | + writeResponse(endpoint, productKey, deviceName, method, responseMessage); | |
| 60 | 47 | } |
| 61 | 48 | |
| 62 | 49 | /** |
| ... | ... | @@ -72,22 +59,32 @@ public abstract class IotMqttAbstractHandler { |
| 72 | 59 | */ |
| 73 | 60 | protected void sendErrorResponse(MqttEndpoint endpoint, String productKey, String deviceName, |
| 74 | 61 | String requestId, String method, Integer errorCode, String errorMessage) { |
| 75 | - try { | |
| 76 | - // 1. 构建响应消息 | |
| 77 | - IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); | |
| 62 | + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, method, null, errorCode, errorMessage); | |
| 63 | + writeResponse(endpoint, productKey, deviceName, method, responseMessage); | |
| 64 | + } | |
| 78 | 65 | |
| 79 | - // 2. 编码消息(使用默认编解码器) | |
| 66 | + /** | |
| 67 | + * 写入响应消息到设备 | |
| 68 | + * | |
| 69 | + * @param endpoint MQTT 连接端点 | |
| 70 | + * @param productKey 产品 Key | |
| 71 | + * @param deviceName 设备名称 | |
| 72 | + * @param method 方法名 | |
| 73 | + * @param responseMessage 响应消息 | |
| 74 | + */ | |
| 75 | + private void writeResponse(MqttEndpoint endpoint, String productKey, String deviceName, | |
| 76 | + String method, IotDeviceMessage responseMessage) { | |
| 77 | + try { | |
| 78 | + // 1. 编码消息(使用默认编解码器) | |
| 80 | 79 | byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, DEFAULT_CODEC_TYPE); |
| 81 | 80 | |
| 82 | - // 3. 构建响应主题并发送 | |
| 81 | + // 2. 构建响应主题,并发送 | |
| 83 | 82 | String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); |
| 84 | 83 | endpoint.publish(replyTopic, Buffer.buffer(encodedData), MqttQoS.AT_LEAST_ONCE, false, false); |
| 85 | - log.debug("[sendErrorResponse][发送错误响应,主题: {}]", replyTopic); | |
| 84 | + log.debug("[writeResponse][发送响应,主题: {},code: {}]", replyTopic, responseMessage.getCode()); | |
| 86 | 85 | } catch (Exception e) { |
| 87 | - log.error("[sendErrorResponse][发送错误响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); | |
| 86 | + log.error("[writeResponse][发送响应异常,客户端 ID: {}]", endpoint.clientIdentifier(), e); | |
| 88 | 87 | } |
| 89 | 88 | } |
| 90 | 89 | |
| 91 | - // TODO @AI:当前 sendSuccessResponse/sendErrorResponse 已足够清晰,暂不抽取 writeResponse(必须抽取!!!) | |
| 92 | - | |
| 93 | 90 | } | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAuthHandler.java
| ... | ... | @@ -103,7 +103,7 @@ public class IotMqttAuthHandler extends IotMqttAbstractHandler { |
| 103 | 103 | .setProductKey(device.getProductKey()) |
| 104 | 104 | .setDeviceName(device.getDeviceName()) |
| 105 | 105 | .setRemoteAddress(connectionManager.getEndpointAddress(endpoint)); |
| 106 | - connectionManager.registerConnection(endpoint, device.getId(), connectionInfo); | |
| 106 | + connectionManager.registerConnection(endpoint, connectionInfo); | |
| 107 | 107 | } |
| 108 | 108 | |
| 109 | 109 | /** | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttRegisterHandler.java
| ... | ... | @@ -77,6 +77,9 @@ public class IotMqttRegisterHandler extends IotMqttAbstractHandler { |
| 77 | 77 | // 接受连接,并发送错误响应 |
| 78 | 78 | endpoint.accept(false); |
| 79 | 79 | sendErrorResponse(endpoint, productKey, deviceName, null, method, 500, e.getMessage()); |
| 80 | + } finally { | |
| 81 | + // 注册完成后关闭连接(一型一密只用于获取 deviceSecret,不保持连接) | |
| 82 | + endpoint.close(); | |
| 80 | 83 | } |
| 81 | 84 | } |
| 82 | 85 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
| ... | ... | @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.lang.Assert; |
| 4 | 4 | import cn.hutool.core.util.ArrayUtil; |
| 5 | -import cn.hutool.core.util.StrUtil; | |
| 6 | 5 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 8 | 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| ... | ... | @@ -50,12 +49,10 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { |
| 50 | 49 | } |
| 51 | 50 | // 1.2 解析主题,获取 productKey 和 deviceName |
| 52 | 51 | String[] topicParts = topic.split("/"); |
| 53 | - if (topicParts.length < 4 || StrUtil.hasBlank(topicParts[2], topicParts[3])) { | |
| 54 | - log.warn("[handleBusinessRequest][topic({}) 格式不正确,无法解析有效的 productKey 和 deviceName]", topic); | |
| 55 | - return; | |
| 56 | - } | |
| 57 | - productKey = topicParts[2]; | |
| 58 | - deviceName = topicParts[3]; | |
| 52 | + productKey = ArrayUtil.get(topicParts, 2); | |
| 53 | + deviceName = ArrayUtil.get(topicParts, 3); | |
| 54 | + Assert.notBlank(productKey, "产品 Key 不能为空"); | |
| 55 | + Assert.notBlank(deviceName, "设备名称不能为空"); | |
| 59 | 56 | // 1.3 校验设备信息,防止伪造设备消息 |
| 60 | 57 | IotMqttConnectionManager.ConnectionInfo connectionInfo = connectionManager.getConnectionInfo(endpoint); |
| 61 | 58 | Assert.notNull(connectionInfo, "无法获取连接信息"); | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/manager/IotMqttConnectionManager.java
| ... | ... | @@ -73,25 +73,24 @@ public class IotMqttConnectionManager { |
| 73 | 73 | * 注册设备连接(包含认证信息) |
| 74 | 74 | * |
| 75 | 75 | * @param endpoint MQTT 连接端点 |
| 76 | - * @param deviceId 设备 ID | |
| 77 | 76 | * @param connectionInfo 连接信息 |
| 78 | 77 | */ |
| 79 | - // TODO @AI:移除掉 deviceId ???参考别的 tcp 等模块协议 | |
| 80 | - public void registerConnection(MqttEndpoint endpoint, Long deviceId, ConnectionInfo connectionInfo) { | |
| 78 | + public void registerConnection(MqttEndpoint endpoint, ConnectionInfo connectionInfo) { | |
| 79 | + Long deviceId = connectionInfo.getDeviceId(); | |
| 81 | 80 | // 如果设备已有其他连接,先清理旧连接 |
| 82 | 81 | MqttEndpoint oldEndpoint = deviceEndpointMap.get(deviceId); |
| 83 | 82 | if (oldEndpoint != null && oldEndpoint != endpoint) { |
| 84 | 83 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", |
| 85 | 84 | deviceId, getEndpointAddress(oldEndpoint)); |
| 86 | - oldEndpoint.close(); | |
| 87 | - // 清理旧连接的映射 | |
| 85 | + // 先清理映射,再关闭连接(避免旧连接处理器干扰) | |
| 88 | 86 | connectionMap.remove(oldEndpoint); |
| 87 | + oldEndpoint.close(); | |
| 89 | 88 | } |
| 90 | 89 | |
| 91 | 90 | // 注册新连接 |
| 92 | 91 | connectionMap.put(endpoint, connectionInfo); |
| 93 | 92 | deviceEndpointMap.put(deviceId, endpoint); |
| 94 | - log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", | |
| 93 | + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},productKey: {},deviceName: {}]", | |
| 95 | 94 | deviceId, getEndpointAddress(endpoint), connectionInfo.getProductKey(), connectionInfo.getDeviceName()); |
| 96 | 95 | } |
| 97 | 96 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java
| ... | ... | @@ -47,8 +47,8 @@ public class IotTcpConnectionManager { |
| 47 | 47 | * @param deviceId 设备 ID |
| 48 | 48 | * @param connectionInfo 连接信息 |
| 49 | 49 | */ |
| 50 | - public void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { | |
| 51 | - // 检查连接数是否已达上限 | |
| 50 | + public synchronized void registerConnection(NetSocket socket, Long deviceId, ConnectionInfo connectionInfo) { | |
| 51 | + // 检查连接数是否已达上限(同步方法确保检查和注册的原子性) | |
| 52 | 52 | if (connectionMap.size() >= maxConnections) { |
| 53 | 53 | throw new IllegalStateException("连接数已达上限: " + maxConnections); |
| 54 | 54 | } |
| ... | ... | @@ -57,9 +57,9 @@ public class IotTcpConnectionManager { |
| 57 | 57 | if (oldSocket != null && oldSocket != socket) { |
| 58 | 58 | log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", |
| 59 | 59 | deviceId, oldSocket.remoteAddress()); |
| 60 | - oldSocket.close(); | |
| 61 | - // 清理旧连接的映射 | |
| 60 | + // 先清理映射,再关闭连接 | |
| 62 | 61 | connectionMap.remove(oldSocket); |
| 62 | + oldSocket.close(); | |
| 63 | 63 | } |
| 64 | 64 | |
| 65 | 65 | // 注册新连接 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/udp/manager/IotUdpSessionManager.java
| ... | ... | @@ -9,6 +9,7 @@ import lombok.Data; |
| 9 | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | 10 | |
| 11 | 11 | import java.net.InetSocketAddress; |
| 12 | +import java.util.concurrent.CompletableFuture; | |
| 12 | 13 | import java.util.concurrent.TimeUnit; |
| 13 | 14 | |
| 14 | 15 | /** |
| ... | ... | @@ -47,8 +48,8 @@ public class IotUdpSessionManager { |
| 47 | 48 | * @param deviceId 设备 ID |
| 48 | 49 | * @param sessionInfo 会话信息 |
| 49 | 50 | */ |
| 50 | - public void registerSession(Long deviceId, SessionInfo sessionInfo) { | |
| 51 | - // 检查是否为新设备,且会话数已达上限 | |
| 51 | + public synchronized void registerSession(Long deviceId, SessionInfo sessionInfo) { | |
| 52 | + // 检查是否为新设备,且会话数已达上限(同步方法确保检查和注册的原子性) | |
| 52 | 53 | if (deviceSessionCache.getIfPresent(deviceId) == null |
| 53 | 54 | && deviceSessionCache.size() >= maxSessions) { |
| 54 | 55 | throw new IllegalStateException("会话数已达上限: " + maxSessions); |
| ... | ... | @@ -113,16 +114,21 @@ public class IotUdpSessionManager { |
| 113 | 114 | } |
| 114 | 115 | InetSocketAddress address = sessionInfo.getAddress(); |
| 115 | 116 | try { |
| 117 | + // 使用 CompletableFuture 同步等待发送结果 | |
| 118 | + CompletableFuture<Boolean> future = new CompletableFuture<>(); | |
| 116 | 119 | socket.send(Buffer.buffer(data), address.getPort(), address.getHostString(), result -> { |
| 117 | 120 | if (result.succeeded()) { |
| 118 | 121 | log.debug("[sendToDevice][发送消息成功,设备 ID: {},地址: {},数据长度: {} 字节]", |
| 119 | 122 | deviceId, buildAddressKey(address), data.length); |
| 120 | - return; | |
| 123 | + future.complete(true); | |
| 124 | + } else { | |
| 125 | + log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", | |
| 126 | + deviceId, buildAddressKey(address), result.cause()); | |
| 127 | + future.complete(false); | |
| 121 | 128 | } |
| 122 | - log.error("[sendToDevice][发送消息失败,设备 ID: {},地址: {}]", | |
| 123 | - deviceId, buildAddressKey(address), result.cause()); | |
| 124 | 129 | }); |
| 125 | - return true; | |
| 130 | + // 同步等待结果,超时 5 秒 | |
| 131 | + return future.get(5, TimeUnit.SECONDS); | |
| 126 | 132 | } catch (Exception e) { |
| 127 | 133 | log.error("[sendToDevice][发送消息异常,设备 ID: {}]", deviceId, e); |
| 128 | 134 | return false; | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
| ... | ... | @@ -63,4 +63,26 @@ public final class IotMqttTopicUtils { |
| 63 | 63 | return SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/" + topicSuffix; |
| 64 | 64 | } |
| 65 | 65 | |
| 66 | + /** | |
| 67 | + * 校验主题是否允许订阅 | |
| 68 | + * <p> | |
| 69 | + * 规则:主题必须以 /sys/{productKey}/{deviceName}/ 开头, | |
| 70 | + * 或者是通配符形式 /sys/{productKey}/{deviceName}/# | |
| 71 | + * | |
| 72 | + * @param topic 订阅的主题 | |
| 73 | + * @param productKey 产品 Key | |
| 74 | + * @param deviceName 设备名称 | |
| 75 | + * @return 是否允许订阅 | |
| 76 | + */ | |
| 77 | + public static boolean isTopicSubscribeAllowed(String topic, String productKey, String deviceName) { | |
| 78 | + if (!StrUtil.isAllNotBlank(topic, productKey, deviceName)) { | |
| 79 | + return false; | |
| 80 | + } | |
| 81 | + // 构建设备主题前缀 | |
| 82 | + String deviceTopicPrefix = SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/"; | |
| 83 | + // 主题必须以设备前缀开头,或者是设备前缀的通配符形式 | |
| 84 | + return topic.startsWith(deviceTopicPrefix) | |
| 85 | + || topic.equals(SYS_TOPIC_PREFIX + productKey + "/" + deviceName + "/#"); | |
| 86 | + } | |
| 87 | + | |
| 66 | 88 | } |
| 67 | 89 | \ No newline at end of file | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml