Commit b800d274a4ce2dfb0a5a401545c0d50005cdafe7

Authored by YunaiV
1 parent fcca74ac

feat(iot):支持 MQTT 设备回复消息的 _reply 方法标准化,并精简上行处理器异常处理

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
... ... @@ -60,7 +60,7 @@ public class MpAccountServiceImpl implements MpAccountService {
60 60 private MpAccountMapper mpAccountMapper;
61 61  
62 62 @Resource
63   - @Lazy // 延迟加载,解决循环依赖的问题
  63 + @Lazy // 延迟加载,解决循环依赖
64 64 private MpServiceFactory mpServiceFactory;
65 65  
66 66 @Override
... ...