fix: 电话报警通知逻辑修改。
This commit is contained in:
@@ -60,13 +60,33 @@
|
||||
<version>1.1.9.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云语音服务 SDK(稳定版)-->
|
||||
<!-- JAXB API(JDK 9+ 已移除内置 JAXB,aliyun-sdk-mns 解析 XML 响应时需要) -->
|
||||
<dependency>
|
||||
<groupId>javax.xml.bind</groupId>
|
||||
<artifactId>jaxb-api</artifactId>
|
||||
<version>2.3.1</version>
|
||||
</dependency>
|
||||
<!-- JAXB 运行时实现 -->
|
||||
<dependency>
|
||||
<groupId>com.sun.xml.bind</groupId>
|
||||
<artifactId>jaxb-impl</artifactId>
|
||||
<version>2.3.9</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云语音服务 SDK -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-dyvmsapi</artifactId>
|
||||
<version>1.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云 Dybaseapi SDK(包含 QueryTokenForMnsQueue,用于获取 MNS STS 临时凭证) -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-dybaseapi</artifactId>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP 客户端 -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
|
||||
@@ -47,6 +47,16 @@ public class AliyunIotProperties {
|
||||
*/
|
||||
private String categoryKey;
|
||||
|
||||
/**
|
||||
* VMS 语音通知 TTS 模板编码
|
||||
*/
|
||||
private String vmsTtsCode;
|
||||
|
||||
/**
|
||||
* VMS 语音通知主叫显号
|
||||
*/
|
||||
private String vmsShowNumber;
|
||||
|
||||
/**
|
||||
* MQTT 配置(可选,用于设备直连)
|
||||
*/
|
||||
@@ -57,6 +67,40 @@ public class AliyunIotProperties {
|
||||
*/
|
||||
private AmqpConfig amqp = new AmqpConfig();
|
||||
|
||||
/**
|
||||
* VMS MNS 回执消费配置
|
||||
*/
|
||||
private VmsMnsConfig vms = new VmsMnsConfig();
|
||||
|
||||
@Data
|
||||
public static class VmsMnsConfig {
|
||||
/**
|
||||
* 是否启用 MNS 回扇消费
|
||||
*/
|
||||
private Boolean mnsEnabled = false;
|
||||
|
||||
/**
|
||||
* MNS AccessKey ID(语音服务对应账号的 AK,可能与 IoT 平台不同)
|
||||
*/
|
||||
private String accessKeyId;
|
||||
|
||||
/**
|
||||
* MNS AccessKey Secret
|
||||
*/
|
||||
private String accessKeySecret;
|
||||
|
||||
/**
|
||||
* MNS 账号内网 Endpoint
|
||||
* 示例:https://{AccountId}.mns.cn-hangzhou.aliyuncs.com/
|
||||
*/
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* MNS 队列名称
|
||||
*/
|
||||
private String queueName = "Alicom-Queue-1572610294777992-VoiceReport";
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class MqttConfig {
|
||||
/**
|
||||
|
||||
@@ -38,6 +38,7 @@ import com.intc.iot.service.IotDeviceService;
|
||||
import com.intc.iot.service.MqttService;
|
||||
import com.intc.iot.service.VmsMnsConsumerService;
|
||||
import com.intc.iot.service.VmsNoticeService;
|
||||
import com.intc.iot.handler.DeviceDataHandler;
|
||||
import com.intc.iot.service.WarnCallNoticeService;
|
||||
import com.intc.iot.utils.AliyunAmqpSignUtil;
|
||||
import com.intc.iot.utils.ControllerHelper;
|
||||
@@ -95,6 +96,9 @@ public class IotController extends BaseController {
|
||||
@Autowired(required = false)
|
||||
private WarnCallNoticeService warnCallNoticeService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private DeviceDataHandler deviceDataHandler;
|
||||
|
||||
@Autowired(required = false)
|
||||
private javax.jms.Connection amqpConnection;
|
||||
|
||||
@@ -588,6 +592,66 @@ public class IotController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== 模拟报警触发接口 ========================
|
||||
|
||||
/**
|
||||
* 模拟设备数据上报,触发报警和打电话流程。
|
||||
* 接口会伪造一条 MQTT 属性上报消息,携带你指定的传感器值,
|
||||
* 走完整的「数据解析 → 阈值判断 → 告警记录 → VMS语音呼叫」链路。
|
||||
*
|
||||
* @param deviceName 设备序列号(需在数据库中存在,且已配置手机号和告警阈值)
|
||||
* @param dissolvedOxygen 溶解氧值(mg/L),不传则使用默认值 1.0(低于通常阈值,触发告警)
|
||||
* @param temperature 水温值(°C),不传则不设置
|
||||
* @param battery 电量(%),不传则不设置
|
||||
*/
|
||||
@Operation(summary = "模拟告警触发(测试打电话)",
|
||||
description = "构造超阈值传感器数据,走完整告警→VMS语音呼叫链路")
|
||||
@PostMapping("/alarm/simulate")
|
||||
public R<Map<String, Object>> simulateAlarm(
|
||||
@Parameter(description = "设备序列号(serialNum)") @RequestParam String deviceName,
|
||||
@Parameter(description = "溶解氧值,默认1.0触发低溶解氧告警") @RequestParam(required = false) Double dissolvedOxygen,
|
||||
@Parameter(description = "水温值,可选") @RequestParam(required = false) Double temperature,
|
||||
@Parameter(description = "电池电量%,可选") @RequestParam(required = false) Double battery) {
|
||||
try {
|
||||
if (deviceDataHandler == null) {
|
||||
return R.fail("DeviceDataHandler 未启用(IDeviceSensorDataService 未加载)");
|
||||
}
|
||||
|
||||
// 默认溶解氧给一个极低值,保证能触发告警
|
||||
double doValue = dissolvedOxygen != null ? dissolvedOxygen : 1.0;
|
||||
|
||||
// 构造飞燕平台属性上报的标准 payload
|
||||
// Topic 格式: /sys/{productKey}/{deviceName}/thing/event/property/post
|
||||
// 这里 productKey 随便填一个非控制器的值,让它走普通传感器处理分支
|
||||
String mockProductKey = "simulate";
|
||||
String mockTopic = "/sys/" + mockProductKey + "/" + deviceName + "/thing/event/property/post";
|
||||
|
||||
// 构造 params JSON,字段数 > 3 以通过过滤条件
|
||||
cn.hutool.json.JSONObject params = new cn.hutool.json.JSONObject();
|
||||
params.set("dissolvedOxygen", doValue);
|
||||
params.set("temperature", temperature != null ? temperature : 25.0);
|
||||
params.set("battery", battery != null ? battery : 80.0);
|
||||
params.set("saturability", 50.0); // 补足字段数 > 3
|
||||
|
||||
cn.hutool.json.JSONObject payload = new cn.hutool.json.JSONObject();
|
||||
payload.set("params", params);
|
||||
|
||||
log.info("[模拟告警] deviceName={}, topic={}, payload={}", deviceName, mockTopic, payload);
|
||||
|
||||
// 调用真实的属性处理方法,走完整链路
|
||||
deviceDataHandler.handlePropertyPost(mockTopic, payload.toString());
|
||||
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("mockTopic", mockTopic);
|
||||
result.put("mockPayload", payload.toString());
|
||||
result.put("message", "模拟消息已发送,请查看日志确认告警和呼叫是否触发");
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("模拟告警触发失败: {}", e.getMessage(), e);
|
||||
return R.fail("模拟告警触发失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== VMS 语音通知相关接口 ========================
|
||||
|
||||
@Operation(summary = "发送语音通知(测试接口)")
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.intc.iot.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.intc.common.tenant.core.TenantEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 报警消息与通知记录关联表 aqu_map_message_warn_call_notice
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("aqu_map_message_warn_call_notice")
|
||||
public class AquMapMessageWarnCallNotice extends TenantEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 报警消息ID(关联 aqu_message_warn.id)
|
||||
*/
|
||||
private Long messageWarnId;
|
||||
|
||||
/**
|
||||
* 通知记录ID(关联 aqu_call_notice.id)
|
||||
*/
|
||||
private Long callNoticeId;
|
||||
|
||||
}
|
||||
@@ -3,27 +3,30 @@ package com.intc.iot.domain;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.intc.common.tenant.core.TenantEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 告警电话通知记录
|
||||
* 对应 C# DbAquWarnCallNotice
|
||||
* 告警电话通知记录 aqu_call_notice
|
||||
*
|
||||
* @author intc-iot
|
||||
* @author intc
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("aqu_call_notice")
|
||||
public class AquWarnCallNotice implements Serializable {
|
||||
public class AquWarnCallNotice extends TenantEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
@@ -131,14 +134,4 @@ public class AquWarnCallNotice implements Serializable {
|
||||
*/
|
||||
private String tollType;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
}
|
||||
|
||||
@@ -3,16 +3,22 @@ package com.intc.iot.handler;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.intc.iot.domain.AquAlarmHistory;
|
||||
import com.intc.iot.domain.AquMapMessageWarnCallNotice;
|
||||
import com.intc.iot.domain.AquWarnCallNotice;
|
||||
import com.intc.iot.domain.VmsNoticeResponse;
|
||||
import com.intc.iot.mapper.AquAlarmHistoryMapper;
|
||||
import com.intc.iot.mapper.AquMapMessageWarnCallNoticeMapper;
|
||||
import com.intc.iot.mapper.AquWarnCallNoticeMapper;
|
||||
import com.intc.iot.mapper.IotDeviceMapper;
|
||||
import com.intc.iot.service.VmsNoticeService;
|
||||
import com.intc.fishery.domain.AquUser;
|
||||
import com.intc.fishery.domain.Device;
|
||||
import com.intc.fishery.domain.MessageWarn;
|
||||
import com.intc.fishery.domain.Pond;
|
||||
import com.intc.fishery.mapper.AquUserMapper;
|
||||
import com.intc.fishery.mapper.DeviceMapper;
|
||||
import com.intc.fishery.mapper.DeviceSwitchMapper;
|
||||
import com.intc.fishery.mapper.MessageWarnMapper;
|
||||
import com.intc.fishery.mapper.PondMapper;
|
||||
import com.intc.tdengine.domain.DeviceSensorData;
|
||||
import com.intc.tdengine.service.IDeviceSensorDataService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -49,10 +55,14 @@ public class DeviceDataHandler {
|
||||
private final AquWarnCallNoticeMapper warnCallNoticeMapper;
|
||||
|
||||
/**
|
||||
* 告警历史记录 Mapper
|
||||
* 报警消息与通知记录关联 Mapper
|
||||
*/
|
||||
private final AquAlarmHistoryMapper alarmHistoryMapper;
|
||||
private final AquMapMessageWarnCallNoticeMapper mapMessageWarnCallNoticeMapper;
|
||||
|
||||
/**
|
||||
* 报警消息 Mapper
|
||||
*/
|
||||
private final MessageWarnMapper messageWarnMapper;
|
||||
|
||||
/**
|
||||
* 设备 Mapper
|
||||
@@ -64,6 +74,16 @@ public class DeviceDataHandler {
|
||||
*/
|
||||
private final DeviceSwitchMapper deviceSwitchMapper;
|
||||
|
||||
/**
|
||||
* 用户 Mapper
|
||||
*/
|
||||
private final AquUserMapper aquUserMapper;
|
||||
|
||||
/**
|
||||
* 塘口 Mapper
|
||||
*/
|
||||
private final PondMapper pondMapper;
|
||||
|
||||
/**
|
||||
* VMS 语音通知服务
|
||||
*/
|
||||
@@ -82,15 +102,7 @@ public class DeviceDataHandler {
|
||||
*/
|
||||
private static final int CALL_STATUS_NO_CALL = 0;
|
||||
|
||||
/**
|
||||
* 告警状态:未处理
|
||||
*/
|
||||
private static final int ALARM_STATUS_PENDING = 0;
|
||||
|
||||
/**
|
||||
* 告警状态:已恢复
|
||||
*/
|
||||
private static final int ALARM_STATUS_RECOVERED = 2;
|
||||
|
||||
/**
|
||||
* 告警间隔时间(分钟)- 同一设备同一类型告警在此时间内不重复发送
|
||||
@@ -98,7 +110,14 @@ public class DeviceDataHandler {
|
||||
private static final int ALARM_NOTIFICATION_INTERVAL = 30;
|
||||
|
||||
/**
|
||||
* 告警类型常量
|
||||
* 告警类型常量(对应 warn_type 字段)
|
||||
*/
|
||||
private static final int WARN_TYPE_DISSOLVED_OXYGEN = 2; // 溶解氧
|
||||
private static final int WARN_TYPE_TEMPERATURE = 2; // 温度
|
||||
private static final int WARN_TYPE_BATTERY = 2; // 电池电量
|
||||
|
||||
/**
|
||||
* 告警类型名称
|
||||
*/
|
||||
private static final String ALARM_TYPE_DISSOLVED_OXYGEN = "溶解氧";
|
||||
private static final String ALARM_TYPE_TEMPERATURE = "温度";
|
||||
@@ -111,6 +130,11 @@ public class DeviceDataHandler {
|
||||
private static final int ALARM_LEVEL_IMPORTANT = 2; // 重要
|
||||
private static final int ALARM_LEVEL_URGENT = 3; // 紧急
|
||||
|
||||
/**
|
||||
* 消息未读状态
|
||||
*/
|
||||
private static final int MSG_IS_READ_NO = 0;
|
||||
|
||||
/**
|
||||
* 处理设备属性上报数据
|
||||
*
|
||||
@@ -190,10 +214,10 @@ public class DeviceDataHandler {
|
||||
}
|
||||
}
|
||||
//
|
||||
// // 检查是否触发报警
|
||||
// if (params != null && sensorData != null) {
|
||||
// checkAndTriggerAlarm(productKey, deviceName, sensorData);
|
||||
// }
|
||||
// 检查是否触发报警
|
||||
if (params != null && sensorData != null) {
|
||||
checkAndTriggerAlarm(productKey, deviceName, sensorData);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理设备属性数据失败", e);
|
||||
@@ -313,9 +337,20 @@ public class DeviceDataHandler {
|
||||
}
|
||||
|
||||
Long deviceId = device.getId();
|
||||
Long userId = device.getUserId();
|
||||
|
||||
// 查询塘口名称
|
||||
String pondName = null;
|
||||
if (device.getPondId() != null) {
|
||||
Pond pond = pondMapper.selectById(device.getPondId());
|
||||
if (pond != null) {
|
||||
pondName = pond.getPondName();
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder alarmMessage = new StringBuilder();
|
||||
boolean hasAlarm = false;
|
||||
java.util.List<AquAlarmHistory> alarmList = new java.util.ArrayList<>();
|
||||
java.util.List<MessageWarn> warnList = new java.util.ArrayList<>();
|
||||
|
||||
// 1. 检查溶解氧(使用设备配置的阈值)
|
||||
if (sensorData.getDissolvedOxygen() != null && device.getOxyWarnCallOpen() != null && device.getOxyWarnCallOpen() == 1) {
|
||||
@@ -323,13 +358,12 @@ public class DeviceDataHandler {
|
||||
Double oxyWarnLower = device.getOxyWarnLower();
|
||||
|
||||
if (oxyWarnLower != null && dissolvedOxygen < oxyWarnLower) {
|
||||
String content = String.format("溶解氧过低: %.2f mg/L (最低: %.2f)",
|
||||
String message = String.format("溶解氧过低: %.2f mg/L (最低: %.2f)",
|
||||
dissolvedOxygen, oxyWarnLower);
|
||||
alarmMessage.append(content).append("; ");
|
||||
alarmMessage.append(message).append("; ");
|
||||
|
||||
AquAlarmHistory alarm = createAlarmHistory(deviceId, deviceName, ALARM_TYPE_DISSOLVED_OXYGEN,
|
||||
ALARM_LEVEL_URGENT, content, dissolvedOxygen, oxyWarnLower);
|
||||
alarmList.add(alarm);
|
||||
warnList.add(createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_DISSOLVED_OXYGEN, WARN_TYPE_DISSOLVED_OXYGEN, message));
|
||||
hasAlarm = true;
|
||||
}
|
||||
}
|
||||
@@ -341,22 +375,20 @@ public class DeviceDataHandler {
|
||||
Double tempWarnUpper = device.getTempWarnUpper();
|
||||
|
||||
if (tempWarnLower != null && temperature < tempWarnLower) {
|
||||
String content = String.format("水温过低: %.2f °C (最低: %.2f)",
|
||||
String message = String.format("水温过低: %.2f °C (最低: %.2f)",
|
||||
temperature, tempWarnLower);
|
||||
alarmMessage.append(content).append("; ");
|
||||
alarmMessage.append(message).append("; ");
|
||||
|
||||
AquAlarmHistory alarm = createAlarmHistory(deviceId, deviceName, ALARM_TYPE_TEMPERATURE,
|
||||
ALARM_LEVEL_IMPORTANT, content, temperature, tempWarnLower);
|
||||
alarmList.add(alarm);
|
||||
warnList.add(createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_TEMPERATURE, WARN_TYPE_TEMPERATURE, message));
|
||||
hasAlarm = true;
|
||||
} else if (tempWarnUpper != null && temperature > tempWarnUpper) {
|
||||
String content = String.format("水温过高: %.2f °C (最高: %.2f)",
|
||||
String message = String.format("水温过高: %.2f °C (最高: %.2f)",
|
||||
temperature, tempWarnUpper);
|
||||
alarmMessage.append(content).append("; ");
|
||||
alarmMessage.append(message).append("; ");
|
||||
|
||||
AquAlarmHistory alarm = createAlarmHistory(deviceId, deviceName, ALARM_TYPE_TEMPERATURE,
|
||||
ALARM_LEVEL_IMPORTANT, content, temperature, tempWarnUpper);
|
||||
alarmList.add(alarm);
|
||||
warnList.add(createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_TEMPERATURE, WARN_TYPE_TEMPERATURE, message));
|
||||
hasAlarm = true;
|
||||
}
|
||||
}
|
||||
@@ -367,36 +399,27 @@ public class DeviceDataHandler {
|
||||
Double batteryWarnLower = device.getBatteryWarnLower() != null ? device.getBatteryWarnLower().doubleValue() : null;
|
||||
|
||||
if (batteryWarnLower != null && battery < batteryWarnLower) {
|
||||
String content = String.format("电池电量低: %.2f%% (最低: %.2f%%)",
|
||||
String message = String.format("电池电量低: %.2f%% (最低: %.2f%%)",
|
||||
battery, batteryWarnLower);
|
||||
alarmMessage.append(content).append("; ");
|
||||
alarmMessage.append(message).append("; ");
|
||||
|
||||
AquAlarmHistory alarm = createAlarmHistory(deviceId, deviceName, ALARM_TYPE_BATTERY,
|
||||
ALARM_LEVEL_NORMAL, content, battery, batteryWarnLower);
|
||||
alarmList.add(alarm);
|
||||
warnList.add(createMessageWarn(deviceId, userId, pondName,
|
||||
ALARM_TYPE_BATTERY, WARN_TYPE_BATTERY, message));
|
||||
hasAlarm = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有告警,保存告警历史并触发通知
|
||||
// 如果有告警,保存报警消息并触发通知
|
||||
if (hasAlarm) {
|
||||
log.warn("[告警] {} - {}", deviceName, alarmMessage);
|
||||
|
||||
// 保存告警历史
|
||||
for (AquAlarmHistory alarm : alarmList) {
|
||||
alarmHistoryMapper.insert(alarm);
|
||||
// 批量保存报警消息
|
||||
for (MessageWarn warn : warnList) {
|
||||
messageWarnMapper.insert(warn);
|
||||
}
|
||||
|
||||
// 触发通知(只对紧急和重要告警发送通知)
|
||||
boolean shouldNotify = alarmList.stream()
|
||||
.anyMatch(alarm -> alarm.getAlarmLevel() >= ALARM_LEVEL_IMPORTANT);
|
||||
|
||||
if (shouldNotify) {
|
||||
triggerAlarmNotification(device, alarmMessage.toString(), sensorData, alarmList);
|
||||
}
|
||||
} else {
|
||||
// 没有告警,检查是否有未恢复的告警需要标记为已恢复
|
||||
checkAndRecoverAlarms(deviceId, device, sensorData);
|
||||
// 触发电话通知
|
||||
triggerAlarmNotification(device, alarmMessage.toString(), sensorData, warnList);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
@@ -411,28 +434,28 @@ public class DeviceDataHandler {
|
||||
* @param device 设备信息
|
||||
* @param alarmMessage 告警信息
|
||||
* @param sensorData 传感器数据
|
||||
* @param alarmList 告警列表
|
||||
* @param warnList 报警消息列表
|
||||
*/
|
||||
private void triggerAlarmNotification(Device device, String alarmMessage,
|
||||
DeviceSensorData sensorData, java.util.List<AquAlarmHistory> alarmList) {
|
||||
DeviceSensorData sensorData, java.util.List<MessageWarn> warnList) {
|
||||
try {
|
||||
Long deviceId = device.getId();
|
||||
String deviceName = device.getDeviceName();
|
||||
|
||||
// 检查设备的免打扰设置
|
||||
boolean shouldSkip = false;
|
||||
for (AquAlarmHistory alarm : alarmList) {
|
||||
if (ALARM_TYPE_DISSOLVED_OXYGEN.equals(alarm.getAlarmType()) &&
|
||||
for (MessageWarn warn : warnList) {
|
||||
if (WARN_TYPE_DISSOLVED_OXYGEN == warn.getWarnType() &&
|
||||
device.getOxyWarnCallNoDis() != null && device.getOxyWarnCallNoDis() == 1) {
|
||||
shouldSkip = true;
|
||||
break;
|
||||
}
|
||||
if (ALARM_TYPE_TEMPERATURE.equals(alarm.getAlarmType()) &&
|
||||
if (WARN_TYPE_TEMPERATURE == warn.getWarnType() &&
|
||||
device.getTempWarnCallNoDis() != null && device.getTempWarnCallNoDis() == 1) {
|
||||
shouldSkip = true;
|
||||
break;
|
||||
}
|
||||
if (ALARM_TYPE_BATTERY.equals(alarm.getAlarmType()) &&
|
||||
if (WARN_TYPE_BATTERY == warn.getWarnType() &&
|
||||
device.getBatteryWarnCallNoDis() != null && device.getBatteryWarnCallNoDis() == 1) {
|
||||
shouldSkip = true;
|
||||
break;
|
||||
@@ -454,23 +477,21 @@ public class DeviceDataHandler {
|
||||
);
|
||||
|
||||
if (recentNoticeCount > 0) {
|
||||
log.debug("[告警] {} - {}分钟内已通知", deviceName, ALARM_NOTIFICATION_INTERVAL);
|
||||
|
||||
// 更新告警历史为已通知,但不发送实际通知
|
||||
for (AquAlarmHistory alarm : alarmList) {
|
||||
alarm.setNotified(1);
|
||||
alarm.setUpdatedTime(LocalDateTime.now());
|
||||
alarmHistoryMapper.updateById(alarm);
|
||||
}
|
||||
log.debug("[告警] {} - {}分钟内已通知,跳过电话通知", deviceName, ALARM_NOTIFICATION_INTERVAL);
|
||||
return;
|
||||
}
|
||||
|
||||
// 从设备信息获取用户信息
|
||||
Long userId = device.getUserId();
|
||||
// TODO: 从用户表查询手机号
|
||||
String mobilePhone = sensorData.getMobilePhone(); // 临时使用传感器数据中的手机号
|
||||
// TODO: 从塘口表查询塘口名称
|
||||
String pondName = deviceName; // 临时使用设备名
|
||||
// 从 aqu_user 表查询手机号
|
||||
String mobilePhone = null;
|
||||
if (userId != null) {
|
||||
AquUser aquUser = aquUserMapper.selectById(userId);
|
||||
if (aquUser != null) {
|
||||
mobilePhone = aquUser.getMobilePhone();
|
||||
}
|
||||
}
|
||||
String pondName = warnList.isEmpty() ? deviceName : warnList.get(0).getPondName();
|
||||
|
||||
// 验证必要信息
|
||||
if (userId == null || StrUtil.isBlank(mobilePhone)) {
|
||||
@@ -486,22 +507,22 @@ public class DeviceDataHandler {
|
||||
callNotice.setPondName(pondName);
|
||||
callNotice.setCallTime(LocalDateTime.now());
|
||||
callNotice.setCallStatus(CALL_STATUS_NO_CALL);
|
||||
callNotice.setCreatedTime(LocalDateTime.now());
|
||||
callNotice.setUpdatedTime(LocalDateTime.now());
|
||||
|
||||
// 保存到数据库
|
||||
warnCallNoticeMapper.insert(callNotice);
|
||||
|
||||
// 更新告警历史中的通知ID
|
||||
for (AquAlarmHistory alarm : alarmList) {
|
||||
alarm.setNoticeId(callNotice.getId());
|
||||
alarm.setNotified(1);
|
||||
alarm.setUpdatedTime(LocalDateTime.now());
|
||||
alarmHistoryMapper.updateById(alarm);
|
||||
// 保存报警消息与通知记录的关联关系
|
||||
for (MessageWarn warn : warnList) {
|
||||
if (warn.getId() != null) {
|
||||
AquMapMessageWarnCallNotice mapRecord = new AquMapMessageWarnCallNotice();
|
||||
mapRecord.setMessageWarnId(warn.getId());
|
||||
mapRecord.setCallNoticeId(callNotice.getId());
|
||||
mapMessageWarnCallNoticeMapper.insert(mapRecord);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送语音通知
|
||||
sendVoiceNotification(callNotice, alarmMessage);
|
||||
sendVoiceNotification(callNotice, deviceName, alarmMessage);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[告警通知] 触发告警通知失败: {}", e.getMessage(), e);
|
||||
@@ -509,16 +530,22 @@ public class DeviceDataHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
// 发送语音通知
|
||||
* @param callNotice 通知记录
|
||||
* @param alarmMessage 告警信息
|
||||
* 发送语音通知
|
||||
*
|
||||
* @param callNotice 通知记录
|
||||
* @param deviceName 设备名称
|
||||
* @param warnMessage 告警内容
|
||||
*/
|
||||
private void sendVoiceNotification(AquWarnCallNotice callNotice, String alarmMessage) {
|
||||
private void sendVoiceNotification(AquWarnCallNotice callNotice, String deviceName, String warnMessage) {
|
||||
try {
|
||||
// 构建语音通知参数
|
||||
// 构建语音通知参数(与模板变量名保持一致)
|
||||
Map<String, String> ttsParams = new HashMap<>();
|
||||
ttsParams.put("pond_name", callNotice.getPondName() != null ? callNotice.getPondName() : "未知塘口");
|
||||
ttsParams.put("alarm_info", alarmMessage);
|
||||
ttsParams.put("pondName", callNotice.getPondName() != null ? callNotice.getPondName() : "未知塘口");
|
||||
ttsParams.put("deviceName", deviceName != null ? deviceName : "未知设备");
|
||||
String warnMessageShort = warnMessage != null && warnMessage.contains(":")
|
||||
? warnMessage.substring(0, warnMessage.indexOf(":")).trim()
|
||||
: (warnMessage != null ? warnMessage : "异常");
|
||||
ttsParams.put("warnMessage", warnMessageShort);
|
||||
|
||||
// 生成业务标识(用于回执关联)
|
||||
String outId = "ALARM_" + callNotice.getId() + "_" + System.currentTimeMillis();
|
||||
@@ -535,7 +562,6 @@ public class DeviceDataHandler {
|
||||
callNotice.setCallStatus(1); // 呼叫成功,等待结果反馈
|
||||
callNotice.setCallId(response.getCallId());
|
||||
callNotice.setOutId(outId);
|
||||
callNotice.setUpdatedTime(LocalDateTime.now());
|
||||
warnCallNoticeMapper.updateById(callNotice);
|
||||
|
||||
log.info("[告警] 呼叫成功: {}", callNotice.getMobilePhone());
|
||||
@@ -543,7 +569,6 @@ public class DeviceDataHandler {
|
||||
// 呼叫失败
|
||||
callNotice.setCallStatus(2); // 呼叫失败
|
||||
callNotice.setStatusMsg(response != null ? response.getMessage() : "呼叫失败");
|
||||
callNotice.setUpdatedTime(LocalDateTime.now());
|
||||
warnCallNoticeMapper.updateById(callNotice);
|
||||
|
||||
log.error("[告警] 呼叫失败: {} - {}", callNotice.getMobilePhone(), callNotice.getStatusMsg());
|
||||
@@ -556,7 +581,6 @@ public class DeviceDataHandler {
|
||||
try {
|
||||
callNotice.setCallStatus(2);
|
||||
callNotice.setStatusMsg("发送异常: " + e.getMessage());
|
||||
callNotice.setUpdatedTime(LocalDateTime.now());
|
||||
warnCallNoticeMapper.updateById(callNotice);
|
||||
} catch (Exception ex) {
|
||||
log.error("[告警] 更新失败: {}", ex.getMessage());
|
||||
@@ -588,34 +612,27 @@ public class DeviceDataHandler {
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建告警历史记录
|
||||
* 创建报警消息记录(写入 aqu_message_warn)
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param deviceName 设备名称
|
||||
* @param alarmType 告警类型
|
||||
* @param alarmLevel 告警级别
|
||||
* @param alarmContent 告警内容
|
||||
* @param alarmValue 告警值
|
||||
* @param thresholdValue 阈值
|
||||
* @return 告警历史对象
|
||||
* @param deviceId 设备ID
|
||||
* @param userId 用户ID
|
||||
* @param pondName 塘口名称
|
||||
* @param title 消息标题(告警类型名称)
|
||||
* @param warnType 告警类型编码
|
||||
* @param message 消息内容
|
||||
* @return MessageWarn 对象
|
||||
*/
|
||||
private AquAlarmHistory createAlarmHistory(Long deviceId, String deviceName, String alarmType,
|
||||
int alarmLevel, String alarmContent,
|
||||
Double alarmValue, Double thresholdValue) {
|
||||
AquAlarmHistory alarm = new AquAlarmHistory();
|
||||
alarm.setDeviceId(deviceId);
|
||||
alarm.setDeviceName(deviceName);
|
||||
alarm.setAlarmType(alarmType);
|
||||
alarm.setAlarmLevel(alarmLevel);
|
||||
alarm.setAlarmContent(alarmContent);
|
||||
alarm.setAlarmValue(alarmValue);
|
||||
alarm.setThresholdValue(thresholdValue);
|
||||
alarm.setAlarmStatus(ALARM_STATUS_PENDING);
|
||||
alarm.setAlarmTime(LocalDateTime.now());
|
||||
alarm.setNotified(0);
|
||||
alarm.setCreatedTime(LocalDateTime.now());
|
||||
alarm.setUpdatedTime(LocalDateTime.now());
|
||||
return alarm;
|
||||
private MessageWarn createMessageWarn(Long deviceId, Long userId, String pondName,
|
||||
String title, int warnType, String message) {
|
||||
MessageWarn warn = new MessageWarn();
|
||||
warn.setDeviceId(deviceId);
|
||||
warn.setUserId(userId);
|
||||
warn.setPondName(pondName);
|
||||
warn.setTitle(title);
|
||||
warn.setWarnType(warnType);
|
||||
warn.setMessage(message);
|
||||
warn.setIsRead(MSG_IS_READ_NO);
|
||||
return warn;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -627,64 +644,7 @@ public class DeviceDataHandler {
|
||||
* @param sensorData 传感器数据
|
||||
*/
|
||||
private void checkAndRecoverAlarms(Long deviceId, Device device, DeviceSensorData sensorData) {
|
||||
try {
|
||||
// 查询该设备所有未恢复的告警
|
||||
java.util.List<AquAlarmHistory> pendingAlarms = alarmHistoryMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AquAlarmHistory>()
|
||||
.eq(AquAlarmHistory::getDeviceId, deviceId)
|
||||
.ne(AquAlarmHistory::getAlarmStatus, ALARM_STATUS_RECOVERED)
|
||||
);
|
||||
|
||||
if (pendingAlarms.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
for (AquAlarmHistory alarm : pendingAlarms) {
|
||||
boolean recovered = false;
|
||||
|
||||
// 根据告警类型检查是否已恢复(使用设备配置的阈值)
|
||||
switch (alarm.getAlarmType()) {
|
||||
case ALARM_TYPE_DISSOLVED_OXYGEN:
|
||||
if (sensorData.getDissolvedOxygen() != null) {
|
||||
Double value = sensorData.getDissolvedOxygen();
|
||||
Double oxyWarnLower = device.getOxyWarnLower();
|
||||
recovered = oxyWarnLower != null && value >= oxyWarnLower;
|
||||
}
|
||||
break;
|
||||
|
||||
case ALARM_TYPE_TEMPERATURE:
|
||||
if (sensorData.getTemperature() != null) {
|
||||
Double value = sensorData.getTemperature();
|
||||
Double tempWarnLower = device.getTempWarnLower();
|
||||
Double tempWarnUpper = device.getTempWarnUpper();
|
||||
recovered = (tempWarnLower == null || value >= tempWarnLower) &&
|
||||
(tempWarnUpper == null || value <= tempWarnUpper);
|
||||
}
|
||||
break;
|
||||
|
||||
case ALARM_TYPE_BATTERY:
|
||||
if (sensorData.getBattery() != null) {
|
||||
Double batteryWarnLower = device.getBatteryWarnLower() != null ? device.getBatteryWarnLower().doubleValue() : null;
|
||||
recovered = batteryWarnLower != null && sensorData.getBattery() >= batteryWarnLower;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 如果告警已恢复,更新状态
|
||||
if (recovered) {
|
||||
alarm.setAlarmStatus(ALARM_STATUS_RECOVERED);
|
||||
alarm.setRecoveryTime(now);
|
||||
alarm.setUpdatedTime(now);
|
||||
alarmHistoryMapper.updateById(alarm);
|
||||
log.info("[恢复] {} - {}", alarm.getDeviceName(), alarm.getAlarmType());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("检查告警恢复失败: {}", e.getMessage(), e);
|
||||
}
|
||||
// aqu_message_warn 表为消息通知,暂无需要恢复标记逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.intc.iot.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.intc.iot.domain.AquMapMessageWarnCallNotice;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 报警消息与通知记录关联表 Mapper
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Mapper
|
||||
public interface AquMapMessageWarnCallNoticeMapper extends BaseMapper<AquMapMessageWarnCallNotice> {
|
||||
}
|
||||
@@ -5,6 +5,11 @@ import com.aliyun.mns.client.CloudAccount;
|
||||
import com.aliyun.mns.client.CloudQueue;
|
||||
import com.aliyun.mns.client.MNSClient;
|
||||
import com.aliyun.mns.model.Message;
|
||||
import com.aliyuncs.DefaultAcsClient;
|
||||
import com.aliyuncs.IAcsClient;
|
||||
import com.aliyuncs.dybaseapi.model.v20170525.QueryTokenForMnsQueueRequest;
|
||||
import com.aliyuncs.dybaseapi.model.v20170525.QueryTokenForMnsQueueResponse;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import com.intc.iot.config.AliyunIotProperties;
|
||||
import com.intc.iot.domain.VmsCallback;
|
||||
import com.intc.iot.mapper.VmsCallbackMapper;
|
||||
@@ -17,15 +22,18 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* VMS MNS 回执消费服务实现
|
||||
* 注意:直接使用 AccessKey/Secret 访问 MNS,不再依赖 dybaseapi
|
||||
* 使用 STS 临时凭证(QueryTokenForMnsQueue)动态获取 Token,对应 C# 的 MNSHandle 实现逻辑
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@@ -39,66 +47,72 @@ public class VmsMnsConsumerServiceImpl implements VmsMnsConsumerService {
|
||||
private final AliyunIotProperties aliyunIotProperties;
|
||||
private final VmsCallbackMapper vmsCallbackMapper;
|
||||
|
||||
/**
|
||||
* MNS 配置常量
|
||||
* 注意:这些值需要根据你的实际阿里云账户配置
|
||||
*/
|
||||
private static final String VMS_MNS_QUEUE_NAME = "Alicom-Queue-1572610294777992-VoiceReport";
|
||||
private static final String MNS_ACCOUNT_ENDPOINT = "https://1943695596114318.mns.cn-hangzhou.aliyuncs.com/";
|
||||
/** MNS 消息类型(固定值) */
|
||||
private static final String VMS_MNS_MESSAGE_TYPE = "VoiceReport";
|
||||
|
||||
/** MNS 默认线程数 */
|
||||
private static final int MNS_THREAD_COUNT = 2;
|
||||
private static final int POLL_WAIT_SECONDS = 30;
|
||||
|
||||
/**
|
||||
* 应用标识(用于过滤 out_id)
|
||||
*/
|
||||
private String appName;
|
||||
/** Token 过期缓冲时间(120秒),小于此值则重新获取,防止服务器时间差异 */
|
||||
private static final long TOKEN_BUFFER_SECONDS = 120;
|
||||
|
||||
/**
|
||||
* MNS 客户端
|
||||
*/
|
||||
/** 线程安全的 Token 缓存 */
|
||||
private final ReentrantLock tokenLock = new ReentrantLock();
|
||||
private QueryTokenForMnsQueueResponse.MessageTokenDTO cachedToken;
|
||||
private CloudQueue cachedQueue;
|
||||
|
||||
/** ACS 客户端(用于获取 STS Token) */
|
||||
private IAcsClient acsClient;
|
||||
|
||||
/** MNS 客户端(临时凭证建立) */
|
||||
private MNSClient mnsClient;
|
||||
|
||||
/**
|
||||
* 线程池
|
||||
*/
|
||||
/** 线程池 */
|
||||
private ExecutorService executorService;
|
||||
|
||||
/**
|
||||
* 运行状态标志
|
||||
*/
|
||||
/** 运行状态标志 */
|
||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
if (running.compareAndSet(false, true)) {
|
||||
log.info("启动 VMS MNS 回执消费服务...");
|
||||
log.info("启动 VMS MNS 回执消费服务(STS 临时凭证模式)...");
|
||||
|
||||
// 从配置读取应用名称
|
||||
appName = aliyunIotProperties.getAppKey() != null && !aliyunIotProperties.getAppKey().isEmpty()
|
||||
? aliyunIotProperties.getAppKey()
|
||||
: "fishery-backend";
|
||||
log.info("VMS 回执过滤应用名称: {}", appName);
|
||||
AliyunIotProperties.VmsMnsConfig vmsMns = aliyunIotProperties.getVms();
|
||||
String mnsEndpoint = vmsMns.getEndpoint();
|
||||
String mnsQueueName = vmsMns.getQueueName();
|
||||
|
||||
// 直接使用 AccessKey/Secret 创建 MNS 客户端
|
||||
CloudAccount account = new CloudAccount(
|
||||
aliyunIotProperties.getAccessKeyId(),
|
||||
aliyunIotProperties.getAccessKeySecret(),
|
||||
MNS_ACCOUNT_ENDPOINT
|
||||
);
|
||||
mnsClient = account.getMNSClient();
|
||||
log.info("MNS 客户端初始化成功,Endpoint: {}", MNS_ACCOUNT_ENDPOINT);
|
||||
if (mnsEndpoint == null || mnsEndpoint.isEmpty()) {
|
||||
log.error("VMS MNS Endpoint 未配置,请在 aliyun.living-iot.vms.endpoint 中配置");
|
||||
running.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用主 AK 创建 ACS 客户端(用于调用 QueryTokenForMnsQueue 接口)
|
||||
String akId = vmsMns.getAccessKeyId() != null && !vmsMns.getAccessKeyId().isEmpty()
|
||||
? vmsMns.getAccessKeyId() : aliyunIotProperties.getAccessKeyId();
|
||||
String akSecret = vmsMns.getAccessKeySecret() != null && !vmsMns.getAccessKeySecret().isEmpty()
|
||||
? vmsMns.getAccessKeySecret() : aliyunIotProperties.getAccessKeySecret();
|
||||
String regionId = aliyunIotProperties.getRegionId() != null
|
||||
? aliyunIotProperties.getRegionId() : "cn-hangzhou";
|
||||
|
||||
DefaultProfile profile = DefaultProfile.getProfile(regionId, akId, akSecret);
|
||||
DefaultProfile.addEndpoint(regionId, regionId, "Dybaseapi", "dybaseapi.aliyuncs.com");
|
||||
acsClient = new DefaultAcsClient(profile);
|
||||
log.info("ACS 客户端创建成功,region: {}", regionId);
|
||||
|
||||
// 创建线程池
|
||||
executorService = Executors.newFixedThreadPool(MNS_THREAD_COUNT);
|
||||
|
||||
// 启动消费线程(对应 C# 的 MNSHandle 循环)
|
||||
// 启动消费线程
|
||||
for (int i = 0; i < MNS_THREAD_COUNT; i++) {
|
||||
executorService.submit(this::mnsConsumeLoop);
|
||||
executorService.submit(() -> mnsConsumeLoop(mnsEndpoint, mnsQueueName));
|
||||
}
|
||||
|
||||
log.info("VMS MNS 回执消费服务启动成功,线程数: {}", MNS_THREAD_COUNT);
|
||||
log.info("VMS MNS 回扇消费服务启动成功,线程数: {}", MNS_THREAD_COUNT);
|
||||
} else {
|
||||
log.warn("VMS MNS 回执消费服务已在运行中,无需重复启动");
|
||||
log.warn("VMS MNS 回扇消费服务已在运行中,无需重复启动");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,9 +133,9 @@ public class VmsMnsConsumerServiceImpl implements VmsMnsConsumerService {
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭 MNS 客户端
|
||||
if (mnsClient != null) {
|
||||
mnsClient.close();
|
||||
mnsClient = null;
|
||||
}
|
||||
|
||||
log.info("VMS MNS 回执消费服务已停止");
|
||||
@@ -134,23 +148,93 @@ public class VmsMnsConsumerServiceImpl implements VmsMnsConsumerService {
|
||||
}
|
||||
|
||||
/**
|
||||
* MNS 消费循环
|
||||
* 获取或刷新 STS Token 并返回对应的 CloudQueue
|
||||
* 对应 C# 的 Token 过期检查 + 自动刷新逻辑
|
||||
*
|
||||
* @param mnsEndpoint MNS endpoint
|
||||
* @param queueName 队列名称
|
||||
* @return 可用的 CloudQueue
|
||||
*/
|
||||
private void mnsConsumeLoop() {
|
||||
log.info("MNS 消费线程 {} 启动", Thread.currentThread().getName());
|
||||
|
||||
CloudQueue queue = null;
|
||||
private CloudQueue getOrRefreshQueue(String mnsEndpoint, String queueName) throws Exception {
|
||||
tokenLock.lock();
|
||||
try {
|
||||
// 获取队列引用
|
||||
queue = mnsClient.getQueueRef(VMS_MNS_QUEUE_NAME);
|
||||
log.info("获取 MNS Queue 成功: {}", VMS_MNS_QUEUE_NAME);
|
||||
} catch (Exception e) {
|
||||
log.error("获取 MNS Queue 失败: {}", e.getMessage(), e);
|
||||
return;
|
||||
// 检查 Token 是否将要过期(小于缓冲时间则重新获取)
|
||||
if (cachedToken != null && cachedQueue != null) {
|
||||
long remainSeconds = getTokenRemainSeconds(cachedToken.getExpireTime());
|
||||
if (remainSeconds >= TOKEN_BUFFER_SECONDS) {
|
||||
return cachedQueue;
|
||||
}
|
||||
log.info("STS Token 将于 {}s 内过期,重新获取...", remainSeconds);
|
||||
} else {
|
||||
log.info("首次获取 MNS STS Token...");
|
||||
}
|
||||
|
||||
// 调用 QueryTokenForMnsQueue 获取临时凭证
|
||||
QueryTokenForMnsQueueRequest request = new QueryTokenForMnsQueueRequest();
|
||||
request.setMessageType(VMS_MNS_MESSAGE_TYPE);
|
||||
QueryTokenForMnsQueueResponse response = acsClient.getAcsResponse(request);
|
||||
QueryTokenForMnsQueueResponse.MessageTokenDTO token =
|
||||
response.getMessageTokenDTO();
|
||||
|
||||
log.info("STS Token 获取成功,过期时间: {}", token.getExpireTime());
|
||||
|
||||
// 关闭旧的 MNS 客户端
|
||||
if (mnsClient != null) {
|
||||
mnsClient.close();
|
||||
}
|
||||
|
||||
// 使用 STS 临时凭证创建新的 MNS 客户端
|
||||
CloudAccount account = new CloudAccount(
|
||||
token.getAccessKeyId(),
|
||||
token.getAccessKeySecret(),
|
||||
mnsEndpoint,
|
||||
token.getSecurityToken()
|
||||
);
|
||||
mnsClient = account.getMNSClient();
|
||||
cachedQueue = mnsClient.getQueueRef(queueName);
|
||||
cachedToken = token;
|
||||
|
||||
log.info("MNS 客户端刷新成功,Queue: {}", queueName);
|
||||
return cachedQueue;
|
||||
|
||||
} finally {
|
||||
tokenLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 Token 剩余有效秒数
|
||||
* SDK 返回的 expireTime 格式为 "yyyy-MM-dd HH:mm:ss",时区为 UTC+8
|
||||
*/
|
||||
private long getTokenRemainSeconds(String expireTime) {
|
||||
try {
|
||||
java.time.LocalDateTime expire = java.time.LocalDateTime.parse(
|
||||
expireTime,
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
);
|
||||
// SDK 返回的时间为 UTC+8
|
||||
long expireEpoch = expire.toEpochSecond(java.time.ZoneOffset.ofHours(8));
|
||||
return expireEpoch - System.currentTimeMillis() / 1000;
|
||||
} catch (Exception e) {
|
||||
log.warn("解析 Token 过期时间失败: {}", expireTime);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MNS 消费循环(每个线程执行)
|
||||
*
|
||||
* @param mnsEndpoint MNS endpoint
|
||||
* @param queueName 队列名称
|
||||
*/
|
||||
private void mnsConsumeLoop(String mnsEndpoint, String queueName) {
|
||||
log.info("MNS 消费线程 {} 启动", Thread.currentThread().getName());
|
||||
|
||||
while (running.get()) {
|
||||
try {
|
||||
// 获取(或刷新)有效的 CloudQueue
|
||||
CloudQueue queue = getOrRefreshQueue(mnsEndpoint, queueName);
|
||||
|
||||
// 批量接收消息
|
||||
List<Message> messages = queue.batchPopMessage(16, POLL_WAIT_SECONDS);
|
||||
|
||||
@@ -160,7 +244,6 @@ public class VmsMnsConsumerServiceImpl implements VmsMnsConsumerService {
|
||||
|
||||
log.debug("接收到 {} 条 VMS 回执消息", messages.size());
|
||||
|
||||
// 处理消息
|
||||
for (Message message : messages) {
|
||||
try {
|
||||
processMessage(message, queue);
|
||||
@@ -170,14 +253,28 @@ public class VmsMnsConsumerServiceImpl implements VmsMnsConsumerService {
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// 检查是否为中断异常
|
||||
if (e instanceof InterruptedException || e.getCause() instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.info("MNS 消费线程被中断");
|
||||
break;
|
||||
}
|
||||
|
||||
log.error("MNS 消费循环异常: {}", e.getMessage(), e);
|
||||
|
||||
// MessageNotExist 表示队列为空,属于正常现象,静默处理(使用长轮询时不应出现,出现时不报错)
|
||||
String errMsg = e.getMessage();
|
||||
if (errMsg != null && errMsg.contains("MessageNotExist")) {
|
||||
log.debug("MNS 队列暂无消息");
|
||||
continue;
|
||||
}
|
||||
|
||||
log.error("MNS 消费循环异常: {}", errMsg, e);
|
||||
// 异常后清除缓存,下次循环重新获取 Token
|
||||
tokenLock.lock();
|
||||
try {
|
||||
cachedToken = null;
|
||||
cachedQueue = null;
|
||||
} finally {
|
||||
tokenLock.unlock();
|
||||
}
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(5);
|
||||
} catch (InterruptedException ie) {
|
||||
@@ -192,64 +289,59 @@ public class VmsMnsConsumerServiceImpl implements VmsMnsConsumerService {
|
||||
|
||||
/**
|
||||
* 处理单条消息
|
||||
*
|
||||
* @param message MNS 消息
|
||||
* @param queue MNS 队列
|
||||
* 注意:MNS 回执消息体可能是明文 JSON,也可能是 Base64 编码的 JSON,需要尝试两种解析方式
|
||||
*/
|
||||
private void processMessage(Message message, CloudQueue queue) {
|
||||
try {
|
||||
// 解码消息体(对应 C# 的 Base64 解码)
|
||||
String body = message.getMessageBodyAsString();
|
||||
byte[] decoded = Base64.getDecoder().decode(body);
|
||||
String jsonStr = new String(decoded, StandardCharsets.UTF_8);
|
||||
|
||||
// 尝试 Base64 解码,如果失败则直接当明文 JSON 处理
|
||||
String jsonStr;
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(body.trim());
|
||||
jsonStr = new String(decoded, StandardCharsets.UTF_8);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// 消息体本身就是 JSON 明文,无需 Base64 解码
|
||||
jsonStr = body;
|
||||
}
|
||||
|
||||
log.debug("收到 VMS 回执消息: {}", jsonStr);
|
||||
|
||||
// 解析为实体(对应 C# 的 JSON.Deserialize<AliVmsCallBack>)
|
||||
Map<String, Object> callbackMap = JSONUtil.toBean(jsonStr, Map.class);
|
||||
String outId = (String) callbackMap.get("out_id");
|
||||
|
||||
// 过滤不属于当前系统的消息(对应 C# 的 out_id 判断)
|
||||
if (outId == null || outId.isEmpty()) {
|
||||
log.debug("跳过无效 out_id 的消息");
|
||||
queue.deleteMessage(message.getReceiptHandle());
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证 out_id 是否属于本系统(对应 C# 的 out_id.Equals(DefineValue.AppName))
|
||||
if (!outId.equals(appName)) {
|
||||
log.debug("收到语音通知回执不属于当前系统, outId={}, 期望={}", outId, appName);
|
||||
queue.deleteMessage(message.getReceiptHandle());
|
||||
// 验证 out_id 是否属于本系统(out_id 格式为 "ALARM_{callNoticeId}_{timestamp}")
|
||||
if (!outId.startsWith("ALARM_")) {
|
||||
log.debug("收到语音通知回执不属于当前系统, outId={}", outId);
|
||||
// 与 C# 一致:不属于本系统的消息不删除,保留在队列
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建实体
|
||||
VmsCallback vmsCallback = buildVmsCallback(callbackMap);
|
||||
|
||||
// 入库(对应 C# 的 CacheData.sListVmsCallback.Add)
|
||||
try {
|
||||
vmsCallbackMapper.insert(vmsCallback);
|
||||
log.info("VMS 回执入库成功 - CallId: {}, StatusCode: {}", vmsCallback.getCallId(), vmsCallback.getStatusCode());
|
||||
|
||||
// 成功处理后删除消息
|
||||
queue.deleteMessage(message.getReceiptHandle());
|
||||
} catch (org.springframework.dao.DuplicateKeyException e) {
|
||||
log.warn("VMS 回执已存在,跳过 - CallId: {}", vmsCallback.getCallId());
|
||||
// 重复数据也删除消息
|
||||
queue.deleteMessage(message.getReceiptHandle());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 VMS 回执消息异常: {}", e.getMessage(), e);
|
||||
// 注意:不删除消息,让它重新回到队列,等待下次处理
|
||||
// 不删除消息,等待重试
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 VmsCallback 实体
|
||||
*
|
||||
* @param map 回执 Map
|
||||
* @return VmsCallback
|
||||
*/
|
||||
private VmsCallback buildVmsCallback(Map<String, Object> map) {
|
||||
VmsCallback callback = new VmsCallback();
|
||||
@@ -260,56 +352,29 @@ public class VmsMnsConsumerServiceImpl implements VmsMnsConsumerService {
|
||||
callback.setHangupDirection((String) map.get("hangup_direction"));
|
||||
callback.setCaller((String) map.get("caller"));
|
||||
callback.setVoiceType((String) map.get("voice_type"));
|
||||
|
||||
// 时间戳字段
|
||||
callback.setOriginateTime(getLongValue(map, "originate_time"));
|
||||
callback.setStartTime(getLongValue(map, "start_time"));
|
||||
callback.setEndTime(getLongValue(map, "end_time"));
|
||||
|
||||
// 整数字段
|
||||
callback.setRingTime(getIntValue(map, "ring_time"));
|
||||
callback.setDuration(getIntValue(map, "duration"));
|
||||
|
||||
callback.setTollType((String) map.get("toll_type"));
|
||||
callback.setProcessed(0); // 初始为未处理
|
||||
callback.setProcessed(0);
|
||||
callback.setCreatedTime(LocalDateTime.now());
|
||||
|
||||
return callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Map 中获取 Long 值
|
||||
*/
|
||||
private Long getLongValue(Map<String, Object> map, String key) {
|
||||
Object value = map.get(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).longValue();
|
||||
}
|
||||
try {
|
||||
return Long.parseLong(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) return ((Number) value).longValue();
|
||||
try { return Long.parseLong(value.toString()); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Map 中获取 Integer 值
|
||||
*/
|
||||
private Integer getIntValue(Map<String, Object> map, String key) {
|
||||
Object value = map.get(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
if (value == null) return null;
|
||||
if (value instanceof Number) return ((Number) value).intValue();
|
||||
try { return Integer.parseInt(value.toString()); } catch (NumberFormatException e) { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ public class VmsNoticeServiceImpl implements VmsNoticeService {
|
||||
private static final String CALL_SHOW_NUMBER = "0571000013978";
|
||||
|
||||
/** 默认 TTS 模板编码(当前取 C# 版本中的正式模板编码,可按环境调整) */
|
||||
private static final String TTS_CODE = "TTS_299550017";
|
||||
private static final String TTS_CODE = "TTS_328610196";
|
||||
|
||||
private final AliyunIotProperties aliyunIotProperties;
|
||||
|
||||
|
||||
/**
|
||||
* VMS 客户端单例,避免重复创建
|
||||
*/
|
||||
@@ -68,7 +68,7 @@ public class VmsNoticeServiceImpl implements VmsNoticeService {
|
||||
request.setSysDomain("dyvmsapi.aliyuncs.com");
|
||||
request.setSysVersion("2017-05-25");
|
||||
request.setSysAction("SingleCallByTts");
|
||||
|
||||
|
||||
// 设置请求参数
|
||||
request.putQueryParameter("CalledNumber", phoneNum);
|
||||
request.putQueryParameter("CalledShowNumber", CALL_SHOW_NUMBER);
|
||||
@@ -83,7 +83,7 @@ public class VmsNoticeServiceImpl implements VmsNoticeService {
|
||||
|
||||
try {
|
||||
CommonResponse response = client.getCommonResponse(request);
|
||||
|
||||
|
||||
if (response == null || response.getData() == null) {
|
||||
log.error("[VmsNoticeService] 语音呼叫请求异常, response 为空");
|
||||
VmsNoticeResponse result = new VmsNoticeResponse();
|
||||
@@ -95,7 +95,7 @@ public class VmsNoticeServiceImpl implements VmsNoticeService {
|
||||
// 解析响应
|
||||
String responseData = response.getData();
|
||||
cn.hutool.json.JSONObject jsonResponse = JSONUtil.parseObj(responseData);
|
||||
|
||||
|
||||
String code = jsonResponse.getStr("Code");
|
||||
String message = jsonResponse.getStr("Message");
|
||||
String callId = jsonResponse.getStr("CallId");
|
||||
@@ -107,7 +107,7 @@ public class VmsNoticeServiceImpl implements VmsNoticeService {
|
||||
result.setSuccess("OK".equalsIgnoreCase(code));
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
log.warn("[VmsNoticeService] 语音呼叫失败, phoneNum={}, code={}, message={}, callId={}",
|
||||
log.warn("[VmsNoticeService] 语音呼叫失败, phoneNum={}, code={}, message={}, callId={}",
|
||||
phoneNum, code, message, callId);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ import java.util.List;
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean({AquWarnCallNoticeMapper.class, VmsCallbackMapper.class})
|
||||
@Slf4j
|
||||
public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
|
||||
@@ -61,17 +60,44 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
return false;
|
||||
}
|
||||
|
||||
log.debug("[告警通知] 处理回执信息 - 呼叫ID: {}, 状态码: {}", callback.getCallId(), callback.getStatusCode());
|
||||
log.debug("[告警通知] 处理回执信息 - 呼叫ID: {}, outId: {}, 状态码: {}",
|
||||
callback.getCallId(), callback.getOutId(), callback.getStatusCode());
|
||||
|
||||
// 根据 CallId 查找对应的通知记录
|
||||
AquWarnCallNotice callNotice = warnCallNoticeMapper.selectOne(
|
||||
new LambdaQueryWrapper<AquWarnCallNotice>()
|
||||
.eq(AquWarnCallNotice::getCallId, callback.getCallId())
|
||||
.eq(AquWarnCallNotice::getCallStatus, CALL_STATUS_CALL_SUCCESS) // 只处理呼叫成功等待反馈的记录
|
||||
);
|
||||
// 优先从 outId(格式:ALARM_{callNoticeId}_{timestamp})中解析主键直接查询
|
||||
// 若解析失败则回退到 callId 匹配
|
||||
AquWarnCallNotice callNotice = null;
|
||||
String outId = callback.getOutId();
|
||||
if (outId != null && outId.startsWith("ALARM_")) {
|
||||
try {
|
||||
// outId 格式:ALARM_{id}_{timestamp},取第二段
|
||||
String[] parts = outId.split("_");
|
||||
if (parts.length >= 2) {
|
||||
Long noticeId = Long.parseLong(parts[1]);
|
||||
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);
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("[告警通知] outId 解析失败,回退到 callId 匹配 - outId: {}", outId);
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:通过 callId 匹配
|
||||
if (callNotice == null) {
|
||||
callNotice = warnCallNoticeMapper.selectOne(
|
||||
new LambdaQueryWrapper<AquWarnCallNotice>()
|
||||
.eq(AquWarnCallNotice::getCallId, callback.getCallId())
|
||||
.eq(AquWarnCallNotice::getCallStatus, CALL_STATUS_CALL_SUCCESS)
|
||||
);
|
||||
}
|
||||
|
||||
if (callNotice == null) {
|
||||
log.warn("[告警通知] 未找到对应的通知记录 - 呼叫ID: {}", callback.getCallId());
|
||||
log.warn("[告警通知] 未找到对应的通知记录 - 呼叫ID: {}, outId: {}", callback.getCallId(), outId);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -183,7 +209,6 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
// 更新状态为回执失败
|
||||
notice.setCallStatus(CALL_STATUS_CALLBACK_FAIL);
|
||||
notice.setStatusMsg("回执超时,未在规定时间内收到回执");
|
||||
notice.setUpdatedTime(now);
|
||||
warnCallNoticeMapper.updateById(notice);
|
||||
|
||||
cleanedCount++;
|
||||
@@ -222,7 +247,6 @@ public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
callNotice.setStatusMsg(callback.getStatusMsg());
|
||||
callNotice.setOutId(callback.getOutId());
|
||||
callNotice.setTollType(callback.getTollType());
|
||||
callNotice.setUpdatedTime(now);
|
||||
|
||||
// 根据状态码判断通话结果(对应 C# 的状态码判断逻辑)
|
||||
if (VMS_STATUS_CODE_SUCCESS.equals(callback.getStatusCode()) ||
|
||||
|
||||
@@ -15,7 +15,6 @@ import org.springframework.stereotype.Component;
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(WarnCallNoticeService.class)
|
||||
@Slf4j
|
||||
public class VmsCallbackProcessTask {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user