Commit 666afcc3e7a2866459eb35840e8d52445234905e

Authored by haohao
1 parent 233a2f7d

refactor(iot): 影子透传按设备协议序列化,reply 对齐 CommonResult

- Gateway 透传编解码改为 IotMessageSerializer.encodeRaw/decodeRaw,移除 IotShadowPassthroughCodec
- 下行 reply 扁平为 { method, code, msg, data },version/timestamp 收进 data(Breaking)
- 扩展 IotDeviceShadowProtocolUtils,biz/gateway 测试同步适配
Showing 15 changed files with 325 additions and 216 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/shadow/IotDeviceShadowServiceImpl.java
@@ -7,10 +7,10 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -7,10 +7,10 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 8 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
9 import cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowErrorCodeEnum; 9 import cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowErrorCodeEnum;
10 -import cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils;  
11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 10 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
12 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO; 11 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO;
13 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowUpdateReqDTO; 12 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowUpdateReqDTO;
  13 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils;
14 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 14 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
15 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceShadowDO; 15 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceShadowDO;
16 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceShadowMapper; 16 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceShadowMapper;
@@ -32,8 +32,7 @@ import java.util.Map; @@ -32,8 +32,7 @@ import java.util.Map;
32 import java.util.function.Function; 32 import java.util.function.Function;
33 33
34 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 34 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
35 -import static cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils.INNER_METHOD_GET;  
36 -import static cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils.INNER_METHOD_UPDATE; 35 +import static cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils.*;
37 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_NOT_ENABLED; 36 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_NOT_ENABLED;
38 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_VERSION_CONFLICT; 37 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_VERSION_CONFLICT;
39 38
@@ -89,23 +88,26 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -89,23 +88,26 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
89 } catch (Exception ex) { 88 } catch (Exception ex) {
90 log.error("[handleDeviceShadowUpdate][device({}) message({}) 处理异常]", device.getId(), message, ex); 89 log.error("[handleDeviceShadowUpdate][device({}) message({}) 处理异常]", device.getId(), message, ex);
91 Long version = resolveShadowVersionSafely(device.getId()); 90 Long version = resolveShadowVersionSafely(device.getId());
92 - sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(version, 91 + sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(
  92 + extractInnerMethod(message.getParams()), version,
93 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getCode(), 93 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getCode(),
94 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getMessage(), DateUtil.currentSeconds())); 94 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getMessage(), DateUtil.currentSeconds()));
95 } 95 }
96 } 96 }
97 97
98 private void handleDeviceShadowUpdate0(IotDeviceDO device, IotDeviceMessage message) { 98 private void handleDeviceShadowUpdate0(IotDeviceDO device, IotDeviceMessage message) {
99 - // 1. 解析请求 99 + // 1. 解析请求(影子 body 当前按 JSON 建模,非 JSON 设备在 gateway 透传层已拒绝)
100 IotDeviceShadowUpdateReqDTO req = JsonUtils.convertObject(message.getParams(), IotDeviceShadowUpdateReqDTO.class); 100 IotDeviceShadowUpdateReqDTO req = JsonUtils.convertObject(message.getParams(), IotDeviceShadowUpdateReqDTO.class);
101 if (req == null) { 101 if (req == null) {
102 - sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(0L, 102 + sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(
  103 + extractInnerMethod(message.getParams()), 0L,
103 IotDeviceShadowErrorCodeEnum.JSON_FORMAT_ERROR.getCode(), 104 IotDeviceShadowErrorCodeEnum.JSON_FORMAT_ERROR.getCode(),
104 IotDeviceShadowErrorCodeEnum.JSON_FORMAT_ERROR.getMessage(), DateUtil.currentSeconds())); 105 IotDeviceShadowErrorCodeEnum.JSON_FORMAT_ERROR.getMessage(), DateUtil.currentSeconds()));
105 return; 106 return;
106 } 107 }
107 if (StrUtil.isEmpty(req.getMethod())) { 108 if (StrUtil.isEmpty(req.getMethod())) {
108 - sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(resolveShadowVersionSafely(device.getId()), 109 + sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(
  110 + INNER_METHOD_UPDATE, resolveShadowVersionSafely(device.getId()),
109 IotDeviceShadowErrorCodeEnum.METHOD_MISSING.getCode(), 111 IotDeviceShadowErrorCodeEnum.METHOD_MISSING.getCode(),
110 IotDeviceShadowErrorCodeEnum.METHOD_MISSING.getMessage(), DateUtil.currentSeconds())); 112 IotDeviceShadowErrorCodeEnum.METHOD_MISSING.getMessage(), DateUtil.currentSeconds()));
111 return; 113 return;
@@ -128,7 +130,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -128,7 +130,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
128 handleReportedUpdate(device, message.getServerId(), req); 130 handleReportedUpdate(device, message.getServerId(), req);
129 return; 131 return;
130 } 132 }
131 - sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(shadow.getVersion(), 133 + sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(
  134 + req.getMethod(), shadow.getVersion(),
132 IotDeviceShadowErrorCodeEnum.INVALID_METHOD.getCode(), 135 IotDeviceShadowErrorCodeEnum.INVALID_METHOD.getCode(),
133 IotDeviceShadowErrorCodeEnum.INVALID_METHOD.getMessage(), DateUtil.currentSeconds())); 136 IotDeviceShadowErrorCodeEnum.INVALID_METHOD.getMessage(), DateUtil.currentSeconds()));
134 } 137 }
@@ -137,19 +140,22 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -137,19 +140,22 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
137 Long deviceId = device.getId(); 140 Long deviceId = device.getId();
138 141
139 if (req.getState() == null) { 142 if (req.getState() == null) {
140 - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(resolveShadowVersionSafely(deviceId), 143 + sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
  144 + INNER_METHOD_UPDATE, resolveShadowVersionSafely(deviceId),
141 IotDeviceShadowErrorCodeEnum.STATE_MISSING.getCode(), 145 IotDeviceShadowErrorCodeEnum.STATE_MISSING.getCode(),
142 IotDeviceShadowErrorCodeEnum.STATE_MISSING.getMessage(), DateUtil.currentSeconds())); 146 IotDeviceShadowErrorCodeEnum.STATE_MISSING.getMessage(), DateUtil.currentSeconds()));
143 return; 147 return;
144 } 148 }
145 if (req.getState().getReported() == null) { 149 if (req.getState().getReported() == null) {
146 - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(resolveShadowVersionSafely(deviceId), 150 + sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
  151 + INNER_METHOD_UPDATE, resolveShadowVersionSafely(deviceId),
147 IotDeviceShadowErrorCodeEnum.REPORTED_MISSING.getCode(), 152 IotDeviceShadowErrorCodeEnum.REPORTED_MISSING.getCode(),
148 IotDeviceShadowErrorCodeEnum.REPORTED_MISSING.getMessage(), DateUtil.currentSeconds())); 153 IotDeviceShadowErrorCodeEnum.REPORTED_MISSING.getMessage(), DateUtil.currentSeconds()));
149 return; 154 return;
150 } 155 }
151 if (CollUtil.isEmpty(req.getState().getReported())) { 156 if (CollUtil.isEmpty(req.getState().getReported())) {
152 - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(resolveShadowVersionSafely(deviceId), 157 + sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
  158 + INNER_METHOD_UPDATE, resolveShadowVersionSafely(deviceId),
153 IotDeviceShadowErrorCodeEnum.REPORTED_EMPTY.getCode(), 159 IotDeviceShadowErrorCodeEnum.REPORTED_EMPTY.getCode(),
154 IotDeviceShadowErrorCodeEnum.REPORTED_EMPTY.getMessage(), DateUtil.currentSeconds())); 160 IotDeviceShadowErrorCodeEnum.REPORTED_EMPTY.getMessage(), DateUtil.currentSeconds()));
155 return; 161 return;
@@ -171,7 +177,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -171,7 +177,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
171 sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.updateReplyOf(committed.getVersion(), 177 sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.updateReplyOf(committed.getVersion(),
172 committed.getShadowTime())); 178 committed.getShadowTime()));
173 } catch (ShadowUpdateRejectedException ex) { 179 } catch (ShadowUpdateRejectedException ex) {
174 - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(ex.getVersion(), 180 + sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
  181 + INNER_METHOD_UPDATE, ex.getVersion(),
175 ex.getErrorCode().getCode(), ex.getErrorCode().getMessage(), DateUtil.currentSeconds())); 182 ex.getErrorCode().getCode(), ex.getErrorCode().getMessage(), DateUtil.currentSeconds()));
176 } catch (ShadowCasExhaustedException ex) { 183 } catch (ShadowCasExhaustedException ex) {
177 handleCasExhaustedForDevice(device, serverId, ex); 184 handleCasExhaustedForDevice(device, serverId, ex);
@@ -340,7 +347,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -340,7 +347,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
340 347
341 private void handleCasExhaustedForDevice(IotDeviceDO device, String serverId, ShadowCasExhaustedException ex) { 348 private void handleCasExhaustedForDevice(IotDeviceDO device, String serverId, ShadowCasExhaustedException ex) {
342 if (ex.isShadowNotFound()) { 349 if (ex.isShadowNotFound()) {
343 - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(0L, 350 + sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
  351 + INNER_METHOD_UPDATE, 0L,
344 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getCode(), 352 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getCode(),
345 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getMessage(), DateUtil.currentSeconds())); 353 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getMessage(), DateUtil.currentSeconds()));
346 return; 354 return;
@@ -349,7 +357,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -349,7 +357,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
349 } 357 }
350 358
351 private void sendVersionConflictReply(IotDeviceDO device, String serverId, Long version) { 359 private void sendVersionConflictReply(IotDeviceDO device, String serverId, Long version) {
352 - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(version, 360 + sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
  361 + INNER_METHOD_UPDATE, version,
353 IotDeviceShadowErrorCodeEnum.VERSION_CONFLICT.getCode(), 362 IotDeviceShadowErrorCodeEnum.VERSION_CONFLICT.getCode(),
354 IotDeviceShadowErrorCodeEnum.VERSION_CONFLICT.getMessage(), DateUtil.currentSeconds())); 363 IotDeviceShadowErrorCodeEnum.VERSION_CONFLICT.getMessage(), DateUtil.currentSeconds()));
355 } 364 }
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/shadow/IotDeviceShadowServiceImplTest.java
@@ -85,7 +85,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -85,7 +85,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
85 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1")); 85 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
86 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams(); 86 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
87 assertEquals("get_reply", reply.getMethod()); 87 assertEquals("get_reply", reply.getMethod());
88 - assertEquals(2L, reply.getVersion()); 88 + assertEquals(2L, getReplyVersion(reply));
89 } 89 }
90 90
91 @Test 91 @Test
@@ -142,9 +142,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -142,9 +142,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
142 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class); 142 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
143 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1")); 143 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
144 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams(); 144 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
145 - @SuppressWarnings("unchecked")  
146 - Map<String, Object> payload = (Map<String, Object>) reply.getPayload();  
147 - assertEquals(409, payload.get("code")); 145 + assertEquals(409, reply.getCode());
148 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any()); 146 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
149 } 147 }
150 148
@@ -196,9 +194,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -196,9 +194,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
196 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1")); 194 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
197 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams(); 195 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
198 assertEquals("update_reply", reply.getMethod()); 196 assertEquals("update_reply", reply.getMethod());
199 - @SuppressWarnings("unchecked")  
200 - Map<String, Object> payload = (Map<String, Object>) reply.getPayload();  
201 - assertEquals(409, payload.get("code")); 197 + assertEquals(409, reply.getCode());
202 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any()); 198 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
203 } 199 }
204 200
@@ -268,7 +264,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -268,7 +264,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
268 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1")); 264 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
269 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams(); 265 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
270 assertEquals("get_reply", reply.getMethod()); 266 assertEquals("get_reply", reply.getMethod());
271 - assertEquals(2L, reply.getVersion()); 267 + assertEquals(2L, getReplyVersion(reply));
272 // getOrCreateShadow + CAS 各一次 SELECT,推送不再额外查库 268 // getOrCreateShadow + CAS 各一次 SELECT,推送不再额外查库
273 verify(deviceShadowMapper, times(2)).selectByDeviceId(deviceId); 269 verify(deviceShadowMapper, times(2)).selectByDeviceId(deviceId);
274 verify(deviceShadowRedisDAO).delete(deviceId); 270 verify(deviceShadowRedisDAO).delete(deviceId);
@@ -293,6 +289,21 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -293,6 +289,21 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
293 } 289 }
294 290
295 @Test 291 @Test
  292 + public void testHandleDeviceShadowUpdate_invalidParams_send400() {
  293 + IotDeviceDO device = buildDevice();
  294 +
  295 + IotDeviceMessage message = IotDeviceMessage.requestOf(device.getId(), 1L, "server-1",
  296 + IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), null);
  297 +
  298 + service.handleDeviceShadowUpdate(device, message);
  299 +
  300 + ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
  301 + verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
  302 + IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
  303 + assertEquals(400, reply.getCode());
  304 + }
  305 +
  306 + @Test
296 public void testHandleDeviceShadowUpdate_methodMissing_send401() { 307 public void testHandleDeviceShadowUpdate_methodMissing_send401() {
297 IotDeviceDO device = buildDevice(); 308 IotDeviceDO device = buildDevice();
298 IotDeviceShadowDO shadow = buildShadow(device, 3L); 309 IotDeviceShadowDO shadow = buildShadow(device, 3L);
@@ -309,9 +320,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -309,9 +320,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
309 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class); 320 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
310 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1")); 321 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
311 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams(); 322 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
312 - @SuppressWarnings("unchecked")  
313 - Map<String, Object> payload = (Map<String, Object>) reply.getPayload();  
314 - assertEquals(401, payload.get("code")); 323 + assertEquals(401, reply.getCode());
315 } 324 }
316 325
317 @Test 326 @Test
@@ -365,7 +374,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -365,7 +374,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
365 ArgumentCaptor<IotDeviceMessage> replyCaptor = ArgumentCaptor.forClass(IotDeviceMessage.class); 374 ArgumentCaptor<IotDeviceMessage> replyCaptor = ArgumentCaptor.forClass(IotDeviceMessage.class);
366 verify(deviceMessageService).sendDeviceMessage(replyCaptor.capture(), eq(device), eq("server-1")); 375 verify(deviceMessageService).sendDeviceMessage(replyCaptor.capture(), eq(device), eq("server-1"));
367 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) replyCaptor.getValue().getParams(); 376 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) replyCaptor.getValue().getParams();
368 - assertEquals(3L, reply.getVersion()); 377 + assertEquals(3L, getReplyVersion(reply));
369 } 378 }
370 379
371 @Test 380 @Test
@@ -429,10 +438,8 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -429,10 +438,8 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
429 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class); 438 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
430 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1")); 439 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
431 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams(); 440 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
432 - assertEquals(0L, reply.getVersion());  
433 - @SuppressWarnings("unchecked")  
434 - Map<String, Object> payload = (Map<String, Object>) reply.getPayload();  
435 - assertEquals(500, payload.get("code")); 441 + assertEquals(0L, getReplyVersion(reply));
  442 + assertEquals(500, reply.getCode());
436 } 443 }
437 444
438 @Test 445 @Test
@@ -458,9 +465,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -458,9 +465,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
458 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class); 465 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
459 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1")); 466 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
460 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams(); 467 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
461 - @SuppressWarnings("unchecked")  
462 - Map<String, Object> payload = (Map<String, Object>) reply.getPayload();  
463 - assertEquals(500, payload.get("code")); 468 + assertEquals(500, reply.getCode());
464 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any()); 469 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
465 } 470 }
466 471
@@ -493,4 +498,13 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -493,4 +498,13 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
493 .build(); 498 .build();
494 } 499 }
495 500
  501 + @SuppressWarnings("unchecked")
  502 + private static Long getReplyVersion(IotDeviceShadowReplyDTO reply) {
  503 + if (!(reply.getData() instanceof Map)) {
  504 + return null;
  505 + }
  506 + Object version = ((Map<String, Object>) reply.getData()).get("version");
  507 + return version instanceof Number ? ((Number) version).longValue() : null;
  508 + }
  509 +
496 } 510 }
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/shadow/IotDeviceShadowErrorCodeEnum.java
@@ -6,7 +6,7 @@ import lombok.Getter; @@ -6,7 +6,7 @@ import lombok.Getter;
6 /** 6 /**
7 * IoT 设备影子设备侧错误码枚举 7 * IoT 设备影子设备侧错误码枚举
8 * <p> 8 * <p>
9 - * 设备侧 wire code,仅用于回填 {@code update_reply.payload.code/msg},不走 {@link cn.iocoder.yudao.framework.common.pojo.CommonResult}。 9 + * 设备侧 wire code,仅用于回填 {@code update_reply.code/msg},不走 {@link cn.iocoder.yudao.framework.common.pojo.CommonResult}。
10 * 服务端内部异常请使用 {@link cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants}(如 {@code DEVICE_SHADOW_NOT_ENABLED})。 10 * 服务端内部异常请使用 {@link cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants}(如 {@code DEVICE_SHADOW_NOT_ENABLED})。
11 * 11 *
12 * @author 芋道源码 12 * @author 芋道源码
@@ -21,7 +21,6 @@ public enum IotDeviceShadowErrorCodeEnum { @@ -21,7 +21,6 @@ public enum IotDeviceShadowErrorCodeEnum {
21 REPORTED_MISSING(404, "缺少 reported"), 21 REPORTED_MISSING(404, "缺少 reported"),
22 REPORTED_EMPTY(405, "reported 为空"), 22 REPORTED_EMPTY(405, "reported 为空"),
23 INVALID_METHOD(406, "无效内层 method"), 23 INVALID_METHOD(406, "无效内层 method"),
24 - SHADOW_EMPTY(407, "影子内容为空"),  
25 PROPERTY_COUNT_EXCEEDED(408, "影子属性数超过限制"), 24 PROPERTY_COUNT_EXCEEDED(408, "影子属性数超过限制"),
26 VERSION_CONFLICT(409, "影子版本冲突"), 25 VERSION_CONFLICT(409, "影子版本冲突"),
27 SERVER_ERROR(500, "服务端异常"); 26 SERVER_ERROR(500, "服务端异常");
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/shadow/IotDeviceShadowReplyDTO.java
@@ -14,7 +14,8 @@ import static cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils @@ -14,7 +14,8 @@ import static cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils
14 /** 14 /**
15 * IoT 设备影子下行 Reply DTO 15 * IoT 设备影子下行 Reply DTO
16 * <p> 16 * <p>
17 - * 用于 {@link IotDeviceMessageMethodEnum#SHADOW_GET} 消息的 params 参数(透传原始线 body) 17 + * 扁平对齐 CommonResult + method,用于 {@link IotDeviceMessageMethodEnum#SHADOW_GET} 透传线 body。
  18 + * 错误回复的 {@link #data} 仍携带 version 供设备 resync,有意偏离纯 CommonResult(error 时 data 通常为 null)。
18 * 19 *
19 * @author 芋道源码 20 * @author 芋道源码
20 */ 21 */
@@ -29,19 +30,19 @@ public class IotDeviceShadowReplyDTO { @@ -29,19 +30,19 @@ public class IotDeviceShadowReplyDTO {
29 private String method; 30 private String method;
30 31
31 /** 32 /**
32 - * 回复载荷(对齐 CommonResult:code / msg / data) 33 + * 结果码,0 表示成功
33 */ 34 */
34 - private Object payload; 35 + private Integer code;
35 36
36 /** 37 /**
37 - * 影子版本号 38 + * 结果描述
38 */ 39 */
39 - private Long version; 40 + private String msg;
40 41
41 /** 42 /**
42 - * 时间戳(epoch 秒) 43 + * 业务数据:get_reply 为 {state, version, timestamp};update_reply 为 {version, timestamp}
43 */ 44 */
44 - private Long timestamp; 45 + private Object data;
45 46
46 public static IotDeviceShadowReplyDTO getReplyOf(Map<String, Object> desired, Map<String, Object> reported, 47 public static IotDeviceShadowReplyDTO getReplyOf(Map<String, Object> desired, Map<String, Object> reported,
47 Long version, Long timestamp) { 48 Long version, Long timestamp) {
@@ -50,45 +51,29 @@ public class IotDeviceShadowReplyDTO { @@ -50,45 +51,29 @@ public class IotDeviceShadowReplyDTO {
50 state.put(STATE_REPORTED, reported != null ? reported : new HashMap<>()); 51 state.put(STATE_REPORTED, reported != null ? reported : new HashMap<>());
51 Map<String, Object> data = new HashMap<>(); 52 Map<String, Object> data = new HashMap<>();
52 data.put(PAYLOAD_STATE, state); 53 data.put(PAYLOAD_STATE, state);
53 - Map<String, Object> payload = buildSuccessPayload(data);  
54 - return new IotDeviceShadowReplyDTO()  
55 - .setMethod(INNER_METHOD_GET_REPLY)  
56 - .setPayload(payload)  
57 - .setVersion(version)  
58 - .setTimestamp(timestamp); 54 + data.put(PAYLOAD_VERSION, version);
  55 + data.put(METADATA_TIMESTAMP, timestamp);
  56 + return of(INNER_METHOD_GET_REPLY, SUCCESS.getCode(), SUCCESS.getMsg(), data);
59 } 57 }
60 58
61 public static IotDeviceShadowReplyDTO updateReplyOf(Long version, Long timestamp) { 59 public static IotDeviceShadowReplyDTO updateReplyOf(Long version, Long timestamp) {
62 - Map<String, Object> data = new HashMap<>();  
63 - data.put(PAYLOAD_VERSION, version);  
64 - Map<String, Object> payload = buildSuccessPayload(data);  
65 - return new IotDeviceShadowReplyDTO()  
66 - .setMethod(INNER_METHOD_UPDATE_REPLY)  
67 - .setPayload(payload)  
68 - .setVersion(version)  
69 - .setTimestamp(timestamp); 60 + return of(INNER_METHOD_UPDATE_REPLY, SUCCESS.getCode(), SUCCESS.getMsg(), dataWithVersionTimestamp(version, timestamp));
70 } 61 }
71 62
72 - public static IotDeviceShadowReplyDTO errorOf(Long version, Integer errorCode, String errorMsg, Long timestamp) {  
73 - Map<String, Object> data = new HashMap<>();  
74 - data.put(PAYLOAD_VERSION, version);  
75 - Map<String, Object> payload = new HashMap<>();  
76 - payload.put(PAYLOAD_CODE, errorCode);  
77 - payload.put(PAYLOAD_MSG, errorMsg);  
78 - payload.put(PAYLOAD_DATA, data);  
79 - return new IotDeviceShadowReplyDTO()  
80 - .setMethod(INNER_METHOD_UPDATE_REPLY)  
81 - .setPayload(payload)  
82 - .setVersion(version)  
83 - .setTimestamp(timestamp); 63 + public static IotDeviceShadowReplyDTO errorOf(String innerMethod, Long version, Integer errorCode,
  64 + String errorMsg, Long timestamp) {
  65 + return of(toReplyMethod(innerMethod), errorCode, errorMsg, dataWithVersionTimestamp(version, timestamp));
84 } 66 }
85 67
86 - private static Map<String, Object> buildSuccessPayload(Map<String, Object> data) {  
87 - Map<String, Object> payload = new HashMap<>();  
88 - payload.put(PAYLOAD_CODE, SUCCESS.getCode());  
89 - payload.put(PAYLOAD_MSG, SUCCESS.getMsg());  
90 - payload.put(PAYLOAD_DATA, data);  
91 - return payload; 68 + private static IotDeviceShadowReplyDTO of(String method, Integer code, String msg, Map<String, Object> data) {
  69 + return new IotDeviceShadowReplyDTO().setMethod(method).setCode(code).setMsg(msg).setData(data);
  70 + }
  71 +
  72 + private static Map<String, Object> dataWithVersionTimestamp(Long version, Long timestamp) {
  73 + Map<String, Object> data = new HashMap<>();
  74 + data.put(PAYLOAD_VERSION, version);
  75 + data.put(METADATA_TIMESTAMP, timestamp);
  76 + return data;
92 } 77 }
93 78
94 } 79 }
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/shadow/IotDeviceShadowUpdateReqDTO.java
@@ -44,11 +44,6 @@ public class IotDeviceShadowUpdateReqDTO { @@ -44,11 +44,6 @@ public class IotDeviceShadowUpdateReqDTO {
44 */ 44 */
45 private Map<String, Object> reported; 45 private Map<String, Object> reported;
46 46
47 - /**  
48 - * 期望状态  
49 - */  
50 - private Map<String, Object> desired;  
51 -  
52 } 47 }
53 48
54 } 49 }
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceShadowProtocolUtils.java
@@ -31,15 +31,33 @@ public final class IotDeviceShadowProtocolUtils { @@ -31,15 +31,33 @@ public final class IotDeviceShadowProtocolUtils {
31 public static final String STATE_REPORTED = "reported"; 31 public static final String STATE_REPORTED = "reported";
32 public static final String METADATA_TIMESTAMP = "timestamp"; 32 public static final String METADATA_TIMESTAMP = "timestamp";
33 33
34 - // ========== payload 键(对齐 CommonResult:code / msg / data)========== 34 + // ========== reply data 键 ==========
35 35
36 - public static final String PAYLOAD_CODE = "code";  
37 - public static final String PAYLOAD_MSG = "msg";  
38 - public static final String PAYLOAD_DATA = "data";  
39 public static final String PAYLOAD_STATE = "state"; 36 public static final String PAYLOAD_STATE = "state";
40 public static final String PAYLOAD_VERSION = "version"; 37 public static final String PAYLOAD_VERSION = "version";
41 38
42 /** 39 /**
  40 + * 由内层 method 推导 reply method(get → get_reply,其余 → update_reply)
  41 + */
  42 + public static String toReplyMethod(String innerMethod) {
  43 + if (INNER_METHOD_GET.equals(innerMethod)) {
  44 + return INNER_METHOD_GET_REPLY;
  45 + }
  46 + return INNER_METHOD_UPDATE_REPLY;
  47 + }
  48 +
  49 + /**
  50 + * 从透传 params 提取内层 method;无法解析时默认 update
  51 + */
  52 + public static String extractInnerMethod(Object params) {
  53 + if (!(params instanceof Map)) {
  54 + return INNER_METHOD_UPDATE;
  55 + }
  56 + Object method = ((Map<?, ?>) params).get("method");
  57 + return method != null ? String.valueOf(method) : INNER_METHOD_UPDATE;
  58 + }
  59 +
  60 + /**
43 * 获取 desired 下某属性的更新时间戳 61 * 获取 desired 下某属性的更新时间戳
44 * 62 *
45 * @param metadata 元数据 63 * @param metadata 元数据
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java
@@ -3,12 +3,16 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream; @@ -3,12 +3,16 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.emqx.handler.upstream;
3 import cn.hutool.core.util.ArrayUtil; 3 import cn.hutool.core.util.ArrayUtil;
4 import cn.hutool.core.util.StrUtil; 4 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.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 8 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; 9 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
9 import io.vertx.mqtt.messages.MqttPublishMessage; 10 import io.vertx.mqtt.messages.MqttPublishMessage;
10 import lombok.extern.slf4j.Slf4j; 11 import lombok.extern.slf4j.Slf4j;
11 12
  13 +import java.util.Arrays;
  14 +import java.util.stream.Collectors;
  15 +
12 /** 16 /**
13 * IoT 网关 EMQX 上行消息处理器 17 * IoT 网关 EMQX 上行消息处理器
14 * 18 *
@@ -44,13 +48,24 @@ public class IotEmqxUpstreamHandler { @@ -44,13 +48,24 @@ public class IotEmqxUpstreamHandler {
44 } 48 }
45 49
46 // 2.1 反序列化消息 50 // 2.1 反序列化消息
47 - IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);  
48 - if (message == null) {  
49 - log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));  
50 - return; 51 + String topicMethod = Arrays.stream(topicParts, 4, topicParts.length)
  52 + .collect(Collectors.joining("."));
  53 + IotDeviceMessage message;
  54 + if (IotDeviceMessageMethodEnum.isPassthrough(topicMethod)) {
  55 + message = deviceMessageService.deserializePassthroughMessage(payload, topicMethod, productKey, deviceName);
  56 + if (message == null) {
  57 + log.warn("[handle][topic({}) 影子透传 payload 解析失败]", topic);
  58 + return;
  59 + }
  60 + } else {
  61 + message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
  62 + if (message == null) {
  63 + log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
  64 + return;
  65 + }
  66 + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)
  67 + IotMqttTopicUtils.normalizeReplyMethod(message);
51 } 68 }
52 - // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)  
53 - IotMqttTopicUtils.normalizeReplyMethod(message);  
54 69
55 // 3. 发送消息到队列 70 // 3. 发送消息到队列
56 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId); 71 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
@@ -17,7 +17,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqt @@ -17,7 +17,6 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqt
17 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler; 17 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler;
18 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; 18 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler;
19 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; 19 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
20 -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;  
21 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 20 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
22 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; 21 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
23 import io.netty.handler.codec.mqtt.MqttConnectReturnCode; 22 import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
@@ -102,10 +101,9 @@ public class IotMqttProtocol implements IotProtocol { @@ -102,10 +101,9 @@ public class IotMqttProtocol implements IotProtocol {
102 // 初始化 Handler 101 // 初始化 Handler
103 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); 102 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
104 IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); 103 IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
105 - IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);  
106 this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); 104 this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
107 this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService); 105 this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService);
108 - this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, deviceService, serverId); 106 + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId);
109 } 107 }
110 108
111 @Override 109 @Override
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
@@ -2,20 +2,15 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; @@ -2,20 +2,15 @@ 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.module.iot.core.biz.dto.IotDeviceRespDTO;  
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 -import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;  
8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
9 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; 7 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
10 -import cn.iocoder.yudao.module.iot.gateway.serialize.IotShadowPassthroughCodec;  
11 -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;  
12 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 8 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
13 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; 9 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
14 import io.vertx.mqtt.MqttEndpoint; 10 import io.vertx.mqtt.MqttEndpoint;
15 import lombok.extern.slf4j.Slf4j; 11 import lombok.extern.slf4j.Slf4j;
16 12
17 import java.util.Arrays; 13 import java.util.Arrays;
18 -import java.util.Map;  
19 import java.util.stream.Collectors; 14 import java.util.stream.Collectors;
20 15
21 16
@@ -28,14 +23,11 @@ import java.util.stream.Collectors; @@ -28,14 +23,11 @@ import java.util.stream.Collectors;
28 public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { 23 public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
29 24
30 private final String serverId; 25 private final String serverId;
31 - private final IotDeviceService deviceService;  
32 26
33 public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager, 27 public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager,
34 IotDeviceMessageService deviceMessageService, 28 IotDeviceMessageService deviceMessageService,
35 - IotDeviceService deviceService,  
36 String serverId) { 29 String serverId) {
37 super(connectionManager, deviceMessageService); 30 super(connectionManager, deviceMessageService);
38 - this.deviceService = deviceService;  
39 this.serverId = serverId; 31 this.serverId = serverId;
40 } 32 }
41 33
@@ -75,17 +67,11 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { @@ -75,17 +67,11 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
75 .collect(Collectors.joining(".")); 67 .collect(Collectors.joining("."));
76 IotDeviceMessage message; 68 IotDeviceMessage message;
77 if (IotDeviceMessageMethodEnum.isPassthrough(topicMethod)) { 69 if (IotDeviceMessageMethodEnum.isPassthrough(topicMethod)) {
78 - // 影子透传:按设备 serializeType 解码,原始 body 作为 params  
79 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);  
80 - IotSerializeTypeEnum serializeType = device != null  
81 - ? IotSerializeTypeEnum.of(device.getSerializeType()) : null;  
82 - Map<String, Object> params = IotShadowPassthroughCodec.decode(payload, serializeType);  
83 - if (params == null) { 70 + message = deviceMessageService.deserializePassthroughMessage(payload, topicMethod, productKey, deviceName);
  71 + if (message == null) {
84 log.warn("[handleBusinessRequest][影子透传 payload 解析失败,客户端 ID: {},主题: {}]", clientId, topic); 72 log.warn("[handleBusinessRequest][影子透传 payload 解析失败,客户端 ID: {},主题: {}]", clientId, topic);
85 return; 73 return;
86 } 74 }
87 - String requestId = (String) params.get("id");  
88 - message = IotDeviceMessage.requestOf(requestId, topicMethod, params);  
89 } else { 75 } else {
90 message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName); 76 message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
91 if (message == null) { 77 if (message == null) {
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializer.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.iot.gateway.serialize; @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.iot.gateway.serialize;
3 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5
  6 +import java.util.Map;
  7 +
6 /** 8 /**
7 * IoT 设备消息序列化器接口 9 * IoT 设备消息序列化器接口
8 * 10 *
@@ -35,4 +37,24 @@ public interface IotMessageSerializer { @@ -35,4 +37,24 @@ public interface IotMessageSerializer {
35 */ 37 */
36 IotSerializeTypeEnum getType(); 38 IotSerializeTypeEnum getType();
37 39
  40 + /**
  41 + * 透传原始 body 编码(影子等不走 {@link IotDeviceMessage} 封套的场景)
  42 + *
  43 + * @param value 待编码对象(通常为 Map 或 DTO)
  44 + * @return 线格式字节
  45 + */
  46 + default byte[] encodeRaw(Object value) {
  47 + throw new UnsupportedOperationException("该序列化器不支持影子透传编码");
  48 + }
  49 +
  50 + /**
  51 + * 透传原始 body 解码
  52 + *
  53 + * @param bytes 线格式字节
  54 + * @return 透传 params
  55 + */
  56 + default Map<String, Object> decodeRaw(byte[] bytes) {
  57 + throw new UnsupportedOperationException("该序列化器不支持影子透传解码");
  58 + }
  59 +
38 } 60 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotShadowPassthroughCodec.java deleted
1 -package cn.iocoder.yudao.module.iot.gateway.serialize;  
2 -  
3 -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;  
4 -import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;  
5 -import lombok.extern.slf4j.Slf4j;  
6 -  
7 -import java.util.Map;  
8 -  
9 -/**  
10 - * IoT 设备影子透传编解码  
11 - * <p>  
12 - * 影子协议本质为 JSON 结构,透传编解码规则:  
13 - * <ul>  
14 - * <li>{@code serializeType == null} 或 {@link IotSerializeTypeEnum#JSON}:按 JSON 编解码(未配置时默认 JSON,兼容历史设备)</li>  
15 - * <li>其它显式类型(如 {@link IotSerializeTypeEnum#BINARY}):warn 并丢弃,返回 {@code null},不静默喂给 JSON 解析器</li>  
16 - * </ul>  
17 - * 下行发布方收到 {@code null} 时应跳过发送,避免 NPE;影子功能目前仅支持 JSON 协议设备。  
18 - *  
19 - * @author 芋道源码  
20 - */  
21 -@Slf4j  
22 -public final class IotShadowPassthroughCodec {  
23 -  
24 - private static final String UNSUPPORTED_MSG = "影子透传暂仅支持 JSON serializeType";  
25 -  
26 - private IotShadowPassthroughCodec() {  
27 - }  
28 -  
29 - /**  
30 - * 编码影子透传 params  
31 - *  
32 - * @param params 透传参数(Map 或 DTO)  
33 - * @param serializeType 设备序列化类型  
34 - * @return 线格式字节;非 JSON 类型返回 null  
35 - */  
36 - public static byte[] encode(Object params, IotSerializeTypeEnum serializeType) {  
37 - if (isJsonSerializeType(serializeType)) {  
38 - return JsonUtils.toJsonByte(params);  
39 - }  
40 - log.warn("[encode][{},serializeType={},已丢弃]", UNSUPPORTED_MSG, serializeType);  
41 - return null;  
42 - }  
43 -  
44 - /**  
45 - * 解码影子透传 payload  
46 - *  
47 - * @param bytes 线格式字节  
48 - * @param serializeType 设备序列化类型  
49 - * @return 透传 params;非 JSON 类型或解析失败返回 null  
50 - */  
51 - @SuppressWarnings("unchecked")  
52 - public static Map<String, Object> decode(byte[] bytes, IotSerializeTypeEnum serializeType) {  
53 - if (!isJsonSerializeType(serializeType)) {  
54 - log.warn("[decode][{},serializeType={},已丢弃]", UNSUPPORTED_MSG, serializeType);  
55 - return null;  
56 - }  
57 - return JsonUtils.parseObject(bytes, Map.class);  
58 - }  
59 -  
60 - private static boolean isJsonSerializeType(IotSerializeTypeEnum serializeType) {  
61 - // null 视为未配置,默认走 JSON(与产品/设备默认 serializeType 一致)  
62 - return serializeType == null || IotSerializeTypeEnum.JSON.equals(serializeType);  
63 - }  
64 -  
65 -}  
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java
@@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; @@ -6,6 +6,8 @@ import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
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.serialize.IotMessageSerializer; 7 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
8 8
  9 +import java.util.Map;
  10 +
9 /** 11 /**
10 * JSON 格式的消息序列化器 12 * JSON 格式的消息序列化器
11 * 13 *
@@ -34,4 +36,17 @@ public class IotJsonSerializer implements IotMessageSerializer { @@ -34,4 +36,17 @@ public class IotJsonSerializer implements IotMessageSerializer {
34 return message; 36 return message;
35 } 37 }
36 38
  39 + @Override
  40 + public byte[] encodeRaw(Object value) {
  41 + Assert.notNull(value, "待编码数据不能为空");
  42 + return JsonUtils.toJsonByte(value);
  43 + }
  44 +
  45 + @Override
  46 + @SuppressWarnings("unchecked")
  47 + public Map<String, Object> decodeRaw(byte[] bytes) {
  48 + Assert.notNull(bytes, "待解码数据不能为空");
  49 + return JsonUtils.parseObject(bytes, Map.class);
  50 + }
  51 +
37 } 52 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java
@@ -62,4 +62,16 @@ public interface IotDeviceMessageService { @@ -62,4 +62,16 @@ public interface IotDeviceMessageService {
62 void sendDeviceMessage(IotDeviceMessage message, 62 void sendDeviceMessage(IotDeviceMessage message,
63 String productKey, String deviceName, String serverId); 63 String productKey, String deviceName, String serverId);
64 64
  65 + /**
  66 + * 透传上行组装:按设备 serializeType 解码 body,提取 id 作 requestId,构建消息
  67 + *
  68 + * @param payload 原始 body
  69 + * @param method 透传 method(来自 topic 等,如 thing.shadow.update)
  70 + * @param productKey 产品 Key
  71 + * @param deviceName 设备名称
  72 + * @return 设备消息;解码失败返回 null
  73 + */
  74 + IotDeviceMessage deserializePassthroughMessage(byte[] payload, String method,
  75 + String productKey, String deviceName);
  76 +
65 } 77 }
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java
1 package cn.iocoder.yudao.module.iot.gateway.service.device.message; 1 package cn.iocoder.yudao.module.iot.gateway.service.device.message;
2 2
  3 +import cn.hutool.core.convert.Convert;
3 import cn.hutool.core.lang.Assert; 4 import cn.hutool.core.lang.Assert;
4 import cn.hutool.core.util.StrUtil; 5 import cn.hutool.core.util.StrUtil;
5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
@@ -10,13 +11,14 @@ import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; @@ -10,13 +11,14 @@ import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
11 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; 12 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
12 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; 13 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
13 -import cn.iocoder.yudao.module.iot.gateway.serialize.IotShadowPassthroughCodec;  
14 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 14 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
15 import jakarta.annotation.Resource; 15 import jakarta.annotation.Resource;
16 import lombok.extern.slf4j.Slf4j; 16 import lombok.extern.slf4j.Slf4j;
17 import org.springframework.stereotype.Service; 17 import org.springframework.stereotype.Service;
18 18
19 import java.time.LocalDateTime; 19 import java.time.LocalDateTime;
  20 +import java.util.Map;
  21 +import java.util.function.Function;
20 22
21 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 23 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
22 import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS; 24 import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS;
@@ -108,6 +110,23 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -108,6 +110,23 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
108 } 110 }
109 111
110 @Override 112 @Override
  113 + public IotDeviceMessage deserializePassthroughMessage(byte[] payload, String method,
  114 + String productKey, String deviceName) {
  115 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
  116 + if (device == null) {
  117 + throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
  118 + }
  119 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType());
  120 + Map<String, Object> params = decodePassthrough(payload, serializeType, productKey, deviceName);
  121 + if (params == null) {
  122 + // 非法/非对象 body:仍投递,由 biz 层回 JSON_FORMAT_ERROR(400)
  123 + return IotDeviceMessage.requestOf(null, method, null);
  124 + }
  125 + String requestId = Convert.toStr(params.get("id"), null);
  126 + return IotDeviceMessage.requestOf(requestId, method, params);
  127 + }
  128 +
  129 + @Override
111 public void sendDeviceMessage(IotDeviceMessage message, 130 public void sendDeviceMessage(IotDeviceMessage message,
112 String productKey, String deviceName, String serverId) { 131 String productKey, String deviceName, String serverId) {
113 // 1. 获取设备信息 132 // 1. 获取设备信息
@@ -139,19 +158,59 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -139,19 +158,59 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
139 } 158 }
140 159
141 /** 160 /**
142 - * 影子透传编码;非 JSON 设备返回 null,由下行发布方跳过发送 161 + * 影子透传编码;不支持的 serializeType 返回 null,由下行发布方跳过发送
143 */ 162 */
144 private byte[] encodePassthrough(IotDeviceMessage message, IotSerializeTypeEnum serializeType, 163 private byte[] encodePassthrough(IotDeviceMessage message, IotSerializeTypeEnum serializeType,
145 String productKey, String deviceName) { 164 String productKey, String deviceName) {
146 - byte[] bytes = IotShadowPassthroughCodec.encode(message.getParams(), serializeType);  
147 - if (bytes != null) {  
148 - return bytes; 165 + return passthroughRaw("encodePassthrough", serializeType, productKey, deviceName, false,
  166 + serializer -> serializer.encodeRaw(message.getParams()));
  167 + }
  168 +
  169 + /**
  170 + * 影子透传解码;解析失败或不支持的 serializeType 返回 null
  171 + */
  172 + private Map<String, Object> decodePassthrough(byte[] bytes, IotSerializeTypeEnum serializeType,
  173 + String productKey, String deviceName) {
  174 + return passthroughRaw("decodePassthrough", serializeType, productKey, deviceName, true,
  175 + serializer -> serializer.decodeRaw(bytes));
  176 + }
  177 +
  178 + private <T> T passthroughRaw(String op, IotSerializeTypeEnum serializeType,
  179 + String productKey, String deviceName, boolean decode,
  180 + Function<IotMessageSerializer, T> action) {
  181 + IotMessageSerializer serializer = getPassthroughSerializer(serializeType);
  182 + if (serializer == null) {
  183 + warnPassthroughUnsupported(op, serializeType, productKey, deviceName);
  184 + return null;
149 } 185 }
  186 + try {
  187 + return action.apply(serializer);
  188 + } catch (UnsupportedOperationException ex) {
  189 + warnPassthroughUnsupported(op, serializeType, productKey, deviceName);
  190 + return null;
  191 + } catch (RuntimeException ex) {
  192 + if (decode) {
  193 + log.warn("[{}][影子透传 payload 解析失败,productKey={}, deviceName={}]",
  194 + op, productKey, deviceName, ex);
  195 + return null;
  196 + }
  197 + throw ex;
  198 + }
  199 + }
  200 +
  201 + private IotMessageSerializer getPassthroughSerializer(IotSerializeTypeEnum serializeType) {
  202 + IotSerializeTypeEnum type = serializeType != null ? serializeType : IotSerializeTypeEnum.JSON;
  203 + return messageSerializerManager.get(type);
  204 + }
  205 +
  206 + private void warnPassthroughUnsupported(String op, IotSerializeTypeEnum serializeType,
  207 + String productKey, String deviceName) {
150 if (productKey != null && deviceName != null) { 208 if (productKey != null && deviceName != null) {
151 - log.warn("[encodePassthrough][影子下行跳过:设备({}/{}) serializeType={} 不支持,目前仅 JSON 协议设备可用]",  
152 - productKey, deviceName, serializeType); 209 + log.warn("[{}][影子透传跳过:设备({}/{}) serializeType={} 不支持,目前仅 JSON 协议设备可用]",
  210 + op, productKey, deviceName, serializeType);
  211 + } else {
  212 + log.warn("[{}][影子透传跳过:serializeType={} 不支持,目前仅 JSON 协议设备可用]", op, serializeType);
153 } 213 }
154 - return null;  
155 } 214 }
156 215
157 } 216 }
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceShadowPassthroughTest.java
@@ -2,20 +2,26 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message; @@ -2,20 +2,26 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message;
2 2
3 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 3 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; 4 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; 7 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO; 9 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO;
9 -import cn.iocoder.yudao.module.iot.gateway.serialize.IotShadowPassthroughCodec; 10 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
  11 +import cn.iocoder.yudao.module.iot.gateway.serialize.json.IotJsonSerializer;
  12 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
  13 +import org.junit.jupiter.api.BeforeEach;
10 import org.junit.jupiter.api.Test; 14 import org.junit.jupiter.api.Test;
11 import org.mockito.InjectMocks; 15 import org.mockito.InjectMocks;
  16 +import org.mockito.Mock;
  17 +import org.mockito.Spy;
12 18
13 import java.util.HashMap; 19 import java.util.HashMap;
14 import java.util.Map; 20 import java.util.Map;
15 21
16 -import static org.junit.jupiter.api.Assertions.assertEquals;  
17 -import static org.junit.jupiter.api.Assertions.assertNotNull;  
18 -import static org.junit.jupiter.api.Assertions.assertNull; 22 +import static org.junit.jupiter.api.Assertions.*;
  23 +import static org.mockito.ArgumentMatchers.eq;
  24 +import static org.mockito.Mockito.when;
19 25
20 /** 26 /**
21 * 设备影子网关透传序列化单元测试 27 * 设备影子网关透传序列化单元测试
@@ -25,35 +31,51 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest { @@ -25,35 +31,51 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest {
25 @InjectMocks 31 @InjectMocks
26 private IotDeviceMessageServiceImpl deviceMessageService; 32 private IotDeviceMessageServiceImpl deviceMessageService;
27 33
  34 + @Mock
  35 + private IotDeviceService deviceService;
  36 +
  37 + @Spy
  38 + private IotMessageSerializerManager messageSerializerManager = new IotMessageSerializerManager();
  39 +
  40 + @BeforeEach
  41 + public void setUpManager() {
  42 + // Spy 需手动触发构造逻辑
  43 + if (messageSerializerManager == null) {
  44 + messageSerializerManager = new IotMessageSerializerManager();
  45 + }
  46 + }
  47 +
28 @Test 48 @Test
29 - public void testSerializeDeviceMessage_passthrough_noEnvelope() {  
30 - Map<String, Object> payload = new HashMap<>();  
31 - payload.put("method", "get_reply");  
32 - payload.put("version", 2L);  
33 - payload.put("timestamp", 1469564576L); 49 + public void testSerializeDeviceMessage_passthrough_flatReply() {
  50 + IotDeviceShadowReplyDTO reply = IotDeviceShadowReplyDTO.getReplyOf(new HashMap<>(), new HashMap<>(), 2L, 1469564576L);
34 IotDeviceMessage message = IotDeviceMessage.requestOf( 51 IotDeviceMessage message = IotDeviceMessage.requestOf(
35 - IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), payload); 52 + IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), reply);
36 53
37 byte[] bytes = deviceMessageService.serializeDeviceMessage(message, IotSerializeTypeEnum.JSON); 54 byte[] bytes = deviceMessageService.serializeDeviceMessage(message, IotSerializeTypeEnum.JSON);
38 55
39 @SuppressWarnings("unchecked") 56 @SuppressWarnings("unchecked")
40 Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class); 57 Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class);
41 assertEquals("get_reply", result.get("method")); 58 assertEquals("get_reply", result.get("method"));
42 - assertEquals(2, ((Number) result.get("version")).intValue()); 59 + assertEquals(0, ((Number) result.get("code")).intValue());
  60 + assertNull(result.get("payload"));
43 assertNull(result.get("id")); 61 assertNull(result.get("id"));
44 - assertNull(result.get("code"));  
45 - assertNull(result.get("requestId")); 62 + @SuppressWarnings("unchecked")
  63 + Map<String, Object> data = (Map<String, Object>) result.get("data");
  64 + assertEquals(2, ((Number) data.get("version")).intValue());
46 } 65 }
47 66
48 @Test 67 @Test
49 - public void testDeserializeUpstreamShadow_fromTopicMethod() {  
50 - // 模拟上行透传:原始 body 不含 thing.shadow.update 封套 method  
51 - Map<String, Object> body = new HashMap<>();  
52 - body.put("method", "get");  
53 - IotDeviceMessage message = IotDeviceMessage.requestOf("req-1",  
54 - IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), body); 68 + public void testDeserializePassthroughMessage_fromTopicMethod() {
  69 + IotDeviceRespDTO device = new IotDeviceRespDTO().setId(1L).setSerializeType(IotSerializeTypeEnum.JSON.getType());
  70 + when(deviceService.getDeviceFromCache(eq("pk"), eq("dn"))).thenReturn(device);
  71 +
  72 + byte[] payload = JsonUtils.toJsonByte(Map.of("method", "get", "id", "req-1"));
  73 + IotDeviceMessage message = deviceMessageService.deserializePassthroughMessage(payload,
  74 + IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), "pk", "dn");
55 75
  76 + assertNotNull(message);
56 assertEquals(IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), message.getMethod()); 77 assertEquals(IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), message.getMethod());
  78 + assertEquals("req-1", message.getRequestId());
57 assertEquals("get", ((Map<?, ?>) message.getParams()).get("method")); 79 assertEquals("get", ((Map<?, ?>) message.getParams()).get("method"));
58 } 80 }
59 81
@@ -67,23 +89,16 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest { @@ -67,23 +89,16 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest {
67 @SuppressWarnings("unchecked") 89 @SuppressWarnings("unchecked")
68 Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class); 90 Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class);
69 assertEquals("update_reply", result.get("method")); 91 assertEquals("update_reply", result.get("method"));
  92 + assertEquals(0, ((Number) result.get("code")).intValue());
70 assertNull(result.get("id")); 93 assertNull(result.get("id"));
71 } 94 }
72 95
73 @Test 96 @Test
74 - public void testPassthroughCodec_jsonSerializeType() {  
75 - Map<String, Object> params = Map.of("method", "get");  
76 - byte[] bytes = IotShadowPassthroughCodec.encode(params, IotSerializeTypeEnum.JSON);  
77 - Map<String, Object> decoded = IotShadowPassthroughCodec.decode(bytes, IotSerializeTypeEnum.JSON);  
78 - assertNotNull(decoded);  
79 - assertEquals("get", decoded.get("method"));  
80 - }  
81 -  
82 - @Test  
83 - public void testPassthroughCodec_nullSerializeTypeAsJson() { 97 + public void testJsonSerializer_encodeRawDecodeRaw() {
  98 + IotJsonSerializer serializer = new IotJsonSerializer();
84 Map<String, Object> params = Map.of("method", "get"); 99 Map<String, Object> params = Map.of("method", "get");
85 - byte[] bytes = IotShadowPassthroughCodec.encode(params, null);  
86 - Map<String, Object> decoded = IotShadowPassthroughCodec.decode(bytes, null); 100 + byte[] bytes = serializer.encodeRaw(params);
  101 + Map<String, Object> decoded = serializer.decodeRaw(bytes);
87 assertNotNull(decoded); 102 assertNotNull(decoded);
88 assertEquals("get", decoded.get("method")); 103 assertEquals("get", decoded.get("method"));
89 } 104 }
@@ -96,10 +111,42 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest { @@ -96,10 +111,42 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest {
96 } 111 }
97 112
98 @Test 113 @Test
99 - public void testPassthroughCodec_binaryDrop() {  
100 - Map<String, Object> params = Map.of("method", "update");  
101 - assertNull(IotShadowPassthroughCodec.encode(params, IotSerializeTypeEnum.BINARY));  
102 - assertNull(IotShadowPassthroughCodec.decode(new byte[]{0x01}, IotSerializeTypeEnum.BINARY)); 114 + public void testBinarySerializer_rawNotSupported() {
  115 + cn.iocoder.yudao.module.iot.gateway.serialize.binary.IotBinarySerializer serializer =
  116 + new cn.iocoder.yudao.module.iot.gateway.serialize.binary.IotBinarySerializer();
  117 + assertThrows(UnsupportedOperationException.class, () -> serializer.encodeRaw(Map.of("method", "update")));
  118 + }
  119 +
  120 + @Test
  121 + public void testDeserializePassthroughMessage_numericId() {
  122 + IotDeviceRespDTO device = new IotDeviceRespDTO().setId(1L).setSerializeType(IotSerializeTypeEnum.JSON.getType());
  123 + when(deviceService.getDeviceFromCache(eq("pk"), eq("dn"))).thenReturn(device);
  124 +
  125 + byte[] payload = JsonUtils.toJsonByte(Map.of("method", "get", "id", 123));
  126 + IotDeviceMessage message = deviceMessageService.deserializePassthroughMessage(payload,
  127 + IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), "pk", "dn");
  128 +
  129 + assertNotNull(message);
  130 + assertEquals("123", message.getRequestId());
  131 + }
  132 +
  133 + @Test
  134 + public void testDeserializePassthroughMessage_invalidJsonStillDelivered() {
  135 + IotDeviceRespDTO device = new IotDeviceRespDTO().setId(1L).setSerializeType(IotSerializeTypeEnum.JSON.getType());
  136 + when(deviceService.getDeviceFromCache(eq("pk"), eq("dn"))).thenReturn(device);
  137 +
  138 + IotDeviceMessage message = deviceMessageService.deserializePassthroughMessage("{invalid".getBytes(),
  139 + IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), "pk", "dn");
  140 +
  141 + assertNotNull(message);
  142 + assertNull(message.getParams());
  143 + }
  144 +
  145 + @Test
  146 + public void testErrorOf_getMethodUsesGetReply() {
  147 + IotDeviceShadowReplyDTO reply = IotDeviceShadowReplyDTO.errorOf("get", 1L, 400, "JSON 格式错误", 1000L);
  148 + assertEquals("get_reply", reply.getMethod());
  149 + assertEquals(400, reply.getCode());
103 } 150 }
104 151
105 } 152 }