fix: 电话通知报警,逻辑完善,改为报警电话列表获取电话号码,通知成功后24小时不再打电话等等。
This commit is contained in:
@@ -239,6 +239,8 @@ aliyun:
|
||||
queue-name: Alicom-Queue-1572610294777992-VoiceReport
|
||||
# 告警通知间隔时间(分钟),同一设备在此时间内不重复发电话通知
|
||||
alarm-notification-interval-minutes: 30
|
||||
# 电话回执成功后的抑制时间(小时),已成功解到就在此时间内不再重复发起电话通知
|
||||
call-success-suppress-hours: 24
|
||||
# 设备离线告警延迟触发时间(分钟),设备离线超过此时间后才发送告警
|
||||
offline-warn-delay-minutes: 6
|
||||
# AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret)
|
||||
|
||||
@@ -77,6 +77,13 @@ public class AliyunIotProperties {
|
||||
*/
|
||||
private int alarmNotificationIntervalMinutes = 30;
|
||||
|
||||
/**
|
||||
* 电话已成功通知后的抑制时间(小时)
|
||||
* 同一设备同一类型告警,如果已有回执成功(callStatus=3)的通知记录,则在此时间内不再重复发起电话通知
|
||||
* 默认 24 小时
|
||||
*/
|
||||
private int callSuccessSuppressHours = 24;
|
||||
|
||||
/**
|
||||
* 离线告警延迟触发时间(分钟),设备离线超过此时间才触发告警
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.intc.iot.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONArray;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.intc.iot.domain.AquMapMessageWarnCallNotice;
|
||||
@@ -130,12 +131,6 @@ public class DeviceDataHandler {
|
||||
private static final String ALARM_TYPE_TEMPERATURE_LOW = "温度过低";
|
||||
private static final String ALARM_TYPE_BATTERY = "电池电量";
|
||||
|
||||
/**
|
||||
* 告警级别
|
||||
*/
|
||||
private static final int ALARM_LEVEL_NORMAL = 1; // 一般
|
||||
private static final int ALARM_LEVEL_IMPORTANT = 2; // 重要
|
||||
private static final int ALARM_LEVEL_URGENT = 3; // 紧急
|
||||
|
||||
/**
|
||||
* 消息未读状态
|
||||
@@ -369,9 +364,11 @@ public class DeviceDataHandler {
|
||||
StringBuilder alarmMessage = new StringBuilder();
|
||||
boolean hasAlarm = false;
|
||||
java.util.List<MessageWarn> warnList = new java.util.ArrayList<>();
|
||||
// 需要触发电话通知的告警列表(仅包含开启了电话通知开关的告警)
|
||||
java.util.List<MessageWarn> callWarnList = new java.util.ArrayList<>();
|
||||
|
||||
// 1. 检查溶解氧(使用设备配置的阈值)
|
||||
if (sensorData.getDissolvedOxygen() != null && device.getOxyWarnCallOpen() != null && device.getOxyWarnCallOpen() == 1) {
|
||||
// 1. 检查溶解氧(阈值判断与告警记录不受电话开关限制)
|
||||
if (sensorData.getDissolvedOxygen() != null) {
|
||||
Double dissolvedOxygen = sensorData.getDissolvedOxygen();
|
||||
Double oxyWarnLower = device.getOxyWarnLower();
|
||||
|
||||
@@ -380,14 +377,19 @@ public class DeviceDataHandler {
|
||||
dissolvedOxygen, oxyWarnLower);
|
||||
alarmMessage.append(message).append("; ");
|
||||
|
||||
warnList.add(createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_DISSOLVED_OXYGEN, WARN_TYPE_DISSOLVED_OXYGEN, message));
|
||||
MessageWarn warn = createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_DISSOLVED_OXYGEN, WARN_TYPE_DISSOLVED_OXYGEN, message);
|
||||
warnList.add(warn);
|
||||
// 仅在开启电话通知时加入电话通知列表
|
||||
if (device.getOxyWarnCallOpen() != null && device.getOxyWarnCallOpen() == 1) {
|
||||
callWarnList.add(warn);
|
||||
}
|
||||
hasAlarm = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查水温(使用设备配置的阈值)
|
||||
if (sensorData.getTemperature() != null && device.getTempWarnCallOpen() != null && device.getTempWarnCallOpen() == 1) {
|
||||
// 2. 检查水温(阈值判断与告警记录不受电话开关限制)
|
||||
if (sensorData.getTemperature() != null) {
|
||||
Double temperature = sensorData.getTemperature();
|
||||
Double tempWarnLower = device.getTempWarnLower();
|
||||
Double tempWarnUpper = device.getTempWarnUpper();
|
||||
@@ -397,22 +399,30 @@ public class DeviceDataHandler {
|
||||
temperature, tempWarnLower);
|
||||
alarmMessage.append(message).append("; ");
|
||||
|
||||
warnList.add(createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_TEMPERATURE_LOW, WARN_TYPE_TEMPERATURE, message));
|
||||
MessageWarn warn = createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_TEMPERATURE_LOW, WARN_TYPE_TEMPERATURE, message);
|
||||
warnList.add(warn);
|
||||
if (device.getTempWarnCallOpen() != null && device.getTempWarnCallOpen() == 1) {
|
||||
callWarnList.add(warn);
|
||||
}
|
||||
hasAlarm = true;
|
||||
} else if (tempWarnUpper != null && temperature > tempWarnUpper) {
|
||||
String message = String.format("水温过高: %.2f °C (最高: %.2f)",
|
||||
temperature, tempWarnUpper);
|
||||
alarmMessage.append(message).append("; ");
|
||||
|
||||
warnList.add(createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_TEMPERATURE_HIGH, WARN_TYPE_TEMPERATURE, message));
|
||||
MessageWarn warn = createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_TEMPERATURE_HIGH, WARN_TYPE_TEMPERATURE, message);
|
||||
warnList.add(warn);
|
||||
if (device.getTempWarnCallOpen() != null && device.getTempWarnCallOpen() == 1) {
|
||||
callWarnList.add(warn);
|
||||
}
|
||||
hasAlarm = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查电池电量(使用设备配置的阈值)
|
||||
if (sensorData.getBattery() != null && device.getBatteryWarnCallOpen() != null && device.getBatteryWarnCallOpen() == 1) {
|
||||
// 3. 检查电池电量(阈值判断与告警记录不受电话开关限制)
|
||||
if (sensorData.getBattery() != null) {
|
||||
Double battery = sensorData.getBattery();
|
||||
Double batteryWarnLower = device.getBatteryWarnLower() != null ? device.getBatteryWarnLower().doubleValue() : null;
|
||||
|
||||
@@ -421,23 +431,29 @@ public class DeviceDataHandler {
|
||||
battery, batteryWarnLower);
|
||||
alarmMessage.append(message).append("; ");
|
||||
|
||||
warnList.add(createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_BATTERY, WARN_TYPE_BATTERY, message));
|
||||
MessageWarn warn = createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_BATTERY, WARN_TYPE_BATTERY, message);
|
||||
warnList.add(warn);
|
||||
if (device.getBatteryWarnCallOpen() != null && device.getBatteryWarnCallOpen() == 1) {
|
||||
callWarnList.add(warn);
|
||||
}
|
||||
hasAlarm = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有告警,保存报警消息并触发通知
|
||||
// 如果有告警,保存全部报警消息记录
|
||||
if (hasAlarm) {
|
||||
log.warn("[告警] {} - {}", deviceName, alarmMessage);
|
||||
|
||||
// 批量保存报警消息
|
||||
// 批量保存报警消息(无论是否开启电话通知,告警记录始终生成)
|
||||
for (MessageWarn warn : warnList) {
|
||||
messageWarnMapper.insert(warn);
|
||||
}
|
||||
|
||||
// 触发电话通知
|
||||
triggerAlarmNotification(device, alarmMessage.toString(), sensorData, warnList);
|
||||
// 仅对开启了电话通知开关的告警触发电话通知
|
||||
if (!callWarnList.isEmpty()) {
|
||||
triggerAlarmNotification(device, alarmMessage.toString(), sensorData, callWarnList);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -445,11 +461,24 @@ public class DeviceDataHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 电话通知间隔时间(分钟)
|
||||
* 参考 C# CallNoticeMinuteInterval = 3
|
||||
*/
|
||||
private static final int CALL_NOTICE_MINUTE_INTERVAL = 3;
|
||||
|
||||
/**
|
||||
* 电话通知重试次数
|
||||
* 参考 C# CallNoticeRetryCount = 3
|
||||
*/
|
||||
private static final int CALL_NOTICE_RETRY_COUNT = 3;
|
||||
|
||||
/**
|
||||
* 触发告警通知
|
||||
* 按 title 分组,每种类型独立判断告警间隔,避免不同类型告警互相屏蔽
|
||||
* 免打扰开启时:每个时段(08:00-14:00、14:00-20:00、20:00-08:00)内只通知一次
|
||||
* 免打扰关闭时:按固定间隔时间判断
|
||||
* 手机号从 warnPhoneJson 获取,支持多号码轮转规避限流
|
||||
*
|
||||
* @param device 设备信息
|
||||
* @param alarmMessage 告警信息
|
||||
@@ -462,23 +491,23 @@ public class DeviceDataHandler {
|
||||
Long deviceId = device.getId();
|
||||
String deviceName = device.getDeviceName();
|
||||
|
||||
// 一次性查询用户手机号
|
||||
// 从 warnPhoneJson 获取告警手机号列表
|
||||
Long userId = device.getUserId();
|
||||
String mobilePhone = null;
|
||||
java.util.List<String> phoneList = null;
|
||||
if (userId != null) {
|
||||
AquUser aquUser = aquUserMapper.selectById(userId);
|
||||
if (aquUser != null) {
|
||||
mobilePhone = aquUser.getMobilePhone();
|
||||
phoneList = parseWarnPhoneList(aquUser.getWarnPhoneJson());
|
||||
}
|
||||
}
|
||||
if (userId == null || StrUtil.isBlank(mobilePhone)) {
|
||||
log.warn("[告警] {} - 信息不完整", deviceName);
|
||||
if (userId == null || phoneList == null || phoneList.isEmpty()) {
|
||||
log.warn("[告警] {} - 未配置告警手机号(warnPhoneJson为空)", deviceName);
|
||||
return;
|
||||
}
|
||||
final String finalMobilePhone = mobilePhone;
|
||||
|
||||
String pondName = warnList.isEmpty() ? deviceName : warnList.get(0).getPondName();
|
||||
LocalDateTime intervalTime = LocalDateTime.now().minusMinutes(aliyunIotProperties.getAlarmNotificationIntervalMinutes());
|
||||
// 抑制时间:根据配置的小时数展开,查找 callStatus=3(回执成功)的记录
|
||||
LocalDateTime intervalTime = LocalDateTime.now().minusHours(aliyunIotProperties.getCallSuccessSuppressHours());
|
||||
LocalDateTime periodStart = getCurrentPeriodStart();
|
||||
|
||||
// 按 title 分组,每种类型独立判断间隔并发送通知
|
||||
@@ -490,17 +519,16 @@ public class DeviceDataHandler {
|
||||
for (Map.Entry<String, java.util.List<MessageWarn>> entry : warnsByTitle.entrySet()) {
|
||||
String title = entry.getKey();
|
||||
java.util.List<MessageWarn> typeWarnList = entry.getValue();
|
||||
int warnType = typeWarnList.get(0).getWarnType();
|
||||
|
||||
// 判断该类型是否开启了免打扰
|
||||
// 判断该类型是否开启了免打扰(按 title 匹配,避免 warnType 值冲突导致判断失效)
|
||||
boolean noDisturbEnabled = false;
|
||||
if (WARN_TYPE_DISSOLVED_OXYGEN == warnType &&
|
||||
if (ALARM_TYPE_DISSOLVED_OXYGEN.equals(title) &&
|
||||
device.getOxyWarnCallNoDis() != null && device.getOxyWarnCallNoDis() == 1) {
|
||||
noDisturbEnabled = true;
|
||||
} else if (WARN_TYPE_TEMPERATURE == warnType &&
|
||||
} else if ((ALARM_TYPE_TEMPERATURE_HIGH.equals(title) || ALARM_TYPE_TEMPERATURE_LOW.equals(title)) &&
|
||||
device.getTempWarnCallNoDis() != null && device.getTempWarnCallNoDis() == 1) {
|
||||
noDisturbEnabled = true;
|
||||
} else if (WARN_TYPE_BATTERY == warnType &&
|
||||
} else if (ALARM_TYPE_BATTERY.equals(title) &&
|
||||
device.getBatteryWarnCallNoDis() != null && device.getBatteryWarnCallNoDis() == 1) {
|
||||
noDisturbEnabled = true;
|
||||
}
|
||||
@@ -515,37 +543,64 @@ public class DeviceDataHandler {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// 免打扰关闭:按固定间隔判断
|
||||
// 免打扰关闭:查找指定小时内是否已有回执成功的通知
|
||||
recentCount = warnCallNoticeMapper.countByDeviceAndTitleAfter(deviceId, title, intervalTime);
|
||||
if (recentCount > 0) {
|
||||
log.debug("[告警] {} title={} - {}min内已通知,跳过", deviceName, title, aliyunIotProperties.getAlarmNotificationIntervalMinutes());
|
||||
log.debug("[告警] {} title={} - {}h内已有回执成功的通知,跳过", deviceName, title, aliyunIotProperties.getCallSuccessSuppressHours());
|
||||
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);
|
||||
// 告警内容(取该类型第一条)
|
||||
String typeAlarmMessage = typeWarnList.get(0).getMessage();
|
||||
|
||||
// 保存报警消息与通知记录的关联关系
|
||||
for (MessageWarn warn : typeWarnList) {
|
||||
if (warn.getId() != null) {
|
||||
AquMapMessageWarnCallNotice mapRecord = new AquMapMessageWarnCallNotice();
|
||||
mapRecord.setMessageWarnId(warn.getId());
|
||||
mapRecord.setCallNoticeId(callNotice.getId());
|
||||
mapMessageWarnCallNoticeMapper.insert(mapRecord);
|
||||
// 为每个手机号 × 重试次数,创建通知记录,callTime 按间隔错开
|
||||
// 参考 C#: CallNoticeRetryCount=3, CallNoticeMinuteInterval=3
|
||||
// 立即尝试发送第一个号码;若失败则后续通知记录会在定时任务中按时间发送
|
||||
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);
|
||||
|
||||
// 保存报警消息与通知记录的关联关系
|
||||
for (MessageWarn warn : typeWarnList) {
|
||||
if (warn.getId() != null) {
|
||||
AquMapMessageWarnCallNotice mapRecord = new AquMapMessageWarnCallNotice();
|
||||
mapRecord.setMessageWarnId(warn.getId());
|
||||
mapRecord.setCallNoticeId(callNotice.getId());
|
||||
mapMessageWarnCallNoticeMapper.insert(mapRecord);
|
||||
}
|
||||
}
|
||||
|
||||
if (firstCallNotice == null) {
|
||||
firstCallNotice = callNotice;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送语音通知(取该类型第一条的告警内容为TTS参数)
|
||||
String typeAlarmMessage = typeWarnList.get(0).getMessage();
|
||||
sendVoiceNotification(callNotice, deviceName, typeAlarmMessage);
|
||||
// 立即尝试发送第一条通知(第一个手机号,第一次重试)
|
||||
// 若限流失败,后续记录的 callTime 到期后由定时任务扫描发送
|
||||
if (firstCallNotice != null) {
|
||||
sendVoiceNotification(firstCallNotice, deviceName, typeAlarmMessage);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -553,6 +608,36 @@ public class DeviceDataHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 warnPhoneJson 字段,返回有效手机号列表
|
||||
* 格式示例: ["13465511438","15726577528","13705354621"]
|
||||
*
|
||||
* @param warnPhoneJson JSON 字符串
|
||||
* @return 手机号列表,解析失败或为空返回 null
|
||||
*/
|
||||
private java.util.List<String> parseWarnPhoneList(String warnPhoneJson) {
|
||||
if (StrUtil.isBlank(warnPhoneJson)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
JSONArray jsonArray = JSONUtil.parseArray(warnPhoneJson);
|
||||
// 使用 LinkedHashSet 去重,同时保持原始顺序
|
||||
java.util.LinkedHashSet<String> phoneSet = new java.util.LinkedHashSet<>();
|
||||
for (Object obj : jsonArray) {
|
||||
if (obj != null && StrUtil.isNotBlank(obj.toString())) {
|
||||
String phone = obj.toString().trim();
|
||||
if (!phoneSet.add(phone)) {
|
||||
log.warn("[告警] warnPhoneJson 中存在重复手机号,已自动去重: {}", phone);
|
||||
}
|
||||
}
|
||||
}
|
||||
return phoneSet.isEmpty() ? null : new java.util.ArrayList<>(phoneSet);
|
||||
} catch (Exception e) {
|
||||
log.error("[告警] 解析 warnPhoneJson 失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前所在时段的起始时间
|
||||
* 时段划分:08:00-14:00 / 14:00-20:00 / 20:00-08:00(跨日)
|
||||
@@ -696,13 +781,17 @@ public class DeviceDataHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// 查询手机号
|
||||
// 从 warnPhoneJson 获取告警手机号列表
|
||||
AquUser aquUser = aquUserMapper.selectById(userId);
|
||||
if (aquUser == null || StrUtil.isBlank(aquUser.getMobilePhone())) {
|
||||
log.warn("[离线告警] 用户手机号不存在,跳过: {}", deviceName);
|
||||
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;
|
||||
}
|
||||
String mobilePhone = aquUser.getMobilePhone();
|
||||
|
||||
// 检查告警间隔(防止频繁通知)
|
||||
LocalDateTime intervalTime = LocalDateTime.now().minusMinutes(aliyunIotProperties.getAlarmNotificationIntervalMinutes());
|
||||
@@ -730,28 +819,50 @@ public class DeviceDataHandler {
|
||||
MessageWarn warn = createMessageWarn(deviceId, userId, pondName, "设备离线", WARN_TYPE_OFFLINE, warnMsg);
|
||||
messageWarnMapper.insert(warn);
|
||||
|
||||
// 创建通知记录
|
||||
AquWarnCallNotice callNotice = new AquWarnCallNotice();
|
||||
callNotice.setUserId(userId);
|
||||
callNotice.setMobilePhone(mobilePhone);
|
||||
callNotice.setDeviceId(deviceId);
|
||||
callNotice.setPondName(pondName);
|
||||
callNotice.setCallTime(LocalDateTime.now());
|
||||
callNotice.setCallStatus(CALL_STATUS_NO_CALL);
|
||||
warnCallNoticeMapper.insert(callNotice);
|
||||
// 创建通知记录(为每个手机号 × 重试次数)
|
||||
LocalDateTime baseTime = LocalDateTime.now();
|
||||
int index = 0;
|
||||
AquWarnCallNotice firstCallNotice = null;
|
||||
|
||||
// 保存关联记录
|
||||
if (warn.getId() != null) {
|
||||
AquMapMessageWarnCallNotice mapRecord = new AquMapMessageWarnCallNotice();
|
||||
mapRecord.setMessageWarnId(warn.getId());
|
||||
mapRecord.setCallNoticeId(callNotice.getId());
|
||||
mapMessageWarnCallNoticeMapper.insert(mapRecord);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送语音通知(TTS 参数用简短告警类型)
|
||||
sendVoiceNotification(callNotice, displayDeviceName, "设备离线");
|
||||
// 立即尝试发送第一条通知
|
||||
if (firstCallNotice != null) {
|
||||
sendVoiceNotification(firstCallNotice, displayDeviceName, "设备离线");
|
||||
}
|
||||
|
||||
log.info("[离线告警] 已触发通知 - 设备: {}, 塘口: {}, 手机: {}", deviceName, pondName, mobilePhone);
|
||||
log.info("[离线告警] 已触发通知 - 设备:{}, 塘口:{}, 手机号数:{}", deviceName, pondName, phoneList.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[离线告警] 处理失败: {}", e.getMessage(), e);
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 告警电话通知记录 Mapper
|
||||
@@ -17,7 +18,8 @@ import java.time.LocalDateTime;
|
||||
public interface AquWarnCallNoticeMapper extends BaseMapper<AquWarnCallNotice> {
|
||||
|
||||
/**
|
||||
* 查询指定设备、告警标题、时间范围内的通知数量(通过关联表反查 title)
|
||||
* 查询指定设备、告警标题、时间范围内「回执成功」的通知数量
|
||||
* 只有 callStatus=3(回执成功)的记录才算真正通知到人,才会触发抑制
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param title 告警标题(如"溶解氧"、"设备离线"等)
|
||||
@@ -29,14 +31,16 @@ public interface AquWarnCallNoticeMapper extends BaseMapper<AquWarnCallNotice> {
|
||||
"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_status = 3 " +
|
||||
"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
|
||||
* 只有 callStatus=3(回执成功)的记录才算真正通知到人,才会触发抑制
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param title 告警标题
|
||||
@@ -48,8 +52,31 @@ public interface AquWarnCallNoticeMapper extends BaseMapper<AquWarnCallNotice> {
|
||||
"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_status = 3 " +
|
||||
"AND n.call_time >= #{periodStart}")
|
||||
long countByDeviceAndTitleInPeriod(@Param("deviceId") Long deviceId,
|
||||
@Param("title") String title,
|
||||
@Param("periodStart") LocalDateTime periodStart);
|
||||
|
||||
/**
|
||||
* 查询 callStatus=0 且 callTime <= now 的到期待发记录
|
||||
* 用于定时任务扫描并触发语音通知
|
||||
*
|
||||
* @param now 当前时间
|
||||
* @return 到期待发的通知记录列表
|
||||
*/
|
||||
@Select("SELECT * FROM aqu_call_notice WHERE call_status = 0 AND call_time <= #{now} ORDER BY call_time ASC")
|
||||
List<AquWarnCallNotice> selectDuePendingNotices(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 通过 callNoticeId 查询关联的第一条告警消息内容
|
||||
* 用于定时任务重发时构建 TTS 参数
|
||||
*
|
||||
* @param callNoticeId 通知记录ID
|
||||
* @return 告警消息内容,不存在时返回 null
|
||||
*/
|
||||
@Select("SELECT w.message FROM aqu_message_warn w " +
|
||||
"INNER JOIN aqu_map_message_warn_call_notice m ON m.message_warn_id = w.id " +
|
||||
"WHERE m.call_notice_id = #{callNoticeId} LIMIT 1")
|
||||
String selectWarnMessageByCallNoticeId(@Param("callNoticeId") Long callNoticeId);
|
||||
}
|
||||
|
||||
@@ -48,4 +48,13 @@ public interface WarnCallNoticeService {
|
||||
* @return 清理的记录数量
|
||||
*/
|
||||
int cleanupExpiredNotifications();
|
||||
|
||||
/**
|
||||
* 扫描到期待发的通知记录,调用 VMS 发送语音通知
|
||||
* 对应注释:“如失败则后续通知记录会在定时任务中按时间发送”
|
||||
* 触发条件:callStatus=0 且 callTime <= 当前时间
|
||||
*
|
||||
* @return 发送成功的数量
|
||||
*/
|
||||
int processPendingNotifications();
|
||||
}
|
||||
|
||||
@@ -4,8 +4,10 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.intc.iot.domain.AquWarnCallNotice;
|
||||
import com.intc.iot.domain.VmsCallback;
|
||||
import com.intc.iot.domain.VmsNoticeResponse;
|
||||
import com.intc.iot.mapper.AquWarnCallNoticeMapper;
|
||||
import com.intc.iot.mapper.VmsCallbackMapper;
|
||||
import com.intc.iot.service.VmsNoticeService;
|
||||
import com.intc.iot.service.WarnCallNoticeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -15,7 +17,9 @@ import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 告警电话通知服务实现
|
||||
@@ -30,6 +34,7 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
|
||||
private final AquWarnCallNoticeMapper warnCallNoticeMapper;
|
||||
private final VmsCallbackMapper vmsCallbackMapper;
|
||||
private final VmsNoticeService vmsNoticeService;
|
||||
|
||||
/**
|
||||
* 呼叫状态枚举(对应 C# EnumCallNoticeStatus)
|
||||
@@ -48,9 +53,9 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
|
||||
/**
|
||||
* 回执超时时间(分钟)
|
||||
* 对应 C# CallNoticeMinuteInterval = 3
|
||||
* VMS 回执延迟较长,改为 5 分钟,避免回执未到就被标记为超时
|
||||
*/
|
||||
private static final int CALLBACK_TIMEOUT_MINUTES = 3;
|
||||
private static final int CALLBACK_TIMEOUT_MINUTES = 5;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@@ -65,6 +70,7 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
|
||||
// 优先从 outId(格式:ALARM_{callNoticeId}_{timestamp})中解析主键直接查询
|
||||
// 若解析失败则回退到 callId 匹配
|
||||
// 注意:不限制 callStatus,避免超时清理后回执仍可被处理
|
||||
AquWarnCallNotice callNotice = null;
|
||||
String outId = callback.getOutId();
|
||||
if (outId != null && outId.startsWith("ALARM_")) {
|
||||
@@ -76,10 +82,9 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
callNotice = warnCallNoticeMapper.selectOne(
|
||||
new LambdaQueryWrapper<AquWarnCallNotice>()
|
||||
.eq(AquWarnCallNotice::getId, noticeId)
|
||||
.eq(AquWarnCallNotice::getCallStatus, CALL_STATUS_CALL_SUCCESS)
|
||||
);
|
||||
if (callNotice != null) {
|
||||
log.debug("[告警通知] 通过 outId 找到通知记录 - 记录ID: {}", noticeId);
|
||||
log.debug("[告警通知] 通过 outId 找到通知记录 - 记录ID: {}, 当前状态: {}", noticeId, callNotice.getCallStatus());
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
@@ -87,12 +92,11 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:通过 callId 匹配
|
||||
// 回退:通过 callId 匹配(同样不限制 callStatus)
|
||||
if (callNotice == null) {
|
||||
callNotice = warnCallNoticeMapper.selectOne(
|
||||
new LambdaQueryWrapper<AquWarnCallNotice>()
|
||||
.eq(AquWarnCallNotice::getCallId, callback.getCallId())
|
||||
.eq(AquWarnCallNotice::getCallStatus, CALL_STATUS_CALL_SUCCESS)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,6 +179,11 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理超时的通知记录
|
||||
* 对应 C# CleanupExpiredNotifications
|
||||
* 清理规则:已呼叫成功(callStatus=1)但超过超时时间未收到回执的记录
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int cleanupExpiredNotifications() {
|
||||
@@ -224,6 +233,85 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描到期待发的通知记录,调用 VMS 发送语音通知
|
||||
* 触发条件:callStatus=0 且 callTime <= 当前时间
|
||||
*/
|
||||
@Override
|
||||
public int processPendingNotifications() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<AquWarnCallNotice> pendingList = warnCallNoticeMapper.selectDuePendingNotices(now);
|
||||
|
||||
if (pendingList.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
log.info("[待发通知] 发现 {} 条到期待发通知记录,开始处理", pendingList.size());
|
||||
|
||||
int sentCount = 0;
|
||||
for (AquWarnCallNotice notice : pendingList) {
|
||||
try {
|
||||
// 查询关联的告警消息内容,用于构建 TTS 参数
|
||||
String rawMessage = warnCallNoticeMapper.selectWarnMessageByCallNoticeId(notice.getId());
|
||||
String warnMessageShort = parseWarnMessageShort(rawMessage);
|
||||
|
||||
// 构建 TTS 参数
|
||||
Map<String, String> ttsParams = new HashMap<>();
|
||||
ttsParams.put("pondName", notice.getPondName() != null ? notice.getPondName() : "未知塘口");
|
||||
ttsParams.put("deviceName", "设备");
|
||||
ttsParams.put("warnMessage", warnMessageShort);
|
||||
|
||||
String outId = "ALARM_" + notice.getId() + "_" + System.currentTimeMillis();
|
||||
|
||||
VmsNoticeResponse response = vmsNoticeService.sendTtsCall(
|
||||
notice.getMobilePhone(),
|
||||
ttsParams,
|
||||
outId
|
||||
);
|
||||
|
||||
if (response != null && response.isSuccess()) {
|
||||
notice.setCallStatus(CALL_STATUS_CALL_SUCCESS);
|
||||
notice.setCallId(response.getCallId());
|
||||
notice.setOutId(outId);
|
||||
warnCallNoticeMapper.updateById(notice);
|
||||
sentCount++;
|
||||
log.info("[待发通知] 呼叫成功: {} - 记录ID: {}", notice.getMobilePhone(), notice.getId());
|
||||
} else {
|
||||
notice.setCallStatus(CALL_STATUS_CALL_FAIL);
|
||||
notice.setStatusMsg(response != null ? response.getMessage() : "呼叫失败");
|
||||
warnCallNoticeMapper.updateById(notice);
|
||||
log.warn("[待发通知] 呼叫失败: {} - 原因: {}", notice.getMobilePhone(), notice.getStatusMsg());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[待发通知] 处理待发通知异常 - 记录ID: {}, 电话: {}, 异常: {}",
|
||||
notice.getId(), notice.getMobilePhone(), e.getMessage(), e);
|
||||
try {
|
||||
notice.setCallStatus(CALL_STATUS_CALL_FAIL);
|
||||
notice.setStatusMsg("异常:" + e.getMessage());
|
||||
warnCallNoticeMapper.updateById(notice);
|
||||
} catch (Exception ex) {
|
||||
log.error("[待发通知] 更新失败状态异常: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[待发通知] 处理完成 - 成功: {}, 总数: {}", sentCount, pendingList.size());
|
||||
return sentCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析告警消息为简短形式(取 ':' 前的部分)
|
||||
* 与 DeviceDataHandler.sendVoiceNotification 保持一致
|
||||
*/
|
||||
private String parseWarnMessageShort(String rawMessage) {
|
||||
if (rawMessage == null) {
|
||||
return "异常";
|
||||
}
|
||||
return rawMessage.contains(":")
|
||||
? rawMessage.substring(0, rawMessage.indexOf(":")).trim()
|
||||
: rawMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电话通知的回执信息
|
||||
* 对应 C# UpdateCallNoticeFromCallback
|
||||
@@ -232,7 +320,12 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
* @param callback VMS 回执
|
||||
*/
|
||||
private void updateCallNoticeFromCallback(AquWarnCallNotice callNotice, VmsCallback callback) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
// 如果记录已是回执成功状态,跳过不再更新,防止重复回执覆盖
|
||||
if (callNotice.getCallStatus() != null && callNotice.getCallStatus() == CALL_STATUS_CALLBACK_SUCCESS) {
|
||||
log.info("[告警通知] 记录已是成功状态,跳过重复回执 - 记录ID: {}, 电话: {}",
|
||||
callNotice.getId(), callNotice.getMobilePhone());
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新回执详细信息(直接映射字段)
|
||||
callNotice.setStatusCode(callback.getStatusCode());
|
||||
|
||||
@@ -9,7 +9,7 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* VMS 回执处理定时任务
|
||||
* 对应 C# AlarmProcessor 中的 ProcessVmsCallbacks + CleanupExpiredNotifications 定时逻辑
|
||||
* 对应 C# AlarmProcessor 中的 ProcessVmsCallbacks + CleanupExpiredNotifications + 待发通知扫描 定时逻辑
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@@ -51,4 +51,21 @@ public class VmsCallbackProcessTask {
|
||||
log.error("[VMS超时清理] 清理超时记录异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时扫描到期待发的语音通知记录并发送
|
||||
* 每 30 秒执行一次
|
||||
* 对应注释:“如失败则后续通知记录会在定时任务中按时间发送”
|
||||
*/
|
||||
@Scheduled(fixedDelay = 30000)
|
||||
public void sendPendingNotifications() {
|
||||
try {
|
||||
int count = warnCallNoticeService.processPendingNotifications();
|
||||
if (count > 0) {
|
||||
log.info("[待发通知扫描] 本次发送了 {} 条到期通知", count);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[待发通知扫描] 处理待发通知异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user