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 7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8 8 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
9 9 import cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowErrorCodeEnum;
10   -import cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils;
11 10 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
12 11 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO;
13 12 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowUpdateReqDTO;
  13 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils;
14 14 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
15 15 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceShadowDO;
16 16 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceShadowMapper;
... ... @@ -32,8 +32,7 @@ import java.util.Map;
32 32 import java.util.function.Function;
33 33  
34 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 36 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_NOT_ENABLED;
38 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 88 } catch (Exception ex) {
90 89 log.error("[handleDeviceShadowUpdate][device({}) message({}) 处理异常]", device.getId(), message, ex);
91 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 93 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getCode(),
94 94 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getMessage(), DateUtil.currentSeconds()));
95 95 }
96 96 }
97 97  
98 98 private void handleDeviceShadowUpdate0(IotDeviceDO device, IotDeviceMessage message) {
99   - // 1. 解析请求
  99 + // 1. 解析请求(影子 body 当前按 JSON 建模,非 JSON 设备在 gateway 透传层已拒绝)
100 100 IotDeviceShadowUpdateReqDTO req = JsonUtils.convertObject(message.getParams(), IotDeviceShadowUpdateReqDTO.class);
101 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 104 IotDeviceShadowErrorCodeEnum.JSON_FORMAT_ERROR.getCode(),
104 105 IotDeviceShadowErrorCodeEnum.JSON_FORMAT_ERROR.getMessage(), DateUtil.currentSeconds()));
105 106 return;
106 107 }
107 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 111 IotDeviceShadowErrorCodeEnum.METHOD_MISSING.getCode(),
110 112 IotDeviceShadowErrorCodeEnum.METHOD_MISSING.getMessage(), DateUtil.currentSeconds()));
111 113 return;
... ... @@ -128,7 +130,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
128 130 handleReportedUpdate(device, message.getServerId(), req);
129 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 135 IotDeviceShadowErrorCodeEnum.INVALID_METHOD.getCode(),
133 136 IotDeviceShadowErrorCodeEnum.INVALID_METHOD.getMessage(), DateUtil.currentSeconds()));
134 137 }
... ... @@ -137,19 +140,22 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
137 140 Long deviceId = device.getId();
138 141  
139 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 145 IotDeviceShadowErrorCodeEnum.STATE_MISSING.getCode(),
142 146 IotDeviceShadowErrorCodeEnum.STATE_MISSING.getMessage(), DateUtil.currentSeconds()));
143 147 return;
144 148 }
145 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 152 IotDeviceShadowErrorCodeEnum.REPORTED_MISSING.getCode(),
148 153 IotDeviceShadowErrorCodeEnum.REPORTED_MISSING.getMessage(), DateUtil.currentSeconds()));
149 154 return;
150 155 }
151 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 159 IotDeviceShadowErrorCodeEnum.REPORTED_EMPTY.getCode(),
154 160 IotDeviceShadowErrorCodeEnum.REPORTED_EMPTY.getMessage(), DateUtil.currentSeconds()));
155 161 return;
... ... @@ -171,7 +177,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
171 177 sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.updateReplyOf(committed.getVersion(),
172 178 committed.getShadowTime()));
173 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 182 ex.getErrorCode().getCode(), ex.getErrorCode().getMessage(), DateUtil.currentSeconds()));
176 183 } catch (ShadowCasExhaustedException ex) {
177 184 handleCasExhaustedForDevice(device, serverId, ex);
... ... @@ -340,7 +347,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
340 347  
341 348 private void handleCasExhaustedForDevice(IotDeviceDO device, String serverId, ShadowCasExhaustedException ex) {
342 349 if (ex.isShadowNotFound()) {
343   - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(0L,
  350 + sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
  351 + INNER_METHOD_UPDATE, 0L,
344 352 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getCode(),
345 353 IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getMessage(), DateUtil.currentSeconds()));
346 354 return;
... ... @@ -349,7 +357,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
349 357 }
350 358  
351 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 362 IotDeviceShadowErrorCodeEnum.VERSION_CONFLICT.getCode(),
354 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 85 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
86 86 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
87 87 assertEquals("get_reply", reply.getMethod());
88   - assertEquals(2L, reply.getVersion());
  88 + assertEquals(2L, getReplyVersion(reply));
89 89 }
90 90  
91 91 @Test
... ... @@ -142,9 +142,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
142 142 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
143 143 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
144 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 146 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
149 147 }
150 148  
... ... @@ -196,9 +194,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
196 194 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
197 195 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
198 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 198 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
203 199 }
204 200  
... ... @@ -268,7 +264,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
268 264 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
269 265 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
270 266 assertEquals("get_reply", reply.getMethod());
271   - assertEquals(2L, reply.getVersion());
  267 + assertEquals(2L, getReplyVersion(reply));
272 268 // getOrCreateShadow + CAS 各一次 SELECT,推送不再额外查库
273 269 verify(deviceShadowMapper, times(2)).selectByDeviceId(deviceId);
274 270 verify(deviceShadowRedisDAO).delete(deviceId);
... ... @@ -293,6 +289,21 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
293 289 }
294 290  
295 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 307 public void testHandleDeviceShadowUpdate_methodMissing_send401() {
297 308 IotDeviceDO device = buildDevice();
298 309 IotDeviceShadowDO shadow = buildShadow(device, 3L);
... ... @@ -309,9 +320,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
309 320 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
310 321 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
311 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 326 @Test
... ... @@ -365,7 +374,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
365 374 ArgumentCaptor<IotDeviceMessage> replyCaptor = ArgumentCaptor.forClass(IotDeviceMessage.class);
366 375 verify(deviceMessageService).sendDeviceMessage(replyCaptor.capture(), eq(device), eq("server-1"));
367 376 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) replyCaptor.getValue().getParams();
368   - assertEquals(3L, reply.getVersion());
  377 + assertEquals(3L, getReplyVersion(reply));
369 378 }
370 379  
371 380 @Test
... ... @@ -429,10 +438,8 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
429 438 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
430 439 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
431 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 445 @Test
... ... @@ -458,9 +465,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
458 465 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
459 466 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
460 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 469 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
465 470 }
466 471  
... ... @@ -493,4 +498,13 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
493 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 6 /**
7 7 * IoT 设备影子设备侧错误码枚举
8 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 10 * 服务端内部异常请使用 {@link cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants}(如 {@code DEVICE_SHADOW_NOT_ENABLED})。
11 11 *
12 12 * @author 芋道源码
... ... @@ -21,7 +21,6 @@ public enum IotDeviceShadowErrorCodeEnum {
21 21 REPORTED_MISSING(404, "缺少 reported"),
22 22 REPORTED_EMPTY(405, "reported 为空"),
23 23 INVALID_METHOD(406, "无效内层 method"),
24   - SHADOW_EMPTY(407, "影子内容为空"),
25 24 PROPERTY_COUNT_EXCEEDED(408, "影子属性数超过限制"),
26 25 VERSION_CONFLICT(409, "影子版本冲突"),
27 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 14 /**
15 15 * IoT 设备影子下行 Reply DTO
16 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 20 * @author 芋道源码
20 21 */
... ... @@ -29,19 +30,19 @@ public class IotDeviceShadowReplyDTO {
29 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 47 public static IotDeviceShadowReplyDTO getReplyOf(Map<String, Object> desired, Map<String, Object> reported,
47 48 Long version, Long timestamp) {
... ... @@ -50,45 +51,29 @@ public class IotDeviceShadowReplyDTO {
50 51 state.put(STATE_REPORTED, reported != null ? reported : new HashMap<>());
51 52 Map<String, Object> data = new HashMap<>();
52 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 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 44 */
45 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 31 public static final String STATE_REPORTED = "reported";
32 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 36 public static final String PAYLOAD_STATE = "state";
40 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 61 * 获取 desired 下某属性的更新时间戳
44 62 *
45 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 3 import cn.hutool.core.util.ArrayUtil;
4 4 import cn.hutool.core.util.StrUtil;
5 5 import cn.hutool.extra.spring.SpringUtil;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 8 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
8 9 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
9 10 import io.vertx.mqtt.messages.MqttPublishMessage;
10 11 import lombok.extern.slf4j.Slf4j;
11 12  
  13 +import java.util.Arrays;
  14 +import java.util.stream.Collectors;
  15 +
12 16 /**
13 17 * IoT 网关 EMQX 上行消息处理器
14 18 *
... ... @@ -44,13 +48,24 @@ public class IotEmqxUpstreamHandler {
44 48 }
45 49  
46 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 70 // 3. 发送消息到队列
56 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 17 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler;
18 18 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler;
19 19 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
20   -import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
21 20 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
22 21 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
23 22 import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
... ... @@ -102,10 +101,9 @@ public class IotMqttProtocol implements IotProtocol {
102 101 // 初始化 Handler
103 102 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
104 103 IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
105   - IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
106 104 this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
107 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 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 2  
3 3 import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.core.util.ArrayUtil;
5   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
6 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7   -import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
8 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
9 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 8 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
13 9 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
14 10 import io.vertx.mqtt.MqttEndpoint;
15 11 import lombok.extern.slf4j.Slf4j;
16 12  
17 13 import java.util.Arrays;
18   -import java.util.Map;
19 14 import java.util.stream.Collectors;
20 15  
21 16  
... ... @@ -28,14 +23,11 @@ import java.util.stream.Collectors;
28 23 public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
29 24  
30 25 private final String serverId;
31   - private final IotDeviceService deviceService;
32 26  
33 27 public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager,
34 28 IotDeviceMessageService deviceMessageService,
35   - IotDeviceService deviceService,
36 29 String serverId) {
37 30 super(connectionManager, deviceMessageService);
38   - this.deviceService = deviceService;
39 31 this.serverId = serverId;
40 32 }
41 33  
... ... @@ -75,17 +67,11 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
75 67 .collect(Collectors.joining("."));
76 68 IotDeviceMessage message;
77 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 72 log.warn("[handleBusinessRequest][影子透传 payload 解析失败,客户端 ID: {},主题: {}]", clientId, topic);
85 73 return;
86 74 }
87   - String requestId = (String) params.get("id");
88   - message = IotDeviceMessage.requestOf(requestId, topicMethod, params);
89 75 } else {
90 76 message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
91 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 3 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5  
  6 +import java.util.Map;
  7 +
6 8 /**
7 9 * IoT 设备消息序列化器接口
8 10 *
... ... @@ -35,4 +37,24 @@ public interface IotMessageSerializer {
35 37 */
36 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 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 7 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
8 8  
  9 +import java.util.Map;
  10 +
9 11 /**
10 12 * JSON 格式的消息序列化器
11 13 *
... ... @@ -34,4 +36,17 @@ public class IotJsonSerializer implements IotMessageSerializer {
34 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 62 void sendDeviceMessage(IotDeviceMessage message,
63 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 1 package cn.iocoder.yudao.module.iot.gateway.service.device.message;
2 2  
  3 +import cn.hutool.core.convert.Convert;
3 4 import cn.hutool.core.lang.Assert;
4 5 import cn.hutool.core.util.StrUtil;
5 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 11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
11 12 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
12 13 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
13   -import cn.iocoder.yudao.module.iot.gateway.serialize.IotShadowPassthroughCodec;
14 14 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
15 15 import jakarta.annotation.Resource;
16 16 import lombok.extern.slf4j.Slf4j;
17 17 import org.springframework.stereotype.Service;
18 18  
19 19 import java.time.LocalDateTime;
  20 +import java.util.Map;
  21 +import java.util.function.Function;
20 22  
21 23 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
22 24 import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS;
... ... @@ -108,6 +110,23 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
108 110 }
109 111  
110 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 130 public void sendDeviceMessage(IotDeviceMessage message,
112 131 String productKey, String deviceName, String serverId) {
113 132 // 1. 获取设备信息
... ... @@ -139,19 +158,59 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
139 158 }
140 159  
141 160 /**
142   - * 影子透传编码;非 JSON 设备返回 null,由下行发布方跳过发送
  161 + * 影子透传编码;不支持的 serializeType 返回 null,由下行发布方跳过发送
143 162 */
144 163 private byte[] encodePassthrough(IotDeviceMessage message, IotSerializeTypeEnum serializeType,
145 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 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 2  
3 3 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4 4 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
5 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 7 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
7 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 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 14 import org.junit.jupiter.api.Test;
11 15 import org.mockito.InjectMocks;
  16 +import org.mockito.Mock;
  17 +import org.mockito.Spy;
12 18  
13 19 import java.util.HashMap;
14 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 31 @InjectMocks
26 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 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 51 IotDeviceMessage message = IotDeviceMessage.requestOf(
35   - IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), payload);
  52 + IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), reply);
36 53  
37 54 byte[] bytes = deviceMessageService.serializeDeviceMessage(message, IotSerializeTypeEnum.JSON);
38 55  
39 56 @SuppressWarnings("unchecked")
40 57 Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class);
41 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 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 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 77 assertEquals(IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), message.getMethod());
  78 + assertEquals("req-1", message.getRequestId());
57 79 assertEquals("get", ((Map<?, ?>) message.getParams()).get("method"));
58 80 }
59 81  
... ... @@ -67,23 +89,16 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest {
67 89 @SuppressWarnings("unchecked")
68 90 Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class);
69 91 assertEquals("update_reply", result.get("method"));
  92 + assertEquals(0, ((Number) result.get("code")).intValue());
70 93 assertNull(result.get("id"));
71 94 }
72 95  
73 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 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 102 assertNotNull(decoded);
88 103 assertEquals("get", decoded.get("method"));
89 104 }
... ... @@ -96,10 +111,42 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest {
96 111 }
97 112  
98 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 }
... ...