From e03e3c1a89ecdde66c2b5a02ed7ed97085d77dfc Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 31 May 2026 00:30:21 +0800 Subject: [PATCH] fix(iot): 修复设备属性上报类型转换 --- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java | 48 ++++++++++++++++++++++++++++++------------------ yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java | 11 ++++++++++- yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java | 79 +++++++++++++++++++++++++++++++++++++++---------------------------------------- yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 532 insertions(+), 59 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java index 93c73f5..6bb5f5f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.iot.service.device.property; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.convert.Convert; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; @@ -148,8 +147,16 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { // 1. 根据物模型,拼接合法的属性 // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)? List thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId()); - Map properties = new HashMap<>(); + Map properties = new LinkedHashMap<>(); params.forEach((key, value) -> { + if (!(key instanceof CharSequence)) { + log.error("[saveDeviceProperty][消息({}) 的属性 key({}) 类型不正确]", message, key); + return; + } + if (value == null) { + log.warn("[saveDeviceProperty][消息({}) 的属性({}) 值为空,跳过]", message, key); + return; + } // 忽略大小写匹配物模型,避免设备上报的 key 与 identifier 大小写不一致导致丢失 IotThingModelDO thingModel = CollUtil.findOne(thingModels, o -> StrUtil.equalsIgnoreCase(o.getIdentifier(), (CharSequence) key)); @@ -158,21 +165,9 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { return; } String identifier = thingModel.getIdentifier(); // 统一以物模型 identifier 作为 key,避免大小写问题 - String dataType = thingModel.getProperty().getDataType(); - if (ObjectUtils.equalsAny(dataType, - IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { - // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储 - properties.put(identifier, JsonUtils.toJsonString(value)); - } else if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) { - properties.put(identifier, Convert.toInt(value)); - } else if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(dataType)) { - properties.put(identifier, Convert.toFloat(value)); - } else if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(dataType)) { - properties.put(identifier, Convert.toDouble(value)); - } else if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(dataType)) { - properties.put(identifier, Convert.toBool(value, false) ? (byte) 1 : (byte) 0); - } else { - properties.put(identifier, value); + Object convertedValue = convertPropertyValue(message, thingModel, value); + if (convertedValue != null) { + properties.put(identifier, convertedValue); } }); if (CollUtil.isEmpty(properties)) { @@ -194,6 +189,23 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { extractAndUpdateDeviceLocation(device, (Map) message.getParams()); } + private Object convertPropertyValue(IotDeviceMessage message, IotThingModelDO thingModel, Object value) { + String identifier = thingModel.getIdentifier(); + String dataType = thingModel.getProperty().getDataType(); + try { + Object convertedValue = thingModelService.convertThingModelPropertyValue(thingModel, value); + if (convertedValue == null) { + log.warn("[saveDeviceProperty][消息({}) 的属性({}) 值({}) 无法转换为类型({}),跳过]", + message, identifier, value, dataType); + } + return convertedValue; + } catch (Exception e) { + log.error("[saveDeviceProperty][消息({}) 的属性({}) 值({}) 转换为类型({}) 异常,跳过]", + message, identifier, value, dataType, e); + return null; + } + } + @Override public Map getLatestDeviceProperties(Long deviceId) { return deviceDataRedisDAO.get(deviceId); @@ -310,4 +322,4 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService { return new BigDecimal[]{longitude, latitude}; } -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java index b8c951b..5bdfd7c 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelService.java @@ -108,4 +108,13 @@ public interface IotThingModelService { */ void validateThingModelListExists(Long productId, Set identifiers); -} \ No newline at end of file + /** + * 按物模型属性的数据类型转换设备上报值 + * + * @param thingModel 物模型 + * @param value 设备上报值 + * @return 转换后的值;无法转换时返回 null + */ + Object convertThingModelPropertyValue(IotThingModelDO thingModel, Object value); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java index 4a8b974..9029960 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java @@ -1,10 +1,16 @@ package cn.iocoder.yudao.module.iot.service.thingmodel; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO; import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelPageReqVO; @@ -12,9 +18,14 @@ import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelS import cn.iocoder.yudao.module.iot.convert.thingmodel.IotThingModelConvert; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelBoolOrEnumDataSpecs; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDataSpecs; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs; import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper; import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; +import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; import jakarta.annotation.Resource; @@ -26,7 +37,15 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.Objects; import java.util.Set; @@ -166,6 +185,239 @@ public class IotThingModelServiceImpl implements IotThingModelService { } } + @Override + public Object convertThingModelPropertyValue(IotThingModelDO thingModel, Object value) { + if (thingModel == null || thingModel.getProperty() == null || value == null) { + return null; + } + String dataType = thingModel.getProperty().getDataType(); + if (ObjectUtils.equalsAny(dataType, + IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) { + // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,没有对应数据类型,只能通过 JSON 来存储 + return convertToVarchar(JsonUtils.toJsonString(value), TDengineTableField.LENGTH_VARCHAR); + } + if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) { + return convertToInt(value); + } + if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(dataType)) { + return convertToFloat(value); + } + if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(dataType)) { + return convertToDouble(value); + } + if (IotDataSpecsDataTypeEnum.ENUM.getDataType().equals(dataType)) { + return convertEnumToTinyInt(thingModel, value); + } + if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(dataType)) { + return convertBoolToTinyInt(value); + } + if (IotDataSpecsDataTypeEnum.TEXT.getDataType().equals(dataType)) { + return convertToVarchar(Convert.toStr(value), getTextLength(thingModel)); + } + if (IotDataSpecsDataTypeEnum.DATE.getDataType().equals(dataType)) { + return convertToTimestamp(value); + } + return null; + } + + private Integer getTextLength(IotThingModelDO thingModel) { + ThingModelDataSpecs dataSpecs = thingModel.getProperty().getDataSpecs(); + if (!(dataSpecs instanceof ThingModelDateOrTextDataSpecs)) { + return null; + } + return ((ThingModelDateOrTextDataSpecs) dataSpecs).getLength(); + } + + private String convertToVarchar(String value, Integer length) { + if (value == null) { + return null; + } + if (length != null && value.getBytes(StandardCharsets.UTF_8).length > length) { + return null; + } + return value; + } + + private Integer convertToInt(Object value) { + BigDecimal decimal = convertToBigDecimal(value); + if (decimal == null) { + return null; + } + try { + return decimal.intValueExact(); + } catch (ArithmeticException e) { + return null; + } + } + + private Float convertToFloat(Object value) { + BigDecimal decimal = convertToBigDecimal(value); + if (decimal == null) { + return null; + } + float result = decimal.floatValue(); + return Float.isFinite(result) ? result : null; + } + + private Double convertToDouble(Object value) { + BigDecimal decimal = convertToBigDecimal(value); + if (decimal == null) { + return null; + } + double result = decimal.doubleValue(); + return Double.isFinite(result) ? result : null; + } + + private BigDecimal convertToBigDecimal(Object value) { + if (value instanceof Boolean) { + return null; + } + String text = Convert.toStr(value); + if (StrUtil.isBlank(text)) { + return null; + } + try { + return new BigDecimal(text); + } catch (NumberFormatException e) { + return null; + } + } + + private Byte convertEnumToTinyInt(IotThingModelDO thingModel, Object value) { + Integer intValue = convertToInt(value); + if (intValue == null && value instanceof CharSequence) { + intValue = getEnumValueByName(thingModel, value.toString()); + } + if (intValue == null || !isTinyInt(intValue)) { + return null; + } + if (CollUtil.isNotEmpty(thingModel.getProperty().getDataSpecsList()) + && getEnumDataSpecsByValue(thingModel, intValue) == null) { + return null; + } + return intValue.byteValue(); + } + + private Integer getEnumValueByName(IotThingModelDO thingModel, String name) { + ThingModelBoolOrEnumDataSpecs dataSpecs = getEnumDataSpecsByName(thingModel, name); + return dataSpecs != null ? dataSpecs.getValue() : null; + } + + private ThingModelBoolOrEnumDataSpecs getEnumDataSpecsByName(IotThingModelDO thingModel, String name) { + if (CollUtil.isEmpty(thingModel.getProperty().getDataSpecsList())) { + return null; + } + for (ThingModelDataSpecs dataSpecs : thingModel.getProperty().getDataSpecsList()) { + if (!(dataSpecs instanceof ThingModelBoolOrEnumDataSpecs)) { + continue; + } + ThingModelBoolOrEnumDataSpecs enumDataSpecs = (ThingModelBoolOrEnumDataSpecs) dataSpecs; + if (StrUtil.equals(enumDataSpecs.getName(), name)) { + return enumDataSpecs; + } + } + return null; + } + + private ThingModelBoolOrEnumDataSpecs getEnumDataSpecsByValue(IotThingModelDO thingModel, Integer value) { + if (CollUtil.isEmpty(thingModel.getProperty().getDataSpecsList())) { + return null; + } + for (ThingModelDataSpecs dataSpecs : thingModel.getProperty().getDataSpecsList()) { + if (!(dataSpecs instanceof ThingModelBoolOrEnumDataSpecs)) { + continue; + } + ThingModelBoolOrEnumDataSpecs enumDataSpecs = (ThingModelBoolOrEnumDataSpecs) dataSpecs; + if (Objects.equals(enumDataSpecs.getValue(), value)) { + return enumDataSpecs; + } + } + return null; + } + + private Byte convertBoolToTinyInt(Object value) { + if (value instanceof Boolean) { + return (Boolean) value ? (byte) 1 : (byte) 0; + } + if (value instanceof CharSequence) { + String text = StrUtil.trim(value.toString()); + if (StrUtil.equalsIgnoreCase(text, "true")) { + return (byte) 1; + } + if (StrUtil.equalsIgnoreCase(text, "false")) { + return (byte) 0; + } + } + Integer intValue = convertToInt(value); + if (intValue == null || (intValue != 0 && intValue != 1)) { + return null; + } + return intValue.byteValue(); + } + + private boolean isTinyInt(Integer value) { + return value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE; + } + + private Long convertToTimestamp(Object value) { + if (value instanceof LocalDateTime) { + return LocalDateTimeUtil.toEpochMilli((LocalDateTime) value); + } + if (value instanceof LocalDate) { + return LocalDateTimeUtil.toEpochMilli(((LocalDate) value).atStartOfDay()); + } + if (value instanceof Instant) { + return ((Instant) value).toEpochMilli(); + } + if (value instanceof OffsetDateTime) { + return ((OffsetDateTime) value).toInstant().toEpochMilli(); + } + if (value instanceof ZonedDateTime) { + return ((ZonedDateTime) value).toInstant().toEpochMilli(); + } + if (value instanceof Date) { + return ((Date) value).getTime(); + } + Long timestamp = convertToLong(value); + if (timestamp != null) { + return timestamp; + } + if (!(value instanceof CharSequence)) { + return null; + } + String text = StrUtil.trim(value.toString()); + if (StrUtil.isBlank(text)) { + return null; + } + try { + return OffsetDateTime.parse(text).toInstant().toEpochMilli(); + } catch (Exception ignored) { + // 尝试本地时间格式,例如 yyyy-MM-dd HH:mm:ss + } + try { + return LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtil.parse(text, DatePattern.NORM_DATETIME_PATTERN)); + } catch (Exception ignored) { + // 尝试其它 Hutool 支持的本地时间格式 + } + try { + return LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtils.parse(text)); + } catch (Exception ignored) { + return null; + } + } + + private Long convertToLong(Object value) { + BigDecimal decimal = convertToBigDecimal(value); + if (decimal == null) { + return null; + } + try { + return decimal.longValueExact(); + } catch (ArithmeticException e) { + return null; + } + } + private void validateProductThingModelMapperExists(Long id) { if (thingModelMapper.selectById(id) == null) { throw exception(THING_MODEL_NOT_EXISTS); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java index 8f87cb8..d2f26aa 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java @@ -54,6 +54,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest { // mock 行为 when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) .thenReturn(singletonList(thingModel)); + when(thingModelService.convertThingModelPropertyValue(thingModel, 100)).thenReturn(100); // 调用 service.saveDeviceProperty(device, message); @@ -91,64 +92,62 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest { } @Test - public void testSaveDeviceProperty_boolFromBooleanTrue() { - // 准备参数:物模型为 BOOL,设备上报原生 boolean true - assertBoolValueConvertedToByte(true, (byte) 1); - } + public void testSaveDeviceProperty_convertValueFailed() { + // 准备参数:物模型存在,但是属性值无法按物模型转换 + IotDeviceDO device = buildDevice(); + IotThingModelDO temperature = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType()); + Map params = new HashMap<>(); + params.put("Temperature", "abc"); + IotDeviceMessage message = buildMessage(params); - @Test - public void testSaveDeviceProperty_boolFromBooleanFalse() { - // 准备参数:物模型为 BOOL,设备上报原生 boolean false - assertBoolValueConvertedToByte(false, (byte) 0); - } + when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) + .thenReturn(singletonList(temperature)); + when(thingModelService.convertThingModelPropertyValue(temperature, "abc")).thenReturn(null); - @Test - public void testSaveDeviceProperty_boolFromStringTrue() { - // 准备参数:物模型为 BOOL,设备上报字符串 "true" - assertBoolValueConvertedToByte("true", (byte) 1); - } + assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); - @Test - public void testSaveDeviceProperty_boolFromStringFalse() { - // 准备参数:物模型为 BOOL,设备上报字符串 "false" - assertBoolValueConvertedToByte("false", (byte) 0); + verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong()); + verify(deviceDataRedisDAO, never()).putAll(anyLong(), any()); } @Test - public void testSaveDeviceProperty_boolFromNumberOne() { - // 准备参数:物模型为 BOOL,设备上报数字 1 - assertBoolValueConvertedToByte(1, (byte) 1); - } + public void testSaveDeviceProperty_skipNullValue() { + // 准备参数:属性值为空,不能写入 TDengine 与 Redis + IotDeviceDO device = buildDevice(); + IotThingModelDO thingModel = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType()); + Map params = new HashMap<>(); + params.put("Temperature", null); + IotDeviceMessage message = buildMessage(params); - @Test - public void testSaveDeviceProperty_boolFromNumberZero() { - // 准备参数:物模型为 BOOL,设备上报数字 0 - assertBoolValueConvertedToByte(0, (byte) 0); + when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) + .thenReturn(singletonList(thingModel)); + + assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); + + verify(thingModelService, never()).convertThingModelPropertyValue(any(), any()); + verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong()); + verify(deviceDataRedisDAO, never()).putAll(anyLong(), any()); } - /** - * 校验 BOOL 类型属性上报后,最终落到 properties Map 的值类型与数值 - */ - private void assertBoolValueConvertedToByte(Object reportedValue, byte expected) { - // 准备参数 + @Test + public void testSaveDeviceProperty_skipInvalidKeyType() { + // 准备参数:Map 中包含非字符串 key,不能因为强转失败影响其它合法属性 IotDeviceDO device = buildDevice(); IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType()); - Map params = new HashMap<>(); - params.put("PowerSwitch", reportedValue); + Map params = new HashMap<>(); + params.put(123, 1); + params.put("PowerSwitch", true); IotDeviceMessage message = buildMessage(params); - // mock 行为 when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) .thenReturn(singletonList(thingModel)); + when(thingModelService.convertThingModelPropertyValue(thingModel, true)).thenReturn((byte) 1); - // 调用:不能抛异常 assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); - // 断言:写入的 value 是 byte 类型,且值匹配 Map dbProperties = captureMapperInsertProperties(); - Object actual = dbProperties.get("PowerSwitch"); - assertTrue(actual instanceof Byte, "BOOL 属性应被转为 Byte 类型,实际为 " + (actual == null ? "null" : actual.getClass())); - assertEquals(expected, actual); + assertEquals(1, dbProperties.size()); + assertEquals((byte) 1, dbProperties.get("PowerSwitch")); } // ========== 辅助方法 ========== @@ -173,7 +172,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest { /** * 构造一条属性上报消息 */ - private IotDeviceMessage buildMessage(Map params) { + private IotDeviceMessage buildMessage(Map params) { IotDeviceMessage message = new IotDeviceMessage(); message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); message.setParams(params); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java new file mode 100644 index 0000000..2668c43 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java @@ -0,0 +1,201 @@ +package cn.iocoder.yudao.module.iot.service.thingmodel; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelBoolOrEnumDataSpecs; +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs; +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService; +import cn.iocoder.yudao.module.iot.service.product.IotProductService; +import jakarta.annotation.Resource; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link IotThingModelServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +@Import(IotThingModelServiceImpl.class) +public class IotThingModelServiceImplTest extends BaseDbUnitTest { + + @Resource + private IotThingModelServiceImpl thingModelService; + + @MockitoBean + private IotProductService productService; + @MockitoBean + private IotDeviceModbusPointService deviceModbusPointService; + + @Test + public void testConvertThingModelPropertyValue_boolFromBooleanTrue() { + assertBoolValueConvertedToByte(true, (byte) 1); + } + + @Test + public void testConvertThingModelPropertyValue_boolFromBooleanFalse() { + assertBoolValueConvertedToByte(false, (byte) 0); + } + + @Test + public void testConvertThingModelPropertyValue_boolFromStringTrue() { + assertBoolValueConvertedToByte("true", (byte) 1); + } + + @Test + public void testConvertThingModelPropertyValue_boolFromStringFalse() { + assertBoolValueConvertedToByte("false", (byte) 0); + } + + @Test + public void testConvertThingModelPropertyValue_boolFromNumberOne() { + assertBoolValueConvertedToByte(1, (byte) 1); + } + + @Test + public void testConvertThingModelPropertyValue_boolFromNumberZero() { + assertBoolValueConvertedToByte(0, (byte) 0); + } + + @Test + public void testConvertThingModelPropertyValue_boolInvalid() { + IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType()); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "yes"); + + assertNull(result); + } + + @Test + public void testConvertThingModelPropertyValue_enumFromStringNumber() { + IotThingModelDO thingModel = buildEnumThingModel("WorkMode", enumSpec("Auto", 1), enumSpec("Manual", 2)); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "1"); + + assertEquals((byte) 1, result); + } + + @Test + public void testConvertThingModelPropertyValue_enumFromSpecName() { + IotThingModelDO thingModel = buildEnumThingModel("WorkMode", enumSpec("Auto", 1), enumSpec("Manual", 2)); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "Manual"); + + assertEquals((byte) 2, result); + } + + @Test + public void testConvertThingModelPropertyValue_enumInvalidSpecValue() { + IotThingModelDO thingModel = buildEnumThingModel("WorkMode", enumSpec("Auto", 1), enumSpec("Manual", 2)); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, 3); + + assertNull(result); + } + + @Test + public void testConvertThingModelPropertyValue_enumOutOfTinyIntRange() { + IotThingModelDO thingModel = buildThingModel("WorkMode", IotDataSpecsDataTypeEnum.ENUM.getDataType()); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, 128); + + assertNull(result); + } + + @Test + public void testConvertThingModelPropertyValue_dateFromLocalDateTime() { + IotThingModelDO thingModel = buildThingModel("CollectTime", IotDataSpecsDataTypeEnum.DATE.getDataType()); + LocalDateTime collectTime = LocalDateTime.of(2025, 1, 2, 3, 4, 5); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, collectTime); + + assertEquals(LocalDateTimeUtil.toEpochMilli(collectTime), result); + } + + @Test + public void testConvertThingModelPropertyValue_dateFromString() { + IotThingModelDO thingModel = buildThingModel("CollectTime", IotDataSpecsDataTypeEnum.DATE.getDataType()); + LocalDateTime collectTime = LocalDateTime.of(2025, 1, 2, 3, 4, 5); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "2025-01-02 03:04:05"); + + assertEquals(LocalDateTimeUtil.toEpochMilli(collectTime), result); + } + + @Test + public void testConvertThingModelPropertyValue_intInvalid() { + IotThingModelDO thingModel = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType()); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "abc"); + + assertNull(result); + } + + @Test + public void testConvertThingModelPropertyValue_textOverLength() { + IotThingModelDO thingModel = buildTextThingModel("Remark", 3); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "abcd"); + + assertNull(result); + } + + @Test + public void testConvertThingModelPropertyValue_structToJson() { + IotThingModelDO thingModel = buildThingModel("GeoLocation", IotDataSpecsDataTypeEnum.STRUCT.getDataType()); + Map value = new HashMap<>(); + value.put("Longitude", 120.1D); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, value); + + assertEquals(JsonUtils.toJsonString(value), result); + } + + private void assertBoolValueConvertedToByte(Object reportedValue, byte expected) { + IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType()); + + Object result = thingModelService.convertThingModelPropertyValue(thingModel, reportedValue); + + assertEquals(expected, result); + } + + private IotThingModelDO buildThingModel(String identifier, String dataType) { + ThingModelProperty property = new ThingModelProperty(); + property.setIdentifier(identifier); + property.setDataType(dataType); + return IotThingModelDO.builder().identifier(identifier).property(property).build(); + } + + private IotThingModelDO buildEnumThingModel(String identifier, ThingModelBoolOrEnumDataSpecs... dataSpecsList) { + IotThingModelDO thingModel = buildThingModel(identifier, IotDataSpecsDataTypeEnum.ENUM.getDataType()); + thingModel.getProperty().setDataSpecsList(Arrays.asList(dataSpecsList)); + return thingModel; + } + + private ThingModelBoolOrEnumDataSpecs enumSpec(String name, Integer value) { + ThingModelBoolOrEnumDataSpecs dataSpecs = new ThingModelBoolOrEnumDataSpecs(); + dataSpecs.setName(name); + dataSpecs.setValue(value); + return dataSpecs; + } + + private IotThingModelDO buildTextThingModel(String identifier, Integer length) { + IotThingModelDO thingModel = buildThingModel(identifier, IotDataSpecsDataTypeEnum.TEXT.getDataType()); + ThingModelDateOrTextDataSpecs dataSpecs = new ThingModelDateOrTextDataSpecs(); + dataSpecs.setLength(length); + thingModel.getProperty().setDataSpecs(dataSpecs); + return thingModel; + } + +} -- libgit2 0.21.4