2026-06-30 24681c81c09022f584a57006f2534b5f74723414
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.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
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.ThingModelProperty;
import cn.iocoder.yudao.module.iot.dal.redis.device.DevicePropertyRedisDAO;
import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotDataSpecsDataTypeEnum;
import cn.iocoder.yudao.module.iot.enums.thingmodel.IotThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -17,8 +21,12 @@
import org.mockito.Mock;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*;
@@ -38,6 +46,8 @@
    @Mock
    private IotThingModelService thingModelService;
    @Mock
    private IotProductService productService;
    @Mock
    private IotDevicePropertyMapper devicePropertyMapper;
    @Mock
    private DevicePropertyRedisDAO deviceDataRedisDAO;
@@ -54,6 +64,7 @@
        // mock 行为
        when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
                .thenReturn(singletonList(thingModel));
        when(thingModelService.convertThingModelPropertyValue(thingModel, 100)).thenReturn(100);
        // 调用
        service.saveDeviceProperty(device, message);
@@ -91,64 +102,99 @@
    }
    @Test
    public void testSaveDeviceProperty_boolFromBooleanTrue() {
        // 准备参数:物模型为 BOOL,设备上报原生 boolean true
        assertBoolValueConvertedToByte(true, (byte) 1);
    }
    @Test
    public void testSaveDeviceProperty_boolFromBooleanFalse() {
        // 准备参数:物模型为 BOOL,设备上报原生 boolean false
        assertBoolValueConvertedToByte(false, (byte) 0);
    }
    @Test
    public void testSaveDeviceProperty_boolFromStringTrue() {
        // 准备参数:物模型为 BOOL,设备上报字符串 "true"
        assertBoolValueConvertedToByte("true", (byte) 1);
    }
    @Test
    public void testSaveDeviceProperty_boolFromStringFalse() {
        // 准备参数:物模型为 BOOL,设备上报字符串 "false"
        assertBoolValueConvertedToByte("false", (byte) 0);
    }
    @Test
    public void testSaveDeviceProperty_boolFromNumberOne() {
        // 准备参数:物模型为 BOOL,设备上报数字 1
        assertBoolValueConvertedToByte(1, (byte) 1);
    }
    @Test
    public void testSaveDeviceProperty_boolFromNumberZero() {
        // 准备参数:物模型为 BOOL,设备上报数字 0
        assertBoolValueConvertedToByte(0, (byte) 0);
    }
    /**
     * 校验 BOOL 类型属性上报后,最终落到 properties Map 的值类型与数值
     */
    private void assertBoolValueConvertedToByte(Object reportedValue, byte expected) {
        // 准备参数
    public void testSaveDeviceProperty_convertValueFailed() {
        // 准备参数:物模型存在,但是属性值无法按物模型转换
        IotDeviceDO device = buildDevice();
        IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType());
        IotThingModelDO temperature = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType());
        Map<String, Object> params = new HashMap<>();
        params.put("PowerSwitch", reportedValue);
        params.put("Temperature", "abc");
        IotDeviceMessage message = buildMessage(params);
        // mock 行为
        // mock 方法
        when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
                .thenReturn(singletonList(temperature));
        when(thingModelService.convertThingModelPropertyValue(temperature, "abc")).thenReturn(null);
        // 调用,并断言:不会抛出异常
        assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
        // 断言:没有合法属性,不会写入 TDengine 与 Redis
        verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
        verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
    }
    @Test
    public void testSaveDeviceProperty_skipNullValue() {
        // 准备参数:属性值为空,不能写入 TDengine 与 Redis
        IotDeviceDO device = buildDevice();
        IotThingModelDO thingModel = buildThingModel("Temperature", IotDataSpecsDataTypeEnum.INT.getDataType());
        Map<String, Object> params = new HashMap<>();
        params.put("Temperature", null);
        IotDeviceMessage message = buildMessage(params);
        // mock 方法
        when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
                .thenReturn(singletonList(thingModel));
        // 调用:不能抛异常
        // 调用,并断言:不会抛出异常
        assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
        // 断言:写入的 value 是 byte 类型,且值匹配
        // 断言:跳过空值,不会转换属性值,也不会写入 TDengine 与 Redis
        verify(thingModelService, never()).convertThingModelPropertyValue(any(), any());
        verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
        verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
    }
    @Test
    public void testSaveDeviceProperty_skipInvalidKeyType() {
        // 准备参数:Map 中包含非字符串 key,不能因为强转失败影响其它合法属性
        IotDeviceDO device = buildDevice();
        IotThingModelDO thingModel = buildThingModel("PowerSwitch", IotDataSpecsDataTypeEnum.BOOL.getDataType());
        Map<Object, Object> 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);
        // 调用,并断言:非字符串 key 不影响其它合法属性
        assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
        // 断言:只写入合法属性
        Map<String, Object> 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"));
    }
    @Test
    public void testDefineDevicePropertyData_fieldNameToLowerCase() {
        // 准备参数:全大写缩写和驼峰缩写都需要转换为 TDengine 实际的小写字段名
        Long productId = 2L;
        IotProductDO product = IotProductDO.builder().id(productId).build();
        List<IotThingModelDO> thingModels = Arrays.asList(
                buildThingModel("Ua", IotDataSpecsDataTypeEnum.FLOAT.getDataType()),
                buildThingModel("PfT", IotDataSpecsDataTypeEnum.FLOAT.getDataType()),
                buildThingModel("PT", IotDataSpecsDataTypeEnum.FLOAT.getDataType()),
                buildThingModel("PA", IotDataSpecsDataTypeEnum.FLOAT.getDataType()));
        thingModels.forEach(thingModel -> thingModel.setType(IotThingModelTypeEnum.PROPERTY.getType()));
        // mock 方法
        when(productService.validateProductExists(productId)).thenReturn(product);
        when(thingModelService.getThingModelListByProductId(productId)).thenReturn(thingModels);
        when(devicePropertyMapper.getProductPropertySTableFieldList(productId)).thenReturn(Collections.emptyList());
        // 调用
        service.defineDevicePropertyData(productId);
        // 断言:字段名统一为小写下划线,避免 PT 和数据库中的 pt 被误判为不同字段
        ArgumentCaptor<List<TDengineTableField>> captor = ArgumentCaptor.forClass(List.class);
        verify(devicePropertyMapper).createProductPropertySTable(eq(productId), captor.capture());
        assertEquals(Arrays.asList("ua", "pf_t", "pt", "pa"), captor.getValue().stream()
                .map(TDengineTableField::getField)
                .collect(Collectors.toList()));
    }
    // ========== 辅助方法 ==========
@@ -173,7 +219,7 @@
    /**
     * 构造一条属性上报消息
     */
    private IotDeviceMessage buildMessage(Map<String, Object> params) {
    private IotDeviceMessage buildMessage(Map<?, ?> params) {
        IotDeviceMessage message = new IotDeviceMessage();
        message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
        message.setParams(params);