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,6 +22,7 @@ import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; | ||
| 22 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; | 22 | import cn.iocoder.yudao.module.iot.service.product.IotProductService; |
| 23 | import jakarta.annotation.Resource; | 23 | import jakarta.annotation.Resource; |
| 24 | import jakarta.annotation.security.PermitAll; | 24 | import jakarta.annotation.security.PermitAll; |
| 25 | +import org.springframework.context.annotation.Lazy; | ||
| 25 | import org.springframework.context.annotation.Primary; | 26 | import org.springframework.context.annotation.Primary; |
| 26 | import org.springframework.validation.annotation.Validated; | 27 | import org.springframework.validation.annotation.Validated; |
| 27 | import org.springframework.web.bind.annotation.PostMapping; | 28 | import org.springframework.web.bind.annotation.PostMapping; |
| @@ -51,8 +52,10 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | @@ -51,8 +52,10 @@ public class IoTDeviceApiImpl implements IotDeviceCommonApi { | ||
| 51 | @Resource | 52 | @Resource |
| 52 | private IotProductService productService; | 53 | private IotProductService productService; |
| 53 | @Resource | 54 | @Resource |
| 55 | + @Lazy // 延迟加载,解决循环依赖 | ||
| 54 | private IotDeviceModbusConfigService modbusConfigService; | 56 | private IotDeviceModbusConfigService modbusConfigService; |
| 55 | @Resource | 57 | @Resource |
| 58 | + @Lazy // 延迟加载,解决循环依赖 | ||
| 56 | private IotDeviceModbusPointService modbusPointService; | 59 | private IotDeviceModbusPointService modbusPointService; |
| 57 | 60 | ||
| 58 | @Override | 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,6 +17,7 @@ import com.baomidou.dynamic.datasource.annotation.DSTransactional; | ||
| 17 | import jakarta.annotation.Resource; | 17 | import jakarta.annotation.Resource; |
| 18 | import org.springframework.cache.annotation.CacheEvict; | 18 | import org.springframework.cache.annotation.CacheEvict; |
| 19 | import org.springframework.cache.annotation.Cacheable; | 19 | import org.springframework.cache.annotation.Cacheable; |
| 20 | +import org.springframework.context.annotation.Lazy; | ||
| 20 | import org.springframework.stereotype.Service; | 21 | import org.springframework.stereotype.Service; |
| 21 | import org.springframework.validation.annotation.Validated; | 22 | import org.springframework.validation.annotation.Validated; |
| 22 | 23 | ||
| @@ -41,9 +42,10 @@ public class IotProductServiceImpl implements IotProductService { | @@ -41,9 +42,10 @@ public class IotProductServiceImpl implements IotProductService { | ||
| 41 | private IotProductMapper productMapper; | 42 | private IotProductMapper productMapper; |
| 42 | 43 | ||
| 43 | @Resource | 44 | @Resource |
| 44 | - private IotDevicePropertyService devicePropertyDataService; | ||
| 45 | - @Resource | ||
| 46 | private IotDeviceService deviceService; | 45 | private IotDeviceService deviceService; |
| 46 | + @Resource | ||
| 47 | + @Lazy // 延迟加载,避免循环依赖 | ||
| 48 | + private IotDevicePropertyService devicePropertyDataService; | ||
| 47 | 49 | ||
| 48 | @Override | 50 | @Override |
| 49 | public Long createProduct(IotProductSaveReqVO createReqVO) { | 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,8 +51,8 @@ public class IotThingModelServiceImpl implements IotThingModelService { | ||
| 51 | @Resource | 51 | @Resource |
| 52 | @Lazy // 延迟加载,解决循环依赖 | 52 | @Lazy // 延迟加载,解决循环依赖 |
| 53 | private IotProductService productService; | 53 | private IotProductService productService; |
| 54 | - | ||
| 55 | @Resource | 54 | @Resource |
| 55 | + @Lazy // 延迟加载,解决循环依赖 | ||
| 56 | private IotDeviceModbusPointService deviceModbusPointService; | 56 | private IotDeviceModbusPointService deviceModbusPointService; |
| 57 | 57 | ||
| 58 | @Override | 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,6 +5,7 @@ import cn.hutool.core.util.StrUtil; | ||
| 5 | import cn.hutool.extra.spring.SpringUtil; | 5 | import cn.hutool.extra.spring.SpringUtil; |
| 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 7 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; |
| 8 | +import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | ||
| 8 | import io.vertx.mqtt.messages.MqttPublishMessage; | 9 | import io.vertx.mqtt.messages.MqttPublishMessage; |
| 9 | import lombok.extern.slf4j.Slf4j; | 10 | import lombok.extern.slf4j.Slf4j; |
| 10 | 11 | ||
| @@ -42,12 +43,14 @@ public class IotEmqxUpstreamHandler { | @@ -42,12 +43,14 @@ public class IotEmqxUpstreamHandler { | ||
| 42 | return; | 43 | return; |
| 43 | } | 44 | } |
| 44 | 45 | ||
| 45 | - // 2. 反序列化消息 | 46 | + // 2.1 反序列化消息 |
| 46 | IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); | 47 | IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); |
| 47 | if (message == null) { | 48 | if (message == null) { |
| 48 | log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); | 49 | log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload)); |
| 49 | return; | 50 | return; |
| 50 | } | 51 | } |
| 52 | + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) | ||
| 53 | + IotMqttTopicUtils.normalizeReplyMethod(message); | ||
| 51 | 54 | ||
| 52 | // 3. 发送消息到队列 | 55 | // 3. 发送消息到队列 |
| 53 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | 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,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.lang.Assert; | 3 | import cn.hutool.core.lang.Assert; |
| 4 | import cn.hutool.core.util.ArrayUtil; | 4 | import cn.hutool.core.util.ArrayUtil; |
| 5 | -import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 6 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; | 5 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; | 6 | import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; |
| 8 | import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; | 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,8 +9,6 @@ import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; | ||
| 10 | import io.vertx.mqtt.MqttEndpoint; | 9 | import io.vertx.mqtt.MqttEndpoint; |
| 11 | import lombok.extern.slf4j.Slf4j; | 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 | * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等) | 14 | * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等) |
| @@ -39,10 +36,6 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | @@ -39,10 +36,6 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | ||
| 39 | */ | 36 | */ |
| 40 | public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) { | 37 | public void handleBusinessRequest(MqttEndpoint endpoint, String topic, byte[] payload) { |
| 41 | String clientId = endpoint.clientIdentifier(); | 38 | String clientId = endpoint.clientIdentifier(); |
| 42 | - IotDeviceMessage message = null; | ||
| 43 | - String productKey = null; | ||
| 44 | - String deviceName = null; | ||
| 45 | - | ||
| 46 | try { | 39 | try { |
| 47 | // 1.1 基础检查 | 40 | // 1.1 基础检查 |
| 48 | if (ArrayUtil.isEmpty(payload)) { | 41 | if (ArrayUtil.isEmpty(payload)) { |
| @@ -50,8 +43,8 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | @@ -50,8 +43,8 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | ||
| 50 | } | 43 | } |
| 51 | // 1.2 解析主题,获取 productKey 和 deviceName | 44 | // 1.2 解析主题,获取 productKey 和 deviceName |
| 52 | String[] topicParts = topic.split("/"); | 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 | Assert.notBlank(productKey, "产品 Key 不能为空"); | 48 | Assert.notBlank(productKey, "产品 Key 不能为空"); |
| 56 | Assert.notBlank(deviceName, "设备名称不能为空"); | 49 | Assert.notBlank(deviceName, "设备名称不能为空"); |
| 57 | // 1.3 校验设备信息,防止伪造设备消息 | 50 | // 1.3 校验设备信息,防止伪造设备消息 |
| @@ -65,38 +58,21 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | @@ -65,38 +58,21 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { | ||
| 65 | return; | 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 | if (message == null) { | 63 | if (message == null) { |
| 71 | log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); | 64 | log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic); |
| 72 | - sendErrorResponse(endpoint, productKey, deviceName, null, null, | ||
| 73 | - BAD_REQUEST.getCode(), "消息解码失败"); | ||
| 74 | return; | 65 | return; |
| 75 | } | 66 | } |
| 67 | + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀) | ||
| 68 | + IotMqttTopicUtils.normalizeReplyMethod(message); | ||
| 76 | 69 | ||
| 77 | // 3. 处理业务消息 | 70 | // 3. 处理业务消息 |
| 78 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); | 71 | deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); |
| 79 | log.debug("[handleBusinessRequest][消息处理成功,客户端 ID: {},主题: {}]", clientId, topic); | 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 | } catch (Exception e) { | 73 | } catch (Exception e) { |
| 94 | log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]", | 74 | log.error("[handleBusinessRequest][消息处理异常,客户端 ID: {},主题: {},错误: {}]", |
| 95 | clientId, topic, e.getMessage(), e); | 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 | package cn.iocoder.yudao.module.iot.gateway.util; | 1 | package cn.iocoder.yudao.module.iot.gateway.util; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.util.StrUtil; | 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 | * IoT 网关 MQTT 主题工具类 | 8 | * IoT 网关 MQTT 主题工具类 |
| @@ -44,6 +46,32 @@ public final class IotMqttTopicUtils { | @@ -44,6 +46,32 @@ public final class IotMqttTopicUtils { | ||
| 44 | */ | 46 | */ |
| 45 | public static final String MQTT_ACL_PATH = "/mqtt/acl"; | 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,12 +239,28 @@ public class IotDirectDeviceMqttProtocolIntegrationTest { | ||
| 239 | log.info("[testSubscribe][连接认证成功]"); | 239 | log.info("[testSubscribe][连接认证成功]"); |
| 240 | 240 | ||
| 241 | try { | 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 | log.info("[testSubscribe][订阅主题: {}]", topic); | 264 | log.info("[testSubscribe][订阅主题: {}]", topic); |
| 249 | subscribe(client, topic); | 265 | subscribe(client, topic); |
| 250 | log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); | 266 | log.info("[testSubscribe][订阅成功,等待下行消息... (30秒后自动断开)]"); |
yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java
| @@ -60,7 +60,7 @@ public class MpAccountServiceImpl implements MpAccountService { | @@ -60,7 +60,7 @@ public class MpAccountServiceImpl implements MpAccountService { | ||
| 60 | private MpAccountMapper mpAccountMapper; | 60 | private MpAccountMapper mpAccountMapper; |
| 61 | 61 | ||
| 62 | @Resource | 62 | @Resource |
| 63 | - @Lazy // 延迟加载,解决循环依赖的问题 | 63 | + @Lazy // 延迟加载,解决循环依赖 |
| 64 | private MpServiceFactory mpServiceFactory; | 64 | private MpServiceFactory mpServiceFactory; |
| 65 | 65 | ||
| 66 | @Override | 66 | @Override |