# Conflicts:
#	README.md
This commit is contained in:
YunaiV
2026-06-27 15:33:01 -07:00
11 changed files with 423 additions and 334 deletions
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -57,10 +57,12 @@ docker load -i dm8_20240715_x86_rh6_rq_single.tar
```Bash ```Bash
docker compose up -d dm8 docker compose up -d dm8
# 注意:启动完 dm 后,需要手动再执行如下命令,因为 dm 不支持初始化脚本 # 注意:启动完 dm 后,需要手动再执行如下命令,因为 dm 不支持初始化脚本
docker compose exec dm8 bash -c '/opt/dmdbms/bin/disql SYSDBA/SYSDBA001 \`/tmp/schema.sql' docker compose exec dm8 bash -c 'printf "SET DEFINE OFF;\n" > /tmp/schema-with-define-off.sql && cat /tmp/schema.sql >> /tmp/schema-with-define-off.sql && /opt/dmdbms/bin/disql SYSDBA/SYSDBA001 \`/tmp/schema-with-define-off.sql'
exit exit
``` ```
> 注意:项目 DM8 脚本使用 `varchar(n char)` 保持和 MySQL `varchar(n)` 一致的字符长度语义,建议初始化 DM8 时使用 `PAGE_SIZE=16`、`UNICODE_FLAG=1`。`sql/tools/docker-compose.yaml` 已按该配置提供示例。使用 `disql` 导入时需要先执行 `SET DEFINE OFF;`,避免数据里的 `&` 被当作变量替换。
### 1.6 KingbaseES 人大金仓 ### 1.6 KingbaseES 人大金仓
① 下载人大金仓 Docker 镜像: ① 下载人大金仓 Docker 镜像:
+3 -1
View File
@@ -871,7 +871,9 @@ class DM8Convertor(Convertor):
type = type.lower() type = type.lower()
if type == "varchar": if type == "varchar":
return f"varchar({size})" # MySQL varchar(n) is character-oriented. DM8 may treat varchar(n)
# as bytes, so use explicit CHAR semantics for generated scripts.
return f"varchar({size} char)"
if type in ("int", "int unsigned"): if type in ("int", "int unsigned"):
return "int" return "int"
if type in ("bigint", "bigint unsigned"): if type in ("bigint", "bigint unsigned"):
@@ -196,6 +196,7 @@ public class BpmProcessInstanceController {
@GetMapping("/get-bpmn-model-view") @GetMapping("/get-bpmn-model-view")
@Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用") @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用")
@Parameter(name = "id", description = "流程实例的编号", required = true) @Parameter(name = "id", description = "流程实例的编号", required = true)
@PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
public CommonResult<BpmProcessInstanceBpmnModelViewRespVO> getProcessInstanceBpmnModelView( public CommonResult<BpmProcessInstanceBpmnModelViewRespVO> getProcessInstanceBpmnModelView(
@RequestParam(value = "id") String id) { @RequestParam(value = "id") String id) {
return success(processInstanceService.getProcessInstanceBpmnModelView(id)); return success(processInstanceService.getProcessInstanceBpmnModelView(id));
@@ -1,9 +1,12 @@
package cn.iocoder.yudao.module.iot.framework.tdengine.core; package cn.iocoder.yudao.module.iot.framework.tdengine.core;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.util.Locale;
/** /**
* TDEngine 表字段 * TDEngine 表字段
*/ */
@@ -61,4 +64,15 @@ public class TDengineTableField {
this.type = type; this.type = type;
} }
/**
* 构建字段名:TDengine 默认会将字段名转为小写,需要和建表、写入、查询保持一致。
* 例如:PT -> pt,PfT -> pf_t。
*/
public static String buildFieldName(String field) {
if (StrUtil.isBlank(field)) {
return field;
}
return StrUtil.toUnderlineCase(field).toLowerCase(Locale.ROOT);
}
} }
@@ -118,7 +118,7 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
private List<TDengineTableField> buildTableFieldList(List<IotThingModelDO> thingModels) { private List<TDengineTableField> buildTableFieldList(List<IotThingModelDO> thingModels) {
return convertList(thingModels, thingModel -> { return convertList(thingModels, thingModel -> {
TDengineTableField field = new TDengineTableField( TDengineTableField field = new TDengineTableField(
StrUtil.toUnderlineCase(thingModel.getIdentifier()), // TDengine 字段默认都是小写 TDengineTableField.buildFieldName(thingModel.getIdentifier()),
TYPE_MAPPING.get(thingModel.getProperty().getDataType())); TYPE_MAPPING.get(thingModel.getProperty().getDataType()));
String dataType = thingModel.getProperty().getDataType(); String dataType = thingModel.getProperty().getDataType();
if (Objects.equals(dataType, IotDataSpecsDataTypeEnum.TEXT.getDataType())) { if (Objects.equals(dataType, IotDataSpecsDataTypeEnum.TEXT.getDataType())) {
@@ -51,7 +51,7 @@
TAGS ('${device.id}') TAGS ('${device.id}')
(ts, report_time, (ts, report_time,
<foreach item="key" collection="properties.keys" separator=","> <foreach item="key" collection="properties.keys" separator=",">
${@cn.hutool.core.util.StrUtil@toUnderlineCase(key)} ${@cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField@buildFieldName(key)}
</foreach> </foreach>
) )
VALUES VALUES
@@ -68,12 +68,12 @@
<select id="selectListByHistory" <select id="selectListByHistory"
resultType="cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO"> resultType="cn.iocoder.yudao.module.iot.controller.admin.device.vo.property.IotDevicePropertyRespVO">
SELECT ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} AS `value`, ts AS update_time SELECT ${@cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField@buildFieldName(reqVO.identifier)} AS `value`, ts AS update_time
FROM device_property_${reqVO.deviceId} FROM device_property_${reqVO.deviceId}
WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL WHERE ${@cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField@buildFieldName(reqVO.identifier)} IS NOT NULL
AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])} AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])}
AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])} AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])}
ORDER BY ts DESC ORDER BY ts DESC
</select> </select>
</mapper> </mapper>
@@ -0,0 +1,27 @@
package cn.iocoder.yudao.module.iot.framework.tdengine.core;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
public class TDengineTableFieldTest {
@Test
public void testBuildFieldName() {
assertEquals("ua", TDengineTableField.buildFieldName("Ua"));
assertEquals("pf_t", TDengineTableField.buildFieldName("PfT"));
assertEquals("pt", TDengineTableField.buildFieldName("PT"));
assertEquals("pa", TDengineTableField.buildFieldName("PA"));
assertEquals("geo_location", TDengineTableField.buildFieldName("GeoLocation"));
assertEquals("light_status", TDengineTableField.buildFieldName("LightStatus"));
}
@Test
public void testBuildFieldName_blank() {
assertNull(TDengineTableField.buildFieldName(null));
assertEquals("", TDengineTableField.buildFieldName(""));
assertEquals(" ", TDengineTableField.buildFieldName(" "));
}
}
@@ -5,11 +5,15 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 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.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; 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.IotThingModelDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.model.ThingModelProperty; 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.redis.device.DevicePropertyRedisDAO;
import cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper; 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.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 cn.iocoder.yudao.module.iot.service.thingmodel.IotThingModelService;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@@ -17,8 +21,12 @@ import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -38,6 +46,8 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
@Mock @Mock
private IotThingModelService thingModelService; private IotThingModelService thingModelService;
@Mock @Mock
private IotProductService productService;
@Mock
private IotDevicePropertyMapper devicePropertyMapper; private IotDevicePropertyMapper devicePropertyMapper;
@Mock @Mock
private DevicePropertyRedisDAO deviceDataRedisDAO; private DevicePropertyRedisDAO deviceDataRedisDAO;
@@ -100,12 +110,15 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
params.put("Temperature", "abc"); params.put("Temperature", "abc");
IotDeviceMessage message = buildMessage(params); IotDeviceMessage message = buildMessage(params);
// mock 方法
when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
.thenReturn(singletonList(temperature)); .thenReturn(singletonList(temperature));
when(thingModelService.convertThingModelPropertyValue(temperature, "abc")).thenReturn(null); when(thingModelService.convertThingModelPropertyValue(temperature, "abc")).thenReturn(null);
// 调用,并断言:不会抛出异常
assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
// 断言:没有合法属性,不会写入 TDengine 与 Redis
verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong()); verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
verify(deviceDataRedisDAO, never()).putAll(anyLong(), any()); verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
} }
@@ -119,11 +132,14 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
params.put("Temperature", null); params.put("Temperature", null);
IotDeviceMessage message = buildMessage(params); IotDeviceMessage message = buildMessage(params);
// mock 方法
when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
.thenReturn(singletonList(thingModel)); .thenReturn(singletonList(thingModel));
// 调用,并断言:不会抛出异常
assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
// 断言:跳过空值,不会转换属性值,也不会写入 TDengine 与 Redis
verify(thingModelService, never()).convertThingModelPropertyValue(any(), any()); verify(thingModelService, never()).convertThingModelPropertyValue(any(), any());
verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong()); verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
verify(deviceDataRedisDAO, never()).putAll(anyLong(), any()); verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
@@ -139,17 +155,48 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
params.put("PowerSwitch", true); params.put("PowerSwitch", true);
IotDeviceMessage message = buildMessage(params); IotDeviceMessage message = buildMessage(params);
// mock 方法
when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId())) when(thingModelService.getThingModelListByProductIdFromCache(device.getProductId()))
.thenReturn(singletonList(thingModel)); .thenReturn(singletonList(thingModel));
when(thingModelService.convertThingModelPropertyValue(thingModel, true)).thenReturn((byte) 1); when(thingModelService.convertThingModelPropertyValue(thingModel, true)).thenReturn((byte) 1);
// 调用,并断言:非字符串 key 不影响其它合法属性
assertDoesNotThrow(() -> service.saveDeviceProperty(device, message)); assertDoesNotThrow(() -> service.saveDeviceProperty(device, message));
// 断言:只写入合法属性
Map<String, Object> dbProperties = captureMapperInsertProperties(); Map<String, Object> dbProperties = captureMapperInsertProperties();
assertEquals(1, dbProperties.size()); assertEquals(1, dbProperties.size());
assertEquals((byte) 1, dbProperties.get("PowerSwitch")); 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()));
}
// ========== 辅助方法 ========== // ========== 辅助方法 ==========
/** /**
@@ -43,7 +43,7 @@ public class SubscribeHandler implements WxMpMessageHandler {
} catch (WxErrorException e) { } catch (WxErrorException e) {
log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e); log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e);
// 特殊情况(个人账号,无接口权限):https://t.zsxq.com/cLFq5 // 特殊情况(个人账号,无接口权限):https://t.zsxq.com/cLFq5
if (ObjUtil.equal(e.getError().getErrorCode(), WxMpErrorMsgEnum.CODE_48001)) { if (ObjUtil.equal(e.getError().getErrorCode(), WxMpErrorMsgEnum.CODE_48001.getCode())) {
wxMpUser = new WxMpUser(); wxMpUser = new WxMpUser();
wxMpUser.setOpenId(wxMessage.getFromUser()); wxMpUser.setOpenId(wxMessage.getFromUser());
wxMpUser.setSubscribe(true); wxMpUser.setSubscribe(true);
@@ -14,7 +14,6 @@ import cn.iocoder.yudao.module.wms.dal.dataobject.md.item.WmsItemSkuDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -100,11 +99,8 @@ public interface WmsInventoryMapper extends BaseMapperX<WmsInventoryDO> {
if (CollUtil.isEmpty(ids)) { if (CollUtil.isEmpty(ids)) {
return Collections.emptyList(); return Collections.emptyList();
} }
List<Long> sortedIds = new ArrayList<>(ids);
Collections.sort(sortedIds);
return selectList(new LambdaQueryWrapperX<WmsInventoryDO>() return selectList(new LambdaQueryWrapperX<WmsInventoryDO>()
.in(WmsInventoryDO::getId, sortedIds) .in(WmsInventoryDO::getId, ids)
.orderByAsc(WmsInventoryDO::getId)
.last("FOR UPDATE")); .last("FOR UPDATE"));
} }