|
|
|
|
@ -18,6 +18,7 @@ import org.dromara.ems.report.mapper.VibrationBoardMapper;
|
|
|
|
|
import org.dromara.ems.report.service.IVibrationBoardService;
|
|
|
|
|
import org.dromara.ems.report.service.impl.support.VibrationAnomalyAnalyzer;
|
|
|
|
|
import org.dromara.ems.report.service.impl.support.VibrationDistributionAggregator;
|
|
|
|
|
import org.dromara.ems.report.service.impl.support.VibrationMathUtils;
|
|
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
|
|
@ -81,6 +82,9 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
/** 单次查询允许返回的估算最大记录数,避免多设备分钟级全量拉取把 JVM 堆打爆 */
|
|
|
|
|
private static final long MAX_ESTIMATED_QUERY_ROWS = 500_000L;
|
|
|
|
|
|
|
|
|
|
/** 单条 SQL 最大 UNION 分表数,超出时分批查询再归并,降低数据库优化器编译压力 */
|
|
|
|
|
private static final int MAX_UNION_TABLES = 31;
|
|
|
|
|
|
|
|
|
|
/** 分布统计聚合器(无状态工具类,用单例避免重复创建) */
|
|
|
|
|
private static final VibrationDistributionAggregator DISTRIBUTION_AGGREGATOR = new VibrationDistributionAggregator();
|
|
|
|
|
|
|
|
|
|
@ -532,13 +536,44 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
// 白名单校验——纵深防御,即使 resolveTables 内部逻辑被修改也不会发生 SQL 注入
|
|
|
|
|
validateResolvedTableNames(tableNames);
|
|
|
|
|
|
|
|
|
|
// 抽样间隔 > 1 分钟时使用 ROW_NUMBER 窗口抽样,否则直查原始数据
|
|
|
|
|
if (query.getSamplingInterval() != null && query.getSamplingInterval() > 1) {
|
|
|
|
|
// 调用抽样查询:按抽样间隔窗口取数据
|
|
|
|
|
return vibrationBoardMapper.selectSampledData(tableNames, query);
|
|
|
|
|
// 抽样间隔 > 1 分钟时使用 ROW_NUMBER 窗口抽样,否则直查原始数据;
|
|
|
|
|
// 当分表数量过多时自动分批查询并在 JVM 侧稳定归并,避免单 SQL 过长压垮优化器。
|
|
|
|
|
return queryDataByTableBatches(tableNames, query);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 分表查询执行器:控制单条 SQL 的 UNION 分表数,并保证最终行序稳定。
|
|
|
|
|
*/
|
|
|
|
|
private List<RecordIotenvInstant> queryDataByTableBatches(List<String> tableNames, VibrationBoardQueryBo query) {
|
|
|
|
|
// 分表数量未超阈值时仍走单次查询,避免不必要的批次合并开销
|
|
|
|
|
if (tableNames.size() <= MAX_UNION_TABLES) {
|
|
|
|
|
return executeSingleBatchQuery(tableNames, query);
|
|
|
|
|
}
|
|
|
|
|
// 直查原始数据:不抽样,返回全部符合条件的记录
|
|
|
|
|
return vibrationBoardMapper.selectRawData(tableNames, query);
|
|
|
|
|
// 分批查询后统一归并,确保超长时间窗口场景下数据库编译压力可控
|
|
|
|
|
List<RecordIotenvInstant> mergedRows = new ArrayList<>();
|
|
|
|
|
for (int index = 0; index < tableNames.size(); index += MAX_UNION_TABLES) {
|
|
|
|
|
int endIndex = Math.min(tableNames.size(), index + MAX_UNION_TABLES);
|
|
|
|
|
List<String> batchTableNames = tableNames.subList(index, endIndex);
|
|
|
|
|
List<RecordIotenvInstant> batchRows = executeSingleBatchQuery(batchTableNames, query);
|
|
|
|
|
if (CollUtil.isNotEmpty(batchRows)) {
|
|
|
|
|
mergedRows.addAll(batchRows);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 分批结果必须再次全局排序,避免不同批次的记录交错后影响“latest/相邻差值”等算法正确性
|
|
|
|
|
mergedRows.sort(Comparator.comparing(RecordIotenvInstant::getMonitorId, Comparator.nullsFirst(Comparator.naturalOrder()))
|
|
|
|
|
.thenComparing(RecordIotenvInstant::getRecodeTime, Comparator.nullsFirst(Comparator.naturalOrder()))
|
|
|
|
|
.thenComparing(RecordIotenvInstant::getObjid, Comparator.nullsFirst(Comparator.naturalOrder())));
|
|
|
|
|
return mergedRows;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 单批次查询执行:根据抽样间隔自动选择原始查询或抽样查询。
|
|
|
|
|
*/
|
|
|
|
|
private List<RecordIotenvInstant> executeSingleBatchQuery(List<String> batchTableNames, VibrationBoardQueryBo query) {
|
|
|
|
|
if (query.getSamplingInterval() != null && query.getSamplingInterval() > 1) {
|
|
|
|
|
return vibrationBoardMapper.selectSampledData(batchTableNames, query);
|
|
|
|
|
}
|
|
|
|
|
return vibrationBoardMapper.selectRawData(batchTableNames, query);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -821,25 +856,6 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
return values.get(index).setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 取时间上最近的一条记录的指标值。
|
|
|
|
|
* 必须从原始行按 recodeTime 排序取最后一条,而不能从排序后的均值列表中取——
|
|
|
|
|
* 否则“latest”实际拿到的是数值最大值,与逻辑语义相差很远。
|
|
|
|
|
*/
|
|
|
|
|
private BigDecimal extractLatestMetricValue(List<RecordIotenvInstant> rows, String metricField) {
|
|
|
|
|
// 流式处理:过滤指标值非空的行 → 按时间取最新的一条 → 提取指标值
|
|
|
|
|
return rows.stream()
|
|
|
|
|
// 过滤指标值为空的行
|
|
|
|
|
.filter(row -> extractMetricValue(row, metricField) != null)
|
|
|
|
|
// 按记录时间升序取最大值(即最新时间),时间相同时按主键 objid 排序保证稳定性
|
|
|
|
|
.max(Comparator.comparing(RecordIotenvInstant::getRecodeTime, Comparator.nullsFirst(Comparator.naturalOrder()))
|
|
|
|
|
.thenComparing(RecordIotenvInstant::getObjid, Comparator.nullsFirst(Comparator.naturalOrder())))
|
|
|
|
|
// 从最新记录中提取指标值
|
|
|
|
|
.map(row -> extractMetricValue(row, metricField))
|
|
|
|
|
// 无符合条件的记录时返回 null
|
|
|
|
|
.orElse(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 构建设备维度画像:将每台设备的四维振动均值 + 当前指标 latest/max/count 聚合成一个 Profile。
|
|
|
|
|
* 多个页面(对比、高级、总览)共用这个 Profile,避免重复分组+统计。
|
|
|
|
|
@ -866,26 +882,75 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
* 单设备画像构建:趋势页、高级页和总览页都可以直接复用,避免重复 groupByMonitor。
|
|
|
|
|
*/
|
|
|
|
|
private DeviceMetricProfile buildDeviceProfile(String monitorId, List<RecordIotenvInstant> monitorRows, String currentMetricField) {
|
|
|
|
|
// 当前设备在主看指标上的有效值已按升序排列,后续 max/分布统计都可直接复用
|
|
|
|
|
List<BigDecimal> currentValues = extractMetricValues(monitorRows, currentMetricField);
|
|
|
|
|
// 无有效值时不构建设备画像,避免空设备参与后续排名
|
|
|
|
|
if (CollUtil.isEmpty(currentValues)) {
|
|
|
|
|
// 单设备无数据时直接返回,避免后续空指针
|
|
|
|
|
if (CollUtil.isEmpty(monitorRows)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
// latestValue 必须从时间维度取最近记录,而不是排序后的尾部
|
|
|
|
|
BigDecimal latestValue = extractLatestMetricValue(monitorRows, currentMetricField);
|
|
|
|
|
|
|
|
|
|
// 主看指标聚合(单 pass):避免每台设备重复 stream + sort 带来的高额 CPU 开销
|
|
|
|
|
BigDecimal currentSum = BigDecimal.ZERO;
|
|
|
|
|
int currentCount = 0;
|
|
|
|
|
BigDecimal currentMax = null;
|
|
|
|
|
BigDecimal currentLatestValue = null;
|
|
|
|
|
Date currentLatestTime = null;
|
|
|
|
|
Long currentLatestObjid = null;
|
|
|
|
|
|
|
|
|
|
// 四个固定维度也在同一次遍历里完成均值累加,避免每个维度都单独再扫一遍
|
|
|
|
|
BigDecimal speedSum = BigDecimal.ZERO;
|
|
|
|
|
int speedCount = 0;
|
|
|
|
|
BigDecimal displacementSum = BigDecimal.ZERO;
|
|
|
|
|
int displacementCount = 0;
|
|
|
|
|
BigDecimal accelerationSum = BigDecimal.ZERO;
|
|
|
|
|
int accelerationCount = 0;
|
|
|
|
|
BigDecimal tempSum = BigDecimal.ZERO;
|
|
|
|
|
int tempCount = 0;
|
|
|
|
|
|
|
|
|
|
for (RecordIotenvInstant row : monitorRows) {
|
|
|
|
|
BigDecimal currentValue = extractMetricValue(row, currentMetricField);
|
|
|
|
|
if (currentValue != null) {
|
|
|
|
|
currentCount++;
|
|
|
|
|
currentSum = currentSum.add(currentValue);
|
|
|
|
|
currentMax = currentMax == null ? currentValue : currentMax.max(currentValue);
|
|
|
|
|
if (currentLatestValue == null || isLaterRecord(row.getRecodeTime(), row.getObjid(), currentLatestTime, currentLatestObjid)) {
|
|
|
|
|
currentLatestValue = currentValue;
|
|
|
|
|
currentLatestTime = row.getRecodeTime();
|
|
|
|
|
currentLatestObjid = row.getObjid();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (row.getVibrationSpeed() != null) {
|
|
|
|
|
speedCount++;
|
|
|
|
|
speedSum = speedSum.add(row.getVibrationSpeed());
|
|
|
|
|
}
|
|
|
|
|
if (row.getVibrationDisplacement() != null) {
|
|
|
|
|
displacementCount++;
|
|
|
|
|
displacementSum = displacementSum.add(row.getVibrationDisplacement());
|
|
|
|
|
}
|
|
|
|
|
if (row.getVibrationAcceleration() != null) {
|
|
|
|
|
accelerationCount++;
|
|
|
|
|
accelerationSum = accelerationSum.add(row.getVibrationAcceleration());
|
|
|
|
|
}
|
|
|
|
|
if (row.getVibrationTemp() != null) {
|
|
|
|
|
tempCount++;
|
|
|
|
|
tempSum = tempSum.add(row.getVibrationTemp());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 主看指标没有有效值时不构建设备画像,避免空设备参与后续排名
|
|
|
|
|
if (currentCount == 0 || currentLatestValue == null || currentMax == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 构建设备画像,四个维度均值保留下来供高级页平行坐标和对比页复用
|
|
|
|
|
return new DeviceMetricProfile(
|
|
|
|
|
monitorId,
|
|
|
|
|
monitorRows.get(0).getMonitorName(),
|
|
|
|
|
avg(currentValues),
|
|
|
|
|
latestValue == null ? null : latestValue.setScale(2, RoundingMode.HALF_UP),
|
|
|
|
|
currentValues.get(currentValues.size() - 1).setScale(2, RoundingMode.HALF_UP),
|
|
|
|
|
currentValues.size(),
|
|
|
|
|
avg(extractMetricValues(monitorRows, "vibrationSpeed")),
|
|
|
|
|
avg(extractMetricValues(monitorRows, "vibrationDisplacement")),
|
|
|
|
|
avg(extractMetricValues(monitorRows, "vibrationAcceleration")),
|
|
|
|
|
avg(extractMetricValues(monitorRows, "vibrationTemp"))
|
|
|
|
|
avg(currentSum, currentCount),
|
|
|
|
|
currentLatestValue.setScale(2, RoundingMode.HALF_UP),
|
|
|
|
|
currentMax.setScale(2, RoundingMode.HALF_UP),
|
|
|
|
|
currentCount,
|
|
|
|
|
avg(speedSum, speedCount),
|
|
|
|
|
avg(displacementSum, displacementCount),
|
|
|
|
|
avg(accelerationSum, accelerationCount),
|
|
|
|
|
avg(tempSum, tempCount)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -1048,24 +1113,21 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
* 保留两位小数与前端展示对齐。
|
|
|
|
|
*/
|
|
|
|
|
private BigDecimal avg(List<BigDecimal> values) {
|
|
|
|
|
// 空列表返回零值(保留两位小数),避免上层做空判断
|
|
|
|
|
if (CollUtil.isEmpty(values)) {
|
|
|
|
|
// 返回 0.00 作为默认均值
|
|
|
|
|
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
|
|
|
|
}
|
|
|
|
|
// 对列表所有元素求和,初始值为零
|
|
|
|
|
BigDecimal sum = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
|
|
|
|
// 求和除以元素个数得到均值,保留两位小数并四舍五入
|
|
|
|
|
return sum.divide(BigDecimal.valueOf(values.size()), 2, RoundingMode.HALF_UP);
|
|
|
|
|
return VibrationMathUtils.avg(values, 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 均值计算:单 pass 累加场景(sum/count)直接复用,避免先组装中间 List。
|
|
|
|
|
*/
|
|
|
|
|
private BigDecimal avg(BigDecimal sum, int count) {
|
|
|
|
|
return VibrationMathUtils.avg(sum, count, 2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 日期格式化工具:统一使用系统时区,确保与数据库存储时区一致。
|
|
|
|
|
*/
|
|
|
|
|
private String formatDate(Date date, String pattern) {
|
|
|
|
|
// 将 Date 转为系统时区的 LocalDateTime,再按指定模式格式化为字符串返回
|
|
|
|
|
return DateTimeFormatter.ofPattern(pattern)
|
|
|
|
|
.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
|
|
|
|
|
private String formatDate(Date date, DateTimeFormatter formatter) {
|
|
|
|
|
return VibrationMathUtils.formatDate(date, formatter);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 指标元数据:字段名 + 显示标签 + 单位,集中维护避免前后端不一致 */
|
|
|
|
|
@ -1352,8 +1414,6 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
* 先压缩到 MAX_TREND_POINTS 以内,避免 ECharts 渲染超过万点时浏览器卡死。
|
|
|
|
|
*/
|
|
|
|
|
private List<VibrationTrendPageVo.TrendSeriesItem> buildTrendMetricSeries(List<RecordIotenvInstant> rows) {
|
|
|
|
|
// 先压缩到 MAX_TREND_POINTS 以内,避免 ECharts 渲染超过万点时浏览器卡死
|
|
|
|
|
List<RecordIotenvInstant> compressed = compressRows(rows, MAX_TREND_POINTS);
|
|
|
|
|
// 定义四个振动维度的元数据
|
|
|
|
|
List<MetricMeta> metas = List.of(
|
|
|
|
|
resolveMetricMeta("vibrationSpeed"),
|
|
|
|
|
@ -1365,6 +1425,8 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
List<VibrationTrendPageVo.TrendSeriesItem> series = new ArrayList<>();
|
|
|
|
|
// 遍历每个振动维度,构建对应的趋势系列
|
|
|
|
|
for (MetricMeta meta : metas) {
|
|
|
|
|
// 按当前维度进行峰值保真压缩,避免桶中位数策略吞掉故障尖峰
|
|
|
|
|
List<RecordIotenvInstant> compressed = compressRows(rows, MAX_TREND_POINTS, meta.field());
|
|
|
|
|
// 将压缩后的每行映射为趋势点,过滤时间或值为空的点
|
|
|
|
|
List<VibrationTrendPageVo.TrendPointItem> points = compressed.stream()
|
|
|
|
|
.map(row -> buildTrendPoint(row.getRecodeTime(), extractMetricValue(row, meta.field())))
|
|
|
|
|
@ -1411,7 +1473,7 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
// 遍历 TOP6 设备,构建每台设备的趋势曲线
|
|
|
|
|
for (DeviceMetricProfile profile : topProfiles) {
|
|
|
|
|
// 压缩当前设备的采样数据到 MAX_TREND_POINTS 以内
|
|
|
|
|
List<RecordIotenvInstant> compressed = compressRows(grouped.get(profile.monitorId()), MAX_TREND_POINTS);
|
|
|
|
|
List<RecordIotenvInstant> compressed = compressRows(grouped.get(profile.monitorId()), MAX_TREND_POINTS, metricField);
|
|
|
|
|
// 将压缩后的每行映射为趋势点,过滤时间或值为空的点
|
|
|
|
|
List<VibrationTrendPageVo.TrendPointItem> points = compressed.stream()
|
|
|
|
|
.map(row -> buildTrendPoint(row.getRecodeTime(), extractMetricValue(row, metricField)))
|
|
|
|
|
@ -1460,7 +1522,7 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
// 创建趋势点实例
|
|
|
|
|
VibrationTrendPageVo.TrendPointItem item = new VibrationTrendPageVo.TrendPointItem();
|
|
|
|
|
// 设置时间字符串,格式为 yyyy-MM-dd HH:mm:ss
|
|
|
|
|
item.setTime(formatDate(time, "yyyy-MM-dd HH:mm:ss"));
|
|
|
|
|
item.setTime(formatDate(time, VibrationMathUtils.FMT_DATETIME));
|
|
|
|
|
// 设置指标值,保留两位小数
|
|
|
|
|
item.setValue(value.setScale(2, RoundingMode.HALF_UP));
|
|
|
|
|
// 返回构建好的趋势点
|
|
|
|
|
@ -1483,7 +1545,7 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// 提取小时数字符串(如 "08"、"14")
|
|
|
|
|
String hour = formatDate(row.getRecodeTime(), "HH");
|
|
|
|
|
String hour = formatDate(row.getRecodeTime(), VibrationMathUtils.FMT_HOUR);
|
|
|
|
|
// 将指标值追加到对应小时桶中
|
|
|
|
|
bucketMap.computeIfAbsent(hour, key -> new ArrayList<>()).add(value);
|
|
|
|
|
}
|
|
|
|
|
@ -1591,26 +1653,75 @@ public class VibrationBoardServiceImpl implements IVibrationBoardService {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 桶中位数压缩:按等宽分桶后取每桶中间点,保留数据分布形态。
|
|
|
|
|
* 比简单的“每 N 点取一个”更能保留峰谷特征,避免抽样后趋势变形。
|
|
|
|
|
* 峰值保真压缩:按桶保留 min+max,优先防止故障尖峰在抽样时被抹平。
|
|
|
|
|
*/
|
|
|
|
|
private List<RecordIotenvInstant> compressRows(List<RecordIotenvInstant> rows, int maxPoints) {
|
|
|
|
|
private List<RecordIotenvInstant> compressRows(List<RecordIotenvInstant> rows, int maxPoints, String metricField) {
|
|
|
|
|
// 空列表或数据量未超过上限时直接返回原始数据,无需压缩
|
|
|
|
|
if (CollUtil.isEmpty(rows) || rows.size() <= maxPoints) {
|
|
|
|
|
return rows;
|
|
|
|
|
}
|
|
|
|
|
// 计算每个桶的大小:向上取整确保桶数 <= maxPoints
|
|
|
|
|
int bucketSize = (int) Math.ceil((double) rows.size() / maxPoints);
|
|
|
|
|
// 每桶最多输出两个点(min + max),因此桶数上限按 maxPoints/2 计算,确保最终点数可控
|
|
|
|
|
int bucketCount = Math.max(1, maxPoints / 2);
|
|
|
|
|
int bucketSize = (int) Math.ceil((double) rows.size() / bucketCount);
|
|
|
|
|
// 创建可变列表用于收集压缩后的采样点
|
|
|
|
|
List<RecordIotenvInstant> result = new ArrayList<>();
|
|
|
|
|
// 按桶大小步进遍历原始数据
|
|
|
|
|
for (int index = 0; index < rows.size(); index += bucketSize) {
|
|
|
|
|
// 截取当前桶的子列表,最后一个桶可能不足 bucketSize
|
|
|
|
|
List<RecordIotenvInstant> bucket = rows.subList(index, Math.min(rows.size(), index + bucketSize));
|
|
|
|
|
// 取桶中间位置的元素作为代表点,比取首/尾更能保留峰谷特征
|
|
|
|
|
result.add(bucket.get(bucket.size() / 2));
|
|
|
|
|
RecordIotenvInstant minRow = null;
|
|
|
|
|
RecordIotenvInstant maxRow = null;
|
|
|
|
|
BigDecimal minValue = null;
|
|
|
|
|
BigDecimal maxValue = null;
|
|
|
|
|
for (RecordIotenvInstant row : bucket) {
|
|
|
|
|
BigDecimal value = extractMetricValue(row, metricField);
|
|
|
|
|
if (value == null) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (minValue == null || value.compareTo(minValue) < 0) {
|
|
|
|
|
minValue = value;
|
|
|
|
|
minRow = row;
|
|
|
|
|
}
|
|
|
|
|
if (maxValue == null || value.compareTo(maxValue) > 0) {
|
|
|
|
|
maxValue = value;
|
|
|
|
|
maxRow = row;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 桶内无有效指标值时回退中位索引,避免整桶被丢弃造成时间轴断裂
|
|
|
|
|
if (minRow == null || maxRow == null) {
|
|
|
|
|
result.add(bucket.get(bucket.size() / 2));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// 同一行同时是最小和最大值时只保留一次,避免重复点污染趋势线
|
|
|
|
|
if (Objects.equals(minRow, maxRow)) {
|
|
|
|
|
result.add(minRow);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
// 保持时间顺序输出,避免折线图出现“回折”伪影
|
|
|
|
|
if (isEarlierRecord(minRow, maxRow)) {
|
|
|
|
|
result.add(minRow);
|
|
|
|
|
result.add(maxRow);
|
|
|
|
|
} else {
|
|
|
|
|
result.add(maxRow);
|
|
|
|
|
result.add(minRow);
|
|
|
|
|
}
|
|
|
|
|
// 兜底裁剪,确保压缩结果不会超出前端可承载点数
|
|
|
|
|
if (result.size() >= maxPoints) {
|
|
|
|
|
return result.subList(0, maxPoints);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 返回压缩后的采样点列表
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 记录先后比较:先比时间,再用 objid 兜底,确保同一时刻下也有稳定顺序。
|
|
|
|
|
*/
|
|
|
|
|
private boolean isEarlierRecord(RecordIotenvInstant first, RecordIotenvInstant second) {
|
|
|
|
|
int timeCompare = Comparator.nullsFirst(Comparator.<Date>naturalOrder()).compare(first.getRecodeTime(), second.getRecodeTime());
|
|
|
|
|
if (timeCompare != 0) {
|
|
|
|
|
return timeCompare < 0;
|
|
|
|
|
}
|
|
|
|
|
return Comparator.nullsFirst(Comparator.<Long>naturalOrder()).compare(first.getObjid(), second.getObjid()) <= 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|