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

新增温度测点下拉列表接口,支持前端快速选择测点
为异常事件列表添加分页支持,防止大数据量导致性能问题
优化多测点对比趋势的聚合粒度,根据时间跨度自动调整
移除箱线图原始数据接口,简化数据分布分析
补充测点名称字段,提升最高/最低温度展示信息量
增加连续高温最少样本数配置,提高异常检测灵活性
完善测试用例,覆盖空测点和时间范围校验场景
main
zch 2 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 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<List<TempBoardMonitorOptionVo>> 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<List<TempBoardDistributionVo>> 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<List<TempBoardAnomalyVo>> getHighTempEvents(TempBoardQueryBo query) {
public R<TableDataInfo<TempBoardAnomalyVo>> getHighTempEvents(TempBoardQueryBo query) {
return R.ok(tempBoardService.getHighTempEvents(query));
}
/** 低温事件 */
@SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/anomaly/lowTemp")
public R<List<TempBoardAnomalyVo>> getLowTempEvents(TempBoardQueryBo query) {
public R<TableDataInfo<TempBoardAnomalyVo>> 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<List<TempBoardAnomalyVo>> getRapidRiseEvents(TempBoardQueryBo query) {
public R<TableDataInfo<TempBoardAnomalyVo>> 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<List<TempBoardQualityVo>> getTimeReversal(TempBoardQueryBo query) {
public R<TableDataInfo<TempBoardQualityVo>> getTimeReversal(TempBoardQueryBo query) {
return R.ok(tempBoardService.getTimeReversal(query));
}
/** 采样间隔异常 */
/** 采样间隔异常(分页) */
@SaCheckPermission("ems/report:tempBoard:query")
@GetMapping("/quality/samplingGap")
public R<List<TempBoardQualityVo>> getSamplingGapAnomalies(TempBoardQueryBo query) {
public R<TableDataInfo<TempBoardQualityVo>> getSamplingGapAnomalies(TempBoardQueryBo query) {
return R.ok(tempBoardService.getSamplingGapAnomalies(query));
}

@ -35,6 +35,12 @@ public class TempBoardAnomalyVo {
/** 标准差(用于抖动检测) */
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 */
private String maxTempMonitorId;
/** 最高温度测点名称 */
private String maxTempMonitorName;
/** 当前最低温度 */
private BigDecimal minLatestTemp;
/** 最低温度测点ID */
private String minTempMonitorId;
/** 最低温度测点名称 */
private String minTempMonitorName;
/** 高温 TopN */
private List<MonitorTempRank> highTempTopN;

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

@ -60,4 +60,18 @@ public class TempBoardTrendVo {
/** 前一时刻时间 */
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("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,
@Param("monitorIds") List<String> monitorIds,
@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,
@ -128,11 +142,6 @@ public interface TempBoardMapper {
@Param("startTime") String startTime,
@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,
@Param("startTime") String startTime,
@ -149,25 +158,50 @@ public interface TempBoardMapper {
List<TempBoardAnomalyVo> selectHighTempEvents(@Param("tableNames") List<String> 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<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("highTempThreshold") double highTempThreshold);
/** 低温事件 */
List<TempBoardAnomalyVo> selectLowTempEvents(@Param("tableNames") List<String> 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<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("lowTempThreshold") double lowTempThreshold);
/** 连续高温时段(岛屿分组) */
List<TempBoardAnomalyVo> selectContinuousHighTemp(@Param("tableNames") List<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("highTempThreshold") double highTempThreshold);
@Param("highTempThreshold") double highTempThreshold,
@Param("continuousMinSamples") int continuousMinSamples);
/** 温升过快事件 */
/** 温升过快事件(分页) */
List<TempBoardAnomalyVo> selectRapidRiseEvents(@Param("tableNames") List<String> 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<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("rapidRiseThreshold") double rapidRiseThreshold);
/** 温度抖动异常 */
List<TempBoardAnomalyVo> selectJitterAnomalies(@Param("tableNames") List<String> tableNames,
@ -214,13 +248,28 @@ public interface TempBoardMapper {
/** 时间逆序可疑数据 */
List<TempBoardQualityVo> selectTimeReversal(@Param("tableNames") List<String> 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<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
/** 采样间隔异常(分页) */
List<TempBoardQualityVo> selectSamplingGapAnomalies(@Param("tableNames") List<String> 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<String> tableNames,
@Param("startTime") String startTime,
@Param("endTime") String endTime,
@Param("gapThreshold") int gapThresholdSeconds);
/** 数据完整率 */
List<TempBoardQualityVo> selectCompletenessRate(@Param("tableNames") List<String> tableNames,

@ -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<TempBoardTrendVo> getMultiCompareTrend(TempBoardQueryBo query);
/** 温度测点下拉列表 */
List<TempBoardMonitorOptionVo> listTempMonitors();
/** 日均温趋势 */
List<TempBoardTrendVo> getDailyTrend(TempBoardQueryBo query);
@ -64,9 +68,6 @@ public interface ITempBoardService {
/** 温度直方图 */
List<TempBoardDistributionVo> getHistogram(TempBoardQueryBo query);
/** 温度箱线图原始数据 */
List<TempBoardDistributionVo> getBoxplotData(TempBoardQueryBo query);
/** 日历热力图 */
List<TempBoardDistributionVo> getCalendarHeatmap(TempBoardQueryBo query);
@ -76,16 +77,16 @@ public interface ITempBoardService {
// ==================== 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> getRapidRiseEvents(TempBoardQueryBo query);
/** 温升过快事件(分页) */
TableDataInfo<TempBoardAnomalyVo> getRapidRiseEvents(TempBoardQueryBo query);
/** 温度抖动异常 */
List<TempBoardAnomalyVo> getJitterAnomalies(TempBoardQueryBo query);
@ -113,10 +114,10 @@ public interface ITempBoardService {
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);

@ -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
* <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";
/** 线程安全的静态格式化器,替代每次 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<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);
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<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()) {
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<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
public List<TempBoardDistributionVo> getCalendarHeatmap(TempBoardQueryBo query) {
@ -314,24 +371,36 @@ public class TempBoardServiceImpl implements ITempBoardService {
/** 高温事件 */
@Override
public List<TempBoardAnomalyVo> getHighTempEvents(TempBoardQueryBo query) {
public TableDataInfo<TempBoardAnomalyVo> getHighTempEvents(TempBoardQueryBo query) {
List<String> 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<TempBoardAnomalyVo> rows = tempBoardMapper.selectHighTempEvents(
tables, fmt(query.getStartTime()), fmt(query.getEndTime()), threshold, offset, pageSize);
return new TableDataInfo<>(rows, total);
}
/** 低温事件 */
@Override
public List<TempBoardAnomalyVo> getLowTempEvents(TempBoardQueryBo query) {
public TableDataInfo<TempBoardAnomalyVo> getLowTempEvents(TempBoardQueryBo query) {
List<String> 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<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();
}
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
public List<TempBoardAnomalyVo> getRapidRiseEvents(TempBoardQueryBo query) {
public TableDataInfo<TempBoardAnomalyVo> getRapidRiseEvents(TempBoardQueryBo query) {
List<String> 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<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昨日对比 */
@Override
public List<TempBoardComparisonVo> 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<TempBoardQualityVo> getTimeReversal(TempBoardQueryBo query) {
public TableDataInfo<TempBoardQualityVo> getTimeReversal(TempBoardQueryBo query) {
List<String> 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<TempBoardQualityVo> rows = tempBoardMapper.selectTimeReversal(
tables, fmt(query.getStartTime()), fmt(query.getEndTime()), offset, pageSize);
return new TableDataInfo<>(rows, total);
}
/** 采样间隔异常 */
/**
*
* <p>
* / /
* / TableDataInfo
*/
@Override
public List<TempBoardQualityVo> getSamplingGapAnomalies(TempBoardQueryBo query) {
public TableDataInfo<TempBoardQualityVo> getSamplingGapAnomalies(TempBoardQueryBo query) {
List<String> 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<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
</sql>
<!-- 公共 JOIN关联测点主信息表获取 monitor_name 和 monitor_type -->
<!-- 公共 JOIN只保留温度相关测点,统一和振动看板保持相同的设备口径 -->
<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>
<!-- 公共过滤:温度类型设备type=5 温度, type=6 温湿度),且温度 > 0 -->
<!-- 公共过滤:温度只要求有值,不再把 0℃ / 负温 / 高温场景误判成脏数据 -->
<sql id="tempFilter">
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
</sql>
<!-- 公共时间过滤 -->
@ -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
</select>
@ -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
</select>
<!-- A5. 低温 TopN -->
@ -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
</select>
<!-- A6. 数据新鲜度概览 -->
@ -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
</select>
@ -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
</select>
@ -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
</select>
@ -360,11 +362,32 @@
ORDER BY statTime
</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">
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
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,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
ROUND(AVG(t.temperature), 2) AS avgTemp
@ -380,12 +403,34 @@
<include refid="timeFilter"/>
<include refid="tempFilter"/>
</where>
GROUP BY FORMAT(t.collectTime, 'yyyy-MM-dd HH:mm:00'), t.monitorId,
COALESCE(ebmi.monitor_name, 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)
</foreach>
ORDER BY statTime, monitorId
</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. 日均温趋势 -->
<select id="selectDailyTrend" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardTrendVo">
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
@ -515,22 +560,6 @@
ORDER BY tempBin
</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. 日历热力图 -->
<select id="selectCalendarHeatmap" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardDistributionVo">
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
@ -585,6 +614,23 @@
</where>
</foreach>
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>
<!-- E2. 低温事件 -->
@ -604,6 +650,23 @@
</where>
</foreach>
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>
<!-- E3. 连续高温时段(岛屿分组) -->
@ -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
</select>
<!-- E4. 温升过快事件 -->
<!--
E4. 温升过快事件(分页)
- 之前全量返回,工况切换或阈值偏低时极易一次返回数千条,让浏览器
DOM 和 JS 同时吃紧。
- 现在同样采用 OFFSET/FETCH 分页模型,配合下面的 countRapidRiseEvents
保持与高温/低温/时间逆序一致的语义。
- CTE 结构不变,仅在最外层保留 ORDER BY 并加分页子句。
-->
<select id="selectRapidRiseEvents" resultType="org.dromara.ems.report.domain.vo.tempboard.TempBoardAnomalyVo">
WITH base AS (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
@ -674,6 +745,33 @@
AND CAST(temperature - prevTemp AS FLOAT) /
NULLIF(CAST(DATEDIFF(SECOND, prevTime, collectTime) AS FLOAT), 0) * 60 >= #{rapidRiseThreshold}
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>
<!-- E5. 温度抖动异常(按小时标准差) -->
@ -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
</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">
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
SELECT t.monitorId,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
MAX(t.temperature) AS maxTemp
FROM ${tableName} t
<include refid="baseJoin"/>
<where>
<include refid="timeFilter"/>
<include refid="tempFilter"/>
</where>
GROUP BY t.monitorId, COALESCE(ebmi.monitor_name, t.monitorId)
</foreach>
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 ">
SELECT t.monitorId,
COALESCE(ebmi.monitor_name, t.monitorId) AS monitorName,
MAX(t.temperature) AS maxTemp,
MIN(t.temperature) AS minTemp,
SUM(t.temperature) AS sumTemp,
COUNT(*) AS sampleCount
FROM ${tableName} t
<include refid="baseJoin"/>
<where>
<include refid="timeFilter"/>
<include refid="tempFilter"/>
</where>
GROUP BY t.monitorId, COALESCE(ebmi.monitor_name, t.monitorId)
</foreach>
) sub
GROUP BY sub.monitorId, sub.monitorName
ORDER BY maxTemp DESC
</select>
@ -850,9 +969,31 @@
</where>
</foreach>
ORDER BY collectTime DESC
OFFSET #{offset} ROWS FETCH NEXT #{pageSize} ROWS ONLY
</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">
WITH base AS (
<foreach collection="tableNames" item="tableName" separator=" UNION ALL ">
@ -876,6 +1017,31 @@
FROM seq WHERE prevTime IS NOT NULL
AND DATEDIFF(SECOND, prevTime, collectTime) >= #{gapThreshold}
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>
<!-- G4. 数据完整率 -->
@ -935,7 +1101,8 @@
WHEN t.temperature &lt; 15 THEN '&lt;15'
WHEN t.temperature &lt; 20 THEN '15-20'
WHEN t.temperature &lt; 25 THEN '20-25'
ELSE '>=25'
WHEN t.temperature &lt; 30 THEN '25-30'
ELSE '>=30'
END AS tempBucket
FROM ${tableName} t
<include refid="baseJoin"/>

Loading…
Cancel
Save