diff --git a/ruoyi-admin/src/test/java/org/dromara/test/TempBoardServiceImplTest.java b/ruoyi-admin/src/test/java/org/dromara/test/TempBoardServiceImplTest.java new file mode 100644 index 0000000..5b1acbc --- /dev/null +++ b/ruoyi-admin/src/test/java/org/dromara/test/TempBoardServiceImplTest.java @@ -0,0 +1,124 @@ +package org.dromara.test; + +import org.dromara.common.core.exception.ServiceException; +import org.dromara.ems.record.mapper.RecordIotenvInstantMapper; +import org.dromara.ems.record.service.RecordIotenvPartitionService; +import org.dromara.ems.report.domain.vo.tempboard.TempBoardQueryBo; +import org.dromara.ems.report.mapper.TempBoardMapper; +import org.dromara.ems.report.service.impl.TempBoardServiceImpl; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * 温度看板 Service 回归测试 + * + *
这里优先锁住这次审阅报告里最容易回退的服务层校验逻辑, + * 避免后续又出现“空测点静默查空”和“时间范围边界漏拦截”的问题。
+ */ +@DisplayName("温度看板 Service 回归测试") +public class TempBoardServiceImplTest { + + @Test + @DisplayName("getDailyDiff 在缺少结束时间时应抛出友好异常") + public void shouldThrowWhenDailyDiffMissingEndTime() { + TempBoardServiceImpl service = createService(Collections.singletonList("record_iotenv_instant_20260417")); + TempBoardQueryBo query = new TempBoardQueryBo(); + + ServiceException exception = Assertions.assertThrows(ServiceException.class, () -> service.getDailyDiff(query)); + + Assertions.assertEquals("请选择时间范围", exception.getMessage()); + } + + @Test + @DisplayName("getMinuteTrend 在未选择测点时应快速失败") + public void shouldThrowWhenMinuteTrendMissingMonitorId() { + TempBoardServiceImpl service = createService(Collections.singletonList("record_iotenv_instant_20260417")); + TempBoardQueryBo query = buildRangeQuery(); + query.setMonitorId(" "); + + ServiceException exception = Assertions.assertThrows(ServiceException.class, () -> service.getMinuteTrend(query)); + + Assertions.assertEquals("请选择测点", exception.getMessage()); + } + + @Test + @DisplayName("getHourlyTrend 在未选择测点时应快速失败") + public void shouldThrowWhenHourlyTrendMissingMonitorId() { + TempBoardServiceImpl service = createService(Collections.singletonList("record_iotenv_instant_20260417")); + TempBoardQueryBo query = buildRangeQuery(); + query.setMonitorId(""); + + ServiceException exception = Assertions.assertThrows(ServiceException.class, () -> service.getHourlyTrend(query)); + + Assertions.assertEquals("请选择测点", exception.getMessage()); + } + + @Test + @DisplayName("查询跨度超过 90 天应被拒绝") + public void shouldRejectRangeLongerThanMaxQueryDays() { + TempBoardServiceImpl service = createService(Collections.emptyList()); + TempBoardQueryBo query = new TempBoardQueryBo(); + long ninetyDaysMs = 90L * 24 * 3600 * 1000; + query.setStartTime(new Date(0L)); + // 这里故意多加 1 秒,验证不能再被整数除法边界漏过去。 + query.setEndTime(new Date(ninetyDaysMs + 1000L)); + + ServiceException exception = Assertions.assertThrows(ServiceException.class, () -> service.getOverview(query)); + + Assertions.assertEquals("查询跨度不能超过90天,请缩小范围", exception.getMessage()); + } + + private TempBoardServiceImpl createService(List+ * 多测点对比趋势会根据查询跨度自动选择粒度: + *
+ * granularity 取值: + *
ems.temp-board.active-monitor-status 覆盖,
+ * 避免未来业务侧调整枚举后测点列表整体消失。
+ */
+ @Value("${ems.temp-board.active-monitor-status:0}")
+ private Integer activeMonitorStatus;
+
private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
/** 线程安全的静态格式化器,替代每次 new SimpleDateFormat */
@@ -45,6 +52,10 @@ public class TempBoardServiceImpl implements ITempBoardService {
/** 最大查询天数(硬限制,防止灾难性查询) */
private static final int MAX_QUERY_DAYS = 90;
+ /** 多测点对比最少/最多测点数,与前端下拉上限保持一致,防止被直调接口绕过。 */
+ private static final int MULTI_COMPARE_MIN = 2;
+ private static final int MULTI_COMPARE_MAX = 10;
+
// ==================== 工具方法 ====================
/**
@@ -57,8 +68,9 @@ public class TempBoardServiceImpl implements ITempBoardService {
}
// 时间范围硬限制校验
long diffMs = query.getEndTime().getTime() - query.getStartTime().getTime();
- long diffDays = diffMs / (24 * 3600 * 1000);
- if (diffDays > MAX_QUERY_DAYS) {
+ long maxRangeMs = (long) MAX_QUERY_DAYS * 24 * 3600 * 1000;
+ // 这里直接比较毫秒值,避免 90 天零 1 秒这类边界被整数除法误放行。
+ if (diffMs > maxRangeMs) {
throw new ServiceException("查询跨度不能超过" + MAX_QUERY_DAYS + "天,请缩小范围");
}
return partitionService.resolveTables(query.getStartTime(), query.getEndTime());
@@ -69,6 +81,16 @@ public class TempBoardServiceImpl implements ITempBoardService {
return FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
}
+ /**
+ * 单测点趋势类接口必须显式选择测点,
+ * 否则前端会把“空结果”误解成系统无数据,排障成本很高。
+ */
+ private void validateMonitorIdRequired(String monitorId) {
+ if (monitorId == null || monitorId.trim().isEmpty()) {
+ throw new ServiceException("请选择测点");
+ }
+ }
+
/**
* 根据查询时间跨度自动决定高级分析的聚合粒度
* 跨度 <= 1 天:分钟级
@@ -87,6 +109,20 @@ public class TempBoardServiceImpl implements ITempBoardService {
}
}
+ /**
+ * 明细分页统一兜底正数,避免前端传 0 或负数时把 SQL Server OFFSET 算成非法值。
+ */
+ private int resolvePageNum(Integer pageNum) {
+ return pageNum == null || pageNum < 1 ? 1 : pageNum;
+ }
+
+ /**
+ * 这里沿用页面默认 20 条;若调用方传非法值,则快速回退到安全页大小。
+ */
+ private int resolvePageSize(Integer pageSize) {
+ return pageSize == null || pageSize < 1 ? 20 : pageSize;
+ }
+
// ==================== A. 温度总览 ====================
/** 温度总览(测点数 + 平均温度 + 最高/最低 + TopN + 新鲜度) */
@@ -128,8 +164,10 @@ public class TempBoardServiceImpl implements ITempBoardService {
if (minMaxVo != null) {
result.setMaxLatestTemp(minMaxVo.getMaxLatestTemp());
result.setMaxTempMonitorId(minMaxVo.getMaxTempMonitorId());
+ result.setMaxTempMonitorName(minMaxVo.getMaxTempMonitorName());
result.setMinLatestTemp(minMaxVo.getMinLatestTemp());
result.setMinTempMonitorId(minMaxVo.getMinTempMonitorId());
+ result.setMinTempMonitorName(minMaxVo.getMinTempMonitorName());
}
result.setHighTempTopN(highTopN);
result.setLowTempTopN(lowTopN);
@@ -201,6 +239,13 @@ public class TempBoardServiceImpl implements ITempBoardService {
if (tables.isEmpty()) {
return Collections.emptyList();
}
+ validateMonitorIdRequired(query.getMonitorId());
+ // 跨天分钟点位过多时直接切小时聚合,优先保证大数据场景可用性。
+ long diffMs = query.getEndTime().getTime() - query.getStartTime().getTime();
+ if (diffMs > 24L * 3600 * 1000) {
+ return tempBoardMapper.selectHourlyTrend(tables, query.getMonitorId(),
+ fmt(query.getStartTime()), fmt(query.getEndTime()));
+ }
return tempBoardMapper.selectMinuteTrend(tables, query.getMonitorId(),
fmt(query.getStartTime()), fmt(query.getEndTime()));
}
@@ -212,6 +257,7 @@ public class TempBoardServiceImpl implements ITempBoardService {
if (tables.isEmpty()) {
return Collections.emptyList();
}
+ validateMonitorIdRequired(query.getMonitorId());
return tempBoardMapper.selectHourlyTrend(tables, query.getMonitorId(),
fmt(query.getStartTime()), fmt(query.getEndTime()));
}
@@ -219,12 +265,32 @@ public class TempBoardServiceImpl implements ITempBoardService {
/** 多测点对比趋势 */
@Override
public List
+ * 采样噪声、工况切换、阈值偏低时这张表会快速碎片化,原先全量 List 会
+ * 把几千条 DOM 直接塞给浏览器。这里改为 TableDataInfo,配合前端
+ * el-table-v2 + 分页以保证大数据场景下的渲染压力收敛。
+ */
@Override
- public List
+ * 通讯抖动 / 断点重传 / 时间同步异常时同样会爆量,改为分页返回,
+ * 与时间逆序 / 温升过快 保持一致的 TableDataInfo 模型。
+ */
@Override
- public List