|
|
|
|
@ -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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 数据完整率 */
|
|
|
|
|
|