reactor:【IoT 物联网】调整物模型相关的时序表,通过 productId 和 deviceId 区分

This commit is contained in:
YunaiV 2025-06-12 09:06:10 +08:00
parent c3499af524
commit b37814ec9c
9 changed files with 48 additions and 86 deletions

View File

@ -20,12 +20,6 @@ public class IotDevicePropertyHistoryPageReqVO extends PageParam {
@NotNull(message = "设备编号不能为空") @NotNull(message = "设备编号不能为空")
private Long deviceId; private Long deviceId;
@Schema(description = "产品 Key", hidden = true)
private String productKey; // 非前端传递后端自己查询设置
@Schema(description = "设备名称", hidden = true)
private String deviceName; // 非前端传递后端自己查询设置
@Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "属性标识符", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "属性标识符不能为空") @NotEmpty(message = "属性标识符不能为空")
private String identifier; private String identifier;

View File

@ -50,10 +50,6 @@ public interface IotThingModelMapper extends BaseMapperX<IotThingModelDO> {
return selectList(IotThingModelDO::getProductId, productId); return selectList(IotThingModelDO::getProductId, productId);
} }
default List<IotThingModelDO> selectListByProductKey(String productKey) {
return selectList(IotThingModelDO::getProductKey, productKey);
}
default List<IotThingModelDO> selectListByProductIdAndType(Long productId, Integer type) { default List<IotThingModelDO> selectListByProductIdAndType(Long productId, Integer type) {
return selectList(IotThingModelDO::getProductId, productId, return selectList(IotThingModelDO::getProductId, productId,
IotThingModelDO::getType, type); IotThingModelDO::getType, type);

View File

@ -9,15 +9,14 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
*/ */
public interface RedisKeyConstants { public interface RedisKeyConstants {
// TODO @芋艿弱化 deviceKey使用 product_key + device_name 替代
/** /**
* 设备属性的数据缓存采用 HASH 结构 * 设备属性的数据缓存采用 HASH 结构
* <p> * <p>
* KEY 格式device_property:{deviceKey} * KEY 格式device_property:{deviceId}
* HASH KEYidentifier 属性标识 * HASH KEYidentifier 属性标识
* VALUE 数据类型String(JSON) {@link IotDevicePropertyDO} * VALUE 数据类型String(JSON) {@link IotDevicePropertyDO}
*/ */
String DEVICE_PROPERTY = "iot:device_property:%s"; String DEVICE_PROPERTY = "iot:device_property:%d";
/** /**
* 设备的最后上报时间采用 ZSET 结构 * 设备的最后上报时间采用 ZSET 结构
@ -31,7 +30,7 @@ public interface RedisKeyConstants {
* 设备关联的网关 serverId 缓存采用 HASH 结构 * 设备关联的网关 serverId 缓存采用 HASH 结构
* *
* KEY 格式device_server_id * KEY 格式device_server_id
* HASH KEY{productKey},{deviceName} * HASH KEY{deviceId}
* VALUE 数据类型String serverId * VALUE 数据类型String serverId
*/ */
String DEVICE_SERVER_ID = "iot:device_server_id"; String DEVICE_SERVER_ID = "iot:device_server_id";
@ -56,7 +55,7 @@ public interface RedisKeyConstants {
/** /**
* 物模型的数据缓存使用 Spring Cache 操作忽略租户 * 物模型的数据缓存使用 Spring Cache 操作忽略租户
* *
* KEY 格式thing_model_${productKey} * KEY 格式thing_model_${productId}
* VALUE 数据类型String 数组(JSON) {@link cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO} 列表 * VALUE 数据类型String 数组(JSON) {@link cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO} 列表
*/ */
String THING_MODEL_LIST = "iot:thing_model_list"; String THING_MODEL_LIST = "iot:thing_model_list";

View File

@ -22,8 +22,8 @@ public class DevicePropertyRedisDAO {
@Resource @Resource
private StringRedisTemplate stringRedisTemplate; private StringRedisTemplate stringRedisTemplate;
public Map<String, IotDevicePropertyDO> get(String productKey, String deviceName) { public Map<String, IotDevicePropertyDO> get(Long id) {
String redisKey = formatKey(productKey, deviceName); String redisKey = formatKey(id);
Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(redisKey); Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries(redisKey);
if (CollUtil.isEmpty(entries)) { if (CollUtil.isEmpty(entries)) {
return Collections.emptyMap(); return Collections.emptyMap();
@ -33,18 +33,18 @@ public class DevicePropertyRedisDAO {
entry -> JsonUtils.parseObject((String) entry.getValue(), IotDevicePropertyDO.class)); entry -> JsonUtils.parseObject((String) entry.getValue(), IotDevicePropertyDO.class));
} }
public void putAll(String productKey, String deviceName, Map<String, IotDevicePropertyDO> properties) { public void putAll(Long id, Map<String, IotDevicePropertyDO> properties) {
if (CollUtil.isEmpty(properties)) { if (CollUtil.isEmpty(properties)) {
return; return;
} }
String redisKey = formatKey(productKey, deviceName); String redisKey = formatKey(id);
stringRedisTemplate.opsForHash().putAll(redisKey, convertMap(properties.entrySet(), stringRedisTemplate.opsForHash().putAll(redisKey, convertMap(properties.entrySet(),
Map.Entry::getKey, Map.Entry::getKey,
entry -> JsonUtils.toJsonString(entry.getValue()))); entry -> JsonUtils.toJsonString(entry.getValue())));
} }
private static String formatKey(String productKey, String deviceName) { private static String formatKey(Long id) {
return String.format(DEVICE_PROPERTY, productKey, deviceName); return String.format(DEVICE_PROPERTY, id);
} }
} }

View File

@ -23,17 +23,17 @@ import java.util.stream.Collectors;
@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析因为 JSqlParser TDengine SQL 解析会报错 @InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析因为 JSqlParser TDengine SQL 解析会报错
public interface IotDevicePropertyMapper { public interface IotDevicePropertyMapper {
List<TDengineTableField> getProductPropertySTableFieldList(@Param("productKey") String productKey); List<TDengineTableField> getProductPropertySTableFieldList(@Param("productId") Long productId);
void createProductPropertySTable(@Param("productKey") String productKey, void createProductPropertySTable(@Param("productId") Long productId,
@Param("fields") List<TDengineTableField> fields); @Param("fields") List<TDengineTableField> fields);
@SuppressWarnings("SimplifyStreamApiCallChains") // 保持 JDK8 兼容性 @SuppressWarnings("SimplifyStreamApiCallChains") // 保持 JDK8 兼容性
default void alterProductPropertySTable(String productKey, default void alterProductPropertySTable(Long productId,
List<TDengineTableField> oldFields, List<TDengineTableField> oldFields,
List<TDengineTableField> newFields) { List<TDengineTableField> newFields) {
oldFields.removeIf(field -> StrUtil.equalsAny(field.getField(), oldFields.removeIf(field -> StrUtil.equalsAny(field.getField(),
TDengineTableField.FIELD_TS, "report_time", "device_name")); TDengineTableField.FIELD_TS, "report_time"));
List<TDengineTableField> addFields = newFields.stream().filter( // 新增的字段 List<TDengineTableField> addFields = newFields.stream().filter( // 新增的字段
newField -> oldFields.stream().noneMatch(oldField -> oldField.getField().equals(newField.getField()))) newField -> oldFields.stream().noneMatch(oldField -> oldField.getField().equals(newField.getField())))
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -62,22 +62,22 @@ public interface IotDevicePropertyMapper {
}); });
// 执行 // 执行
addFields.forEach(field -> alterProductPropertySTableAddField(productKey, field)); addFields.forEach(field -> alterProductPropertySTableAddField(productId, field));
dropFields.forEach(field -> alterProductPropertySTableDropField(productKey, field)); dropFields.forEach(field -> alterProductPropertySTableDropField(productId, field));
modifyLengthFields.forEach(field -> alterProductPropertySTableModifyField(productKey, field)); modifyLengthFields.forEach(field -> alterProductPropertySTableModifyField(productId, field));
modifyTypeFields.forEach(field -> { modifyTypeFields.forEach(field -> {
alterProductPropertySTableDropField(productKey, field); alterProductPropertySTableDropField(productId, field);
alterProductPropertySTableAddField(productKey, field); alterProductPropertySTableAddField(productId, field);
}); });
} }
void alterProductPropertySTableAddField(@Param("productKey") String productKey, void alterProductPropertySTableAddField(@Param("productId") Long productId,
@Param("field") TDengineTableField field); @Param("field") TDengineTableField field);
void alterProductPropertySTableModifyField(@Param("productKey") String productKey, void alterProductPropertySTableModifyField(@Param("productId") Long productId,
@Param("field") TDengineTableField field); @Param("field") TDengineTableField field);
void alterProductPropertySTableDropField(@Param("productKey") String productKey, void alterProductPropertySTableDropField(@Param("productId") Long productId,
@Param("field") TDengineTableField field); @Param("field") TDengineTableField field);
void insert(@Param("device") IotDeviceDO device, void insert(@Param("device") IotDeviceDO device,

View File

@ -20,7 +20,6 @@ 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.enums.thingmodel.IotThingModelTypeEnum;
import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField; import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.product.IotProductService; 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 com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
@ -59,8 +58,6 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
.put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿怎么映射 .put(IotDataSpecsDataTypeEnum.ARRAY.getDataType(), TDengineTableField.TYPE_NCHAR) // TODO 芋艿怎么映射
.build(); .build();
@Resource
private IotDeviceService deviceService;
@Resource @Resource
private IotThingModelService thingModelService; private IotThingModelService thingModelService;
@Resource @Resource
@ -87,7 +84,7 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
// 1.2 解析 DB 里的字段 // 1.2 解析 DB 里的字段
List<TDengineTableField> oldFields = new ArrayList<>(); List<TDengineTableField> oldFields = new ArrayList<>();
try { try {
oldFields.addAll(devicePropertyMapper.getProductPropertySTableFieldList(product.getProductKey())); oldFields.addAll(devicePropertyMapper.getProductPropertySTableFieldList(product.getId()));
} catch (Exception e) { } catch (Exception e) {
if (!e.getMessage().contains("Table does not exist")) { if (!e.getMessage().contains("Table does not exist")) {
throw e; throw e;
@ -101,11 +98,11 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
log.info("[defineDevicePropertyData][productId({}) 没有需要定义的属性]", productId); log.info("[defineDevicePropertyData][productId({}) 没有需要定义的属性]", productId);
return; return;
} }
devicePropertyMapper.createProductPropertySTable(product.getProductKey(), newFields); devicePropertyMapper.createProductPropertySTable(product.getId(), newFields);
return; return;
} }
// 2.2 情况二如果是修改的时候需要更新表 // 2.2 情况二如果是修改的时候需要更新表
devicePropertyMapper.alterProductPropertySTable(product.getProductKey(), oldFields, newFields); devicePropertyMapper.alterProductPropertySTable(product.getId(), oldFields, newFields);
} }
private List<TDengineTableField> buildTableFieldList(List<IotThingModelDO> thingModels) { private List<TDengineTableField> buildTableFieldList(List<IotThingModelDO> thingModels) {
@ -122,16 +119,17 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
@Override @Override
public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) { public void saveDeviceProperty(IotDeviceDO device, IotDeviceMessage message) {
if (!(message.getData() instanceof Map)) { // TODO @芋艿这里要改下协议
if (!(message.getParams() instanceof Map)) {
log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message); log.error("[saveDeviceProperty][消息内容({}) 的 data 类型不正确]", message);
return; return;
} }
// 1. 根据物模型拼接合法的属性 // 1. 根据物模型拼接合法的属性
// TODO @芋艿待定 004赋能后属性到底以 thingModel 为准ik还是 db 的表结构为准tl // TODO @芋艿待定 004赋能后属性到底以 thingModel 为准ik还是 db 的表结构为准tl
List<IotThingModelDO> thingModels = thingModelService.getThingModelListByProductKeyFromCache(device.getProductKey()); List<IotThingModelDO> thingModels = thingModelService.getThingModelListByProductIdFromCache(device.getProductId());
Map<String, Object> properties = new HashMap<>(); Map<String, Object> properties = new HashMap<>();
((Map<?, ?>) message.getData()).forEach((key, value) -> { ((Map<?, ?>) message.getParams()).forEach((key, value) -> {
if (CollUtil.findOne(thingModels, thingModel -> thingModel.getIdentifier().equals(key)) == null) { if (CollUtil.findOne(thingModels, thingModel -> thingModel.getIdentifier().equals(key)) == null) {
log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key); log.error("[saveDeviceProperty][消息({}) 的属性({}) 不存在]", message, key);
return; return;
@ -150,25 +148,16 @@ public class IotDevicePropertyServiceImpl implements IotDevicePropertyService {
// 2.2 保存设备属性日志 // 2.2 保存设备属性日志
Map<String, IotDevicePropertyDO> properties2 = convertMap(properties.entrySet(), Map.Entry::getKey, entry -> Map<String, IotDevicePropertyDO> properties2 = convertMap(properties.entrySet(), Map.Entry::getKey, entry ->
IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build()); IotDevicePropertyDO.builder().value(entry.getValue()).updateTime(message.getReportTime()).build());
deviceDataRedisDAO.putAll(device.getProductKey(), device.getDeviceName(), properties2); deviceDataRedisDAO.putAll(device.getId(), properties2);
} }
@Override @Override
public Map<String, IotDevicePropertyDO> getLatestDeviceProperties(Long deviceId) { public Map<String, IotDevicePropertyDO> getLatestDeviceProperties(Long deviceId) {
// 获取设备信息 return deviceDataRedisDAO.get(deviceId);
IotDeviceDO device = deviceService.validateDeviceExists(deviceId);
// 获得设备属性
return deviceDataRedisDAO.get(device.getProductKey(), device.getDeviceName());
} }
@Override @Override
public PageResult<IotDevicePropertyRespVO> getHistoryDevicePropertyPage(IotDevicePropertyHistoryPageReqVO pageReqVO) { public PageResult<IotDevicePropertyRespVO> getHistoryDevicePropertyPage(IotDevicePropertyHistoryPageReqVO pageReqVO) {
// 获取设备信息
IotDeviceDO device = deviceService.validateDeviceExists(pageReqVO.getDeviceId());
pageReqVO.setProductKey(device.getProductKey()).setDeviceName(device.getDeviceName());
// 分页查询
try { try {
IPage<IotDevicePropertyRespVO> page = devicePropertyMapper.selectPageByHistory( IPage<IotDevicePropertyRespVO> page = devicePropertyMapper.selectPageByHistory(
new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO); new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize()), pageReqVO);

View File

@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.iot.controller.admin.thingmodel.vo.IotThingModelS
import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO; import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/** /**
@ -60,10 +59,10 @@ public interface IotThingModelService {
* *
* 注意该方法会忽略租户信息所以调用时需要确认会不会有跨租户访问的风险 * 注意该方法会忽略租户信息所以调用时需要确认会不会有跨租户访问的风险
* *
* @param productKey 产品标识 * @param productId 产品编号
* @return 产品物模型列表 * @return 产品物模型列表
*/ */
List<IotThingModelDO> getThingModelListByProductKeyFromCache(String productKey); List<IotThingModelDO> getThingModelListByProductIdFromCache(Long productId);
/** /**
* 获得产品物模型分页 * 获得产品物模型分页
@ -81,13 +80,4 @@ public interface IotThingModelService {
*/ */
List<IotThingModelDO> getThingModelList(IotThingModelListReqVO reqVO); List<IotThingModelDO> getThingModelList(IotThingModelListReqVO reqVO);
// TODO @super用不到删除下哈
/**
* 获得物模型数量
*
* @param createTime 创建时间如果为空则统计所有物模型数量
* @return 物模型数量
*/
Long getThingModelCount(LocalDateTime createTime);
} }

View File

@ -29,7 +29,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.*; import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -133,10 +132,10 @@ public class IotThingModelServiceImpl implements IotThingModelService {
} }
@Override @Override
@Cacheable(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productKey") @Cacheable(value = RedisKeyConstants.THING_MODEL_LIST, key = "#productId")
@TenantIgnore // 忽略租户信息跨租户 productKey 是唯一的 @TenantIgnore // 忽略租户信息跨租户 productKey 是唯一的
public List<IotThingModelDO> getThingModelListByProductKeyFromCache(String productKey) { public List<IotThingModelDO> getThingModelListByProductIdFromCache(Long productId) {
return thingModelMapper.selectListByProductKey(productKey); return thingModelMapper.selectListByProductId(productId);
} }
@Override @Override
@ -364,10 +363,4 @@ public class IotThingModelServiceImpl implements IotThingModelService {
return SpringUtil.getBean(getClass()); return SpringUtil.getBean(getClass());
} }
// TODO @super用不到删除下
@Override
public Long getThingModelCount(LocalDateTime createTime) {
return thingModelMapper.selectCountByCreateTime(createTime);
}
} }

View File

@ -5,11 +5,11 @@
<mapper namespace="cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper"> <mapper namespace="cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper">
<select id="getProductPropertySTableFieldList" resultType="cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField"> <select id="getProductPropertySTableFieldList" resultType="cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField">
DESCRIBE product_property_${productKey} DESCRIBE product_property_${productId}
</select> </select>
<update id="createProductPropertySTable"> <update id="createProductPropertySTable">
CREATE STABLE product_property_${productKey} ( CREATE STABLE product_property_${productId} (
ts TIMESTAMP, ts TIMESTAMP,
report_time TIMESTAMP, report_time TIMESTAMP,
<foreach item="field" collection="fields" separator=","> <foreach item="field" collection="fields" separator=",">
@ -20,12 +20,12 @@
</foreach> </foreach>
) )
TAGS ( TAGS (
device_name NCHAR(50) device_id BIGINT
) )
</update> </update>
<update id="alterProductPropertySTableAddField"> <update id="alterProductPropertySTableAddField">
ALTER STABLE product_property_${productKey} ALTER STABLE product_property_${productId}
ADD COLUMN ${field.field} ${field.type} ADD COLUMN ${field.field} ${field.type}
<if test="field.length != null and field.length > 0"> <if test="field.length != null and field.length > 0">
(${field.length}) (${field.length})
@ -33,7 +33,7 @@
</update> </update>
<update id="alterProductPropertySTableModifyField"> <update id="alterProductPropertySTableModifyField">
ALTER STABLE product_property_${productKey} ALTER STABLE product_property_${productId}
MODIFY COLUMN ${field.field} ${field.type} MODIFY COLUMN ${field.field} ${field.type}
<if test="field.length != null and field.length > 0"> <if test="field.length != null and field.length > 0">
(${field.length}) (${field.length})
@ -41,14 +41,14 @@
</update> </update>
<update id="alterProductPropertySTableDropField"> <update id="alterProductPropertySTableDropField">
ALTER STABLE product_property_${productKey} ALTER STABLE product_property_${productId}
DROP COLUMN ${field.field} DROP COLUMN ${field.field}
</update> </update>
<insert id="insert"> <insert id="insert">
INSERT INTO device_property_${device.productKey}_${device.deviceName} INSERT INTO device_property_${device.id}
USING product_property_${device.productKey} USING product_property_${device.productId}
TAGS ('${device.deviceName}') 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.hutool.core.util.StrUtil@toUnderlineCase(key)}
@ -63,13 +63,14 @@
</insert> </insert>
<select id="describeSuperTable" resultType="java.util.Map"> <select id="describeSuperTable" resultType="java.util.Map">
DESCRIBE product_property_${productKey} DESCRIBE product_property_${productId}
</select> </select>
<!-- TODO @芋艿:这里查询有点问题 -->
<select id="selectPageByHistory" <select id="selectPageByHistory"
resultType="cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO"> resultType="cn.iocoder.yudao.module.iot.controller.admin.device.vo.data.IotDevicePropertyRespVO">
SELECT ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} AS `value`, ts AS update_time SELECT ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} AS `value`, ts AS update_time
FROM device_property_${reqVO.productKey}_${reqVO.deviceName} FROM device_property_${reqVO.deviceId}
WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(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])}