fix: 电话通知报警,逻辑完善,改为报警电话列表获取电话号码,通知成功后24小时不再打电话等等。

This commit is contained in:
tianyongbao
2026-03-09 23:37:33 +08:00
parent abadce8318
commit 1676ef7599
7 changed files with 355 additions and 89 deletions

View File

@@ -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);