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,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