Commit 8865481b2e9d2357998b09196510fc174d662299

Authored by haohao
1 parent 6e9b58cb

refactor(iot): 设备影子改回标准 IotDeviceMessage 封套,移除透传链路

- 上行/下行统一走 Serializer 标准封套,删除 passthrough 与 encodeRaw/decodeRaw
- 删除 IotDeviceShadowReplyDTO,回复改用 replyOf + ProtocolUtils.buildGetData/buildUpdateData
- 下行回复走 thing/shadow/update_reply,移除 SHADOW_GET
- 同步调整 biz/gateway 测试
Showing 18 changed files with 156 additions and 544 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/shadow/IotDeviceShadowServiceImpl.java
... ... @@ -8,7 +8,6 @@ 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 10 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
11   -import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO;
12 11 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowUpdateReqDTO;
13 12 import cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils;
14 13 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
... ... @@ -31,8 +30,10 @@ import java.util.HashMap;
31 30 import java.util.Map;
32 31 import java.util.function.Function;
33 32  
  33 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
34 34 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
35   -import static cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils.*;
  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;
36 37 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_NOT_ENABLED;
37 38 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_VERSION_CONFLICT;
38 39  
... ... @@ -88,29 +89,23 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
88 89 } catch (Exception ex) {
89 90 log.error("[handleDeviceShadowUpdate][device({}) message({}) 处理异常]", device.getId(), message, ex);
90 91 Long version = resolveShadowVersionSafely(device.getId());
91   - sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(
92   - extractInnerMethod(message.getParams()), version,
93   - IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getCode(),
94   - IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getMessage(), DateUtil.currentSeconds()));
  92 + sendShadowErrorReply(device, message.getServerId(), message.getRequestId(),
  93 + IotDeviceShadowErrorCodeEnum.SERVER_ERROR, version);
95 94 }
96 95 }
97 96  
98 97 private void handleDeviceShadowUpdate0(IotDeviceDO device, IotDeviceMessage message) {
99   - // 1. 解析请求(影子 body 当前按 JSON 建模,非 JSON 设备在 gateway 透传层已拒绝)
  98 + // 1. 解析请求(影子 params 按 JSON 建模,设备须配置 serializeType=JSON)
100 99 IotDeviceShadowUpdateReqDTO req = JsonUtils.convertObject(message.getParams(),
101 100 IotDeviceShadowUpdateReqDTO.class);
102 101 if (req == null) {
103   - sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(
104   - extractInnerMethod(message.getParams()), 0L,
105   - IotDeviceShadowErrorCodeEnum.JSON_FORMAT_ERROR.getCode(),
106   - IotDeviceShadowErrorCodeEnum.JSON_FORMAT_ERROR.getMessage(), DateUtil.currentSeconds()));
  102 + sendShadowErrorReply(device, message.getServerId(), message.getRequestId(),
  103 + IotDeviceShadowErrorCodeEnum.JSON_FORMAT_ERROR, 0L);
107 104 return;
108 105 }
109 106 if (StrUtil.isEmpty(req.getMethod())) {
110   - sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(
111   - INNER_METHOD_UPDATE, resolveShadowVersionSafely(device.getId()),
112   - IotDeviceShadowErrorCodeEnum.METHOD_MISSING.getCode(),
113   - IotDeviceShadowErrorCodeEnum.METHOD_MISSING.getMessage(), DateUtil.currentSeconds()));
  107 + sendShadowErrorReply(device, message.getServerId(), message.getRequestId(),
  108 + IotDeviceShadowErrorCodeEnum.METHOD_MISSING, resolveShadowVersionSafely(device.getId()));
114 109 return;
115 110 }
116 111  
... ... @@ -123,42 +118,35 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
123 118  
124 119 // 3. 内层 method 分发
125 120 if (INNER_METHOD_GET.equals(req.getMethod())) {
126   - sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.getReplyOf(
127   - shadow.getDesired(), shadow.getReported(), shadow.getVersion(), shadow.getShadowTime()));
  121 + sendShadowGetReply(device, message.getServerId(), message.getRequestId(),
  122 + shadow.getDesired(), shadow.getReported(), shadow.getVersion(), shadow.getShadowTime());
128 123 return;
129 124 }
130 125 if (INNER_METHOD_UPDATE.equals(req.getMethod())) {
131   - handleReportedUpdate(device, message.getServerId(), req);
  126 + handleReportedUpdate(device, message.getServerId(), message.getRequestId(), req);
132 127 return;
133 128 }
134   - sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf(
135   - req.getMethod(), shadow.getVersion(),
136   - IotDeviceShadowErrorCodeEnum.INVALID_METHOD.getCode(),
137   - IotDeviceShadowErrorCodeEnum.INVALID_METHOD.getMessage(), DateUtil.currentSeconds()));
  129 + sendShadowErrorReply(device, message.getServerId(), message.getRequestId(),
  130 + IotDeviceShadowErrorCodeEnum.INVALID_METHOD, shadow.getVersion());
138 131 }
139 132  
140   - private void handleReportedUpdate(IotDeviceDO device, String serverId, IotDeviceShadowUpdateReqDTO req) {
  133 + private void handleReportedUpdate(IotDeviceDO device, String serverId, String requestId,
  134 + IotDeviceShadowUpdateReqDTO req) {
141 135 Long deviceId = device.getId();
142 136  
143 137 if (req.getState() == null) {
144   - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
145   - INNER_METHOD_UPDATE, resolveShadowVersionSafely(deviceId),
146   - IotDeviceShadowErrorCodeEnum.STATE_MISSING.getCode(),
147   - IotDeviceShadowErrorCodeEnum.STATE_MISSING.getMessage(), DateUtil.currentSeconds()));
  138 + sendShadowErrorReply(device, serverId, requestId,
  139 + IotDeviceShadowErrorCodeEnum.STATE_MISSING, resolveShadowVersionSafely(deviceId));
148 140 return;
149 141 }
150 142 if (req.getState().getReported() == null) {
151   - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
152   - INNER_METHOD_UPDATE, resolveShadowVersionSafely(deviceId),
153   - IotDeviceShadowErrorCodeEnum.REPORTED_MISSING.getCode(),
154   - IotDeviceShadowErrorCodeEnum.REPORTED_MISSING.getMessage(), DateUtil.currentSeconds()));
  143 + sendShadowErrorReply(device, serverId, requestId,
  144 + IotDeviceShadowErrorCodeEnum.REPORTED_MISSING, resolveShadowVersionSafely(deviceId));
155 145 return;
156 146 }
157 147 if (CollUtil.isEmpty(req.getState().getReported())) {
158   - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
159   - INNER_METHOD_UPDATE, resolveShadowVersionSafely(deviceId),
160   - IotDeviceShadowErrorCodeEnum.REPORTED_EMPTY.getCode(),
161   - IotDeviceShadowErrorCodeEnum.REPORTED_EMPTY.getMessage(), DateUtil.currentSeconds()));
  148 + sendShadowErrorReply(device, serverId, requestId,
  149 + IotDeviceShadowErrorCodeEnum.REPORTED_EMPTY, resolveShadowVersionSafely(deviceId));
162 150 return;
163 151 }
164 152  
... ... @@ -166,24 +154,20 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
166 154 if (req.getVersion() != null && req.getVersion() == -1L) {
167 155 try {
168 156 resetShadowWithRetry(deviceId);
169   - sendShadowReplySafely(device, serverId,
170   - IotDeviceShadowReplyDTO.updateReplyOf(0L, DateUtil.currentSeconds()));
  157 + sendShadowUpdateReply(device, serverId, requestId, 0L, DateUtil.currentSeconds());
171 158 } catch (ShadowCasExhaustedException ex) {
172   - handleCasExhaustedForDevice(device, serverId, ex);
  159 + handleCasExhaustedForDevice(device, serverId, requestId, ex);
173 160 }
174 161 return;
175 162 }
176 163  
177 164 try {
178 165 IotDeviceShadowDO committed = casUpdateShadow(deviceId, latest -> buildReportedUpdate(latest, req));
179   - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.updateReplyOf(committed.getVersion(),
180   - committed.getShadowTime()));
  166 + sendShadowUpdateReply(device, serverId, requestId, committed.getVersion(), committed.getShadowTime());
181 167 } catch (ShadowUpdateRejectedException ex) {
182   - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
183   - INNER_METHOD_UPDATE, ex.getVersion(),
184   - ex.getErrorCode().getCode(), ex.getErrorCode().getMessage(), DateUtil.currentSeconds()));
  168 + sendShadowErrorReply(device, serverId, requestId, ex.getErrorCode(), ex.getVersion());
185 169 } catch (ShadowCasExhaustedException ex) {
186   - handleCasExhaustedForDevice(device, serverId, ex);
  170 + handleCasExhaustedForDevice(device, serverId, requestId, ex);
187 171 }
188 172 }
189 173  
... ... @@ -203,9 +187,9 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
203 187 // 设备在线则主动推送全量影子(复用 CAS 返回值,避免二次查库)
204 188 String serverId = devicePropertyService.getDeviceServerId(deviceId);
205 189 if (StrUtil.isNotEmpty(serverId)) {
206   - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.getReplyOf(
  190 + sendShadowGetReply(device, serverId, null,
207 191 committed.getDesired(), committed.getReported(),
208   - committed.getVersion(), committed.getShadowTime()));
  192 + committed.getVersion(), committed.getShadowTime());
209 193 }
210 194 } catch (ShadowCasExhaustedException ex) {
211 195 throw exception(DEVICE_SHADOW_VERSION_CONFLICT);
... ... @@ -337,7 +321,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
337 321 }
338 322  
339 323 /**
340   - * 将 CAS 成功写入的局部字段与 latest 快照合并,供全量 get_reply 使用
  324 + * 将 CAS 成功写入的局部字段与 latest 快照合并,供全量 get 回复使用
341 325 */
342 326 private static void enrichCommittedShadow(IotDeviceShadowDO committed, IotDeviceShadowDO latest) {
343 327 if (committed.getDesired() == null) {
... ... @@ -351,22 +335,19 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
351 335 }
352 336 }
353 337  
354   - private void handleCasExhaustedForDevice(IotDeviceDO device, String serverId, ShadowCasExhaustedException ex) {
  338 + private void handleCasExhaustedForDevice(IotDeviceDO device, String serverId, String requestId,
  339 + ShadowCasExhaustedException ex) {
355 340 if (ex.isShadowNotFound()) {
356   - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
357   - INNER_METHOD_UPDATE, 0L,
358   - IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getCode(),
359   - IotDeviceShadowErrorCodeEnum.SERVER_ERROR.getMessage(), DateUtil.currentSeconds()));
  341 + sendShadowErrorReply(device, serverId, requestId,
  342 + IotDeviceShadowErrorCodeEnum.SERVER_ERROR, 0L);
360 343 return;
361 344 }
362   - sendVersionConflictReply(device, serverId, ex.getLastKnownVersion());
  345 + sendVersionConflictReply(device, serverId, requestId, ex.getLastKnownVersion());
363 346 }
364 347  
365   - private void sendVersionConflictReply(IotDeviceDO device, String serverId, Long version) {
366   - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.errorOf(
367   - INNER_METHOD_UPDATE, version,
368   - IotDeviceShadowErrorCodeEnum.VERSION_CONFLICT.getCode(),
369   - IotDeviceShadowErrorCodeEnum.VERSION_CONFLICT.getMessage(), DateUtil.currentSeconds()));
  348 + private void sendVersionConflictReply(IotDeviceDO device, String serverId, String requestId, Long version) {
  349 + sendShadowErrorReply(device, serverId, requestId,
  350 + IotDeviceShadowErrorCodeEnum.VERSION_CONFLICT, version);
370 351 }
371 352  
372 353 private Long resolveShadowVersionSafely(Long deviceId) {
... ... @@ -407,15 +388,41 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
407 388 return shadow;
408 389 }
409 390  
  391 + private void sendShadowGetReply(IotDeviceDO device, String serverId, String requestId,
  392 + Map<String, Object> desired, Map<String, Object> reported, Long version, Long timestamp) {
  393 + sendShadowReplySafely(device, serverId, requestId, SUCCESS.getCode(), SUCCESS.getMsg(),
  394 + IotDeviceShadowProtocolUtils.buildGetData(desired, reported, version, timestamp));
  395 + }
  396 +
  397 + private void sendShadowUpdateReply(IotDeviceDO device, String serverId, String requestId,
  398 + Long version, Long timestamp) {
  399 + sendShadowReplySafely(device, serverId, requestId, SUCCESS.getCode(), SUCCESS.getMsg(),
  400 + IotDeviceShadowProtocolUtils.buildUpdateData(version, timestamp));
  401 + }
  402 +
  403 + /**
  404 + * 错误回复:data 仍带 version 供设备 resync(有意偏离纯 CommonResult 的 error-data-null 约定)
  405 + */
  406 + private void sendShadowErrorReply(IotDeviceDO device, String serverId, String requestId,
  407 + IotDeviceShadowErrorCodeEnum errorCode, Long version) {
  408 + sendShadowReplySafely(device, serverId, requestId, errorCode.getCode(), errorCode.getMessage(),
  409 + IotDeviceShadowProtocolUtils.buildUpdateData(version, DateUtil.currentSeconds()));
  410 + }
  411 +
410 412 /**
411   - * 下发影子回复,内部吞掉异常,保证上行处理链路不向上抛
  413 + * 下发影子回复(标准 IotDeviceMessage 封套),内部吞掉异常,保证上行处理链路不向上抛
  414 + * <p>
  415 + * 回复 method 取上行的 {@link IotDeviceMessageMethodEnum#SHADOW_UPDATE}(与框架"回复用原请求 method"一致):
  416 + * 带 code 的回复经 IotDeviceMessageUtils#isUpstreamMessage 取反 SHADOW_UPDATE.upstream(true) → 判定为下行,
  417 + * 正确投递到设备;下行 Topic 为 thing/shadow/update_reply。
412 418 */
413   - private void sendShadowReplySafely(IotDeviceDO device, String serverId, IotDeviceShadowReplyDTO replyDTO) {
414   - IotDeviceMessage shadowGetMsg = IotDeviceMessage.requestOf(
415   - device.getId(), device.getTenantId(), serverId,
416   - IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), replyDTO);
  419 + private void sendShadowReplySafely(IotDeviceDO device, String serverId, String requestId,
  420 + Integer code, String msg, Map<String, Object> data) {
  421 + IotDeviceMessage shadowReplyMsg = IotDeviceMessage.replyOf(
  422 + requestId, IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), data, code, msg);
  423 + shadowReplyMsg.setDeviceId(device.getId()).setTenantId(device.getTenantId()).setServerId(serverId);
417 424 try {
418   - deviceMessageService.sendDeviceMessage(shadowGetMsg, device, serverId);
  425 + deviceMessageService.sendDeviceMessage(shadowReplyMsg, device, serverId);
419 426 } catch (Exception ex) {
420 427 log.error("[sendShadowReply][device({}) 下发影子回复失败]", device.getId(), ex);
421 428 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/shadow/IotDeviceShadowServiceImplTest.java
... ... @@ -5,7 +5,6 @@ import cn.iocoder.yudao.framework.common.exception.ServiceException;
5 5 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
6 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
7 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8   -import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO;
9 8 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
10 9 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceShadowDO;
11 10 import cn.iocoder.yudao.module.iot.dal.mysql.device.IotDeviceShadowMapper;
... ... @@ -76,15 +75,18 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
76 75  
77 76 Map<String, Object> params = new HashMap<>();
78 77 params.put("method", "get");
79   - IotDeviceMessage message = IotDeviceMessage.requestOf(device.getId(), 1L, "server-1",
  78 + IotDeviceMessage message = IotDeviceMessage.requestOf("req-get-1",
80 79 IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), params);
  80 + message.setDeviceId(device.getId()).setTenantId(1L).setServerId("server-1");
81 81  
82 82 service.handleDeviceShadowUpdate(device, message);
83 83  
84 84 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
85 85 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
86   - IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
87   - assertEquals("get_reply", reply.getMethod());
  86 + IotDeviceMessage reply = captor.getValue();
  87 + assertEquals(IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), reply.getMethod());
  88 + assertEquals("req-get-1", reply.getRequestId());
  89 + assertEquals(0, reply.getCode());
88 90 assertEquals(2L, getReplyVersion(reply));
89 91 }
90 92  
... ... @@ -141,7 +143,8 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
141 143  
142 144 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
143 145 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
144   - IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
  146 + IotDeviceMessage reply = captor.getValue();
  147 + assertEquals(IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), reply.getMethod());
145 148 assertEquals(409, reply.getCode());
146 149 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
147 150 }
... ... @@ -192,8 +195,8 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
192 195  
193 196 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
194 197 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
195   - IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
196   - assertEquals("update_reply", reply.getMethod());
  198 + IotDeviceMessage reply = captor.getValue();
  199 + assertEquals(IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), reply.getMethod());
197 200 assertEquals(409, reply.getCode());
198 201 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
199 202 }
... ... @@ -262,8 +265,8 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
262 265  
263 266 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
264 267 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
265   - IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
266   - assertEquals("get_reply", reply.getMethod());
  268 + IotDeviceMessage reply = captor.getValue();
  269 + assertEquals(IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), reply.getMethod());
267 270 assertEquals(2L, getReplyVersion(reply));
268 271 // getOrCreateShadow + CAS 各一次 SELECT,推送不再额外查库
269 272 verify(deviceShadowMapper, times(2)).selectByDeviceId(deviceId);
... ... @@ -299,7 +302,8 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
299 302  
300 303 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
301 304 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
302   - IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
  305 + IotDeviceMessage reply = captor.getValue();
  306 + assertEquals(IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), reply.getMethod());
303 307 assertEquals(400, reply.getCode());
304 308 }
305 309  
... ... @@ -319,7 +323,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
319 323  
320 324 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
321 325 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
322   - IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
  326 + IotDeviceMessage reply = captor.getValue();
323 327 assertEquals(401, reply.getCode());
324 328 }
325 329  
... ... @@ -375,7 +379,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
375 379  
376 380 ArgumentCaptor<IotDeviceMessage> replyCaptor = ArgumentCaptor.forClass(IotDeviceMessage.class);
377 381 verify(deviceMessageService).sendDeviceMessage(replyCaptor.capture(), eq(device), eq("server-1"));
378   - IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) replyCaptor.getValue().getParams();
  382 + IotDeviceMessage reply = replyCaptor.getValue();
379 383 assertEquals(3L, getReplyVersion(reply));
380 384 }
381 385  
... ... @@ -439,7 +443,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
439 443  
440 444 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
441 445 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
442   - IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
  446 + IotDeviceMessage reply = captor.getValue();
443 447 assertEquals(0L, getReplyVersion(reply));
444 448 assertEquals(500, reply.getCode());
445 449 }
... ... @@ -466,7 +470,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
466 470  
467 471 ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
468 472 verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
469   - IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
  473 + IotDeviceMessage reply = captor.getValue();
470 474 assertEquals(500, reply.getCode());
471 475 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
472 476 }
... ... @@ -501,7 +505,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
501 505 }
502 506  
503 507 @SuppressWarnings("unchecked")
504   - private static Long getReplyVersion(IotDeviceShadowReplyDTO reply) {
  508 + private static Long getReplyVersion(IotDeviceMessage reply) {
505 509 if (!(reply.getData() instanceof Map)) {
506 510 return null;
507 511 }
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/IotDeviceMessageMethodEnum.java
... ... @@ -70,7 +70,6 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
70 70 // ========== 设备影子 ==========
71 71  
72 72 SHADOW_UPDATE("thing.shadow.update", "设备影子更新", true),
73   - SHADOW_GET("thing.shadow.get", "设备影子获取", false),
74 73  
75 74 ;
76 75  
... ... @@ -83,15 +82,7 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
83 82 public static final Set<String> REPLY_DISABLED = SetUtils.asSet(
84 83 STATE_UPDATE.getMethod(),
85 84 OTA_PROGRESS.getMethod(), // 参考阿里云,OTA 升级进度上报,不进行回复
86   - SHADOW_UPDATE.getMethod() // 设备影子由影子 Service 主动下发 thing.shadow.get
87   - );
88   -
89   - /**
90   - * 透传方法集合:设备侧使用原始线格式,不套 IotDeviceMessage 封套
91   - */
92   - public static final Set<String> PASSTHROUGH_METHODS = SetUtils.asSet(
93   - SHADOW_UPDATE.getMethod(),
94   - SHADOW_GET.getMethod()
  85 + SHADOW_UPDATE.getMethod() // 设备影子由影子 Service 主动下发回复(thing/shadow/update_reply)
95 86 );
96 87  
97 88 private final String method;
... ... @@ -114,8 +105,4 @@ public enum IotDeviceMessageMethodEnum implements ArrayValuable&lt;String&gt; {
114 105 return REPLY_DISABLED.contains(method);
115 106 }
116 107  
117   - public static boolean isPassthrough(String method) {
118   - return PASSTHROUGH_METHODS.contains(method);
119   - }
120   -
121 108 }
... ...
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.code/msg},不走
  9 + * 设备侧 wire code,仅用于回填影子下行回复(thing.shadow.update_reply)的 code/msg,不走
10 10 * {@link cn.iocoder.yudao.framework.common.pojo.CommonResult}。
11 11 * 服务端内部异常请使用 {@link cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants}(如
12 12 * {@code DEVICE_SHADOW_NOT_ENABLED})。
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/shadow/IotDeviceShadowReplyDTO.java deleted
1   -package cn.iocoder.yudao.module.iot.core.topic.shadow;
2   -
3   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
4   -import lombok.AllArgsConstructor;
5   -import lombok.Data;
6   -import lombok.NoArgsConstructor;
7   -
8   -import java.util.HashMap;
9   -import java.util.Map;
10   -
11   -import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
12   -import static cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils.*;
13   -
14   -/**
15   - * IoT 设备影子下行 Reply DTO
16   - * <p>
17   - * 扁平对齐 CommonResult + method,用于 {@link IotDeviceMessageMethodEnum#SHADOW_GET}
18   - * 透传线 body。
19   - * 错误回复的 {@link #data} 仍携带 version 供设备 resync,有意偏离纯 CommonResult(error 时 data
20   - * 通常为 null)。
21   - *
22   - * @author 芋道源码
23   - */
24   -@Data
25   -@NoArgsConstructor
26   -@AllArgsConstructor
27   -public class IotDeviceShadowReplyDTO {
28   -
29   - /**
30   - * 内层方法:get_reply / update_reply
31   - */
32   - private String method;
33   -
34   - /**
35   - * 结果码,0 表示成功
36   - */
37   - private Integer code;
38   -
39   - /**
40   - * 结果描述
41   - */
42   - private String msg;
43   -
44   - /**
45   - * 业务数据:get_reply 为 {state, version, timestamp};update_reply 为 {version,
46   - * timestamp}
47   - */
48   - private Object data;
49   -
50   - public static IotDeviceShadowReplyDTO getReplyOf(Map<String, Object> desired, Map<String, Object> reported,
51   - Long version, Long timestamp) {
52   - Map<String, Object> state = new HashMap<>();
53   - state.put(STATE_DESIRED, desired != null ? desired : new HashMap<>());
54   - state.put(STATE_REPORTED, reported != null ? reported : new HashMap<>());
55   - Map<String, Object> data = new HashMap<>();
56   - data.put(PAYLOAD_STATE, state);
57   - data.put(PAYLOAD_VERSION, version);
58   - data.put(METADATA_TIMESTAMP, timestamp);
59   - return of(INNER_METHOD_GET_REPLY, SUCCESS.getCode(), SUCCESS.getMsg(), data);
60   - }
61   -
62   - public static IotDeviceShadowReplyDTO updateReplyOf(Long version, Long timestamp) {
63   - return of(INNER_METHOD_UPDATE_REPLY, SUCCESS.getCode(), SUCCESS.getMsg(),
64   - dataWithVersionTimestamp(version, timestamp));
65   - }
66   -
67   - public static IotDeviceShadowReplyDTO errorOf(String innerMethod, Long version, Integer errorCode,
68   - String errorMsg, Long timestamp) {
69   - return of(toReplyMethod(innerMethod), errorCode, errorMsg, dataWithVersionTimestamp(version, timestamp));
70   - }
71   -
72   - private static IotDeviceShadowReplyDTO of(String method, Integer code, String msg, Map<String, Object> data) {
73   - return new IotDeviceShadowReplyDTO().setMethod(method).setCode(code).setMsg(msg).setData(data);
74   - }
75   -
76   - private static Map<String, Object> dataWithVersionTimestamp(Long version, Long timestamp) {
77   - Map<String, Object> data = new HashMap<>();
78   - data.put(PAYLOAD_VERSION, version);
79   - data.put(METADATA_TIMESTAMP, timestamp);
80   - return data;
81   - }
82   -
83   -}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/shadow/IotDeviceShadowUpdateReqDTO.java
... ... @@ -10,7 +10,7 @@ import java.util.Map;
10 10 /**
11 11 * IoT 设备影子上行 Request DTO
12 12 * <p>
13   - * 用于 {@link IotDeviceMessageMethodEnum#SHADOW_UPDATE} 消息的 params 参数(透传原始线 body)
  13 + * 用于 {@link IotDeviceMessageMethodEnum#SHADOW_UPDATE} 标准封套的 params 参数
14 14 *
15 15 * @author 芋道源码
16 16 */
... ...
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceShadowProtocolUtils.java
... ... @@ -9,7 +9,7 @@ import java.util.Set;
9 9 /**
10 10 * IoT 设备影子协议工具
11 11 * <p>
12   - * 内层 method、state 键名、payload 字段等与设备侧透传 JSON 对齐;并提供 metadata 读写。
  12 + * 内层 method、state 键名、reply data 字段等与设备侧标准封套 params/data 对齐;并提供 metadata 读写。
13 13 *
14 14 * @author 芋道源码
15 15 */
... ... @@ -18,12 +18,10 @@ public final class IotDeviceShadowProtocolUtils {
18 18 private IotDeviceShadowProtocolUtils() {
19 19 }
20 20  
21   - // ========== 内层 method ==========
  21 + // ========== 内层 method(上行 params.method) ==========
22 22  
23 23 public static final String INNER_METHOD_GET = "get";
24 24 public static final String INNER_METHOD_UPDATE = "update";
25   - public static final String INNER_METHOD_GET_REPLY = "get_reply";
26   - public static final String INNER_METHOD_UPDATE_REPLY = "update_reply";
27 25  
28 26 // ========== state / metadata 键 ==========
29 27  
... ... @@ -37,24 +35,28 @@ public final class IotDeviceShadowProtocolUtils {
37 35 public static final String PAYLOAD_VERSION = "version";
38 36  
39 37 /**
40   - * 由内层 method 推导 reply method(get → get_reply,其余 → update_reply)
  38 + * 构建 get 响应 data:{ state:{desired,reported}, version, timestamp }
41 39 */
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;
  40 + public static Map<String, Object> buildGetData(Map<String, Object> desired, Map<String, Object> reported,
  41 + Long version, Long timestamp) {
  42 + Map<String, Object> state = new HashMap<>();
  43 + state.put(STATE_DESIRED, desired != null ? desired : new HashMap<>());
  44 + state.put(STATE_REPORTED, reported != null ? reported : new HashMap<>());
  45 + Map<String, Object> data = new HashMap<>();
  46 + data.put(PAYLOAD_STATE, state);
  47 + data.put(PAYLOAD_VERSION, version);
  48 + data.put(METADATA_TIMESTAMP, timestamp);
  49 + return data;
47 50 }
48 51  
49 52 /**
50   - * 从透传 params 提取内层 method;无法解析时默认 update
  53 + * 构建 update 响应 / 错误 data:{ version, timestamp }
51 54 */
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;
  55 + public static Map<String, Object> buildUpdateData(Long version, Long timestamp) {
  56 + Map<String, Object> data = new HashMap<>();
  57 + data.put(PAYLOAD_VERSION, version);
  58 + data.put(METADATA_TIMESTAMP, timestamp);
  59 + return data;
58 60 }
59 61  
60 62 /**
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java
... ... @@ -55,11 +55,6 @@ public class IotEmqxDownstreamHandler {
55 55 // 2.2 构建载荷
56 56 byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(),
57 57 deviceInfo.getDeviceName());
58   - if (payload == null) {
59   - log.warn("[handle][影子下行跳过,设备 ID: {},method: {},非 JSON 协议设备不支持影子透传]",
60   - message.getDeviceId(), message.getMethod());
61   - return;
62   - }
63 58  
64 59 // 3. 发布消息
65 60 protocol.publishMessage(topic, payload);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/upstream/IotEmqxUpstreamHandler.java
... ... @@ -3,16 +3,12 @@ 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;
7 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
9 8 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
10 9 import io.vertx.mqtt.messages.MqttPublishMessage;
11 10 import lombok.extern.slf4j.Slf4j;
12 11  
13   -import java.util.Arrays;
14   -import java.util.stream.Collectors;
15   -
16 12 /**
17 13 * IoT 网关 EMQX 上行消息处理器
18 14 *
... ... @@ -47,25 +43,14 @@ public class IotEmqxUpstreamHandler {
47 43 return;
48 44 }
49 45  
50   - // 2.1 反序列化消息
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);
  46 + // 2.1 反序列化消息(标准 IotDeviceMessage 封套,method 来自 body)
  47 + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
  48 + if (message == null) {
  49 + log.warn("[handle][topic({}) payload({}) 消息解码失败]", topic, new String(payload));
  50 + return;
68 51 }
  52 + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)
  53 + IotMqttTopicUtils.normalizeReplyMethod(message);
69 54  
70 55 // 3. 发送消息到队列
71 56 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java
... ... @@ -45,11 +45,6 @@ public class IotMqttDownstreamHandler {
45 45 // 2.1 序列化消息
46 46 byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(),
47 47 connectionInfo.getDeviceName());
48   - if (payload == null) {
49   - log.warn("[handle][影子下行跳过,设备 ID: {},method: {},非 JSON 协议设备不支持影子透传]",
50   - message.getDeviceId(), message.getMethod());
51   - return;
52   - }
53 48 Assert.isTrue(payload.length > 0, "消息编码结果不能为空");
54 49 // 2.2 构建主题
55 50 Assert.notBlank(message.getMethod(), "消息方法不能为空");
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java
... ... @@ -72,11 +72,6 @@ public abstract class IotMqttAbstractHandler {
72 72 try {
73 73 // 1.1 序列化消息(根据设备配置的序列化类型)
74 74 byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, productKey, deviceName);
75   - if (encodedData == null) {
76   - // 影子透传在非 JSON 设备上 encode 返回 null,跳过发布避免 NPE
77   - log.warn("[writeResponse][影子下行跳过,客户端 ID: {},method: {}]", endpoint.clientIdentifier(), method);
78   - return;
79   - }
80 75 // 1.2 构建响应主题
81 76 String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true);
82 77  
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
... ... @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
2 2  
3 3 import cn.hutool.core.lang.Assert;
4 4 import cn.hutool.core.util.ArrayUtil;
5   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 6 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
8 7 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
... ... @@ -10,10 +9,6 @@ import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
10 9 import io.vertx.mqtt.MqttEndpoint;
11 10 import lombok.extern.slf4j.Slf4j;
12 11  
13   -import java.util.Arrays;
14   -import java.util.stream.Collectors;
15   -
16   -
17 12 /**
18 13 * IoT 网关 MQTT 上行消息处理器:处理业务消息(属性上报、事件上报等)
19 14 *
... ... @@ -62,25 +57,14 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
62 57 return;
63 58 }
64 59  
65   - // 2.1 反序列化消息
66   - String topicMethod = Arrays.stream(topicParts, 4, topicParts.length)
67   - .collect(Collectors.joining("."));
68   - IotDeviceMessage message;
69   - if (IotDeviceMessageMethodEnum.isPassthrough(topicMethod)) {
70   - message = deviceMessageService.deserializePassthroughMessage(payload, topicMethod, productKey, deviceName);
71   - if (message == null) {
72   - log.warn("[handleBusinessRequest][影子透传 payload 解析失败,客户端 ID: {},主题: {}]", clientId, topic);
73   - return;
74   - }
75   - } else {
76   - message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
77   - if (message == null) {
78   - log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
79   - return;
80   - }
81   - // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)
82   - IotMqttTopicUtils.normalizeReplyMethod(message);
  60 + // 2.1 反序列化消息(标准 IotDeviceMessage 封套,method 来自 body)
  61 + IotDeviceMessage message = deviceMessageService.deserializeDeviceMessage(payload, productKey, deviceName);
  62 + if (message == null) {
  63 + log.warn("[handleBusinessRequest][消息解码失败,客户端 ID: {},主题: {}]", clientId, topic);
  64 + return;
83 65 }
  66 + // 2.2 标准化回复消息的 method(MQTT 协议中,设备回复消息的 method 会携带 _reply 后缀)
  67 + IotMqttTopicUtils.normalizeReplyMethod(message);
84 68  
85 69 // 3. 处理业务消息
86 70 deviceMessageService.sendDeviceMessage(message, productKey, deviceName, serverId);
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotMessageSerializer.java
... ... @@ -3,8 +3,6 @@ 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   -
8 6 /**
9 7 * IoT 设备消息序列化器接口
10 8 *
... ... @@ -37,24 +35,4 @@ public interface IotMessageSerializer {
37 35 */
38 36 IotSerializeTypeEnum getType();
39 37  
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   -
60 38 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/json/IotJsonSerializer.java
... ... @@ -6,8 +6,6 @@ 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   -
11 9 /**
12 10 * JSON 格式的消息序列化器
13 11 *
... ... @@ -36,17 +34,4 @@ public class IotJsonSerializer implements IotMessageSerializer {
36 34 return message;
37 35 }
38 36  
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   -
52 37 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java
... ... @@ -62,16 +62,4 @@ 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   -
77 65 }
... ...
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;
4 3 import cn.hutool.core.lang.Assert;
5 4 import cn.hutool.core.util.StrUtil;
6 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
7   -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 6 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
9 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
10 8 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
... ... @@ -17,8 +15,6 @@ import lombok.extern.slf4j.Slf4j;
17 15 import org.springframework.stereotype.Service;
18 16  
19 17 import java.time.LocalDateTime;
20   -import java.util.Map;
21   -import java.util.function.Function;
22 18  
23 19 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
24 20 import static cn.iocoder.yudao.module.iot.gateway.enums.ErrorCodeConstants.DEVICE_NOT_EXISTS;
... ... @@ -44,14 +40,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
44 40 @Override
45 41 public byte[] serializeDeviceMessage(IotDeviceMessage message,
46 42 String productKey, String deviceName) {
47   - if (IotDeviceMessageMethodEnum.isPassthrough(message.getMethod())) {
48   - IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
49   - if (device == null) {
50   - throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
51   - }
52   - IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType());
53   - return encodePassthrough(message, serializeType, productKey, deviceName);
54   - }
55 43 // 1.1 获取设备信息
56 44 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
57 45 if (device == null) {
... ... @@ -68,9 +56,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
68 56 @Override
69 57 public byte[] serializeDeviceMessage(IotDeviceMessage message,
70 58 IotSerializeTypeEnum serializeType) {
71   - if (IotDeviceMessageMethodEnum.isPassthrough(message.getMethod())) {
72   - return encodePassthrough(message, serializeType, null, null);
73   - }
74 59 // 1. 获取序列化器
75 60 IotMessageSerializer serializer = messageSerializerManager.get(serializeType);
76 61 if (serializer == null) {
... ... @@ -110,23 +95,6 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
110 95 }
111 96  
112 97 @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
130 98 public void sendDeviceMessage(IotDeviceMessage message,
131 99 String productKey, String deviceName, String serverId) {
132 100 // 1. 获取设备信息
... ... @@ -157,60 +125,4 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
157 125 }
158 126 }
159 127  
160   - /**
161   - * 影子透传编码;不支持的 serializeType 返回 null,由下行发布方跳过发送
162   - */
163   - private byte[] encodePassthrough(IotDeviceMessage message, IotSerializeTypeEnum serializeType,
164   - String productKey, String deviceName) {
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;
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) {
208   - if (productKey != null && deviceName != null) {
209   - log.warn("[{}][影子透传跳过:设备({}/{}) serializeType={} 不支持,目前仅 JSON 协议设备可用]",
210   - op, productKey, deviceName, serializeType);
211   - } else {
212   - log.warn("[{}][影子透传跳过:serializeType={} 不支持,目前仅 JSON 协议设备可用]", op, serializeType);
213   - }
214   - }
215   -
216 128 }
... ...
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceShadowPassthroughTest.java deleted
1   -package cn.iocoder.yudao.module.iot.gateway.service.device.message;
2   -
3   -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4   -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
5   -import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
6   -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;
9   -import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO;
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;
14   -import org.junit.jupiter.api.Test;
15   -import org.mockito.InjectMocks;
16   -import org.mockito.Mock;
17   -import org.mockito.Spy;
18   -
19   -import java.util.HashMap;
20   -import java.util.Map;
21   -
22   -import static org.junit.jupiter.api.Assertions.*;
23   -import static org.mockito.ArgumentMatchers.eq;
24   -import static org.mockito.Mockito.when;
25   -
26   -/**
27   - * 设备影子网关透传序列化单元测试
28   - */
29   -public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest {
30   -
31   - @InjectMocks
32   - private IotDeviceMessageServiceImpl deviceMessageService;
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   -
48   - @Test
49   - public void testSerializeDeviceMessage_passthrough_flatReply() {
50   - IotDeviceShadowReplyDTO reply = IotDeviceShadowReplyDTO.getReplyOf(new HashMap<>(), new HashMap<>(), 2L, 1469564576L);
51   - IotDeviceMessage message = IotDeviceMessage.requestOf(
52   - IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), reply);
53   -
54   - byte[] bytes = deviceMessageService.serializeDeviceMessage(message, IotSerializeTypeEnum.JSON);
55   -
56   - @SuppressWarnings("unchecked")
57   - Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class);
58   - assertEquals("get_reply", result.get("method"));
59   - assertEquals(0, ((Number) result.get("code")).intValue());
60   - assertNull(result.get("payload"));
61   - assertNull(result.get("id"));
62   - @SuppressWarnings("unchecked")
63   - Map<String, Object> data = (Map<String, Object>) result.get("data");
64   - assertEquals(2, ((Number) data.get("version")).intValue());
65   - }
66   -
67   - @Test
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");
75   -
76   - assertNotNull(message);
77   - assertEquals(IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), message.getMethod());
78   - assertEquals("req-1", message.getRequestId());
79   - assertEquals("get", ((Map<?, ?>) message.getParams()).get("method"));
80   - }
81   -
82   - @Test
83   - public void testSerializeUpdateReply_passthrough() {
84   - IotDeviceShadowReplyDTO reply = IotDeviceShadowReplyDTO.updateReplyOf(3L, 1469564576L);
85   - IotDeviceMessage message = IotDeviceMessage.requestOf(
86   - IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), reply);
87   -
88   - byte[] bytes = deviceMessageService.serializeDeviceMessage(message, IotSerializeTypeEnum.JSON);
89   - @SuppressWarnings("unchecked")
90   - Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class);
91   - assertEquals("update_reply", result.get("method"));
92   - assertEquals(0, ((Number) result.get("code")).intValue());
93   - assertNull(result.get("id"));
94   - }
95   -
96   - @Test
97   - public void testJsonSerializer_encodeRawDecodeRaw() {
98   - IotJsonSerializer serializer = new IotJsonSerializer();
99   - Map<String, Object> params = Map.of("method", "get");
100   - byte[] bytes = serializer.encodeRaw(params);
101   - Map<String, Object> decoded = serializer.decodeRaw(bytes);
102   - assertNotNull(decoded);
103   - assertEquals("get", decoded.get("method"));
104   - }
105   -
106   - @Test
107   - public void testSerializeDeviceMessage_passthrough_binaryReturnsNull() {
108   - IotDeviceMessage message = IotDeviceMessage.requestOf(
109   - IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), Map.of("method", "get_reply"));
110   - assertNull(deviceMessageService.serializeDeviceMessage(message, IotSerializeTypeEnum.BINARY));
111   - }
112   -
113   - @Test
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());
150   - }
151   -
152   -}
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/util/IotMqttTopicUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.gateway.util;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import static org.junit.jupiter.api.Assertions.assertEquals;
  7 +
  8 +/**
  9 + * {@link IotMqttTopicUtils} 单元测试
  10 + */
  11 +class IotMqttTopicUtilsTest {
  12 +
  13 + private static final String PRODUCT_KEY = "pk";
  14 + private static final String DEVICE_NAME = "dn";
  15 +
  16 + @Test
  17 + void testBuildTopicByMethod_shadowUpdateReplyAppendsReplySuffix() {
  18 + String topic = IotMqttTopicUtils.buildTopicByMethod(
  19 + IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), PRODUCT_KEY, DEVICE_NAME, true);
  20 + assertEquals("/sys/pk/dn/thing/shadow/update_reply", topic);
  21 + }
  22 +
  23 + @Test
  24 + void testBuildTopicByMethod_serviceInvokeReplyAppendsReplySuffix() {
  25 + String topic = IotMqttTopicUtils.buildTopicByMethod(
  26 + IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), PRODUCT_KEY, DEVICE_NAME, true);
  27 + assertEquals("/sys/pk/dn/thing/service/invoke_reply", topic);
  28 + }
  29 +
  30 +}
... ...