Commit 7e35f1dab9c7bb49d6637cb929c85a42acd26c30

Authored by YunaiV
1 parent 9705ce11

fix(iot): 统一 TDengine 设备属性字段名规范化

- 新增 TDengineTableField.buildFieldName,统一转换为小写下划线字段名
- 建表、写入、历史查询统一使用同一字段名生成规则
- 修复 PT/PA 等全大写 identifier 与 TDengine 小写字段比较不一致的问题
- 补充字段名转换契约测试和设备属性表结构同步测试
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/tdengine/core/TDengineTableField.java
1 1 package cn.iocoder.yudao.module.iot.framework.tdengine.core;
2 2  
  3 +import cn.hutool.core.util.StrUtil;
3 4 import lombok.AllArgsConstructor;
4 5 import lombok.Data;
5 6 import lombok.NoArgsConstructor;
6 7  
  8 +import java.util.Locale;
  9 +
7 10 /**
8 11 * TDEngine 表字段
9 12 */
... ... @@ -61,4 +64,15 @@ public class TDengineTableField {
61 64 this.type = type;
62 65 }
63 66  
  67 + /**
  68 + * 构建字段名:TDengine 默认会将字段名转为小写,需要和建表、写入、查询保持一致。
  69 + * 例如:PT -> pt,PfT -> pf_t。
  70 + */
  71 + public static String buildFieldName(String field) {
  72 + if (StrUtil.isBlank(field)) {
  73 + return field;
  74 + }
  75 + return StrUtil.toUnderlineCase(field).toLowerCase(Locale.ROOT);
  76 + }
  77 +
64 78 }
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImpl.java
... ... @@ -118,7 +118,7 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
118 118 private List<TDengineTableField> buildTableFieldList(List<IotThingModelDO> thingModels) {
119 119 return convertList(thingModels, thingModel -> {
120 120 TDengineTableField field = new TDengineTableField(
121   - StrUtil.toUnderlineCase(thingModel.getIdentifier()), // TDengine 字段默认都是小写
  121 + TDengineTableField.buildFieldName(thingModel.getIdentifier()),
122 122 TYPE_MAPPING.get(thingModel.getProperty().getDataType()));
123 123 String dataType = thingModel.getProperty().getDataType();
124 124 if (Objects.equals(dataType, IotDataSpecsDataTypeEnum.TEXT.getDataType())) {
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml
... ... @@ -51,7 +51,7 @@
51 51 TAGS ('${device.id}')
52 52 (ts, report_time,
53 53 <foreach item="key" collection="properties.keys" separator=",">
54   - ${@cn.hutool.core.util.StrUtil@toUnderlineCase(key)}
  54 + ${@cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField@buildFieldName(key)}
55 55 </foreach>
56 56 )
57 57 VALUES
... ... @@ -68,12 +68,12 @@
68 68  
69 69 <select id="selectListByHistory"
70 70 resultType="cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO">
71   - SELECT ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} AS `value`, ts AS update_time
  71 + SELECT ${@cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField@buildFieldName(reqVO.identifier)} AS `value`, ts AS update_time
72 72 FROM device_property_${reqVO.deviceId}
73   - WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL
  73 + WHERE ${@cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField@buildFieldName(reqVO.identifier)} IS NOT NULL
74 74 AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])}
75 75 AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])}
76 76 ORDER BY ts DESC
77 77 </select>
78 78  
79   -</mapper>
80 79 \ No newline at end of file
  80 +</mapper>
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java
... ... @@ -5,11 +5,15 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
7 7 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
  8 +import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
8 9 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
9 10 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty;
10 11 import cn.iocoder.yudao.module.iot.dal.redis.device.DevicePropertyRedisDAO;
11 12 import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper;
12 13 import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
  14 +import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
  15 +import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
  16 +import cn.iocoder.yudao.module.iot.service.product.IotProductService;
13 17 import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
14 18 import org.junit.jupiter.api.Test;
15 19 import org.mockito.ArgumentCaptor;
... ... @@ -17,8 +21,12 @@ import org.mockito.InjectMocks;
17 21 import org.mockito.Mock;
18 22  
19 23 import java.time.LocalDateTime;
  24 +import java.util.Arrays;
  25 +import java.util.Collections;
20 26 import java.util.HashMap;
  27 +import java.util.List;
21 28 import java.util.Map;
  29 +import java.util.stream.Collectors;
22 30  
23 31 import static java.util.Collections.singletonList;
24 32 import static org.junit.jupiter.api.Assertions.*;
... ... @@ -38,6 +46,8 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
38 46 @Mock
39 47 private IotThingModelService thingModelService;
40 48 @Mock
  49 + private IotProductService productService;
  50 + @Mock
41 51 private IotDevicePropertyMapper devicePropertyMapper;
42 52 @Mock
43 53 private DevicePropertyRedisDAO deviceDataRedisDAO;
... ... @@ -100,12 +110,15 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
100 110 params.put("Temperature", "abc");
101 111 IotDeviceMessage message = buildMessage(params);
102 112  
  113 + // mock 方法
103 114 when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
104 115 .thenReturn(singletonList(temperature));
105 116 when(thingModelService.convertThingModelPropertyValue(temperature, "abc")).thenReturn(null);
106 117  
  118 + // 调用,并断言:不会抛出异常
107 119 assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
108 120  
  121 + // 断言:没有合法属性,不会写入 TDengine 与 Redis
109 122 verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
110 123 verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
111 124 }
... ... @@ -119,11 +132,14 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
119 132 params.put("Temperature", null);
120 133 IotDeviceMessage message = buildMessage(params);
121 134  
  135 + // mock 方法
122 136 when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
123 137 .thenReturn(singletonList(thingModel));
124 138  
  139 + // 调用,并断言:不会抛出异常
125 140 assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
126 141  
  142 + // 断言:跳过空值,不会转换属性值,也不会写入 TDengine 与 Redis
127 143 verify(thingModelService, never()).convertThingModelPropertyValue(any(), any());
128 144 verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
129 145 verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
... ... @@ -139,17 +155,48 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
139 155 params.put("PowerSwitch", true);
140 156 IotDeviceMessage message = buildMessage(params);
141 157  
  158 + // mock 方法
142 159 when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
143 160 .thenReturn(singletonList(thingModel));
144 161 when(thingModelService.convertThingModelPropertyValue(thingModel, true)).thenReturn((byte) 1);
145 162  
  163 + // 调用,并断言:非字符串 key 不影响其它合法属性
146 164 assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
147 165  
  166 + // 断言:只写入合法属性
148 167 Map<String, Object> dbProperties = captureMapperInsertProperties();
149 168 assertEquals(1, dbProperties.size());
150 169 assertEquals((byte) 1, dbProperties.get("PowerSwitch"));
151 170 }
152 171  
  172 + @Test
  173 + public void testDefineDevicePropertyData_fieldNameToLowerCase() {
  174 + // 准备参数:全大写缩写和驼峰缩写都需要转换为 TDengine 实际的小写字段名
  175 + Long productId = 2L;
  176 + IotProductDO product = IotProductDO.builder().id(productId).build();
  177 + List<IotThingModelDO> thingModels = Arrays.asList(
  178 + buildThingModel("Ua", IotDataSpecsDataTypeEnum.FLOAT.getDataType()),
  179 + buildThingModel("PfT", IotDataSpecsDataTypeEnum.FLOAT.getDataType()),
  180 + buildThingModel("PT", IotDataSpecsDataTypeEnum.FLOAT.getDataType()),
  181 + buildThingModel("PA", IotDataSpecsDataTypeEnum.FLOAT.getDataType()));
  182 + thingModels.forEach(thingModel -> thingModel.setType(IotThingModelTypeEnum.PROPERTY.getType()));
  183 +
  184 + // mock 方法
  185 + when(productService.validateProductExists(productId)).thenReturn(product);
  186 + when(thingModelService.getThingModelListByProductId(productId)).thenReturn(thingModels);
  187 + when(devicePropertyMapper.getProductPropertySTableFieldList(productId)).thenReturn(Collections.emptyList());
  188 +
  189 + // 调用
  190 + service.defineDevicePropertyData(productId);
  191 +
  192 + // 断言:字段名统一为小写下划线,避免 PT 和数据库中的 pt 被误判为不同字段
  193 + ArgumentCaptor<List<TDengineTableField>> captor = ArgumentCaptor.forClass(List.class);
  194 + verify(devicePropertyMapper).createProductPropertySTable(eq(productId), captor.capture());
  195 + assertEquals(Arrays.asList("ua", "pf_t", "pt", "pa"), captor.getValue().stream()
  196 + .map(TDengineTableField::getField)
  197 + .collect(Collectors.toList()));
  198 + }
  199 +
153 200 // ========== 辅助方法 ==========
154 201  
155 202 /**
... ...