diff --git a/ruoyi-admin/src/test/java/org/dromara/test/TempBoardServiceImplTest.java b/ruoyi-admin/src/test/java/org/dromara/test/TempBoardServiceImplTest.java new file mode 100644 index 0000000..5b1acbc --- /dev/null +++ b/ruoyi-admin/src/test/java/org/dromara/test/TempBoardServiceImplTest.java @@ -0,0 +1,124 @@ +package org.dromara.test; + +import org.dromara.common.core.exception.ServiceException; +import org.dromara.ems.record.mapper.RecordIotenvInstantMapper; +import org.dromara.ems.record.service.RecordIotenvPartitionService; +import org.dromara.ems.report.domain.vo.tempboard.TempBoardQueryBo; +import org.dromara.ems.report.mapper.TempBoardMapper; +import org.dromara.ems.report.service.impl.TempBoardServiceImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * 温度看板 Service 回归测试 + * + *

这里优先锁住这次审阅报告里最容易回退的服务层校验逻辑, + * 避免后续又出现“空测点静默查空”和“时间范围边界漏拦截”的问题。

+ */ +@DisplayName("温度看板 Service 回归测试") +public class TempBoardServiceImplTest { + + @Test + @DisplayName("getDailyDiff 在缺少结束时间时应抛出友好异常") + public void shouldThrowWhenDailyDiffMissingEndTime() { + TempBoardServiceImpl service = createService(Collections.singletonList("record_iotenv_instant_20260417")); + TempBoardQueryBo query = new TempBoardQueryBo(); + + ServiceException exception = Assertions.assertThrows(ServiceException.class, () -> service.getDailyDiff(query)); + + Assertions.assertEquals("请选择时间范围", exception.getMessage()); + } + + @Test + @DisplayName("getMinuteTrend 在未选择测点时应快速失败") + public void shouldThrowWhenMinuteTrendMissingMonitorId() { + TempBoardServiceImpl service = createService(Collections.singletonList("record_iotenv_instant_20260417")); + TempBoardQueryBo query = buildRangeQuery(); + query.setMonitorId(" "); + + ServiceException exception = Assertions.assertThrows(ServiceException.class, () -> service.getMinuteTrend(query)); + + Assertions.assertEquals("请选择测点", exception.getMessage()); + } + + @Test + @DisplayName("getHourlyTrend 在未选择测点时应快速失败") + public void shouldThrowWhenHourlyTrendMissingMonitorId() { + TempBoardServiceImpl service = createService(Collections.singletonList("record_iotenv_instant_20260417")); + TempBoardQueryBo query = buildRangeQuery(); + query.setMonitorId(""); + + ServiceException exception = Assertions.assertThrows(ServiceException.class, () -> service.getHourlyTrend(query)); + + Assertions.assertEquals("请选择测点", exception.getMessage()); + } + + @Test + @DisplayName("查询跨度超过 90 天应被拒绝") + public void shouldRejectRangeLongerThanMaxQueryDays() { + TempBoardServiceImpl service = createService(Collections.emptyList()); + TempBoardQueryBo query = new TempBoardQueryBo(); + long ninetyDaysMs = 90L * 24 * 3600 * 1000; + query.setStartTime(new Date(0L)); + // 这里故意多加 1 秒,验证不能再被整数除法边界漏过去。 + query.setEndTime(new Date(ninetyDaysMs + 1000L)); + + ServiceException exception = Assertions.assertThrows(ServiceException.class, () -> service.getOverview(query)); + + Assertions.assertEquals("查询跨度不能超过90天,请缩小范围", exception.getMessage()); + } + + private TempBoardServiceImpl createService(List tables) { + TempBoardMapper tempBoardMapper = (TempBoardMapper) Proxy.newProxyInstance( + TempBoardMapper.class.getClassLoader(), + new Class[]{TempBoardMapper.class}, + (proxy, method, args) -> { + Class returnType = method.getReturnType(); + if (List.class.equals(returnType)) { + return Collections.emptyList(); + } + if (int.class.equals(returnType) || Integer.class.equals(returnType)) { + return 0; + } + return null; + } + ); + return new TempBoardServiceImpl(tempBoardMapper, new StubPartitionService(tables)); + } + + private TempBoardQueryBo buildRangeQuery() { + TempBoardQueryBo query = new TempBoardQueryBo(); + query.setStartTime(new Date(1713312000000L)); + query.setEndTime(new Date(1713398400000L)); + return query; + } + + /** + * 这里用最小桩实现隔离分表元数据查询, + * 让测试只聚焦本次修复的参数校验,而不是数据库依赖。 + */ + private static class StubPartitionService extends RecordIotenvPartitionService { + + private final List tables; + + private StubPartitionService(List tables) { + super((RecordIotenvInstantMapper) Proxy.newProxyInstance( + RecordIotenvInstantMapper.class.getClassLoader(), + new Class[]{RecordIotenvInstantMapper.class}, + (proxy, method, args) -> 0 + )); + this.tables = tables; + } + + @Override + public List resolveTables(Date beginTime, Date endTime) { + return tables; + } + } +} diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/controller/TempBoardController.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/controller/TempBoardController.java index 8344a6d..fc6208a 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/report/controller/TempBoardController.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/controller/TempBoardController.java @@ -3,6 +3,7 @@ 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.common.mybatis.core.page.TableDataInfo; import org.dromara.ems.report.domain.vo.tempboard.*; import org.dromara.ems.report.service.ITempBoardService; import org.springframework.web.bind.annotation.*; @@ -97,6 +98,13 @@ public class TempBoardController { return R.ok(tempBoardService.getMultiCompareTrend(query)); } + /** 温度测点下拉列表 */ + @SaCheckPermission("ems/report:tempBoard:query") + @GetMapping("/monitor/list") + public R> listTempMonitors() { + return R.ok(tempBoardService.listTempMonitors()); + } + /** 日均温趋势 */ @SaCheckPermission("ems/report:tempBoard:query") @GetMapping("/trend/daily") @@ -134,13 +142,6 @@ public class TempBoardController { return R.ok(tempBoardService.getHistogram(query)); } - /** 温度箱线图数据 */ - @SaCheckPermission("ems/report:tempBoard:query") - @GetMapping("/distribution/boxplot") - public R> getBoxplotData(TempBoardQueryBo query) { - return R.ok(tempBoardService.getBoxplotData(query)); - } - /** 日历热力图 */ @SaCheckPermission("ems/report:tempBoard:query") @GetMapping("/distribution/calendarHeatmap") @@ -160,14 +161,14 @@ public class TempBoardController { /** 高温事件 */ @SaCheckPermission("ems/report:tempBoard:query") @GetMapping("/anomaly/highTemp") - public R> getHighTempEvents(TempBoardQueryBo query) { + public R> getHighTempEvents(TempBoardQueryBo query) { return R.ok(tempBoardService.getHighTempEvents(query)); } /** 低温事件 */ @SaCheckPermission("ems/report:tempBoard:query") @GetMapping("/anomaly/lowTemp") - public R> getLowTempEvents(TempBoardQueryBo query) { + public R> getLowTempEvents(TempBoardQueryBo query) { return R.ok(tempBoardService.getLowTempEvents(query)); } @@ -178,10 +179,10 @@ public class TempBoardController { return R.ok(tempBoardService.getContinuousHighTemp(query)); } - /** 温升过快事件 */ + /** 温升过快事件(分页) */ @SaCheckPermission("ems/report:tempBoard:query") @GetMapping("/anomaly/rapidRise") - public R> getRapidRiseEvents(TempBoardQueryBo query) { + public R> getRapidRiseEvents(TempBoardQueryBo query) { return R.ok(tempBoardService.getRapidRiseEvents(query)); } @@ -241,14 +242,14 @@ public class TempBoardController { /** 时间逆序可疑数据 */ @SaCheckPermission("ems/report:tempBoard:query") @GetMapping("/quality/timeReversal") - public R> getTimeReversal(TempBoardQueryBo query) { + public R> getTimeReversal(TempBoardQueryBo query) { return R.ok(tempBoardService.getTimeReversal(query)); } - /** 采样间隔异常 */ + /** 采样间隔异常(分页) */ @SaCheckPermission("ems/report:tempBoard:query") @GetMapping("/quality/samplingGap") - public R> getSamplingGapAnomalies(TempBoardQueryBo query) { + public R> getSamplingGapAnomalies(TempBoardQueryBo query) { return R.ok(tempBoardService.getSamplingGapAnomalies(query)); } diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardAnomalyVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardAnomalyVo.java index 0ef0923..c6103ca 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardAnomalyVo.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardAnomalyVo.java @@ -35,6 +35,12 @@ public class TempBoardAnomalyVo { /** 标准差(用于抖动检测) */ private BigDecimal tempStddev; + /** 小时均温(用于抖动检测时补充排障上下文) */ + private BigDecimal avgTemp; + + /** 统计时段(用于抖动检测) */ + private String statTime; + // ========== 连续高温 ========== /** 连续开始时间 */ diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardMonitorOptionVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardMonitorOptionVo.java new file mode 100644 index 0000000..3f66cac --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardMonitorOptionVo.java @@ -0,0 +1,19 @@ +package org.dromara.ems.report.domain.vo.tempboard; + +import lombok.Data; + +/** + * 温度测点下拉选项 + * + * @author zch + * @date 2026-04-17 + */ +@Data +public class TempBoardMonitorOptionVo { + + /** 测点ID,前端继续沿用 monitorId 命名,避免额外做字段映射。 */ + private String monitorId; + + /** 测点名称,优先展示业务名称,减少手输编码带来的误选成本。 */ + private String monitorName; +} diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardOverviewVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardOverviewVo.java index 765241c..835dd0a 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardOverviewVo.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardOverviewVo.java @@ -27,12 +27,18 @@ public class TempBoardOverviewVo { /** 最高温度测点ID */ private String maxTempMonitorId; + /** 最高温度测点名称 */ + private String maxTempMonitorName; + /** 当前最低温度 */ private BigDecimal minLatestTemp; /** 最低温度测点ID */ private String minTempMonitorId; + /** 最低温度测点名称 */ + private String minTempMonitorName; + /** 高温 TopN */ private List highTempTopN; diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardQueryBo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardQueryBo.java index 6fcc84a..156e611 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardQueryBo.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardQueryBo.java @@ -47,6 +47,9 @@ public class TempBoardQueryBo { /** 单测点趋势查询时的测点ID */ private String monitorId; + /** 连续高温最少样本数(默认2) */ + private Integer continuousMinSamples = 2; + /** 预期采样数(用于完整率计算) */ private Integer expectedSampleCount; diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardTrendVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardTrendVo.java index 555a63d..6f4b3a2 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardTrendVo.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/domain/vo/tempboard/TempBoardTrendVo.java @@ -60,4 +60,18 @@ public class TempBoardTrendVo { /** 前一时刻时间 */ private Date prevTime; + + /** + * 聚合粒度标识(MINUTE / FIFTEEN_MINUTE / HOUR) + *

+ * 多测点对比趋势会根据查询跨度自动选择粒度: + *

    + *
  • ≤ 1 天:分钟
  • + *
  • > 1 天 且 ≤ 7 天:15 分钟
  • + *
  • > 7 天:小时
  • + *
+ * 该字段由 Service 在返回前统一写入,前端据此展示"当前聚合粒度"提示, + * 避免用户把聚合后的折点数量误读为原始采样密度。 + */ + private String granularity; } diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/mapper/TempBoardMapper.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/mapper/TempBoardMapper.java index 696e71b..ee00f66 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/report/mapper/TempBoardMapper.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/mapper/TempBoardMapper.java @@ -94,11 +94,25 @@ public interface TempBoardMapper { @Param("startTime") String startTime, @Param("endTime") String endTime); - /** 多测点对比趋势(分钟粒度) */ + /** + * 多测点对比趋势(支持动态聚合粒度) + *

+ * granularity 取值: + *

    + *
  • MINUTE:分钟聚合(≤ 1 天)
  • + *
  • FIFTEEN_MINUTE:15 分钟聚合(1~7 天)
  • + *
  • HOUR:小时聚合(> 7 天)
  • + *
+ * 由 Service 层根据时间跨度自动决策,避免大时间窗把数据库/网络/浏览器同时打爆。 + */ List selectMultiCompareTrend(@Param("tableNames") List tableNames, @Param("monitorIds") List monitorIds, @Param("startTime") String startTime, - @Param("endTime") String endTime); + @Param("endTime") String endTime, + @Param("granularity") String granularity); + + /** 温度测点下拉列表 */ + List selectTempMonitors(@Param("activeStatus") Integer activeStatus); /** 日均温趋势 */ List selectDailyTrend(@Param("tableNames") List tableNames, @@ -128,11 +142,6 @@ public interface TempBoardMapper { @Param("startTime") String startTime, @Param("endTime") String endTime); - /** 温度箱线图原始数据(按测点) */ - List selectBoxplotData(@Param("tableNames") List tableNames, - @Param("startTime") String startTime, - @Param("endTime") String endTime); - /** 日历热力图 */ List selectCalendarHeatmap(@Param("tableNames") List tableNames, @Param("startTime") String startTime, @@ -149,25 +158,50 @@ public interface TempBoardMapper { List selectHighTempEvents(@Param("tableNames") List tableNames, @Param("startTime") String startTime, @Param("endTime") String endTime, - @Param("highTempThreshold") double highTempThreshold); + @Param("highTempThreshold") double highTempThreshold, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** 高温事件总数 */ + long countHighTempEvents(@Param("tableNames") List tableNames, + @Param("startTime") String startTime, + @Param("endTime") String endTime, + @Param("highTempThreshold") double highTempThreshold); /** 低温事件 */ List selectLowTempEvents(@Param("tableNames") List tableNames, @Param("startTime") String startTime, @Param("endTime") String endTime, - @Param("lowTempThreshold") double lowTempThreshold); + @Param("lowTempThreshold") double lowTempThreshold, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** 低温事件总数 */ + long countLowTempEvents(@Param("tableNames") List tableNames, + @Param("startTime") String startTime, + @Param("endTime") String endTime, + @Param("lowTempThreshold") double lowTempThreshold); /** 连续高温时段(岛屿分组) */ List selectContinuousHighTemp(@Param("tableNames") List tableNames, @Param("startTime") String startTime, @Param("endTime") String endTime, - @Param("highTempThreshold") double highTempThreshold); + @Param("highTempThreshold") double highTempThreshold, + @Param("continuousMinSamples") int continuousMinSamples); - /** 温升过快事件 */ + /** 温升过快事件(分页) */ List selectRapidRiseEvents(@Param("tableNames") List tableNames, @Param("startTime") String startTime, @Param("endTime") String endTime, - @Param("rapidRiseThreshold") double rapidRiseThreshold); + @Param("rapidRiseThreshold") double rapidRiseThreshold, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** 温升过快事件总数 */ + long countRapidRiseEvents(@Param("tableNames") List tableNames, + @Param("startTime") String startTime, + @Param("endTime") String endTime, + @Param("rapidRiseThreshold") double rapidRiseThreshold); /** 温度抖动异常 */ List selectJitterAnomalies(@Param("tableNames") List tableNames, @@ -214,13 +248,28 @@ public interface TempBoardMapper { /** 时间逆序可疑数据 */ List selectTimeReversal(@Param("tableNames") List tableNames, @Param("startTime") String startTime, - @Param("endTime") String endTime); + @Param("endTime") String endTime, + @Param("offset") int offset, + @Param("pageSize") int pageSize); - /** 采样间隔异常 */ + /** 时间逆序可疑数据总数 */ + long countTimeReversal(@Param("tableNames") List tableNames, + @Param("startTime") String startTime, + @Param("endTime") String endTime); + + /** 采样间隔异常(分页) */ List selectSamplingGapAnomalies(@Param("tableNames") List tableNames, @Param("startTime") String startTime, @Param("endTime") String endTime, - @Param("gapThreshold") int gapThresholdSeconds); + @Param("gapThreshold") int gapThresholdSeconds, + @Param("offset") int offset, + @Param("pageSize") int pageSize); + + /** 采样间隔异常总数 */ + long countSamplingGapAnomalies(@Param("tableNames") List tableNames, + @Param("startTime") String startTime, + @Param("endTime") String endTime, + @Param("gapThreshold") int gapThresholdSeconds); /** 数据完整率 */ List selectCompletenessRate(@Param("tableNames") List tableNames, diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/service/ITempBoardService.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/service/ITempBoardService.java index 13b84e8..622be5f 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/report/service/ITempBoardService.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/service/ITempBoardService.java @@ -1,5 +1,6 @@ package org.dromara.ems.report.service; +import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.ems.report.domain.vo.tempboard.*; import java.util.List; @@ -47,6 +48,9 @@ public interface ITempBoardService { /** 多测点对比趋势 */ List getMultiCompareTrend(TempBoardQueryBo query); + /** 温度测点下拉列表 */ + List listTempMonitors(); + /** 日均温趋势 */ List getDailyTrend(TempBoardQueryBo query); @@ -64,9 +68,6 @@ public interface ITempBoardService { /** 温度直方图 */ List getHistogram(TempBoardQueryBo query); - /** 温度箱线图原始数据 */ - List getBoxplotData(TempBoardQueryBo query); - /** 日历热力图 */ List getCalendarHeatmap(TempBoardQueryBo query); @@ -76,16 +77,16 @@ public interface ITempBoardService { // ==================== E. 异常预警 ==================== /** 高温事件 */ - List getHighTempEvents(TempBoardQueryBo query); + TableDataInfo getHighTempEvents(TempBoardQueryBo query); /** 低温事件 */ - List getLowTempEvents(TempBoardQueryBo query); + TableDataInfo getLowTempEvents(TempBoardQueryBo query); /** 连续高温时段 */ List getContinuousHighTemp(TempBoardQueryBo query); - /** 温升过快事件 */ - List getRapidRiseEvents(TempBoardQueryBo query); + /** 温升过快事件(分页) */ + TableDataInfo getRapidRiseEvents(TempBoardQueryBo query); /** 温度抖动异常 */ List getJitterAnomalies(TempBoardQueryBo query); @@ -113,10 +114,10 @@ public interface ITempBoardService { List getDelayDistribution(TempBoardQueryBo query); /** 时间逆序可疑数据 */ - List getTimeReversal(TempBoardQueryBo query); + TableDataInfo getTimeReversal(TempBoardQueryBo query); - /** 采样间隔异常 */ - List getSamplingGapAnomalies(TempBoardQueryBo query); + /** 采样间隔异常(分页) */ + TableDataInfo getSamplingGapAnomalies(TempBoardQueryBo query); /** 数据完整率 */ List getCompletenessRate(TempBoardQueryBo query); diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/report/service/impl/TempBoardServiceImpl.java b/ruoyi-ems/src/main/java/org/dromara/ems/report/service/impl/TempBoardServiceImpl.java index 52b5bb7..6f2cb6e 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/report/service/impl/TempBoardServiceImpl.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/report/service/impl/TempBoardServiceImpl.java @@ -3,15 +3,14 @@ package org.dromara.ems.report.service.impl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.exception.ServiceException; +import org.dromara.common.mybatis.core.page.TableDataInfo; import org.dromara.ems.record.service.RecordIotenvPartitionService; import org.dromara.ems.report.domain.vo.tempboard.*; import org.dromara.ems.report.mapper.TempBoardMapper; import org.dromara.ems.report.service.ITempBoardService; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import java.text.SimpleDateFormat; -import java.time.Duration; -import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Calendar; @@ -37,6 +36,14 @@ public class TempBoardServiceImpl implements ITempBoardService { //RecordIotenvPartitionService 按时间范围解析分表 private final RecordIotenvPartitionService partitionService; + /** + * 温度测点启用状态码,默认 0。可通过 application.yml 中 + * ems.temp-board.active-monitor-status 覆盖, + * 避免未来业务侧调整枚举后测点列表整体消失。 + */ + @Value("${ems.temp-board.active-monitor-status:0}") + private Integer activeMonitorStatus; + private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; /** 线程安全的静态格式化器,替代每次 new SimpleDateFormat */ @@ -45,6 +52,10 @@ public class TempBoardServiceImpl implements ITempBoardService { /** 最大查询天数(硬限制,防止灾难性查询) */ private static final int MAX_QUERY_DAYS = 90; + /** 多测点对比最少/最多测点数,与前端下拉上限保持一致,防止被直调接口绕过。 */ + private static final int MULTI_COMPARE_MIN = 2; + private static final int MULTI_COMPARE_MAX = 10; + // ==================== 工具方法 ==================== /** @@ -57,8 +68,9 @@ public class TempBoardServiceImpl implements ITempBoardService { } // 时间范围硬限制校验 long diffMs = query.getEndTime().getTime() - query.getStartTime().getTime(); - long diffDays = diffMs / (24 * 3600 * 1000); - if (diffDays > MAX_QUERY_DAYS) { + long maxRangeMs = (long) MAX_QUERY_DAYS * 24 * 3600 * 1000; + // 这里直接比较毫秒值,避免 90 天零 1 秒这类边界被整数除法误放行。 + if (diffMs > maxRangeMs) { throw new ServiceException("查询跨度不能超过" + MAX_QUERY_DAYS + "天,请缩小范围"); } return partitionService.resolveTables(query.getStartTime(), query.getEndTime()); @@ -69,6 +81,16 @@ public class TempBoardServiceImpl implements ITempBoardService { return FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()); } + /** + * 单测点趋势类接口必须显式选择测点, + * 否则前端会把“空结果”误解成系统无数据,排障成本很高。 + */ + private void validateMonitorIdRequired(String monitorId) { + if (monitorId == null || monitorId.trim().isEmpty()) { + throw new ServiceException("请选择测点"); + } + } + /** * 根据查询时间跨度自动决定高级分析的聚合粒度 * 跨度 <= 1 天:分钟级 @@ -87,6 +109,20 @@ public class TempBoardServiceImpl implements ITempBoardService { } } + /** + * 明细分页统一兜底正数,避免前端传 0 或负数时把 SQL Server OFFSET 算成非法值。 + */ + private int resolvePageNum(Integer pageNum) { + return pageNum == null || pageNum < 1 ? 1 : pageNum; + } + + /** + * 这里沿用页面默认 20 条;若调用方传非法值,则快速回退到安全页大小。 + */ + private int resolvePageSize(Integer pageSize) { + return pageSize == null || pageSize < 1 ? 20 : pageSize; + } + // ==================== A. 温度总览 ==================== /** 温度总览(测点数 + 平均温度 + 最高/最低 + TopN + 新鲜度) */ @@ -128,8 +164,10 @@ public class TempBoardServiceImpl implements ITempBoardService { if (minMaxVo != null) { result.setMaxLatestTemp(minMaxVo.getMaxLatestTemp()); result.setMaxTempMonitorId(minMaxVo.getMaxTempMonitorId()); + result.setMaxTempMonitorName(minMaxVo.getMaxTempMonitorName()); result.setMinLatestTemp(minMaxVo.getMinLatestTemp()); result.setMinTempMonitorId(minMaxVo.getMinTempMonitorId()); + result.setMinTempMonitorName(minMaxVo.getMinTempMonitorName()); } result.setHighTempTopN(highTopN); result.setLowTempTopN(lowTopN); @@ -201,6 +239,13 @@ public class TempBoardServiceImpl implements ITempBoardService { if (tables.isEmpty()) { return Collections.emptyList(); } + validateMonitorIdRequired(query.getMonitorId()); + // 跨天分钟点位过多时直接切小时聚合,优先保证大数据场景可用性。 + long diffMs = query.getEndTime().getTime() - query.getStartTime().getTime(); + if (diffMs > 24L * 3600 * 1000) { + return tempBoardMapper.selectHourlyTrend(tables, query.getMonitorId(), + fmt(query.getStartTime()), fmt(query.getEndTime())); + } return tempBoardMapper.selectMinuteTrend(tables, query.getMonitorId(), fmt(query.getStartTime()), fmt(query.getEndTime())); } @@ -212,6 +257,7 @@ public class TempBoardServiceImpl implements ITempBoardService { if (tables.isEmpty()) { return Collections.emptyList(); } + validateMonitorIdRequired(query.getMonitorId()); return tempBoardMapper.selectHourlyTrend(tables, query.getMonitorId(), fmt(query.getStartTime()), fmt(query.getEndTime())); } @@ -219,12 +265,32 @@ public class TempBoardServiceImpl implements ITempBoardService { /** 多测点对比趋势 */ @Override public List getMultiCompareTrend(TempBoardQueryBo query) { + // 先做入参强校验:不传 monitorIds 会让 SQL 退化为全测点 LAG,生产环境直接把 DB 打爆。 + int size = query.getMonitorIds() == null ? 0 : query.getMonitorIds().size(); + if (size < MULTI_COMPARE_MIN) { + throw new ServiceException("请选择至少 " + MULTI_COMPARE_MIN + " 个测点进行对比"); + } + if (size > MULTI_COMPARE_MAX) { + throw new ServiceException("多测点对比最多支持 " + MULTI_COMPARE_MAX + " 个测点"); + } List tables = resolveTables(query); if (tables.isEmpty()) { return Collections.emptyList(); } - return tempBoardMapper.selectMultiCompareTrend(tables, query.getMonitorIds(), - fmt(query.getStartTime()), fmt(query.getEndTime())); + // 跨度决定粒度:短窗用分钟保精度,大窗降到 15 分钟 / 小时级以保护 DB 和前端渲染。 + String granularity = resolveGranularity(query); + List list = tempBoardMapper.selectMultiCompareTrend( + tables, query.getMonitorIds(), + fmt(query.getStartTime()), fmt(query.getEndTime()), granularity); + // 把本次聚合粒度回填到每行 VO,前端无需再请求额外接口即可在标题上展示。 + list.forEach(item -> item.setGranularity(granularity)); + return list; + } + + /** 温度测点下拉列表 */ + @Override + public List listTempMonitors() { + return tempBoardMapper.selectTempMonitors(activeMonitorStatus); } /** 日均温趋势 */ @@ -244,6 +310,7 @@ public class TempBoardServiceImpl implements ITempBoardService { if (tables.isEmpty()) { return Collections.emptyList(); } + validateMonitorIdRequired(query.getMonitorId()); return tempBoardMapper.selectChangeRateTrend(tables, query.getMonitorId(), fmt(query.getStartTime()), fmt(query.getEndTime())); } @@ -280,16 +347,6 @@ public class TempBoardServiceImpl implements ITempBoardService { return tempBoardMapper.selectHistogram(tables, fmt(query.getStartTime()), fmt(query.getEndTime())); } - /** 温度箱线图原始数据 */ - @Override - public List getBoxplotData(TempBoardQueryBo query) { - List tables = resolveTables(query); - if (tables.isEmpty()) { - return Collections.emptyList(); - } - return tempBoardMapper.selectBoxplotData(tables, fmt(query.getStartTime()), fmt(query.getEndTime())); - } - /** 日历热力图 */ @Override public List getCalendarHeatmap(TempBoardQueryBo query) { @@ -314,24 +371,36 @@ public class TempBoardServiceImpl implements ITempBoardService { /** 高温事件 */ @Override - public List getHighTempEvents(TempBoardQueryBo query) { + public TableDataInfo getHighTempEvents(TempBoardQueryBo query) { List tables = resolveTables(query); if (tables.isEmpty()) { - return Collections.emptyList(); + return TableDataInfo.build(); } double threshold = query.getHighTempThreshold() != null ? query.getHighTempThreshold() : 35.0; - return tempBoardMapper.selectHighTempEvents(tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold); + int pageNum = resolvePageNum(query.getPageNum()); + int pageSize = resolvePageSize(query.getPageSize()); + int offset = (pageNum - 1) * pageSize; + long total = tempBoardMapper.countHighTempEvents(tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold); + List rows = tempBoardMapper.selectHighTempEvents( + tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold, offset, pageSize); + return new TableDataInfo<>(rows, total); } /** 低温事件 */ @Override - public List getLowTempEvents(TempBoardQueryBo query) { + public TableDataInfo getLowTempEvents(TempBoardQueryBo query) { List tables = resolveTables(query); if (tables.isEmpty()) { - return Collections.emptyList(); + return TableDataInfo.build(); } double threshold = query.getLowTempThreshold() != null ? query.getLowTempThreshold() : 10.0; - return tempBoardMapper.selectLowTempEvents(tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold); + int pageNum = resolvePageNum(query.getPageNum()); + int pageSize = resolvePageSize(query.getPageSize()); + int offset = (pageNum - 1) * pageSize; + long total = tempBoardMapper.countLowTempEvents(tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold); + List rows = tempBoardMapper.selectLowTempEvents( + tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold, offset, pageSize); + return new TableDataInfo<>(rows, total); } /** 连续高温时段 */ @@ -342,18 +411,38 @@ public class TempBoardServiceImpl implements ITempBoardService { return Collections.emptyList(); } double threshold = query.getHighTempThreshold() != null ? query.getHighTempThreshold() : 35.0; - return tempBoardMapper.selectContinuousHighTemp(tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold); + int continuousMinSamples = query.getContinuousMinSamples() != null ? query.getContinuousMinSamples() : 2; + return tempBoardMapper.selectContinuousHighTemp( + tables, + fmt(query.getStartTime()), + fmt(query.getEndTime()), + threshold, + continuousMinSamples + ); } - /** 温升过快事件 */ + /** + * 温升过快事件(分页) + *

+ * 采样噪声、工况切换、阈值偏低时这张表会快速碎片化,原先全量 List 会 + * 把几千条 DOM 直接塞给浏览器。这里改为 TableDataInfo,配合前端 + * el-table-v2 + 分页以保证大数据场景下的渲染压力收敛。 + */ @Override - public List getRapidRiseEvents(TempBoardQueryBo query) { + public TableDataInfo getRapidRiseEvents(TempBoardQueryBo query) { List tables = resolveTables(query); if (tables.isEmpty()) { - return Collections.emptyList(); + return TableDataInfo.build(); } double threshold = query.getRapidRiseThreshold() != null ? query.getRapidRiseThreshold() : 1.0; - return tempBoardMapper.selectRapidRiseEvents(tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold); + int pageNum = resolvePageNum(query.getPageNum()); + int pageSize = resolvePageSize(query.getPageSize()); + int offset = (pageNum - 1) * pageSize; + long total = tempBoardMapper.countRapidRiseEvents( + tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold); + List rows = tempBoardMapper.selectRapidRiseEvents( + tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold, offset, pageSize); + return new TableDataInfo<>(rows, total); } /** 温度抖动异常 */ @@ -392,6 +481,9 @@ public class TempBoardServiceImpl implements ITempBoardService { /** 今日vs昨日对比 */ @Override public List getDailyDiff(TempBoardQueryBo query) { + if (query.getEndTime() == null) { + throw new ServiceException("请选择时间范围"); + } // 今日和昨日的时间范围由 endTime 推算 Date endTime = query.getEndTime(); Calendar cal = Calendar.getInstance(); @@ -462,23 +554,41 @@ public class TempBoardServiceImpl implements ITempBoardService { /** 时间逆序可疑数据 */ @Override - public List getTimeReversal(TempBoardQueryBo query) { + public TableDataInfo getTimeReversal(TempBoardQueryBo query) { List tables = resolveTables(query); if (tables.isEmpty()) { - return Collections.emptyList(); + return TableDataInfo.build(); } - return tempBoardMapper.selectTimeReversal(tables, fmt(query.getStartTime()), fmt(query.getEndTime())); + int pageNum = resolvePageNum(query.getPageNum()); + int pageSize = resolvePageSize(query.getPageSize()); + int offset = (pageNum - 1) * pageSize; + long total = tempBoardMapper.countTimeReversal(tables, fmt(query.getStartTime()), fmt(query.getEndTime())); + List rows = tempBoardMapper.selectTimeReversal( + tables, fmt(query.getStartTime()), fmt(query.getEndTime()), offset, pageSize); + return new TableDataInfo<>(rows, total); } - /** 采样间隔异常 */ + /** + * 采样间隔异常(分页) + *

+ * 通讯抖动 / 断点重传 / 时间同步异常时同样会爆量,改为分页返回, + * 与时间逆序 / 温升过快 保持一致的 TableDataInfo 模型。 + */ @Override - public List getSamplingGapAnomalies(TempBoardQueryBo query) { + public TableDataInfo getSamplingGapAnomalies(TempBoardQueryBo query) { List tables = resolveTables(query); if (tables.isEmpty()) { - return Collections.emptyList(); + return TableDataInfo.build(); } int threshold = query.getGapThresholdSeconds() != null ? query.getGapThresholdSeconds() : 300; - return tempBoardMapper.selectSamplingGapAnomalies(tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold); + int pageNum = resolvePageNum(query.getPageNum()); + int pageSize = resolvePageSize(query.getPageSize()); + int offset = (pageNum - 1) * pageSize; + long total = tempBoardMapper.countSamplingGapAnomalies( + tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold); + List rows = tempBoardMapper.selectSamplingGapAnomalies( + tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold, offset, pageSize); + return new TableDataInfo<>(rows, total); } /** 数据完整率 */ diff --git a/ruoyi-ems/src/main/resources/mapper/ems/report/TempBoardMapper.xml b/ruoyi-ems/src/main/resources/mapper/ems/report/TempBoardMapper.xml index cb178d9..b9c0e94 100644 --- a/ruoyi-ems/src/main/resources/mapper/ems/report/TempBoardMapper.xml +++ b/ruoyi-ems/src/main/resources/mapper/ems/report/TempBoardMapper.xml @@ -11,20 +11,16 @@ COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName - + - LEFT JOIN ems_base_monitor_info ebmi ON t.monitorId = ebmi.monitor_code + INNER JOIN ems_base_monitor_info ebmi + ON t.monitorId = ebmi.monitor_code + AND ebmi.monitor_type IN (5, 6) - + - AND (ebmi.monitor_type IN (5, 6) OR ebmi.monitor_type IS NULL) - AND (t.temperature IS NULL OR (t.temperature BETWEEN 0 AND 79)) - AND ( - (ebmi.monitor_type = 5 AND t.temperature > 0) OR - (ebmi.monitor_type = 6 AND t.temperature > 0) OR - (ebmi.monitor_type NOT IN (5, 6) OR ebmi.monitor_type IS NULL) - ) + AND t.temperature IS NOT NULL @@ -98,15 +94,21 @@ FROM latest WHERE rn = 1 ), max_val AS ( - SELECT TOP 1 monitorId AS maxTempMonitorId, temperature AS maxLatestTemp - FROM latest_only ORDER BY temperature DESC + SELECT TOP 1 monitorId AS maxTempMonitorId, + monitorName AS maxTempMonitorName, + temperature AS maxLatestTemp + FROM latest_only + ORDER BY temperature DESC, monitorId ), min_val AS ( - SELECT TOP 1 monitorId AS minTempMonitorId, temperature AS minLatestTemp - FROM latest_only ORDER BY temperature ASC + SELECT TOP 1 monitorId AS minTempMonitorId, + monitorName AS minTempMonitorName, + temperature AS minLatestTemp + FROM latest_only + ORDER BY temperature ASC, monitorId ) - SELECT m.maxTempMonitorId, m.maxLatestTemp, - n.minTempMonitorId, n.minLatestTemp + SELECT m.maxTempMonitorId, m.maxTempMonitorName, m.maxLatestTemp, + n.minTempMonitorId, n.minTempMonitorName, n.minLatestTemp FROM max_val m CROSS JOIN min_val n @@ -131,7 +133,7 @@ ) SELECT TOP (#{topN}) monitorId, monitorName, temperature, collectTime FROM latest WHERE rn = 1 - ORDER BY temperature DESC + ORDER BY temperature DESC, monitorId @@ -155,7 +157,7 @@ ) SELECT TOP (#{topN}) monitorId, monitorName, temperature, collectTime FROM latest WHERE rn = 1 - ORDER BY temperature ASC + ORDER BY temperature ASC, monitorId @@ -178,7 +180,7 @@ FROM all_data ) SELECT monitorId, monitorName, temperature, collectTime, - DATEDIFF(SECOND, collectTime, GETUTCDATE()) AS ageSeconds + DATEDIFF(SECOND, collectTime, GETDATE()) AS ageSeconds FROM latest WHERE rn = 1 ORDER BY ageSeconds DESC @@ -206,7 +208,7 @@ ) SELECT monitorId, monitorName, temperature, collectTime, recodeTime, DATEDIFF(SECOND, collectTime, recodeTime) AS delaySeconds, - DATEDIFF(SECOND, collectTime, GETUTCDATE()) AS staleSeconds + DATEDIFF(SECOND, collectTime, GETDATE()) AS staleSeconds FROM latest WHERE rn = 1 ORDER BY collectTime DESC, monitorId @@ -281,9 +283,9 @@ FROM all_data ) SELECT monitorId, monitorName, temperature, collectTime, - DATEDIFF(SECOND, collectTime, GETUTCDATE()) AS staleSeconds + DATEDIFF(SECOND, collectTime, GETDATE()) AS staleSeconds FROM latest WHERE rn = 1 - AND DATEDIFF(SECOND, collectTime, GETUTCDATE()) >= #{staleThreshold} + AND DATEDIFF(SECOND, collectTime, GETDATE()) >= #{staleThreshold} ORDER BY staleSeconds DESC @@ -360,11 +362,32 @@ ORDER BY statTime - + + + + - - - + + @@ -604,6 +650,23 @@ ORDER BY collectTime DESC + OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY + + + @@ -638,11 +701,19 @@ COUNT(*) AS sampleCount FROM grouped WHERE isHigh = 1 GROUP BY monitorId, monitorName, grp - HAVING COUNT(*) >= 2 + HAVING COUNT(*) >= #{continuousMinSamples} ORDER BY monitorId, startTime - + + + + @@ -682,6 +780,7 @@ SELECT t.monitorId, COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName, FORMAT(t.collectTime, 'yyyy-MM-dd HH:00:00') AS statTime, + ROUND(AVG(t.temperature), 2) AS avgTemp, ROUND(STDEVP(t.temperature), 4) AS tempStddev, 'JITTER' AS anomalyType FROM ${tableName} t @@ -772,20 +871,40 @@ ORDER BY diffValue DESC - + @@ -850,9 +969,31 @@ ORDER BY collectTime DESC + OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY - + + + + + + @@ -935,7 +1101,8 @@ WHEN t.temperature < 15 THEN '<15' WHEN t.temperature < 20 THEN '15-20' WHEN t.temperature < 25 THEN '20-25' - ELSE '>=25' + WHEN t.temperature < 30 THEN '25-30' + ELSE '>=30' END AS tempBucket FROM ${tableName} t