diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/controller/DisplacementBoardController.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/controller/DisplacementBoardController.java
new file mode 100644
index 0000000..37a8e39
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/controller/DisplacementBoardController.java
@@ -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。
+ *
+ *
本 Controller 是本轮“位移单独文件”要求的主入口。
+ * 权限点当前先复用旧振动看板权限,避免因菜单/权限配置未同步而导致接口无法访问。
+ */
+@RestController
+@RequiredArgsConstructor
+@RequestMapping("/ems/report/displacementBoard")
+public class DisplacementBoardController {
+
+ private final IDisplacementBoardService displacementBoardService;
+
+ @SaCheckPermission("ems/report:vibrationBoard:query")
+ @GetMapping("/overview")
+ public R overview(DisplacementBoardQueryBo bo) {
+ return R.ok(displacementBoardService.listOverviewData(bo));
+ }
+
+ @SaCheckPermission("ems/report:vibrationBoard:query")
+ @GetMapping("/trend")
+ public R trend(DisplacementBoardQueryBo bo) {
+ return R.ok(displacementBoardService.listTrendData(bo));
+ }
+
+ @SaCheckPermission("ems/report:vibrationBoard:query")
+ @GetMapping("/comparison")
+ public R comparison(DisplacementBoardQueryBo bo) {
+ return R.ok(displacementBoardService.listComparisonData(bo));
+ }
+
+ @SaCheckPermission("ems/report:vibrationBoard:query")
+ @GetMapping("/quality")
+ public R quality(DisplacementBoardQueryBo bo) {
+ return R.ok(displacementBoardService.listQualityData(bo));
+ }
+
+ @SaCheckPermission("ems/report:vibrationBoard:query")
+ @GetMapping("/distribution")
+ public R distribution(DisplacementBoardQueryBo bo) {
+ return R.ok(displacementBoardService.listDistributionData(bo));
+ }
+
+ @SaCheckPermission("ems/report:vibrationBoard:query")
+ @GetMapping("/anomaly")
+ public R anomaly(DisplacementBoardQueryBo bo) {
+ return R.ok(displacementBoardService.listAnomalyData(bo));
+ }
+
+ @SaCheckPermission("ems/report:vibrationBoard:query")
+ @GetMapping("/advanced")
+ public R advanced(DisplacementBoardQueryBo bo) {
+ return R.ok(displacementBoardService.listAdvancedData(bo));
+ }
+}
diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/bo/DisplacementBoardQueryBo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/bo/DisplacementBoardQueryBo.java
new file mode 100644
index 0000000..166638c
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/bo/DisplacementBoardQueryBo.java
@@ -0,0 +1,11 @@
+package org.dromara.ems.report.domain.bo;
+
+/**
+ * 位移专属看板查询参数。
+ *
+ * 当前先复用振动看板已有字段结构,
+ * 目的是在不破坏现有校验与序列化行为的前提下,
+ * 为 displacementBoard 独立 Controller / Service 提供单独文件入口。
+ */
+public class DisplacementBoardQueryBo extends VibrationBoardQueryBo {
+}
diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementAdvancedPageVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementAdvancedPageVo.java
new file mode 100644
index 0000000..52fd164
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementAdvancedPageVo.java
@@ -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 sankeyNodes;
+ private List sankeyLinks;
+ private List treemapItems;
+ private List parallelAxes;
+ private List 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 values;
+ }
+}
diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementAnomalyPageVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementAnomalyPageVo.java
new file mode 100644
index 0000000..09896e9
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementAnomalyPageVo.java
@@ -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 highEvents;
+ private List continuousEvents;
+ private List rapidRiseEvents;
+ private List 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;
+ }
+}
diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementComparisonPageVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementComparisonPageVo.java
new file mode 100644
index 0000000..426d417
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementComparisonPageVo.java
@@ -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 rankItems;
+ private List 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;
+ }
+}
diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementDistributionPageVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementDistributionPageVo.java
new file mode 100644
index 0000000..6454ad5
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementDistributionPageVo.java
@@ -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 intervalBuckets;
+ private List histogramBuckets;
+ private List calendarHeatmap;
+ private List 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;
+ }
+}
diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementOverviewPageVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementOverviewPageVo.java
new file mode 100644
index 0000000..ee21eba
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementOverviewPageVo.java
@@ -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 metricCards;
+ private List gaugeItems;
+ private PrimaryMetricStats primaryMetricStats;
+ private List 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;
+ }
+}
diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementQualityPageVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementQualityPageVo.java
new file mode 100644
index 0000000..e1b53a7
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementQualityPageVo.java
@@ -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 metricQualityItems;
+
+ @Data
+ public static class MetricQualityItem {
+ private String field;
+ private String label;
+ private String unit;
+ private BigDecimal validRate;
+ private Integer validCount;
+ }
+}
diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementTrendPageVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementTrendPageVo.java
new file mode 100644
index 0000000..f83dfd5
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/displacementboard/DisplacementTrendPageVo.java
@@ -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 series;
+ private List hourlyItems;
+
+ @Data
+ public static class TrendSeriesItem {
+ private String name;
+ private String field;
+ private String unit;
+ private List points;
+ }
+
+ @Data
+ public static class TrendPointItem {
+ private String time;
+ private BigDecimal value;
+ }
+
+ @Data
+ public static class HourlyItem {
+ private String hour;
+ private BigDecimal avgValue;
+ }
+}
diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/service/IDisplacementBoardService.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/service/IDisplacementBoardService.java
new file mode 100644
index 0000000..aab0fff
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/service/IDisplacementBoardService.java
@@ -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;
+
+/**
+ * 位移专属看板服务接口。
+ *
+ * 当前返回体先复用振动看板 VO,
+ * 这样可以在新增 displacementBoard 独立文件的同时,
+ * 保持与已存在页面结构的兼容性。
+ */
+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);
+}
diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/service/impl/DisplacementBoardServiceImpl.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/service/impl/DisplacementBoardServiceImpl.java
new file mode 100644
index 0000000..ebf350e
--- /dev/null
+++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/service/impl/DisplacementBoardServiceImpl.java
@@ -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;
+
+/**
+ * 位移专属看板服务实现。
+ *
+ * 职责边界:
+ * 1. 振动通用能力仍由 {@link IVibrationBoardService} 提供
+ * 2. 位移专属统计口径、结果裁剪和质量页分母逻辑全部收口在本类
+ * 3. 不再把位移专属实现塞回 VibrationBoardServiceImpl,避免污染振动多指标兼容能力
+ */
+@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 displacementCards = (result.getMetricCards() == null ? Collections.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.emptyList() : trend.getSeries())
+ .stream()
+ .filter(item -> DISPLACEMENT_FIELD.equals(item.getField()))
+ .map(this::toDisplacementTrendSeriesItem)
+ .toList());
+ } else {
+ result.setSeries((trend.getSeries() == null ? Collections.emptyList() : trend.getSeries())
+ .stream()
+ .map(this::toDisplacementTrendSeriesItem)
+ .toList());
+ }
+ result.setHourlyItems((trend.getHourlyItems() == null ? Collections.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.emptyList() : comparison.getRankItems())
+ .stream()
+ .map(this::toDisplacementRankItem)
+ .toList());
+ result.setScatterItems((comparison.getScatterItems() == null ? Collections.emptyList() : comparison.getScatterItems())
+ .stream()
+ .map(this::toDisplacementScatterItem)
+ .toList());
+ return result;
+ }
+
+ @Override
+ public DisplacementQualityPageVo listQualityData(DisplacementBoardQueryBo bo) {
+ List 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.emptyList() : distribution.getIntervalBuckets())
+ .stream()
+ .map(this::toDisplacementIntervalBucketItem)
+ .toList());
+ result.setHistogramBuckets((distribution.getHistogramBuckets() == null ? Collections.emptyList() : distribution.getHistogramBuckets())
+ .stream()
+ .map(this::toDisplacementHistogramBucketItem)
+ .toList());
+ result.setCalendarHeatmap((distribution.getCalendarHeatmap() == null ? Collections.emptyList() : distribution.getCalendarHeatmap())
+ .stream()
+ .map(this::toDisplacementCalendarHeatmapItem)
+ .toList());
+ result.setHourlyHeatmap((distribution.getHourlyHeatmap() == null ? Collections.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.emptyList() : anomaly.getHighEvents())
+ .stream()
+ .map(this::toDisplacementHighEventItem)
+ .toList());
+ result.setContinuousEvents((anomaly.getContinuousEvents() == null ? Collections.emptyList() : anomaly.getContinuousEvents())
+ .stream()
+ .map(this::toDisplacementContinuousEventItem)
+ .toList());
+ result.setRapidRiseEvents((anomaly.getRapidRiseEvents() == null ? Collections.emptyList() : anomaly.getRapidRiseEvents())
+ .stream()
+ .map(this::toDisplacementRapidRiseEventItem)
+ .toList());
+ result.setJitterEvents((anomaly.getJitterEvents() == null ? Collections.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.emptyList() : advanced.getSankeyNodes())
+ .stream()
+ .map(this::toDisplacementSankeyNodeItem)
+ .toList());
+ result.setSankeyLinks((advanced.getSankeyLinks() == null ? Collections.emptyList() : advanced.getSankeyLinks())
+ .stream()
+ .map(this::toDisplacementSankeyLinkItem)
+ .toList());
+ result.setTreemapItems((advanced.getTreemapItems() == null ? Collections.emptyList() : advanced.getTreemapItems())
+ .stream()
+ .map(this::toDisplacementTreemapItem)
+ .toList());
+ // 位移专属高级页只保留风险分带分析,不再输出四指标平行坐标画像。
+ result.setParallelAxes(Collections.emptyList());
+ result.setParallelSeries(Collections.emptyList());
+ return result;
+ }
+
+ /**
+ * 质量页底层查询:只在本类内部使用,避免把位移专属分母口径继续混进振动通用服务。
+ */
+ private List 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 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 tableNames = recordIotenvPartitionService.resolveTables(beginTime, endTime);
+ if (CollUtil.isEmpty(tableNames)) {
+ return Collections.emptyList();
+ }
+ validateResolvedTableNames(tableNames);
+ return queryQualityDataByTableBatches(tableNames, query);
+ }
+
+ private List queryQualityDataByTableBatches(List tableNames, VibrationBoardQueryBo query) {
+ if (tableNames.size() <= MAX_UNION_TABLES) {
+ return executeSingleBatchQualityQuery(tableNames, query);
+ }
+ List 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 batchTableNames = tableNames.subList(index, endIndex);
+ List 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 executeSingleBatchQualityQuery(List 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 rows) {
+ return (int) rows.stream()
+ .map(RecordIotenvInstant::getMonitorId)
+ .filter(StrUtil::isNotBlank)
+ .distinct()
+ .count();
+ }
+
+ private int countValidDisplacementRows(List 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 buildDisplacementGaugeItems(
+ List 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 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.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.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 normalizeMonitorIds(VibrationBoardQueryBo bo) {
+ Set 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 tableNames) {
+ for (String tableName : tableNames) {
+ if (!TABLE_NAME_PATTERN.matcher(tableName).matches()) {
+ throw new ServiceException("非法分表名称:" + tableName);
+ }
+ }
+ }
+}