From 35901f667d63252101d05bdd43d577863dc4489c Mon Sep 17 00:00:00 2001 From: tianyongbao Date: Wed, 18 Mar 2026 23:17:37 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=A5=B1=E5=92=8C=E5=BA=A6=E6=B5=93?= =?UTF-8?q?=E5=BA=A6=E8=B6=85=E8=BF=87200=EF=BC=8C=E8=BF=9E=E7=BB=AD8?= =?UTF-8?q?=E5=B0=8F=E6=97=B6=EF=BC=8C=E6=8A=A5=E8=AD=A6=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../intc/iot/config/AliyunIotProperties.java | 12 ++ .../intc/iot/handler/DeviceDataHandler.java | 160 ++++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java b/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java index b9186ce..f02adc6 100644 --- a/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java +++ b/intc-modules/intc-iot/src/main/java/com/intc/iot/config/AliyunIotProperties.java @@ -89,6 +89,18 @@ public class AliyunIotProperties { */ private int offlineWarnDelayMinutes = 6; + /** + * 夜间溶解氧饱和度告警阈值(%),超过此值时开始计时 + * 对应 C# DeviceSaturabilityWarnValue = 200 + */ + private double saturabilityWarnValue = 200; + + /** + * 夜间溶解氧饱和度持续超阈值后才触发告警的小时数 + * 对应 C# DeviceSaturabilityWarnHour = 8 + */ + private int saturabilityWarnHours = 8; + @Data public static class VmsMnsConfig { /** 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 0eb979c..36d05c9 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 @@ -130,6 +130,7 @@ public class DeviceDataHandler { private static final String ALARM_TYPE_TEMPERATURE_HIGH = "温度过高"; private static final String ALARM_TYPE_TEMPERATURE_LOW = "温度过低"; 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:"; + /** + * 夜间饱和度持续超阈等待告警的 Redis key 前缀 + * value = 首次超阈时间(yyyy-MM-dd HH:mm:ss) + */ + private static final String DEVICE_SATURA_WAIT_KEY_PREFIX = "device:satura:wait:"; + /** * 离线等待 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) { 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() + .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 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)的紧急告警