Commit e03e3c1a89ecdde66c2b5a02ed7ed97085d77dfc

Authored by YunaiV
1 parent 23c642ed

fix(iot): 修复设备属性上报类型转换

- 抽取物模型属性值转换到 ThingModelService
- 支持 ENUM、DATE、BOOL、STRUCT、ARRAY、TEXT 等 TDengine 类型转换
- 跳过 null、非字符串 key 和无效值,并补充单测
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java
1 1 package cn.iocoder.yudao.module.iot.service.device.property;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4   -import cn.hutool.core.convert.Convert;
5 4 import cn.hutool.core.date.LocalDateTimeUtil;
6 5 import cn.hutool.core.map.MapUtil;
7 6 import cn.hutool.core.util.ObjUtil;
... ... @@ -148,8 +147,16 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
148 147 // 1. 根据物模型,拼接合法的属性
149 148 // TODO @芋艿:【待定 004】赋能后,属性到底以 thingModel 为准(ik),还是 db 的表结构为准(tl)?
150 149 List<IotThingModelDO> thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId());
151   - Map<String, Object> properties = new HashMap<>();
  150 + Map<String, Object> properties = new LinkedHashMap<>();
152 151 params.forEach((key, value) -> {
  152 + if (!(key instanceof CharSequence)) {
  153 + log.error("[saveDeviceProperty][消息({}) 的属性 key({}) 类型不正确]", message, key);
  154 + return;
  155 + }
  156 + if (value == null) {
  157 + log.warn("[saveDeviceProperty][消息({}) 的属性({}) 值为空,跳过]", message, key);
  158 + return;
  159 + }
153 160 // 忽略大小写匹配物模型,避免设备上报的 key 与 identifier 大小写不一致导致丢失
154 161 IotThingModelDO thingModel = CollUtil.findOne(thingModels,
155 162 o -> StrUtil.equalsIgnoreCase(o.getIdentifier(), (CharSequence) key));
... ... @@ -158,21 +165,9 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
158 165 return;
159 166 }
160 167 String identifier = thingModel.getIdentifier(); // 统一以物模型 identifier 作为 key,避免大小写问题
161   - String dataType = thingModel.getProperty().getDataType();
162   - if (ObjectUtils.equalsAny(dataType,
163   - IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
164   - // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,有没对应数据类型,只能通过 JSON 来存储
165   - properties.put(identifier, JsonUtils.toJsonString(value));
166   - } else if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) {
167   - properties.put(identifier, Convert.toInt(value));
168   - } else if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(dataType)) {
169   - properties.put(identifier, Convert.toFloat(value));
170   - } else if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(dataType)) {
171   - properties.put(identifier, Convert.toDouble(value));
172   - } else if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(dataType)) {
173   - properties.put(identifier, Convert.toBool(value, false) ? (byte) 1 : (byte) 0);
174   - } else {
175   - properties.put(identifier, value);
  168 + Object convertedValue = convertPropertyValue(message, thingModel, value);
  169 + if (convertedValue != null) {
  170 + properties.put(identifier, convertedValue);
176 171 }
177 172 });
178 173 if (CollUtil.isEmpty(properties)) {
... ... @@ -194,6 +189,23 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
194 189 extractAndUpdateDeviceLocation(device, (Map<?, ?>) message.getParams());
195 190 }
196 191  
  192 + private Object convertPropertyValue(IotDeviceMessage message, IotThingModelDO thingModel, Object value) {
  193 + String identifier = thingModel.getIdentifier();
  194 + String dataType = thingModel.getProperty().getDataType();
  195 + try {
  196 + Object convertedValue = thingModelService.convertThingModelPropertyValue(thingModel, value);
  197 + if (convertedValue == null) {
  198 + log.warn("[saveDeviceProperty][消息({}) 的属性({}) 值({}) 无法转换为类型({}),跳过]",
  199 + message, identifier, value, dataType);
  200 + }
  201 + return convertedValue;
  202 + } catch (Exception e) {
  203 + log.error("[saveDeviceProperty][消息({}) 的属性({}) 值({}) 转换为类型({}) 异常,跳过]",
  204 + message, identifier, value, dataType, e);
  205 + return null;
  206 + }
  207 + }
  208 +
197 209 @Override
198 210 public Map<String, IotDevicePropertyDO> getLatestDeviceProperties(Long deviceId) {
199 211 return deviceDataRedisDAO.get(deviceId);
... ... @@ -310,4 +322,4 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
310 322 return new BigDecimal[]{longitude, latitude};
311 323 }
312 324  
313   -}
314 325 \ No newline at end of file
  326 +}
... ...
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 {
108 108 */
109 109 void validateThingModelListExists(Long productId, Set<String> identifiers);
110 110  
111   -}
112 111 \ No newline at end of file
  112 + /**
  113 + * 按物模型属性的数据类型转换设备上报值
  114 + *
  115 + * @param thingModel 物模型
  116 + * @param value 设备上报值
  117 + * @return 转换后的值;无法转换时返回 null
  118 + */
  119 + Object convertThingModelPropertyValue(IotThingModelDO thingModel, Object value);
  120 +
  121 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImpl.java
1 1 package cn.iocoder.yudao.module.iot.service.thingmodel;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.convert.Convert;
  5 +import cn.hutool.core.date.DatePattern;
  6 +import cn.hutool.core.date.LocalDateTimeUtil;
4 7 import cn.hutool.core.util.ObjectUtil;
5 8 import cn.hutool.core.util.StrUtil;
6 9 import cn.hutool.extra.spring.SpringUtil;
7 10 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  11 +import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
  12 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  13 +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
8 14 import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
9 15 import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelListReqVO;
10 16 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
12 18 import cn.iocoder.yudao.module.iot.convert.thingmodel.IotThingModelConvert;
13 19 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
14 20 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
  21 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelBoolOrEnumDataSpecs;
  22 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDataSpecs;
  23 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
15 24 import cn.iocoder.yudao.module.iot.dal.mysql.thingmodel.IotThingModelMapper;
16 25 import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
17 26 import cn.iocoder.yudao.module.iot.enums.product.IotProductStatusEnum;
  27 +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
  28 +import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
18 29 import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService;
19 30 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
20 31 import jakarta.annotation.Resource;
... ... @@ -26,7 +37,15 @@ import org.springframework.stereotype.Service;
26 37 import org.springframework.transaction.annotation.Transactional;
27 38 import org.springframework.validation.annotation.Validated;
28 39  
  40 +import java.math.BigDecimal;
  41 +import java.nio.charset.StandardCharsets;
  42 +import java.time.Instant;
  43 +import java.time.LocalDate;
  44 +import java.time.LocalDateTime;
  45 +import java.time.OffsetDateTime;
  46 +import java.time.ZonedDateTime;
29 47 import java.util.Collection;
  48 +import java.util.Date;
30 49 import java.util.List;
31 50 import java.util.Objects;
32 51 import java.util.Set;
... ... @@ -166,6 +185,239 @@ public class IotThingModelServiceImpl implements IotThingModelService {
166 185 }
167 186 }
168 187  
  188 + @Override
  189 + public Object convertThingModelPropertyValue(IotThingModelDO thingModel, Object value) {
  190 + if (thingModel == null || thingModel.getProperty() == null || value == null) {
  191 + return null;
  192 + }
  193 + String dataType = thingModel.getProperty().getDataType();
  194 + if (ObjectUtils.equalsAny(dataType,
  195 + IotDataSpecsDataTypeEnum.STRUCT.getDataType(), IotDataSpecsDataTypeEnum.ARRAY.getDataType())) {
  196 + // 特殊:STRUCT 和 ARRAY 类型,在 TDengine 里,没有对应数据类型,只能通过 JSON 来存储
  197 + return convertToVarchar(JsonUtils.toJsonString(value), TDengineTableField.LENGTH_VARCHAR);
  198 + }
  199 + if (IotDataSpecsDataTypeEnum.INT.getDataType().equals(dataType)) {
  200 + return convertToInt(value);
  201 + }
  202 + if (IotDataSpecsDataTypeEnum.FLOAT.getDataType().equals(dataType)) {
  203 + return convertToFloat(value);
  204 + }
  205 + if (IotDataSpecsDataTypeEnum.DOUBLE.getDataType().equals(dataType)) {
  206 + return convertToDouble(value);
  207 + }
  208 + if (IotDataSpecsDataTypeEnum.ENUM.getDataType().equals(dataType)) {
  209 + return convertEnumToTinyInt(thingModel, value);
  210 + }
  211 + if (IotDataSpecsDataTypeEnum.BOOL.getDataType().equals(dataType)) {
  212 + return convertBoolToTinyInt(value);
  213 + }
  214 + if (IotDataSpecsDataTypeEnum.TEXT.getDataType().equals(dataType)) {
  215 + return convertToVarchar(Convert.toStr(value), getTextLength(thingModel));
  216 + }
  217 + if (IotDataSpecsDataTypeEnum.DATE.getDataType().equals(dataType)) {
  218 + return convertToTimestamp(value);
  219 + }
  220 + return null;
  221 + }
  222 +
  223 + private Integer getTextLength(IotThingModelDO thingModel) {
  224 + ThingModelDataSpecs dataSpecs = thingModel.getProperty().getDataSpecs();
  225 + if (!(dataSpecs instanceof ThingModelDateOrTextDataSpecs)) {
  226 + return null;
  227 + }
  228 + return ((ThingModelDateOrTextDataSpecs) dataSpecs).getLength();
  229 + }
  230 +
  231 + private String convertToVarchar(String value, Integer length) {
  232 + if (value == null) {
  233 + return null;
  234 + }
  235 + if (length != null && value.getBytes(StandardCharsets.UTF_8).length > length) {
  236 + return null;
  237 + }
  238 + return value;
  239 + }
  240 +
  241 + private Integer convertToInt(Object value) {
  242 + BigDecimal decimal = convertToBigDecimal(value);
  243 + if (decimal == null) {
  244 + return null;
  245 + }
  246 + try {
  247 + return decimal.intValueExact();
  248 + } catch (ArithmeticException e) {
  249 + return null;
  250 + }
  251 + }
  252 +
  253 + private Float convertToFloat(Object value) {
  254 + BigDecimal decimal = convertToBigDecimal(value);
  255 + if (decimal == null) {
  256 + return null;
  257 + }
  258 + float result = decimal.floatValue();
  259 + return Float.isFinite(result) ? result : null;
  260 + }
  261 +
  262 + private Double convertToDouble(Object value) {
  263 + BigDecimal decimal = convertToBigDecimal(value);
  264 + if (decimal == null) {
  265 + return null;
  266 + }
  267 + double result = decimal.doubleValue();
  268 + return Double.isFinite(result) ? result : null;
  269 + }
  270 +
  271 + private BigDecimal convertToBigDecimal(Object value) {
  272 + if (value instanceof Boolean) {
  273 + return null;
  274 + }
  275 + String text = Convert.toStr(value);
  276 + if (StrUtil.isBlank(text)) {
  277 + return null;
  278 + }
  279 + try {
  280 + return new BigDecimal(text);
  281 + } catch (NumberFormatException e) {
  282 + return null;
  283 + }
  284 + }
  285 +
  286 + private Byte convertEnumToTinyInt(IotThingModelDO thingModel, Object value) {
  287 + Integer intValue = convertToInt(value);
  288 + if (intValue == null && value instanceof CharSequence) {
  289 + intValue = getEnumValueByName(thingModel, value.toString());
  290 + }
  291 + if (intValue == null || !isTinyInt(intValue)) {
  292 + return null;
  293 + }
  294 + if (CollUtil.isNotEmpty(thingModel.getProperty().getDataSpecsList())
  295 + && getEnumDataSpecsByValue(thingModel, intValue) == null) {
  296 + return null;
  297 + }
  298 + return intValue.byteValue();
  299 + }
  300 +
  301 + private Integer getEnumValueByName(IotThingModelDO thingModel, String name) {
  302 + ThingModelBoolOrEnumDataSpecs dataSpecs = getEnumDataSpecsByName(thingModel, name);
  303 + return dataSpecs != null ? dataSpecs.getValue() : null;
  304 + }
  305 +
  306 + private ThingModelBoolOrEnumDataSpecs getEnumDataSpecsByName(IotThingModelDO thingModel, String name) {
  307 + if (CollUtil.isEmpty(thingModel.getProperty().getDataSpecsList())) {
  308 + return null;
  309 + }
  310 + for (ThingModelDataSpecs dataSpecs : thingModel.getProperty().getDataSpecsList()) {
  311 + if (!(dataSpecs instanceof ThingModelBoolOrEnumDataSpecs)) {
  312 + continue;
  313 + }
  314 + ThingModelBoolOrEnumDataSpecs enumDataSpecs = (ThingModelBoolOrEnumDataSpecs) dataSpecs;
  315 + if (StrUtil.equals(enumDataSpecs.getName(), name)) {
  316 + return enumDataSpecs;
  317 + }
  318 + }
  319 + return null;
  320 + }
  321 +
  322 + private ThingModelBoolOrEnumDataSpecs getEnumDataSpecsByValue(IotThingModelDO thingModel, Integer value) {
  323 + if (CollUtil.isEmpty(thingModel.getProperty().getDataSpecsList())) {
  324 + return null;
  325 + }
  326 + for (ThingModelDataSpecs dataSpecs : thingModel.getProperty().getDataSpecsList()) {
  327 + if (!(dataSpecs instanceof ThingModelBoolOrEnumDataSpecs)) {
  328 + continue;
  329 + }
  330 + ThingModelBoolOrEnumDataSpecs enumDataSpecs = (ThingModelBoolOrEnumDataSpecs) dataSpecs;
  331 + if (Objects.equals(enumDataSpecs.getValue(), value)) {
  332 + return enumDataSpecs;
  333 + }
  334 + }
  335 + return null;
  336 + }
  337 +
  338 + private Byte convertBoolToTinyInt(Object value) {
  339 + if (value instanceof Boolean) {
  340 + return (Boolean) value ? (byte) 1 : (byte) 0;
  341 + }
  342 + if (value instanceof CharSequence) {
  343 + String text = StrUtil.trim(value.toString());
  344 + if (StrUtil.equalsIgnoreCase(text, "true")) {
  345 + return (byte) 1;
  346 + }
  347 + if (StrUtil.equalsIgnoreCase(text, "false")) {
  348 + return (byte) 0;
  349 + }
  350 + }
  351 + Integer intValue = convertToInt(value);
  352 + if (intValue == null || (intValue != 0 && intValue != 1)) {
  353 + return null;
  354 + }
  355 + return intValue.byteValue();
  356 + }
  357 +
  358 + private boolean isTinyInt(Integer value) {
  359 + return value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE;
  360 + }
  361 +
  362 + private Long convertToTimestamp(Object value) {
  363 + if (value instanceof LocalDateTime) {
  364 + return LocalDateTimeUtil.toEpochMilli((LocalDateTime) value);
  365 + }
  366 + if (value instanceof LocalDate) {
  367 + return LocalDateTimeUtil.toEpochMilli(((LocalDate) value).atStartOfDay());
  368 + }
  369 + if (value instanceof Instant) {
  370 + return ((Instant) value).toEpochMilli();
  371 + }
  372 + if (value instanceof OffsetDateTime) {
  373 + return ((OffsetDateTime) value).toInstant().toEpochMilli();
  374 + }
  375 + if (value instanceof ZonedDateTime) {
  376 + return ((ZonedDateTime) value).toInstant().toEpochMilli();
  377 + }
  378 + if (value instanceof Date) {
  379 + return ((Date) value).getTime();
  380 + }
  381 + Long timestamp = convertToLong(value);
  382 + if (timestamp != null) {
  383 + return timestamp;
  384 + }
  385 + if (!(value instanceof CharSequence)) {
  386 + return null;
  387 + }
  388 + String text = StrUtil.trim(value.toString());
  389 + if (StrUtil.isBlank(text)) {
  390 + return null;
  391 + }
  392 + try {
  393 + return OffsetDateTime.parse(text).toInstant().toEpochMilli();
  394 + } catch (Exception ignored) {
  395 + // 尝试本地时间格式,例如 yyyy-MM-dd HH:mm:ss
  396 + }
  397 + try {
  398 + return LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtil.parse(text, DatePattern.NORM_DATETIME_PATTERN));
  399 + } catch (Exception ignored) {
  400 + // 尝试其它 Hutool 支持的本地时间格式
  401 + }
  402 + try {
  403 + return LocalDateTimeUtil.toEpochMilli(LocalDateTimeUtils.parse(text));
  404 + } catch (Exception ignored) {
  405 + return null;
  406 + }
  407 + }
  408 +
  409 + private Long convertToLong(Object value) {
  410 + BigDecimal decimal = convertToBigDecimal(value);
  411 + if (decimal == null) {
  412 + return null;
  413 + }
  414 + try {
  415 + return decimal.longValueExact();
  416 + } catch (ArithmeticException e) {
  417 + return null;
  418 + }
  419 + }
  420 +
169 421 private void validateProductThingModelMapperExists(Long id) {
170 422 if (thingModelMapper.selectById(id) == null) {
171 423 throw exception(THING_MODEL_NOT_EXISTS);
... ...
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 {
54 54 // mock 行为
55 55 when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
56 56 .thenReturn(singletonList(thingModel));
  57 + when(thingModelService.convertThingModelPropertyValue(thingModel, 100)).thenReturn(100);
57 58  
58 59 // 调用
59 60 service.saveDeviceProperty(device, message);
... ... @@ -91,64 +92,62 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
91 92 }
92 93  
93 94 @Test
94   - public void testSaveDeviceProperty_boolFromBooleanTrue() {
95   - // 准备参数:物模型为 BOOL,设备上报原生 boolean true
96   - assertBoolValueConvertedToByte(true, (byte) 1);
97   - }
  95 + public void testSaveDeviceProperty_convertValueFailed() {
  96 + // 准备参数:物模型存在,但是属性值无法按物模型转换
  97 + IotDeviceDO device = buildDevice();
  98 + IotThingModelDO temperature = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType());
  99 + Map<String, Object> params = new HashMap<>();
  100 + params.put("Temperature", "abc");
  101 + IotDeviceMessage message = buildMessage(params);
98 102  
99   - @Test
100   - public void testSaveDeviceProperty_boolFromBooleanFalse() {
101   - // 准备参数:物模型为 BOOL,设备上报原生 boolean false
102   - assertBoolValueConvertedToByte(false, (byte) 0);
103   - }
  103 + when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
  104 + .thenReturn(singletonList(temperature));
  105 + when(thingModelService.convertThingModelPropertyValue(temperature, "abc")).thenReturn(null);
104 106  
105   - @Test
106   - public void testSaveDeviceProperty_boolFromStringTrue() {
107   - // 准备参数:物模型为 BOOL,设备上报字符串 "true"
108   - assertBoolValueConvertedToByte("true", (byte) 1);
109   - }
  107 + assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
110 108  
111   - @Test
112   - public void testSaveDeviceProperty_boolFromStringFalse() {
113   - // 准备参数:物模型为 BOOL,设备上报字符串 "false"
114   - assertBoolValueConvertedToByte("false", (byte) 0);
  109 + verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
  110 + verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
115 111 }
116 112  
117 113 @Test
118   - public void testSaveDeviceProperty_boolFromNumberOne() {
119   - // 准备参数:物模型为 BOOL,设备上报数字 1
120   - assertBoolValueConvertedToByte(1, (byte) 1);
121   - }
  114 + public void testSaveDeviceProperty_skipNullValue() {
  115 + // 准备参数:属性值为空,不能写入 TDengine 与 Redis
  116 + IotDeviceDO device = buildDevice();
  117 + IotThingModelDO thingModel = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType());
  118 + Map<String, Object> params = new HashMap<>();
  119 + params.put("Temperature", null);
  120 + IotDeviceMessage message = buildMessage(params);
122 121  
123   - @Test
124   - public void testSaveDeviceProperty_boolFromNumberZero() {
125   - // 准备参数:物模型为 BOOL,设备上报数字 0
126   - assertBoolValueConvertedToByte(0, (byte) 0);
  122 + when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
  123 + .thenReturn(singletonList(thingModel));
  124 +
  125 + assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
  126 +
  127 + verify(thingModelService, never()).convertThingModelPropertyValue(any(), any());
  128 + verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
  129 + verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
127 130 }
128 131  
129   - /**
130   - * 校验 BOOL 类型属性上报后,最终落到 properties Map 的值类型与数值
131   - */
132   - private void assertBoolValueConvertedToByte(Object reportedValue, byte expected) {
133   - // 准备参数
  132 + @Test
  133 + public void testSaveDeviceProperty_skipInvalidKeyType() {
  134 + // 准备参数:Map 中包含非字符串 key,不能因为强转失败影响其它合法属性
134 135 IotDeviceDO device = buildDevice();
135 136 IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType());
136   - Map<String, Object> params = new HashMap<>();
137   - params.put("PowerSwitch", reportedValue);
  137 + Map<Object, Object> params = new HashMap<>();
  138 + params.put(123, 1);
  139 + params.put("PowerSwitch", true);
138 140 IotDeviceMessage message = buildMessage(params);
139 141  
140   - // mock 行为
141 142 when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
142 143 .thenReturn(singletonList(thingModel));
  144 + when(thingModelService.convertThingModelPropertyValue(thingModel, true)).thenReturn((byte) 1);
143 145  
144   - // 调用:不能抛异常
145 146 assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
146 147  
147   - // 断言:写入的 value 是 byte 类型,且值匹配
148 148 Map<String, Object> dbProperties = captureMapperInsertProperties();
149   - Object actual = dbProperties.get("PowerSwitch");
150   - assertTrue(actual instanceof Byte, "BOOL 属性应被转为 Byte 类型,实际为 " + (actual == null ? "null" : actual.getClass()));
151   - assertEquals(expected, actual);
  149 + assertEquals(1, dbProperties.size());
  150 + assertEquals((byte) 1, dbProperties.get("PowerSwitch"));
152 151 }
153 152  
154 153 // ========== 辅助方法 ==========
... ... @@ -173,7 +172,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
173 172 /**
174 173 * 构造一条属性上报消息
175 174 */
176   - private IotDeviceMessage buildMessage(Map<String, Object> params) {
  175 + private IotDeviceMessage buildMessage(Map<?, ?> params) {
177 176 IotDeviceMessage message = new IotDeviceMessage();
178 177 message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
179 178 message.setParams(params);
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/thingmodel/IotThingModelServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.thingmodel;
  2 +
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
  4 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty;
  8 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelBoolOrEnumDataSpecs;
  9 +import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.dataType.ThingModelDateOrTextDataSpecs;
  10 +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
  11 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceModbusPointService;
  12 +import cn.iocoder.yudao.module.iot.service.product.IotProductService;
  13 +import jakarta.annotation.Resource;
  14 +import org.junit.jupiter.api.Test;
  15 +import org.springframework.context.annotation.Import;
  16 +import org.springframework.test.context.bean.override.mockito.MockitoBean;
  17 +
  18 +import java.time.LocalDateTime;
  19 +import java.util.Arrays;
  20 +import java.util.HashMap;
  21 +import java.util.Map;
  22 +
  23 +import static org.junit.jupiter.api.Assertions.*;
  24 +
  25 +/**
  26 + * {@link IotThingModelServiceImpl} 的单元测试
  27 + *
  28 + * @author 芋道源码
  29 + */
  30 +@Import(IotThingModelServiceImpl.class)
  31 +public class IotThingModelServiceImplTest extends BaseDbUnitTest {
  32 +
  33 + @Resource
  34 + private IotThingModelServiceImpl thingModelService;
  35 +
  36 + @MockitoBean
  37 + private IotProductService productService;
  38 + @MockitoBean
  39 + private IotDeviceModbusPointService deviceModbusPointService;
  40 +
  41 + @Test
  42 + public void testConvertThingModelPropertyValue_boolFromBooleanTrue() {
  43 + assertBoolValueConvertedToByte(true, (byte) 1);
  44 + }
  45 +
  46 + @Test
  47 + public void testConvertThingModelPropertyValue_boolFromBooleanFalse() {
  48 + assertBoolValueConvertedToByte(false, (byte) 0);
  49 + }
  50 +
  51 + @Test
  52 + public void testConvertThingModelPropertyValue_boolFromStringTrue() {
  53 + assertBoolValueConvertedToByte("true", (byte) 1);
  54 + }
  55 +
  56 + @Test
  57 + public void testConvertThingModelPropertyValue_boolFromStringFalse() {
  58 + assertBoolValueConvertedToByte("false", (byte) 0);
  59 + }
  60 +
  61 + @Test
  62 + public void testConvertThingModelPropertyValue_boolFromNumberOne() {
  63 + assertBoolValueConvertedToByte(1, (byte) 1);
  64 + }
  65 +
  66 + @Test
  67 + public void testConvertThingModelPropertyValue_boolFromNumberZero() {
  68 + assertBoolValueConvertedToByte(0, (byte) 0);
  69 + }
  70 +
  71 + @Test
  72 + public void testConvertThingModelPropertyValue_boolInvalid() {
  73 + IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType());
  74 +
  75 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "yes");
  76 +
  77 + assertNull(result);
  78 + }
  79 +
  80 + @Test
  81 + public void testConvertThingModelPropertyValue_enumFromStringNumber() {
  82 + IotThingModelDO thingModel = buildEnumThingModel("WorkMode", enumSpec("Auto", 1), enumSpec("Manual", 2));
  83 +
  84 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "1");
  85 +
  86 + assertEquals((byte) 1, result);
  87 + }
  88 +
  89 + @Test
  90 + public void testConvertThingModelPropertyValue_enumFromSpecName() {
  91 + IotThingModelDO thingModel = buildEnumThingModel("WorkMode", enumSpec("Auto", 1), enumSpec("Manual", 2));
  92 +
  93 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "Manual");
  94 +
  95 + assertEquals((byte) 2, result);
  96 + }
  97 +
  98 + @Test
  99 + public void testConvertThingModelPropertyValue_enumInvalidSpecValue() {
  100 + IotThingModelDO thingModel = buildEnumThingModel("WorkMode", enumSpec("Auto", 1), enumSpec("Manual", 2));
  101 +
  102 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, 3);
  103 +
  104 + assertNull(result);
  105 + }
  106 +
  107 + @Test
  108 + public void testConvertThingModelPropertyValue_enumOutOfTinyIntRange() {
  109 + IotThingModelDO thingModel = buildThingModel("WorkMode", IotDataSpecsDataTypeEnum.ENUM.getDataType());
  110 +
  111 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, 128);
  112 +
  113 + assertNull(result);
  114 + }
  115 +
  116 + @Test
  117 + public void testConvertThingModelPropertyValue_dateFromLocalDateTime() {
  118 + IotThingModelDO thingModel = buildThingModel("CollectTime", IotDataSpecsDataTypeEnum.DATE.getDataType());
  119 + LocalDateTime collectTime = LocalDateTime.of(2025, 1, 2, 3, 4, 5);
  120 +
  121 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, collectTime);
  122 +
  123 + assertEquals(LocalDateTimeUtil.toEpochMilli(collectTime), result);
  124 + }
  125 +
  126 + @Test
  127 + public void testConvertThingModelPropertyValue_dateFromString() {
  128 + IotThingModelDO thingModel = buildThingModel("CollectTime", IotDataSpecsDataTypeEnum.DATE.getDataType());
  129 + LocalDateTime collectTime = LocalDateTime.of(2025, 1, 2, 3, 4, 5);
  130 +
  131 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "2025-01-02 03:04:05");
  132 +
  133 + assertEquals(LocalDateTimeUtil.toEpochMilli(collectTime), result);
  134 + }
  135 +
  136 + @Test
  137 + public void testConvertThingModelPropertyValue_intInvalid() {
  138 + IotThingModelDO thingModel = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType());
  139 +
  140 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "abc");
  141 +
  142 + assertNull(result);
  143 + }
  144 +
  145 + @Test
  146 + public void testConvertThingModelPropertyValue_textOverLength() {
  147 + IotThingModelDO thingModel = buildTextThingModel("Remark", 3);
  148 +
  149 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, "abcd");
  150 +
  151 + assertNull(result);
  152 + }
  153 +
  154 + @Test
  155 + public void testConvertThingModelPropertyValue_structToJson() {
  156 + IotThingModelDO thingModel = buildThingModel("GeoLocation", IotDataSpecsDataTypeEnum.STRUCT.getDataType());
  157 + Map<String, Object> value = new HashMap<>();
  158 + value.put("Longitude", 120.1D);
  159 +
  160 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, value);
  161 +
  162 + assertEquals(JsonUtils.toJsonString(value), result);
  163 + }
  164 +
  165 + private void assertBoolValueConvertedToByte(Object reportedValue, byte expected) {
  166 + IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType());
  167 +
  168 + Object result = thingModelService.convertThingModelPropertyValue(thingModel, reportedValue);
  169 +
  170 + assertEquals(expected, result);
  171 + }
  172 +
  173 + private IotThingModelDO buildThingModel(String identifier, String dataType) {
  174 + ThingModelProperty property = new ThingModelProperty();
  175 + property.setIdentifier(identifier);
  176 + property.setDataType(dataType);
  177 + return IotThingModelDO.builder().identifier(identifier).property(property).build();
  178 + }
  179 +
  180 + private IotThingModelDO buildEnumThingModel(String identifier, ThingModelBoolOrEnumDataSpecs... dataSpecsList) {
  181 + IotThingModelDO thingModel = buildThingModel(identifier, IotDataSpecsDataTypeEnum.ENUM.getDataType());
  182 + thingModel.getProperty().setDataSpecsList(Arrays.asList(dataSpecsList));
  183 + return thingModel;
  184 + }
  185 +
  186 + private ThingModelBoolOrEnumDataSpecs enumSpec(String name, Integer value) {
  187 + ThingModelBoolOrEnumDataSpecs dataSpecs = new ThingModelBoolOrEnumDataSpecs();
  188 + dataSpecs.setName(name);
  189 + dataSpecs.setValue(value);
  190 + return dataSpecs;
  191 + }
  192 +
  193 + private IotThingModelDO buildTextThingModel(String identifier, Integer length) {
  194 + IotThingModelDO thingModel = buildThingModel(identifier, IotDataSpecsDataTypeEnum.TEXT.getDataType());
  195 + ThingModelDateOrTextDataSpecs dataSpecs = new ThingModelDateOrTextDataSpecs();
  196 + dataSpecs.setLength(length);
  197 + thingModel.getProperty().setDataSpecs(dataSpecs);
  198 + return thingModel;
  199 + }
  200 +
  201 +}
... ...