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); + } + } + } +}