feat(温度看板): 新增测点下拉列表并优化查询功能

新增温度测点下拉列表接口,支持前端快速选择测点
为异常事件列表添加分页支持,防止大数据量导致性能问题
优化多测点对比趋势的聚合粒度,根据时间跨度自动调整
移除箱线图原始数据接口,简化数据分布分析
补充测点名称字段,提升最高/最低温度展示信息量
增加连续高温最少样本数配置,提高异常检测灵活性
完善测试用例,覆盖空测点和时间范围校验场景
main
zch 3 months ago
parent 4d8177d07e
commit 99ffe8c8c8

@ -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
*
* <p>退
* </p>
*/
@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<String> 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<String> tables;
private StubPartitionService(List<String> tables) {
super((RecordIotenvInstantMapper) Proxy.newProxyInstance(
RecordIotenvInstantMapper.class.getClassLoader(),
new Class<?>[]{RecordIotenvInstantMapper.class},
(proxy, method, args) -> 0
));
this.tables = tables;
}
@Override
public List<String> resolveTables(Date beginTime, Date endTime) {
return tables;
}
}
}

@ -3,6 +3,7 @@ package org.dromara.ems.report.controller;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R; 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.domain.vo.tempboard.*;
import org.dromara.ems.report.service.ITempBoardService; import org.dromara.ems.report.service.ITempBoardService;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -97,6 +98,13 @@ public class TempBoardController {
return R.ok(tempBoardService.getMultiCompareTrend(query)); return R.ok(tempBoardService.getMultiCompareTrend(query));
} }
/** 温度测点下拉列表 */
@SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/monitor/list")
public R<List<TempBoardMonitorOptionVo>> listTempMonitors() {
return R.ok(tempBoardService.listTempMonitors());
}
/** 日均温趋势 */ /** 日均温趋势 */
@SaCheckPermission("ems/report:tempBoard:query") @SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/trend/daily") @GetMapping("/trend/daily")
@ -134,13 +142,6 @@ public class TempBoardController {
return R.ok(tempBoardService.getHistogram(query)); return R.ok(tempBoardService.getHistogram(query));
} }
/** 温度箱线图数据 */
@SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/distribution/boxplot")
public R<List<TempBoardDistributionVo>> getBoxplotData(TempBoardQueryBo query) {
return R.ok(tempBoardService.getBoxplotData(query));
}
/** 日历热力图 */ /** 日历热力图 */
@SaCheckPermission("ems/report:tempBoard:query") @SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/distribution/calendarHeatmap") @GetMapping("/distribution/calendarHeatmap")
@ -160,14 +161,14 @@ public class TempBoardController {
/** 高温事件 */ /** 高温事件 */
@SaCheckPermission("ems/report:tempBoard:query") @SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/anomaly/highTemp") @GetMapping("/anomaly/highTemp")
public R<List<TempBoardAnomalyVo>> getHighTempEvents(TempBoardQueryBo query) { public R<TableDataInfo<TempBoardAnomalyVo>> getHighTempEvents(TempBoardQueryBo query) {
return R.ok(tempBoardService.getHighTempEvents(query)); return R.ok(tempBoardService.getHighTempEvents(query));
} }
/** 低温事件 */ /** 低温事件 */
@SaCheckPermission("ems/report:tempBoard:query") @SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/anomaly/lowTemp") @GetMapping("/anomaly/lowTemp")
public R<List<TempBoardAnomalyVo>> getLowTempEvents(TempBoardQueryBo query) { public R<TableDataInfo<TempBoardAnomalyVo>> getLowTempEvents(TempBoardQueryBo query) {
return R.ok(tempBoardService.getLowTempEvents(query)); return R.ok(tempBoardService.getLowTempEvents(query));
} }
@ -178,10 +179,10 @@ public class TempBoardController {
return R.ok(tempBoardService.getContinuousHighTemp(query)); return R.ok(tempBoardService.getContinuousHighTemp(query));
} }
/** 温升过快事件 */ /** 温升过快事件(分页) */
@SaCheckPermission("ems/report:tempBoard:query") @SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/anomaly/rapidRise") @GetMapping("/anomaly/rapidRise")
public R<List<TempBoardAnomalyVo>> getRapidRiseEvents(TempBoardQueryBo query) { public R<TableDataInfo<TempBoardAnomalyVo>> getRapidRiseEvents(TempBoardQueryBo query) {
return R.ok(tempBoardService.getRapidRiseEvents(query)); return R.ok(tempBoardService.getRapidRiseEvents(query));
} }
@ -241,14 +242,14 @@ public class TempBoardController {
/** 时间逆序可疑数据 */ /** 时间逆序可疑数据 */
@SaCheckPermission("ems/report:tempBoard:query") @SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/quality/timeReversal") @GetMapping("/quality/timeReversal")
public R<List<TempBoardQualityVo>> getTimeReversal(TempBoardQueryBo query) { public R<TableDataInfo<TempBoardQualityVo>> getTimeReversal(TempBoardQueryBo query) {
return R.ok(tempBoardService.getTimeReversal(query)); return R.ok(tempBoardService.getTimeReversal(query));
} }
/** 采样间隔异常 */ /** 采样间隔异常(分页) */
@SaCheckPermission("ems/report:tempBoard:query") @SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/quality/samplingGap") @GetMapping("/quality/samplingGap")
public R<List<TempBoardQualityVo>> getSamplingGapAnomalies(TempBoardQueryBo query) { public R<TableDataInfo<TempBoardQualityVo>> getSamplingGapAnomalies(TempBoardQueryBo query) {
return R.ok(tempBoardService.getSamplingGapAnomalies(query)); return R.ok(tempBoardService.getSamplingGapAnomalies(query));
} }

@ -35,6 +35,12 @@ public class TempBoardAnomalyVo {
/** 标准差(用于抖动检测) */ /** 标准差(用于抖动检测) */
private BigDecimal tempStddev; private BigDecimal tempStddev;
/** 小时均温(用于抖动检测时补充排障上下文) */
private BigDecimal avgTemp;
/** 统计时段(用于抖动检测) */
private String statTime;
// ========== 连续高温 ========== // ========== 连续高温 ==========
/** 连续开始时间 */ /** 连续开始时间 */

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

@ -27,12 +27,18 @@ public class TempBoardOverviewVo {
/** 最高温度测点ID */ /** 最高温度测点ID */
private String maxTempMonitorId; private String maxTempMonitorId;
/** 最高温度测点名称 */
private String maxTempMonitorName;
/** 当前最低温度 */ /** 当前最低温度 */
private BigDecimal minLatestTemp; private BigDecimal minLatestTemp;
/** 最低温度测点ID */ /** 最低温度测点ID */
private String minTempMonitorId; private String minTempMonitorId;
/** 最低温度测点名称 */
private String minTempMonitorName;
/** 高温 TopN */ /** 高温 TopN */
private List<MonitorTempRank> highTempTopN; private List<MonitorTempRank> highTempTopN;

@ -47,6 +47,9 @@ public class TempBoardQueryBo {
/** 单测点趋势查询时的测点ID */ /** 单测点趋势查询时的测点ID */
private String monitorId; private String monitorId;
/** 连续高温最少样本数默认2 */
private Integer continuousMinSamples = 2;
/** 预期采样数(用于完整率计算) */ /** 预期采样数(用于完整率计算) */
private Integer expectedSampleCount; private Integer expectedSampleCount;

@ -60,4 +60,18 @@ public class TempBoardTrendVo {
/** 前一时刻时间 */ /** 前一时刻时间 */
private Date prevTime; private Date prevTime;
/**
* MINUTE / FIFTEEN_MINUTE / HOUR
* <p>
*
* <ul>
* <li>&le; 1 </li>
* <li>&gt; 1 &le; 7 15 </li>
* <li>&gt; 7 </li>
* </ul>
* Service "当前聚合粒度"
*
*/
private String granularity;
} }

@ -94,11 +94,25 @@ public interface TempBoardMapper {
@Param("startTime") String startTime, @Param("startTime") String startTime,
@Param("endTime") String endTime); @Param("endTime") String endTime);
/** 多测点对比趋势(分钟粒度) */ /**
*
* <p>
* granularity
* <ul>
* <li>MINUTE 1 </li>
* <li>FIFTEEN_MINUTE15 1~7 </li>
* <li>HOUR&gt; 7 </li>
* </ul>
* Service //
*/
List<TempBoardTrendVo> selectMultiCompareTrend(@Param("tableNames") List<String> tableNames, List<TempBoardTrendVo> selectMultiCompareTrend(@Param("tableNames") List<String> tableNames,
@Param("monitorIds") List<String> monitorIds, @Param("monitorIds") List<String> monitorIds,
@Param("startTime") String startTime, @Param("startTime") String startTime,
@Param("endTime") String endTime); @Param("endTime") String endTime,
@Param("granularity") String granularity);
/** 温度测点下拉列表 */
List<TempBoardMonitorOptionVo> selectTempMonitors(@Param("activeStatus") Integer activeStatus);
/** 日均温趋势 */ /** 日均温趋势 */
List<TempBoardTrendVo> selectDailyTrend(@Param("tableNames") List<String> tableNames, List<TempBoardTrendVo> selectDailyTrend(@Param("tableNames") List<String> tableNames,
@ -128,11 +142,6 @@ public interface TempBoardMapper {
@Param("startTime") String startTime, @Param("startTime") String startTime,
@Param("endTime") String endTime); @Param("endTime") String endTime);
/** 温度箱线图原始数据(按测点) */
List<TempBoardDistributionVo> selectBoxplotData(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/** 日历热力图 */ /** 日历热力图 */
List<TempBoardDistributionVo> selectCalendarHeatmap(@Param("tableNames") List<String> tableNames, List<TempBoardDistributionVo> selectCalendarHeatmap(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime, @Param("startTime") String startTime,
@ -147,12 +156,28 @@ public interface TempBoardMapper {
/** 高温事件 */ /** 高温事件 */
List<TempBoardAnomalyVo> selectHighTempEvents(@Param("tableNames") List<String> tableNames, List<TempBoardAnomalyVo> selectHighTempEvents(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("highTempThreshold") double highTempThreshold,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
/** 高温事件总数 */
long countHighTempEvents(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime, @Param("startTime") String startTime,
@Param("endTime") String endTime, @Param("endTime") String endTime,
@Param("highTempThreshold") double highTempThreshold); @Param("highTempThreshold") double highTempThreshold);
/** 低温事件 */ /** 低温事件 */
List<TempBoardAnomalyVo> selectLowTempEvents(@Param("tableNames") List<String> tableNames, List<TempBoardAnomalyVo> selectLowTempEvents(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("lowTempThreshold") double lowTempThreshold,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
/** 低温事件总数 */
long countLowTempEvents(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime, @Param("startTime") String startTime,
@Param("endTime") String endTime, @Param("endTime") String endTime,
@Param("lowTempThreshold") double lowTempThreshold); @Param("lowTempThreshold") double lowTempThreshold);
@ -161,10 +186,19 @@ public interface TempBoardMapper {
List<TempBoardAnomalyVo> selectContinuousHighTemp(@Param("tableNames") List<String> tableNames, List<TempBoardAnomalyVo> selectContinuousHighTemp(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime, @Param("startTime") String startTime,
@Param("endTime") String endTime, @Param("endTime") String endTime,
@Param("highTempThreshold") double highTempThreshold); @Param("highTempThreshold") double highTempThreshold,
@Param("continuousMinSamples") int continuousMinSamples);
/** 温升过快事件 */ /** 温升过快事件(分页) */
List<TempBoardAnomalyVo> selectRapidRiseEvents(@Param("tableNames") List<String> tableNames, List<TempBoardAnomalyVo> selectRapidRiseEvents(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("rapidRiseThreshold") double rapidRiseThreshold,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
/** 温升过快事件总数 */
long countRapidRiseEvents(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime, @Param("startTime") String startTime,
@Param("endTime") String endTime, @Param("endTime") String endTime,
@Param("rapidRiseThreshold") double rapidRiseThreshold); @Param("rapidRiseThreshold") double rapidRiseThreshold);
@ -213,11 +247,26 @@ public interface TempBoardMapper {
/** 时间逆序可疑数据 */ /** 时间逆序可疑数据 */
List<TempBoardQualityVo> selectTimeReversal(@Param("tableNames") List<String> tableNames, List<TempBoardQualityVo> selectTimeReversal(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
/** 时间逆序可疑数据总数 */
long countTimeReversal(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime, @Param("startTime") String startTime,
@Param("endTime") String endTime); @Param("endTime") String endTime);
/** 采样间隔异常 */ /** 采样间隔异常(分页) */
List<TempBoardQualityVo> selectSamplingGapAnomalies(@Param("tableNames") List<String> tableNames, List<TempBoardQualityVo> selectSamplingGapAnomalies(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("gapThreshold") int gapThresholdSeconds,
@Param("offset") int offset,
@Param("pageSize") int pageSize);
/** 采样间隔异常总数 */
long countSamplingGapAnomalies(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime, @Param("startTime") String startTime,
@Param("endTime") String endTime, @Param("endTime") String endTime,
@Param("gapThreshold") int gapThresholdSeconds); @Param("gapThreshold") int gapThresholdSeconds);

@ -1,5 +1,6 @@
package org.dromara.ems.report.service; package org.dromara.ems.report.service;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.ems.report.domain.vo.tempboard.*; import org.dromara.ems.report.domain.vo.tempboard.*;
import java.util.List; import java.util.List;
@ -47,6 +48,9 @@ public interface ITempBoardService {
/** 多测点对比趋势 */ /** 多测点对比趋势 */
List<TempBoardTrendVo> getMultiCompareTrend(TempBoardQueryBo query); List<TempBoardTrendVo> getMultiCompareTrend(TempBoardQueryBo query);
/** 温度测点下拉列表 */
List<TempBoardMonitorOptionVo> listTempMonitors();
/** 日均温趋势 */ /** 日均温趋势 */
List<TempBoardTrendVo> getDailyTrend(TempBoardQueryBo query); List<TempBoardTrendVo> getDailyTrend(TempBoardQueryBo query);
@ -64,9 +68,6 @@ public interface ITempBoardService {
/** 温度直方图 */ /** 温度直方图 */
List<TempBoardDistributionVo> getHistogram(TempBoardQueryBo query); List<TempBoardDistributionVo> getHistogram(TempBoardQueryBo query);
/** 温度箱线图原始数据 */
List<TempBoardDistributionVo> getBoxplotData(TempBoardQueryBo query);
/** 日历热力图 */ /** 日历热力图 */
List<TempBoardDistributionVo> getCalendarHeatmap(TempBoardQueryBo query); List<TempBoardDistributionVo> getCalendarHeatmap(TempBoardQueryBo query);
@ -76,16 +77,16 @@ public interface ITempBoardService {
// ==================== E. 异常预警 ==================== // ==================== E. 异常预警 ====================
/** 高温事件 */ /** 高温事件 */
List<TempBoardAnomalyVo> getHighTempEvents(TempBoardQueryBo query); TableDataInfo<TempBoardAnomalyVo> getHighTempEvents(TempBoardQueryBo query);
/** 低温事件 */ /** 低温事件 */
List<TempBoardAnomalyVo> getLowTempEvents(TempBoardQueryBo query); TableDataInfo<TempBoardAnomalyVo> getLowTempEvents(TempBoardQueryBo query);
/** 连续高温时段 */ /** 连续高温时段 */
List<TempBoardAnomalyVo> getContinuousHighTemp(TempBoardQueryBo query); List<TempBoardAnomalyVo> getContinuousHighTemp(TempBoardQueryBo query);
/** 温升过快事件 */ /** 温升过快事件(分页) */
List<TempBoardAnomalyVo> getRapidRiseEvents(TempBoardQueryBo query); TableDataInfo<TempBoardAnomalyVo> getRapidRiseEvents(TempBoardQueryBo query);
/** 温度抖动异常 */ /** 温度抖动异常 */
List<TempBoardAnomalyVo> getJitterAnomalies(TempBoardQueryBo query); List<TempBoardAnomalyVo> getJitterAnomalies(TempBoardQueryBo query);
@ -113,10 +114,10 @@ public interface ITempBoardService {
List<TempBoardQualityVo> getDelayDistribution(TempBoardQueryBo query); List<TempBoardQualityVo> getDelayDistribution(TempBoardQueryBo query);
/** 时间逆序可疑数据 */ /** 时间逆序可疑数据 */
List<TempBoardQualityVo> getTimeReversal(TempBoardQueryBo query); TableDataInfo<TempBoardQualityVo> getTimeReversal(TempBoardQueryBo query);
/** 采样间隔异常 */ /** 采样间隔异常(分页) */
List<TempBoardQualityVo> getSamplingGapAnomalies(TempBoardQueryBo query); TableDataInfo<TempBoardQualityVo> getSamplingGapAnomalies(TempBoardQueryBo query);
/** 数据完整率 */ /** 数据完整率 */
List<TempBoardQualityVo> getCompletenessRate(TempBoardQueryBo query); List<TempBoardQualityVo> getCompletenessRate(TempBoardQueryBo query);

@ -3,15 +3,14 @@ package org.dromara.ems.report.service.impl;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException; 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.record.service.RecordIotenvPartitionService;
import org.dromara.ems.report.domain.vo.tempboard.*; import org.dromara.ems.report.domain.vo.tempboard.*;
import org.dromara.ems.report.mapper.TempBoardMapper; import org.dromara.ems.report.mapper.TempBoardMapper;
import org.dromara.ems.report.service.ITempBoardService; import org.dromara.ems.report.service.ITempBoardService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.Calendar; import java.util.Calendar;
@ -37,6 +36,14 @@ public class TempBoardServiceImpl implements ITempBoardService {
//RecordIotenvPartitionService 按时间范围解析分表 //RecordIotenvPartitionService 按时间范围解析分表
private final RecordIotenvPartitionService partitionService; private final RecordIotenvPartitionService partitionService;
/**
* 0 application.yml
* <code>ems.temp-board.active-monitor-status</code>
*
*/
@Value("${ems.temp-board.active-monitor-status:0}")
private Integer activeMonitorStatus;
private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
/** 线程安全的静态格式化器,替代每次 new SimpleDateFormat */ /** 线程安全的静态格式化器,替代每次 new SimpleDateFormat */
@ -45,6 +52,10 @@ public class TempBoardServiceImpl implements ITempBoardService {
/** 最大查询天数(硬限制,防止灾难性查询) */ /** 最大查询天数(硬限制,防止灾难性查询) */
private static final int MAX_QUERY_DAYS = 90; 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 diffMs = query.getEndTime().getTime() - query.getStartTime().getTime();
long diffDays = diffMs / (24 * 3600 * 1000); long maxRangeMs = (long) MAX_QUERY_DAYS * 24 * 3600 * 1000;
if (diffDays > MAX_QUERY_DAYS) { // 这里直接比较毫秒值,避免 90 天零 1 秒这类边界被整数除法误放行。
if (diffMs > maxRangeMs) {
throw new ServiceException("查询跨度不能超过" + MAX_QUERY_DAYS + "天,请缩小范围"); throw new ServiceException("查询跨度不能超过" + MAX_QUERY_DAYS + "天,请缩小范围");
} }
return partitionService.resolveTables(query.getStartTime(), query.getEndTime()); 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()); return FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
} }
/**
*
*
*/
private void validateMonitorIdRequired(String monitorId) {
if (monitorId == null || monitorId.trim().isEmpty()) {
throw new ServiceException("请选择测点");
}
}
/** /**
* *
* <= 1 * <= 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. 温度总览 ==================== // ==================== A. 温度总览 ====================
/** 温度总览(测点数 + 平均温度 + 最高/最低 + TopN + 新鲜度) */ /** 温度总览(测点数 + 平均温度 + 最高/最低 + TopN + 新鲜度) */
@ -128,8 +164,10 @@ public class TempBoardServiceImpl implements ITempBoardService {
if (minMaxVo != null) { if (minMaxVo != null) {
result.setMaxLatestTemp(minMaxVo.getMaxLatestTemp()); result.setMaxLatestTemp(minMaxVo.getMaxLatestTemp());
result.setMaxTempMonitorId(minMaxVo.getMaxTempMonitorId()); result.setMaxTempMonitorId(minMaxVo.getMaxTempMonitorId());
result.setMaxTempMonitorName(minMaxVo.getMaxTempMonitorName());
result.setMinLatestTemp(minMaxVo.getMinLatestTemp()); result.setMinLatestTemp(minMaxVo.getMinLatestTemp());
result.setMinTempMonitorId(minMaxVo.getMinTempMonitorId()); result.setMinTempMonitorId(minMaxVo.getMinTempMonitorId());
result.setMinTempMonitorName(minMaxVo.getMinTempMonitorName());
} }
result.setHighTempTopN(highTopN); result.setHighTempTopN(highTopN);
result.setLowTempTopN(lowTopN); result.setLowTempTopN(lowTopN);
@ -201,6 +239,13 @@ public class TempBoardServiceImpl implements ITempBoardService {
if (tables.isEmpty()) { if (tables.isEmpty()) {
return Collections.emptyList(); 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(), return tempBoardMapper.selectMinuteTrend(tables, query.getMonitorId(),
fmt(query.getStartTime()), fmt(query.getEndTime())); fmt(query.getStartTime()), fmt(query.getEndTime()));
} }
@ -212,6 +257,7 @@ public class TempBoardServiceImpl implements ITempBoardService {
if (tables.isEmpty()) { if (tables.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
validateMonitorIdRequired(query.getMonitorId());
return tempBoardMapper.selectHourlyTrend(tables, query.getMonitorId(), return tempBoardMapper.selectHourlyTrend(tables, query.getMonitorId(),
fmt(query.getStartTime()), fmt(query.getEndTime())); fmt(query.getStartTime()), fmt(query.getEndTime()));
} }
@ -219,12 +265,32 @@ public class TempBoardServiceImpl implements ITempBoardService {
/** 多测点对比趋势 */ /** 多测点对比趋势 */
@Override @Override
public List<TempBoardTrendVo> getMultiCompareTrend(TempBoardQueryBo query) { public List<TempBoardTrendVo> 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<String> tables = resolveTables(query); List<String> tables = resolveTables(query);
if (tables.isEmpty()) { if (tables.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
return tempBoardMapper.selectMultiCompareTrend(tables, query.getMonitorIds(), // 跨度决定粒度:短窗用分钟保精度,大窗降到 15 分钟 / 小时级以保护 DB 和前端渲染。
fmt(query.getStartTime()), fmt(query.getEndTime())); String granularity = resolveGranularity(query);
List<TempBoardTrendVo> 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<TempBoardMonitorOptionVo> listTempMonitors() {
return tempBoardMapper.selectTempMonitors(activeMonitorStatus);
} }
/** 日均温趋势 */ /** 日均温趋势 */
@ -244,6 +310,7 @@ public class TempBoardServiceImpl implements ITempBoardService {
if (tables.isEmpty()) { if (tables.isEmpty()) {
return Collections.emptyList(); return Collections.emptyList();
} }
validateMonitorIdRequired(query.getMonitorId());
return tempBoardMapper.selectChangeRateTrend(tables, query.getMonitorId(), return tempBoardMapper.selectChangeRateTrend(tables, query.getMonitorId(),
fmt(query.getStartTime()), fmt(query.getEndTime())); 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())); return tempBoardMapper.selectHistogram(tables, fmt(query.getStartTime()), fmt(query.getEndTime()));
} }
/** 温度箱线图原始数据 */
@Override
public List<TempBoardDistributionVo> getBoxplotData(TempBoardQueryBo query) {
List<String> tables = resolveTables(query);
if (tables.isEmpty()) {
return Collections.emptyList();
}
return tempBoardMapper.selectBoxplotData(tables, fmt(query.getStartTime()), fmt(query.getEndTime()));
}
/** 日历热力图 */ /** 日历热力图 */
@Override @Override
public List<TempBoardDistributionVo> getCalendarHeatmap(TempBoardQueryBo query) { public List<TempBoardDistributionVo> getCalendarHeatmap(TempBoardQueryBo query) {
@ -314,24 +371,36 @@ public class TempBoardServiceImpl implements ITempBoardService {
/** 高温事件 */ /** 高温事件 */
@Override @Override
public List<TempBoardAnomalyVo> getHighTempEvents(TempBoardQueryBo query) { public TableDataInfo<TempBoardAnomalyVo> getHighTempEvents(TempBoardQueryBo query) {
List<String> tables = resolveTables(query); List<String> tables = resolveTables(query);
if (tables.isEmpty()) { if (tables.isEmpty()) {
return Collections.emptyList(); return TableDataInfo.build();
} }
double threshold = query.getHighTempThreshold() != null ? query.getHighTempThreshold() : 35.0; 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<TempBoardAnomalyVo> rows = tempBoardMapper.selectHighTempEvents(
tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold, offset, pageSize);
return new TableDataInfo<>(rows, total);
} }
/** 低温事件 */ /** 低温事件 */
@Override @Override
public List<TempBoardAnomalyVo> getLowTempEvents(TempBoardQueryBo query) { public TableDataInfo<TempBoardAnomalyVo> getLowTempEvents(TempBoardQueryBo query) {
List<String> tables = resolveTables(query); List<String> tables = resolveTables(query);
if (tables.isEmpty()) { if (tables.isEmpty()) {
return Collections.emptyList(); return TableDataInfo.build();
} }
double threshold = query.getLowTempThreshold() != null ? query.getLowTempThreshold() : 10.0; 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<TempBoardAnomalyVo> 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(); return Collections.emptyList();
} }
double threshold = query.getHighTempThreshold() != null ? query.getHighTempThreshold() : 35.0; 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
);
} }
/** 温升过快事件 */ /**
*
* <p>
* List
* DOM TableDataInfo
* el-table-v2 +
*/
@Override @Override
public List<TempBoardAnomalyVo> getRapidRiseEvents(TempBoardQueryBo query) { public TableDataInfo<TempBoardAnomalyVo> getRapidRiseEvents(TempBoardQueryBo query) {
List<String> tables = resolveTables(query); List<String> tables = resolveTables(query);
if (tables.isEmpty()) { if (tables.isEmpty()) {
return Collections.emptyList(); return TableDataInfo.build();
} }
double threshold = query.getRapidRiseThreshold() != null ? query.getRapidRiseThreshold() : 1.0; 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<TempBoardAnomalyVo> 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昨日对比 */ /** 今日vs昨日对比 */
@Override @Override
public List<TempBoardComparisonVo> getDailyDiff(TempBoardQueryBo query) { public List<TempBoardComparisonVo> getDailyDiff(TempBoardQueryBo query) {
if (query.getEndTime() == null) {
throw new ServiceException("请选择时间范围");
}
// 今日和昨日的时间范围由 endTime 推算 // 今日和昨日的时间范围由 endTime 推算
Date endTime = query.getEndTime(); Date endTime = query.getEndTime();
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
@ -462,23 +554,41 @@ public class TempBoardServiceImpl implements ITempBoardService {
/** 时间逆序可疑数据 */ /** 时间逆序可疑数据 */
@Override @Override
public List<TempBoardQualityVo> getTimeReversal(TempBoardQueryBo query) { public TableDataInfo<TempBoardQualityVo> getTimeReversal(TempBoardQueryBo query) {
List<String> tables = resolveTables(query); List<String> tables = resolveTables(query);
if (tables.isEmpty()) { 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<TempBoardQualityVo> rows = tempBoardMapper.selectTimeReversal(
tables, fmt(query.getStartTime()), fmt(query.getEndTime()), offset, pageSize);
return new TableDataInfo<>(rows, total);
} }
/** 采样间隔异常 */ /**
*
* <p>
* / /
* / TableDataInfo
*/
@Override @Override
public List<TempBoardQualityVo> getSamplingGapAnomalies(TempBoardQueryBo query) { public TableDataInfo<TempBoardQualityVo> getSamplingGapAnomalies(TempBoardQueryBo query) {
List<String> tables = resolveTables(query); List<String> tables = resolveTables(query);
if (tables.isEmpty()) { if (tables.isEmpty()) {
return Collections.emptyList(); return TableDataInfo.build();
} }
int threshold = query.getGapThresholdSeconds() != null ? query.getGapThresholdSeconds() : 300; 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<TempBoardQualityVo> rows = tempBoardMapper.selectSamplingGapAnomalies(
tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold, offset, pageSize);
return new TableDataInfo<>(rows, total);
} }
/** 数据完整率 */ /** 数据完整率 */

@ -11,20 +11,16 @@
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName
</sql> </sql>
<!-- 公共 JOIN关联测点主信息表获取 monitor_name 和 monitor_type --> <!-- 公共 JOIN只保留温度相关测点,统一和振动看板保持相同的设备口径 -->
<sql id="baseJoin"> <sql id="baseJoin">
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)
</sql> </sql>
<!-- 公共过滤:温度类型设备type=5 温度, type=6 温湿度),且温度 > 0 --> <!-- 公共过滤:温度只要求有值,不再把 0℃ / 负温 / 高温场景误判成脏数据 -->
<sql id="tempFilter"> <sql id="tempFilter">
AND (ebmi.monitor_type IN (5, 6) OR ebmi.monitor_type IS NULL) AND t.temperature IS NOT 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)
)
</sql> </sql>
<!-- 公共时间过滤 --> <!-- 公共时间过滤 -->
@ -98,15 +94,21 @@
FROM latest WHERE rn = 1 FROM latest WHERE rn = 1
), ),
max_val AS ( max_val AS (
SELECT TOP 1 monitorId AS maxTempMonitorId, temperature AS maxLatestTemp SELECT TOP 1 monitorId AS maxTempMonitorId,
FROM latest_only ORDER BY temperature DESC monitorName AS maxTempMonitorName,
temperature AS maxLatestTemp
FROM latest_only
ORDER BY temperature DESC, monitorId
), ),
min_val AS ( min_val AS (
SELECT TOP 1 monitorId AS minTempMonitorId, temperature AS minLatestTemp SELECT TOP 1 monitorId AS minTempMonitorId,
FROM latest_only ORDER BY temperature ASC monitorName AS minTempMonitorName,
temperature AS minLatestTemp
FROM latest_only
ORDER BY temperature ASC, monitorId
) )
SELECT m.maxTempMonitorId, m.maxLatestTemp, SELECT m.maxTempMonitorId, m.maxTempMonitorName, m.maxLatestTemp,
n.minTempMonitorId, n.minLatestTemp n.minTempMonitorId, n.minTempMonitorName, n.minLatestTemp
FROM max_val m CROSS JOIN min_val n FROM max_val m CROSS JOIN min_val n
</select> </select>
@ -131,7 +133,7 @@
) )
SELECT TOP (#{topN}) monitorId, monitorName, temperature, collectTime SELECT TOP (#{topN}) monitorId, monitorName, temperature, collectTime
FROM latest WHERE rn = 1 FROM latest WHERE rn = 1
ORDER BY temperature DESC ORDER BY temperature DESC, monitorId
</select> </select>
<!-- A5. 低温 TopN --> <!-- A5. 低温 TopN -->
@ -155,7 +157,7 @@
) )
SELECT TOP (#{topN}) monitorId, monitorName, temperature, collectTime SELECT TOP (#{topN}) monitorId, monitorName, temperature, collectTime
FROM latest WHERE rn = 1 FROM latest WHERE rn = 1
ORDER BY temperature ASC ORDER BY temperature ASC, monitorId
</select> </select>
<!-- A6. 数据新鲜度概览 --> <!-- A6. 数据新鲜度概览 -->
@ -178,7 +180,7 @@
FROM all_data FROM all_data
) )
SELECT monitorId, monitorName, temperature, collectTime, SELECT monitorId, monitorName, temperature, collectTime,
DATEDIFF(SECOND, collectTime, GETUTCDATE()) AS ageSeconds DATEDIFF(SECOND, collectTime, GETDATE()) AS ageSeconds
FROM latest WHERE rn = 1 FROM latest WHERE rn = 1
ORDER BY ageSeconds DESC ORDER BY ageSeconds DESC
</select> </select>
@ -206,7 +208,7 @@
) )
SELECT monitorId, monitorName, temperature, collectTime, recodeTime, SELECT monitorId, monitorName, temperature, collectTime, recodeTime,
DATEDIFF(SECOND, collectTime, recodeTime) AS delaySeconds, DATEDIFF(SECOND, collectTime, recodeTime) AS delaySeconds,
DATEDIFF(SECOND, collectTime, GETUTCDATE()) AS staleSeconds DATEDIFF(SECOND, collectTime, GETDATE()) AS staleSeconds
FROM latest WHERE rn = 1 FROM latest WHERE rn = 1
ORDER BY collectTime DESC, monitorId ORDER BY collectTime DESC, monitorId
</select> </select>
@ -281,9 +283,9 @@
FROM all_data FROM all_data
) )
SELECT monitorId, monitorName, temperature, collectTime, SELECT monitorId, monitorName, temperature, collectTime,
DATEDIFF(SECOND, collectTime, GETUTCDATE()) AS staleSeconds DATEDIFF(SECOND, collectTime, GETDATE()) AS staleSeconds
FROM latest WHERE rn = 1 FROM latest WHERE rn = 1
AND DATEDIFF(SECOND, collectTime, GETUTCDATE()) >= #{staleThreshold} AND DATEDIFF(SECOND, collectTime, GETDATE()) >= #{staleThreshold}
ORDER BY staleSeconds DESC ORDER BY staleSeconds DESC
</select> </select>
@ -360,11 +362,32 @@
ORDER BY statTime ORDER BY statTime
</select> </select>
<!-- C3. 多测点对比趋势 --> <!--
C3. 多测点对比趋势(支持动态聚合粒度)
- 之前固定按分钟聚合10 测点 × 30 天会产生 ~43 万点位,
SQL/网络/ECharts 三端同时受压;即使前端有 LTTB 也只能减轻渲染。
- 现改为由 Service 根据时间跨度传入 granularity
* MINUTE≤ 1 天
* FIFTEEN_MINUTE1~7 天(约 672 点/测点)
* HOUR> 7 天(约 720 点/30 天/测点)
- SELECT 和 GROUP BY 两处使用完全一致的时间表达式,避免 SQL Server
报 "不是 GROUP BY 中表达式" 的错误。
-->
<select id="selectMultiCompareTrend" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardTrendVo"> <select id="selectMultiCompareTrend" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardTrendVo">
<foreach collection="tableNames" item="tableName" separator=" UNION ALL "> <foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT SELECT
FORMAT(t.collectTime, 'yyyy-MM-dd HH:mm:00') AS statTime, <choose>
<when test="granularity == 'HOUR'">
FORMAT(t.collectTime, 'yyyy-MM-dd HH:00:00')
</when>
<when test="granularity == 'FIFTEEN_MINUTE'">
FORMAT(DATEADD(MINUTE, DATEDIFF(MINUTE, 0, t.collectTime) / 15 * 15, 0), 'yyyy-MM-dd HH:mm:00')
</when>
<otherwise>
FORMAT(t.collectTime, 'yyyy-MM-dd HH:mm:00')
</otherwise>
</choose> AS statTime,
t.monitorId, t.monitorId,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName, COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
ROUND(AVG(t.temperature), 2) AS avgTemp ROUND(AVG(t.temperature), 2) AS avgTemp
@ -380,12 +403,34 @@
<include refid="timeFilter"/> <include refid="timeFilter"/>
<include refid="tempFilter"/> <include refid="tempFilter"/>
</where> </where>
GROUP BY FORMAT(t.collectTime, 'yyyy-MM-dd HH:mm:00'), t.monitorId, GROUP BY
<choose>
<when test="granularity == 'HOUR'">
FORMAT(t.collectTime, 'yyyy-MM-dd HH:00:00')
</when>
<when test="granularity == 'FIFTEEN_MINUTE'">
FORMAT(DATEADD(MINUTE, DATEDIFF(MINUTE, 0, t.collectTime) / 15 * 15, 0), 'yyyy-MM-dd HH:mm:00')
</when>
<otherwise>
FORMAT(t.collectTime, 'yyyy-MM-dd HH:mm:00')
</otherwise>
</choose>,
t.monitorId,
COALESCE(ebmi.monitor_name, t.monitorId) COALESCE(ebmi.monitor_name, t.monitorId)
</foreach> </foreach>
ORDER BY statTime, monitorId ORDER BY statTime, monitorId
</select> </select>
<!-- C3-Ext. 温度测点下拉列表(启用状态由 Service 注入,默认 0 -->
<select id="selectTempMonitors" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardMonitorOptionVo">
SELECT monitor_code AS monitorId,
monitor_name AS monitorName
FROM ems_base_monitor_info
WHERE monitor_type IN (5, 6)
AND monitor_status = #{activeStatus}
ORDER BY monitor_name
</select>
<!-- C4. 日均温趋势 --> <!-- C4. 日均温趋势 -->
<select id="selectDailyTrend" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardTrendVo"> <select id="selectDailyTrend" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardTrendVo">
<foreach collection="tableNames" item="tableName" separator=" UNION ALL "> <foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
@ -515,22 +560,6 @@
ORDER BY tempBin ORDER BY tempBin
</select> </select>
<!-- D3. 温度箱线图原始数据(按测点) -->
<select id="selectBoxplotData" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardDistributionVo">
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT t.monitorId,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
t.temperature
FROM ${tableName} t
<include refid="baseJoin"/>
<where>
<include refid="timeFilter"/>
<include refid="tempFilter"/>
</where>
</foreach>
ORDER BY monitorId, temperature
</select>
<!-- D4. 日历热力图 --> <!-- D4. 日历热力图 -->
<select id="selectCalendarHeatmap" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardDistributionVo"> <select id="selectCalendarHeatmap" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardDistributionVo">
<foreach collection="tableNames" item="tableName" separator=" UNION ALL "> <foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
@ -585,6 +614,23 @@
</where> </where>
</foreach> </foreach>
ORDER BY collectTime DESC ORDER BY collectTime DESC
OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY
</select>
<select id="countHighTempEvents" resultType="long">
SELECT COUNT(1)
FROM (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT t.monitorId
FROM ${tableName} t
<include refid="baseJoin"/>
<where>
t.temperature >= #{highTempThreshold}
<include refid="timeFilter"/>
<include refid="tempFilter"/>
</where>
</foreach>
) sub
</select> </select>
<!-- E2. 低温事件 --> <!-- E2. 低温事件 -->
@ -604,6 +650,23 @@
</where> </where>
</foreach> </foreach>
ORDER BY collectTime DESC ORDER BY collectTime DESC
OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY
</select>
<select id="countLowTempEvents" resultType="long">
SELECT COUNT(1)
FROM (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT t.monitorId
FROM ${tableName} t
<include refid="baseJoin"/>
<where>
t.temperature &lt;= #{lowTempThreshold}
<include refid="timeFilter"/>
<include refid="tempFilter"/>
</where>
</foreach>
) sub
</select> </select>
<!-- E3. 连续高温时段(岛屿分组) --> <!-- E3. 连续高温时段(岛屿分组) -->
@ -638,11 +701,19 @@
COUNT(*) AS sampleCount COUNT(*) AS sampleCount
FROM grouped WHERE isHigh = 1 FROM grouped WHERE isHigh = 1
GROUP BY monitorId, monitorName, grp GROUP BY monitorId, monitorName, grp
HAVING COUNT(*) >= 2 HAVING COUNT(*) >= #{continuousMinSamples}
ORDER BY monitorId, startTime ORDER BY monitorId, startTime
</select> </select>
<!-- E4. 温升过快事件 --> <!--
E4. 温升过快事件(分页)
- 之前全量返回,工况切换或阈值偏低时极易一次返回数千条,让浏览器
DOM 和 JS 同时吃紧。
- 现在同样采用 OFFSET/FETCH 分页模型,配合下面的 countRapidRiseEvents
保持与高温/低温/时间逆序一致的语义。
- CTE 结构不变,仅在最外层保留 ORDER BY 并加分页子句。
-->
<select id="selectRapidRiseEvents" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardAnomalyVo"> <select id="selectRapidRiseEvents" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardAnomalyVo">
WITH base AS ( WITH base AS (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL "> <foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
@ -674,6 +745,33 @@
AND CAST(temperature - prevTemp AS FLOAT) / AND CAST(temperature - prevTemp AS FLOAT) /
NULLIF(CAST(DATEDIFF(SECOND, prevTime, collectTime) AS FLOAT), 0) * 60 >= #{rapidRiseThreshold} NULLIF(CAST(DATEDIFF(SECOND, prevTime, collectTime) AS FLOAT), 0) * 60 >= #{rapidRiseThreshold}
ORDER BY risePerMin DESC ORDER BY risePerMin DESC
OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY
</select>
<!-- E4-count. 温升过快事件总数(不带 ORDER BY / 分页,仅用于计总) -->
<select id="countRapidRiseEvents" resultType="long">
WITH base AS (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT t.monitorId, t.temperature, t.collectTime
FROM ${tableName} t
<include refid="baseJoin"/>
<where>
<include refid="timeFilter"/>
<include refid="tempFilter"/>
</where>
</foreach>
),
seq AS (
SELECT monitorId, collectTime, temperature,
LAG(collectTime) OVER (PARTITION BY monitorId ORDER BY collectTime) AS prevTime,
LAG(temperature) OVER (PARTITION BY monitorId ORDER BY collectTime) AS prevTemp
FROM base
)
SELECT COUNT(1)
FROM seq
WHERE prevTime IS NOT NULL
AND CAST(temperature - prevTemp AS FLOAT) /
NULLIF(CAST(DATEDIFF(SECOND, prevTime, collectTime) AS FLOAT), 0) * 60 >= #{rapidRiseThreshold}
</select> </select>
<!-- E5. 温度抖动异常(按小时标准差) --> <!-- E5. 温度抖动异常(按小时标准差) -->
@ -682,6 +780,7 @@
SELECT t.monitorId, SELECT t.monitorId,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName, COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
FORMAT(t.collectTime, 'yyyy-MM-dd HH:00:00') AS statTime, 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, ROUND(STDEVP(t.temperature), 4) AS tempStddev,
'JITTER' AS anomalyType 'JITTER' AS anomalyType
FROM ${tableName} t FROM ${tableName} t
@ -772,12 +871,30 @@
ORDER BY diffValue DESC ORDER BY diffValue DESC
</select> </select>
<!-- F4. 峰值对比 --> <!--
F4. 峰值对比(含平均温度 + 波动幅度)
- 前端峰值排行表需要同时展示 maxTemp / avgTemp / tempRange 三列,
这里一次性在聚合里补齐 AVG 与 MAX-MIN避免前端只能渲染 0.00℃ 的伪数据。
- 外层 sub 先做"按分表聚合",再在外层对同一测点合并:
* maxTemp 用 MAX天然可以跨分表合并
* avgTemp 用样本数加权平均,保证多分表下统计口径仍然正确;
* tempRange = MAX(maxTemp) - MIN(minTemp),先在分表里算出 min/max 再外层合并。
-->
<select id="selectPeakCompare" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardComparisonVo"> <select id="selectPeakCompare" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardComparisonVo">
SELECT sub.monitorId,
sub.monitorName,
ROUND(MAX(sub.maxTemp), 2) AS maxTemp,
ROUND(SUM(sub.sumTemp) / NULLIF(SUM(sub.sampleCount), 0), 2) AS avgTemp,
ROUND(MAX(sub.maxTemp) - MIN(sub.minTemp), 2) AS tempRange
FROM (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL "> <foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT t.monitorId, SELECT t.monitorId,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName, COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
MAX(t.temperature) AS maxTemp MAX(t.temperature) AS maxTemp,
MIN(t.temperature) AS minTemp,
SUM(t.temperature) AS sumTemp,
COUNT(*) AS sampleCount
FROM ${tableName} t FROM ${tableName} t
<include refid="baseJoin"/> <include refid="baseJoin"/>
<where> <where>
@ -786,6 +903,8 @@
</where> </where>
GROUP BY t.monitorId, COALESCE(ebmi.monitor_name, t.monitorId) GROUP BY t.monitorId, COALESCE(ebmi.monitor_name, t.monitorId)
</foreach> </foreach>
) sub
GROUP BY sub.monitorId, sub.monitorName
ORDER BY maxTemp DESC ORDER BY maxTemp DESC
</select> </select>
@ -850,9 +969,31 @@
</where> </where>
</foreach> </foreach>
ORDER BY collectTime DESC ORDER BY collectTime DESC
OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY
</select> </select>
<!-- G3. 采样间隔异常 --> <select id="countTimeReversal" resultType="long">
SELECT COUNT(1)
FROM (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT t.monitorId
FROM ${tableName} t
<include refid="baseJoin"/>
<where>
t.recodeTime &lt; t.collectTime
<include refid="timeFilter"/>
<include refid="tempFilter"/>
</where>
</foreach>
) sub
</select>
<!--
G3. 采样间隔异常(分页)
- 通讯抖动 / 断点重传 / 时间同步异常时这张表会爆量,
同样改为 OFFSET/FETCH 分页 + count 双接口,和其他异常表对齐。
-->
<select id="selectSamplingGapAnomalies" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardQualityVo"> <select id="selectSamplingGapAnomalies" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardQualityVo">
WITH base AS ( WITH base AS (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL "> <foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
@ -876,6 +1017,31 @@
FROM seq WHERE prevTime IS NOT NULL FROM seq WHERE prevTime IS NOT NULL
AND DATEDIFF(SECOND, prevTime, collectTime) >= #{gapThreshold} AND DATEDIFF(SECOND, prevTime, collectTime) >= #{gapThreshold}
ORDER BY gapSeconds DESC ORDER BY gapSeconds DESC
OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY
</select>
<!-- G3-count. 采样间隔异常总数 -->
<select id="countSamplingGapAnomalies" resultType="long">
WITH base AS (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT t.monitorId, t.collectTime
FROM ${tableName} t
<include refid="baseJoin"/>
<where>
<include refid="timeFilter"/>
<include refid="tempFilter"/>
</where>
</foreach>
),
seq AS (
SELECT monitorId, collectTime,
LAG(collectTime) OVER (PARTITION BY monitorId ORDER BY collectTime) AS prevTime
FROM base
)
SELECT COUNT(1)
FROM seq
WHERE prevTime IS NOT NULL
AND DATEDIFF(SECOND, prevTime, collectTime) >= #{gapThreshold}
</select> </select>
<!-- G4. 数据完整率 --> <!-- G4. 数据完整率 -->
@ -935,7 +1101,8 @@
WHEN t.temperature &lt; 15 THEN '&lt;15' WHEN t.temperature &lt; 15 THEN '&lt;15'
WHEN t.temperature &lt; 20 THEN '15-20' WHEN t.temperature &lt; 20 THEN '15-20'
WHEN t.temperature &lt; 25 THEN '20-25' WHEN t.temperature &lt; 25 THEN '20-25'
ELSE '>=25' WHEN t.temperature &lt; 30 THEN '25-30'
ELSE '>=30'
END AS tempBucket END AS tempBucket
FROM ${tableName} t FROM ${tableName} t
<include refid="baseJoin"/> <include refid="baseJoin"/>

Loading…
Cancel
Save