Commit 6e9b58cbcd4442af5b0a8343d6ead3e86788ddce
1 parent
666afcc3
refactor(iot): 影子透传按 serializeType 序列化,reply 对齐 CommonResult
Showing
5 changed files
with
50 additions
and
36 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/shadow/IotDeviceShadowServiceImpl.java
| ... | ... | @@ -97,7 +97,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { |
| 97 | 97 | |
| 98 | 98 | private void handleDeviceShadowUpdate0(IotDeviceDO device, IotDeviceMessage message) { |
| 99 | 99 | // 1. 解析请求(影子 body 当前按 JSON 建模,非 JSON 设备在 gateway 透传层已拒绝) |
| 100 | - IotDeviceShadowUpdateReqDTO req = JsonUtils.convertObject(message.getParams(), IotDeviceShadowUpdateReqDTO.class); | |
| 100 | + IotDeviceShadowUpdateReqDTO req = JsonUtils.convertObject(message.getParams(), | |
| 101 | + IotDeviceShadowUpdateReqDTO.class); | |
| 101 | 102 | if (req == null) { |
| 102 | 103 | sendShadowReplySafely(device, message.getServerId(), IotDeviceShadowReplyDTO.errorOf( |
| 103 | 104 | extractInnerMethod(message.getParams()), 0L, |
| ... | ... | @@ -165,7 +166,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { |
| 165 | 166 | if (req.getVersion() != null && req.getVersion() == -1L) { |
| 166 | 167 | try { |
| 167 | 168 | resetShadowWithRetry(deviceId); |
| 168 | - sendShadowReplySafely(device, serverId, IotDeviceShadowReplyDTO.updateReplyOf(0L, DateUtil.currentSeconds())); | |
| 169 | + sendShadowReplySafely(device, serverId, | |
| 170 | + IotDeviceShadowReplyDTO.updateReplyOf(0L, DateUtil.currentSeconds())); | |
| 169 | 171 | } catch (ShadowCasExhaustedException ex) { |
| 170 | 172 | handleCasExhaustedForDevice(device, serverId, ex); |
| 171 | 173 | } |
| ... | ... | @@ -266,10 +268,12 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { |
| 266 | 268 | } |
| 267 | 269 | Map<String, Object> mergedReported = mergeMap(latest.getReported(), req.getState().getReported()); |
| 268 | 270 | if (mergedReported.size() > MAX_PROPERTY_COUNT) { |
| 269 | - throw new ShadowUpdateRejectedException(latest.getVersion(), IotDeviceShadowErrorCodeEnum.PROPERTY_COUNT_EXCEEDED); | |
| 271 | + throw new ShadowUpdateRejectedException(latest.getVersion(), | |
| 272 | + IotDeviceShadowErrorCodeEnum.PROPERTY_COUNT_EXCEEDED); | |
| 270 | 273 | } |
| 271 | 274 | long now = DateUtil.currentSeconds(); |
| 272 | - Map<String, Object> metadata = latest.getMetadata() != null ? new HashMap<>(latest.getMetadata()) : new HashMap<>(); | |
| 275 | + Map<String, Object> metadata = latest.getMetadata() != null ? new HashMap<>(latest.getMetadata()) | |
| 276 | + : new HashMap<>(); | |
| 273 | 277 | IotDeviceShadowProtocolUtils.putReportedTimestamps(metadata, req.getState().getReported().keySet(), now); |
| 274 | 278 | return IotDeviceShadowDO.builder() |
| 275 | 279 | .reported(mergedReported) |
| ... | ... | @@ -282,7 +286,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { |
| 282 | 286 | private IotDeviceShadowDO buildDesiredUpdate(IotDeviceShadowDO latest, Map<String, Object> desired) { |
| 283 | 287 | long now = DateUtil.currentSeconds(); |
| 284 | 288 | Map<String, Object> mergedDesired = mergeMap(latest.getDesired(), desired); |
| 285 | - Map<String, Object> metadata = latest.getMetadata() != null ? new HashMap<>(latest.getMetadata()) : new HashMap<>(); | |
| 289 | + Map<String, Object> metadata = latest.getMetadata() != null ? new HashMap<>(latest.getMetadata()) | |
| 290 | + : new HashMap<>(); | |
| 286 | 291 | IotDeviceShadowProtocolUtils.putDesiredTimestamps(metadata, desired.keySet(), now); |
| 287 | 292 | return IotDeviceShadowDO.builder() |
| 288 | 293 | .desired(mergedDesired) |
| ... | ... | @@ -307,7 +312,8 @@ public class IotDeviceShadowServiceImpl implements IotDeviceShadowService { |
| 307 | 312 | * |
| 308 | 313 | * @return 写入成功后的 update DO(回填未更新字段,便于构建全量回复) |
| 309 | 314 | */ |
| 310 | - private IotDeviceShadowDO casUpdateShadow(Long deviceId, Function<IotDeviceShadowDO, IotDeviceShadowDO> updateBuilder) { | |
| 315 | + private IotDeviceShadowDO casUpdateShadow(Long deviceId, | |
| 316 | + Function<IotDeviceShadowDO, IotDeviceShadowDO> updateBuilder) { | |
| 311 | 317 | Long lastKnownVersion = 0L; |
| 312 | 318 | for (int i = 0; i < OPTIMISTIC_LOCK_RETRY; i++) { |
| 313 | 319 | // 首次尝试走缓存,重试时直读 DB 获取最新 version | ... | ... |
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/shadow/IotDeviceShadowServiceImplTest.java
| ... | ... | @@ -339,16 +339,17 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { |
| 339 | 339 | // 前两次 select 返回 v=1 的快照,之后返回 v=2 的最新快照 |
| 340 | 340 | return selectCount.incrementAndGet() <= 2 ? staleShadow : latestShadow; |
| 341 | 341 | }); |
| 342 | - when(deviceShadowMapper.updateByDeviceIdAndVersion(any(), eq(device.getId()), anyLong())).thenAnswer(invocation -> { | |
| 343 | - Long expectVersion = invocation.getArgument(2); | |
| 344 | - if (expectVersion == 1L) { | |
| 345 | - return 0; | |
| 346 | - } | |
| 347 | - if (expectVersion == 2L) { | |
| 348 | - return 1; | |
| 349 | - } | |
| 350 | - return 0; | |
| 351 | - }); | |
| 342 | + when(deviceShadowMapper.updateByDeviceIdAndVersion(any(), eq(device.getId()), anyLong())) | |
| 343 | + .thenAnswer(invocation -> { | |
| 344 | + Long expectVersion = invocation.getArgument(2); | |
| 345 | + if (expectVersion == 1L) { | |
| 346 | + return 0; | |
| 347 | + } | |
| 348 | + if (expectVersion == 2L) { | |
| 349 | + return 1; | |
| 350 | + } | |
| 351 | + return 0; | |
| 352 | + }); | |
| 352 | 353 | |
| 353 | 354 | Map<String, Object> reported = new HashMap<>(); |
| 354 | 355 | reported.put("Temperature", 25); |
| ... | ... | @@ -364,7 +365,8 @@ public class IotDeviceShadowServiceImplTest extends BaseMockitoUnitTest { |
| 364 | 365 | service.handleDeviceShadowUpdate(device, message); |
| 365 | 366 | |
| 366 | 367 | ArgumentCaptor<IotDeviceShadowDO> updateCaptor = ArgumentCaptor.forClass(IotDeviceShadowDO.class); |
| 367 | - verify(deviceShadowMapper, atLeastOnce()).updateByDeviceIdAndVersion(updateCaptor.capture(), eq(device.getId()), any()); | |
| 368 | + verify(deviceShadowMapper, atLeastOnce()).updateByDeviceIdAndVersion(updateCaptor.capture(), eq(device.getId()), | |
| 369 | + any()); | |
| 368 | 370 | IotDeviceShadowDO finalUpdate = updateCaptor.getAllValues().get(updateCaptor.getAllValues().size() - 1); |
| 369 | 371 | // 重试时应基于 v=2 的最新 reported 合并,保留 Humidity=50 |
| 370 | 372 | assertEquals(25, finalUpdate.getReported().get("Temperature")); | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/enums/shadow/IotDeviceShadowErrorCodeEnum.java
| ... | ... | @@ -6,8 +6,10 @@ import lombok.Getter; |
| 6 | 6 | /** |
| 7 | 7 | * IoT 设备影子设备侧错误码枚举 |
| 8 | 8 | * <p> |
| 9 | - * 设备侧 wire code,仅用于回填 {@code update_reply.code/msg},不走 {@link cn.iocoder.yudao.framework.common.pojo.CommonResult}。 | |
| 10 | - * 服务端内部异常请使用 {@link cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants}(如 {@code DEVICE_SHADOW_NOT_ENABLED})。 | |
| 9 | + * 设备侧 wire code,仅用于回填 {@code update_reply.code/msg},不走 | |
| 10 | + * {@link cn.iocoder.yudao.framework.common.pojo.CommonResult}。 | |
| 11 | + * 服务端内部异常请使用 {@link cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants}(如 | |
| 12 | + * {@code DEVICE_SHADOW_NOT_ENABLED})。 | |
| 11 | 13 | * |
| 12 | 14 | * @author 芋道源码 |
| 13 | 15 | */ | ... | ... |
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/topic/shadow/IotDeviceShadowReplyDTO.java
| ... | ... | @@ -14,8 +14,10 @@ import static cn.iocoder.yudao.module.iot.core.util.IotDeviceShadowProtocolUtils |
| 14 | 14 | /** |
| 15 | 15 | * IoT 设备影子下行 Reply DTO |
| 16 | 16 | * <p> |
| 17 | - * 扁平对齐 CommonResult + method,用于 {@link IotDeviceMessageMethodEnum#SHADOW_GET} 透传线 body。 | |
| 18 | - * 错误回复的 {@link #data} 仍携带 version 供设备 resync,有意偏离纯 CommonResult(error 时 data 通常为 null)。 | |
| 17 | + * 扁平对齐 CommonResult + method,用于 {@link IotDeviceMessageMethodEnum#SHADOW_GET} | |
| 18 | + * 透传线 body。 | |
| 19 | + * 错误回复的 {@link #data} 仍携带 version 供设备 resync,有意偏离纯 CommonResult(error 时 data | |
| 20 | + * 通常为 null)。 | |
| 19 | 21 | * |
| 20 | 22 | * @author 芋道源码 |
| 21 | 23 | */ |
| ... | ... | @@ -40,12 +42,13 @@ public class IotDeviceShadowReplyDTO { |
| 40 | 42 | private String msg; |
| 41 | 43 | |
| 42 | 44 | /** |
| 43 | - * 业务数据:get_reply 为 {state, version, timestamp};update_reply 为 {version, timestamp} | |
| 45 | + * 业务数据:get_reply 为 {state, version, timestamp};update_reply 为 {version, | |
| 46 | + * timestamp} | |
| 44 | 47 | */ |
| 45 | 48 | private Object data; |
| 46 | 49 | |
| 47 | 50 | public static IotDeviceShadowReplyDTO getReplyOf(Map<String, Object> desired, Map<String, Object> reported, |
| 48 | - Long version, Long timestamp) { | |
| 51 | + Long version, Long timestamp) { | |
| 49 | 52 | Map<String, Object> state = new HashMap<>(); |
| 50 | 53 | state.put(STATE_DESIRED, desired != null ? desired : new HashMap<>()); |
| 51 | 54 | state.put(STATE_REPORTED, reported != null ? reported : new HashMap<>()); |
| ... | ... | @@ -57,11 +60,12 @@ public class IotDeviceShadowReplyDTO { |
| 57 | 60 | } |
| 58 | 61 | |
| 59 | 62 | public static IotDeviceShadowReplyDTO updateReplyOf(Long version, Long timestamp) { |
| 60 | - return of(INNER_METHOD_UPDATE_REPLY, SUCCESS.getCode(), SUCCESS.getMsg(), dataWithVersionTimestamp(version, timestamp)); | |
| 63 | + return of(INNER_METHOD_UPDATE_REPLY, SUCCESS.getCode(), SUCCESS.getMsg(), | |
| 64 | + dataWithVersionTimestamp(version, timestamp)); | |
| 61 | 65 | } |
| 62 | 66 | |
| 63 | 67 | public static IotDeviceShadowReplyDTO errorOf(String innerMethod, Long version, Integer errorCode, |
| 64 | - String errorMsg, Long timestamp) { | |
| 68 | + String errorMsg, Long timestamp) { | |
| 65 | 69 | return of(toReplyMethod(innerMethod), errorCode, errorMsg, dataWithVersionTimestamp(version, timestamp)); |
| 66 | 70 | } |
| 67 | 71 | ... | ... |
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java
| ... | ... | @@ -43,7 +43,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 43 | 43 | |
| 44 | 44 | @Override |
| 45 | 45 | public byte[] serializeDeviceMessage(IotDeviceMessage message, |
| 46 | - String productKey, String deviceName) { | |
| 46 | + String productKey, String deviceName) { | |
| 47 | 47 | if (IotDeviceMessageMethodEnum.isPassthrough(message.getMethod())) { |
| 48 | 48 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); |
| 49 | 49 | if (device == null) { |
| ... | ... | @@ -67,7 +67,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 67 | 67 | |
| 68 | 68 | @Override |
| 69 | 69 | public byte[] serializeDeviceMessage(IotDeviceMessage message, |
| 70 | - IotSerializeTypeEnum serializeType) { | |
| 70 | + IotSerializeTypeEnum serializeType) { | |
| 71 | 71 | if (IotDeviceMessageMethodEnum.isPassthrough(message.getMethod())) { |
| 72 | 72 | return encodePassthrough(message, serializeType, null, null); |
| 73 | 73 | } |
| ... | ... | @@ -83,7 +83,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 83 | 83 | |
| 84 | 84 | @Override |
| 85 | 85 | public IotDeviceMessage deserializeDeviceMessage(byte[] bytes, |
| 86 | - String productKey, String deviceName) { | |
| 86 | + String productKey, String deviceName) { | |
| 87 | 87 | // 1.1 获取设备信息 |
| 88 | 88 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); |
| 89 | 89 | if (device == null) { |
| ... | ... | @@ -111,7 +111,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 111 | 111 | |
| 112 | 112 | @Override |
| 113 | 113 | public IotDeviceMessage deserializePassthroughMessage(byte[] payload, String method, |
| 114 | - String productKey, String deviceName) { | |
| 114 | + String productKey, String deviceName) { | |
| 115 | 115 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); |
| 116 | 116 | if (device == null) { |
| 117 | 117 | throw exception(DEVICE_NOT_EXISTS, productKey, deviceName); |
| ... | ... | @@ -128,7 +128,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 128 | 128 | |
| 129 | 129 | @Override |
| 130 | 130 | public void sendDeviceMessage(IotDeviceMessage message, |
| 131 | - String productKey, String deviceName, String serverId) { | |
| 131 | + String productKey, String deviceName, String serverId) { | |
| 132 | 132 | // 1. 获取设备信息 |
| 133 | 133 | IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceName); |
| 134 | 134 | if (device == null) { |
| ... | ... | @@ -148,7 +148,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 148 | 148 | * @param serverId 设备连接的 serverId |
| 149 | 149 | */ |
| 150 | 150 | private void appendDeviceMessage(IotDeviceMessage message, |
| 151 | - IotDeviceRespDTO device, String serverId) { | |
| 151 | + IotDeviceRespDTO device, String serverId) { | |
| 152 | 152 | message.setId(IotDeviceMessageUtils.generateMessageId()).setReportTime(LocalDateTime.now()) |
| 153 | 153 | .setDeviceId(device.getId()).setTenantId(device.getTenantId()).setServerId(serverId); |
| 154 | 154 | // 特殊:如果设备没有指定 requestId,则使用 messageId |
| ... | ... | @@ -161,7 +161,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 161 | 161 | * 影子透传编码;不支持的 serializeType 返回 null,由下行发布方跳过发送 |
| 162 | 162 | */ |
| 163 | 163 | private byte[] encodePassthrough(IotDeviceMessage message, IotSerializeTypeEnum serializeType, |
| 164 | - String productKey, String deviceName) { | |
| 164 | + String productKey, String deviceName) { | |
| 165 | 165 | return passthroughRaw("encodePassthrough", serializeType, productKey, deviceName, false, |
| 166 | 166 | serializer -> serializer.encodeRaw(message.getParams())); |
| 167 | 167 | } |
| ... | ... | @@ -170,14 +170,14 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 170 | 170 | * 影子透传解码;解析失败或不支持的 serializeType 返回 null |
| 171 | 171 | */ |
| 172 | 172 | private Map<String, Object> decodePassthrough(byte[] bytes, IotSerializeTypeEnum serializeType, |
| 173 | - String productKey, String deviceName) { | |
| 173 | + String productKey, String deviceName) { | |
| 174 | 174 | return passthroughRaw("decodePassthrough", serializeType, productKey, deviceName, true, |
| 175 | 175 | serializer -> serializer.decodeRaw(bytes)); |
| 176 | 176 | } |
| 177 | 177 | |
| 178 | 178 | private <T> T passthroughRaw(String op, IotSerializeTypeEnum serializeType, |
| 179 | - String productKey, String deviceName, boolean decode, | |
| 180 | - Function<IotMessageSerializer, T> action) { | |
| 179 | + String productKey, String deviceName, boolean decode, | |
| 180 | + Function<IotMessageSerializer, T> action) { | |
| 181 | 181 | IotMessageSerializer serializer = getPassthroughSerializer(serializeType); |
| 182 | 182 | if (serializer == null) { |
| 183 | 183 | warnPassthroughUnsupported(op, serializeType, productKey, deviceName); |
| ... | ... | @@ -204,7 +204,7 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { |
| 204 | 204 | } |
| 205 | 205 | |
| 206 | 206 | private void warnPassthroughUnsupported(String op, IotSerializeTypeEnum serializeType, |
| 207 | - String productKey, String deviceName) { | |
| 207 | + String productKey, String deviceName) { | |
| 208 | 208 | if (productKey != null && deviceName != null) { |
| 209 | 209 | log.warn("[{}][影子透传跳过:设备({}/{}) serializeType={} 不支持,目前仅 JSON 协议设备可用]", |
| 210 | 210 | op, productKey, deviceName, serializeType); | ... | ... |