fix: 饱和度浓度超过200,连续8小时,报警新增。
This commit is contained in:
@@ -89,6 +89,18 @@ public class AliyunIotProperties {
|
|||||||
*/
|
*/
|
||||||
private int offlineWarnDelayMinutes = 6;
|
private int offlineWarnDelayMinutes = 6;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 夜间溶解氧饱和度告警阈值(%),超过此值时开始计时
|
||||||
|
* 对应 C# DeviceSaturabilityWarnValue = 200
|
||||||
|
*/
|
||||||
|
private double saturabilityWarnValue = 200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 夜间溶解氧饱和度持续超阈值后才触发告警的小时数
|
||||||
|
* 对应 C# DeviceSaturabilityWarnHour = 8
|
||||||
|
*/
|
||||||
|
private int saturabilityWarnHours = 8;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class VmsMnsConfig {
|
public static class VmsMnsConfig {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ public class DeviceDataHandler {
|
|||||||
private static final String ALARM_TYPE_TEMPERATURE_HIGH = "温度过高";
|
private static final String ALARM_TYPE_TEMPERATURE_HIGH = "温度过高";
|
||||||
private static final String ALARM_TYPE_TEMPERATURE_LOW = "温度过低";
|
private static final String ALARM_TYPE_TEMPERATURE_LOW = "温度过低";
|
||||||
private static final String ALARM_TYPE_BATTERY = "电池电量";
|
private static final String ALARM_TYPE_BATTERY = "电池电量";
|
||||||
|
private static final String ALARM_TYPE_SATURA_HIGH = "饱和度过高";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,6 +144,12 @@ public class DeviceDataHandler {
|
|||||||
*/
|
*/
|
||||||
private static final String DEVICE_OFFLINE_WAIT_KEY_PREFIX = "device:offline:wait:";
|
private static final String DEVICE_OFFLINE_WAIT_KEY_PREFIX = "device:offline:wait:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 夜间饱和度持续超阈等待告警的 Redis key 前缀
|
||||||
|
* value = 首次超阈时间(yyyy-MM-dd HH:mm:ss)
|
||||||
|
*/
|
||||||
|
private static final String DEVICE_SATURA_WAIT_KEY_PREFIX = "device:satura:wait:";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 离线等待 key 的 Redis TTL 倍数(相对于延迟触发时间)
|
* 离线等待 key 的 Redis TTL 倍数(相对于延迟触发时间)
|
||||||
*/
|
*/
|
||||||
@@ -441,6 +448,34 @@ public class DeviceDataHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 检查夜间溶解氧饱和度(对应 C# SaturaHigh 逻辑)
|
||||||
|
// 夜间(20:00-08:00)且饱和度 >= 阈值时开始计时,持续超过配置小时数就触发告警
|
||||||
|
// 饱和度恢复正常(或日间)则清除等待标记
|
||||||
|
if (sensorData.getSaturability() != null && device.getPondId() != null) {
|
||||||
|
Double saturability = sensorData.getSaturability();
|
||||||
|
double saturabilityWarnValue = aliyunIotProperties.getSaturabilityWarnValue();
|
||||||
|
int nowHour = java.time.LocalTime.now().getHour();
|
||||||
|
boolean isNight = (nowHour >= 20 || nowHour < 8);
|
||||||
|
String saturaKey = DEVICE_SATURA_WAIT_KEY_PREFIX + deviceName;
|
||||||
|
|
||||||
|
if (saturability >= saturabilityWarnValue && isNight) {
|
||||||
|
// 夜间且超阈:已有等待标记则不重复写入(计时从首次超阈算起)
|
||||||
|
if (!Boolean.TRUE.equals(RedisUtils.hasKey(saturaKey))) {
|
||||||
|
String firstExceedTime = LocalDateTime.now().format(DATETIME_FORMATTER);
|
||||||
|
// TTL = 告警小时数 * 3,足够长以应对跨夜场景
|
||||||
|
int saturaTtlHours = aliyunIotProperties.getSaturabilityWarnHours() * 3;
|
||||||
|
RedisUtils.setCacheObject(saturaKey, firstExceedTime, Duration.ofHours(saturaTtlHours));
|
||||||
|
log.info("[饱和度告警] 设备夜间饱和度超过{}%,开始计时: {}", saturabilityWarnValue, deviceName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 饱和度恢复正常或平时段:清除等待标记
|
||||||
|
if (Boolean.TRUE.equals(RedisUtils.hasKey(saturaKey))) {
|
||||||
|
RedisUtils.deleteObject(saturaKey);
|
||||||
|
log.info("[饱和度告警] 设备饱和度恢复正常,取消等待标记: {}", deviceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 如果有告警,处理报警消息记录
|
// 如果有告警,处理报警消息记录
|
||||||
if (hasAlarm) {
|
if (hasAlarm) {
|
||||||
log.warn("[告警] {} - {}", deviceName, alarmMessage);
|
log.warn("[告警] {} - {}", deviceName, alarmMessage);
|
||||||
@@ -932,6 +967,131 @@ public class DeviceDataHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 真正触发喑夜饱和度持续超阈告警(由定时任务调用)
|
||||||
|
* 对应 C# CheckDeviceMessageWarn(warnCode=SaturaHigh) 逻辑:
|
||||||
|
* title="饱和度过高",强制触发电话通知(无免打扰开关限制)
|
||||||
|
*
|
||||||
|
* @param deviceName 设备名称(serialNum)
|
||||||
|
*/
|
||||||
|
public void triggerSaturaHighAlarm(String deviceName) {
|
||||||
|
try {
|
||||||
|
Device device = deviceMapper.selectOne(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||||
|
.eq(Device::getSerialNum, deviceName)
|
||||||
|
.last("LIMIT 1")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (device == null) {
|
||||||
|
log.debug("[饱和度告警] 设备不存在,跳过: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long deviceId = device.getId();
|
||||||
|
Long userId = device.getUserId();
|
||||||
|
|
||||||
|
if (userId == null) {
|
||||||
|
log.debug("[饱和度告警] 设备未绑定用户,跳过: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询塗口名称
|
||||||
|
if (device.getPondId() == null) {
|
||||||
|
log.debug("[饱和度告警] 设备未绑定塗口,跳过: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Pond pond = pondMapper.selectById(device.getPondId());
|
||||||
|
if (pond == null) {
|
||||||
|
log.debug("[饱和度告警] 塗口不存在,跳过: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String pondName = pond.getPondName();
|
||||||
|
String displayDeviceName = device.getDeviceName() != null ? device.getDeviceName() : deviceName;
|
||||||
|
|
||||||
|
// 双重抑制:1) 活跃批次检查; 2) 成功通知抑制
|
||||||
|
int maxBatchMinutesSat = CALL_NOTICE_RETRY_COUNT * CALL_NOTICE_MINUTE_INTERVAL * 2;
|
||||||
|
LocalDateTime activeBatchStartSat = LocalDateTime.now().minusMinutes(maxBatchMinutesSat);
|
||||||
|
long activeCountSat = warnCallNoticeMapper.countActiveByDeviceAndTitleAfter(deviceId, ALARM_TYPE_SATURA_HIGH, activeBatchStartSat);
|
||||||
|
if (activeCountSat > 0) {
|
||||||
|
log.debug("[饱和度告警] 当前有活跃通知批次在进行,跳过: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LocalDateTime suppressTimeSat = LocalDateTime.now().minusHours(aliyunIotProperties.getCallSuccessSuppressHours());
|
||||||
|
long recentCountSat = warnCallNoticeMapper.countSuccessByDeviceAndTitleAfter(deviceId, ALARM_TYPE_SATURA_HIGH, suppressTimeSat);
|
||||||
|
if (recentCountSat > 0) {
|
||||||
|
log.debug("[饱和度告警] {}h内已有回执成功的通知,跳过: {}", aliyunIotProperties.getCallSuccessSuppressHours(), deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取告警手机号
|
||||||
|
AquUser aquUser = aquUserMapper.selectById(userId);
|
||||||
|
if (aquUser == null) {
|
||||||
|
log.warn("[饱和度告警] 用户不存在,跳过:{}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
java.util.List<String> phoneList = parseWarnPhoneList(aquUser.getWarnPhoneJson());
|
||||||
|
if (phoneList == null || phoneList.isEmpty()) {
|
||||||
|
log.warn("[饱和度告警] 用户未配置告警手机号,跳过:{}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建告警消息(内容与 C# 一致)
|
||||||
|
String warnMsg = String.format("%s塗口中%s(%s)夜间饱和度连续%d小时超过%.0f%%,请检查荧光膜是否有损坏或联系售后。",
|
||||||
|
pondName, displayDeviceName, deviceName,
|
||||||
|
aliyunIotProperties.getSaturabilityWarnHours(),
|
||||||
|
aliyunIotProperties.getSaturabilityWarnValue());
|
||||||
|
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, ALARM_TYPE_SATURA_HIGH, WARN_TYPE_DISSOLVED_OXYGEN, warnMsg);
|
||||||
|
messageWarnMapper.insert(warn);
|
||||||
|
|
||||||
|
// 创建通知记录(每个手机号 × 重试次数)
|
||||||
|
LocalDateTime baseTime = LocalDateTime.now();
|
||||||
|
int index = 0;
|
||||||
|
AquWarnCallNotice firstCallNotice = null;
|
||||||
|
|
||||||
|
for (int retry = 0; retry < CALL_NOTICE_RETRY_COUNT; retry++) {
|
||||||
|
for (String phoneNum : phoneList) {
|
||||||
|
if (StrUtil.isBlank(phoneNum) || phoneNum.length() < 11) {
|
||||||
|
log.warn("[饱和度告警] 无效的手机号:{}", phoneNum);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime callTime = baseTime.plusMinutes((long) CALL_NOTICE_MINUTE_INTERVAL * index);
|
||||||
|
index++;
|
||||||
|
|
||||||
|
AquWarnCallNotice callNotice = new AquWarnCallNotice();
|
||||||
|
callNotice.setUserId(userId);
|
||||||
|
callNotice.setMobilePhone(phoneNum);
|
||||||
|
callNotice.setDeviceId(deviceId);
|
||||||
|
callNotice.setPondName(pondName);
|
||||||
|
callNotice.setCallTime(callTime);
|
||||||
|
callNotice.setCallStatus(CALL_STATUS_NO_CALL);
|
||||||
|
warnCallNoticeMapper.insert(callNotice);
|
||||||
|
|
||||||
|
if (warn.getId() != null) {
|
||||||
|
AquMapMessageWarnCallNotice mapRecord = new AquMapMessageWarnCallNotice();
|
||||||
|
mapRecord.setMessageWarnId(warn.getId());
|
||||||
|
mapRecord.setCallNoticeId(callNotice.getId());
|
||||||
|
mapMessageWarnCallNoticeMapper.insert(mapRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstCallNotice == null) {
|
||||||
|
firstCallNotice = callNotice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即尝试发送第一条通知
|
||||||
|
if (firstCallNotice != null) {
|
||||||
|
sendVoiceNotification(firstCallNotice, displayDeviceName, ALARM_TYPE_SATURA_HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[饱和度告警] 已触发通知 - 设备:{}, 塗口:{}, 手机号数:{}", deviceName, pondName, phoneList.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[饱和度告警] 处理失败: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 触发设备故障告警
|
* 触发设备故障告警
|
||||||
* 针对设备事件(error)的紧急告警
|
* 针对设备事件(error)的紧急告警
|
||||||
|
|||||||
Reference in New Issue
Block a user