refactor(振动报表): 优化时间处理与数据压缩逻辑

将时间字段类型从Date改为String,统一使用VibrationMathUtils处理时间格式
重构数据压缩算法保留峰值特征,优化分表查询性能与设备画像构建
增加VibrationMathUtils工具类集中数学计算与时间格式化逻辑
main
zch 3 months ago
parent 97f8958427
commit 057749a9c9

@ -3,7 +3,6 @@ package org.dromara.ems.report.domain.vo.vibrationboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
@ -79,7 +78,7 @@ public class VibrationAnomalyPageVo {
/** 触发高风险的指标值 */
private BigDecimal value;
/** 事件发生时刻 */
private Date recodeTime;
private String recodeTime;
}
/**
@ -92,9 +91,9 @@ public class VibrationAnomalyPageVo {
/** 设备名称 */
private String monitorName;
/** 连续超标段开始时刻 */
private Date startTime;
private String startTime;
/** 连续超标段结束时刻 */
private Date endTime;
private String endTime;
/** 段内峰值 */
private BigDecimal maxValue;
/** 连续超标的采样点数,越长说明劣化越严重 */
@ -113,7 +112,7 @@ public class VibrationAnomalyPageVo {
/** 相邻两点的差值 */
private BigDecimal diff;
/** 突变发生时刻(后一个点的时间) */
private Date recodeTime;
private String recodeTime;
}
/**

@ -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;
}
}

@ -6,8 +6,6 @@ import org.dromara.ems.report.domain.vo.vibrationboard.VibrationAnomalyPageVo;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
@ -56,7 +54,7 @@ public class VibrationAnomalyAnalyzer {
// 设置指标值,保留两位小数
item.setValue(metricExtractor.apply(row).setScale(2, RoundingMode.HALF_UP));
// 设置记录时间
item.setRecodeTime(row.getRecodeTime());
item.setRecodeTime(formatDateTime(row.getRecodeTime()));
// 返回构建好的事件项
return item;
})
@ -101,7 +99,7 @@ public class VibrationAnomalyAnalyzer {
// 设置差值,保留两位小数
item.setDiff(diff.setScale(2, RoundingMode.HALF_UP));
// 设置发生时间(取当前点的时间)
item.setRecodeTime(monitorRows.get(i).getRecodeTime());
item.setRecodeTime(formatDateTime(monitorRows.get(i).getRecodeTime()));
// 将事件添加到结果列表
result.add(item);
}
@ -167,9 +165,9 @@ public class VibrationAnomalyAnalyzer {
// 设置设备名称(取超标段起始点的设备名)
item.setMonitorName(monitorRows.get(startIndex).getMonitorName());
// 设置超标段开始时间
item.setStartTime(monitorRows.get(startIndex).getRecodeTime());
item.setStartTime(formatDateTime(monitorRows.get(startIndex).getRecodeTime()));
// 设置超标段结束时间
item.setEndTime(monitorRows.get(endIndex).getRecodeTime());
item.setEndTime(formatDateTime(monitorRows.get(endIndex).getRecodeTime()));
// 设置超标段内的峰值,保留两位小数
item.setMaxValue(maxValue.setScale(2, RoundingMode.HALF_UP));
// 设置超标段内的采样点数
@ -210,7 +208,7 @@ public class VibrationAnomalyAnalyzer {
continue;
}
// 将记录时间格式化为小时级别的时间桶(如 "2026-03-26 14:00:00"
String hourBucket = formatDate(row.getRecodeTime(), "yyyy-MM-dd HH") + ":00:00";
String hourBucket = formatDateHourBucket(row.getRecodeTime());
// 拼接"设备ID_小时桶"作为分桶 key确保不同设备同一小时不会混淆
String key = row.getMonitorId() + "_" + hourBucket;
// 将指标值追加到对应桶中,桶不存在时自动创建
@ -309,15 +307,8 @@ public class VibrationAnomalyAnalyzer {
* Service avg2
*/
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);
// 求和除以元素个数得到均值保留6位小数比展示层多减少标准差计算的累积截断误差
return sum.divide(BigDecimal.valueOf(values.size()), 6, RoundingMode.HALF_UP);
// 标准差前置计算需要更高精度,统一用 6 位小数均值减少累计截断误差
return VibrationMathUtils.avg(values, 6);
}
/**
@ -352,12 +343,11 @@ public class VibrationAnomalyAnalyzer {
return current.setScale(scale, RoundingMode.HALF_UP);
}
/**
* 使
*/
private String formatDate(java.util.Date date, String pattern) {
// 将 Date 转为系统时区的 LocalDateTime再按指定模式格式化为字符串返回
return DateTimeFormatter.ofPattern(pattern)
.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
private String formatDateTime(java.util.Date date) {
return VibrationMathUtils.formatDate(date, VibrationMathUtils.FMT_DATETIME);
}
private String formatDateHourBucket(java.util.Date date) {
return VibrationMathUtils.formatDate(date, VibrationMathUtils.FMT_DATE_HOUR) + ":00:00";
}
}

@ -6,8 +6,6 @@ import org.dromara.ems.report.domain.vo.vibrationboard.VibrationDistributionPage
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
@ -172,7 +170,7 @@ public class VibrationDistributionAggregator {
continue;
}
// 将记录时间格式化为"年-月-日"字符串作为日期桶的 key
String statDate = formatDate(row.getRecodeTime(), "yyyy-MM-dd");
String statDate = formatDate(row.getRecodeTime(), VibrationMathUtils.FMT_DATE);
// 将指标值追加到对应日期桶中,桶不存在时自动创建
bucketMap.computeIfAbsent(statDate, key -> new ArrayList<>()).add(value);
}
@ -189,8 +187,11 @@ public class VibrationDistributionAggregator {
// 将当天热力图项添加到结果列表
result.add(item);
});
// 返回按日期排列的日历热力图数据
return result;
// 多设备查询时原始行序是“设备→时间”,这里必须按日期重排,
// 否则前端用首尾日期推 calendar range 时会截断热力图范围。
return result.stream()
.sorted((left, right) -> left.getStatDate().compareTo(right.getStatDate()))
.toList();
}
/**
@ -210,7 +211,7 @@ public class VibrationDistributionAggregator {
continue;
}
// 将记录时间格式化为"年-月-日"字符串
String statDate = formatDate(row.getRecodeTime(), "yyyy-MM-dd");
String statDate = formatDate(row.getRecodeTime(), VibrationMathUtils.FMT_DATE);
// 提取记录时间的小时数0~23
Integer hour = parseHour(row.getRecodeTime());
// 使用"日期_小时"拼接作为桶 key确保不同天同一小时的数据不会混淆
@ -233,8 +234,16 @@ public class VibrationDistributionAggregator {
// 将热力图项添加到结果列表
result.add(item);
});
// 返回按日期×小时排列的热力图数据
return result;
// 统一按“日期升序 + 小时升序”返回,保证前端二维热力图坐标稳定。
return result.stream()
.sorted((left, right) -> {
int dateCompare = left.getStatDate().compareTo(right.getStatDate());
if (dateCompare != 0) {
return dateCompare;
}
return left.getStatHour().compareTo(right.getStatHour());
})
.toList();
}
/** 构建四分位区间项——纯工厂方法,简化上层调用 */
@ -270,31 +279,20 @@ public class VibrationDistributionAggregator {
*
*/
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);
}
/**
* 使
*/
private String formatDate(java.util.Date date, String pattern) {
// 将 Date 转为系统时区的 LocalDateTime再按指定模式格式化为字符串返回
return DateTimeFormatter.ofPattern(pattern)
.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
private String formatDate(java.util.Date date, java.time.format.DateTimeFormatter formatter) {
return VibrationMathUtils.formatDate(date, formatter);
}
/**
* 0~23× key
*/
private Integer parseHour(java.util.Date date) {
// 将 Date 转为系统时区的 ZonedDateTime提取小时数0~23返回
return date.toInstant().atZone(ZoneId.systemDefault()).getHour();
return VibrationMathUtils.parseHour(date);
}
}

@ -0,0 +1,70 @@
package org.dromara.ems.report.service.impl.support;
import cn.hutool.core.collection.CollUtil;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
/**
* /
*
* <p>
* </p>
*/
public final class VibrationMathUtils {
public static final DateTimeFormatter FMT_DATETIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static final DateTimeFormatter FMT_DATE_HOUR = DateTimeFormatter.ofPattern("yyyy-MM-dd HH");
public static final DateTimeFormatter FMT_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static final DateTimeFormatter FMT_HOUR = DateTimeFormatter.ofPattern("HH");
private static final ZoneId SYSTEM_ZONE_ID = ZoneId.systemDefault();
private VibrationMathUtils() {
}
/**
* 0
*/
public static BigDecimal avg(List<BigDecimal> values, int scale) {
if (CollUtil.isEmpty(values)) {
return BigDecimal.ZERO.setScale(scale, RoundingMode.HALF_UP);
}
BigDecimal sum = values.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
return avg(sum, values.size(), scale);
}
/**
* sum/count 便 pass
*/
public static BigDecimal avg(BigDecimal sum, int count, int scale) {
if (count <= 0) {
return BigDecimal.ZERO.setScale(scale, RoundingMode.HALF_UP);
}
return sum.divide(BigDecimal.valueOf(count), scale, RoundingMode.HALF_UP);
}
/**
* formatter GC
*/
public static String formatDate(Date date, DateTimeFormatter formatter) {
if (date == null || formatter == null) {
return null;
}
return formatter.format(date.toInstant().atZone(SYSTEM_ZONE_ID).toLocalDateTime());
}
/**
* 0~23
*/
public static Integer parseHour(Date date) {
if (date == null) {
return null;
}
return date.toInstant().atZone(SYSTEM_ZONE_ID).getHour();
}
}
Loading…
Cancel
Save