Commit acdeb6e5c8f032e1c6312c42020963359506ebea

Authored by haohao
1 parent 84c46ad6

refactor(iot): 移除 IoT 设备影子表及相关 SQL,重构影子协议工具类

- 删除 `iot_device_shadow` 表的 SQL 定义及相关菜单权限插入语句。
- 重构 `IotDeviceShadowController`,使用新的 `IotDeviceShadowProtocolUtils` 处理影子状态的时间戳。
- 更新 Redis DAO 和服务实现,确保影子数据的缓存逻辑与新工具类兼容。
- 增加影子透传编解码逻辑,支持 JSON 格式的设备透传。
- 补充单元测试,确保新逻辑的正确性与稳定性。
Showing 18 changed files with 318 additions and 152 deletions
sql/mysql/iot_device_shadow.sql deleted
1 --- IoT 设备影子表  
2 -CREATE TABLE IF NOT EXISTS `iot_device_shadow`  
3 -(  
4 - `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',  
5 - `device_id` bigint NOT NULL COMMENT '设备编号',  
6 - `product_id` bigint NOT NULL COMMENT '产品编号',  
7 - `product_key` varchar(64) NOT NULL COMMENT '产品标识',  
8 - `device_name` varchar(255) NOT NULL COMMENT '设备名称',  
9 - `reported` json DEFAULT NULL COMMENT '上报状态',  
10 - `desired` json DEFAULT NULL COMMENT '期望状态',  
11 - `metadata` json DEFAULT NULL COMMENT '元数据',  
12 - `version` bigint NOT NULL DEFAULT 0 COMMENT '版本号',  
13 - `shadow_time` bigint NOT NULL DEFAULT 0 COMMENT '文档最后更新时间',  
14 - `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态',  
15 - `creator` varchar(64) DEFAULT '' COMMENT '创建者',  
16 - `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',  
17 - `updater` varchar(64) DEFAULT '' COMMENT '更新者',  
18 - `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',  
19 - `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',  
20 - `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',  
21 - PRIMARY KEY (`id`),  
22 - UNIQUE KEY `uk_device_id` (`device_id`)  
23 -) ENGINE = InnoDB  
24 - DEFAULT CHARSET = utf8mb4 COMMENT ='IoT 设备影子';  
25 -  
26 --- 菜单权限  
27 -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`,  
28 - `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`,  
29 - `updater`, `update_time`, `deleted`)  
30 -VALUES (4056, '设备影子查询', 'iot:device:shadow-query', 3, 13, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(),  
31 - '1', NOW(), b'0');  
32 -INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`,  
33 - `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`,  
34 - `updater`, `update_time`, `deleted`)  
35 -VALUES (4057, '设备影子更新', 'iot:device:shadow-update', 3, 14, 4008, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(),  
36 - '1', NOW(), b'0');  
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/IotDeviceShadowController.java
@@ -3,11 +3,12 @@ package cn.iocoder.yudao.module.iot.controller.admin.device; @@ -3,11 +3,12 @@ package cn.iocoder.yudao.module.iot.controller.admin.device;
3 import cn.hutool.core.lang.Assert; 3 import cn.hutool.core.lang.Assert;
4 import cn.hutool.core.map.MapUtil; 4 import cn.hutool.core.map.MapUtil;
5 import cn.iocoder.yudao.framework.common.pojo.CommonResult; 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
  6 +import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
6 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.shadow.IotDeviceShadowPropertyRespVO; 7 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.shadow.IotDeviceShadowPropertyRespVO;
7 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.shadow.IotDeviceShadowRespVO; 8 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.shadow.IotDeviceShadowRespVO;
8 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.shadow.IotDeviceShadowUpdateDesiredReqVO; 9 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.shadow.IotDeviceShadowUpdateDesiredReqVO;
9 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.shadow.IotDeviceShadowUpdateStatusReqVO; 10 import cn.iocoder.yudao.module.iot.controller.admin.device.vo.shadow.IotDeviceShadowUpdateStatusReqVO;
10 -import cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowMetadatas; 11 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils;
11 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; 12 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
12 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceShadowDO; 13 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceShadowDO;
13 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; 14 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
@@ -57,29 +58,26 @@ public class IotDeviceShadowController { @@ -57,29 +58,26 @@ public class IotDeviceShadowController {
57 Map<String, Object> desired = shadow.getDesired() != null ? shadow.getDesired() : MapUtil.empty(); 58 Map<String, Object> desired = shadow.getDesired() != null ? shadow.getDesired() : MapUtil.empty();
58 Map<String, Object> reported = shadow.getReported() != null ? shadow.getReported() : MapUtil.empty(); 59 Map<String, Object> reported = shadow.getReported() != null ? shadow.getReported() : MapUtil.empty();
59 Map<String, Object> metadata = shadow.getMetadata() != null ? shadow.getMetadata() : MapUtil.empty(); 60 Map<String, Object> metadata = shadow.getMetadata() != null ? shadow.getMetadata() : MapUtil.empty();
  61 + // 拼接 VO
60 List<IotDeviceShadowPropertyRespVO> properties = convertList(thingModels, thingModel -> { 62 List<IotDeviceShadowPropertyRespVO> properties = convertList(thingModels, thingModel -> {
61 ThingModelProperty prop = thingModel.getProperty(); 63 ThingModelProperty prop = thingModel.getProperty();
62 Assert.notNull(prop, "属性不能为空"); 64 Assert.notNull(prop, "属性不能为空");
63 return new IotDeviceShadowPropertyRespVO() 65 return new IotDeviceShadowPropertyRespVO()
64 - .setIdentifier(thingModel.getIdentifier())  
65 - .setName(thingModel.getName())  
66 - .setDataType(prop.getDataType())  
67 - .setDataSpecs(prop.getDataSpecs())  
68 - .setDataSpecsList(prop.getDataSpecsList()) 66 + .setIdentifier(thingModel.getIdentifier()).setName(thingModel.getName())
  67 + .setDataType(prop.getDataType()).setDataSpecs(prop.getDataSpecs()).setDataSpecsList(prop.getDataSpecsList())
69 .setDesiredValue(desired.get(thingModel.getIdentifier())) 68 .setDesiredValue(desired.get(thingModel.getIdentifier()))
70 .setReportedValue(reported.get(thingModel.getIdentifier())) 69 .setReportedValue(reported.get(thingModel.getIdentifier()))
71 - .setDesiredTime(IotDeviceShadowMetadatas.getDesiredTimestamp(metadata, thingModel.getIdentifier()))  
72 - .setReportedTime(IotDeviceShadowMetadatas.getReportedTimestamp(metadata, thingModel.getIdentifier())); 70 + .setDesiredTime(IotDeviceShadowProtocolUtils.getDesiredTimestamp(metadata, thingModel.getIdentifier()))
  71 + .setReportedTime(IotDeviceShadowProtocolUtils.getReportedTimestamp(metadata, thingModel.getIdentifier()));
73 }); 72 });
74 73
75 - return success(new IotDeviceShadowRespVO()  
76 - .setVersion(shadow.getVersion())  
77 - .setShadowTime(shadow.getShadowTime())  
78 - .setStatus(shadow.getStatus())  
79 - .setProperties(properties)  
80 - .setDesired(desired)  
81 - .setReported(reported)  
82 - .setMetadata(metadata)); 74 + IotDeviceShadowRespVO respVO = BeanUtils.toBean(shadow, IotDeviceShadowRespVO.class);
  75 + // toBean 会拷贝 DO 中同名标量;Map 字段须用下方已归一化的局部变量覆盖,避免 null 透传前端
  76 + respVO.setProperties(properties);
  77 + respVO.setDesired(desired);
  78 + respVO.setReported(reported);
  79 + respVO.setMetadata(metadata);
  80 + return success(respVO);
83 } 81 }
84 82
85 @PutMapping("/update-desired") 83 @PutMapping("/update-desired")
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java
@@ -21,6 +21,7 @@ public interface RedisKeyConstants { @@ -21,6 +21,7 @@ public interface RedisKeyConstants {
21 /** 21 /**
22 * 设备影子数据缓存,采用 STRING 结构 22 * 设备影子数据缓存,采用 STRING 结构
23 * <p> 23 * <p>
  24 + * 影子缓存:DB 为准,Redis 仅读缓存(cache-aside),写后失效。
24 * KEY 格式:device_shadow:{deviceId} 25 * KEY 格式:device_shadow:{deviceId}
25 * VALUE 数据类型:String(JSON) {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceShadowDO} 26 * VALUE 数据类型:String(JSON) {@link cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceShadowDO}
26 */ 27 */
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/device/DeviceShadowRedisDAO.java
@@ -10,6 +10,10 @@ import static cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants.DEVICE_SHA @@ -10,6 +10,10 @@ import static cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants.DEVICE_SHA
10 10
11 /** 11 /**
12 * {@link IotDeviceShadowDO} 的 Redis DAO 12 * {@link IotDeviceShadowDO} 的 Redis DAO
  13 + * <p>
  14 + * 影子缓存:DB 为准,Redis 仅作读缓存(cache-aside),写后失效。
  15 + *
  16 + * @author 芋道源码
13 */ 17 */
14 @Repository 18 @Repository
15 public class DeviceShadowRedisDAO { 19 public class DeviceShadowRedisDAO {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/shadow/IotDeviceShadowServiceImpl.java
@@ -7,7 +7,7 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; @@ -7,7 +7,7 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 7 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
8 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 8 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
9 import cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowErrorCodeEnum; 9 import cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowErrorCodeEnum;
10 -import cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowMetadatas; 10 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils;
11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 11 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
12 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO; 12 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO;
13 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowUpdateReqDTO; 13 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowUpdateReqDTO;
@@ -32,8 +32,8 @@ import java.util.Map; @@ -32,8 +32,8 @@ import java.util.Map;
32 import java.util.function.Function; 32 import java.util.function.Function;
33 33
34 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 34 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
35 -import static cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowProtocolConstants.INNER_METHOD_GET;  
36 -import static cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowProtocolConstants.INNER_METHOD_UPDATE; 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;
37 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_NOT_ENABLED; 37 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_NOT_ENABLED;
38 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_VERSION_CONFLICT; 38 import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_SHADOW_VERSION_CONFLICT;
39 39
@@ -54,17 +54,19 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -54,17 +54,19 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
54 private IotDeviceShadowMapper deviceShadowMapper; 54 private IotDeviceShadowMapper deviceShadowMapper;
55 @Resource 55 @Resource
56 private DeviceShadowRedisDAO deviceShadowRedisDAO; 56 private DeviceShadowRedisDAO deviceShadowRedisDAO;
  57 +
57 @Resource 58 @Resource
58 private IotDeviceService deviceService; 59 private IotDeviceService deviceService;
59 @Resource 60 @Resource
60 private IotDevicePropertyService devicePropertyService; 61 private IotDevicePropertyService devicePropertyService;
  62 + // 解决 IotDeviceShadowServiceImpl 与 IotDeviceMessageService 循环依赖
61 @Resource 63 @Resource
62 @Lazy 64 @Lazy
63 private IotDeviceMessageService deviceMessageService; 65 private IotDeviceMessageService deviceMessageService;
64 66
65 @Override 67 @Override
66 public IotDeviceShadowDO getOrCreateShadow(IotDeviceDO device) { 68 public IotDeviceShadowDO getOrCreateShadow(IotDeviceDO device) {
67 - IotDeviceShadowDO shadow = getShadowFromCacheOrDb(device.getId()); 69 + IotDeviceShadowDO shadow = getDeviceShadowFromCacheOrDb(device.getId());
68 if (shadow != null) { 70 if (shadow != null) {
69 return shadow; 71 return shadow;
70 } 72 }
@@ -204,7 +206,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -204,7 +206,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
204 @Override 206 @Override
205 public void resetShadow(Long deviceId) { 207 public void resetShadow(Long deviceId) {
206 deviceService.validateDeviceExists(deviceId); 208 deviceService.validateDeviceExists(deviceId);
207 - IotDeviceShadowDO shadow = getShadowFromCacheOrDb(deviceId); 209 + IotDeviceShadowDO shadow = getDeviceShadowFromCacheOrDb(deviceId);
208 if (shadow == null) { 210 if (shadow == null) {
209 return; 211 return;
210 } 212 }
@@ -219,7 +221,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -219,7 +221,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
219 @Transactional(rollbackFor = Exception.class) 221 @Transactional(rollbackFor = Exception.class)
220 public void updateStatus(Long deviceId, Integer status) { 222 public void updateStatus(Long deviceId, Integer status) {
221 IotDeviceDO device = deviceService.validateDeviceExists(deviceId); 223 IotDeviceDO device = deviceService.validateDeviceExists(deviceId);
222 - IotDeviceShadowDO shadow = getShadowFromCacheOrDb(deviceId); 224 + IotDeviceShadowDO shadow = getDeviceShadowFromCacheOrDb(deviceId);
223 if (shadow == null) { 225 if (shadow == null) {
224 // 影子行不存在时直接按目标 status 懒建,避免后续 getOrCreateShadow 始终以 ENABLE 插入 226 // 影子行不存在时直接按目标 status 懒建,避免后续 getOrCreateShadow 始终以 ENABLE 插入
225 IotDeviceShadowDO newShadow = buildEmptyShadow(device).status(status).build(); 227 IotDeviceShadowDO newShadow = buildEmptyShadow(device).status(status).build();
@@ -251,7 +253,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -251,7 +253,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
251 // ========== 内部方法 ========== 253 // ========== 内部方法 ==========
252 254
253 private IotDeviceShadowDO buildReportedUpdate(IotDeviceShadowDO latest, IotDeviceShadowUpdateReqDTO req) { 255 private IotDeviceShadowDO buildReportedUpdate(IotDeviceShadowDO latest, IotDeviceShadowUpdateReqDTO req) {
254 - if (req.getVersion() != null && req.getVersion() < latest.getVersion()) { 256 + // 乐观锁:带 version 时必须与当前完全一致(null=强制更新、-1=清空已在上层处理)
  257 + if (req.getVersion() != null && !req.getVersion().equals(latest.getVersion())) {
255 throw new ShadowUpdateRejectedException(latest.getVersion(), IotDeviceShadowErrorCodeEnum.VERSION_CONFLICT); 258 throw new ShadowUpdateRejectedException(latest.getVersion(), IotDeviceShadowErrorCodeEnum.VERSION_CONFLICT);
256 } 259 }
257 Map<String, Object> mergedReported = mergeMap(latest.getReported(), req.getState().getReported()); 260 Map<String, Object> mergedReported = mergeMap(latest.getReported(), req.getState().getReported());
@@ -260,7 +263,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -260,7 +263,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
260 } 263 }
261 long now = DateUtil.currentSeconds(); 264 long now = DateUtil.currentSeconds();
262 Map<String, Object> metadata = latest.getMetadata() != null ? new HashMap<>(latest.getMetadata()) : new HashMap<>(); 265 Map<String, Object> metadata = latest.getMetadata() != null ? new HashMap<>(latest.getMetadata()) : new HashMap<>();
263 - IotDeviceShadowMetadatas.putReportedTimestamps(metadata, req.getState().getReported().keySet(), now); 266 + IotDeviceShadowProtocolUtils.putReportedTimestamps(metadata, req.getState().getReported().keySet(), now);
264 return IotDeviceShadowDO.builder() 267 return IotDeviceShadowDO.builder()
265 .reported(mergedReported) 268 .reported(mergedReported)
266 .metadata(metadata) 269 .metadata(metadata)
@@ -273,7 +276,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -273,7 +276,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
273 long now = DateUtil.currentSeconds(); 276 long now = DateUtil.currentSeconds();
274 Map<String, Object> mergedDesired = mergeMap(latest.getDesired(), desired); 277 Map<String, Object> mergedDesired = mergeMap(latest.getDesired(), desired);
275 Map<String, Object> metadata = latest.getMetadata() != null ? new HashMap<>(latest.getMetadata()) : new HashMap<>(); 278 Map<String, Object> metadata = latest.getMetadata() != null ? new HashMap<>(latest.getMetadata()) : new HashMap<>();
276 - IotDeviceShadowMetadatas.putDesiredTimestamps(metadata, desired.keySet(), now); 279 + IotDeviceShadowProtocolUtils.putDesiredTimestamps(metadata, desired.keySet(), now);
277 return IotDeviceShadowDO.builder() 280 return IotDeviceShadowDO.builder()
278 .desired(mergedDesired) 281 .desired(mergedDesired)
279 .metadata(metadata) 282 .metadata(metadata)
@@ -302,7 +305,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -302,7 +305,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
302 for (int i = 0; i < OPTIMISTIC_LOCK_RETRY; i++) { 305 for (int i = 0; i < OPTIMISTIC_LOCK_RETRY; i++) {
303 // 首次尝试走缓存,重试时直读 DB 获取最新 version 306 // 首次尝试走缓存,重试时直读 DB 获取最新 version
304 IotDeviceShadowDO latest = i == 0 307 IotDeviceShadowDO latest = i == 0
305 - ? getShadowFromCacheOrDb(deviceId) 308 + ? getDeviceShadowFromCacheOrDb(deviceId)
306 : deviceShadowMapper.selectByDeviceId(deviceId); 309 : deviceShadowMapper.selectByDeviceId(deviceId);
307 if (latest == null) { 310 if (latest == null) {
308 throw ShadowCasExhaustedException.shadowNotFound(); 311 throw ShadowCasExhaustedException.shadowNotFound();
@@ -311,6 +314,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -311,6 +314,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
311 IotDeviceShadowDO update = updateBuilder.apply(latest); 314 IotDeviceShadowDO update = updateBuilder.apply(latest);
312 int rows = deviceShadowMapper.updateByDeviceIdAndVersion(update, deviceId, latest.getVersion()); 315 int rows = deviceShadowMapper.updateByDeviceIdAndVersion(update, deviceId, latest.getVersion());
313 if (rows > 0) { 316 if (rows > 0) {
  317 + // cache-aside:写后失效 Redis 读缓存
314 deviceShadowRedisDAO.delete(deviceId); 318 deviceShadowRedisDAO.delete(deviceId);
315 enrichCommittedShadow(update, latest); 319 enrichCommittedShadow(update, latest);
316 return update; 320 return update;
@@ -352,7 +356,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -352,7 +356,7 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
352 356
353 private Long resolveShadowVersionSafely(Long deviceId) { 357 private Long resolveShadowVersionSafely(Long deviceId) {
354 try { 358 try {
355 - IotDeviceShadowDO shadow = getShadowFromCacheOrDb(deviceId); 359 + IotDeviceShadowDO shadow = getDeviceShadowFromCacheOrDb(deviceId);
356 return shadow != null ? shadow.getVersion() : 0L; 360 return shadow != null ? shadow.getVersion() : 0L;
357 } catch (Exception ex) { 361 } catch (Exception ex) {
358 log.warn("[resolveShadowVersionSafely][device({}) 读取 version 失败,使用保底 0]", deviceId, ex); 362 log.warn("[resolveShadowVersionSafely][device({}) 读取 version 失败,使用保底 0]", deviceId, ex);
@@ -373,7 +377,10 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { @@ -373,7 +377,10 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService {
373 .shadowTime(DateUtil.currentSeconds()); 377 .shadowTime(DateUtil.currentSeconds());
374 } 378 }
375 379
376 - private IotDeviceShadowDO getShadowFromCacheOrDb(Long deviceId) { 380 + /**
  381 + * cache-aside 读路径:Redis 命中则返回,否则查 DB 并回填缓存
  382 + */
  383 + private IotDeviceShadowDO getDeviceShadowFromCacheOrDb(Long deviceId) {
377 IotDeviceShadowDO shadow = deviceShadowRedisDAO.get(deviceId); 384 IotDeviceShadowDO shadow = deviceShadowRedisDAO.get(deviceId);
378 if (shadow != null) { 385 if (shadow != null) {
379 return shadow; 386 return shadow;
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/shadow/IotDeviceShadowServiceImplTest.java
@@ -54,6 +54,20 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -54,6 +54,20 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
54 } 54 }
55 55
56 @Test 56 @Test
  57 + public void testGetOrCreateShadow_whenNotExists_insertNewRow() {
  58 + // 影子不存在时懒插入;并发下 DuplicateKeyException 由 getOrCreateShadow 兜底
  59 + IotDeviceDO device = buildDevice();
  60 + when(deviceShadowRedisDAO.get(device.getId())).thenReturn(null);
  61 + when(deviceShadowMapper.selectByDeviceId(device.getId())).thenReturn(null);
  62 +
  63 + service.getOrCreateShadow(device);
  64 +
  65 + ArgumentCaptor<IotDeviceShadowDO> captor = ArgumentCaptor.forClass(IotDeviceShadowDO.class);
  66 + verify(deviceShadowMapper).insert(captor.capture());
  67 + assertEquals(device.getId(), captor.getValue().getDeviceId());
  68 + }
  69 +
  70 + @Test
57 public void testHandleDeviceShadowUpdate_get_replyFullShadow() { 71 public void testHandleDeviceShadowUpdate_get_replyFullShadow() {
58 IotDeviceDO device = buildDevice(); 72 IotDeviceDO device = buildDevice();
59 IotDeviceShadowDO shadow = buildShadow(device, 2L); 73 IotDeviceShadowDO shadow = buildShadow(device, 2L);
@@ -106,6 +120,59 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -106,6 +120,59 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
106 } 120 }
107 121
108 @Test 122 @Test
  123 + public void testHandleDeviceShadowUpdate_versionGreaterThanLatest_send409() {
  124 + IotDeviceDO device = buildDevice();
  125 + IotDeviceShadowDO shadow = buildShadow(device, 1L);
  126 + when(deviceShadowRedisDAO.get(device.getId())).thenReturn(null);
  127 + when(deviceShadowMapper.selectByDeviceId(device.getId())).thenReturn(shadow);
  128 +
  129 + Map<String, Object> reported = new HashMap<>();
  130 + reported.put("Temperature", 25);
  131 + Map<String, Object> state = new HashMap<>();
  132 + state.put("reported", reported);
  133 + Map<String, Object> params = new HashMap<>();
  134 + params.put("method", "update");
  135 + params.put("state", state);
  136 + params.put("version", 3L);
  137 + IotDeviceMessage message = IotDeviceMessage.requestOf(device.getId(), 1L, "server-1",
  138 + IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), params);
  139 +
  140 + service.handleDeviceShadowUpdate(device, message);
  141 +
  142 + ArgumentCaptor<IotDeviceMessage> captor = ArgumentCaptor.forClass(IotDeviceMessage.class);
  143 + verify(deviceMessageService).sendDeviceMessage(captor.capture(), eq(device), eq("server-1"));
  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"));
  148 + verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
  149 + }
  150 +
  151 + @Test
  152 + public void testHandleDeviceShadowUpdate_versionNull_forceUpdateWithoutVersionCheck() {
  153 + IotDeviceDO device = buildDevice();
  154 + IotDeviceShadowDO shadow = buildShadow(device, 5L);
  155 + when(deviceShadowRedisDAO.get(device.getId())).thenReturn(null);
  156 + when(deviceShadowMapper.selectByDeviceId(device.getId())).thenReturn(shadow);
  157 + when(deviceShadowMapper.updateByDeviceIdAndVersion(any(), eq(device.getId()), eq(5L))).thenReturn(1);
  158 +
  159 + Map<String, Object> reported = new HashMap<>();
  160 + reported.put("Temperature", 25);
  161 + Map<String, Object> state = new HashMap<>();
  162 + state.put("reported", reported);
  163 + Map<String, Object> params = new HashMap<>();
  164 + params.put("method", "update");
  165 + params.put("state", state);
  166 + // 不传 version → 强制更新,跳过版本精确匹配
  167 + IotDeviceMessage message = IotDeviceMessage.requestOf(device.getId(), 1L, "server-1",
  168 + IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), params);
  169 +
  170 + service.handleDeviceShadowUpdate(device, message);
  171 +
  172 + verify(deviceShadowMapper).updateByDeviceIdAndVersion(any(), eq(device.getId()), eq(5L));
  173 + }
  174 +
  175 + @Test
109 public void testHandleDeviceShadowUpdate_versionConflict_send409() { 176 public void testHandleDeviceShadowUpdate_versionConflict_send409() {
110 IotDeviceDO device = buildDevice(); 177 IotDeviceDO device = buildDevice();
111 IotDeviceShadowDO shadow = buildShadow(device, 5L); 178 IotDeviceShadowDO shadow = buildShadow(device, 5L);
@@ -131,7 +198,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -131,7 +198,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
131 assertEquals("update_reply", reply.getMethod()); 198 assertEquals("update_reply", reply.getMethod());
132 @SuppressWarnings("unchecked") 199 @SuppressWarnings("unchecked")
133 Map<String, Object> payload = (Map<String, Object>) reply.getPayload(); 200 Map<String, Object> payload = (Map<String, Object>) reply.getPayload();
134 - assertEquals("error", payload.get("status")); 201 + assertEquals(409, payload.get("code"));
135 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any()); 202 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
136 } 203 }
137 204
@@ -244,9 +311,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -244,9 +311,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
244 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams(); 311 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
245 @SuppressWarnings("unchecked") 312 @SuppressWarnings("unchecked")
246 Map<String, Object> payload = (Map<String, Object>) reply.getPayload(); 313 Map<String, Object> payload = (Map<String, Object>) reply.getPayload();
247 - @SuppressWarnings("unchecked")  
248 - Map<String, Object> content = (Map<String, Object>) payload.get("content");  
249 - assertEquals(401, content.get("errorcode")); 314 + assertEquals(401, payload.get("code"));
250 } 315 }
251 316
252 @Test 317 @Test
@@ -283,7 +348,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -283,7 +348,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
283 Map<String, Object> params = new HashMap<>(); 348 Map<String, Object> params = new HashMap<>();
284 params.put("method", "update"); 349 params.put("method", "update");
285 params.put("state", state); 350 params.put("state", state);
286 - params.put("version", 2L); 351 + // version=null:强制更新,CAS 重试时不受请求 version 约束
287 IotDeviceMessage message = IotDeviceMessage.requestOf(device.getId(), 1L, "server-1", 352 IotDeviceMessage message = IotDeviceMessage.requestOf(device.getId(), 1L, "server-1",
288 IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), params); 353 IotDeviceMessageMethodEnum.SHADOW_UPDATE.getMethod(), params);
289 354
@@ -367,9 +432,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -367,9 +432,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
367 assertEquals(0L, reply.getVersion()); 432 assertEquals(0L, reply.getVersion());
368 @SuppressWarnings("unchecked") 433 @SuppressWarnings("unchecked")
369 Map<String, Object> payload = (Map<String, Object>) reply.getPayload(); 434 Map<String, Object> payload = (Map<String, Object>) reply.getPayload();
370 - @SuppressWarnings("unchecked")  
371 - Map<String, Object> content = (Map<String, Object>) payload.get("content");  
372 - assertEquals(500, content.get("errorcode")); 435 + assertEquals(500, payload.get("code"));
373 } 436 }
374 437
375 @Test 438 @Test
@@ -397,9 +460,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { @@ -397,9 +460,7 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest {
397 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams(); 460 IotDeviceShadowReplyDTO reply = (IotDeviceShadowReplyDTO) captor.getValue().getParams();
398 @SuppressWarnings("unchecked") 461 @SuppressWarnings("unchecked")
399 Map<String, Object> payload = (Map<String, Object>) reply.getPayload(); 462 Map<String, Object> payload = (Map<String, Object>) reply.getPayload();
400 - @SuppressWarnings("unchecked")  
401 - Map<String, Object> content = (Map<String, Object>) payload.get("content");  
402 - assertEquals(500, content.get("errorcode")); 463 + assertEquals(500, payload.get("code"));
403 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any()); 464 verify(deviceShadowMapper, never()).updateByDeviceIdAndVersion(any(), any(), any());
404 } 465 }
405 466
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/shadow/IotDeviceShadowErrorCodeEnum.java
@@ -6,7 +6,8 @@ import lombok.Getter; @@ -6,7 +6,8 @@ import lombok.Getter;
6 /** 6 /**
7 * IoT 设备影子设备侧错误码枚举 7 * IoT 设备影子设备侧错误码枚举
8 * <p> 8 * <p>
9 - * 用于回填 update_reply.payload.content,不走 CommonResult 9 + * 设备侧 wire code,仅用于回填 {@code update_reply.payload.code/msg},不走 {@link cn.iocoder.yudao.framework.common.pojo.CommonResult}。
  10 + * 服务端内部异常请使用 {@link cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants}(如 {@code DEVICE_SHADOW_NOT_ENABLED})。
10 * 11 *
11 * @author 芋道源码 12 * @author 芋道源码
12 */ 13 */
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/shadow/IotDeviceShadowProtocolConstants.java deleted
1 -package cn.iocoder.yudao.module.iot.core.enums.shadow;  
2 -  
3 -/**  
4 - * IoT 设备影子协议常量  
5 - * <p>  
6 - * 内层 method、state 键名等与设备侧透传 JSON 对齐  
7 - *  
8 - * @author 芋道源码  
9 - */  
10 -public final class IotDeviceShadowProtocolConstants {  
11 -  
12 - private IotDeviceShadowProtocolConstants() {  
13 - }  
14 -  
15 - // ========== 内层 method ==========  
16 -  
17 - public static final String INNER_METHOD_GET = "get";  
18 - public static final String INNER_METHOD_UPDATE = "update";  
19 - public static final String INNER_METHOD_GET_REPLY = "get_reply";  
20 - public static final String INNER_METHOD_UPDATE_REPLY = "update_reply";  
21 -  
22 - // ========== state / metadata 键 ==========  
23 -  
24 - public static final String STATE_DESIRED = "desired";  
25 - public static final String STATE_REPORTED = "reported";  
26 - public static final String METADATA_TIMESTAMP = "timestamp";  
27 -  
28 - // ========== payload 键 ==========  
29 -  
30 - public static final String PAYLOAD_STATUS = "status";  
31 - public static final String PAYLOAD_STATE = "state";  
32 - public static final String PAYLOAD_VERSION = "version";  
33 - public static final String PAYLOAD_CONTENT = "content";  
34 - public static final String PAYLOAD_ERROR_CODE = "errorcode";  
35 - public static final String PAYLOAD_ERROR_MESSAGE = "errormessage";  
36 -  
37 - // ========== payload status 值 ==========  
38 -  
39 - public static final String STATUS_SUCCESS = "success";  
40 - public static final String STATUS_ERROR = "error";  
41 -  
42 -}  
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/shadow/IotDeviceShadowReplyDTO.java
@@ -8,7 +8,8 @@ import lombok.NoArgsConstructor; @@ -8,7 +8,8 @@ import lombok.NoArgsConstructor;
8 import java.util.HashMap; 8 import java.util.HashMap;
9 import java.util.Map; 9 import java.util.Map;
10 10
11 -import static cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowProtocolConstants.*; 11 +import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS;
  12 +import static cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils.*;
12 13
13 /** 14 /**
14 * IoT 设备影子下行 Reply DTO 15 * IoT 设备影子下行 Reply DTO
@@ -28,7 +29,7 @@ public class IotDeviceShadowReplyDTO { @@ -28,7 +29,7 @@ public class IotDeviceShadowReplyDTO {
28 private String method; 29 private String method;
29 30
30 /** 31 /**
31 - * 回复载荷 32 + * 回复载荷(对齐 CommonResult:code / msg / data)
32 */ 33 */
33 private Object payload; 34 private Object payload;
34 35
@@ -47,9 +48,9 @@ public class IotDeviceShadowReplyDTO { @@ -47,9 +48,9 @@ public class IotDeviceShadowReplyDTO {
47 Map<String, Object> state = new HashMap<>(); 48 Map<String, Object> state = new HashMap<>();
48 state.put(STATE_DESIRED, desired != null ? desired : new HashMap<>()); 49 state.put(STATE_DESIRED, desired != null ? desired : new HashMap<>());
49 state.put(STATE_REPORTED, reported != null ? reported : new HashMap<>()); 50 state.put(STATE_REPORTED, reported != null ? reported : new HashMap<>());
50 - Map<String, Object> payload = new HashMap<>();  
51 - payload.put(PAYLOAD_STATUS, STATUS_SUCCESS);  
52 - payload.put(PAYLOAD_STATE, state); 51 + Map<String, Object> data = new HashMap<>();
  52 + data.put(PAYLOAD_STATE, state);
  53 + Map<String, Object> payload = buildSuccessPayload(data);
53 return new IotDeviceShadowReplyDTO() 54 return new IotDeviceShadowReplyDTO()
54 .setMethod(INNER_METHOD_GET_REPLY) 55 .setMethod(INNER_METHOD_GET_REPLY)
55 .setPayload(payload) 56 .setPayload(payload)
@@ -58,9 +59,9 @@ public class IotDeviceShadowReplyDTO { @@ -58,9 +59,9 @@ public class IotDeviceShadowReplyDTO {
58 } 59 }
59 60
60 public static IotDeviceShadowReplyDTO updateReplyOf(Long version, Long timestamp) { 61 public static IotDeviceShadowReplyDTO updateReplyOf(Long version, Long timestamp) {
61 - Map<String, Object> payload = new HashMap<>();  
62 - payload.put(PAYLOAD_STATUS, STATUS_SUCCESS);  
63 - payload.put(PAYLOAD_VERSION, version); 62 + Map<String, Object> data = new HashMap<>();
  63 + data.put(PAYLOAD_VERSION, version);
  64 + Map<String, Object> payload = buildSuccessPayload(data);
64 return new IotDeviceShadowReplyDTO() 65 return new IotDeviceShadowReplyDTO()
65 .setMethod(INNER_METHOD_UPDATE_REPLY) 66 .setMethod(INNER_METHOD_UPDATE_REPLY)
66 .setPayload(payload) 67 .setPayload(payload)
@@ -68,14 +69,13 @@ public class IotDeviceShadowReplyDTO { @@ -68,14 +69,13 @@ public class IotDeviceShadowReplyDTO {
68 .setTimestamp(timestamp); 69 .setTimestamp(timestamp);
69 } 70 }
70 71
71 - public static IotDeviceShadowReplyDTO errorOf(Long version, Integer errorcode, String errormessage, Long timestamp) {  
72 - Map<String, Object> content = new HashMap<>();  
73 - content.put(PAYLOAD_ERROR_CODE, errorcode);  
74 - content.put(PAYLOAD_ERROR_MESSAGE, errormessage); 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<>(); 75 Map<String, Object> payload = new HashMap<>();
76 - payload.put(PAYLOAD_STATUS, STATUS_ERROR);  
77 - payload.put(PAYLOAD_VERSION, version);  
78 - payload.put(PAYLOAD_CONTENT, content); 76 + payload.put(PAYLOAD_CODE, errorCode);
  77 + payload.put(PAYLOAD_MSG, errorMsg);
  78 + payload.put(PAYLOAD_DATA, data);
79 return new IotDeviceShadowReplyDTO() 79 return new IotDeviceShadowReplyDTO()
80 .setMethod(INNER_METHOD_UPDATE_REPLY) 80 .setMethod(INNER_METHOD_UPDATE_REPLY)
81 .setPayload(payload) 81 .setPayload(payload)
@@ -83,4 +83,12 @@ public class IotDeviceShadowReplyDTO { @@ -83,4 +83,12 @@ public class IotDeviceShadowReplyDTO {
83 .setTimestamp(timestamp); 83 .setTimestamp(timestamp);
84 } 84 }
85 85
  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;
  92 + }
  93 +
86 } 94 }
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/shadow/IotDeviceShadowMetadatas.java renamed to yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceShadowProtocolUtils.java
1 -package cn.iocoder.yudao.module.iot.core.enums.shadow; 1 +package cn.iocoder.yudao.module.iot.core.util;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 4
@@ -6,20 +6,39 @@ import java.util.HashMap; @@ -6,20 +6,39 @@ import java.util.HashMap;
6 import java.util.Map; 6 import java.util.Map;
7 import java.util.Set; 7 import java.util.Set;
8 8
9 -import static cn.iocoder.yudao.module.iot.core.enums.shadow.IotDeviceShadowProtocolConstants.*;  
10 -  
11 /** 9 /**
12 - * IoT 设备影子 metadata 读写工具 10 + * IoT 设备影子协议工具
13 * <p> 11 * <p>
14 - * 结构:{ desired: { identifier: { timestamp } }, reported: { ... } } 12 + * 内层 method、state 键名、payload 字段等与设备侧透传 JSON 对齐;并提供 metadata 读写。
15 * 13 *
16 * @author 芋道源码 14 * @author 芋道源码
17 */ 15 */
18 -public final class IotDeviceShadowMetadatas { 16 +public final class IotDeviceShadowProtocolUtils {
19 17
20 - private IotDeviceShadowMetadatas() { 18 + private IotDeviceShadowProtocolUtils() {
21 } 19 }
22 20
  21 + // ========== 内层 method ==========
  22 +
  23 + public static final String INNER_METHOD_GET = "get";
  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 +
  28 + // ========== state / metadata 键 ==========
  29 +
  30 + public static final String STATE_DESIRED = "desired";
  31 + public static final String STATE_REPORTED = "reported";
  32 + public static final String METADATA_TIMESTAMP = "timestamp";
  33 +
  34 + // ========== payload 键(对齐 CommonResult:code / msg / data)==========
  35 +
  36 + public static final String PAYLOAD_CODE = "code";
  37 + public static final String PAYLOAD_MSG = "msg";
  38 + public static final String PAYLOAD_DATA = "data";
  39 + public static final String PAYLOAD_STATE = "state";
  40 + public static final String PAYLOAD_VERSION = "version";
  41 +
23 /** 42 /**
24 * 获取 desired 下某属性的更新时间戳 43 * 获取 desired 下某属性的更新时间戳
25 * 44 *
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/emqx/handler/downstream/IotEmqxDownstreamHandler.java
@@ -55,6 +55,11 @@ public class IotEmqxDownstreamHandler { @@ -55,6 +55,11 @@ public class IotEmqxDownstreamHandler {
55 // 2.2 构建载荷 55 // 2.2 构建载荷
56 byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(), 56 byte[] payload = deviceMessageService.serializeDeviceMessage(message, deviceInfo.getProductKey(),
57 deviceInfo.getDeviceName()); 57 deviceInfo.getDeviceName());
  58 + if (payload == null) {
  59 + log.warn("[handle][影子下行跳过,设备 ID: {},method: {},非 JSON 协议设备不支持影子透传]",
  60 + message.getDeviceId(), message.getMethod());
  61 + return;
  62 + }
58 63
59 // 3. 发布消息 64 // 3. 发布消息
60 protocol.publishMessage(topic, payload); 65 protocol.publishMessage(topic, payload);
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/IotMqttProtocol.java
@@ -17,6 +17,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqt @@ -17,6 +17,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqt
17 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler; 17 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttRegisterHandler;
18 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler; 18 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream.IotMqttUpstreamHandler;
19 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; 19 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager;
  20 +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
20 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 21 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
21 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; 22 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
22 import io.netty.handler.codec.mqtt.MqttConnectReturnCode; 23 import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
@@ -101,9 +102,10 @@ public class IotMqttProtocol implements IotProtocol { @@ -101,9 +102,10 @@ public class IotMqttProtocol implements IotProtocol {
101 // 初始化 Handler 102 // 初始化 Handler
102 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class); 103 this.deviceMessageService = SpringUtil.getBean(IotDeviceMessageService.class);
103 IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); 104 IotDeviceCommonApi deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class);
  105 + IotDeviceService deviceService = SpringUtil.getBean(IotDeviceService.class);
104 this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId); 106 this.authHandler = new IotMqttAuthHandler(connectionManager, deviceMessageService, deviceApi, serverId);
105 this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService); 107 this.registerHandler = new IotMqttRegisterHandler(connectionManager, deviceMessageService);
106 - this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, serverId); 108 + this.upstreamHandler = new IotMqttUpstreamHandler(connectionManager, deviceMessageService, deviceService, serverId);
107 } 109 }
108 110
109 @Override 111 @Override
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/downstream/IotMqttDownstreamHandler.java
@@ -45,7 +45,12 @@ public class IotMqttDownstreamHandler { @@ -45,7 +45,12 @@ public class IotMqttDownstreamHandler {
45 // 2.1 序列化消息 45 // 2.1 序列化消息
46 byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(), 46 byte[] payload = deviceMessageService.serializeDeviceMessage(message, connectionInfo.getProductKey(),
47 connectionInfo.getDeviceName()); 47 connectionInfo.getDeviceName());
48 - Assert.isTrue(payload != null && payload.length > 0, "消息编码结果不能为空"); 48 + if (payload == null) {
  49 + log.warn("[handle][影子下行跳过,设备 ID: {},method: {},非 JSON 协议设备不支持影子透传]",
  50 + message.getDeviceId(), message.getMethod());
  51 + return;
  52 + }
  53 + Assert.isTrue(payload.length > 0, "消息编码结果不能为空");
49 // 2.2 构建主题 54 // 2.2 构建主题
50 Assert.notBlank(message.getMethod(), "消息方法不能为空"); 55 Assert.notBlank(message.getMethod(), "消息方法不能为空");
51 boolean isReply = IotDeviceMessageUtils.isReplyMessage(message); 56 boolean isReply = IotDeviceMessageUtils.isReplyMessage(message);
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttAbstractHandler.java
@@ -72,6 +72,11 @@ public abstract class IotMqttAbstractHandler { @@ -72,6 +72,11 @@ public abstract class IotMqttAbstractHandler {
72 try { 72 try {
73 // 1.1 序列化消息(根据设备配置的序列化类型) 73 // 1.1 序列化消息(根据设备配置的序列化类型)
74 byte[] encodedData = deviceMessageService.serializeDeviceMessage(responseMessage, productKey, deviceName); 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 + }
75 // 1.2 构建响应主题 80 // 1.2 构建响应主题
76 String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true); 81 String replyTopic = IotMqttTopicUtils.buildTopicByMethod(method, productKey, deviceName, true);
77 82
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/handler/upstream/IotMqttUpstreamHandler.java
@@ -2,10 +2,13 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream; @@ -2,10 +2,13 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.handler.upstream;
2 2
3 import cn.hutool.core.lang.Assert; 3 import cn.hutool.core.lang.Assert;
4 import cn.hutool.core.util.ArrayUtil; 4 import cn.hutool.core.util.ArrayUtil;
5 -import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 5 +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  7 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 8 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
8 import cn.iocoder.yudao.module.iot.gateway.protocol.mqtt.manager.IotMqttConnectionManager; 9 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;
9 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; 12 import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService;
10 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils; 13 import cn.iocoder.yudao.module.iot.gateway.util.IotMqttTopicUtils;
11 import io.vertx.mqtt.MqttEndpoint; 14 import io.vertx.mqtt.MqttEndpoint;
@@ -25,11 +28,14 @@ import java.util.stream.Collectors; @@ -25,11 +28,14 @@ import java.util.stream.Collectors;
25 public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { 28 public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
26 29
27 private final String serverId; 30 private final String serverId;
  31 + private final IotDeviceService deviceService;
28 32
29 public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager, 33 public IotMqttUpstreamHandler(IotMqttConnectionManager connectionManager,
30 IotDeviceMessageService deviceMessageService, 34 IotDeviceMessageService deviceMessageService,
  35 + IotDeviceService deviceService,
31 String serverId) { 36 String serverId) {
32 super(connectionManager, deviceMessageService); 37 super(connectionManager, deviceMessageService);
  38 + this.deviceService = deviceService;
33 this.serverId = serverId; 39 this.serverId = serverId;
34 } 40 }
35 41
@@ -69,8 +75,11 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler { @@ -69,8 +75,11 @@ public class IotMqttUpstreamHandler extends IotMqttAbstractHandler {
69 .collect(Collectors.joining(".")); 75 .collect(Collectors.joining("."));
70 IotDeviceMessage message; 76 IotDeviceMessage message;
71 if (IotDeviceMessageMethodEnum.isPassthrough(topicMethod)) { 77 if (IotDeviceMessageMethodEnum.isPassthrough(topicMethod)) {
72 - // 影子透传:从 topic 解析 yudao method,原始 body 作为 params  
73 - Map<String, Object> params = JsonUtils.parseObject(payload, Map.class); 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);
74 if (params == null) { 83 if (params == null) {
75 log.warn("[handleBusinessRequest][影子透传 payload 解析失败,客户端 ID: {},主题: {}]", clientId, topic); 84 log.warn("[handleBusinessRequest][影子透传 payload 解析失败,客户端 ID: {},主题: {}]", clientId, topic);
76 return; 85 return;
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/serialize/IotShadowPassthroughCodec.java 0 → 100644
  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/service/device/message/IotDeviceMessageServiceImpl.java
@@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message; @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message;
2 2
3 import cn.hutool.core.lang.Assert; 3 import cn.hutool.core.lang.Assert;
4 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
5 -import cn.iocoder.yudao.framework.common.util.json.JsonUtils;  
6 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; 5 import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO;
7 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 6 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
8 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum; 7 import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
@@ -11,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; @@ -11,6 +10,7 @@ import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
11 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 10 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
12 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer; 11 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializer;
13 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager; 12 import cn.iocoder.yudao.module.iot.gateway.serialize.IotMessageSerializerManager;
  13 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotShadowPassthroughCodec;
14 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; 14 import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService;
15 import jakarta.annotation.Resource; 15 import jakarta.annotation.Resource;
16 import lombok.extern.slf4j.Slf4j; 16 import lombok.extern.slf4j.Slf4j;
@@ -42,9 +42,13 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -42,9 +42,13 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
42 @Override 42 @Override
43 public byte[] serializeDeviceMessage(IotDeviceMessage message, 43 public byte[] serializeDeviceMessage(IotDeviceMessage message,
44 String productKey, String deviceName) { 44 String productKey, String deviceName) {
45 - // 影子透传:直接序列化 params 为原始线 JSON,忽略产品 serializeType  
46 if (IotDeviceMessageMethodEnum.isPassthrough(message.getMethod())) { 45 if (IotDeviceMessageMethodEnum.isPassthrough(message.getMethod())) {
47 - return JsonUtils.toJsonByte(message.getParams()); 46 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
  47 + if (device == null) {
  48 + throw exception(DEVICE_NOT_EXISTS, productKey, deviceName);
  49 + }
  50 + IotSerializeTypeEnum serializeType = IotSerializeTypeEnum.of(device.getSerializeType());
  51 + return encodePassthrough(message, serializeType, productKey, deviceName);
48 } 52 }
49 // 1.1 获取设备信息 53 // 1.1 获取设备信息
50 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); 54 IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName);
@@ -62,9 +66,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -62,9 +66,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
62 @Override 66 @Override
63 public byte[] serializeDeviceMessage(IotDeviceMessage message, 67 public byte[] serializeDeviceMessage(IotDeviceMessage message,
64 IotSerializeTypeEnum serializeType) { 68 IotSerializeTypeEnum serializeType) {
65 - // 影子透传  
66 if (IotDeviceMessageMethodEnum.isPassthrough(message.getMethod())) { 69 if (IotDeviceMessageMethodEnum.isPassthrough(message.getMethod())) {
67 - return JsonUtils.toJsonByte(message.getParams()); 70 + return encodePassthrough(message, serializeType, null, null);
68 } 71 }
69 // 1. 获取序列化器 72 // 1. 获取序列化器
70 IotMessageSerializer serializer = messageSerializerManager.get(serializeType); 73 IotMessageSerializer serializer = messageSerializerManager.get(serializeType);
@@ -135,4 +138,20 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @@ -135,4 +138,20 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService {
135 } 138 }
136 } 139 }
137 140
  141 + /**
  142 + * 影子透传编码;非 JSON 设备返回 null,由下行发布方跳过发送
  143 + */
  144 + private byte[] encodePassthrough(IotDeviceMessage message, IotSerializeTypeEnum serializeType,
  145 + String productKey, String deviceName) {
  146 + byte[] bytes = IotShadowPassthroughCodec.encode(message.getParams(), serializeType);
  147 + if (bytes != null) {
  148 + return bytes;
  149 + }
  150 + if (productKey != null && deviceName != null) {
  151 + log.warn("[encodePassthrough][影子下行跳过:设备({}/{}) serializeType={} 不支持,目前仅 JSON 协议设备可用]",
  152 + productKey, deviceName, serializeType);
  153 + }
  154 + return null;
  155 + }
  156 +
138 } 157 }
yudao-module-iot/yudao-module-iot-gateway/src/test/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceShadowPassthroughTest.java
@@ -3,8 +3,10 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message; @@ -3,8 +3,10 @@ package cn.iocoder.yudao.module.iot.gateway.service.device.message;
3 import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 3 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
4 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; 4 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotSerializeTypeEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 7 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO; 8 import cn.iocoder.yudao.module.iot.core.topic.shadow.IotDeviceShadowReplyDTO;
  9 +import cn.iocoder.yudao.module.iot.gateway.serialize.IotShadowPassthroughCodec;
8 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
9 import org.mockito.InjectMocks; 11 import org.mockito.InjectMocks;
10 12
@@ -12,6 +14,7 @@ import java.util.HashMap; @@ -12,6 +14,7 @@ import java.util.HashMap;
12 import java.util.Map; 14 import java.util.Map;
13 15
14 import static org.junit.jupiter.api.Assertions.assertEquals; 16 import static org.junit.jupiter.api.Assertions.assertEquals;
  17 +import static org.junit.jupiter.api.Assertions.assertNotNull;
15 import static org.junit.jupiter.api.Assertions.assertNull; 18 import static org.junit.jupiter.api.Assertions.assertNull;
16 19
17 /** 20 /**
@@ -31,7 +34,7 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest { @@ -31,7 +34,7 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest {
31 IotDeviceMessage message = IotDeviceMessage.requestOf( 34 IotDeviceMessage message = IotDeviceMessage.requestOf(
32 IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), payload); 35 IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), payload);
33 36
34 - byte[] bytes = deviceMessageService.serializeDeviceMessage(message, null); 37 + byte[] bytes = deviceMessageService.serializeDeviceMessage(message, IotSerializeTypeEnum.JSON);
35 38
36 @SuppressWarnings("unchecked") 39 @SuppressWarnings("unchecked")
37 Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class); 40 Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class);
@@ -60,11 +63,43 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest { @@ -60,11 +63,43 @@ public class IotDeviceShadowPassthroughTest extends BaseMockitoUnitTest {
60 IotDeviceMessage message = IotDeviceMessage.requestOf( 63 IotDeviceMessage message = IotDeviceMessage.requestOf(
61 IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), reply); 64 IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), reply);
62 65
63 - byte[] bytes = deviceMessageService.serializeDeviceMessage(message, null); 66 + byte[] bytes = deviceMessageService.serializeDeviceMessage(message, IotSerializeTypeEnum.JSON);
64 @SuppressWarnings("unchecked") 67 @SuppressWarnings("unchecked")
65 Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class); 68 Map<String, Object> result = JsonUtils.parseObject(bytes, Map.class);
66 assertEquals("update_reply", result.get("method")); 69 assertEquals("update_reply", result.get("method"));
67 assertNull(result.get("id")); 70 assertNull(result.get("id"));
68 } 71 }
69 72
  73 + @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() {
  84 + Map<String, Object> params = Map.of("method", "get");
  85 + byte[] bytes = IotShadowPassthroughCodec.encode(params, null);
  86 + Map<String, Object> decoded = IotShadowPassthroughCodec.decode(bytes, null);
  87 + assertNotNull(decoded);
  88 + assertEquals("get", decoded.get("method"));
  89 + }
  90 +
  91 + @Test
  92 + public void testSerializeDeviceMessage_passthrough_binaryReturnsNull() {
  93 + IotDeviceMessage message = IotDeviceMessage.requestOf(
  94 + IotDeviceMessageMethodEnum.SHADOW_GET.getMethod(), Map.of("method", "get_reply"));
  95 + assertNull(deviceMessageService.serializeDeviceMessage(message, IotSerializeTypeEnum.BINARY));
  96 + }
  97 +
  98 + @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));
  103 + }
  104 +
70 } 105 }