From 1a66982586d4be71c290c69357837a80fc5261d9 Mon Sep 17 00:00:00 2001 From: tianyongbao Date: Sat, 2 May 2026 22:18:46 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=BA=B6=E8=A7=A3=E6=B0=A7=E6=8E=A2?= =?UTF-8?q?=E5=A4=B4=E7=A6=BB=E7=BA=BF=E5=91=8A=E8=AD=A6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../constant/DefineDeviceWarnCode.java | 3 +- .../fishery/controller/PondController.java | 9 +- .../intc/iot/handler/DeviceDataHandler.java | 167 ++++++++++++++++-- .../intc/iot/task/DeviceOfflineWarnTask.java | 46 +++++ 4 files changed, 206 insertions(+), 19 deletions(-) diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/constant/DefineDeviceWarnCode.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/constant/DefineDeviceWarnCode.java index 287776a..573dffd 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/constant/DefineDeviceWarnCode.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/constant/DefineDeviceWarnCode.java @@ -98,7 +98,8 @@ public class DefineDeviceWarnCode { /** * 转换告警码为告警描述(与 C# ToWarnDescription 保持一致) - * 优先级:设备过期 > 设备离线 > 设备断电 > 溶解氧离线 > 溶解氧告警 > 温度告警 > 饱和度过高 + * 优先级:设备过期 > 设备离线 > 设备休眠 > 设备断电 > 溶解氧离线 > 溶解氧告警 > 温度告警 > 饱和度过高 + * 注:PH/盐度/浊度/水位/氨氮探头离线不需要前端展示,故不在此处理 */ public static String toWarnDescription(int warnCode) { if (warnCode == None) { diff --git a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java index 37f8007..eb457f8 100644 --- a/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java +++ b/intc-modules/intc-fishery/src/main/java/com/intc/fishery/controller/PondController.java @@ -306,7 +306,8 @@ public class PondController extends BaseController { PondMode1WarnCodeInfo detectorWarnCodeInfo = new PondMode1WarnCodeInfo(); if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) { detectorWarnCodeInfo.setWarnCode(device.getWarnCode()); - detectorWarnCodeInfo.setWarnDescription(DefineDeviceWarnCode.toWarnDescription(device.getWarnCode())); + String desc = DefineDeviceWarnCode.toWarnDescription(device.getWarnCode()); + detectorWarnCodeInfo.setWarnDescription(desc != null ? desc : ""); } else { detectorWarnCodeInfo.setWarnCode(DefineDeviceWarnCode.None); detectorWarnCodeInfo.setWarnDescription(""); @@ -357,7 +358,8 @@ public class PondController extends BaseController { PondMode1WarnCodeInfo warnCodeInfo = new PondMode1WarnCodeInfo(); if (device.getWarnCode() != null && device.getWarnCode() < DefineDeviceWarnCode.UserMaxCode) { warnCodeInfo.setWarnCode(device.getWarnCode()); - warnCodeInfo.setWarnDescription(DefineDeviceWarnCode.toWarnDescription(device.getWarnCode())); + String desc = DefineDeviceWarnCode.toWarnDescription(device.getWarnCode()); + warnCodeInfo.setWarnDescription(desc != null ? desc : ""); } else { warnCodeInfo.setWarnCode(DefineDeviceWarnCode.None); warnCodeInfo.setWarnDescription(""); @@ -923,7 +925,8 @@ public class PondController extends BaseController { data.setDeviceCount(hashSetDeviceId.size()); data.setSwitchCount(pondSwitches.size()); data.getWarnCodeInfo().setWarnCode(warnCode); - data.getWarnCodeInfo().setWarnDescription(DefineDeviceWarnCode.toWarnDescription(warnCode)); + String warnDesc = DefineDeviceWarnCode.toWarnDescription(warnCode); + data.getWarnCodeInfo().setWarnDescription(warnDesc != null ? warnDesc : ""); listData.add(data); } diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java index c30aff9..2331514 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/handler/DeviceDataHandler.java @@ -156,6 +156,7 @@ public class DeviceDataHandler { private static final String ALARM_TYPE_TEMPERATURE_LOW = "温度过低"; private static final String ALARM_TYPE_BATTERY = "电池电量"; 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:"; + /** + * 溶解氧探头离线等待告警的 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 倍数(相对于延迟触发时间) */ @@ -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() + .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 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) 逻辑: @@ -1887,7 +2034,7 @@ public class DeviceDataHandler { // 只有开启了溶氧检测且处于塘口中才创建待告警 if (device.getIsOxygenUsed() != null && device.getIsOxygenUsed() == 1 && 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))) { String offlineTime = LocalDateTime.now().format(DATETIME_FORMATTER); int offlineDelayMinutes = aliyunIotProperties.getOfflineWarnDelayMinutes(); @@ -1903,25 +2050,15 @@ public class DeviceDataHandler { if (oxyOfflinePrev) { // 恢复:清位 + 删除 Redis 待告警标记 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); log.info("[探头错误码] 溶解氧探头恢复,已删除待告警标记: {}", deviceName); } } - // 2. DetectorPHOffline:直接更新位 - if ((sensorErrorCode & DefineDeviceWarnCode.DetectorPHOffline) != 0) { - newCode |= DefineDeviceWarnCode.DetectorPHOffline; - } else { - newCode &= ~DefineDeviceWarnCode.DetectorPHOffline; - } - - // 3. DetectorSalinityOffline:直接更新位 - if ((sensorErrorCode & DefineDeviceWarnCode.DetectorSalinityOffline) != 0) { - newCode |= DefineDeviceWarnCode.DetectorSalinityOffline; - } else { - newCode &= ~DefineDeviceWarnCode.DetectorSalinityOffline; - } + // 2. PH探头/盐度探头离线:不需要前端展示,始终清除这些位(兼容历史数据) + newCode &= ~DefineDeviceWarnCode.DetectorPHOffline; + newCode &= ~DefineDeviceWarnCode.DetectorSalinityOffline; // 有变化才更新数据库 if (newCode != currentCode) { diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/task/DeviceOfflineWarnTask.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/task/DeviceOfflineWarnTask.java index c1a0c8f..d8db3ee 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/task/DeviceOfflineWarnTask.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/task/DeviceOfflineWarnTask.java @@ -109,4 +109,50 @@ public class DeviceOfflineWarnTask { log.error("[离线告警] 扫描离线标记异常: {}", e.getMessage(), e); } } + + /** + * 每分钟扫描一次溶解氧探头离线等待标记 + * 对超过配置的延迟时长的设备触发探头离线告警 + * Redis value 格式:{offlineTime}(yyyy-MM-dd HH:mm:ss) + */ + @Scheduled(fixedDelay = 60000) + public void checkDetectorOxyOfflineWaitAndTrigger() { + try { + Collection 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); + } + } }