feat(位移看板): 新增位移专属看板模块

实现位移专属看板的独立模块,包括控制器、服务接口及实现、查询参数和视图对象
复用振动看板现有结构但保持独立文件入口,确保不破坏现有校验与序列化行为
位移质量页采用专属统计口径,高级页移除四指标平行坐标画像
main
zch 2 months ago
parent 99ffe8c8c8
commit ace1b02658

@ -0,0 +1,73 @@
package org.dromara.ems.report.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.ems.report.domain.bo.DisplacementBoardQueryBo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementAdvancedPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementAnomalyPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementComparisonPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementDistributionPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementOverviewPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementQualityPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementTrendPageVo;
import org.dromara.ems.report.service.IDisplacementBoardService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Controller
*
* <p> Controller
* /访</p>
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/ems/report/displacementBoard")
public class DisplacementBoardController {
private final IDisplacementBoardService displacementBoardService;
@SaCheckPermission("ems/report:vibrationBoard:query")
@GetMapping("/overview")
public R<DisplacementOverviewPageVo> overview(DisplacementBoardQueryBo bo) {
return R.ok(displacementBoardService.listOverviewData(bo));
}
@SaCheckPermission("ems/report:vibrationBoard:query")
@GetMapping("/trend")
public R<DisplacementTrendPageVo> trend(DisplacementBoardQueryBo bo) {
return R.ok(displacementBoardService.listTrendData(bo));
}
@SaCheckPermission("ems/report:vibrationBoard:query")
@GetMapping("/comparison")
public R<DisplacementComparisonPageVo> comparison(DisplacementBoardQueryBo bo) {
return R.ok(displacementBoardService.listComparisonData(bo));
}
@SaCheckPermission("ems/report:vibrationBoard:query")
@GetMapping("/quality")
public R<DisplacementQualityPageVo> quality(DisplacementBoardQueryBo bo) {
return R.ok(displacementBoardService.listQualityData(bo));
}
@SaCheckPermission("ems/report:vibrationBoard:query")
@GetMapping("/distribution")
public R<DisplacementDistributionPageVo> distribution(DisplacementBoardQueryBo bo) {
return R.ok(displacementBoardService.listDistributionData(bo));
}
@SaCheckPermission("ems/report:vibrationBoard:query")
@GetMapping("/anomaly")
public R<DisplacementAnomalyPageVo> anomaly(DisplacementBoardQueryBo bo) {
return R.ok(displacementBoardService.listAnomalyData(bo));
}
@SaCheckPermission("ems/report:vibrationBoard:query")
@GetMapping("/advanced")
public R<DisplacementAdvancedPageVo> advanced(DisplacementBoardQueryBo bo) {
return R.ok(displacementBoardService.listAdvancedData(bo));
}
}

@ -0,0 +1,11 @@
package org.dromara.ems.report.domain.bo;
/**
*
*
* <p>
*
* displacementBoard Controller / Service </p>
*/
public class DisplacementBoardQueryBo extends VibrationBoardQueryBo {
}

@ -0,0 +1,57 @@
package org.dromara.ems.report.domain.vo.displacementboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
*
*/
@Data
public class DisplacementAdvancedPageVo {
private String metricField;
private String metricLabel;
private String unit;
private BigDecimal lowBandUpper;
private BigDecimal focusBandUpper;
private List<SankeyNodeItem> sankeyNodes;
private List<SankeyLinkItem> sankeyLinks;
private List<TreemapItem> treemapItems;
private List<ParallelAxisItem> parallelAxes;
private List<ParallelSeriesItem> parallelSeries;
@Data
public static class SankeyNodeItem {
private String name;
}
@Data
public static class SankeyLinkItem {
private String source;
private String target;
private Integer value;
}
@Data
public static class TreemapItem {
private String name;
private BigDecimal value;
private String levelTag;
}
@Data
public static class ParallelAxisItem {
private Integer dim;
private String name;
private BigDecimal max;
}
@Data
public static class ParallelSeriesItem {
private String monitorId;
private String monitorName;
private List<BigDecimal> values;
}
}

@ -0,0 +1,65 @@
package org.dromara.ems.report.domain.vo.displacementboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
*
*/
@Data
public class DisplacementAnomalyPageVo {
private String metricField;
private String metricLabel;
private String unit;
private BigDecimal highThreshold;
private BigDecimal warningThreshold;
private BigDecimal rapidRiseThreshold;
private BigDecimal stddevThreshold;
private Integer minContinuousSamples;
private Integer highEventCount;
private Integer continuousEventCount;
private Integer rapidRiseEventCount;
private Integer jitterEventCount;
private List<HighEventItem> highEvents;
private List<ContinuousEventItem> continuousEvents;
private List<RapidRiseEventItem> rapidRiseEvents;
private List<JitterEventItem> jitterEvents;
@Data
public static class HighEventItem {
private String monitorId;
private String monitorName;
private BigDecimal value;
private String recodeTime;
}
@Data
public static class ContinuousEventItem {
private String monitorId;
private String monitorName;
private String startTime;
private String endTime;
private BigDecimal maxValue;
private Integer sampleCount;
}
@Data
public static class RapidRiseEventItem {
private String monitorId;
private String monitorName;
private BigDecimal diff;
private String recodeTime;
}
@Data
public static class JitterEventItem {
private String monitorId;
private String monitorName;
private String hourBucket;
private BigDecimal stddev;
private Integer sampleCount;
}
}

@ -0,0 +1,36 @@
package org.dromara.ems.report.domain.vo.displacementboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
*
*/
@Data
public class DisplacementComparisonPageVo {
private String metricField;
private String metricLabel;
private String unit;
private List<RankItem> rankItems;
private List<ScatterItem> scatterItems;
@Data
public static class RankItem {
private String monitorId;
private String monitorName;
private BigDecimal avg;
private BigDecimal latest;
}
@Data
public static class ScatterItem {
private String monitorId;
private String monitorName;
private BigDecimal avg;
private BigDecimal max;
private Integer sampleCount;
}
}

@ -0,0 +1,47 @@
package org.dromara.ems.report.domain.vo.displacementboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
*
*/
@Data
public class DisplacementDistributionPageVo {
private String metricField;
private String metricLabel;
private String unit;
private List<IntervalBucketItem> intervalBuckets;
private List<HistogramBucketItem> histogramBuckets;
private List<CalendarHeatmapItem> calendarHeatmap;
private List<HourlyHeatmapItem> hourlyHeatmap;
@Data
public static class IntervalBucketItem {
private String label;
private Long count;
}
@Data
public static class HistogramBucketItem {
private BigDecimal startValue;
private BigDecimal endValue;
private Long count;
}
@Data
public static class CalendarHeatmapItem {
private String statDate;
private BigDecimal avgValue;
}
@Data
public static class HourlyHeatmapItem {
private String statDate;
private Integer statHour;
private BigDecimal avgValue;
}
}

@ -0,0 +1,60 @@
package org.dromara.ems.report.domain.vo.displacementboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
*
*/
@Data
public class DisplacementOverviewPageVo {
private String metricField;
private String metricLabel;
private String unit;
private Integer sampleCount;
private Integer deviceCount;
private BigDecimal coverageRate;
private List<MetricCardItem> metricCards;
private List<GaugeItem> gaugeItems;
private PrimaryMetricStats primaryMetricStats;
private List<DeviceRankItem> deviceRanks;
@Data
public static class MetricCardItem {
private String field;
private String label;
private String unit;
private BigDecimal latest;
private BigDecimal avg;
private BigDecimal max;
}
@Data
public static class GaugeItem {
private String name;
private BigDecimal value;
private BigDecimal maxValue;
private String unit;
}
@Data
public static class PrimaryMetricStats {
private BigDecimal latest;
private BigDecimal min;
private BigDecimal avg;
private BigDecimal max;
}
@Data
public static class DeviceRankItem {
private String monitorId;
private String monitorName;
private BigDecimal avg;
private BigDecimal latest;
private BigDecimal max;
private Integer sampleCount;
}
}

@ -0,0 +1,31 @@
package org.dromara.ems.report.domain.vo.displacementboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
*
*/
@Data
public class DisplacementQualityPageVo {
private Integer sampleCount;
private Integer deviceCount;
private BigDecimal coverageRate;
private Integer validCount;
private Integer invalidCount;
private BigDecimal validRate;
private BigDecimal invalidRate;
private List<MetricQualityItem> metricQualityItems;
@Data
public static class MetricQualityItem {
private String field;
private String label;
private String unit;
private BigDecimal validRate;
private Integer validCount;
}
}

@ -0,0 +1,40 @@
package org.dromara.ems.report.domain.vo.displacementboard;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
*
*/
@Data
public class DisplacementTrendPageVo {
private String metricField;
private String metricLabel;
private String unit;
private Boolean multiDevice;
private List<TrendSeriesItem> series;
private List<HourlyItem> hourlyItems;
@Data
public static class TrendSeriesItem {
private String name;
private String field;
private String unit;
private List<TrendPointItem> points;
}
@Data
public static class TrendPointItem {
private String time;
private BigDecimal value;
}
@Data
public static class HourlyItem {
private String hour;
private BigDecimal avgValue;
}
}

@ -0,0 +1,34 @@
package org.dromara.ems.report.service;
import org.dromara.ems.report.domain.bo.DisplacementBoardQueryBo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementAdvancedPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementAnomalyPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementComparisonPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementDistributionPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementOverviewPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementQualityPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementTrendPageVo;
/**
*
*
* <p> VO
* displacementBoard
* </p>
*/
public interface IDisplacementBoardService {
DisplacementOverviewPageVo listOverviewData(DisplacementBoardQueryBo bo);
DisplacementTrendPageVo listTrendData(DisplacementBoardQueryBo bo);
DisplacementComparisonPageVo listComparisonData(DisplacementBoardQueryBo bo);
DisplacementQualityPageVo listQualityData(DisplacementBoardQueryBo bo);
DisplacementDistributionPageVo listDistributionData(DisplacementBoardQueryBo bo);
DisplacementAnomalyPageVo listAnomalyData(DisplacementBoardQueryBo bo);
DisplacementAdvancedPageVo listAdvancedData(DisplacementBoardQueryBo bo);
}

@ -0,0 +1,659 @@
package org.dromara.ems.report.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.ems.record.domain.RecordIotenvInstant;
import org.dromara.ems.record.service.RecordIotenvPartitionService;
import org.dromara.ems.report.domain.bo.DisplacementBoardQueryBo;
import org.dromara.ems.report.domain.bo.VibrationBoardQueryBo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementAdvancedPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementAnomalyPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementComparisonPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementDistributionPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementOverviewPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementQualityPageVo;
import org.dromara.ems.report.domain.vo.displacementboard.DisplacementTrendPageVo;
import org.dromara.ems.report.domain.vo.vibrationboard.VibrationAdvancedPageVo;
import org.dromara.ems.report.domain.vo.vibrationboard.VibrationAnomalyPageVo;
import org.dromara.ems.report.domain.vo.vibrationboard.VibrationComparisonPageVo;
import org.dromara.ems.report.domain.vo.vibrationboard.VibrationDistributionPageVo;
import org.dromara.ems.report.domain.vo.vibrationboard.VibrationOverviewPageVo;
import org.dromara.ems.report.domain.vo.vibrationboard.VibrationQualityPageVo;
import org.dromara.ems.report.domain.vo.vibrationboard.VibrationTrendPageVo;
import org.dromara.ems.report.mapper.VibrationBoardMapper;
import org.dromara.ems.report.service.IDisplacementBoardService;
import org.dromara.ems.report.service.IVibrationBoardService;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
/**
*
*
* <p>
* 1. {@link IVibrationBoardService}
* 2.
* 3. VibrationBoardServiceImpl</p>
*/
@Service
@RequiredArgsConstructor
public class DisplacementBoardServiceImpl implements IDisplacementBoardService {
private static final String DISPLACEMENT_FIELD = "vibrationDisplacement";
private static final String DISPLACEMENT_LABEL = "位移";
private static final String DISPLACEMENT_UNIT = "um";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("^record_iotenv_instant_\\d{8}$");
private static final int MAX_QUERY_DAYS = 90;
private static final long MAX_ESTIMATED_QUERY_ROWS = 500_000L;
private static final int MAX_UNION_TABLES = 31;
private final IVibrationBoardService vibrationBoardService;
private final VibrationBoardMapper vibrationBoardMapper;
private final RecordIotenvPartitionService recordIotenvPartitionService;
@Override
public DisplacementOverviewPageVo listOverviewData(DisplacementBoardQueryBo bo) {
VibrationBoardQueryBo query = normalizeDisplacementQueryBo(bo);
VibrationOverviewPageVo overview = vibrationBoardService.listOverviewData(query);
DisplacementQualityPageVo quality = listQualityData(bo);
VibrationOverviewPageVo result = overview == null ? new VibrationOverviewPageVo() : overview;
List<VibrationOverviewPageVo.MetricCardItem> displacementCards = (result.getMetricCards() == null ? Collections.<VibrationOverviewPageVo.MetricCardItem>emptyList() : result.getMetricCards())
.stream()
.filter(item -> DISPLACEMENT_FIELD.equals(item.getField()))
.toList();
return buildDisplacementOverviewVo(result, quality, displacementCards);
}
@Override
public DisplacementTrendPageVo listTrendData(DisplacementBoardQueryBo bo) {
VibrationTrendPageVo trend = vibrationBoardService.listTrendData(normalizeDisplacementQueryBo(bo));
if (trend == null) {
return new DisplacementTrendPageVo();
}
DisplacementTrendPageVo result = new DisplacementTrendPageVo();
result.setMetricField(DISPLACEMENT_FIELD);
result.setMetricLabel(DISPLACEMENT_LABEL);
result.setUnit(DISPLACEMENT_UNIT);
result.setMultiDevice(trend.getMultiDevice());
if (Boolean.FALSE.equals(trend.getMultiDevice())) {
result.setSeries((trend.getSeries() == null ? Collections.<VibrationTrendPageVo.TrendSeriesItem>emptyList() : trend.getSeries())
.stream()
.filter(item -> DISPLACEMENT_FIELD.equals(item.getField()))
.map(this::toDisplacementTrendSeriesItem)
.toList());
} else {
result.setSeries((trend.getSeries() == null ? Collections.<VibrationTrendPageVo.TrendSeriesItem>emptyList() : trend.getSeries())
.stream()
.map(this::toDisplacementTrendSeriesItem)
.toList());
}
result.setHourlyItems((trend.getHourlyItems() == null ? Collections.<VibrationTrendPageVo.HourlyItem>emptyList() : trend.getHourlyItems())
.stream()
.map(this::toDisplacementHourlyItem)
.toList());
return result;
}
@Override
public DisplacementComparisonPageVo listComparisonData(DisplacementBoardQueryBo bo) {
VibrationComparisonPageVo comparison = vibrationBoardService.listComparisonData(normalizeDisplacementQueryBo(bo));
DisplacementComparisonPageVo result = new DisplacementComparisonPageVo();
if (comparison == null) {
return result;
}
result.setMetricField(DISPLACEMENT_FIELD);
result.setMetricLabel(DISPLACEMENT_LABEL);
result.setUnit(DISPLACEMENT_UNIT);
result.setRankItems((comparison.getRankItems() == null ? Collections.<VibrationComparisonPageVo.RankItem>emptyList() : comparison.getRankItems())
.stream()
.map(this::toDisplacementRankItem)
.toList());
result.setScatterItems((comparison.getScatterItems() == null ? Collections.<VibrationComparisonPageVo.ScatterItem>emptyList() : comparison.getScatterItems())
.stream()
.map(this::toDisplacementScatterItem)
.toList());
return result;
}
@Override
public DisplacementQualityPageVo listQualityData(DisplacementBoardQueryBo bo) {
List<RecordIotenvInstant> rows = listQualityPageData(bo);
DisplacementQualityPageVo vo = new DisplacementQualityPageVo();
if (CollUtil.isEmpty(rows)) {
vo.setSampleCount(0);
vo.setDeviceCount(0);
vo.setCoverageRate(zeroRate());
vo.setValidCount(0);
vo.setInvalidCount(0);
vo.setValidRate(zeroRate());
vo.setInvalidRate(zeroRate());
vo.setMetricQualityItems(Collections.emptyList());
return vo;
}
int validCount = countValidDisplacementRows(rows);
int sampleCount = rows.size();
int invalidCount = sampleCount - validCount;
BigDecimal validRate = divideRate(validCount, sampleCount);
vo.setSampleCount(sampleCount);
vo.setDeviceCount(countDistinctMonitors(rows));
vo.setCoverageRate(validRate);
vo.setValidCount(validCount);
vo.setInvalidCount(invalidCount);
vo.setValidRate(validRate);
vo.setInvalidRate(divideRate(invalidCount, sampleCount));
vo.setMetricQualityItems(List.of(buildDisplacementQualityItem(validCount, sampleCount)));
return vo;
}
@Override
public DisplacementDistributionPageVo listDistributionData(DisplacementBoardQueryBo bo) {
VibrationDistributionPageVo distribution = vibrationBoardService.listDistributionData(normalizeDisplacementQueryBo(bo));
DisplacementDistributionPageVo result = new DisplacementDistributionPageVo();
if (distribution == null) {
return result;
}
result.setMetricField(DISPLACEMENT_FIELD);
result.setMetricLabel(DISPLACEMENT_LABEL);
result.setUnit(DISPLACEMENT_UNIT);
result.setIntervalBuckets((distribution.getIntervalBuckets() == null ? Collections.<VibrationDistributionPageVo.IntervalBucketItem>emptyList() : distribution.getIntervalBuckets())
.stream()
.map(this::toDisplacementIntervalBucketItem)
.toList());
result.setHistogramBuckets((distribution.getHistogramBuckets() == null ? Collections.<VibrationDistributionPageVo.HistogramBucketItem>emptyList() : distribution.getHistogramBuckets())
.stream()
.map(this::toDisplacementHistogramBucketItem)
.toList());
result.setCalendarHeatmap((distribution.getCalendarHeatmap() == null ? Collections.<VibrationDistributionPageVo.CalendarHeatmapItem>emptyList() : distribution.getCalendarHeatmap())
.stream()
.map(this::toDisplacementCalendarHeatmapItem)
.toList());
result.setHourlyHeatmap((distribution.getHourlyHeatmap() == null ? Collections.<VibrationDistributionPageVo.HourlyHeatmapItem>emptyList() : distribution.getHourlyHeatmap())
.stream()
.map(this::toDisplacementHourlyHeatmapItem)
.toList());
return result;
}
@Override
public DisplacementAnomalyPageVo listAnomalyData(DisplacementBoardQueryBo bo) {
VibrationAnomalyPageVo anomaly = vibrationBoardService.listAnomalyData(normalizeDisplacementQueryBo(bo));
DisplacementAnomalyPageVo result = new DisplacementAnomalyPageVo();
if (anomaly == null) {
return result;
}
result.setMetricField(DISPLACEMENT_FIELD);
result.setMetricLabel(DISPLACEMENT_LABEL);
result.setUnit(DISPLACEMENT_UNIT);
result.setHighThreshold(anomaly.getHighThreshold());
result.setWarningThreshold(anomaly.getWarningThreshold());
result.setRapidRiseThreshold(anomaly.getRapidRiseThreshold());
result.setStddevThreshold(anomaly.getStddevThreshold());
result.setMinContinuousSamples(anomaly.getMinContinuousSamples());
result.setHighEventCount(anomaly.getHighEventCount());
result.setContinuousEventCount(anomaly.getContinuousEventCount());
result.setRapidRiseEventCount(anomaly.getRapidRiseEventCount());
result.setJitterEventCount(anomaly.getJitterEventCount());
result.setHighEvents((anomaly.getHighEvents() == null ? Collections.<VibrationAnomalyPageVo.HighEventItem>emptyList() : anomaly.getHighEvents())
.stream()
.map(this::toDisplacementHighEventItem)
.toList());
result.setContinuousEvents((anomaly.getContinuousEvents() == null ? Collections.<VibrationAnomalyPageVo.ContinuousEventItem>emptyList() : anomaly.getContinuousEvents())
.stream()
.map(this::toDisplacementContinuousEventItem)
.toList());
result.setRapidRiseEvents((anomaly.getRapidRiseEvents() == null ? Collections.<VibrationAnomalyPageVo.RapidRiseEventItem>emptyList() : anomaly.getRapidRiseEvents())
.stream()
.map(this::toDisplacementRapidRiseEventItem)
.toList());
result.setJitterEvents((anomaly.getJitterEvents() == null ? Collections.<VibrationAnomalyPageVo.JitterEventItem>emptyList() : anomaly.getJitterEvents())
.stream()
.map(this::toDisplacementJitterEventItem)
.toList());
return result;
}
@Override
public DisplacementAdvancedPageVo listAdvancedData(DisplacementBoardQueryBo bo) {
VibrationAdvancedPageVo advanced = vibrationBoardService.listAdvancedData(normalizeDisplacementQueryBo(bo));
if (advanced == null) {
return new DisplacementAdvancedPageVo();
}
DisplacementAdvancedPageVo result = new DisplacementAdvancedPageVo();
result.setMetricField(DISPLACEMENT_FIELD);
result.setMetricLabel(DISPLACEMENT_LABEL);
result.setUnit(DISPLACEMENT_UNIT);
result.setLowBandUpper(advanced.getLowBandUpper());
result.setFocusBandUpper(advanced.getFocusBandUpper());
result.setSankeyNodes((advanced.getSankeyNodes() == null ? Collections.<VibrationAdvancedPageVo.SankeyNodeItem>emptyList() : advanced.getSankeyNodes())
.stream()
.map(this::toDisplacementSankeyNodeItem)
.toList());
result.setSankeyLinks((advanced.getSankeyLinks() == null ? Collections.<VibrationAdvancedPageVo.SankeyLinkItem>emptyList() : advanced.getSankeyLinks())
.stream()
.map(this::toDisplacementSankeyLinkItem)
.toList());
result.setTreemapItems((advanced.getTreemapItems() == null ? Collections.<VibrationAdvancedPageVo.TreemapItem>emptyList() : advanced.getTreemapItems())
.stream()
.map(this::toDisplacementTreemapItem)
.toList());
// 位移专属高级页只保留风险分带分析,不再输出四指标平行坐标画像。
result.setParallelAxes(Collections.emptyList());
result.setParallelSeries(Collections.emptyList());
return result;
}
/**
* 使
*/
private List<RecordIotenvInstant> listQualityPageData(DisplacementBoardQueryBo bo) {
if (bo == null) {
throw new ServiceException("查询参数不能为空");
}
Date beginTime = parseDateTime(bo.getBeginRecordTime(), "开始记录时间");
Date endTime = parseDateTime(bo.getEndRecordTime(), "结束记录时间");
if (beginTime.after(endTime)) {
throw new ServiceException("开始记录时间不能晚于结束记录时间");
}
validateQuerySpan(beginTime, endTime);
List<String> monitorIds = normalizeMonitorIds(bo);
if (CollUtil.isEmpty(monitorIds)) {
throw new ServiceException("请选择至少一个振动设备");
}
VibrationBoardQueryBo query = normalizeDisplacementQueryBo(bo);
query.setMonitorId(monitorIds.size() == 1 ? monitorIds.get(0) : null);
query.setMonitorIds(monitorIds.size() > 1 ? monitorIds : null);
validateEstimatedQueryRows(beginTime, endTime, monitorIds.size(), query.getSamplingInterval());
List<String> tableNames = recordIotenvPartitionService.resolveTables(beginTime, endTime);
if (CollUtil.isEmpty(tableNames)) {
return Collections.emptyList();
}
validateResolvedTableNames(tableNames);
return queryQualityDataByTableBatches(tableNames, query);
}
private List<RecordIotenvInstant> queryQualityDataByTableBatches(List<String> tableNames, VibrationBoardQueryBo query) {
if (tableNames.size() <= MAX_UNION_TABLES) {
return executeSingleBatchQualityQuery(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 = executeSingleBatchQualityQuery(batchTableNames, query);
if (CollUtil.isNotEmpty(batchRows)) {
mergedRows.addAll(batchRows);
}
}
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> executeSingleBatchQualityQuery(List<String> batchTableNames, VibrationBoardQueryBo query) {
if (query.getSamplingInterval() != null && query.getSamplingInterval() > 1) {
return vibrationBoardMapper.selectQualitySampledData(batchTableNames, query);
}
return vibrationBoardMapper.selectQualityRawData(batchTableNames, query);
}
private VibrationBoardQueryBo normalizeDisplacementQueryBo(DisplacementBoardQueryBo source) {
if (source == null) {
throw new ServiceException("查询参数不能为空");
}
VibrationBoardQueryBo query = new VibrationBoardQueryBo();
query.setMonitorId(source.getMonitorId());
query.setMonitorIds(source.getMonitorIds());
query.setBeginRecordTime(source.getBeginRecordTime());
query.setEndRecordTime(source.getEndRecordTime());
query.setSamplingInterval(normalizeSamplingInterval(source.getSamplingInterval()));
// 位移专属接口在入参层强制收口,避免错误传参被悄悄转成其它振动指标。
query.setVibrationParam(DISPLACEMENT_FIELD);
query.setHighThreshold(source.getHighThreshold());
query.setWarningThreshold(source.getWarningThreshold());
query.setMinContinuousSamples(source.getMinContinuousSamples());
query.setRapidRiseThreshold(source.getRapidRiseThreshold());
query.setStddevThreshold(source.getStddevThreshold());
return query;
}
private int countDistinctMonitors(List<RecordIotenvInstant> rows) {
return (int) rows.stream()
.map(RecordIotenvInstant::getMonitorId)
.filter(StrUtil::isNotBlank)
.distinct()
.count();
}
private int countValidDisplacementRows(List<RecordIotenvInstant> rows) {
return (int) rows.stream()
.map(RecordIotenvInstant::getVibrationDisplacement)
.filter(value -> value != null && value.compareTo(BigDecimal.ZERO) > 0)
.count();
}
private BigDecimal zeroRate() {
return BigDecimal.ZERO.setScale(4, RoundingMode.HALF_UP);
}
private BigDecimal divideRate(int numerator, int denominator) {
if (denominator <= 0) {
return zeroRate();
}
return BigDecimal.valueOf(numerator).divide(BigDecimal.valueOf(denominator), 4, RoundingMode.HALF_UP);
}
private DisplacementQualityPageVo.MetricQualityItem buildDisplacementQualityItem(int validCount, int sampleCount) {
DisplacementQualityPageVo.MetricQualityItem item = new DisplacementQualityPageVo.MetricQualityItem();
item.setField(DISPLACEMENT_FIELD);
item.setLabel(DISPLACEMENT_LABEL);
item.setUnit(DISPLACEMENT_UNIT);
item.setValidCount(validCount);
item.setValidRate(divideRate(validCount, sampleCount));
return item;
}
private List<DisplacementOverviewPageVo.GaugeItem> buildDisplacementGaugeItems(
List<VibrationOverviewPageVo.MetricCardItem> metricCards,
VibrationOverviewPageVo.PrimaryMetricStats primaryMetricStats,
Integer deviceCount
) {
if (CollUtil.isNotEmpty(metricCards)) {
VibrationOverviewPageVo.MetricCardItem card = metricCards.get(0);
BigDecimal value = deviceCount != null && deviceCount > 1 ? card.getAvg() : card.getLatest();
String name = deviceCount != null && deviceCount > 1 ? "群组均值" : card.getLabel();
return List.of(buildGaugeItem(name, value, card.getMax(), card.getUnit()));
}
if (primaryMetricStats == null) {
return Collections.emptyList();
}
BigDecimal value = deviceCount != null && deviceCount > 1 ? primaryMetricStats.getAvg() : primaryMetricStats.getLatest();
String name = deviceCount != null && deviceCount > 1 ? "群组均值" : DISPLACEMENT_LABEL;
return List.of(buildGaugeItem(name, value, primaryMetricStats.getMax(), DISPLACEMENT_UNIT));
}
private DisplacementOverviewPageVo buildDisplacementOverviewVo(
VibrationOverviewPageVo overview,
DisplacementQualityPageVo quality,
List<VibrationOverviewPageVo.MetricCardItem> displacementCards
) {
DisplacementOverviewPageVo result = new DisplacementOverviewPageVo();
result.setMetricField(DISPLACEMENT_FIELD);
result.setMetricLabel(DISPLACEMENT_LABEL);
result.setUnit(DISPLACEMENT_UNIT);
result.setSampleCount(quality.getSampleCount());
result.setDeviceCount(quality.getDeviceCount());
result.setCoverageRate(quality.getCoverageRate());
result.setMetricCards(displacementCards.stream().map(this::toDisplacementMetricCardItem).toList());
result.setPrimaryMetricStats(toDisplacementPrimaryMetricStats(overview.getPrimaryMetricStats()));
result.setDeviceRanks((overview.getDeviceRanks() == null ? Collections.<VibrationOverviewPageVo.DeviceRankItem>emptyList() : overview.getDeviceRanks())
.stream()
.map(this::toDisplacementDeviceRankItem)
.toList());
result.setGaugeItems(buildDisplacementGaugeItems(displacementCards, overview.getPrimaryMetricStats(), quality.getDeviceCount()));
return result;
}
private DisplacementOverviewPageVo.GaugeItem buildGaugeItem(String name, BigDecimal value, BigDecimal maxValue, String unit) {
DisplacementOverviewPageVo.GaugeItem item = new DisplacementOverviewPageVo.GaugeItem();
item.setName(name);
item.setValue(value);
item.setMaxValue(maxValue == null ? BigDecimal.ONE : maxValue.multiply(BigDecimal.valueOf(1.2D)).setScale(2, RoundingMode.HALF_UP));
item.setUnit(unit);
return item;
}
private DisplacementOverviewPageVo.MetricCardItem toDisplacementMetricCardItem(VibrationOverviewPageVo.MetricCardItem source) {
DisplacementOverviewPageVo.MetricCardItem item = new DisplacementOverviewPageVo.MetricCardItem();
item.setField(source.getField());
item.setLabel(source.getLabel());
item.setUnit(source.getUnit());
item.setLatest(source.getLatest());
item.setAvg(source.getAvg());
item.setMax(source.getMax());
return item;
}
private DisplacementOverviewPageVo.PrimaryMetricStats toDisplacementPrimaryMetricStats(VibrationOverviewPageVo.PrimaryMetricStats source) {
DisplacementOverviewPageVo.PrimaryMetricStats stats = new DisplacementOverviewPageVo.PrimaryMetricStats();
if (source == null) {
return stats;
}
stats.setLatest(source.getLatest());
stats.setMin(source.getMin());
stats.setAvg(source.getAvg());
stats.setMax(source.getMax());
return stats;
}
private DisplacementOverviewPageVo.DeviceRankItem toDisplacementDeviceRankItem(VibrationOverviewPageVo.DeviceRankItem source) {
DisplacementOverviewPageVo.DeviceRankItem item = new DisplacementOverviewPageVo.DeviceRankItem();
item.setMonitorId(source.getMonitorId());
item.setMonitorName(source.getMonitorName());
item.setAvg(source.getAvg());
item.setLatest(source.getLatest());
item.setMax(source.getMax());
item.setSampleCount(source.getSampleCount());
return item;
}
private DisplacementTrendPageVo.TrendSeriesItem toDisplacementTrendSeriesItem(VibrationTrendPageVo.TrendSeriesItem source) {
DisplacementTrendPageVo.TrendSeriesItem item = new DisplacementTrendPageVo.TrendSeriesItem();
item.setName(source.getName());
item.setField(source.getField());
item.setUnit(source.getUnit());
item.setPoints((source.getPoints() == null ? Collections.<VibrationTrendPageVo.TrendPointItem>emptyList() : source.getPoints())
.stream()
.map(this::toDisplacementTrendPointItem)
.toList());
return item;
}
private DisplacementTrendPageVo.TrendPointItem toDisplacementTrendPointItem(VibrationTrendPageVo.TrendPointItem source) {
DisplacementTrendPageVo.TrendPointItem item = new DisplacementTrendPageVo.TrendPointItem();
item.setTime(source.getTime());
item.setValue(source.getValue());
return item;
}
private DisplacementTrendPageVo.HourlyItem toDisplacementHourlyItem(VibrationTrendPageVo.HourlyItem source) {
DisplacementTrendPageVo.HourlyItem item = new DisplacementTrendPageVo.HourlyItem();
item.setHour(source.getHour());
item.setAvgValue(source.getAvgValue());
return item;
}
private DisplacementComparisonPageVo.RankItem toDisplacementRankItem(VibrationComparisonPageVo.RankItem source) {
DisplacementComparisonPageVo.RankItem item = new DisplacementComparisonPageVo.RankItem();
item.setMonitorId(source.getMonitorId());
item.setMonitorName(source.getMonitorName());
item.setAvg(source.getAvg());
item.setLatest(source.getLatest());
return item;
}
private DisplacementComparisonPageVo.ScatterItem toDisplacementScatterItem(VibrationComparisonPageVo.ScatterItem source) {
DisplacementComparisonPageVo.ScatterItem item = new DisplacementComparisonPageVo.ScatterItem();
item.setMonitorId(source.getMonitorId());
item.setMonitorName(source.getMonitorName());
item.setAvg(source.getAvg());
item.setMax(source.getMax());
item.setSampleCount(source.getSampleCount());
return item;
}
private DisplacementDistributionPageVo.IntervalBucketItem toDisplacementIntervalBucketItem(VibrationDistributionPageVo.IntervalBucketItem source) {
DisplacementDistributionPageVo.IntervalBucketItem item = new DisplacementDistributionPageVo.IntervalBucketItem();
item.setLabel(source.getLabel());
item.setCount(source.getCount());
return item;
}
private DisplacementDistributionPageVo.HistogramBucketItem toDisplacementHistogramBucketItem(VibrationDistributionPageVo.HistogramBucketItem source) {
DisplacementDistributionPageVo.HistogramBucketItem item = new DisplacementDistributionPageVo.HistogramBucketItem();
item.setStartValue(source.getStartValue());
item.setEndValue(source.getEndValue());
item.setCount(source.getCount());
return item;
}
private DisplacementDistributionPageVo.CalendarHeatmapItem toDisplacementCalendarHeatmapItem(VibrationDistributionPageVo.CalendarHeatmapItem source) {
DisplacementDistributionPageVo.CalendarHeatmapItem item = new DisplacementDistributionPageVo.CalendarHeatmapItem();
item.setStatDate(source.getStatDate());
item.setAvgValue(source.getAvgValue());
return item;
}
private DisplacementDistributionPageVo.HourlyHeatmapItem toDisplacementHourlyHeatmapItem(VibrationDistributionPageVo.HourlyHeatmapItem source) {
DisplacementDistributionPageVo.HourlyHeatmapItem item = new DisplacementDistributionPageVo.HourlyHeatmapItem();
item.setStatDate(source.getStatDate());
item.setStatHour(source.getStatHour());
item.setAvgValue(source.getAvgValue());
return item;
}
private DisplacementAnomalyPageVo.HighEventItem toDisplacementHighEventItem(VibrationAnomalyPageVo.HighEventItem source) {
DisplacementAnomalyPageVo.HighEventItem item = new DisplacementAnomalyPageVo.HighEventItem();
item.setMonitorId(source.getMonitorId());
item.setMonitorName(source.getMonitorName());
item.setValue(source.getValue());
item.setRecodeTime(source.getRecodeTime());
return item;
}
private DisplacementAnomalyPageVo.ContinuousEventItem toDisplacementContinuousEventItem(VibrationAnomalyPageVo.ContinuousEventItem source) {
DisplacementAnomalyPageVo.ContinuousEventItem item = new DisplacementAnomalyPageVo.ContinuousEventItem();
item.setMonitorId(source.getMonitorId());
item.setMonitorName(source.getMonitorName());
item.setStartTime(source.getStartTime());
item.setEndTime(source.getEndTime());
item.setMaxValue(source.getMaxValue());
item.setSampleCount(source.getSampleCount());
return item;
}
private DisplacementAnomalyPageVo.RapidRiseEventItem toDisplacementRapidRiseEventItem(VibrationAnomalyPageVo.RapidRiseEventItem source) {
DisplacementAnomalyPageVo.RapidRiseEventItem item = new DisplacementAnomalyPageVo.RapidRiseEventItem();
item.setMonitorId(source.getMonitorId());
item.setMonitorName(source.getMonitorName());
item.setDiff(source.getDiff());
item.setRecodeTime(source.getRecodeTime());
return item;
}
private DisplacementAnomalyPageVo.JitterEventItem toDisplacementJitterEventItem(VibrationAnomalyPageVo.JitterEventItem source) {
DisplacementAnomalyPageVo.JitterEventItem item = new DisplacementAnomalyPageVo.JitterEventItem();
item.setMonitorId(source.getMonitorId());
item.setMonitorName(source.getMonitorName());
item.setHourBucket(source.getHourBucket());
item.setStddev(source.getStddev());
item.setSampleCount(source.getSampleCount());
return item;
}
private DisplacementAdvancedPageVo.SankeyNodeItem toDisplacementSankeyNodeItem(VibrationAdvancedPageVo.SankeyNodeItem source) {
DisplacementAdvancedPageVo.SankeyNodeItem item = new DisplacementAdvancedPageVo.SankeyNodeItem();
item.setName(source.getName());
return item;
}
private DisplacementAdvancedPageVo.SankeyLinkItem toDisplacementSankeyLinkItem(VibrationAdvancedPageVo.SankeyLinkItem source) {
DisplacementAdvancedPageVo.SankeyLinkItem item = new DisplacementAdvancedPageVo.SankeyLinkItem();
item.setSource(source.getSource());
item.setTarget(source.getTarget());
item.setValue(source.getValue());
return item;
}
private DisplacementAdvancedPageVo.TreemapItem toDisplacementTreemapItem(VibrationAdvancedPageVo.TreemapItem source) {
DisplacementAdvancedPageVo.TreemapItem item = new DisplacementAdvancedPageVo.TreemapItem();
item.setName(source.getName());
item.setValue(source.getValue());
item.setLevelTag(source.getLevelTag());
return item;
}
private List<String> normalizeMonitorIds(VibrationBoardQueryBo bo) {
Set<String> monitorIdSet = new LinkedHashSet<>();
if (StrUtil.isNotBlank(bo.getMonitorId())) {
monitorIdSet.add(bo.getMonitorId().trim());
}
if (CollUtil.isNotEmpty(bo.getMonitorIds())) {
for (String monitorId : bo.getMonitorIds()) {
if (StrUtil.isNotBlank(monitorId)) {
monitorIdSet.add(monitorId.trim());
}
}
}
return new ArrayList<>(monitorIdSet);
}
private Integer normalizeSamplingInterval(Integer samplingInterval) {
if (samplingInterval == null || samplingInterval < 1) {
return 1;
}
return Math.min(samplingInterval, 1440);
}
private Date parseDateTime(String value, String fieldName) {
if (StrUtil.isBlank(value)) {
throw new ServiceException(fieldName + "不能为空");
}
try {
LocalDateTime localDateTime = LocalDateTime.parse(value.trim(), DATE_TIME_FORMATTER);
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
} catch (DateTimeParseException ex) {
throw new ServiceException(fieldName + "格式不正确,请使用 yyyy-MM-dd HH:mm:ss");
}
}
private void validateQuerySpan(Date beginTime, Date endTime) {
long diffMs = endTime.getTime() - beginTime.getTime();
long diffDays = diffMs / (24L * 3600L * 1000L);
if (diffDays > MAX_QUERY_DAYS) {
throw new ServiceException("查询跨度不能超过" + MAX_QUERY_DAYS + "天,请缩小时间范围");
}
}
private void validateEstimatedQueryRows(Date beginTime, Date endTime, int monitorCount, Integer samplingInterval) {
int effectiveSamplingInterval = normalizeSamplingInterval(samplingInterval);
long diffMinutes = Math.max(1L, (endTime.getTime() - beginTime.getTime()) / (60L * 1000L) + 1L);
long rowsPerMonitor = (long) Math.ceil((double) diffMinutes / effectiveSamplingInterval);
long estimatedRows = rowsPerMonitor * Math.max(1, monitorCount);
if (estimatedRows > MAX_ESTIMATED_QUERY_ROWS) {
long recommendedSamplingInterval = Math.max(1L, (long) Math.ceil((double) diffMinutes * Math.max(1, monitorCount) / MAX_ESTIMATED_QUERY_ROWS));
throw new ServiceException("当前查询预计返回约" + estimatedRows
+ "条记录,超过系统上限" + MAX_ESTIMATED_QUERY_ROWS
+ "条,请将抽样间隔至少调整为" + recommendedSamplingInterval + "分钟,或缩小时间范围/设备范围");
}
}
private void validateResolvedTableNames(List<String> tableNames) {
for (String tableName : tableNames) {
if (!TABLE_NAME_PATTERN.matcher(tableName).matches()) {
throw new ServiceException("非法分表名称:" + tableName);
}
}
}
}
Loading…
Cancel
Save