Commit b800d274a4ce2dfb0a5a401545c0d50005cdafe7
1 parent
fcca74ac
feat(iot):支持 MQTT 设备回复消息的 _reply 方法标准化,并精简上行处理器异常处理
Showing
8 changed files
with
68 additions
and
40 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/api/device/IoTDeviceApiImpl.java
| ... | ... | @@ -22,6 +22,7 @@ import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; |
| 22 | 22 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 23 | 23 | import jakarta.annotation.Resource; |
| 24 | 24 | import jakarta.annotation.security.PermitAll; |
| 25 | +import org.springframework.context.annotation.Lazy; | |
| 25 | 26 | import org.springframework.context.annotation.Primary; |
| 26 | 27 | import org.springframework.validation.annotation.Validated; |
| 27 | 28 | import org.springframework.web.bind.annotation.PostMapping; |
| ... | ... | @@ -51,8 +52,10 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { |
| 51 | 52 | @Resource |
| 52 | 53 | private IotProductService productService; |
| 53 | 54 | @Resource |
| 55 | + @Lazy // 延迟加载,解决循环依赖 | |
| 54 | 56 | private IotDeviceModbusConfigService modbusConfigService; |
| 55 | 57 | @Resource |
| 58 | + @Lazy // 延迟加载,解决循环依赖 | |
| 56 | 59 | private IotDeviceModbusPointService modbusPointService; |
| 57 | 60 | |
| 58 | 61 | @Override | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/product/IotProductServiceImpl.java
| ... | ... | @@ -17,6 +17,7 @@ import com.baomidou.dynamic.datasource.annotation.DSTransactional; |
| 17 | 17 | import jakarta.annotation.Resource; |
| 18 | 18 | import org.springframework.cache.annotation.CacheEvict; |
| 19 | 19 | import org.springframework.cache.annotation.Cacheable; |
| 20 | +import org.springframework.context.annotation.Lazy; | |
| 20 | 21 | import org.springframework.stereotype.Service; |
| 21 | 22 | import org.springframework.validation.annotation.Validated; |
| 22 | 23 | |
| ... | ... | @@ -41,9 +42,10 @@ public class IotProductServiceImpl implements IotProductService { |
| 41 | 42 | private IotProductMapper productMapper; |
| 42 | 43 | |
| 43 | 44 | @Resource |
| 44 | - private IotDevicePropertyService devicePropertyDataService; | |
| 45 | - @Resource | |
| 46 | 45 | private IotDeviceService deviceService; |
| 46 | + @Resource | |
| 47 | + @Lazy // 延迟加载,避免循环依赖 | |
| 48 | + private IotDevicePropertyService devicePropertyDataService; | |
| 47 | 49 | |
| 48 | 50 | @Override |
| 49 | 51 | public Long createProduct(IotProductSaveReqVO createReqVO) { | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java
| ... | ... | @@ -51,8 +51,8 @@ public class IotThingModelServiceImpl implements IotThingModelService { |
| 51 | 51 | @Resource |
| 52 | 52 | @Lazy // 延迟加载,解决循环依赖 |
| 53 | 53 | private IotProductService productService; |
| 54 | - | |
| 55 | 54 | @Resource |
| 55 | + @Lazy // 延迟加载,解决循环依赖 | |
| 56 | 56 | private IotDeviceModbusPointService deviceModbusPointService; |
| 57 | 57 | |
| 58 | 58 | @Override | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java
| ... | ... | @@ -5,6 +5,7 @@ import cn.hutool.core.util.StrUtil; |
| 5 | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 6 | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | |
| 8 | 9 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| 9 | 10 | import lombok.extern.slf4j.Slf4j; |
| 10 | 11 | |
| ... | ... | @@ -42,12 +43,14 @@ public class IotEmqxUpstreamHandler { |
| 42 | 43 | return; |
| 43 | 44 | } |
| 44 | 45 | |
| 45 | - // 2. 反序列化消息 | |
| 46 | + // 2.1 反序列化消息 | |
| 46 | 47 | IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); |
| 47 | 48 | if (message == null) { |
| 48 | 49 | log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); |
| 49 | 50 | return; |
| 50 | 51 | } |
| 52 | + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) | |
| 53 | + IotMqttTopicUtils.normalizeReplyMethod(message); | |
| 51 | 54 | |
| 52 | 55 | // 3. 发送消息到队列 |
| 53 | 56 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | ... | ... |
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.iocoder.yudao.framework.common.exception.ServiceException; | |
| 6 | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 8 | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| ... | ... | @@ -10,8 +9,6 @@ import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; |
| 10 | 9 | import io.vertx.mqtt.MqttEndpoint; |
| 11 | 10 | import lombok.extern.slf4j.Slf4j; |
| 12 | 11 | |
| 13 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; | |
| 14 | -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; | |
| 15 | 12 | |
| 16 | 13 | /** |
| 17 | 14 | * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等) |
| ... | ... | @@ -39,10 +36,6 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { |
| 39 | 36 | */ |
| 40 | 37 | public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) { |
| 41 | 38 | String clientId = endpoint.clientIdentifier(); |
| 42 | - IotDeviceMessage message = null; | |
| 43 | - String productKey = null; | |
| 44 | - String deviceName = null; | |
| 45 | - | |
| 46 | 39 | try { |
| 47 | 40 | // 1.1 基础检查 |
| 48 | 41 | if (ArrayUtil.isEmpty(payload)) { |
| ... | ... | @@ -50,8 +43,8 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { |
| 50 | 43 | } |
| 51 | 44 | // 1.2 解析主题,获取 productKey 和 deviceName |
| 52 | 45 | String[] topicParts = topic.split("/"); |
| 53 | - productKey = ArrayUtil.get(topicParts, 2); | |
| 54 | - deviceName = ArrayUtil.get(topicParts, 3); | |
| 46 | + String productKey = ArrayUtil.get(topicParts, 2); | |
| 47 | + String deviceName = ArrayUtil.get(topicParts, 3); | |
| 55 | 48 | Assert.notBlank(productKey, "产品 Key 不能为空"); |
| 56 | 49 | Assert.notBlank(deviceName, "设备名称不能为空"); |
| 57 | 50 | // 1.3 校验设备信息,防止伪造设备消息 |
| ... | ... | @@ -65,38 +58,21 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { |
| 65 | 58 | return; |
| 66 | 59 | } |
| 67 | 60 | |
| 68 | - // 2. 反序列化消息 | |
| 69 | - message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); | |
| 61 | + // 2.1 反序列化消息 | |
| 62 | + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); | |
| 70 | 63 | if (message == null) { |
| 71 | 64 | log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); |
| 72 | - sendErrorResponse(endpoint, productKey, deviceName, null, null, | |
| 73 | - BAD_REQUEST.getCode(), "消息解码失败"); | |
| 74 | 65 | return; |
| 75 | 66 | } |
| 67 | + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) | |
| 68 | + IotMqttTopicUtils.normalizeReplyMethod(message); | |
| 76 | 69 | |
| 77 | 70 | // 3. 处理业务消息 |
| 78 | 71 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); |
| 79 | 72 | log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic); |
| 80 | - } catch (ServiceException e) { | |
| 81 | - log.warn("[handleBusinessRequest][业务异常,客户端 ID: {},主题: {},错误: {}]", | |
| 82 | - clientId, topic, e.getMessage()); | |
| 83 | - String requestId = message != null ? message.getRequestId() : null; | |
| 84 | - String method = message != null ? message.getMethod() : null; | |
| 85 | - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, e.getCode(), e.getMessage()); | |
| 86 | - } catch (IllegalArgumentException e) { | |
| 87 | - log.warn("[handleBusinessRequest][参数校验失败,客户端 ID: {},主题: {},错误: {}]", | |
| 88 | - clientId, topic, e.getMessage()); | |
| 89 | - String requestId = message != null ? message.getRequestId() : null; | |
| 90 | - String method = message != null ? message.getMethod() : null; | |
| 91 | - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, | |
| 92 | - BAD_REQUEST.getCode(), e.getMessage()); | |
| 93 | 73 | } catch (Exception e) { |
| 94 | 74 | log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]", |
| 95 | 75 | clientId, topic, e.getMessage(), e); |
| 96 | - String requestId = message != null ? message.getRequestId() : null; | |
| 97 | - String method = message != null ? message.getMethod() : null; | |
| 98 | - sendErrorResponse(endpoint, productKey, deviceName, requestId, method, | |
| 99 | - INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | |
| 100 | 76 | } |
| 101 | 77 | } |
| 102 | 78 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtils.java
| 1 | 1 | package cn.iocoder.yudao.module.iot.gateway.util; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | +import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | |
| 5 | +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | |
| 4 | 6 | |
| 5 | 7 | /** |
| 6 | 8 | * IoT 网关 MQTT 主题工具类 |
| ... | ... | @@ -44,6 +46,32 @@ public final class IotMqttTopicUtils { |
| 44 | 46 | */ |
| 45 | 47 | public static final String MQTT_ACL_PATH = "/mqtt/acl"; |
| 46 | 48 | |
| 49 | + // ========== 消息方法标准化 ========== | |
| 50 | + | |
| 51 | + /** | |
| 52 | + * 标准化设备回复消息的 method | |
| 53 | + * <p> | |
| 54 | + * MQTT 协议中,设备回复下行指令时,topic 和 method 会携带 _reply 后缀 | |
| 55 | + * (如 thing.service.invoke_reply)。平台内部统一使用基础 method(如 thing.service.invoke), | |
| 56 | + * 通过 {@link IotDeviceMessage#getCode()} 非空来识别回复消息。 | |
| 57 | + * <p> | |
| 58 | + * 此方法剥离 _reply 后缀,并确保 code 字段被设置。 | |
| 59 | + * | |
| 60 | + * @param message 设备消息 | |
| 61 | + */ | |
| 62 | + public static void normalizeReplyMethod(IotDeviceMessage message) { | |
| 63 | + String method = message.getMethod(); | |
| 64 | + if (!StrUtil.endWith(method, REPLY_TOPIC_SUFFIX)) { | |
| 65 | + return; | |
| 66 | + } | |
| 67 | + // 1. 剥离 _reply 后缀 | |
| 68 | + message.setMethod(method.substring(0, method.length() - REPLY_TOPIC_SUFFIX.length())); | |
| 69 | + // 2. 确保 code 被设置,使 isReplyMessage() 能正确识别 | |
| 70 | + if (message.getCode() == null) { | |
| 71 | + message.setCode(GlobalErrorCodeConstants.SUCCESS.getCode()); | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 47 | 75 | // ========== 工具方法 ========== |
| 48 | 76 | |
| 49 | 77 | /** | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotDirectDeviceMqttProtocolIntegrationTest.java
| ... | ... | @@ -239,12 +239,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { |
| 239 | 239 | log.info("[testSubscribe][连接认证成功]"); |
| 240 | 240 | |
| 241 | 241 | try { |
| 242 | - // 2. 设置消息处理器 | |
| 243 | - client.publishHandler(message -> log.info("[testSubscribe][收到消息: topic={}, payload={}]", | |
| 244 | - message.topicName(), message.payload().toString())); | |
| 242 | + // 2. 设置消息处理器:收到属性设置时,回复 _reply 消息 | |
| 243 | + client.publishHandler(message -> { | |
| 244 | + log.info("[testSubscribe][收到消息: topic={}, payload={}]", | |
| 245 | + message.topicName(), message.payload().toString()); | |
| 246 | + // 收到属性设置消息时,回复 _reply | |
| 247 | + if (message.topicName().endsWith("/thing/property/set")) { | |
| 248 | + try { | |
| 249 | + IotDeviceMessage received = SERIALIZER.deserialize(message.payload().getBytes()); | |
| 250 | + IotDeviceMessage reply = IotDeviceMessage.replyOf( | |
| 251 | + received.getRequestId(), "thing.property.set_reply", null, 0, null); | |
| 252 | + String replyTopic = String.format("/sys/%s/%s/thing/property/set_reply", PRODUCT_KEY, DEVICE_NAME); | |
| 253 | + byte[] replyPayload = SERIALIZER.serialize(reply); | |
| 254 | + client.publish(replyTopic, Buffer.buffer(replyPayload), MqttQoS.AT_LEAST_ONCE, false, false); | |
| 255 | + log.info("[testSubscribe][已回复属性设置: topic={}]", replyTopic); | |
| 256 | + } catch (Exception e) { | |
| 257 | + log.error("[testSubscribe][回复属性设置异常]", e); | |
| 258 | + } | |
| 259 | + } | |
| 260 | + }); | |
| 245 | 261 | |
| 246 | - // 3. 订阅下行主题 | |
| 247 | - String topic = String.format("/sys/%s/%s/thing/service/#", PRODUCT_KEY, DEVICE_NAME); | |
| 262 | + // 3. 订阅下行主题(属性设置 + 服务调用) | |
| 263 | + String topic = String.format("/sys/%s/%s/#", PRODUCT_KEY, DEVICE_NAME); | |
| 248 | 264 | log.info("[testSubscribe][订阅主题: {}]", topic); |
| 249 | 265 | subscribe(client, topic); |
| 250 | 266 | log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); | ... | ... |
yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java