fix: 溶解氧探头离线告警修复。
This commit is contained in:
@@ -98,7 +98,8 @@ public class DefineDeviceWarnCode {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换告警码为告警描述(与 C# ToWarnDescription 保持一致)
|
* 转换告警码为告警描述(与 C# ToWarnDescription 保持一致)
|
||||||
* 优先级:设备过期 > 设备离线 > 设备断电 > 溶解氧离线 > 溶解氧告警 > 温度告警 > 饱和度过高
|
* 优先级:设备过期 > 设备离线 > 设备休眠 > 设备断电 > 溶解氧离线 > 溶解氧告警 > 温度告警 > 饱和度过高
|
||||||
|
* 注:PH/盐度/浊度/水位/氨氮探头离线不需要前端展示,故不在此处理
|
||||||
*/
|
*/
|
||||||
public static String toWarnDescription(int warnCode) {
|
public static String toWarnDescription(int warnCode) {
|
||||||
if (warnCode == None) {
|
if (warnCode == None) {
|
||||||
|
|||||||
@@ -306,7 +306,8 @@ public class PondController extends BaseController {
|
|||||||
PondMode1WarnCodeInfo detectorWarnCodeInfo = new PondMode1WarnCodeInfo();
|
PondMode1WarnCodeInfo detectorWarnCodeInfo = new PondMode1WarnCodeInfo();
|
||||||
if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) {
|
if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) {
|
||||||
detectorWarnCodeInfo.setWarnCode(device.getWarnCode());
|
detectorWarnCodeInfo.setWarnCode(device.getWarnCode());
|
||||||
detectorWarnCodeInfo.setWarnDescription(DefineDeviceWarnCode.toWarnDescription(device.getWarnCode()));
|
String desc = DefineDeviceWarnCode.toWarnDescription(device.getWarnCode());
|
||||||
|
detectorWarnCodeInfo.setWarnDescription(desc != null ? desc : "");
|
||||||
} else {
|
} else {
|
||||||
detectorWarnCodeInfo.setWarnCode(DefineDeviceWarnCode.None);
|
detectorWarnCodeInfo.setWarnCode(DefineDeviceWarnCode.None);
|
||||||
detectorWarnCodeInfo.setWarnDescription("");
|
detectorWarnCodeInfo.setWarnDescription("");
|
||||||
@@ -357,7 +358,8 @@ public class PondController extends BaseController {
|
|||||||
PondMode1WarnCodeInfo warnCodeInfo = new PondMode1WarnCodeInfo();
|
PondMode1WarnCodeInfo warnCodeInfo = new PondMode1WarnCodeInfo();
|
||||||
if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) {
|
if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) {
|
||||||
warnCodeInfo.setWarnCode(device.getWarnCode());
|
warnCodeInfo.setWarnCode(device.getWarnCode());
|
||||||
warnCodeInfo.setWarnDescription(DefineDeviceWarnCode.toWarnDescription(device.getWarnCode()));
|
String desc = DefineDeviceWarnCode.toWarnDescription(device.getWarnCode());
|
||||||
|
warnCodeInfo.setWarnDescription(desc != null ? desc : "");
|
||||||
} else {
|
} else {
|
||||||
warnCodeInfo.setWarnCode(DefineDeviceWarnCode.None);
|
warnCodeInfo.setWarnCode(DefineDeviceWarnCode.None);
|
||||||
warnCodeInfo.setWarnDescription("");
|
warnCodeInfo.setWarnDescription("");
|
||||||
@@ -923,7 +925,8 @@ public class PondController extends BaseController {
|
|||||||
data.setDeviceCount(hashSetDeviceId.size());
|
data.setDeviceCount(hashSetDeviceId.size());
|
||||||
data.setSwitchCount(pondSwitches.size());
|
data.setSwitchCount(pondSwitches.size());
|
||||||
data.getWarnCodeInfo().setWarnCode(warnCode);
|
data.getWarnCodeInfo().setWarnCode(warnCode);
|
||||||
data.getWarnCodeInfo().setWarnDescription(DefineDeviceWarnCode.toWarnDescription(warnCode));
|
String warnDesc = DefineDeviceWarnCode.toWarnDescription(warnCode);
|
||||||
|
data.getWarnCodeInfo().setWarnDescription(warnDesc != null ? warnDesc : "");
|
||||||
|
|
||||||
listData.add(data);
|
listData.add(data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ public class DeviceDataHandler {
|
|||||||
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 = "饱和度过高";
|
private static final String ALARM_TYPE_SATURA_HIGH = "饱和度过高";
|
||||||
|
private static final String ALARM_TYPE_DETECTOR_OXY_OFFLINE = "溶解氧离线";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -175,6 +176,12 @@ public class DeviceDataHandler {
|
|||||||
*/
|
*/
|
||||||
private static final String DEVICE_SATURA_WAIT_KEY_PREFIX = "device:satura:wait:";
|
private static final String DEVICE_SATURA_WAIT_KEY_PREFIX = "device:satura:wait:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 溶解氧探头离线等待告警的 Redis key 前缀
|
||||||
|
* value = 离线时间(yyyy-MM-dd HH:mm:ss)
|
||||||
|
*/
|
||||||
|
public static final String DETECTOR_OXY_OFFLINE_WAIT_KEY_PREFIX = "device:detector:oxy:offline:wait:";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 离线等待 key 的 Redis TTL 倍数(相对于延迟触发时间)
|
* 离线等待 key 的 Redis TTL 倍数(相对于延迟触发时间)
|
||||||
*/
|
*/
|
||||||
@@ -1082,6 +1089,146 @@ public class DeviceDataHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 触发溶解氧探头离线告警(由定时任务扫描 Redis 待告警标记后调用)
|
||||||
|
* 参考 C# DetectorOxyOffline 的告警逻辑:查设备→查塘口→写告警消息→创建电话通知记录→立即发送
|
||||||
|
*
|
||||||
|
* @param deviceName 设备名称(serialNum)
|
||||||
|
*/
|
||||||
|
public void triggerDetectorOxyOfflineAlarm(String deviceName) {
|
||||||
|
// 全局电话通知开关检查
|
||||||
|
if (!aliyunIotProperties.isCallNoticeEnabled()) {
|
||||||
|
log.debug("[探头离线告警] 电话通知已全局关闭,跳过: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设备已过期,不触发告警
|
||||||
|
if (device.getDeadTime() != null && device.getDeadTime().before(new java.util.Date())) {
|
||||||
|
log.debug("[探头离线告警] 设备已过期,跳过: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认 warnCode 仍包含 DetectorOxyOffline 位(可能在等待期间已恢复)
|
||||||
|
int warnCode = device.getWarnCode() != null ? device.getWarnCode() : 0;
|
||||||
|
if ((warnCode & DefineDeviceWarnCode.DetectorOxyOffline) == 0) {
|
||||||
|
log.info("[探头离线告警] 设备已恢复,跳过告警: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long deviceId = device.getId();
|
||||||
|
Long userId = device.getUserId();
|
||||||
|
|
||||||
|
if (userId == null) {
|
||||||
|
log.debug("[探头离线告警] 设备未绑定用户,跳过: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 warnPhoneJson 获取告警手机号列表
|
||||||
|
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("[探头离线告警] 用户未配置告警手机号(warnPhoneJson为空),跳过:{}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 双重抑制:1) 活跃批次检查;2) 成功通知抑制
|
||||||
|
int maxBatchMinutes = CALL_NOTICE_RETRY_COUNT * CALL_NOTICE_MINUTE_INTERVAL * 2;
|
||||||
|
LocalDateTime activeBatchStart = LocalDateTime.now().minusMinutes(maxBatchMinutes);
|
||||||
|
long activeCount = warnCallNoticeMapper.countActiveByDeviceAndTitleAfter(deviceId, ALARM_TYPE_DETECTOR_OXY_OFFLINE, activeBatchStart);
|
||||||
|
if (activeCount > 0) {
|
||||||
|
log.debug("[探头离线告警] 当前有活跃通知批次在进行,跳过: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LocalDateTime suppressTime = LocalDateTime.now().minusHours(aliyunIotProperties.getCallSuccessSuppressHours());
|
||||||
|
long recentCount = warnCallNoticeMapper.countSuccessByDeviceAndTitleAfter(deviceId, ALARM_TYPE_DETECTOR_OXY_OFFLINE, suppressTime);
|
||||||
|
if (recentCount > 0) {
|
||||||
|
log.debug("[探头离线告警] {}h内已有回执成功的通知,跳过: {}", aliyunIotProperties.getCallSuccessSuppressHours(), 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;
|
||||||
|
|
||||||
|
// 告警消息内容
|
||||||
|
String warnMsg = String.format("%s塘口中%s(%s)溶解氧探头离线", pondName, displayDeviceName, deviceName);
|
||||||
|
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, ALARM_TYPE_DETECTOR_OXY_OFFLINE, 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_DETECTOR_OXY_OFFLINE);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[探头离线告警] 已触发通知 - 设备:{}, 塘口:{}, 手机号数:{}", deviceName, pondName, phoneList.size());
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[探头离线告警] 处理失败: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 真正触发喑夜饱和度持续超阈告警(由定时任务调用)
|
* 真正触发喑夜饱和度持续超阈告警(由定时任务调用)
|
||||||
* 对应 C# CheckDeviceMessageWarn(warnCode=SaturaHigh) 逻辑:
|
* 对应 C# CheckDeviceMessageWarn(warnCode=SaturaHigh) 逻辑:
|
||||||
@@ -1887,7 +2034,7 @@ public class DeviceDataHandler {
|
|||||||
// 只有开启了溶氧检测且处于塘口中才创建待告警
|
// 只有开启了溶氧检测且处于塘口中才创建待告警
|
||||||
if (device.getIsOxygenUsed() != null && device.getIsOxygenUsed() == 1
|
if (device.getIsOxygenUsed() != null && device.getIsOxygenUsed() == 1
|
||||||
&& device.getPondId() != null) {
|
&& device.getPondId() != null) {
|
||||||
String redisKey = "device:detector:oxy:offline:wait:" + deviceName;
|
String redisKey = DETECTOR_OXY_OFFLINE_WAIT_KEY_PREFIX + deviceName;
|
||||||
if (!Boolean.TRUE.equals(com.intc.common.redis.utils.RedisUtils.hasKey(redisKey))) {
|
if (!Boolean.TRUE.equals(com.intc.common.redis.utils.RedisUtils.hasKey(redisKey))) {
|
||||||
String offlineTime = LocalDateTime.now().format(DATETIME_FORMATTER);
|
String offlineTime = LocalDateTime.now().format(DATETIME_FORMATTER);
|
||||||
int offlineDelayMinutes = aliyunIotProperties.getOfflineWarnDelayMinutes();
|
int offlineDelayMinutes = aliyunIotProperties.getOfflineWarnDelayMinutes();
|
||||||
@@ -1903,25 +2050,15 @@ public class DeviceDataHandler {
|
|||||||
if (oxyOfflinePrev) {
|
if (oxyOfflinePrev) {
|
||||||
// 恢复:清位 + 删除 Redis 待告警标记
|
// 恢复:清位 + 删除 Redis 待告警标记
|
||||||
newCode &= ~DefineDeviceWarnCode.DetectorOxyOffline;
|
newCode &= ~DefineDeviceWarnCode.DetectorOxyOffline;
|
||||||
String redisKey = "device:detector:oxy:offline:wait:" + deviceName;
|
String redisKey = DETECTOR_OXY_OFFLINE_WAIT_KEY_PREFIX + deviceName;
|
||||||
com.intc.common.redis.utils.RedisUtils.deleteObject(redisKey);
|
com.intc.common.redis.utils.RedisUtils.deleteObject(redisKey);
|
||||||
log.info("[探头错误码] 溶解氧探头恢复,已删除待告警标记: {}", deviceName);
|
log.info("[探头错误码] 溶解氧探头恢复,已删除待告警标记: {}", deviceName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. DetectorPHOffline:直接更新位
|
// 2. PH探头/盐度探头离线:不需要前端展示,始终清除这些位(兼容历史数据)
|
||||||
if ((sensorErrorCode & DefineDeviceWarnCode.DetectorPHOffline) != 0) {
|
newCode &= ~DefineDeviceWarnCode.DetectorPHOffline;
|
||||||
newCode |= DefineDeviceWarnCode.DetectorPHOffline;
|
newCode &= ~DefineDeviceWarnCode.DetectorSalinityOffline;
|
||||||
} else {
|
|
||||||
newCode &= ~DefineDeviceWarnCode.DetectorPHOffline;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. DetectorSalinityOffline:直接更新位
|
|
||||||
if ((sensorErrorCode & DefineDeviceWarnCode.DetectorSalinityOffline) != 0) {
|
|
||||||
newCode |= DefineDeviceWarnCode.DetectorSalinityOffline;
|
|
||||||
} else {
|
|
||||||
newCode &= ~DefineDeviceWarnCode.DetectorSalinityOffline;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 有变化才更新数据库
|
// 有变化才更新数据库
|
||||||
if (newCode != currentCode) {
|
if (newCode != currentCode) {
|
||||||
|
|||||||
@@ -109,4 +109,50 @@ public class DeviceOfflineWarnTask {
|
|||||||
log.error("[离线告警] 扫描离线标记异常: {}", e.getMessage(), e);
|
log.error("[离线告警] 扫描离线标记异常: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每分钟扫描一次溶解氧探头离线等待标记
|
||||||
|
* 对超过配置的延迟时长的设备触发探头离线告警
|
||||||
|
* Redis value 格式:{offlineTime}(yyyy-MM-dd HH:mm:ss)
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 60000)
|
||||||
|
public void checkDetectorOxyOfflineWaitAndTrigger() {
|
||||||
|
try {
|
||||||
|
Collection<String> keys = RedisUtils.keys(DeviceDataHandler.DETECTOR_OXY_OFFLINE_WAIT_KEY_PREFIX + "*");
|
||||||
|
if (keys == null || keys.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int delayMinutes = aliyunIotProperties.getOfflineWarnDelayMinutes();
|
||||||
|
|
||||||
|
for (String key : keys) {
|
||||||
|
try {
|
||||||
|
String offlineTimeStr = RedisUtils.getCacheObject(key);
|
||||||
|
if (offlineTimeStr == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime offlineTime = LocalDateTime.parse(offlineTimeStr, FORMATTER);
|
||||||
|
long elapsedMinutes = java.time.Duration.between(offlineTime, now).toMinutes();
|
||||||
|
|
||||||
|
if (elapsedMinutes >= delayMinutes) {
|
||||||
|
// 提取 deviceName(去掉前缀)
|
||||||
|
String deviceName = key.substring(DeviceDataHandler.DETECTOR_OXY_OFFLINE_WAIT_KEY_PREFIX.length());
|
||||||
|
|
||||||
|
// 触发告警前先删除标记,防止重复触发
|
||||||
|
RedisUtils.deleteObject(key);
|
||||||
|
|
||||||
|
log.info("[探头离线告警] 溶解氧探头离线已超过 {} 分钟(阈值{}分钟),触发告警: {}", elapsedMinutes, delayMinutes, deviceName);
|
||||||
|
|
||||||
|
deviceDataHandler.triggerDetectorOxyOfflineAlarm(deviceName);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[探头离线告警] 处理单条探头离线标记异常,key={}: {}", key, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[探头离线告警] 扫描探头离线标记异常: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user