<think>我们需要对代码进行以下改进:
1. 数据缺失处理:在按6小时分桶分析时,即使某些小时数据缺失,也要基于已有的数据进行分析。但要注意,如果某个桶内完全没有数据,则不能认为该桶有效。
2. 活跃报警处理:对于已经在报警中的设备,如果分析后报警仍然持续,则不进行操作;如果报警结束,则更新记录(补全结束时间)。
3. 数据预过滤:在分析每个设备时,先检查小时数据的最小值是否超过远高于日常值(farbeyondvalue)和高限值(limitingvalue)。如果最小值都没有超过,则无需进行详细分析。
4. 报警时间精确:报警的开始和结束时间应该是实际发生的小时时间(即小时数据的datadt字段)。
具体修改点:
1. 在`handleNewAlarm`方法中,在准备时间序列数据后,先检查最小值是否超过阈值(farbeyondvalue和limitingvalue)。如果没有超过,直接跳过该设备。
2. 在`handleActiveAlarm`方法中,同样先检查最小值(从报警开始时间起的数据)是否低于阈值,如果低于则说明可能恢复,但具体恢复时间需要进一步分析。不过,我们已经在`checkRecovery`方法中按桶检查,所以这里可以保留。但为了效率,可以先检查最小值,如果最小值都没有超过远高于日常值,那么可以立即确定恢复时间(取最早的小时数据点)。
3. 修改`findValidPeriods`方法,使其能够处理数据缺失的情况。在分桶时,如果某个桶内没有数据点,则该桶无效(即不满足条件)。同时,在检查限制值条件时,同样考虑数据缺失,只要存在一个数据点超过限制值即可。
4. 报警时间使用小时数据中的实际时间(datadt),在创建报警记录时,开始时间取第一个满足条件的小时数据点的时间,结束时间取最后一个满足条件的小时数据点的时间(如果报警结束)。
由于代码较长,下面只展示修改的关键部分:
### 修改1:在`handleNewAlarm`中添加最小值检查
```java
private void handleNewAlarm(EquipmentparameterEntity equipment,
List<Map> hourDataList,
double farBeyondValue,
double limitingValue) {
// 1. 准备时间序列数据
List<DataPoint> dataPoints = ... ; // 同上
// 新增:检查最小值是否超过阈值(至少有一个数据点超过farbeyondValue或limitingValue)
boolean hasExceeded = dataPoints.stream()
.anyMatch(dp -> dp.value >= farBeyondValue || dp.value >= limitingValue);
if (!hasExceeded) {
logger.info("设备{}的小时数据未超过阈值,跳过分析", equipment.getEncode());
return;
}
// 后续代码不变...
}
```
### 修改2:在`handleActiveAlarm`中添加最小值检查(可选,但为了效率)
```java
private void handleActiveAlarm(UleabnormalrecordsEntity activeAlarm,
List<Map> hourDataList,
double farBeyondValue,
double limitingValue) {
// ... 准备数据点
// 新增:如果从报警开始时间起的所有数据点都低于两个阈值,则直接认为报警结束,恢复时间为第一个数据点的时间(如果存在的话)
boolean allBelow = dataPoints.stream()
.allMatch(dp -> dp.value < farBeyondValue && dp.value < limitingValue);
if (allBelow && !dataPoints.isEmpty()) {
// 取第一个数据点的时间作为恢复时间(因为从报警开始后第一个小时就恢复了)
LocalDateTime recoveryTime = dataPoints.get(0).dateTime;
activeAlarm.setCompletetime(toDate(recoveryTime));
activeAlarm.setAbnormalstate("1");
uleabnormalrecordsService.updateById(activeAlarm);
logger.info("报警{}已恢复,结束时间: {}", activeAlarm.getId(), recoveryTime);
return;
}
// 否则,继续原来的检查恢复流程
LocalDateTime recoveryTime = checkRecovery(...);
// ... 后续不变
}
```
### 修改3:调整`findValidPeriods`方法,处理数据缺失
在分桶检查时,如果某个桶内没有数据点,则该桶无效(即不满足条件)。同时,在检查限制值条件时,只要窗口内存在一个数据点超过限制值即可,即使其他点缺失。
修改`findValidPeriods`中的分桶部分:
```java
// 检查6小时分桶条件
boolean[] bucketValid = new boolean[4]; // 4个6小时区间
Arrays.fill(bucketValid, false);
int[] bucketDataCount = new int[4]; // 记录每个桶的数据点数量
for (int i = startIdx; i <= endIdx; i++) {
DataPoint point = dataPoints.get(i);
int hourBucket = (point.dateTime.getHour() / 6) % 4;
bucketDataCount[hourBucket]++; // 增加该桶的数据计数
if (point.value >= farBeyondValue) {
bucketValid[hourBucket] = true;
}
}
// 所有桶都必须满足条件(即每个桶都有至少一个数据点且至少有一个数据点超过远高于日常值)
boolean allBucketsValid = true;
for (int i = 0; i < 4; i++) {
// 如果某个桶没有数据,则条件不满足
if (bucketDataCount[i] == 0) {
allBucketsValid = false;
break;
}
// 如果有数据,但没有超过远高于日常值的数据点
if (!bucketValid[i]) {
allBucketsValid = false;
break;
}
}
```
### 修改4:报警时间精确
在创建报警记录时,我们使用实际的小时数据点的时间。在`findValidPeriods`方法中,我们记录的时间段开始和结束时间已经是数据点的时间,所以无需修改。
### 完整修改后的代码
由于代码较长,这里只展示修改后的关键方法。但为了清晰,我提供完整的类代码(注意:由于篇幅,省略了部分未修改的方法,如`mergePeriods`等,但实际需要保留):
```java
package jnpf.task;
// ... 导入不变
@Slf4j
@Component
public class HAHFAnalysisTask {
// ... 其他不变
private void handleNewAlarm(EquipmentparameterEntity equipment,
List<Map> hourDataList,
double farBeyondValue,
double limitingValue) {
// 1. 准备时间序列数据
List<DataPoint> dataPoints = hourDataList.stream()
.map(map -> new DataPoint(
(String) map.get("datadt"),
((Number) map.get("avgdata")).doubleValue()
))
.sorted(Comparator.comparing(DataPoint::getDateTime))
.collect(Collectors.toList());
// 新增:检查是否有数据点超过阈值(farbeyondValue或limitingValue)
if (dataPoints.isEmpty()) {
logger.info("设备{}没有小时数据,跳过分析", equipment.getEncode());
return;
}
boolean hasExceeded = dataPoints.stream()
.anyMatch(dp -> dp.value >= farBeyondValue || dp.value >= limitingValue);
if (!hasExceeded) {
logger.info("设备{}的小时数据未超过阈值,跳过分析", equipment.getEncode());
return;
}
// 2. 寻找所有符合条件的连续时间段
List<TimePeriod> validPeriods = findValidPeriods(dataPoints, farBeyondValue, limitingValue);
// ... 后续不变
}
private void handleActiveAlarm(UleabnormalrecordsEntity activeAlarm,
List<Map> hourDataList,
double farBeyondValue,
double limitingValue) {
// 1. 将Date转换为LocalDateTime
LocalDateTime alarmStartTime = toLocalDateTime(activeAlarm.getAbnormaltime());
// 2. 准备时间序列数据(从报警开始时间起)
List<DataPoint> dataPoints = hourDataList.stream()
.map(map -> new DataPoint(
(String) map.get("datadt"),
((Number) map.get("avgdata")).doubleValue()
))
.filter(point -> !point.dateTime.isBefore(alarmStartTime))
.sorted(Comparator.comparing(DataPoint::getDateTime))
.collect(Collectors.toList());
if (dataPoints.isEmpty()) {
logger.info("报警{}从开始时间{}起没有后续数据,无法判断恢复", activeAlarm.getId(), alarmStartTime);
return;
}
// 新增:检查是否所有数据点都低于两个阈值
boolean allBelow = dataPoints.stream()
.allMatch(dp -> dp.value < farBeyondValue && dp.value < limitingValue);
if (allBelow) {
// 取第一个数据点的时间作为恢复时间(因为报警开始后的第一个小时就低于阈值了)
LocalDateTime recoveryTime = dataPoints.get(0).dateTime;
activeAlarm.setCompletetime(toDate(recoveryTime));
activeAlarm.setAbnormalstate("1");
uleabnormalrecordsService.updateById(activeAlarm);
logger.info("报警{}已恢复,结束时间: {}", activeAlarm.getId(), recoveryTime);
return;
}
// 3. 检查是否恢复
LocalDateTime recoveryTime = checkRecovery(
dataPoints,
alarmStartTime,
farBeyondValue,
limitingValue
);
// ... 后续不变
}
private List<TimePeriod> findValidPeriods(List<DataPoint> dataPoints,
double farBeyondValue,
double limitingValue) {
List<TimePeriod> validPeriods = new ArrayList<>();
if (dataPoints.size() < 24) return validPeriods;
// 滑动窗口检测
int startIdx = 0;
while (startIdx <= dataPoints.size() - 24) {
int endIdx = startIdx + 23; // 24小时窗口
// 检查限制值条件:只要窗口内有一个数据点超过限制值即可
boolean hasLimitingValue = false;
for (int i = startIdx; i <= endIdx; i++) {
if (dataPoints.get(i).value >= limitingValue) {
hasLimitingValue = true;
break;
}
}
if (!hasLimitingValue) {
startIdx++;
continue;
}
// 检查6小时分桶条件
boolean[] bucketValid = new boolean[4]; // 4个6小时区间
int[] bucketDataCount = new int[4]; // 记录每个桶的数据点数量
Arrays.fill(bucketValid, false);
Arrays.fill(bucketDataCount, 0);
for (int i = startIdx; i <= endIdx; i++) {
DataPoint point = dataPoints.get(i);
int hourBucket = (point.dateTime.getHour() / 6) % 4;
bucketDataCount[hourBucket]++;
if (point.value >= farBeyondValue) {
bucketValid[hourBucket] = true;
}
}
// 检查每个桶:必须都有数据且至少有一个数据点超过远高于日常值
boolean allBucketsValid = true;
for (int i = 0; i < 4; i++) {
if (bucketDataCount[i] == 0) {
allBucketsValid = false;
break;
}
if (!bucketValid[i]) {
allBucketsValid = false;
break;
}
}
if (allBucketsValid) {
// 对于当前时间仍在持续的报警,结束时间为null
LocalDateTime endTime = (endIdx == dataPoints.size() - 1) ?
null : dataPoints.get(endIdx).dateTime;
validPeriods.add(new TimePeriod(
dataPoints.get(startIdx).dateTime,
endTime
));
startIdx += 24; // 跳到下一窗口
} else {
startIdx++;
}
}
return validPeriods;
}
// ... 其他方法不变
}
```
### 解释
1. **最小值检查**:在分析新报警和活跃报警时,先检查数据点是否超过阈值(farbeyondValue或limitingValue),避免不必要的详细分析。
2. **数据缺失处理**:在分桶检查时,记录每个桶的数据点数量。如果某个桶没有数据点,则整个24小时窗口不满足条件。
3. **活跃报警的快速恢复**:如果活跃报警从开始时间起的所有数据点都低于两个阈值,则直接认为在第一个数据点时间就恢复了。
4. **报警时间精确**:使用小时数据点的时间(datadt)作为报警的开始和结束时间。
### 注意事项
- 在检查桶的数据时,我们要求每个桶至少有一个数据点。如果实际数据缺失严重(比如某个桶完全没有数据),则跳过该窗口。
- 快速恢复的判断:当活跃报警开始后,如果第一个小时数据点就低于阈值,则立即结束报警。这可能会忽略中间波动,但符合要求(因为要求持续一天以上,如果第一个小时就低于,说明报警在第一个小时就中断了)。
§§相关问题§§:
1. 如何处理时间窗口内数据点不连续的情况?
2. 如何优化滑动窗口算法以减少计算复杂度?
3. 在分布式系统中,如何保证报警分析任务的高可用性和负载均衡?
4. 如何设计报警的去重和合并策略,避免生成过多重复报警?</think>根据您的要求,我将对高频高幅分析任务进行以下优化:
1. 处理数据缺失情况下的6小时分析
2. 优化活跃报警处理逻辑(不操作持续报警,仅更新结束报警)
3. 添加最小值阈值检查优化性能
4. 精确报警开始/结束时间为实际发生的小时时间
以下是完整的优化实现:
```java
package jnpf.task;
import jnpf.entity.EquipmentparameterEntity;
import jnpf.entity.UleabnormalrecordsEntity;
import jnpf.myutils.DateUtils;
import jnpf.service.EquipmentparameterService;
import jnpf.service.RealDataService;
import jnpf.service.UleabnormalrecordsService;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Component
public class HAHFAnalysisTask {
private static final Logger logger = LoggerFactory.getLogger(HAHFAnalysisTask.class);
@Autowired
private EquipmentparameterService equipmentparameterService;
@Autowired
private UleabnormalrecordsService uleabnormalrecordsService;
@Autowired
private RealDataService realDataService;
//@XxlJob("hahfAnalysis")
public void hahfAnalysis() {
logger.info("HAHF分析任务启动");
// 1. 获取TSP设备列表
List<EquipmentparameterEntity> equipmentList = equipmentparameterService.geHAHFList();
if (equipmentList.isEmpty()) {
logger.info("没有可分析的TSP设备");
return;
}
// 2. 准备查询参数
Map<String, Object> params = new HashMap<>();
List<String> tags = equipmentList.stream()
.map(EquipmentparameterEntity::getTag)
.collect(Collectors.toList());
params.put("tags", tags);
// 3. 获取日常值数据
params.put("begindate", DateUtils.dataTimeformat(new Date()));
List<Map> dailyValueMaps = realDataService.selectLastmonthDailyvalue(params);
Map<String, Map> dailyValueMap = dailyValueMaps.stream()
.collect(Collectors.toMap(
m -> (String) m.get("datag"),
m -> m
));
// 4. 获取小时数据(前一天到当前)
params.put("begindate", timeCalculation());
params.put("enddate", DateUtils.dataTimeformat(new Date()));
List<Map> hourDataMaps = realDataService.selectHourdataByTagIdsAndDate(params);
// 5. 按tag分组小时数据
Map<String, List<Map>> hourDataByTag = hourDataMaps.stream()
.collect(Collectors.groupingBy(
m -> (String) m.get("datag")
));
// 6. 获取当前报警中的记录
List<UleabnormalrecordsEntity> activeAlarms = uleabnormalrecordsService.getOnhahfList();
Map<String, UleabnormalrecordsEntity> activeAlarmMap = activeAlarms.stream()
.collect(Collectors.toMap(
UleabnormalrecordsEntity::getParacode,
alarm -> alarm
));
// 7. 处理每个设备
for (EquipmentparameterEntity equipment : equipmentList) {
try {
processEquipment(
equipment,
dailyValueMap,
hourDataByTag,
activeAlarmMap
);
} catch (Exception e) {
logger.error("处理设备{}失败: {}", equipment.getEncode(), e.getMessage());
}
}
logger.info("HAHF分析任务完成");
}
private void processEquipment(EquipmentparameterEntity equipment,
Map<String, Map> dailyValueMap,
Map<String, List<Map>> hourDataByTag,
Map<String, UleabnormalrecordsEntity> activeAlarmMap) {
String tag = equipment.getTag();
String paraCode = equipment.getEncode();
// 获取该tag的日常值配置
Map dailyConfig = dailyValueMap.get(tag);
if (dailyConfig == null) {
logger.warn("设备{}缺少日常值配置", paraCode);
return;
}
double farBeyondValue = ((Number) dailyConfig.get("farbeyondvalue")).doubleValue();
double limitingValue = ((Number) dailyConfig.get("limitingvalue")).doubleValue();
// 获取该tag的小时数据
List<Map> hourDataList = hourDataByTag.getOrDefault(tag, new ArrayList<>());
// 优化点3:先检查最小值是否超过阈值
double minValue = hourDataList.stream()
.mapToDouble(map -> ((Number) map.get("avgdata")).doubleValue())
.min()
.orElse(Double.MAX_VALUE);
if (minValue < farBeyondValue && minValue < limitingValue) {
logger.info("设备{}数据最小值{}未超过阈值,跳过分析", paraCode, minValue);
return;
}
// 处理报警状态
if (activeAlarmMap.containsKey(paraCode)) {
handleActiveAlarm(
activeAlarmMap.get(paraCode),
hourDataList,
farBeyondValue,
limitingValue
);
} else {
handleNewAlarm(
equipment,
hourDataList,
farBeyondValue,
limitingValue
);
}
}
/**
* 处理新报警检测
*/
private void handleNewAlarm(EquipmentparameterEntity equipment,
List<Map> hourDataList,
double farBeyondValue,
double limitingValue) {
// 1. 准备时间序列数据
List<DataPoint> dataPoints = hourDataList.stream()
.map(map -> new DataPoint(
(String) map.get("datadt"),
((Number) map.get("avgdata")).doubleValue()
))
.sorted(Comparator.comparing(DataPoint::getDateTime))
.collect(Collectors.toList());
// 2. 寻找所有符合条件的连续时间段
List<TimePeriod> validPeriods = findValidPeriods(dataPoints, farBeyondValue, limitingValue);
// 3. 合并重叠时间段
List<TimePeriod> mergedPeriods = mergePeriods(validPeriods);
// 4. 创建报警记录
for (TimePeriod period : mergedPeriods) {
// 优化点4:使用实际发生的小时时间作为报警时间
LocalDateTime actualStart = findActualStartTime(dataPoints, period.start, farBeyondValue, limitingValue);
LocalDateTime actualEnd = period.end != null ?
findActualEndTime(dataPoints, period.end, farBeyondValue, limitingValue) : null;
UleabnormalrecordsEntity record = new UleabnormalrecordsEntity();
record.setParacode(equipment.getEncode());
record.setAbnormaltime(toDate(actualStart)); // 设置实际开始时间
record.setCompletetime(toDate(actualEnd)); // 设置实际结束时间
// 设置其他必要字段
record.setAbnormaltype("HAHF"); // 高频高幅报警
record.setAbnormalstate(actualEnd == null ? "0" : "1"); // 0-报警中, 1-已结束
uleabnormalrecordsService.save(record);
logger.info("创建新报警记录: {} - {} 至 {}",
equipment.getEncode(),
actualStart,
actualEnd);
}
}
/**
* 处理活跃报警的恢复检测
*/
private void handleActiveAlarm(UleabnormalrecordsEntity activeAlarm,
List<Map> hourDataList,
double farBeyondValue,
double limitingValue) {
// 1. 将Date转换为LocalDateTime
LocalDateTime alarmStartTime = toLocalDateTime(activeAlarm.getAbnormaltime());
// 2. 准备时间序列数据(从报警开始时间起)
List<DataPoint> dataPoints = hourDataList.stream()
.map(map -> new DataPoint(
(String) map.get("datadt"),
((Number) map.get("avgdata")).doubleValue()
))
.filter(point -> !point.dateTime.isBefore(alarmStartTime))
.sorted(Comparator.comparing(DataPoint::getDateTime))
.collect(Collectors.toList());
// 3. 检查是否恢复
LocalDateTime recoveryTime = checkRecovery(
dataPoints,
alarmStartTime,
farBeyondValue,
limitingValue
);
// 优化点2:仅当报警结束才更新记录
if (recoveryTime != null) {
// 优化点4:使用实际恢复的小时时间
LocalDateTime actualRecoveryTime = findActualEndTime(dataPoints, recoveryTime, farBeyondValue, limitingValue);
activeAlarm.setCompletetime(toDate(actualRecoveryTime));
activeAlarm.setAbnormalstate("1"); // 设置为已结束
uleabnormalrecordsService.updateById(activeAlarm);
logger.info("报警{}已恢复,结束时间: {}",
activeAlarm.getId(), actualRecoveryTime);
} else {
logger.info("报警{}仍在持续中", activeAlarm.getId());
}
}
/**
* 核心算法:寻找有效时间段(处理数据缺失)
*/
private List<TimePeriod> findValidPeriods(List<DataPoint> dataPoints,
double farBeyondValue,
double limitingValue) {
List<TimePeriod> validPeriods = new ArrayList<>();
if (dataPoints.size() < 24) return validPeriods;
// 按日期分组数据(处理数据缺失)
Map<LocalDate, List<DataPoint>> dataByDate = dataPoints.stream()
.collect(Collectors.groupingBy(
point -> point.dateTime.toLocalDate()
));
List<LocalDate> sortedDates = new ArrayList<>(dataByDate.keySet());
Collections.sort(sortedDates);
// 使用滑动窗口检测连续日期
int startDateIdx = 0;
while (startDateIdx <= sortedDates.size() - 1) {
LocalDate startDate = sortedDates.get(startDateIdx);
LocalDate endDate = startDate.plusDays(1);
// 获取24小时时间段内的数据
List<DataPoint> windowData = new ArrayList<>();
for (int i = startDateIdx; i < sortedDates.size(); i++) {
LocalDate date = sortedDates.get(i);
if (date.isAfter(endDate)) break;
windowData.addAll(dataByDate.get(date));
}
// 按时间排序窗口数据
windowData.sort(Comparator.comparing(DataPoint::getDateTime));
// 优化点1:处理数据缺失的分桶分析
if (isValidPeriod(windowData, farBeyondValue, limitingValue)) {
LocalDateTime periodStart = findActualStartTime(windowData, windowData.get(0).dateTime, farBeyondValue, limitingValue);
LocalDateTime periodEnd = windowData.get(windowData.size() - 1).dateTime;
// 对于当前时间仍在持续的报警,结束时间为null
LocalDateTime lastPossibleTime = LocalDateTime.now().minusHours(1);
LocalDateTime actualEnd = periodEnd.isBefore(lastPossibleTime) ? periodEnd : null;
validPeriods.add(new TimePeriod(periodStart, actualEnd));
startDateIdx += 1; // 移动到下一天
} else {
startDateIdx++;
}
}
return validPeriods;
}
/**
* 检查24小时窗口是否有效(处理数据缺失)
*/
private boolean isValidPeriod(List<DataPoint> dataPoints,
double farBeyondValue,
double limitingValue) {
if (dataPoints.isEmpty()) return false;
// 检查限制值条件
boolean hasLimitingValue = dataPoints.stream()
.anyMatch(point -> point.value >= limitingValue);
if (!hasLimitingValue) return false;
// 优化点1:处理数据缺失的分桶分析
// 按6小时分桶组织数据
Map<Integer, List<DataPoint>> bucketMap = new HashMap<>();
for (DataPoint point : dataPoints) {
int bucketIdx = (point.dateTime.getHour() / 6) % 4;
bucketMap.computeIfAbsent(bucketIdx, k -> new ArrayList<>()).add(point);
}
// 检查每个桶是否满足条件(允许数据缺失)
for (int bucketIdx = 0; bucketIdx < 4; bucketIdx++) {
List<DataPoint> bucketData = bucketMap.getOrDefault(bucketIdx, new ArrayList<>());
// 如果桶内有数据,检查是否有超过远高于日常值的点
if (!bucketData.isEmpty()) {
boolean hasValidPoint = bucketData.stream()
.anyMatch(point -> point.value >= farBeyondValue);
if (!hasValidPoint) {
return false;
}
} else {
// 优化点1:如果桶内没有数据,视为条件不满足
return false;
}
}
return true;
}
/**
* 合并重叠时间段
*/
private List<TimePeriod> mergePeriods(List<TimePeriod> periods) {
if (periods.isEmpty()) return periods;
List<TimePeriod> merged = new ArrayList<>();
periods.sort(Comparator.comparing(p -> p.start));
TimePeriod current = periods.get(0);
for (int i = 1; i < periods.size(); i++) {
TimePeriod next = periods.get(i);
// 检查时间段是否重叠或连续
boolean shouldMerge = false;
if (current.end != null && next.start != null) {
shouldMerge = !next.start.isAfter(current.end.plusHours(1));
} else if (current.end == null) {
// 当前时间段仍在持续,合并所有后续时间段
shouldMerge = true;
}
if (shouldMerge) {
// 合并时间段
if (next.end == null) {
current.end = null; // 合并后仍在持续
} else if (current.end == null || next.end.isAfter(current.end)) {
current.end = next.end;
}
} else {
merged.add(current);
current = next;
}
}
merged.add(current);
return merged;
}
/**
* 检查报警恢复(处理数据缺失)
*/
private LocalDateTime checkRecovery(List<DataPoint> dataPoints,
LocalDateTime startTime,
double farBeyondValue,
double limitingValue) {
if (dataPoints.isEmpty()) return null;
// 按6小时分桶组织数据
Map<Integer, List<DataPoint>> bucketMap = new HashMap<>();
for (DataPoint point : dataPoints) {
int bucketIdx = (point.dateTime.getHour() / 6) % 4;
bucketMap.computeIfAbsent(bucketIdx, k -> new ArrayList<>()).add(point);
}
// 检查每个桶是否仍有有效数据点
for (int bucketIdx = 0; bucketIdx < 4; bucketIdx++) {
List<DataPoint> bucketData = bucketMap.getOrDefault(bucketIdx, new ArrayList<>());
// 优化点1:处理数据缺失 - 如果桶内没有数据,视为未恢复
if (bucketData.isEmpty()) {
return null;
}
// 检查桶内是否有有效点
boolean hasValidPoint = bucketData.stream()
.anyMatch(point -> point.value >= farBeyondValue);
// 如果某个桶没有有效点,取该桶最早时间作为恢复时间候选
if (!hasValidPoint) {
// 找到该桶中最后一个数据点的时间
LocalDateTime bucketEnd = bucketData.get(bucketData.size() - 1).dateTime;
// 验证在恢复时间后是否还有有效点
boolean laterValid = dataPoints.stream()
.filter(point -> point.dateTime.isAfter(bucketEnd))
.anyMatch(point -> point.value >= farBeyondValue);
if (!laterValid) {
return bucketEnd;
}
}
}
return null;
}
/**
* 优化点4:找到实际报警开始时间(第一个超过阈值的小时)
*/
private LocalDateTime findActualStartTime(List<DataPoint> dataPoints,
LocalDateTime windowStart,
double farBeyondValue,
double limitingValue) {
return dataPoints.stream()
.filter(point ->
!point.dateTime.isBefore(windowStart) &&
(point.value >= farBeyondValue || point.value >= limitingValue))
.min(Comparator.comparing(DataPoint::getDateTime))
.map(point -> point.dateTime)
.orElse(windowStart);
}
/**
* 优化点4:找到实际报警结束时间(最后一个超过阈值的小时)
*/
private LocalDateTime findActualEndTime(List<DataPoint> dataPoints,
LocalDateTime windowEnd,
double farBeyondValue,
double limitingValue) {
return dataPoints.stream()
.filter(point ->
!point.dateTime.isAfter(windowEnd) &&
(point.value >= farBeyondValue || point.value >= limitingValue))
.max(Comparator.comparing(DataPoint::getDateTime))
.map(point -> point.dateTime)
.orElse(windowEnd);
}
/**
* 辅助类:数据点
*/
private static class DataPoint {
LocalDateTime dateTime;
double value;
DataPoint(String dateStr, double value) {
// 处理可能的时间格式差异
if (dateStr.length() == 13) { // "yyyy-MM-dd HH"
this.dateTime = LocalDateTime.parse(dateStr + ":00:00",
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} else {
this.dateTime = LocalDateTime.parse(dateStr,
DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
this.value = value;
}
LocalDateTime getDateTime() {
return dateTime;
}
}
/**
* 辅助类:时间段
*/
private static class TimePeriod {
LocalDateTime start;
LocalDateTime end; // null表示仍在持续
TimePeriod(LocalDateTime start, LocalDateTime end) {
this.start = start;
this.end = end;
}
}
/**
* 获取前一天开始时间
*/
private String timeCalculation() {
LocalDate yesterday = LocalDate.now().minusDays(1);
LocalDateTime startOfYesterday = yesterday.atStartOfDay();
return startOfYesterday.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
/**
* LocalDateTime 转 Date
*/
private Date toDate(LocalDateTime localDateTime) {
if (localDateTime == null) return null;
return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
}
/**
* Date 转 LocalDateTime
*/
private LocalDateTime toLocalDateTime(Date date) {
if (date == null) return null;
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
}
}
```
### 优化点详细解释
#### 1. 处理数据缺失情况下的6小时分析
```java
private boolean isValidPeriod(List<DataPoint> dataPoints,
double farBeyondValue,
double limitingValue) {
// 按6小时分桶组织数据
Map<Integer, List<DataPoint>> bucketMap = new HashMap<>();
for (DataPoint point : dataPoints) {
int bucketIdx = (point.dateTime.getHour() / 6) % 4;
bucketMap.computeIfAbsent(bucketIdx, k -> new ArrayList<>()).add(point);
}
// 检查每个桶是否满足条件(允许数据缺失)
for (int bucketIdx = 0; bucketIdx < 4; bucketIdx++) {
List<DataPoint> bucketData = bucketMap.getOrDefault(bucketIdx, new ArrayList<>());
// 如果桶内有数据,检查是否有超过远高于日常值的点
if (!bucketData.isEmpty()) {
boolean hasValidPoint = bucketData.stream()
.anyMatch(point -> point.value >= farBeyondValue);
if (!hasValidPoint) {
return false;
}
} else {
// 优化点1:如果桶内没有数据,视为条件不满足
return false;
}
}
return true;
}
```
- **处理缺失**:按6小时分桶处理数据,如果某个桶内完全没有数据,则视为条件不满足
- **部分数据**:只要桶内有数据,就检查是否有超过阈值的点
- **时间窗口**:使用日期分组处理不连续的时间数据
#### 2. 活跃报警处理优化
```java
private void handleActiveAlarm(UleabnormalrecordsEntity activeAlarm,
List<Map> hourDataList,
double farBeyondValue,
double limitingValue) {
// ... [准备数据]
// 检查是否恢复
LocalDateTime recoveryTime = checkRecovery(
dataPoints,
alarmStartTime,
farBeyondValue,
limitingValue
);
// 优化点2:仅当报警结束才更新记录
if (recoveryTime != null) {
// 更新结束时间
activeAlarm.setCompletetime(toDate(actualRecoveryTime));
activeAlarm.setAbnormalstate("1"); // 设置为已结束
uleabnormalrecordsService.updateById(activeAlarm);
} else {
logger.info("报警{}仍在持续中", activeAlarm.getId());
}
}
```
- **不操作持续报警**:如果报警仍在持续,只记录日志不更新数据库
- **仅更新结束报警**:只有当报警结束时才更新记录状态和结束时间
- **减少数据库操作**:避免不必要的更新操作
#### 3. 最小值阈值检查优化性能
```java
private void processEquipment(EquipmentparameterEntity equipment,
Map<String, Map> dailyValueMap,
Map<String, List<Map>> hourDataByTag,
Map<String, UleabnormalrecordsEntity> activeAlarmMap) {
// ... [获取配置]
// 优化点3:先检查最小值是否超过阈值
double minValue = hourDataList.stream()
.mapToDouble(map -> ((Number) map.get("avgdata")).doubleValue())
.min()
.orElse(Double.MAX_VALUE);
if (minValue < farBeyondValue && minValue < limitingValue) {
logger.info("设备{}数据最小值{}未超过阈值,跳过分析", paraCode, minValue);
return;
}
// ... [继续处理]
}
```
- **性能优化**:在详细分析前先检查最小值
- **跳过无效分析**:如果最小值都低于阈值,直接跳过该设备
- **减少计算量**:避免不必要的复杂分析
#### 4. 精确报警时间
```java
/**
* 优化点4:找到实际报警开始时间(第一个超过阈值的小时)
*/
private LocalDateTime findActualStartTime(List<DataPoint> dataPoints,
LocalDateTime windowStart,
double farBeyondValue,
double limitingValue) {
return dataPoints.stream()
.filter(point ->
!point.dateTime.isBefore(windowStart) &&
(point.value >= farBeyondValue || point.value >= limitingValue))
.min(Comparator.comparing(DataPoint::getDateTime))
.map(point -> point.dateTime)
.orElse(windowStart);
}
/**
* 优化点4:找到实际报警结束时间(最后一个超过阈值的小时)
*/
private LocalDateTime findActualEndTime(List<DataPoint> dataPoints,
LocalDateTime windowEnd,
double farBeyondValue,
double limitingValue) {
return dataPoints.stream()
.filter(point ->
!point.dateTime.isAfter(windowEnd) &&
(point.value >= farBeyondValue || point.value >= limitingValue))
.max(Comparator.comparing(DataPoint::getDateTime))
.map(point -> point.dateTime)
.orElse(windowEnd);
}
```
- **精确时间**:找到实际触发报警的第一个小时
- **结束时间**:找到报警条件不再满足的最后一个小时
- **避免窗口边界误差**:使用实际数据点时间而非分析窗口边界
### 注意事项
1. **数据缺失处理**:
- 使用日期分组处理不连续数据
- 分桶分析时,空桶视为条件不满足
- 报警恢复检测时,空桶视为未恢复
2. **报警状态管理**:
- 新报警:创建记录并设置状态为"0"(报警中)
- 持续报警:不更新数据库,仅记录日志
- 结束报警:更新结束时间和状态为"1"(已结束)
3. **性能优化**:
- 最小值检查快速跳过未超标设备
- 减少不必要的复杂计算
- 按日期分组减少滑动窗口次数
4. **时间精确性**:
- 使用`findActualStartTime`和`findActualEndTime`确保报警时间精确
- 处理不同时间格式的数据点
- 使用实际数据点而非分析窗口边界