fix: 设备在离线,溶解氧和温度报警逻辑修改。
This commit is contained in:
@@ -237,6 +237,10 @@ aliyun:
|
|||||||
endpoint: https://1943695596114318.mns.cn-hangzhou.aliyuncs.com/
|
endpoint: https://1943695596114318.mns.cn-hangzhou.aliyuncs.com/
|
||||||
# MNS 队列名称(保持默认即可)
|
# MNS 队列名称(保持默认即可)
|
||||||
queue-name: Alicom-Queue-1572610294777992-VoiceReport
|
queue-name: Alicom-Queue-1572610294777992-VoiceReport
|
||||||
|
# 告警通知间隔时间(分钟),同一设备在此时间内不重复发电话通知
|
||||||
|
alarm-notification-interval-minutes: 30
|
||||||
|
# 设备离线告警延迟触发时间(分钟),设备离线超过此时间后才发送告警
|
||||||
|
offline-warn-delay-minutes: 6
|
||||||
# AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret)
|
# AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret)
|
||||||
amqp:
|
amqp:
|
||||||
# 是否启用
|
# 是否启用
|
||||||
|
|||||||
@@ -72,6 +72,16 @@ public class AliyunIotProperties {
|
|||||||
*/
|
*/
|
||||||
private VmsMnsConfig vms = new VmsMnsConfig();
|
private VmsMnsConfig vms = new VmsMnsConfig();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 告警通知间隔时间(分钟),同一设备同一类型告警在此时间内不重复发送
|
||||||
|
*/
|
||||||
|
private int alarmNotificationIntervalMinutes = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 离线告警延迟触发时间(分钟),设备离线超过此时间才触发告警
|
||||||
|
*/
|
||||||
|
private int offlineWarnDelayMinutes = 6;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class VmsMnsConfig {
|
public static class VmsMnsConfig {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -21,12 +21,15 @@ import com.intc.fishery.mapper.MessageWarnMapper;
|
|||||||
import com.intc.fishery.mapper.PondMapper;
|
import com.intc.fishery.mapper.PondMapper;
|
||||||
import com.intc.tdengine.domain.DeviceSensorData;
|
import com.intc.tdengine.domain.DeviceSensorData;
|
||||||
import com.intc.tdengine.service.IDeviceSensorDataService;
|
import com.intc.tdengine.service.IDeviceSensorDataService;
|
||||||
|
import com.intc.iot.config.AliyunIotProperties;
|
||||||
|
import com.intc.common.redis.utils.RedisUtils;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -89,6 +92,11 @@ public class DeviceDataHandler {
|
|||||||
*/
|
*/
|
||||||
private final VmsNoticeService vmsNoticeService;
|
private final VmsNoticeService vmsNoticeService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 阿里云 IoT 配置(包含告警间隔、离线延迟等可配置项)
|
||||||
|
*/
|
||||||
|
private final AliyunIotProperties aliyunIotProperties;
|
||||||
|
|
||||||
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,11 +114,8 @@ public class DeviceDataHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 告警间隔时间(分钟)- 同一设备同一类型告警在此时间内不重复发送
|
* 告警间隔时间(分钟)- 同一设备同一类型告警在此时间内不重复发送
|
||||||
*/
|
* 配置项: aliyun.living-iot.alarm-notification-interval-minutes,默认 30 分钟
|
||||||
private static final int ALARM_NOTIFICATION_INTERVAL = 30;
|
* 离线延迟触发时间: aliyun.living-iot.offline-warn-delay-minutes,默认 6 分钟
|
||||||
|
|
||||||
/**
|
|
||||||
* 告警类型常量(对应 warn_type 字段)
|
|
||||||
*/
|
*/
|
||||||
private static final int WARN_TYPE_DISSOLVED_OXYGEN = 2; // 溶解氧
|
private static final int WARN_TYPE_DISSOLVED_OXYGEN = 2; // 溶解氧
|
||||||
private static final int WARN_TYPE_TEMPERATURE = 2; // 温度
|
private static final int WARN_TYPE_TEMPERATURE = 2; // 温度
|
||||||
@@ -137,6 +142,17 @@ public class DeviceDataHandler {
|
|||||||
*/
|
*/
|
||||||
private static final int MSG_IS_READ_NO = 0;
|
private static final int MSG_IS_READ_NO = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备离线等待告警的 Redis key 前缀
|
||||||
|
* value = 离线时间(ISO格式),TTL 由配置参数决定
|
||||||
|
*/
|
||||||
|
private static final String DEVICE_OFFLINE_WAIT_KEY_PREFIX = "device:offline:wait:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 离线等待 key 的 Redis TTL 倍数(相对于延迟触发时间)
|
||||||
|
*/
|
||||||
|
private static final int OFFLINE_WAIT_TTL_MULTIPLIER = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理设备属性上报数据
|
* 处理设备属性上报数据
|
||||||
*
|
*
|
||||||
@@ -431,7 +447,9 @@ public class DeviceDataHandler {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 触发告警通知
|
* 触发告警通知
|
||||||
* 创建电话通知记录,并发送语音通知
|
* 按 title 分组,每种类型独立判断告警间隔,避免不同类型告警互相屏蔽
|
||||||
|
* 免打扰开启时:每个时段(08:00-14:00、14:00-20:00、20:00-08:00)内只通知一次
|
||||||
|
* 免打扰关闭时:按固定间隔时间判断
|
||||||
*
|
*
|
||||||
* @param device 设备信息
|
* @param device 设备信息
|
||||||
* @param alarmMessage 告警信息
|
* @param alarmMessage 告警信息
|
||||||
@@ -444,48 +462,8 @@ public class DeviceDataHandler {
|
|||||||
Long deviceId = device.getId();
|
Long deviceId = device.getId();
|
||||||
String deviceName = device.getDeviceName();
|
String deviceName = device.getDeviceName();
|
||||||
|
|
||||||
// 检查设备的免打扰设置
|
// 一次性查询用户手机号
|
||||||
boolean shouldSkip = false;
|
|
||||||
for (MessageWarn warn : warnList) {
|
|
||||||
if (WARN_TYPE_DISSOLVED_OXYGEN == warn.getWarnType() &&
|
|
||||||
device.getOxyWarnCallNoDis() != null && device.getOxyWarnCallNoDis() == 1) {
|
|
||||||
shouldSkip = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (WARN_TYPE_TEMPERATURE == warn.getWarnType() &&
|
|
||||||
device.getTempWarnCallNoDis() != null && device.getTempWarnCallNoDis() == 1) {
|
|
||||||
shouldSkip = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (WARN_TYPE_BATTERY == warn.getWarnType() &&
|
|
||||||
device.getBatteryWarnCallNoDis() != null && device.getBatteryWarnCallNoDis() == 1) {
|
|
||||||
shouldSkip = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldSkip) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否在告警间隔时间内已经发送过通知(防止频繁通知)
|
|
||||||
LocalDateTime intervalTime = LocalDateTime.now().minusMinutes(ALARM_NOTIFICATION_INTERVAL);
|
|
||||||
|
|
||||||
// 查询最近的通知记录
|
|
||||||
long recentNoticeCount = warnCallNoticeMapper.selectCount(
|
|
||||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AquWarnCallNotice>()
|
|
||||||
.eq(AquWarnCallNotice::getDeviceId, deviceId)
|
|
||||||
.ge(AquWarnCallNotice::getCallTime, intervalTime)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (recentNoticeCount > 0) {
|
|
||||||
log.debug("[告警] {} - {}分钟内已通知,跳过电话通知", deviceName, ALARM_NOTIFICATION_INTERVAL);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从设备信息获取用户信息
|
|
||||||
Long userId = device.getUserId();
|
Long userId = device.getUserId();
|
||||||
// 从 aqu_user 表查询手机号
|
|
||||||
String mobilePhone = null;
|
String mobilePhone = null;
|
||||||
if (userId != null) {
|
if (userId != null) {
|
||||||
AquUser aquUser = aquUserMapper.selectById(userId);
|
AquUser aquUser = aquUserMapper.selectById(userId);
|
||||||
@@ -493,44 +471,110 @@ public class DeviceDataHandler {
|
|||||||
mobilePhone = aquUser.getMobilePhone();
|
mobilePhone = aquUser.getMobilePhone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
String pondName = warnList.isEmpty() ? deviceName : warnList.get(0).getPondName();
|
|
||||||
|
|
||||||
// 验证必要信息
|
|
||||||
if (userId == null || StrUtil.isBlank(mobilePhone)) {
|
if (userId == null || StrUtil.isBlank(mobilePhone)) {
|
||||||
log.warn("[告警] {} - 信息不完整", deviceName);
|
log.warn("[告警] {} - 信息不完整", deviceName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final String finalMobilePhone = mobilePhone;
|
||||||
|
|
||||||
// 创建告警通知记录
|
String pondName = warnList.isEmpty() ? deviceName : warnList.get(0).getPondName();
|
||||||
AquWarnCallNotice callNotice = new AquWarnCallNotice();
|
LocalDateTime intervalTime = LocalDateTime.now().minusMinutes(aliyunIotProperties.getAlarmNotificationIntervalMinutes());
|
||||||
callNotice.setUserId(userId);
|
LocalDateTime periodStart = getCurrentPeriodStart();
|
||||||
callNotice.setMobilePhone(mobilePhone);
|
|
||||||
callNotice.setDeviceId(deviceId);
|
|
||||||
callNotice.setPondName(pondName);
|
|
||||||
callNotice.setCallTime(LocalDateTime.now());
|
|
||||||
callNotice.setCallStatus(CALL_STATUS_NO_CALL);
|
|
||||||
|
|
||||||
// 保存到数据库
|
// 按 title 分组,每种类型独立判断间隔并发送通知
|
||||||
warnCallNoticeMapper.insert(callNotice);
|
Map<String, java.util.List<MessageWarn>> warnsByTitle = new java.util.LinkedHashMap<>();
|
||||||
|
|
||||||
// 保存报警消息与通知记录的关联关系
|
|
||||||
for (MessageWarn warn : warnList) {
|
for (MessageWarn warn : warnList) {
|
||||||
if (warn.getId() != null) {
|
warnsByTitle.computeIfAbsent(warn.getTitle(), k -> new java.util.ArrayList<>()).add(warn);
|
||||||
AquMapMessageWarnCallNotice mapRecord = new AquMapMessageWarnCallNotice();
|
|
||||||
mapRecord.setMessageWarnId(warn.getId());
|
|
||||||
mapRecord.setCallNoticeId(callNotice.getId());
|
|
||||||
mapMessageWarnCallNoticeMapper.insert(mapRecord);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送语音通知
|
for (Map.Entry<String, java.util.List<MessageWarn>> entry : warnsByTitle.entrySet()) {
|
||||||
sendVoiceNotification(callNotice, deviceName, alarmMessage);
|
String title = entry.getKey();
|
||||||
|
java.util.List<MessageWarn> typeWarnList = entry.getValue();
|
||||||
|
int warnType = typeWarnList.get(0).getWarnType();
|
||||||
|
|
||||||
|
// 判断该类型是否开启了免打扰
|
||||||
|
boolean noDisturbEnabled = false;
|
||||||
|
if (WARN_TYPE_DISSOLVED_OXYGEN == warnType &&
|
||||||
|
device.getOxyWarnCallNoDis() != null && device.getOxyWarnCallNoDis() == 1) {
|
||||||
|
noDisturbEnabled = true;
|
||||||
|
} else if (WARN_TYPE_TEMPERATURE == warnType &&
|
||||||
|
device.getTempWarnCallNoDis() != null && device.getTempWarnCallNoDis() == 1) {
|
||||||
|
noDisturbEnabled = true;
|
||||||
|
} else if (WARN_TYPE_BATTERY == warnType &&
|
||||||
|
device.getBatteryWarnCallNoDis() != null && device.getBatteryWarnCallNoDis() == 1) {
|
||||||
|
noDisturbEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按设备+title检查是否已通知
|
||||||
|
long recentCount;
|
||||||
|
if (noDisturbEnabled) {
|
||||||
|
// 免打扰开启:当前时段内已通知过则跳过
|
||||||
|
recentCount = warnCallNoticeMapper.countByDeviceAndTitleInPeriod(deviceId, title, periodStart);
|
||||||
|
if (recentCount > 0) {
|
||||||
|
log.debug("[告警] {} title={} 免打扰:当前时段内已通知,跳过", deviceName, title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 免打扰关闭:按固定间隔判断
|
||||||
|
recentCount = warnCallNoticeMapper.countByDeviceAndTitleAfter(deviceId, title, intervalTime);
|
||||||
|
if (recentCount > 0) {
|
||||||
|
log.debug("[告警] {} title={} - {}min内已通知,跳过", deviceName, title, aliyunIotProperties.getAlarmNotificationIntervalMinutes());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建这个类型的通知记录
|
||||||
|
AquWarnCallNotice callNotice = new AquWarnCallNotice();
|
||||||
|
callNotice.setUserId(userId);
|
||||||
|
callNotice.setMobilePhone(finalMobilePhone);
|
||||||
|
callNotice.setDeviceId(deviceId);
|
||||||
|
callNotice.setPondName(pondName);
|
||||||
|
callNotice.setCallTime(LocalDateTime.now());
|
||||||
|
callNotice.setCallStatus(CALL_STATUS_NO_CALL);
|
||||||
|
warnCallNoticeMapper.insert(callNotice);
|
||||||
|
|
||||||
|
// 保存报警消息与通知记录的关联关系
|
||||||
|
for (MessageWarn warn : typeWarnList) {
|
||||||
|
if (warn.getId() != null) {
|
||||||
|
AquMapMessageWarnCallNotice mapRecord = new AquMapMessageWarnCallNotice();
|
||||||
|
mapRecord.setMessageWarnId(warn.getId());
|
||||||
|
mapRecord.setCallNoticeId(callNotice.getId());
|
||||||
|
mapMessageWarnCallNoticeMapper.insert(mapRecord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送语音通知(取该类型第一条的告警内容为TTS参数)
|
||||||
|
String typeAlarmMessage = typeWarnList.get(0).getMessage();
|
||||||
|
sendVoiceNotification(callNotice, deviceName, typeAlarmMessage);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[告警通知] 触发告警通知失败: {}", e.getMessage(), e);
|
log.error("[告警通知] 触发告警通知失败: {}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前所在时段的起始时间
|
||||||
|
* 时段划分:08:00-14:00 / 14:00-20:00 / 20:00-08:00(跨日)
|
||||||
|
*
|
||||||
|
* @return 当前时段起始的 LocalDateTime
|
||||||
|
*/
|
||||||
|
private LocalDateTime getCurrentPeriodStart() {
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
int hour = now.getHour();
|
||||||
|
java.time.LocalDate today = now.toLocalDate();
|
||||||
|
if (hour >= 8 && hour < 14) {
|
||||||
|
return today.atTime(8, 0);
|
||||||
|
} else if (hour >= 14 && hour < 20) {
|
||||||
|
return today.atTime(14, 0);
|
||||||
|
} else if (hour >= 20) {
|
||||||
|
return today.atTime(20, 0);
|
||||||
|
} else {
|
||||||
|
// 00:00-08:00,属于前一天 20:00 开始的时段
|
||||||
|
return today.minusDays(1).atTime(20, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送语音通知
|
* 发送语音通知
|
||||||
*
|
*
|
||||||
@@ -591,12 +635,47 @@ public class DeviceDataHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理设备离线告警
|
* 处理设备离线事件(参考 C# UpdateDeviceStatus 逻辑)
|
||||||
* 当设备离线时,查询设备信息并发送语音通知
|
* 收到离线消息后写入 Redis 等待标记,不立即告警。
|
||||||
|
* 由定时任务扫描超过配置的 offline-warn-delay-minutes 分钟的标记后才真正触发告警。
|
||||||
*
|
*
|
||||||
* @param deviceName 设备名称(serialNum)
|
* @param deviceName 设备名称(serialNum)
|
||||||
*/
|
*/
|
||||||
public void handleDeviceOffline(String deviceName) {
|
public void handleDeviceOffline(String deviceName) {
|
||||||
|
String redisKey = DEVICE_OFFLINE_WAIT_KEY_PREFIX + deviceName;
|
||||||
|
// 已有等待标记则不重复写入(等待时间从首次离线算起)
|
||||||
|
if (Boolean.TRUE.equals(RedisUtils.hasKey(redisKey))) {
|
||||||
|
log.debug("[离线告警] 已有等待标记,跳过重复写入: {}", deviceName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String offlineTime = LocalDateTime.now().format(DATETIME_FORMATTER);
|
||||||
|
int offlineDelayMinutes = aliyunIotProperties.getOfflineWarnDelayMinutes();
|
||||||
|
int ttlMinutes = offlineDelayMinutes * OFFLINE_WAIT_TTL_MULTIPLIER;
|
||||||
|
RedisUtils.setCacheObject(redisKey, offlineTime, Duration.ofMinutes(ttlMinutes));
|
||||||
|
log.info("[离线告警] 设备离线,已记录等待标记,将在 {} 分钟后触发告警: {}", offlineDelayMinutes, deviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理设备上线事件(参考 C# UpdateDeviceStatus 逻辑)
|
||||||
|
* 上线时清除 Redis 等待标记,取消离线告警。
|
||||||
|
*
|
||||||
|
* @param deviceName 设备名称(serialNum)
|
||||||
|
*/
|
||||||
|
public void handleDeviceOnline(String deviceName) {
|
||||||
|
String redisKey = DEVICE_OFFLINE_WAIT_KEY_PREFIX + deviceName;
|
||||||
|
boolean removed = RedisUtils.deleteObject(redisKey);
|
||||||
|
if (removed) {
|
||||||
|
log.info("[离线告警] 设备已上线,取消离线告警: {}", deviceName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 真正触发设备离线告警(由定时任务调用)
|
||||||
|
* 对应 C# CheckDeviceMessageWarn(warnCode=DeviceOffline) 逻辑
|
||||||
|
*
|
||||||
|
* @param deviceName 设备名称(serialNum)
|
||||||
|
*/
|
||||||
|
public void triggerOfflineAlarm(String deviceName) {
|
||||||
try {
|
try {
|
||||||
Device device = deviceMapper.selectOne(
|
Device device = deviceMapper.selectOne(
|
||||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||||
@@ -626,30 +705,29 @@ public class DeviceDataHandler {
|
|||||||
String mobilePhone = aquUser.getMobilePhone();
|
String mobilePhone = aquUser.getMobilePhone();
|
||||||
|
|
||||||
// 检查告警间隔(防止频繁通知)
|
// 检查告警间隔(防止频繁通知)
|
||||||
LocalDateTime intervalTime = LocalDateTime.now().minusMinutes(ALARM_NOTIFICATION_INTERVAL);
|
LocalDateTime intervalTime = LocalDateTime.now().minusMinutes(aliyunIotProperties.getAlarmNotificationIntervalMinutes());
|
||||||
long recentCount = warnCallNoticeMapper.selectCount(
|
long recentCount = warnCallNoticeMapper.countByDeviceAndTitleAfter(deviceId, "设备离线", intervalTime);
|
||||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AquWarnCallNotice>()
|
|
||||||
.eq(AquWarnCallNotice::getDeviceId, deviceId)
|
|
||||||
.ge(AquWarnCallNotice::getCallTime, intervalTime)
|
|
||||||
);
|
|
||||||
if (recentCount > 0) {
|
if (recentCount > 0) {
|
||||||
log.debug("[离线告警] {}分钟内已通知,跳过: {}", ALARM_NOTIFICATION_INTERVAL, deviceName);
|
log.debug("[离线告警] {}分钟内已通知,跳过: {}", aliyunIotProperties.getAlarmNotificationIntervalMinutes(), deviceName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询塘口名称
|
// 查询塘口名称(设备未绑定塘口则跳过,参考 C# 只处理塘口中的设备)
|
||||||
String pondName = deviceName;
|
if (device.getPondId() == null) {
|
||||||
if (device.getPondId() != null) {
|
log.debug("[离线告警] 设备未绑定塘口,跳过: {}", deviceName);
|
||||||
Pond pond = pondMapper.selectById(device.getPondId());
|
return;
|
||||||
if (pond != null) {
|
|
||||||
pondName = pond.getPondName();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
// 写入告警消息
|
// 告警消息内容与 C# 保持一致:{pondName}塘口中{deviceName}({serialNum})设备离线
|
||||||
String warnMsg = "设备离线";
|
String warnMsg = String.format("%s塘口中%s(%s)设备离线", pondName, displayDeviceName, deviceName);
|
||||||
MessageWarn warn = createMessageWarn(deviceId, userId, pondName,
|
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, "设备离线", WARN_TYPE_OFFLINE, warnMsg);
|
||||||
"设备离线", WARN_TYPE_OFFLINE, warnMsg);
|
|
||||||
messageWarnMapper.insert(warn);
|
messageWarnMapper.insert(warn);
|
||||||
|
|
||||||
// 创建通知记录
|
// 创建通知记录
|
||||||
@@ -670,10 +748,10 @@ public class DeviceDataHandler {
|
|||||||
mapMessageWarnCallNoticeMapper.insert(mapRecord);
|
mapMessageWarnCallNoticeMapper.insert(mapRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送语音通知
|
// 发送语音通知(TTS 参数用简短告警类型)
|
||||||
sendVoiceNotification(callNotice, device.getDeviceName() != null ? device.getDeviceName() : deviceName, warnMsg);
|
sendVoiceNotification(callNotice, displayDeviceName, "设备离线");
|
||||||
|
|
||||||
log.info("[离线告警] 已触发通知 - 设备: {}, 手机: {}", deviceName, mobilePhone);
|
log.info("[离线告警] 已触发通知 - 设备: {}, 塘口: {}, 手机: {}", deviceName, pondName, mobilePhone);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[离线告警] 处理失败: {}", e.getMessage(), e);
|
log.error("[离线告警] 处理失败: {}", e.getMessage(), e);
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ package com.intc.iot.mapper;
|
|||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.intc.iot.domain.AquWarnCallNotice;
|
import com.intc.iot.domain.AquWarnCallNotice;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 告警电话通知记录 Mapper
|
* 告警电话通知记录 Mapper
|
||||||
@@ -11,4 +15,41 @@ import org.apache.ibatis.annotations.Mapper;
|
|||||||
*/
|
*/
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface AquWarnCallNoticeMapper extends BaseMapper<AquWarnCallNotice> {
|
public interface AquWarnCallNoticeMapper extends BaseMapper<AquWarnCallNotice> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定设备、告警标题、时间范围内的通知数量(通过关联表反查 title)
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param title 告警标题(如"溶解氧"、"设备离线"等)
|
||||||
|
* @param afterTime 起始时间
|
||||||
|
* @return 满足条件的通知数量
|
||||||
|
*/
|
||||||
|
@Select("SELECT COUNT(1) FROM aqu_call_notice n " +
|
||||||
|
"INNER JOIN aqu_map_message_warn_call_notice m ON m.call_notice_id = n.id " +
|
||||||
|
"INNER JOIN aqu_message_warn w ON w.id = m.message_warn_id " +
|
||||||
|
"WHERE n.device_id = #{deviceId} " +
|
||||||
|
"AND w.title = #{title} " +
|
||||||
|
"AND n.call_time >= #{afterTime}")
|
||||||
|
long countByDeviceAndTitleAfter(@Param("deviceId") Long deviceId,
|
||||||
|
@Param("title") String title,
|
||||||
|
@Param("afterTime") LocalDateTime afterTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询指定设备、告警标题、时段范围内的通知数量(免打扰模式:每个时段只通知一次)
|
||||||
|
* 时段划分:08:00-14:00、14:00-20:00、20:00-08:00
|
||||||
|
*
|
||||||
|
* @param deviceId 设备ID
|
||||||
|
* @param title 告警标题
|
||||||
|
* @param periodStart 当前时段起始时间
|
||||||
|
* @return 满足条件的通知数量
|
||||||
|
*/
|
||||||
|
@Select("SELECT COUNT(1) FROM aqu_call_notice n " +
|
||||||
|
"INNER JOIN aqu_map_message_warn_call_notice m ON m.call_notice_id = n.id " +
|
||||||
|
"INNER JOIN aqu_message_warn w ON w.id = m.message_warn_id " +
|
||||||
|
"WHERE n.device_id = #{deviceId} " +
|
||||||
|
"AND w.title = #{title} " +
|
||||||
|
"AND n.call_time >= #{periodStart}")
|
||||||
|
long countByDeviceAndTitleInPeriod(@Param("deviceId") Long deviceId,
|
||||||
|
@Param("title") String title,
|
||||||
|
@Param("periodStart") LocalDateTime periodStart);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,6 +246,8 @@ public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
|
|||||||
|
|
||||||
if ("offline".equals(status) && deviceDataHandler != null && deviceName != null) {
|
if ("offline".equals(status) && deviceDataHandler != null && deviceName != null) {
|
||||||
deviceDataHandler.handleDeviceOffline(deviceName);
|
deviceDataHandler.handleDeviceOffline(deviceName);
|
||||||
|
} else if ("online".equals(status) && deviceDataHandler != null && deviceName != null) {
|
||||||
|
deviceDataHandler.handleDeviceOnline(deviceName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +266,8 @@ public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
|
|||||||
|
|
||||||
if ("offline".equals(action) && deviceDataHandler != null && deviceName != null) {
|
if ("offline".equals(action) && deviceDataHandler != null && deviceName != null) {
|
||||||
deviceDataHandler.handleDeviceOffline(deviceName);
|
deviceDataHandler.handleDeviceOffline(deviceName);
|
||||||
|
} else if ("online".equals(action) && deviceDataHandler != null && deviceName != null) {
|
||||||
|
deviceDataHandler.handleDeviceOnline(deviceName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.intc.iot.task;
|
||||||
|
|
||||||
|
import com.intc.common.redis.utils.RedisUtils;
|
||||||
|
import com.intc.iot.config.AliyunIotProperties;
|
||||||
|
import com.intc.iot.handler.DeviceDataHandler;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备离线告警延迟触发定时任务
|
||||||
|
* 对应 C# UpdateDataSecondlyJob.CheckWarnAndNotice 中对 DeviceOffline WarnWait 的处理:
|
||||||
|
* 离线超过配置的 offline-warn-delay-minutes 分钟才真正触发告警。
|
||||||
|
*
|
||||||
|
* @author intc-iot
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConditionalOnBean(DeviceDataHandler.class)
|
||||||
|
public class DeviceOfflineWarnTask {
|
||||||
|
|
||||||
|
private static final String OFFLINE_WAIT_KEY_PREFIX = "device:offline:wait:";
|
||||||
|
|
||||||
|
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
private final DeviceDataHandler deviceDataHandler;
|
||||||
|
|
||||||
|
private final AliyunIotProperties aliyunIotProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每分钟扫描一次离线等待标记
|
||||||
|
* 对超过配置的 offline-warn-delay-minutes 分钟的设备触发告警
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedDelay = 60000)
|
||||||
|
public void checkOfflineWaitAndTrigger() {
|
||||||
|
try {
|
||||||
|
Collection<String> keys = RedisUtils.keys(OFFLINE_WAIT_KEY_PREFIX + "*");
|
||||||
|
if (keys == null || keys.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
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 >= aliyunIotProperties.getOfflineWarnDelayMinutes()) {
|
||||||
|
// 提取 deviceName(去掉前缀)
|
||||||
|
String deviceName = key.substring(OFFLINE_WAIT_KEY_PREFIX.length());
|
||||||
|
|
||||||
|
// 触发告警前先删除标记,防止重复触发
|
||||||
|
RedisUtils.deleteObject(key);
|
||||||
|
|
||||||
|
log.info("[离线告警] 设备离线已超过 {} 分钟,触发告警: {}", elapsedMinutes, deviceName);
|
||||||
|
deviceDataHandler.triggerOfflineAlarm(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