fix: 物联网平台,amqp数据接入并插入TD数据库相关逻辑编码。
This commit is contained in:
@@ -108,6 +108,12 @@
|
||||
<groupId>com.intc</groupId>
|
||||
<artifactId>intc-workflow</artifactId>
|
||||
</dependency>
|
||||
<!-- 物联网模块 -->
|
||||
<dependency>
|
||||
<groupId>com.intc</groupId>
|
||||
<artifactId>intc-iot</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.codecentric</groupId>
|
||||
|
||||
32
intc-admin/src/main/resources/alarm-config-example.yml
Normal file
32
intc-admin/src/main/resources/alarm-config-example.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
# ==================== 告警配置示例 ====================
|
||||
# 将以下配置添加到 application.yml 或 application-dev.yml 中
|
||||
|
||||
# 告警阈值配置
|
||||
alarm:
|
||||
threshold:
|
||||
# 溶解氧阈值(mg/L)
|
||||
dissolved-oxygen:
|
||||
min: 4.0 # 最低阈值
|
||||
max: 15.0 # 最高阈值
|
||||
|
||||
# 温度阈值(℃)
|
||||
temperature:
|
||||
min: 10.0 # 最低阈值
|
||||
max: 35.0 # 最高阈值
|
||||
|
||||
# pH值阈值
|
||||
ph:
|
||||
min: 6.5 # 最低阈值
|
||||
max: 8.5 # 最高阈值
|
||||
|
||||
# 盐度阈值
|
||||
salinity:
|
||||
max: 35.0 # 最高阈值
|
||||
|
||||
# 电池电量阈值(%)
|
||||
battery:
|
||||
min: 20.0 # 最低阈值
|
||||
|
||||
# 告警通知配置
|
||||
notification:
|
||||
interval: 30 # 告警通知间隔时间(分钟),同一设备在此时间内不会重复发送通知
|
||||
@@ -13,7 +13,7 @@ spring.boot.admin.client:
|
||||
|
||||
--- # snail-job 配置
|
||||
snail-job:
|
||||
enabled: true
|
||||
enabled: false
|
||||
# 需要在 SnailJob 后台组管理创建对应名称的组,然后创建任务的时候选择对应的组,才能正确分派任务
|
||||
group: "ruoyi_group"
|
||||
# SnailJob 接入验证令牌 详见 script/sql/ry_job.sql `sj_group_config` 表
|
||||
@@ -35,7 +35,7 @@ spring:
|
||||
# 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
|
||||
dynamic:
|
||||
# 性能分析插件(有性能损耗 不建议生产环境使用)
|
||||
p6spy: true
|
||||
p6spy: false
|
||||
# 设置默认的数据源或者数据源组,默认值即为 master
|
||||
primary: master
|
||||
# 严格模式 匹配不到数据源则报错
|
||||
@@ -204,6 +204,44 @@ sms:
|
||||
code-template-id: 'SMS_465720430' # TODO: 请填写您的阿里云短信模板CODE
|
||||
|
||||
|
||||
--- # 阿里云生活物联网平台(飞燕平台)配置
|
||||
aliyun:
|
||||
living-iot:
|
||||
# 阿里云 AccessKey ID(必填)
|
||||
access-key-id: LTAI5tRnPowmTLjH181nSbsR
|
||||
# 阿里云 AccessKey Secret(必填)
|
||||
access-key-secret: Vh2LoAM1t3XuMUVy2wTWSACJ97kOUW
|
||||
# 地域节点(必填)
|
||||
region-id: cn-shanghai
|
||||
# 飞燕平台项目ID(Project ID)
|
||||
project-id: a123nMibvh0q4UnU
|
||||
# App Key(用于 API 调用)
|
||||
app-key: 334224397
|
||||
# App Secret
|
||||
app-secret: 70de3018ec39423e9ca1e1b6a6a84ad6
|
||||
# AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret)
|
||||
amqp:
|
||||
# 是否启用
|
||||
enabled: true
|
||||
# 数据同步 AppKey(与上面的 app-key 不同!)
|
||||
data-sync-app-key: 334224409
|
||||
# 数据同步 AppSecret(请在阿里云控制台查看)
|
||||
data-sync-app-secret: 17fdd58f9a4c4c90be236897b1f8e8f7
|
||||
# AMQP 接入点地址(使用生活物联网平台官方地址)
|
||||
endpoint: amqps://ilop.iot-amqp.cn-shanghai.aliyuncs.com:5671
|
||||
# 消费组 ID(按照官方文档,应该与数据同步 AppKey 相同)
|
||||
consumer-group-id: 334224409
|
||||
# 客户端 ID(建议使用机器 UUID、MAC 地址等唯一标识)
|
||||
client-id: fishery-backend-001
|
||||
# 连接超时时间(毫秒)
|
||||
connection-timeout: 80000
|
||||
# 是否自动重连
|
||||
auto-reconnect: true
|
||||
# 最大重连次数
|
||||
max-reconnect-attempts: 10
|
||||
# 重连延迟(毫秒)
|
||||
reconnect-delay: 30000
|
||||
|
||||
--- # 三方授权
|
||||
justauth:
|
||||
# 前端外网访问地址
|
||||
|
||||
@@ -35,7 +35,7 @@ captcha:
|
||||
# 日志配置
|
||||
logging:
|
||||
level:
|
||||
org.dromara: @logging.level@
|
||||
org.dromara: info
|
||||
org.springframework: warn
|
||||
org.mybatis.spring.mapper: error
|
||||
org.apache.fury: warn
|
||||
@@ -69,7 +69,7 @@ spring:
|
||||
# 国际化资源文件路径
|
||||
basename: i18n/messages
|
||||
profiles:
|
||||
active: @profiles.active@
|
||||
active: dev
|
||||
# 文件上传
|
||||
servlet:
|
||||
multipart:
|
||||
@@ -118,6 +118,9 @@ security:
|
||||
- /*/api-docs
|
||||
- /*/api-docs/**
|
||||
- /warm-flow-ui/config
|
||||
- /iot/test
|
||||
- /iot/device/**
|
||||
- /iot/amqp/**
|
||||
|
||||
# 多租户配置
|
||||
tenant:
|
||||
|
||||
@@ -17,4 +17,4 @@ databaseDialectTimestampFormat=yyyy-MM-dd HH:mm:ss
|
||||
# 是否过滤 Log
|
||||
filter=true
|
||||
# 过滤 Log 时所排除的 sql 关键字,以逗号分隔
|
||||
exclude=
|
||||
exclude=information_schema.ins_tables
|
||||
|
||||
@@ -31,11 +31,40 @@
|
||||
<version>7.46.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MQTT 客户端 -->
|
||||
<!-- MQTT 客户端(可选,用于设备直连)-->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>1.2.5</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- AMQP 1.0 客户端(阿里云IoT服务端订阅使用 AMQP 1.0 协议)-->
|
||||
<dependency>
|
||||
<groupId>org.apache.qpid</groupId>
|
||||
<artifactId>qpid-jms-client</artifactId>
|
||||
<version>0.53.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Apache Commons Codec(用于 Base64 和 Hex 编码)-->
|
||||
<dependency>
|
||||
<groupId>commons-codec</groupId>
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<version>1.15</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云 MNS SDK -->
|
||||
<dependency>
|
||||
<groupId>com.aliyun.mns</groupId>
|
||||
<artifactId>aliyun-sdk-mns</artifactId>
|
||||
<version>1.1.9.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 阿里云语音服务 SDK(稳定版)-->
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>aliyun-java-sdk-dyvmsapi</artifactId>
|
||||
<version>1.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP 客户端 -->
|
||||
@@ -81,11 +110,30 @@
|
||||
<artifactId>intc-common-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.intc</groupId>
|
||||
<artifactId>intc-common-excel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.intc</groupId>
|
||||
<artifactId>intc-common-tenant</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- TDengine 模块(用于时序数据存储) -->
|
||||
<dependency>
|
||||
<groupId>com.intc</groupId>
|
||||
<artifactId>intc-tdengine</artifactId>
|
||||
<version>5.5.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.intc</groupId>
|
||||
<artifactId>intc-fishery</artifactId>
|
||||
<version>5.5.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.aliyuncs.profile.DefaultProfile;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -17,6 +18,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@EnableConfigurationProperties(AliyunIotProperties.class)
|
||||
@ConditionalOnProperty(prefix = "aliyun.living-iot", name = "access-key-id")
|
||||
public class AliyunIotConfiguration {
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.intc.iot.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 阿里云生活物联网平台(飞燕平台)配置属性
|
||||
@@ -10,7 +9,6 @@ import org.springframework.stereotype.Component;
|
||||
* @author intc
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "aliyun.living-iot")
|
||||
public class AliyunIotProperties {
|
||||
|
||||
@@ -50,10 +48,15 @@ public class AliyunIotProperties {
|
||||
private String categoryKey;
|
||||
|
||||
/**
|
||||
* MQTT 配置
|
||||
* MQTT 配置(可选,用于设备直连)
|
||||
*/
|
||||
private MqttConfig mqtt = new MqttConfig();
|
||||
|
||||
/**
|
||||
* AMQP 服务端订阅配置(推荐)
|
||||
*/
|
||||
private AmqpConfig amqp = new AmqpConfig();
|
||||
|
||||
@Data
|
||||
public static class MqttConfig {
|
||||
/**
|
||||
@@ -97,4 +100,62 @@ public class AliyunIotProperties {
|
||||
private Boolean cleanSession = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* AMQP 服务端订阅配置(使用数据同步的 AppKey + AppSecret)
|
||||
*/
|
||||
@Data
|
||||
public static class AmqpConfig {
|
||||
/**
|
||||
* 是否启用 AMQP 订阅
|
||||
*/
|
||||
private Boolean enabled = false;
|
||||
|
||||
/**
|
||||
* 数据同步 AppKey(与外层的 appKey 不同!)
|
||||
*/
|
||||
private String dataSyncAppKey;
|
||||
|
||||
/**
|
||||
* 数据同步 AppSecret
|
||||
*/
|
||||
private String dataSyncAppSecret;
|
||||
|
||||
/**
|
||||
* AMQP 接入点地址(使用 amqps 协议)
|
||||
* 中国内地:amqps://ilop.iot-amqp.cn-shanghai.aliyuncs.com:5671
|
||||
* 新加坡:amqps://ilop.iot-amqp.ap-southeast-1.aliyuncs.com:5671
|
||||
*/
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* 消费组 ID
|
||||
*/
|
||||
private String consumerGroupId;
|
||||
|
||||
/**
|
||||
* 客户端 ID(建议使用机器 UUID、MAC 地址等唯一标识)
|
||||
*/
|
||||
private String clientId;
|
||||
|
||||
/**
|
||||
* 连接超时时间(毫秒)
|
||||
*/
|
||||
private Integer connectionTimeout = 80000;
|
||||
|
||||
/**
|
||||
* 自动重连
|
||||
*/
|
||||
private Boolean autoReconnect = true;
|
||||
|
||||
/**
|
||||
* 最大重连次数
|
||||
*/
|
||||
private Integer maxReconnectAttempts = 10;
|
||||
|
||||
/**
|
||||
* 重连延迟(毫秒)
|
||||
*/
|
||||
private Integer reconnectDelay = 30000;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
package com.intc.iot.config;
|
||||
|
||||
import com.intc.iot.service.AmqpMessageHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.qpid.jms.JmsConnection;
|
||||
import org.apache.qpid.jms.JmsConnectionListener;
|
||||
import org.apache.qpid.jms.message.JmsInboundMessageDispatch;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.jms.*;
|
||||
import javax.jms.Connection;
|
||||
import javax.naming.Context;
|
||||
import javax.naming.InitialContext;
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import java.net.URI;
|
||||
import java.util.Hashtable;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 阿里云 IoT AMQP 服务端订阅配置
|
||||
*
|
||||
* 使用说明:
|
||||
* 1. 在阿里云IoT控制台开启设备数据同步
|
||||
* 2. 配置 AMQP 连接参数(endpoint、consumerGroupId 等)
|
||||
* 3. 启用配置:aliyun.living-iot.amqp.enabled=true
|
||||
*
|
||||
* 注意:
|
||||
* - 阿里云生活物联网平台使用 AMQP 1.0 协议
|
||||
* - 使用 AppKey + AppSecret 认证(不是 AccessKey)
|
||||
* - 连接地址:amqps://ilop.iot-amqp.cn-shanghai.aliyuncs.com:5671
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@EnableConfigurationProperties(AliyunIotProperties.class)
|
||||
@ConditionalOnProperty(prefix = "aliyun.living-iot.amqp", name = "enabled", havingValue = "true")
|
||||
public class AmqpConfiguration {
|
||||
|
||||
private final AliyunIotProperties iotProperties;
|
||||
private final AmqpMessageHandler messageHandler;
|
||||
|
||||
/**
|
||||
* 创建 AMQP 1.0 连接(使用 JMS)
|
||||
*/
|
||||
@Bean
|
||||
public Connection amqpConnection() throws Exception {
|
||||
AliyunIotProperties.AmqpConfig amqp = iotProperties.getAmqp();
|
||||
// 使用数据同步的 AppKey/AppSecret 进行 AMQP 认证
|
||||
String dataSyncAppKey = amqp.getDataSyncAppKey();
|
||||
String dataSyncAppSecret = amqp.getDataSyncAppSecret();
|
||||
|
||||
log.info("========== AMQP 连接配置 ==========");
|
||||
log.info("Endpoint: {}", amqp.getEndpoint());
|
||||
log.info("数据同步 AppKey: {}", dataSyncAppKey);
|
||||
log.info("数据同步 AppSecret: {}...", dataSyncAppSecret != null && dataSyncAppSecret.length() > 4
|
||||
? dataSyncAppSecret.substring(0, 4) + "****" : "NULL");
|
||||
log.info("ConsumerGroupId: {}", amqp.getConsumerGroupId());
|
||||
log.info("ClientId: {}", amqp.getClientId());
|
||||
log.info("ConnectionTimeout: {}ms", amqp.getConnectionTimeout());
|
||||
log.info("====================================");
|
||||
|
||||
try {
|
||||
// 生成随机数(按照官方示例)
|
||||
long random = ThreadLocalRandom.current().nextLong(Long.MAX_VALUE);
|
||||
|
||||
// 构建用户名(完全按照官方示例)
|
||||
String userName = amqp.getClientId() + "|authMode=appkey"
|
||||
+ ",signMethod=SHA256"
|
||||
+ ",random=" + random
|
||||
+ ",appKey=" + dataSyncAppKey
|
||||
+ ",groupId=" + amqp.getConsumerGroupId() + "|";
|
||||
|
||||
// 计算密码(完全按照官方示例)
|
||||
String signContent = "random=" + random;
|
||||
String password = doSign(signContent, dataSyncAppSecret, "HmacSHA256");
|
||||
|
||||
log.info("用户名: {}", userName);
|
||||
log.info("签名内容: {}", signContent);
|
||||
log.info("密码(完整): {}", password);
|
||||
log.info("密码长度: {}", password.length());
|
||||
log.info("ConsumerGroupId(用于groupId): {}", amqp.getConsumerGroupId());
|
||||
|
||||
// 构建连接 URL(带故障转移和 SSL 配置)
|
||||
String connectionUrl = String.format(
|
||||
"failover:(%s?amqp.idleTimeout=%d&transport.verifyHost=false)?failover.maxReconnectAttempts=%d&failover.reconnectDelay=%d",
|
||||
amqp.getEndpoint(),
|
||||
amqp.getConnectionTimeout(),
|
||||
amqp.getMaxReconnectAttempts(),
|
||||
amqp.getReconnectDelay()
|
||||
);
|
||||
|
||||
log.info("连接 URL: {}", connectionUrl);
|
||||
|
||||
// 使用 JNDI 初始化连接
|
||||
Hashtable<String, String> hashtable = new Hashtable<>();
|
||||
hashtable.put("connectionfactory.SBCF", connectionUrl);
|
||||
hashtable.put("queue.QUEUE", "default");
|
||||
hashtable.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.qpid.jms.jndi.JmsInitialContextFactory");
|
||||
|
||||
Context context = new InitialContext(hashtable);
|
||||
ConnectionFactory cf = (ConnectionFactory) context.lookup("SBCF");
|
||||
Destination queue = (Destination) context.lookup("QUEUE");
|
||||
|
||||
log.info("正在连接到 AMQP 服务器...");
|
||||
log.info("请耐必等待,连接可能需要几秒...");
|
||||
|
||||
// 创建连接
|
||||
Connection connection = cf.createConnection(userName, password);
|
||||
|
||||
// 添加连接监听器
|
||||
if (connection instanceof JmsConnection) {
|
||||
((JmsConnection) connection).addConnectionListener(new JmsConnectionListener() {
|
||||
@Override
|
||||
public void onConnectionEstablished(URI remoteURI) {
|
||||
log.info("✅ AMQP 连接建立成功: {}", remoteURI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionFailure(Throwable error) {
|
||||
log.error("❌ AMQP 连接失败回调");
|
||||
log.error("失败原因: {}", error.getMessage());
|
||||
log.error("异常类型: {}", error.getClass().getName());
|
||||
if (error.getCause() != null) {
|
||||
log.error("根本原因: {}", error.getCause().getMessage());
|
||||
log.error("根本原因类型: {}", error.getCause().getClass().getName());
|
||||
}
|
||||
// 打印完整堆栈
|
||||
log.error("完整异常信息:", error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionInterrupted(URI remoteURI) {
|
||||
log.warn("⚠️ AMQP 连接中断: {}", remoteURI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConnectionRestored(URI remoteURI) {
|
||||
log.info("✅ AMQP 连接恢复: {}", remoteURI);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInboundMessage(JmsInboundMessageDispatch envelope) {}
|
||||
|
||||
@Override
|
||||
public void onSessionClosed(Session session, Throwable cause) {}
|
||||
|
||||
@Override
|
||||
public void onConsumerClosed(MessageConsumer consumer, Throwable cause) {}
|
||||
|
||||
@Override
|
||||
public void onProducerClosed(MessageProducer producer, Throwable cause) {}
|
||||
});
|
||||
}
|
||||
|
||||
// 启动连接
|
||||
connection.start();
|
||||
log.info("✅ AMQP 连接创建成功!");
|
||||
|
||||
// 启动消息消费
|
||||
startConsuming(connection, queue);
|
||||
|
||||
return connection;
|
||||
} catch (Exception e) {
|
||||
log.error("❌ AMQP 连接失败", e);
|
||||
log.error("===== 详细错误信息 =====");
|
||||
log.error("异常类型: {}", e.getClass().getName());
|
||||
log.error("异常信息: {}", e.getMessage());
|
||||
|
||||
// 打印完整异常链
|
||||
Throwable cause = e.getCause();
|
||||
int level = 1;
|
||||
while (cause != null) {
|
||||
log.error("原因 {} - 类型: {}", level, cause.getClass().getName());
|
||||
log.error("原因 {} - 信息: {}", level, cause.getMessage());
|
||||
cause = cause.getCause();
|
||||
level++;
|
||||
}
|
||||
|
||||
log.error("===== 配置信息 =====");
|
||||
log.error(" - Endpoint: {}", amqp.getEndpoint());
|
||||
log.error(" - 数据同步 AppKey: {}", dataSyncAppKey);
|
||||
log.error(" - 数据同步 AppSecret: {}...", dataSyncAppSecret != null && dataSyncAppSecret.length() > 4 ? dataSyncAppSecret.substring(0, 4) + "****" : "NULL");
|
||||
log.error(" - ConsumerGroupId: {}", amqp.getConsumerGroupId());
|
||||
log.error(" - ClientId: {}", amqp.getClientId());
|
||||
|
||||
log.error("===== 可能原因 =====");
|
||||
log.error("1. 网络无法连接到服务器: {}", amqp.getEndpoint());
|
||||
log.error("2. 数据同步 AppKey 或 AppSecret 错误");
|
||||
log.error("3. 消费组 ID 不存在或未授权");
|
||||
log.error("4. 未开启设备数据同步");
|
||||
log.error("5. 防火墙阻止了端口 5671");
|
||||
log.error("6. 签名算法或格式错误");
|
||||
|
||||
throw new RuntimeException("创建 AMQP 连接失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动消息消费
|
||||
*/
|
||||
private void startConsuming(Connection connection, Destination queue) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
// 创建会话(自动 ACK)
|
||||
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
|
||||
|
||||
// 创建消费者
|
||||
MessageConsumer consumer = session.createConsumer(queue);
|
||||
|
||||
log.info("开始消费 AMQP 消息...");
|
||||
|
||||
// 设置消息监听器
|
||||
consumer.setMessageListener(message -> {
|
||||
try {
|
||||
// 阿里云 IoT 发送的是 BytesMessage
|
||||
byte[] body = message.getBody(byte[].class);
|
||||
String content = new String(body, "UTF-8");
|
||||
|
||||
// 处理消息(异步)
|
||||
messageHandler.handleMessage(content);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 AMQP 消息失败", e);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("启动 AMQP 消费失败", e);
|
||||
}
|
||||
}, "AMQP-Consumer").start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算签名(完全按照官方示例)
|
||||
*/
|
||||
private String doSign(String toSignString, String secret, String signMethod) throws Exception {
|
||||
SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), signMethod);
|
||||
Mac mac = Mac.getInstance(signMethod);
|
||||
mac.init(signingKey);
|
||||
byte[] rawHmac = mac.doFinal(toSignString.getBytes());
|
||||
|
||||
// 使用 Hex 编码(按照官方示例)
|
||||
return Hex.encodeHexString(rawHmac);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
|
||||
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -17,6 +18,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnClass(MqttClient.class)
|
||||
@ConditionalOnProperty(prefix = "aliyun.living-iot.mqtt", name = "broker-url")
|
||||
public class MqttConfiguration {
|
||||
|
||||
@@ -24,26 +26,40 @@ public class MqttConfiguration {
|
||||
|
||||
/**
|
||||
* 创建 MQTT 客户端
|
||||
* 添加容错机制,连接失败时不影响应用启动
|
||||
*/
|
||||
@Bean
|
||||
public MqttClient mqttClient() throws Exception {
|
||||
public MqttClient mqttClient() {
|
||||
AliyunIotProperties.MqttConfig mqtt = iotProperties.getMqtt();
|
||||
|
||||
MemoryPersistence persistence = new MemoryPersistence();
|
||||
MqttClient client = new MqttClient(mqtt.getBrokerUrl(), mqtt.getClientId(), persistence);
|
||||
try {
|
||||
MemoryPersistence persistence = new MemoryPersistence();
|
||||
MqttClient client = new MqttClient(mqtt.getBrokerUrl(), mqtt.getClientId(), persistence);
|
||||
|
||||
MqttConnectOptions options = new MqttConnectOptions();
|
||||
options.setUserName(mqtt.getUsername());
|
||||
options.setPassword(mqtt.getPassword().toCharArray());
|
||||
options.setConnectionTimeout(mqtt.getConnectionTimeout());
|
||||
options.setKeepAliveInterval(mqtt.getKeepAliveInterval());
|
||||
options.setAutomaticReconnect(mqtt.getAutoReconnect());
|
||||
options.setCleanSession(mqtt.getCleanSession());
|
||||
MqttConnectOptions options = new MqttConnectOptions();
|
||||
options.setUserName(mqtt.getUsername());
|
||||
options.setPassword(mqtt.getPassword().toCharArray());
|
||||
options.setConnectionTimeout(mqtt.getConnectionTimeout());
|
||||
options.setKeepAliveInterval(mqtt.getKeepAliveInterval());
|
||||
options.setAutomaticReconnect(mqtt.getAutoReconnect());
|
||||
options.setCleanSession(mqtt.getCleanSession());
|
||||
|
||||
client.connect(options);
|
||||
log.info("MQTT 客户端连接成功,Broker: {}", mqtt.getBrokerUrl());
|
||||
client.connect(options);
|
||||
log.info("MQTT 客户端连接成功,Broker: {}", mqtt.getBrokerUrl());
|
||||
|
||||
return client;
|
||||
return client;
|
||||
} catch (Exception e) {
|
||||
log.error("MQTT 客户端连接失败,将返回未连接的客户端: {}", e.getMessage());
|
||||
|
||||
try {
|
||||
// 返回未连接的客户端,由 MqttServiceImpl 中的自动重连机制处理
|
||||
MemoryPersistence persistence = new MemoryPersistence();
|
||||
return new MqttClient(mqtt.getBrokerUrl(), mqtt.getClientId(), persistence);
|
||||
} catch (Exception ex) {
|
||||
log.error("MQTT 客户端创建失败,请检查配置: {}", ex.getMessage());
|
||||
throw new RuntimeException("创建 MQTT 客户端失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.intc.iot.config;
|
||||
|
||||
import com.intc.iot.service.VmsMnsConsumerService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
/**
|
||||
* VMS MNS 消费服务自动启动器
|
||||
* 在 Spring 容器启动后自动启动 MNS 消费
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(VmsMnsConsumerService.class)
|
||||
@Slf4j
|
||||
public class VmsMnsAutoStarter implements ApplicationRunner {
|
||||
|
||||
private final VmsMnsConsumerService vmsMnsConsumerService;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
log.info("自动启动 VMS MNS 回执消费服务");
|
||||
vmsMnsConsumerService.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 容器销毁时停止服务
|
||||
*/
|
||||
@PreDestroy
|
||||
public void onDestroy() {
|
||||
log.info("容器销毁,停止 VMS MNS 回执消费服务");
|
||||
vmsMnsConsumerService.stop();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,20 @@
|
||||
package com.intc.iot.controller;
|
||||
|
||||
import com.intc.common.core.domain.R;
|
||||
import com.intc.common.mybatis.core.page.PageQuery;
|
||||
import com.intc.common.mybatis.core.page.TableDataInfo;
|
||||
import com.intc.common.web.core.BaseController;
|
||||
import com.intc.iot.domain.bo.DeviceRealtimeDataBo;
|
||||
import com.intc.iot.domain.vo.DeviceRealtimeDataVo;
|
||||
import com.intc.iot.service.DeviceDataService;
|
||||
import com.intc.iot.service.DeviceRealtimeDataService;
|
||||
import com.intc.iot.service.DeviceStatusService;
|
||||
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.service.WarnCallNoticeService;
|
||||
import com.intc.iot.utils.AliyunAmqpSignUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -36,22 +46,115 @@ public class IotController extends BaseController {
|
||||
@Autowired(required = false)
|
||||
private DeviceDataService deviceDataService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private DeviceRealtimeDataService deviceRealtimeDataService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private DeviceStatusService deviceStatusService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private VmsNoticeService vmsNoticeService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private VmsMnsConsumerService vmsMnsConsumerService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private WarnCallNoticeService warnCallNoticeService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private javax.jms.Connection amqpConnection;
|
||||
|
||||
@Operation(summary = "测试接口")
|
||||
@GetMapping("/test")
|
||||
public R<String> test() {
|
||||
return R.ok("飞燕平台模块测试成功!");
|
||||
}
|
||||
|
||||
@Operation(summary = "查询 AMQP 连接状态")
|
||||
@GetMapping("/amqp/status")
|
||||
public R<Map<String, Object>> getAmqpStatus() {
|
||||
Map<String, Object> status = new java.util.HashMap<>();
|
||||
|
||||
if (amqpConnection == null) {
|
||||
status.put("configured", false);
|
||||
status.put("connected", false);
|
||||
status.put("message", "AMQP 未启用或未配置");
|
||||
} else {
|
||||
try {
|
||||
status.put("configured", true);
|
||||
// JMS Connection 没有直接的 isOpen 方法,通过尝试获取元数据来检查连接
|
||||
javax.jms.ConnectionMetaData metaData = amqpConnection.getMetaData();
|
||||
status.put("connected", true);
|
||||
status.put("provider", metaData.getJMSProviderName());
|
||||
status.put("version", metaData.getProviderVersion());
|
||||
status.put("message", "AMQP 连接正常");
|
||||
} catch (Exception e) {
|
||||
status.put("configured", true);
|
||||
status.put("connected", false);
|
||||
status.put("message", "AMQP 连接已关闭或异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return R.ok(status);
|
||||
}
|
||||
|
||||
@Operation(summary = "生成 AMQP 配置信息")
|
||||
@GetMapping("/amqp/generate-config")
|
||||
public R<Map<String, Object>> generateAmqpConfig(
|
||||
@Parameter(description = "AccessKey ID") @RequestParam String accessKeyId,
|
||||
@Parameter(description = "AccessKey Secret") @RequestParam String accessKeySecret,
|
||||
@Parameter(description = "消费组 ID") @RequestParam String consumerGroupId,
|
||||
@Parameter(description = "阿里云账号 UID") @RequestParam String uid,
|
||||
@Parameter(description = "地域 ID(如:cn-shanghai)") @RequestParam String regionId) {
|
||||
try {
|
||||
String host = AliyunAmqpSignUtil.generateHost(uid, regionId);
|
||||
String virtualHost = AliyunAmqpSignUtil.generateVirtualHost(accessKeyId);
|
||||
String username = AliyunAmqpSignUtil.generateUsername(accessKeyId);
|
||||
String password = AliyunAmqpSignUtil.generatePassword(accessKeySecret, consumerGroupId);
|
||||
|
||||
Map<String, Object> config = new java.util.HashMap<>();
|
||||
config.put("host", host);
|
||||
config.put("port", 5672);
|
||||
config.put("virtualHost", virtualHost);
|
||||
config.put("username", username);
|
||||
config.put("password", password);
|
||||
config.put("consumerGroupId", consumerGroupId);
|
||||
|
||||
return R.ok(config);
|
||||
} catch (Exception e) {
|
||||
log.error("生成 AMQP 配置失败", e);
|
||||
return R.fail("生成配置失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "根据 ProductKey 和 DeviceName 查询设备信息")
|
||||
@GetMapping("/device/find")
|
||||
public R<Map<String, Object>> findDeviceByProductKeyAndName(
|
||||
@Parameter(description = "产品Key") @RequestParam String productKey,
|
||||
@Parameter(description = "设备名称") @RequestParam String deviceName) {
|
||||
try {
|
||||
if (iotDeviceService == null) {
|
||||
return R.fail("飞燕平台配置未启用");
|
||||
}
|
||||
Map<String, Object> response = iotDeviceService.findDeviceByProductKeyAndName(productKey, deviceName);
|
||||
return R.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("根据 ProductKey 和 DeviceName 查询设备信息失败", e);
|
||||
return R.fail("查询设备信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "查询设备列表")
|
||||
@GetMapping("/device/list")
|
||||
public R<Map<String, Object>> queryDeviceList(
|
||||
@Parameter(description = "产品Key(必填)") @RequestParam(value = "productKey", required = true) String productKey,
|
||||
@Parameter(description = "页码") @RequestParam(defaultValue = "1") Integer pageNo,
|
||||
@Parameter(description = "每页大小") @RequestParam(defaultValue = "20") Integer pageSize) {
|
||||
try {
|
||||
if (iotDeviceService == null) {
|
||||
return R.fail("飞燕平台配置未启用");
|
||||
}
|
||||
Map<String, Object> response = iotDeviceService.queryDeviceList(pageNo, pageSize);
|
||||
Map<String, Object> response = iotDeviceService.queryDeviceList(productKey, pageNo, pageSize);
|
||||
return R.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("查询设备列表失败", e);
|
||||
@@ -103,7 +206,7 @@ public class IotController extends BaseController {
|
||||
Map<String, Object> response = iotDeviceService.setDeviceProperty(iotId, properties);
|
||||
return R.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("设置设备属性失败", e);
|
||||
log.error("设置设备属性失败: {}", e.getMessage());
|
||||
return R.fail("设置设备属性失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -121,7 +224,7 @@ public class IotController extends BaseController {
|
||||
Map<String, Object> response = iotDeviceService.invokeService(iotId, identifier, args);
|
||||
return R.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("调用设备服务失败", e);
|
||||
log.error("调用设备服务失败: {}", e.getMessage());
|
||||
return R.fail("调用设备服务失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -137,11 +240,65 @@ public class IotController extends BaseController {
|
||||
Map<String, Object> response = iotDeviceService.unbindDevice(iotId);
|
||||
return R.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("解绑设备失败", e);
|
||||
log.error("解绑设备失败: {}", e.getMessage());
|
||||
return R.fail("解绑设备失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "查询设备物模型(模板)")
|
||||
@GetMapping("/device/thing-model")
|
||||
public R<Map<String, Object>> queryThingModel(
|
||||
@Parameter(description = "产品Key") @RequestParam String productKey) {
|
||||
try {
|
||||
if (iotDeviceService == null) {
|
||||
return R.fail("飞燕平台配置未启用");
|
||||
}
|
||||
Map<String, Object> response = iotDeviceService.queryThingModel(productKey);
|
||||
return R.ok(response);
|
||||
} catch (Exception e) {
|
||||
log.error("查询设备物模型失败: {}", e.getMessage());
|
||||
return R.fail("查询设备物模型失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "查询设备当前状态")
|
||||
@GetMapping("/device/status")
|
||||
public R<com.intc.iot.domain.IotDeviceStatus> queryDeviceStatus(
|
||||
@Parameter(description = "产品Key") @RequestParam String productKey,
|
||||
@Parameter(description = "设备名称") @RequestParam String deviceName) {
|
||||
try {
|
||||
if (deviceStatusService == null) {
|
||||
return R.fail("设备状态服务未启用");
|
||||
}
|
||||
com.intc.iot.domain.IotDeviceStatus status = deviceStatusService.queryDeviceStatus(productKey, deviceName);
|
||||
return R.ok(status);
|
||||
} catch (Exception e) {
|
||||
log.error("查询设备状态失败: {}", e.getMessage());
|
||||
return R.fail("查询设备状态失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "判断设备是否在线")
|
||||
@GetMapping("/device/online")
|
||||
public R<Map<String, Object>> isDeviceOnline(
|
||||
@Parameter(description = "产品Key") @RequestParam String productKey,
|
||||
@Parameter(description = "设备名称") @RequestParam String deviceName) {
|
||||
try {
|
||||
if (deviceStatusService == null) {
|
||||
return R.fail("设备状态服务未启用");
|
||||
}
|
||||
boolean online = deviceStatusService.isDeviceOnline(productKey, deviceName);
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("online", online);
|
||||
result.put("productKey", productKey);
|
||||
result.put("deviceName", deviceName);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("查询设备在线状态失败: {}", e.getMessage());
|
||||
return R.fail("查询设备在线状态失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "发布MQTT消息")
|
||||
@PostMapping("/mqtt/publish")
|
||||
public R<String> publishMqtt(
|
||||
@@ -155,7 +312,7 @@ public class IotController extends BaseController {
|
||||
mqttService.publish(topic, payload, qos);
|
||||
return R.ok("消息发布成功");
|
||||
} catch (Exception e) {
|
||||
log.error("发布MQTT消息失败", e);
|
||||
log.error("发布MQTT消息失败: {}", e.getMessage());
|
||||
return R.fail("发布MQTT消息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -172,7 +329,7 @@ public class IotController extends BaseController {
|
||||
mqttService.subscribe(topic, qos);
|
||||
return R.ok("订阅成功");
|
||||
} catch (Exception e) {
|
||||
log.error("订阅MQTT主题失败", e);
|
||||
log.error("订阅MQTT主题失败: {}", e.getMessage());
|
||||
return R.fail("订阅MQTT主题失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -188,11 +345,28 @@ public class IotController extends BaseController {
|
||||
mqttService.unsubscribe(topic);
|
||||
return R.ok("取消订阅成功");
|
||||
} catch (Exception e) {
|
||||
log.error("取消订阅MQTT主题失败", e);
|
||||
log.error("取消订阅失败: {}", e.getMessage());
|
||||
return R.fail("取消订阅MQTT主题失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "订阅设备状态 Topic")
|
||||
@PostMapping("/mqtt/subscribe/status")
|
||||
public R<String> subscribeDeviceStatus(
|
||||
@Parameter(description = "产品Key") @RequestParam String productKey,
|
||||
@Parameter(description = "设备名称") @RequestParam String deviceName) {
|
||||
try {
|
||||
if (mqttService == null) {
|
||||
return R.fail("MQTT配置未启用");
|
||||
}
|
||||
mqttService.subscribeDeviceStatus(productKey, deviceName);
|
||||
return R.ok("订阅设备状态成功");
|
||||
} catch (Exception e) {
|
||||
log.error("订阅设备状态失败: {}", e.getMessage());
|
||||
return R.fail("订阅设备状态失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "订阅设备实时数据(按产品)")
|
||||
@PostMapping("/device/data/subscribe")
|
||||
public R<String> subscribeDeviceData(
|
||||
@@ -204,9 +378,208 @@ public class IotController extends BaseController {
|
||||
deviceDataService.subscribeAllDevices(productKey);
|
||||
return R.ok("订阅成功,设备数据将实时推送");
|
||||
} catch (Exception e) {
|
||||
log.error("订阅设备数据失败", e);
|
||||
log.error("订阅设备数据失败: {}", e.getMessage());
|
||||
return R.fail("订阅设备数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "查询设备实时数据列表(分页)")
|
||||
@GetMapping("/device/realtime/list")
|
||||
public R<TableDataInfo<DeviceRealtimeDataVo>> queryRealtimeDataList(
|
||||
DeviceRealtimeDataBo bo, PageQuery pageQuery) {
|
||||
try {
|
||||
if (deviceRealtimeDataService == null) {
|
||||
return R.fail("设备数据服务未启用");
|
||||
}
|
||||
TableDataInfo<DeviceRealtimeDataVo> dataInfo = deviceRealtimeDataService.queryPageList(bo, pageQuery);
|
||||
return R.ok(dataInfo);
|
||||
} catch (Exception e) {
|
||||
log.error("查询实时数据失败: {}", e.getMessage());
|
||||
return R.fail("查询设备实时数据列表失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "查询设备最新数据")
|
||||
@GetMapping("/device/realtime/latest")
|
||||
public R<DeviceRealtimeDataVo> queryLatestData(
|
||||
@Parameter(description = "产品Key") @RequestParam String productKey,
|
||||
@Parameter(description = "设备名称") @RequestParam String deviceName) {
|
||||
try {
|
||||
if (deviceRealtimeDataService == null) {
|
||||
return R.fail("设备数据服务未启用");
|
||||
}
|
||||
DeviceRealtimeDataVo data = deviceRealtimeDataService.queryLatestData(productKey, deviceName);
|
||||
return R.ok(data);
|
||||
} catch (Exception e) {
|
||||
log.error("查询最新数据失败: {}", e.getMessage());
|
||||
return R.fail("查询设备最新数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "查询设备最新属性数据")
|
||||
@GetMapping("/device/realtime/latest-property")
|
||||
public R<DeviceRealtimeDataVo> queryLatestPropertyData(
|
||||
@Parameter(description = "产品Key") @RequestParam String productKey,
|
||||
@Parameter(description = "设备名称") @RequestParam String deviceName) {
|
||||
try {
|
||||
if (deviceRealtimeDataService == null) {
|
||||
return R.fail("设备数据服务未启用");
|
||||
}
|
||||
DeviceRealtimeDataVo data = deviceRealtimeDataService.queryLatestPropertyData(productKey, deviceName);
|
||||
return R.ok(data);
|
||||
} catch (Exception e) {
|
||||
log.error("查询最新属性失败: {}", e.getMessage());
|
||||
return R.fail("查询设备最新属性数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "删除设备实时数据")
|
||||
@DeleteMapping("/device/realtime/{ids}")
|
||||
public R<Void> deleteRealtimeData(
|
||||
@Parameter(description = "主键ID数组") @PathVariable Long[] ids) {
|
||||
try {
|
||||
if (deviceRealtimeDataService == null) {
|
||||
return R.fail("设备数据服务未启用");
|
||||
}
|
||||
boolean success = deviceRealtimeDataService.deleteByIds(ids);
|
||||
return success ? R.ok() : R.fail("删除失败");
|
||||
} catch (Exception e) {
|
||||
log.error("删除实时数据失败: {}", e.getMessage());
|
||||
return R.fail("删除设备实时数据失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== VMS 语音通知相关接口 ========================
|
||||
|
||||
@Operation(summary = "发送语音通知(测试接口)")
|
||||
@PostMapping("/vms/call")
|
||||
public R<Map<String, Object>> sendVoiceCall(
|
||||
@Parameter(description = "手机号") @RequestParam String phoneNumber,
|
||||
@Parameter(description = "模板参数JSON") @RequestParam String params,
|
||||
@Parameter(description = "业务ID") @RequestParam(required = false) String outId) {
|
||||
try {
|
||||
if (vmsNoticeService == null) {
|
||||
return R.fail("VMS语音服务未启用");
|
||||
}
|
||||
Map<String, String> paramsMap = cn.hutool.json.JSONUtil.toBean(params, Map.class);
|
||||
com.intc.iot.domain.VmsNoticeResponse response = vmsNoticeService.sendTtsCall(
|
||||
phoneNumber, paramsMap, outId != null ? outId : "TEST"
|
||||
);
|
||||
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("success", response.isSuccess());
|
||||
result.put("code", response.getCode());
|
||||
result.put("message", response.getMessage());
|
||||
result.put("callId", response.getCallId());
|
||||
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("发送语音通知失败: {}", e.getMessage());
|
||||
return R.fail("发送语音通知失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "启动 VMS MNS 回执消费服务")
|
||||
@PostMapping("/vms/mns/start")
|
||||
public R<String> startVmsMnsConsumer() {
|
||||
try {
|
||||
if (vmsMnsConsumerService == null) {
|
||||
return R.fail("VMS MNS消费服务未配置");
|
||||
}
|
||||
if (vmsMnsConsumerService.isRunning()) {
|
||||
return R.fail("VMS MNS消费服务已在运行中");
|
||||
}
|
||||
vmsMnsConsumerService.start();
|
||||
return R.ok("VMS MNS消费服务启动成功");
|
||||
} catch (Exception e) {
|
||||
log.error("启动VMS失败: {}", e.getMessage());
|
||||
return R.fail("启动失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "停止 VMS MNS 回执消费服务")
|
||||
@PostMapping("/vms/mns/stop")
|
||||
public R<String> stopVmsMnsConsumer() {
|
||||
try {
|
||||
if (vmsMnsConsumerService == null) {
|
||||
return R.fail("VMS MNS消费服务未配置");
|
||||
}
|
||||
if (!vmsMnsConsumerService.isRunning()) {
|
||||
return R.fail("VMS MNS消费服务未在运行");
|
||||
}
|
||||
vmsMnsConsumerService.stop();
|
||||
return R.ok("VMS MNS消费服务已停止");
|
||||
} catch (Exception e) {
|
||||
log.error("停止VMS失败: {}", e.getMessage());
|
||||
return R.fail("停止失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "查询 VMS MNS 消费服务运行状态")
|
||||
@GetMapping("/vms/mns/status")
|
||||
public R<Map<String, Object>> getVmsMnsStatus() {
|
||||
try {
|
||||
if (vmsMnsConsumerService == null) {
|
||||
return R.fail("VMS MNS消费服务未配置");
|
||||
}
|
||||
Map<String, Object> status = new java.util.HashMap<>();
|
||||
status.put("running", vmsMnsConsumerService.isRunning());
|
||||
return R.ok(status);
|
||||
} catch (Exception e) {
|
||||
log.error("查询VMS状态失败: {}", e.getMessage());
|
||||
return R.fail("查询失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "手动触发 VMS 回执处理")
|
||||
@PostMapping("/vms/callback/process")
|
||||
public R<Map<String, Object>> processVmsCallbacks() {
|
||||
try {
|
||||
if (warnCallNoticeService == null) {
|
||||
return R.fail("告警通知服务未启用");
|
||||
}
|
||||
int count = warnCallNoticeService.processUnhandledCallbacks();
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("processedCount", count);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("处理回执失败: {}", e.getMessage());
|
||||
return R.fail("处理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "移除指定设备的待通知记录")
|
||||
@DeleteMapping("/vms/pending/{deviceId}")
|
||||
public R<Map<String, Object>> removePendingNotifications(
|
||||
@Parameter(description = "设备ID") @PathVariable Long deviceId) {
|
||||
try {
|
||||
if (warnCallNoticeService == null) {
|
||||
return R.fail("告警通知服务未启用");
|
||||
}
|
||||
int count = warnCallNoticeService.removePendingNotificationsByDevice(deviceId);
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("removedCount", count);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("移除待通知失败: {}", e.getMessage());
|
||||
return R.fail("移除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "手动清理超时的通知记录")
|
||||
@PostMapping("/vms/cleanup/expired")
|
||||
public R<Map<String, Object>> cleanupExpiredNotifications() {
|
||||
try {
|
||||
if (warnCallNoticeService == null) {
|
||||
return R.fail("告警通知服务未启用");
|
||||
}
|
||||
int count = warnCallNoticeService.cleanupExpiredNotifications();
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("cleanedCount", count);
|
||||
return R.ok(result);
|
||||
} catch (Exception e) {
|
||||
log.error("清理超时失败: {}", e.getMessage());
|
||||
return R.fail("清理失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.intc.iot.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 水质告警历史记录
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Data
|
||||
@TableName("aqu_alarm_history")
|
||||
public class AquAlarmHistory implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 设备ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 塘口名称
|
||||
*/
|
||||
private String pondName;
|
||||
|
||||
/**
|
||||
* 告警类型(溶解氧、温度、pH、盐度、电池等)
|
||||
*/
|
||||
private String alarmType;
|
||||
|
||||
/**
|
||||
* 告警级别:1-一般 2-重要 3-紧急
|
||||
*/
|
||||
private Integer alarmLevel;
|
||||
|
||||
/**
|
||||
* 告警内容
|
||||
*/
|
||||
private String alarmContent;
|
||||
|
||||
/**
|
||||
* 告警值
|
||||
*/
|
||||
private Double alarmValue;
|
||||
|
||||
/**
|
||||
* 阈值(最小或最大)
|
||||
*/
|
||||
private Double thresholdValue;
|
||||
|
||||
/**
|
||||
* 告警状态:0-未处理 1-已处理 2-已恢复
|
||||
*/
|
||||
private Integer alarmStatus;
|
||||
|
||||
/**
|
||||
* 告警时间
|
||||
*/
|
||||
private LocalDateTime alarmTime;
|
||||
|
||||
/**
|
||||
* 恢复时间
|
||||
*/
|
||||
private LocalDateTime recoveryTime;
|
||||
|
||||
/**
|
||||
* 处理时间
|
||||
*/
|
||||
private LocalDateTime handleTime;
|
||||
|
||||
/**
|
||||
* 处理人
|
||||
*/
|
||||
private Long handleBy;
|
||||
|
||||
/**
|
||||
* 处理备注
|
||||
*/
|
||||
private String handleRemark;
|
||||
|
||||
/**
|
||||
* 是否已通知:0-未通知 1-已通知
|
||||
*/
|
||||
private Integer notified;
|
||||
|
||||
/**
|
||||
* 通知记录ID(关联 aqu_call_notice)
|
||||
*/
|
||||
private Long noticeId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.intc.iot.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 告警电话通知记录
|
||||
* 对应 C# DbAquWarnCallNotice
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Data
|
||||
@TableName("aqu_call_notice")
|
||||
public class AquWarnCallNotice implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 通知手机号
|
||||
*/
|
||||
private String mobilePhone;
|
||||
|
||||
/**
|
||||
* 设备ID
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
/**
|
||||
* 塘口名称
|
||||
*/
|
||||
private String pondName;
|
||||
|
||||
/**
|
||||
* 设定的呼叫时间
|
||||
*/
|
||||
private LocalDateTime callTime;
|
||||
|
||||
/**
|
||||
* 呼叫状态
|
||||
* 0-未呼叫(NoCall)
|
||||
* 1-呼叫成功,等待结果反馈(CallSuccess)
|
||||
* 2-呼叫失败(CallFail)
|
||||
* 3-呼叫反馈成功(CallBackSuccess)
|
||||
* 4-呼叫反馈失败(CallBackFail)
|
||||
*/
|
||||
private Integer callStatus;
|
||||
|
||||
/**
|
||||
* 呼叫的CallId,CallStatus为1/3/4时有值
|
||||
*/
|
||||
private String callId;
|
||||
|
||||
// ==================== 以下为呼叫回执字段 ====================
|
||||
|
||||
/**
|
||||
* 呼叫结果状态码
|
||||
* 200000: 用户听完语音
|
||||
* 200001: 用户未听完挂断
|
||||
* 其他: 失败
|
||||
*/
|
||||
private String statusCode;
|
||||
|
||||
/**
|
||||
* 挂断方向(user: 用户挂断, system: 系统挂断)
|
||||
*/
|
||||
private String hangupDirection;
|
||||
|
||||
/**
|
||||
* 主叫号码
|
||||
*/
|
||||
private String caller;
|
||||
|
||||
/**
|
||||
* 被叫响铃时间
|
||||
*/
|
||||
private String ringTime;
|
||||
|
||||
/**
|
||||
* 通话时长(秒),未接通时为0
|
||||
*/
|
||||
private String duration;
|
||||
|
||||
/**
|
||||
* 话单类型(voice-普通话单,asr-asr话单,smart_transfer-智能外呼转接话单)
|
||||
*/
|
||||
private String voiceType;
|
||||
|
||||
/**
|
||||
* 呼叫发起时间
|
||||
*/
|
||||
private String originateTime;
|
||||
|
||||
/**
|
||||
* 通话接通时间
|
||||
*/
|
||||
private String startTime;
|
||||
|
||||
/**
|
||||
* 通话结束时间
|
||||
*/
|
||||
private String endTime;
|
||||
|
||||
/**
|
||||
* 结果描述
|
||||
*/
|
||||
private String statusMsg;
|
||||
|
||||
/**
|
||||
* 扩展字段回传
|
||||
*/
|
||||
private String outId;
|
||||
|
||||
/**
|
||||
* 通话类型(LOCAL-市话,PROVINCE-省内长途,DOMESTIC-国内长途,INTERNATIONAL-国际长途,UNKNOWN-未知)
|
||||
*/
|
||||
private String tollType;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
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.mybatis.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备实时数据
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("iot_device_realtime_data")
|
||||
public class DeviceRealtimeData extends BaseEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 产品Key
|
||||
*/
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* IoT平台设备ID
|
||||
*/
|
||||
private String iotId;
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 数据类型:property-属性,event-事件
|
||||
*/
|
||||
private String dataType;
|
||||
|
||||
/**
|
||||
* 属性/事件标识符
|
||||
*/
|
||||
private String identifier;
|
||||
|
||||
/**
|
||||
* 数据内容(JSON格式)
|
||||
*/
|
||||
private String dataContent;
|
||||
|
||||
/**
|
||||
* 数据上报时间
|
||||
*/
|
||||
private LocalDateTime reportTime;
|
||||
|
||||
/**
|
||||
* 原始Topic
|
||||
*/
|
||||
private String topic;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.intc.iot.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备上下线状态记录
|
||||
* 对应 C# IOTTopicDeviceStatus
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Data
|
||||
@TableName("iot_device_status")
|
||||
public class IotDeviceStatus implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 产品Key
|
||||
*/
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* 设备IotId
|
||||
*/
|
||||
private String iotId;
|
||||
|
||||
/**
|
||||
* 状态(online-在线,offline-离线)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 状态变更时间(秒时间戳)
|
||||
*/
|
||||
private Long statusTime;
|
||||
|
||||
/**
|
||||
* 上次更新时间(秒时间戳)
|
||||
*/
|
||||
private Long lastTime;
|
||||
|
||||
/**
|
||||
* 客户端IP
|
||||
*/
|
||||
private String clientIp;
|
||||
|
||||
/**
|
||||
* 用户标识(可选)
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.intc.iot.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* VMS 语音回执实体
|
||||
* 对应 C# AliVmsCallBack
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Data
|
||||
@TableName("vms_callback")
|
||||
public class VmsCallback implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 呼叫唯一标识
|
||||
*/
|
||||
private String callId;
|
||||
|
||||
/**
|
||||
* 业务扩展字段(用于区分系统)
|
||||
*/
|
||||
private String outId;
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
* 200000: 用户接听并听完
|
||||
* 200001: 用户未听完挂断
|
||||
* 其他: 失败
|
||||
*/
|
||||
private String statusCode;
|
||||
|
||||
/**
|
||||
* 状态描述
|
||||
*/
|
||||
private String statusMsg;
|
||||
|
||||
/**
|
||||
* 挂断方向
|
||||
* user: 用户挂断
|
||||
* system: 系统挂断
|
||||
*/
|
||||
private String hangupDirection;
|
||||
|
||||
/**
|
||||
* 被叫号码
|
||||
*/
|
||||
private String caller;
|
||||
|
||||
/**
|
||||
* 呼叫类型
|
||||
* tts: 文本转语音
|
||||
*/
|
||||
private String voiceType;
|
||||
|
||||
/**
|
||||
* 通话发起时间(秒时间戳)
|
||||
*/
|
||||
private Long originateTime;
|
||||
|
||||
/**
|
||||
* 通话开始时间(秒时间戳)
|
||||
*/
|
||||
private Long startTime;
|
||||
|
||||
/**
|
||||
* 通话结束时间(秒时间戳)
|
||||
*/
|
||||
private Long endTime;
|
||||
|
||||
/**
|
||||
* 振铃时长(秒)
|
||||
*/
|
||||
private Integer ringTime;
|
||||
|
||||
/**
|
||||
* 通话时长(秒)
|
||||
*/
|
||||
private Integer duration;
|
||||
|
||||
/**
|
||||
* 计费类型
|
||||
*/
|
||||
private String tollType;
|
||||
|
||||
/**
|
||||
* 是否已处理(0-未处理,1-已处理)
|
||||
*/
|
||||
private Integer processed;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.intc.iot.domain;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 语音通知返回结果(Java 版)。
|
||||
*/
|
||||
@Data
|
||||
public class VmsNoticeResponse {
|
||||
|
||||
/** 是否调用成功 */
|
||||
private boolean success;
|
||||
|
||||
/** 阿里云返回的状态码 */
|
||||
private String code;
|
||||
|
||||
/** 状态码描述信息 */
|
||||
private String message;
|
||||
|
||||
/** 呼叫ID */
|
||||
private String callId;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.intc.iot.domain.bo;
|
||||
|
||||
import com.intc.common.mybatis.core.domain.BaseEntity;
|
||||
import com.intc.iot.domain.DeviceRealtimeData;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备实时数据业务对象
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = DeviceRealtimeData.class, reverseConvertGenerate = false)
|
||||
public class DeviceRealtimeDataBo extends BaseEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 产品Key
|
||||
*/
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* IoT平台设备ID
|
||||
*/
|
||||
private String iotId;
|
||||
|
||||
/**
|
||||
* 数据类型:property-属性,event-事件
|
||||
*/
|
||||
private String dataType;
|
||||
|
||||
/**
|
||||
* 属性/事件标识符
|
||||
*/
|
||||
private String identifier;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
private LocalDateTime endTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.intc.iot.domain.vo;
|
||||
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import com.intc.iot.domain.DeviceRealtimeData;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 设备实时数据视图对象
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@AutoMapper(target = DeviceRealtimeData.class)
|
||||
public class DeviceRealtimeDataVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@ExcelProperty(value = "主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 产品Key
|
||||
*/
|
||||
@ExcelProperty(value = "产品Key")
|
||||
private String productKey;
|
||||
|
||||
/**
|
||||
* 设备名称
|
||||
*/
|
||||
@ExcelProperty(value = "设备名称")
|
||||
private String deviceName;
|
||||
|
||||
/**
|
||||
* IoT平台设备ID
|
||||
*/
|
||||
@ExcelProperty(value = "设备ID")
|
||||
private String iotId;
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@ExcelProperty(value = "消息ID")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 数据类型:property-属性,event-事件
|
||||
*/
|
||||
@ExcelProperty(value = "数据类型")
|
||||
private String dataType;
|
||||
|
||||
/**
|
||||
* 属性/事件标识符
|
||||
*/
|
||||
@ExcelProperty(value = "标识符")
|
||||
private String identifier;
|
||||
|
||||
/**
|
||||
* 数据内容(JSON格式)
|
||||
*/
|
||||
@ExcelProperty(value = "数据内容")
|
||||
private String dataContent;
|
||||
|
||||
/**
|
||||
* 数据上报时间
|
||||
*/
|
||||
@ExcelProperty(value = "上报时间")
|
||||
private LocalDateTime reportTime;
|
||||
|
||||
/**
|
||||
* 原始Topic
|
||||
*/
|
||||
@ExcelProperty(value = "Topic")
|
||||
private String topic;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@ExcelProperty(value = "备注")
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@ExcelProperty(value = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
}
|
||||
@@ -1,10 +1,29 @@
|
||||
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.AquWarnCallNotice;
|
||||
import com.intc.iot.domain.VmsNoticeResponse;
|
||||
import com.intc.iot.mapper.AquAlarmHistoryMapper;
|
||||
import com.intc.iot.mapper.AquWarnCallNoticeMapper;
|
||||
import com.intc.iot.mapper.IotDeviceMapper;
|
||||
import com.intc.iot.service.VmsNoticeService;
|
||||
import com.intc.fishery.domain.Device;
|
||||
import com.intc.fishery.mapper.DeviceMapper;
|
||||
import com.intc.tdengine.domain.DeviceSensorData;
|
||||
import com.intc.tdengine.service.IDeviceSensorDataService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设备数据处理器
|
||||
*
|
||||
@@ -12,8 +31,76 @@ import org.springframework.stereotype.Component;
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(IDeviceSensorDataService.class)
|
||||
public class DeviceDataHandler {
|
||||
|
||||
/**
|
||||
* TDengine 服务,用于存储设备时序数据
|
||||
*/
|
||||
private final IDeviceSensorDataService deviceSensorDataService;
|
||||
|
||||
/**
|
||||
* 告警通知 Mapper
|
||||
*/
|
||||
private final AquWarnCallNoticeMapper warnCallNoticeMapper;
|
||||
|
||||
/**
|
||||
* 告警历史记录 Mapper
|
||||
*/
|
||||
private final AquAlarmHistoryMapper alarmHistoryMapper;
|
||||
|
||||
/**
|
||||
* IoT设备 Mapper
|
||||
*/
|
||||
private final IotDeviceMapper iotDeviceMapper;
|
||||
|
||||
/**
|
||||
* 设备 Mapper
|
||||
*/
|
||||
private final DeviceMapper deviceMapper;
|
||||
|
||||
/**
|
||||
* VMS 语音通知服务
|
||||
*/
|
||||
private final VmsNoticeService vmsNoticeService;
|
||||
|
||||
private static final DateTimeFormatter DATETIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* 呼叫状态:未呼叫
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 告警间隔时间(分钟)- 同一设备同一类型告警在此时间内不重复发送
|
||||
*/
|
||||
private static final int ALARM_NOTIFICATION_INTERVAL = 30;
|
||||
|
||||
/**
|
||||
* 告警类型常量
|
||||
*/
|
||||
private static final String ALARM_TYPE_DISSOLVED_OXYGEN = "溶解氧";
|
||||
private static final String ALARM_TYPE_TEMPERATURE = "温度";
|
||||
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; // 紧急
|
||||
|
||||
/**
|
||||
* 处理设备属性上报数据
|
||||
*
|
||||
@@ -21,32 +108,60 @@ public class DeviceDataHandler {
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
public void handlePropertyPost(String topic, String payload) {
|
||||
log.info("收到设备属性上报,Topic: {}", topic);
|
||||
|
||||
try {
|
||||
JSONObject data = JSONUtil.parseObj(payload);
|
||||
|
||||
// 解析飞燕平台消息格式
|
||||
String method = data.getStr("method");
|
||||
String id = data.getStr("id");
|
||||
JSONObject params = data.getJSONObject("params");
|
||||
|
||||
log.info("设备属性数据 - Method: {}, ID: {}, Params: {}", method, id, params);
|
||||
|
||||
// TODO: 这里添加您的业务逻辑
|
||||
// 1. 存储到数据库
|
||||
// 2. 触发告警
|
||||
// 3. 推送到前端(通过 WebSocket/SSE)
|
||||
// 4. 数据分析处理
|
||||
|
||||
if (params != null) {
|
||||
params.forEach((key, value) -> {
|
||||
log.info("属性: {} = {}", key, value);
|
||||
// 根据不同属性进行不同处理
|
||||
handleProperty(key, value);
|
||||
});
|
||||
// 从Topic中解析产品Key和设备名称
|
||||
// Topic格式: /sys/{ProductKey}/{DeviceName}/thing/event/property/post
|
||||
String[] topicParts = topic.split("/");
|
||||
if (topicParts.length < 4) {
|
||||
log.warn("设备属性数据 Topic 格式不正确: {}", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
String productKey = topicParts[2];
|
||||
String deviceName = topicParts[3];
|
||||
|
||||
if (productKey == null || productKey.isEmpty() || deviceName == null || deviceName.isEmpty()) {
|
||||
log.warn("设备属性数据 ProductKey 或 DeviceName 为空: {}", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存到 TDengine 时序数据库
|
||||
DeviceSensorData sensorData = null;
|
||||
if (params != null && params.size() > 3) {
|
||||
// // 检查 sensorerrorcode 是否为 6
|
||||
// Object errorCode = params.get("sensorerrorcode");
|
||||
// if (errorCode == null || !"6".equals(String.valueOf(errorCode))) {
|
||||
// log.debug("数据误码不为6,已过滤: {} (errorCode: {})", deviceName, errorCode);
|
||||
// return;
|
||||
// }
|
||||
|
||||
try {
|
||||
sensorData = convertToSensorData(productKey, deviceName, params);
|
||||
if (sensorData != null) {
|
||||
// 1. 插入到 TDengine
|
||||
deviceSensorDataService.batchInsertDeviceSensorData(java.util.Collections.singletonList(sensorData));
|
||||
log.debug("数据已保存: {}", deviceName);
|
||||
|
||||
// 2. 更新设备表的实时数据
|
||||
updateDeviceRealTimeData(deviceName, sensorData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("保存数据失败[{}]: {}", deviceName, e.getMessage());
|
||||
}
|
||||
} else if (params != null && params.size() < 3) {
|
||||
log.debug("数据字段不足3个,已过滤: {} (字段数: {})", deviceName, params.size());
|
||||
}
|
||||
//
|
||||
// // 检查是否触发报警
|
||||
// if (params != null && sensorData != null) {
|
||||
// checkAndTriggerAlarm(productKey, deviceName, sensorData);
|
||||
// }
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理设备属性数据失败", e);
|
||||
}
|
||||
@@ -59,76 +174,602 @@ public class DeviceDataHandler {
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
public void handleEventPost(String topic, String payload) {
|
||||
log.info("收到设备事件上报,Topic: {}", topic);
|
||||
|
||||
try {
|
||||
JSONObject data = JSONUtil.parseObj(payload);
|
||||
|
||||
String method = data.getStr("method");
|
||||
String id = data.getStr("id");
|
||||
JSONObject params = data.getJSONObject("params");
|
||||
|
||||
log.info("设备事件数据 - Method: {}, ID: {}, Params: {}", method, id, params);
|
||||
// 从Topic中解析产品Key、设备名称和事件标识符
|
||||
// Topic格式: /sys/{ProductKey}/{DeviceName}/thing/event/{EventIdentifier}/post
|
||||
String[] topicParts = topic.split("/");
|
||||
if (topicParts.length < 7) {
|
||||
log.warn("设备事件数据 Topic 格式不正确: {}", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
String productKey = topicParts[2];
|
||||
String deviceName = topicParts[3];
|
||||
String eventIdentifier = topicParts[6];
|
||||
|
||||
if (productKey == null || productKey.isEmpty() ||
|
||||
deviceName == null || deviceName.isEmpty() ||
|
||||
eventIdentifier == null || eventIdentifier.isEmpty()) {
|
||||
log.warn("设备事件数据 ProductKey、DeviceName 或 EventIdentifier 为空: {}", topic);
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存到 TDengine 时序数据库
|
||||
DeviceSensorData sensorData = new DeviceSensorData();
|
||||
|
||||
// 时间字段
|
||||
String now = LocalDateTime.now().format(DATETIME_FORMATTER);
|
||||
sensorData.setTime(now);
|
||||
sensorData.setCreateTime(now);
|
||||
|
||||
// 设备标识
|
||||
sensorData.setSerialNum(deviceName);
|
||||
sensorData.setDeviceName(deviceName);
|
||||
|
||||
// 存储完整的事件数据(可以根据需要扩展)
|
||||
// 目前暂不处理事件数据的详细字段映射
|
||||
|
||||
try {
|
||||
deviceSensorDataService.batchInsertDeviceSensorData(java.util.Collections.singletonList(sensorData));
|
||||
log.debug("事件已保存: {} - {}", deviceName, eventIdentifier);
|
||||
} catch (Exception e) {
|
||||
log.error("保存事件失败[{}]: {}", deviceName, e.getMessage());
|
||||
}
|
||||
|
||||
// TODO: 处理设备事件
|
||||
// 例如:故障告警、状态变化等
|
||||
handleDeviceEvent(productKey, deviceName, eventIdentifier, params);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理设备事件数据失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理单个属性
|
||||
* 处理设备事件
|
||||
*
|
||||
* @param propertyName 属性名称
|
||||
* @param propertyValue 属性值
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @param eventIdentifier 事件标识符
|
||||
* @param params 事件参数
|
||||
*/
|
||||
private void handleProperty(String propertyName, Object propertyValue) {
|
||||
// 根据属性名称进行不同的业务处理
|
||||
switch (propertyName) {
|
||||
case "temperature":
|
||||
handleTemperature(propertyValue);
|
||||
private void handleDeviceEvent(String productKey, String deviceName, String eventIdentifier, JSONObject params) {
|
||||
// 根据事件类型进行不同处理
|
||||
switch (eventIdentifier) {
|
||||
case "error":
|
||||
// 故障事件
|
||||
log.warn("设备故障告警: {}", params);
|
||||
// 直接触发电话告警
|
||||
triggerDeviceErrorAlarm(deviceName, params);
|
||||
break;
|
||||
case "humidity":
|
||||
handleHumidity(propertyValue);
|
||||
break;
|
||||
case "status":
|
||||
handleStatus(propertyValue);
|
||||
case "alert":
|
||||
// 告警事件
|
||||
log.warn("设备告警: {}", params);
|
||||
break;
|
||||
default:
|
||||
log.debug("未处理的属性: {} = {}", propertyName, propertyValue);
|
||||
log.debug("未处理的事件类型: {}", eventIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 报警逻辑 ====================
|
||||
|
||||
/**
|
||||
* 检查并触发报警
|
||||
* 根据传感器数据判断是否超过阈值,并触发电话通知
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @param sensorData 传感器数据
|
||||
*/
|
||||
private void checkAndTriggerAlarm(String productKey, String deviceName, DeviceSensorData sensorData) {
|
||||
try {
|
||||
// 查询设备信息,获取设备配置和告警阈值
|
||||
Device device = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getSerialNum, deviceName)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Long deviceId = device.getId();
|
||||
StringBuilder alarmMessage = new StringBuilder();
|
||||
boolean hasAlarm = false;
|
||||
java.util.List<AquAlarmHistory> alarmList = new java.util.ArrayList<>();
|
||||
|
||||
// 1. 检查溶解氧(使用设备配置的阈值)
|
||||
if (sensorData.getDissolvedOxygen() != null && device.getOxyWarnCallOpen() != null && device.getOxyWarnCallOpen() == 1) {
|
||||
Double dissolvedOxygen = sensorData.getDissolvedOxygen();
|
||||
Double oxyWarnLower = device.getOxyWarnLower();
|
||||
|
||||
if (oxyWarnLower != null && dissolvedOxygen < oxyWarnLower) {
|
||||
String content = String.format("溶解氧过低: %.2f mg/L (最低: %.2f)",
|
||||
dissolvedOxygen, oxyWarnLower);
|
||||
alarmMessage.append(content).append("; ");
|
||||
|
||||
AquAlarmHistory alarm = createAlarmHistory(deviceId, deviceName, ALARM_TYPE_DISSOLVED_OXYGEN,
|
||||
ALARM_LEVEL_URGENT, content, dissolvedOxygen, oxyWarnLower);
|
||||
alarmList.add(alarm);
|
||||
hasAlarm = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查水温(使用设备配置的阈值)
|
||||
if (sensorData.getTemperature() != null && device.getTempWarnCallOpen() != null && device.getTempWarnCallOpen() == 1) {
|
||||
Double temperature = sensorData.getTemperature();
|
||||
Double tempWarnLower = device.getTempWarnLower();
|
||||
Double tempWarnUpper = device.getTempWarnUpper();
|
||||
|
||||
if (tempWarnLower != null && temperature < tempWarnLower) {
|
||||
String content = String.format("水温过低: %.2f °C (最低: %.2f)",
|
||||
temperature, tempWarnLower);
|
||||
alarmMessage.append(content).append("; ");
|
||||
|
||||
AquAlarmHistory alarm = createAlarmHistory(deviceId, deviceName, ALARM_TYPE_TEMPERATURE,
|
||||
ALARM_LEVEL_IMPORTANT, content, temperature, tempWarnLower);
|
||||
alarmList.add(alarm);
|
||||
hasAlarm = true;
|
||||
} else if (tempWarnUpper != null && temperature > tempWarnUpper) {
|
||||
String content = String.format("水温过高: %.2f °C (最高: %.2f)",
|
||||
temperature, tempWarnUpper);
|
||||
alarmMessage.append(content).append("; ");
|
||||
|
||||
AquAlarmHistory alarm = createAlarmHistory(deviceId, deviceName, ALARM_TYPE_TEMPERATURE,
|
||||
ALARM_LEVEL_IMPORTANT, content, temperature, tempWarnUpper);
|
||||
alarmList.add(alarm);
|
||||
hasAlarm = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查电池电量(使用设备配置的阈值)
|
||||
if (sensorData.getBattery() != null && device.getBatteryWarnCallOpen() != null && device.getBatteryWarnCallOpen() == 1) {
|
||||
Double battery = sensorData.getBattery();
|
||||
Double batteryWarnLower = device.getBatteryWarnLower() != null ? device.getBatteryWarnLower().doubleValue() : null;
|
||||
|
||||
if (batteryWarnLower != null && battery < batteryWarnLower) {
|
||||
String content = String.format("电池电量低: %.2f%% (最低: %.2f%%)",
|
||||
battery, batteryWarnLower);
|
||||
alarmMessage.append(content).append("; ");
|
||||
|
||||
AquAlarmHistory alarm = createAlarmHistory(deviceId, deviceName, ALARM_TYPE_BATTERY,
|
||||
ALARM_LEVEL_NORMAL, content, battery, batteryWarnLower);
|
||||
alarmList.add(alarm);
|
||||
hasAlarm = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有告警,保存告警历史并触发通知
|
||||
if (hasAlarm) {
|
||||
log.warn("[告警] {} - {}", deviceName, alarmMessage);
|
||||
|
||||
// 保存告警历史
|
||||
for (AquAlarmHistory alarm : alarmList) {
|
||||
alarmHistoryMapper.insert(alarm);
|
||||
}
|
||||
|
||||
// 触发通知(只对紧急和重要告警发送通知)
|
||||
boolean shouldNotify = alarmList.stream()
|
||||
.anyMatch(alarm -> alarm.getAlarmLevel() >= ALARM_LEVEL_IMPORTANT);
|
||||
|
||||
if (shouldNotify) {
|
||||
triggerAlarmNotification(device, alarmMessage.toString(), sensorData, alarmList);
|
||||
}
|
||||
} else {
|
||||
// 没有告警,检查是否有未恢复的告警需要标记为已恢复
|
||||
checkAndRecoverAlarms(deviceId, device, sensorData);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("检查报警失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理温度数据
|
||||
* 触发告警通知
|
||||
* 创建电话通知记录,并发送语音通知
|
||||
*
|
||||
* @param device 设备信息
|
||||
* @param alarmMessage 告警信息
|
||||
* @param sensorData 传感器数据
|
||||
* @param alarmList 告警列表
|
||||
*/
|
||||
private void handleTemperature(Object value) {
|
||||
// 示例:温度告警
|
||||
if (value instanceof Number) {
|
||||
double temp = ((Number) value).doubleValue();
|
||||
if (temp > 80) {
|
||||
log.warn("温度过高告警: {}°C", temp);
|
||||
// TODO: 发送告警通知
|
||||
private void triggerAlarmNotification(Device device, String alarmMessage,
|
||||
DeviceSensorData sensorData, java.util.List<AquAlarmHistory> alarmList) {
|
||||
try {
|
||||
Long deviceId = device.getId();
|
||||
String deviceName = device.getDeviceName();
|
||||
|
||||
// 检查设备的免打扰设置
|
||||
boolean shouldSkip = false;
|
||||
for (AquAlarmHistory alarm : alarmList) {
|
||||
if (ALARM_TYPE_DISSOLVED_OXYGEN.equals(alarm.getAlarmType()) &&
|
||||
device.getOxyWarnCallNoDis() != null && device.getOxyWarnCallNoDis() == 1) {
|
||||
shouldSkip = true;
|
||||
break;
|
||||
}
|
||||
if (ALARM_TYPE_TEMPERATURE.equals(alarm.getAlarmType()) &&
|
||||
device.getTempWarnCallNoDis() != null && device.getTempWarnCallNoDis() == 1) {
|
||||
shouldSkip = true;
|
||||
break;
|
||||
}
|
||||
if (ALARM_TYPE_BATTERY.equals(alarm.getAlarmType()) &&
|
||||
device.getBatteryWarnCallNoDis() != null && device.getBatteryWarnCallNoDis() == 1) {
|
||||
shouldSkip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldSkip) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否在告警间隔时间内已经发送过通知(防止频繁通知)
|
||||
LocalDateTime intervalTime = LocalDateTime.now().minusMinutes(ALARM_NOTIFICATION_INTERVAL);
|
||||
|
||||
// 查询最近的通知记录
|
||||
long recentNoticeCount = warnCallNoticeMapper.selectCount(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AquWarnCallNotice>()
|
||||
.eq(AquWarnCallNotice::getDeviceId, deviceId)
|
||||
.ge(AquWarnCallNotice::getCallTime, intervalTime)
|
||||
);
|
||||
|
||||
if (recentNoticeCount > 0) {
|
||||
log.debug("[告警] {} - {}分钟内已通知", deviceName, ALARM_NOTIFICATION_INTERVAL);
|
||||
|
||||
// 更新告警历史为已通知,但不发送实际通知
|
||||
for (AquAlarmHistory alarm : alarmList) {
|
||||
alarm.setNotified(1);
|
||||
alarm.setUpdatedTime(LocalDateTime.now());
|
||||
alarmHistoryMapper.updateById(alarm);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 从设备信息获取用户信息
|
||||
Long userId = device.getUserId();
|
||||
// TODO: 从用户表查询手机号
|
||||
String mobilePhone = sensorData.getMobilePhone(); // 临时使用传感器数据中的手机号
|
||||
// TODO: 从塘口表查询塘口名称
|
||||
String pondName = deviceName; // 临时使用设备名
|
||||
|
||||
// 验证必要信息
|
||||
if (userId == null || StrUtil.isBlank(mobilePhone)) {
|
||||
log.warn("[告警] {} - 信息不完整", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建告警通知记录
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
// 发送语音通知
|
||||
sendVoiceNotification(callNotice, alarmMessage);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[告警通知] 触发告警通知失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
// 发送语音通知
|
||||
* @param callNotice 通知记录
|
||||
* @param alarmMessage 告警信息
|
||||
*/
|
||||
private void sendVoiceNotification(AquWarnCallNotice callNotice, String alarmMessage) {
|
||||
try {
|
||||
// 构建语音通知参数
|
||||
Map<String, String> ttsParams = new HashMap<>();
|
||||
ttsParams.put("pond_name", callNotice.getPondName() != null ? callNotice.getPondName() : "未知塘口");
|
||||
ttsParams.put("alarm_info", alarmMessage);
|
||||
|
||||
// 生成业务标识(用于回执关联)
|
||||
String outId = "ALARM_" + callNotice.getId() + "_" + System.currentTimeMillis();
|
||||
|
||||
// 调用 VMS 发送语音通知
|
||||
VmsNoticeResponse response = vmsNoticeService.sendTtsCall(
|
||||
callNotice.getMobilePhone(),
|
||||
ttsParams,
|
||||
outId
|
||||
);
|
||||
|
||||
if (response != null && response.isSuccess()) {
|
||||
// 更新通知记录状态
|
||||
callNotice.setCallStatus(1); // 呼叫成功,等待结果反馈
|
||||
callNotice.setCallId(response.getCallId());
|
||||
callNotice.setOutId(outId);
|
||||
callNotice.setUpdatedTime(LocalDateTime.now());
|
||||
warnCallNoticeMapper.updateById(callNotice);
|
||||
|
||||
log.info("[告警] 呼叫成功: {}", callNotice.getMobilePhone());
|
||||
} else {
|
||||
// 呼叫失败
|
||||
callNotice.setCallStatus(2); // 呼叫失败
|
||||
callNotice.setStatusMsg(response != null ? response.getMessage() : "呼叫失败");
|
||||
callNotice.setUpdatedTime(LocalDateTime.now());
|
||||
warnCallNoticeMapper.updateById(callNotice);
|
||||
|
||||
log.error("[告警] 呼叫失败: {} - {}", callNotice.getMobilePhone(), callNotice.getStatusMsg());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[告警] 语音通知异常: {}", e.getMessage());
|
||||
|
||||
// 更新为呼叫失败
|
||||
try {
|
||||
callNotice.setCallStatus(2);
|
||||
callNotice.setStatusMsg("发送异常: " + e.getMessage());
|
||||
callNotice.setUpdatedTime(LocalDateTime.now());
|
||||
warnCallNoticeMapper.updateById(callNotice);
|
||||
} catch (Exception ex) {
|
||||
log.error("[告警] 更新失败: {}", ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理湿度数据
|
||||
* 触发设备故障告警
|
||||
* 针对设备事件(error)的紧急告警
|
||||
*
|
||||
* @param deviceName 设备名称
|
||||
* @param params 事件参数
|
||||
*/
|
||||
private void handleHumidity(Object value) {
|
||||
// 示例:湿度处理逻辑
|
||||
log.debug("湿度数据: {}", value);
|
||||
private void triggerDeviceErrorAlarm(String deviceName, JSONObject params) {
|
||||
try {
|
||||
String errorMsg = params != null ? params.toString() : "设备故障";
|
||||
log.error("[设备故障] 设备: {}, 故障信息: {}", deviceName, errorMsg);
|
||||
|
||||
// TODO: 实现紧急告警逻辑
|
||||
// 1. 查询设备信息和用户
|
||||
// 2. 创建紧急告警记录
|
||||
// 3. 立即发送语音通知
|
||||
// 4. 可选:发送短信、推送消息等多种通知方式
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[设备故障] 触发故障告警失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理状态数据
|
||||
* 创建告警历史记录
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param deviceName 设备名称
|
||||
* @param alarmType 告警类型
|
||||
* @param alarmLevel 告警级别
|
||||
* @param alarmContent 告警内容
|
||||
* @param alarmValue 告警值
|
||||
* @param thresholdValue 阈值
|
||||
* @return 告警历史对象
|
||||
*/
|
||||
private void handleStatus(Object value) {
|
||||
// 示例:设备状态变化
|
||||
log.info("设备状态变化: {}", value);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并标记已恢复的告警
|
||||
* 当传感器数据恢复正常时,将未恢复的告警标记为已恢复
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @param device 设备信息
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 TDengine 传感器数据格式
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @param params 设备参数
|
||||
* @return DeviceSensorData
|
||||
*/
|
||||
private DeviceSensorData convertToSensorData(String productKey, String deviceName, JSONObject params) {
|
||||
try {
|
||||
DeviceSensorData sensorData = new DeviceSensorData();
|
||||
|
||||
// 时间字段
|
||||
String now = LocalDateTime.now().format(DATETIME_FORMATTER);
|
||||
sensorData.setTime(now);
|
||||
sensorData.setCreateTime(now);
|
||||
|
||||
// 设备标识(使用 deviceName 作为 serialNum)
|
||||
sensorData.setSerialNum(deviceName);
|
||||
sensorData.setDeviceName(deviceName);
|
||||
|
||||
// 解析传感器数据(根据实际字段映射)
|
||||
if (params.containsKey("dissolvedoxygen")) {
|
||||
sensorData.setDissolvedOxygen(params.getDouble("dissolvedoxygen"));
|
||||
}
|
||||
if (params.containsKey("temperature")) {
|
||||
sensorData.setTemperature(params.getDouble("temperature"));
|
||||
}
|
||||
if (params.containsKey("saturability")) {
|
||||
sensorData.setSaturability(params.getDouble("saturability"));
|
||||
}
|
||||
if (params.containsKey("ph")) {
|
||||
sensorData.setPh(params.getDouble("ph"));
|
||||
}
|
||||
if (params.containsKey("salinity")) {
|
||||
sensorData.setSalinity(params.getDouble("salinity"));
|
||||
}
|
||||
if (params.containsKey("treference")) {
|
||||
sensorData.setTreference(params.getDouble("treference"));
|
||||
}
|
||||
if (params.containsKey("tfluorescence")) {
|
||||
sensorData.setTfluorescence(params.getDouble("tfluorescence"));
|
||||
}
|
||||
if (params.containsKey("phasedifference")) {
|
||||
sensorData.setPhaseDifference(params.getDouble("phasedifference"));
|
||||
}
|
||||
if (params.containsKey("battery")) {
|
||||
sensorData.setBattery(params.getDouble("battery"));
|
||||
}
|
||||
|
||||
// TODO: 如果需要设置 deviceId、userId 等标签字段,需要从设备管理系统查询
|
||||
// 可以通过 deviceName 或 productKey 查询设备信息
|
||||
// DeviceInfo deviceInfo = deviceService.getByDeviceName(deviceName);
|
||||
// if (deviceInfo != null) {
|
||||
// sensorData.setDeviceId(deviceInfo.getId());
|
||||
// sensorData.setUserId(deviceInfo.getUserId());
|
||||
// sensorData.setUserName(deviceInfo.getUserName());
|
||||
// sensorData.setMobilePhone(deviceInfo.getMobilePhone());
|
||||
// sensorData.setDeviceType(deviceInfo.getDeviceType());
|
||||
// }
|
||||
|
||||
return sensorData;
|
||||
} catch (Exception e) {
|
||||
log.error("转换传感器数据失败: {}", e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新设备表的实时数据
|
||||
*
|
||||
* @param deviceName 设备名称
|
||||
* @param sensorData 传感器数据
|
||||
*/
|
||||
private void updateDeviceRealTimeData(String deviceName, DeviceSensorData sensorData) {
|
||||
try {
|
||||
// 查询设备
|
||||
Device device = deviceMapper.selectOne(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<Device>()
|
||||
.eq(Device::getSerialNum, deviceName)
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
|
||||
if (device == null) {
|
||||
log.debug("设备不存在,无法更新实时数据: {}", deviceName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新实时数据
|
||||
Device updateDevice = new Device();
|
||||
updateDevice.setId(device.getId());
|
||||
|
||||
if (sensorData.getDissolvedOxygen() != null) {
|
||||
updateDevice.setValueDissolvedOxygen(sensorData.getDissolvedOxygen());
|
||||
}
|
||||
if (sensorData.getTemperature() != null) {
|
||||
updateDevice.setValueTemperature(sensorData.getTemperature());
|
||||
}
|
||||
if (sensorData.getSaturability() != null) {
|
||||
updateDevice.setValueSaturability(sensorData.getSaturability());
|
||||
}
|
||||
if (sensorData.getPh() != null) {
|
||||
updateDevice.setValuePh(sensorData.getPh());
|
||||
}
|
||||
if (sensorData.getSalinity() != null) {
|
||||
updateDevice.setValueSalinity(sensorData.getSalinity());
|
||||
}
|
||||
|
||||
// 执行更新
|
||||
int updated = deviceMapper.updateById(updateDevice);
|
||||
if (updated > 0) {
|
||||
log.debug("设备实时数据已更新: {}", deviceName);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("更新设备实时数据失败[{}]: {}", deviceName, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.intc.iot.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.intc.iot.domain.AquAlarmHistory;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 告警历史记录 Mapper
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Mapper
|
||||
public interface AquAlarmHistoryMapper extends BaseMapper<AquAlarmHistory> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.intc.iot.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.intc.iot.domain.AquWarnCallNotice;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 告警电话通知记录 Mapper
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Mapper
|
||||
public interface AquWarnCallNoticeMapper extends BaseMapper<AquWarnCallNotice> {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.intc.iot.mapper;
|
||||
|
||||
import com.intc.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import com.intc.iot.domain.DeviceRealtimeData;
|
||||
import com.intc.iot.domain.vo.DeviceRealtimeDataVo;
|
||||
|
||||
/**
|
||||
* 设备实时数据Mapper接口
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
public interface DeviceRealtimeDataMapper extends BaseMapperPlus<DeviceRealtimeData, DeviceRealtimeDataVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.intc.iot.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.intc.iot.domain.IotDevice;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* IoT设备 Mapper
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Mapper
|
||||
public interface IotDeviceMapper extends BaseMapper<IotDevice> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.intc.iot.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.intc.iot.domain.IotDeviceStatus;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 设备上下线状态 Mapper
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Mapper
|
||||
public interface IotDeviceStatusMapper extends BaseMapper<IotDeviceStatus> {
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.intc.iot.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.intc.iot.domain.VmsCallback;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* VMS 语音回执 Mapper
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Mapper
|
||||
public interface VmsCallbackMapper extends BaseMapper<VmsCallback> {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
/**
|
||||
* AMQP 消息处理器接口
|
||||
*
|
||||
* 用于处理从阿里云 IoT 平台通过 AMQP 推送的设备消息
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
public interface AmqpMessageHandler {
|
||||
|
||||
/**
|
||||
* 处理接收到的 AMQP 消息
|
||||
*
|
||||
* @param message JSON 格式的消息内容
|
||||
*/
|
||||
void handleMessage(String message);
|
||||
|
||||
}
|
||||
@@ -39,4 +39,13 @@ public interface DeviceDataService {
|
||||
*/
|
||||
void unsubscribeDevice(String iotId) throws Exception;
|
||||
|
||||
/**
|
||||
* 保存设备数据(AMQP 推送的数据)
|
||||
*
|
||||
* @param deviceName 设备名称
|
||||
* @param productKey 产品Key
|
||||
* @param data 设备数据
|
||||
*/
|
||||
void saveDeviceData(String deviceName, String productKey, Object data);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
import com.intc.common.mybatis.core.page.PageQuery;
|
||||
import com.intc.common.mybatis.core.page.TableDataInfo;
|
||||
import com.intc.iot.domain.bo.DeviceRealtimeDataBo;
|
||||
import com.intc.iot.domain.vo.DeviceRealtimeDataVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备实时数据服务接口
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
public interface DeviceRealtimeDataService {
|
||||
|
||||
/**
|
||||
* 查询设备实时数据列表(分页)
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @param pageQuery 分页参数
|
||||
* @return 设备实时数据列表
|
||||
*/
|
||||
TableDataInfo<DeviceRealtimeDataVo> queryPageList(DeviceRealtimeDataBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询设备实时数据列表
|
||||
*
|
||||
* @param bo 查询条件
|
||||
* @return 设备实时数据列表
|
||||
*/
|
||||
List<DeviceRealtimeDataVo> queryList(DeviceRealtimeDataBo bo);
|
||||
|
||||
/**
|
||||
* 根据ID查询设备实时数据
|
||||
*
|
||||
* @param id 主键ID
|
||||
* @return 设备实时数据
|
||||
*/
|
||||
DeviceRealtimeDataVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询设备最新数据
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 最新数据
|
||||
*/
|
||||
DeviceRealtimeDataVo queryLatestData(String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 查询设备最新属性数据
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 最新属性数据
|
||||
*/
|
||||
DeviceRealtimeDataVo queryLatestPropertyData(String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 删除设备实时数据
|
||||
*
|
||||
* @param ids 主键ID数组
|
||||
* @return 是否成功
|
||||
*/
|
||||
Boolean deleteByIds(Long[] ids);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
import com.intc.iot.domain.IotDeviceStatus;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设备状态服务接口
|
||||
* 对应 C# 的设备上下线状态处理
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
public interface DeviceStatusService {
|
||||
|
||||
/**
|
||||
* 处理设备状态变更事件
|
||||
* 对应 C# IOTTopicDeviceStatus 的处理
|
||||
*
|
||||
* @param statusData 状态数据
|
||||
*/
|
||||
void handleStatusChange(Map<String, Object> statusData);
|
||||
|
||||
/**
|
||||
* 查询设备当前状态
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 设备状态
|
||||
*/
|
||||
IotDeviceStatus queryDeviceStatus(String productKey, String deviceName);
|
||||
|
||||
/**
|
||||
* 查询设备是否在线
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return true-在线,false-离线
|
||||
*/
|
||||
boolean isDeviceOnline(String productKey, String deviceName);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 云端操作服务(Java 版等价封装)。
|
||||
*
|
||||
* 封装常用的设备查询、属性查询与属性下发能力,
|
||||
* 底层基于 {@link com.intc.iot.service.IotDeviceService} 和阿里云 IoT Java SDK。
|
||||
*/
|
||||
public interface IotCloudService {
|
||||
|
||||
// ================= 按 productKey + deviceName 操作 =================
|
||||
|
||||
/**
|
||||
* 根据 productKey + deviceName 查询设备详情。
|
||||
*/
|
||||
Map<String, Object> getDeviceInfo(String productKey, String deviceName) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 productKey + deviceName 查询设备属性状态。
|
||||
*/
|
||||
Map<String, Object> getDeviceProperties(String productKey, String deviceName) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 productKey + deviceName 设置设备属性。
|
||||
*/
|
||||
boolean setProperty(String productKey, String deviceName, Map<String, Object> properties, boolean checkSuccess, int retryCount) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 productKey + deviceName 调用设备服务。
|
||||
*/
|
||||
Map<String, Object> invokeService(String productKey, String deviceName, String identifier, String args) throws Exception;
|
||||
|
||||
// ================= 按 iotId 操作 =================
|
||||
|
||||
/**
|
||||
* 根据 iotId 查询设备详情。
|
||||
*/
|
||||
Map<String, Object> getDeviceInfo(String iotId) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 iotId 查询设备属性状态。
|
||||
*/
|
||||
Map<String, Object> getDeviceProperties(String iotId) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 iotId 设置设备属性。
|
||||
*/
|
||||
boolean setProperty(String iotId, Map<String, Object> properties, boolean checkSuccess, int retryCount) throws Exception;
|
||||
|
||||
/**
|
||||
* 调用设备服务(如自定义服务能力)。
|
||||
*/
|
||||
Map<String, Object> invokeService(String iotId, String identifier, String args) throws Exception;
|
||||
}
|
||||
@@ -12,12 +12,23 @@ public interface IotDeviceService {
|
||||
/**
|
||||
* 查询设备列表
|
||||
*
|
||||
* @param productKey 产品Key(可选)
|
||||
* @param pageNo 页码
|
||||
* @param pageSize 每页大小
|
||||
* @return 设备列表
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
Map<String, Object> queryDeviceList(Integer pageNo, Integer pageSize) throws Exception;
|
||||
Map<String, Object> queryDeviceList(String productKey, Integer pageNo, Integer pageSize) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据 ProductKey 和 DeviceName 查询设备信息
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return 设备详情(包含 iotId 等)
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
Map<String, Object> findDeviceByProductKeyAndName(String productKey, String deviceName) throws Exception;
|
||||
|
||||
/**
|
||||
* 查询设备详情
|
||||
@@ -67,4 +78,14 @@ public interface IotDeviceService {
|
||||
*/
|
||||
Map<String, Object> unbindDevice(String iotId) throws Exception;
|
||||
|
||||
/**
|
||||
* 查询设备物模型(模板)
|
||||
* 对应 C# IIOTCloudService.GetTemplate
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @return 物模型数据
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
Map<String, Object> queryThingModel(String productKey) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@@ -34,4 +34,14 @@ public interface MqttService {
|
||||
*/
|
||||
void unsubscribe(String topic) throws Exception;
|
||||
|
||||
/**
|
||||
* 订阅设备状态 Topic
|
||||
* 对应 C# 的 /as/mqtt/status/ Topic 订阅
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
void subscribeDeviceStatus(String productKey, String deviceName) throws Exception;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
/**
|
||||
* VMS MNS 回执消费服务接口
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
public interface VmsMnsConsumerService {
|
||||
|
||||
/**
|
||||
* 启动 MNS 消费者
|
||||
*
|
||||
* @throws Exception 启动异常
|
||||
*/
|
||||
void start() throws Exception;
|
||||
|
||||
/**
|
||||
* 停止 MNS 消费者
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* 判断是否正在运行
|
||||
*
|
||||
* @return true: 运行中, false: 已停止
|
||||
*/
|
||||
boolean isRunning();
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
import com.intc.iot.domain.VmsNoticeResponse;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 语音电话通知服务(VMS)。
|
||||
*/
|
||||
public interface VmsNoticeService {
|
||||
|
||||
/**
|
||||
* 发送语音 TTS 呼叫(验证码或通知)。
|
||||
*
|
||||
* @param phoneNum 被叫手机号
|
||||
* @param params 模板参数键值对
|
||||
* @param outId 业务唯一标识(会在回执中返回)
|
||||
* @return 调用结果
|
||||
* @throws Exception 调用阿里云接口失败
|
||||
*/
|
||||
VmsNoticeResponse sendTtsCall(String phoneNum, Map<String, String> params, String outId) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.intc.iot.service;
|
||||
|
||||
import com.intc.iot.domain.VmsCallback;
|
||||
|
||||
/**
|
||||
* 告警电话通知服务接口
|
||||
* 对应 C# AlarmProcessor 中的 UpdateCallNoticeFromCallback 逻辑
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
public interface WarnCallNoticeService {
|
||||
|
||||
/**
|
||||
* 处理 VMS 回执,更新告警通知记录
|
||||
* 对应 C# ProcessVmsCallbacks + UpdateCallNoticeFromCallback
|
||||
*
|
||||
* @param callback VMS 回执
|
||||
* @return true: 处理成功, false: 未找到对应记录
|
||||
*/
|
||||
boolean processVmsCallback(VmsCallback callback);
|
||||
|
||||
/**
|
||||
* 批量处理未处理的 VMS 回执
|
||||
* 由定时任务调用
|
||||
*
|
||||
* @return 处理的回执数量
|
||||
*/
|
||||
int processUnhandledCallbacks();
|
||||
|
||||
/**
|
||||
* 移除指定设备的待通知记录
|
||||
* 当某个设备的语音通知成功后,移除该设备的其他待通知记录
|
||||
* 对应 C# RemovePendingNotificationsForDevice
|
||||
*
|
||||
* @param deviceId 设备ID
|
||||
* @return 移除的记录数量
|
||||
*/
|
||||
int removePendingNotificationsByDevice(Long deviceId);
|
||||
|
||||
/**
|
||||
* 清理超时的通知记录
|
||||
* 对应 C# CleanupExpiredNotifications
|
||||
*
|
||||
* 清理规则:
|
||||
* - 已呼叫成功(callStatus=1)但超过超时时间未收到回执
|
||||
* - 超时时间:3 分钟(与 C# 保持一致)
|
||||
*
|
||||
* @return 清理的记录数量
|
||||
*/
|
||||
int cleanupExpiredNotifications();
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.intc.iot.handler.DeviceDataHandler;
|
||||
import com.intc.iot.service.AmqpMessageHandler;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* AMQP 消息处理器实现类
|
||||
*
|
||||
* 处理从阿里云 IoT 平台推送的设备消息,包括:
|
||||
* - 设备上报数据
|
||||
* - 设备上线/下线
|
||||
* - 设备状态变化
|
||||
* - 设备属性变化
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AmqpMessageHandlerImpl implements AmqpMessageHandler {
|
||||
|
||||
@Autowired(required = false)
|
||||
private DeviceDataHandler deviceDataHandler;
|
||||
|
||||
@Override
|
||||
public void handleMessage(String message) {
|
||||
try {
|
||||
JSONObject json = JSONUtil.parseObj(message);
|
||||
|
||||
// 检查是否包含 items 字段(设备属性数据)
|
||||
if (json.containsKey("items")) {
|
||||
// 直接处理设备属性数据
|
||||
handlePropertyMessage(json);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取消息类型
|
||||
String messageType = json.getStr("messageType");
|
||||
|
||||
if (messageType == null) {
|
||||
// 如果没有 messageType,可能是飞燕平台的原始消息格式
|
||||
// 尝试从 topic 或 method 判断消息类型
|
||||
String topic = json.getStr("topic");
|
||||
String method = json.getStr("method");
|
||||
|
||||
if (topic != null) {
|
||||
// 根据 topic 判断消息类型
|
||||
if (topic.contains("/property/post")) {
|
||||
handlePropertyMessageFromTopic(topic, message);
|
||||
} else if (topic.contains("/event/")) {
|
||||
handleEventMessageFromTopic(topic, message);
|
||||
} else {
|
||||
log.warn("未知的 Topic 格式: {}", topic);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (messageType) {
|
||||
case "property":
|
||||
// 设备属性上报
|
||||
handlePropertyMessage(json);
|
||||
break;
|
||||
case "event":
|
||||
// 设备事件上报
|
||||
handleEventMessage(json);
|
||||
break;
|
||||
case "status":
|
||||
// 设备状态变化
|
||||
handleStatusMessage(json);
|
||||
break;
|
||||
case "deviceLifeCycle":
|
||||
// 设备生命周期(上线/下线)
|
||||
handleLifeCycleMessage(json);
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的消息类型: {}", messageType);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理 AMQP 消息失败: {}", message, e);
|
||||
throw new RuntimeException("消息处理失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备属性上报消息
|
||||
*/
|
||||
private void handlePropertyMessage(JSONObject json) {
|
||||
String deviceName = json.getStr("deviceName");
|
||||
String productKey = json.getStr("productKey");
|
||||
JSONObject items = json.getJSONObject("items");
|
||||
|
||||
// 如果没有 DeviceDataHandler,只记录日志
|
||||
if (deviceDataHandler == null) {
|
||||
log.warn("设备数据处理器未配置,跳过数据处理");
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用 DeviceDataHandler 处理数据(包括存储和报警)
|
||||
try {
|
||||
// 转换 items 格式:{"dissolvedOxygen": {"value": 7.82, "time": xxx}} -> {"dissolvedOxygen": 7.82}
|
||||
JSONObject params = new JSONObject();
|
||||
if (items != null) {
|
||||
for (String key : items.keySet()) {
|
||||
Object itemValue = items.get(key);
|
||||
if (itemValue instanceof JSONObject) {
|
||||
JSONObject itemObj = (JSONObject) itemValue;
|
||||
// 提取 value 字段
|
||||
if (itemObj.containsKey("value")) {
|
||||
// 字段名映射:currentTemperature -> temperature, dosat -> saturability
|
||||
String mappedKey = mapFieldName(key);
|
||||
params.set(mappedKey, itemObj.get("value"));
|
||||
}
|
||||
} else {
|
||||
// 如果不是嵌套格式,直接使用
|
||||
String mappedKey = mapFieldName(key);
|
||||
params.set(mappedKey, itemValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构造 topic 格式:/sys/{ProductKey}/{DeviceName}/thing/event/property/post
|
||||
String topic = String.format("/sys/%s/%s/thing/event/property/post", productKey, deviceName);
|
||||
// 构造飞燕平台消息格式
|
||||
JSONObject message = new JSONObject();
|
||||
message.set("method", "thing.event.property.post");
|
||||
message.set("params", params);
|
||||
|
||||
deviceDataHandler.handlePropertyPost(topic, message.toString());
|
||||
} catch (Exception e) {
|
||||
log.error("使用 DeviceDataHandler 处理属性数据失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段名映射:将阿里云字段名映射为应用字段名
|
||||
*/
|
||||
private String mapFieldName(String fieldName) {
|
||||
switch (fieldName) {
|
||||
case "currentTemperature":
|
||||
return "temperature";
|
||||
case "dosat":
|
||||
return "saturability";
|
||||
case "Treference":
|
||||
return "treference";
|
||||
case "Tfluorescence":
|
||||
return "tfluorescence";
|
||||
case "Tcorrect":
|
||||
return "tcorrect";
|
||||
case "Tsignal":
|
||||
return "tsignal";
|
||||
case "salinitySet":
|
||||
return "salinity";
|
||||
default:
|
||||
// 其他字段名保持不变(转为小写)
|
||||
return fieldName.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理从 Topic 判断的属性消息(飞燕平台原始格式)
|
||||
*/
|
||||
private void handlePropertyMessageFromTopic(String topic, String payload) {
|
||||
if (deviceDataHandler == null) {
|
||||
log.warn("设备数据处理器未配置,跳过数据处理");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
deviceDataHandler.handlePropertyPost(topic, payload);
|
||||
} catch (Exception e) {
|
||||
log.error("处理属性消息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备事件消息
|
||||
*/
|
||||
private void handleEventMessage(JSONObject json) {
|
||||
String deviceName = json.getStr("deviceName");
|
||||
String productKey = json.getStr("productKey");
|
||||
String identifier = json.getStr("identifier");
|
||||
JSONObject value = json.getJSONObject("value");
|
||||
|
||||
if (deviceDataHandler == null) {
|
||||
log.warn("设备数据处理器未配置,跳过事件处理");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 构造 topic 格式:/sys/{ProductKey}/{DeviceName}/thing/event/{EventIdentifier}/post
|
||||
String topic = String.format("/sys/%s/%s/thing/event/%s/post", productKey, deviceName, identifier);
|
||||
// 构造飞燕平台消息格式
|
||||
JSONObject message = new JSONObject();
|
||||
message.set("method", "thing.event." + identifier + ".post");
|
||||
message.set("params", value);
|
||||
|
||||
deviceDataHandler.handleEventPost(topic, message.toString());
|
||||
} catch (Exception e) {
|
||||
log.error("处理设备事件失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理从 Topic 判断的事件消息(飞燕平台原始格式)
|
||||
*/
|
||||
private void handleEventMessageFromTopic(String topic, String payload) {
|
||||
if (deviceDataHandler == null) {
|
||||
log.warn("设备数据处理器未配置,跳过事件处理");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
deviceDataHandler.handleEventPost(topic, payload);
|
||||
} catch (Exception e) {
|
||||
log.error("处理事件消息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备状态变化消息
|
||||
*/
|
||||
private void handleStatusMessage(JSONObject json) {
|
||||
String deviceName = json.getStr("deviceName");
|
||||
String status = json.getStr("status");
|
||||
|
||||
log.debug("设备状态: {} - {}", deviceName, status);
|
||||
|
||||
// TODO: 更新设备状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备生命周期消息(上线/下线)
|
||||
*/
|
||||
private void handleLifeCycleMessage(JSONObject json) {
|
||||
String deviceName = json.getStr("deviceName");
|
||||
String action = json.getStr("action");
|
||||
|
||||
log.debug("设备{}: {}", "online".equals(action) ? "上线" : "下线", deviceName);
|
||||
|
||||
// action: "online" 或 "offline"
|
||||
// TODO: 更新设备在线状态
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
@@ -17,6 +18,7 @@ import org.springframework.stereotype.Service;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnClass(MqttClient.class)
|
||||
@ConditionalOnBean(MqttClient.class)
|
||||
public class DeviceDataServiceImpl implements DeviceDataService {
|
||||
|
||||
@@ -44,24 +46,14 @@ public class DeviceDataServiceImpl implements DeviceDataService {
|
||||
public void subscribeDeviceProperties(String iotId) throws Exception {
|
||||
// 注意:需要根据 iotId 获取 productKey 和 deviceName
|
||||
// 这里简化处理,实际应该从数据库或缓存中查询
|
||||
log.info("订阅设备属性上报,IotId: {}", iotId);
|
||||
|
||||
// 示例:假设从设备信息中获取
|
||||
// String productKey = getProductKeyByIotId(iotId);
|
||||
// String deviceName = getDeviceNameByIotId(iotId);
|
||||
// String topic = String.format(PROPERTY_POST_TOPIC, productKey, deviceName);
|
||||
|
||||
// mqttService.subscribe(topic, 1);
|
||||
|
||||
log.warn("请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
log.warn("订阅设备属性上报功能未实现,请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
throw new UnsupportedOperationException("订阅设备属性功能未实现,请使用 subscribeAllDevices 方法");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribeDeviceEvents(String iotId) throws Exception {
|
||||
log.info("订阅设备事件上报,IotId: {}", iotId);
|
||||
|
||||
// 类似属性订阅,需要映射关系
|
||||
log.warn("请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
log.warn("订阅设备事件上报功能未实现,请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
throw new UnsupportedOperationException("订阅设备事件功能未实现,请使用 subscribeAllDevices 方法");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,10 +68,22 @@ public class DeviceDataServiceImpl implements DeviceDataService {
|
||||
|
||||
@Override
|
||||
public void unsubscribeDevice(String iotId) throws Exception {
|
||||
log.info("取消订阅设备数据,IotId: {}", iotId);
|
||||
log.warn("取消订阅设备数据功能未实现,请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
throw new UnsupportedOperationException("取消订阅设备功能未实现");
|
||||
}
|
||||
|
||||
// 需要取消对应的 Topic 订阅
|
||||
log.warn("请先实现 iotId 到 productKey/deviceName 的映射逻辑");
|
||||
@Override
|
||||
public void saveDeviceData(String deviceName, String productKey, Object data) {
|
||||
log.info("保存设备数据 - 设备: {}, 产品: {}, 数据: {}", deviceName, productKey, data);
|
||||
|
||||
// TODO: 实现数据保存逻辑
|
||||
// 1. 保存到 MySQL/PostgreSQL(结构化数据)
|
||||
// 2. 保存到 TDengine(时序数据)
|
||||
// 3. 触发告警逻辑
|
||||
// 4. 发送通知
|
||||
|
||||
// 示例:保存到实时数据表
|
||||
// deviceRealtimeDataService.save(deviceName, productKey, data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.intc.common.core.utils.MapstructUtils;
|
||||
import com.intc.common.mybatis.core.page.PageQuery;
|
||||
import com.intc.common.mybatis.core.page.TableDataInfo;
|
||||
import com.intc.iot.domain.DeviceRealtimeData;
|
||||
import com.intc.iot.domain.bo.DeviceRealtimeDataBo;
|
||||
import com.intc.iot.domain.vo.DeviceRealtimeDataVo;
|
||||
import com.intc.iot.mapper.DeviceRealtimeDataMapper;
|
||||
import com.intc.iot.service.DeviceRealtimeDataService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设备实时数据服务实现
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceRealtimeDataServiceImpl implements DeviceRealtimeDataService {
|
||||
|
||||
private final DeviceRealtimeDataMapper deviceRealtimeDataMapper;
|
||||
|
||||
/**
|
||||
* 构建查询条件
|
||||
*/
|
||||
private LambdaQueryWrapper<DeviceRealtimeData> buildQueryWrapper(DeviceRealtimeDataBo bo) {
|
||||
LambdaQueryWrapper<DeviceRealtimeData> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(ObjectUtil.isNotEmpty(bo.getProductKey()), DeviceRealtimeData::getProductKey, bo.getProductKey())
|
||||
.eq(ObjectUtil.isNotEmpty(bo.getDeviceName()), DeviceRealtimeData::getDeviceName, bo.getDeviceName())
|
||||
.eq(ObjectUtil.isNotEmpty(bo.getIotId()), DeviceRealtimeData::getIotId, bo.getIotId())
|
||||
.eq(ObjectUtil.isNotEmpty(bo.getDataType()), DeviceRealtimeData::getDataType, bo.getDataType())
|
||||
.eq(ObjectUtil.isNotEmpty(bo.getIdentifier()), DeviceRealtimeData::getIdentifier, bo.getIdentifier())
|
||||
.ge(ObjectUtil.isNotEmpty(bo.getStartTime()), DeviceRealtimeData::getReportTime, bo.getStartTime())
|
||||
.le(ObjectUtil.isNotEmpty(bo.getEndTime()), DeviceRealtimeData::getReportTime, bo.getEndTime())
|
||||
.orderByDesc(DeviceRealtimeData::getReportTime);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TableDataInfo<DeviceRealtimeDataVo> queryPageList(DeviceRealtimeDataBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<DeviceRealtimeData> wrapper = buildQueryWrapper(bo);
|
||||
Page<DeviceRealtimeDataVo> page = deviceRealtimeDataMapper.selectVoPage(pageQuery.build(), wrapper);
|
||||
return TableDataInfo.build(page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DeviceRealtimeDataVo> queryList(DeviceRealtimeDataBo bo) {
|
||||
LambdaQueryWrapper<DeviceRealtimeData> wrapper = buildQueryWrapper(bo);
|
||||
return deviceRealtimeDataMapper.selectVoList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceRealtimeDataVo queryById(Long id) {
|
||||
return deviceRealtimeDataMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceRealtimeDataVo queryLatestData(String productKey, String deviceName) {
|
||||
LambdaQueryWrapper<DeviceRealtimeData> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(DeviceRealtimeData::getProductKey, productKey)
|
||||
.eq(DeviceRealtimeData::getDeviceName, deviceName)
|
||||
.orderByDesc(DeviceRealtimeData::getReportTime)
|
||||
.last("LIMIT 1");
|
||||
return deviceRealtimeDataMapper.selectVoOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceRealtimeDataVo queryLatestPropertyData(String productKey, String deviceName) {
|
||||
LambdaQueryWrapper<DeviceRealtimeData> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(DeviceRealtimeData::getProductKey, productKey)
|
||||
.eq(DeviceRealtimeData::getDeviceName, deviceName)
|
||||
.eq(DeviceRealtimeData::getDataType, "property")
|
||||
.orderByDesc(DeviceRealtimeData::getReportTime)
|
||||
.last("LIMIT 1");
|
||||
return deviceRealtimeDataMapper.selectVoOne(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean deleteByIds(Long[] ids) {
|
||||
int rows = deviceRealtimeDataMapper.deleteBatchIds(Arrays.asList(ids));
|
||||
return rows > 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.intc.iot.domain.IotDeviceStatus;
|
||||
import com.intc.iot.mapper.IotDeviceStatusMapper;
|
||||
import com.intc.iot.service.DeviceStatusService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设备状态服务实现
|
||||
* 对应 C# IOTTopicDeviceStatus 的处理逻辑
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(IotDeviceStatusMapper.class)
|
||||
@Slf4j
|
||||
public class DeviceStatusServiceImpl implements DeviceStatusService {
|
||||
|
||||
private final IotDeviceStatusMapper deviceStatusMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void handleStatusChange(Map<String, Object> statusData) {
|
||||
try {
|
||||
String productKey = (String) statusData.get("productKey");
|
||||
String deviceName = (String) statusData.get("deviceName");
|
||||
String status = (String) statusData.get("status");
|
||||
Long statusTime = getLongValue(statusData, "statusTime");
|
||||
Long lastTime = getLongValue(statusData, "lastTime");
|
||||
String clientIp = (String) statusData.get("clientIp");
|
||||
|
||||
if (productKey == null || deviceName == null || status == null) {
|
||||
log.warn("[设备状态] 状态数据不完整,跳过处理: {}", JSONUtil.toJsonStr(statusData));
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("[设备状态] 设备状态变更 - ProductKey: {}, DeviceName: {}, Status: {}",
|
||||
productKey, deviceName, status);
|
||||
|
||||
// 查询是否已存在记录
|
||||
IotDeviceStatus existingStatus = deviceStatusMapper.selectOne(
|
||||
new LambdaQueryWrapper<IotDeviceStatus>()
|
||||
.eq(IotDeviceStatus::getProductKey, productKey)
|
||||
.eq(IotDeviceStatus::getDeviceName, deviceName)
|
||||
);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
if (existingStatus != null) {
|
||||
// 更新现有记录
|
||||
existingStatus.setStatus(status);
|
||||
existingStatus.setStatusTime(statusTime);
|
||||
existingStatus.setLastTime(lastTime);
|
||||
existingStatus.setClientIp(clientIp);
|
||||
existingStatus.setUpdatedTime(now);
|
||||
deviceStatusMapper.updateById(existingStatus);
|
||||
|
||||
log.debug("[设备状态] 更新设备状态记录 - ID: {}, Status: {}", existingStatus.getId(), status);
|
||||
} else {
|
||||
// 新增记录
|
||||
IotDeviceStatus newStatus = new IotDeviceStatus();
|
||||
newStatus.setProductKey(productKey);
|
||||
newStatus.setDeviceName(deviceName);
|
||||
newStatus.setIotId((String) statusData.get("iotId"));
|
||||
newStatus.setStatus(status);
|
||||
newStatus.setStatusTime(statusTime);
|
||||
newStatus.setLastTime(lastTime);
|
||||
newStatus.setClientIp(clientIp);
|
||||
newStatus.setCreatedTime(now);
|
||||
newStatus.setUpdatedTime(now);
|
||||
deviceStatusMapper.insert(newStatus);
|
||||
|
||||
log.info("[设备状态] 新增设备状态记录 - ProductKey: {}, DeviceName: {}, Status: {}",
|
||||
productKey, deviceName, status);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[设备状态] 处理设备状态变更异常: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotDeviceStatus queryDeviceStatus(String productKey, String deviceName) {
|
||||
return deviceStatusMapper.selectOne(
|
||||
new LambdaQueryWrapper<IotDeviceStatus>()
|
||||
.eq(IotDeviceStatus::getProductKey, productKey)
|
||||
.eq(IotDeviceStatus::getDeviceName, deviceName)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeviceOnline(String productKey, String deviceName) {
|
||||
IotDeviceStatus status = queryDeviceStatus(productKey, deviceName);
|
||||
return status != null && "online".equalsIgnoreCase(status.getStatus());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.aliyuncs.IAcsClient;
|
||||
import com.intc.iot.service.IotCloudService;
|
||||
import com.intc.iot.service.IotDeviceService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 云端操作服务实现。
|
||||
*
|
||||
* 基于 {@link IotDeviceService} 封装常用的设备查询、属性查询与属性下发能力,
|
||||
* 方便业务侧直接按 iotId 进行操作。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean({IAcsClient.class, IotDeviceService.class})
|
||||
public class IotCloudServiceImpl implements IotCloudService {
|
||||
|
||||
private final IotDeviceService iotDeviceService;
|
||||
|
||||
/**
|
||||
* 根据 productKey + deviceName 解析 iotId。
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private String resolveIotId(String productKey, String deviceName) throws Exception {
|
||||
Map<String, Object> resp = iotDeviceService.findDeviceByProductKeyAndName(productKey, deviceName);
|
||||
Object successObj = resp.get("success");
|
||||
boolean success = successObj instanceof Boolean && (Boolean) successObj;
|
||||
if (!success) {
|
||||
log.warn("[IotCloudService] 根据 productKey={} deviceName={} 查询设备失败: {}", productKey, deviceName, resp);
|
||||
return null;
|
||||
}
|
||||
|
||||
Object dataObj = resp.get("data");
|
||||
if (dataObj == null) {
|
||||
log.warn("[IotCloudService] 查询设备返回数据为空");
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理阿里云 SDK 返回的数据结构
|
||||
try {
|
||||
// 尝试获取 deviceList
|
||||
List<?> deviceList = null;
|
||||
if (dataObj instanceof Map) {
|
||||
Map<String, Object> dataMap = (Map<String, Object>) dataObj;
|
||||
Object listObj = dataMap.get("deviceList");
|
||||
if (listObj instanceof List) {
|
||||
deviceList = (List<?>) listObj;
|
||||
}
|
||||
} else {
|
||||
// 如果 dataObj 本身有 getDeviceList 方法(通过反射调用)
|
||||
java.lang.reflect.Method method = dataObj.getClass().getMethod("getDeviceList");
|
||||
Object result = method.invoke(dataObj);
|
||||
if (result instanceof List) {
|
||||
deviceList = (List<?>) result;
|
||||
}
|
||||
}
|
||||
|
||||
if (deviceList == null || deviceList.isEmpty()) {
|
||||
log.warn("[IotCloudService] 未找到设备, productKey={}, deviceName={}", productKey, deviceName);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取第一个设备的 iotId
|
||||
Object deviceObj = deviceList.get(0);
|
||||
if (deviceObj instanceof Map) {
|
||||
Map<String, Object> deviceMap = (Map<String, Object>) deviceObj;
|
||||
Object iotIdObj = deviceMap.get("iotId");
|
||||
return iotIdObj != null ? iotIdObj.toString() : null;
|
||||
} else {
|
||||
// 通过反射获取 iotId
|
||||
java.lang.reflect.Method method = deviceObj.getClass().getMethod("getIotId");
|
||||
Object iotIdObj = method.invoke(deviceObj);
|
||||
return iotIdObj != null ? iotIdObj.toString() : null;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[IotCloudService] 解析设备信息失败, productKey={}, deviceName={}", productKey, deviceName, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDeviceInfo(String productKey, String deviceName) throws Exception {
|
||||
String iotId = resolveIotId(productKey, deviceName);
|
||||
if (iotId == null) {
|
||||
return null;
|
||||
}
|
||||
return getDeviceInfo(iotId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDeviceProperties(String productKey, String deviceName) throws Exception {
|
||||
String iotId = resolveIotId(productKey, deviceName);
|
||||
if (iotId == null) {
|
||||
return null;
|
||||
}
|
||||
return getDeviceProperties(iotId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setProperty(String productKey, String deviceName, Map<String, Object> properties, boolean checkSuccess, int retryCount) throws Exception {
|
||||
String iotId = resolveIotId(productKey, deviceName);
|
||||
if (iotId == null) {
|
||||
return false;
|
||||
}
|
||||
return setProperty(iotId, properties, checkSuccess, retryCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> invokeService(String productKey, String deviceName, String identifier, String args) throws Exception {
|
||||
String iotId = resolveIotId(productKey, deviceName);
|
||||
if (iotId == null) {
|
||||
return null;
|
||||
}
|
||||
return invokeService(iotId, identifier, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDeviceInfo(String iotId) throws Exception {
|
||||
log.info("[IotCloudService] 查询设备详情, iotId={}", iotId);
|
||||
return iotDeviceService.queryDeviceInfo(iotId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDeviceProperties(String iotId) throws Exception {
|
||||
log.info("[IotCloudService] 查询设备属性, iotId={}", iotId);
|
||||
return iotDeviceService.queryDeviceProperties(iotId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setProperty(String iotId, Map<String, Object> properties, boolean checkSuccess, int retryCount) throws Exception {
|
||||
log.info("[IotCloudService] 设置设备属性, iotId={}, properties={} ", iotId, properties);
|
||||
|
||||
// 将属性 Map 转为阿里云 SDK 需要的 JSON 字符串
|
||||
String itemsJson = JSONUtil.toJsonStr(properties);
|
||||
|
||||
int attempt = 0;
|
||||
while (true) {
|
||||
Map<String, Object> resp = iotDeviceService.setDeviceProperty(iotId, itemsJson);
|
||||
Object successObj = resp.get("success");
|
||||
boolean success = successObj instanceof Boolean && (Boolean) successObj;
|
||||
|
||||
if (!checkSuccess) {
|
||||
// 不强制校验 success,直接按照返回值判断即可
|
||||
return success;
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return true;
|
||||
}
|
||||
|
||||
attempt++;
|
||||
if (attempt > retryCount) {
|
||||
log.warn("[IotCloudService] 设置设备属性失败且重试次数耗尽, iotId={}, properties={}, resp={}", iotId, properties, resp);
|
||||
return false;
|
||||
}
|
||||
|
||||
log.warn("[IotCloudService] 设置设备属性失败, 准备重试 {}/{} 次, iotId={}, properties={}, resp={}", attempt, retryCount, iotId, properties, resp);
|
||||
try {
|
||||
Thread.sleep(500L);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> invokeService(String iotId, String identifier, String args) throws Exception {
|
||||
log.info("[IotCloudService] 调用设备服务, iotId={}, identifier={} ", iotId, identifier);
|
||||
return iotDeviceService.invokeService(iotId, identifier, args);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.intc.iot.service.IotDeviceService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -20,6 +21,7 @@ import java.util.Map;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnClass(IAcsClient.class)
|
||||
@ConditionalOnBean(IAcsClient.class)
|
||||
public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
|
||||
@@ -27,15 +29,27 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
private final AliyunIotProperties iotProperties;
|
||||
|
||||
@Override
|
||||
public Map<String, Object> queryDeviceList(Integer pageNo, Integer pageSize) throws Exception {
|
||||
log.info("查询设备列表,页码: {}, 每页大小: {}", pageNo, pageSize);
|
||||
public Map<String, Object> queryDeviceList(String productKey, Integer pageNo, Integer pageSize) throws Exception {
|
||||
log.info("查询设备列表,ProductKey: {}, 页码: {}, 每页大小: {}", productKey, pageNo, pageSize);
|
||||
|
||||
QueryDeviceRequest request = new QueryDeviceRequest();
|
||||
// ProductKey 是必填参数,如果为空则报错
|
||||
if (productKey == null || productKey.isEmpty()) {
|
||||
log.error("查询设备列表失败:ProductKey 不能为空");
|
||||
throw new IllegalArgumentException("ProductKey is mandatory for this action");
|
||||
}
|
||||
request.setProductKey(productKey);
|
||||
request.setCurrentPage(pageNo);
|
||||
request.setPageSize(pageSize);
|
||||
|
||||
log.info("调用阿里云 API,请求参数: ProductKey={}, CurrentPage={}, PageSize={}",
|
||||
request.getProductKey(), request.getCurrentPage(), request.getPageSize());
|
||||
|
||||
QueryDeviceResponse response = acsClient.getAcsResponse(request);
|
||||
|
||||
log.info("阿里云 API 返回: Success={}, ErrorMessage={}",
|
||||
response.getSuccess(), response.getErrorMessage());
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", response.getSuccess());
|
||||
result.put("data", response.getData());
|
||||
@@ -43,6 +57,38 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> findDeviceByProductKeyAndName(String productKey, String deviceName) throws Exception {
|
||||
log.info("根据 ProductKey 和 DeviceName 查询设备信息,ProductKey: {}, DeviceName: {}", productKey, deviceName);
|
||||
|
||||
// 使用 QueryDeviceDetail API 查询特定设备
|
||||
QueryDeviceDetailRequest request = new QueryDeviceDetailRequest();
|
||||
request.setProductKey(productKey);
|
||||
request.setDeviceName(deviceName);
|
||||
|
||||
QueryDeviceDetailResponse response = acsClient.getAcsResponse(request);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", response.getSuccess());
|
||||
|
||||
// 将设备详情数据包装成类似 QueryDevice 的格式,以保持接口兼容性
|
||||
if (response.getSuccess() && response.getData() != null) {
|
||||
// 创建一个设备列表,包含查询到的单个设备
|
||||
Map<String, Object> dataWrapper = new HashMap<>();
|
||||
java.util.List<Object> deviceList = new java.util.ArrayList<>();
|
||||
deviceList.add(response.getData());
|
||||
dataWrapper.put("deviceList", deviceList);
|
||||
result.put("data", dataWrapper);
|
||||
result.put("total", 1);
|
||||
} else {
|
||||
result.put("data", null);
|
||||
result.put("total", 0);
|
||||
result.put("errorMessage", response.getErrorMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> queryDeviceInfo(String iotId) throws Exception {
|
||||
log.info("查询设备详情,IotId: {}", iotId);
|
||||
@@ -122,4 +168,20 @@ public class IotDeviceServiceImpl implements IotDeviceService {
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> queryThingModel(String productKey) throws Exception {
|
||||
log.info("查询设备物模型,ProductKey: {}", productKey);
|
||||
|
||||
QueryThingModelRequest request = new QueryThingModelRequest();
|
||||
request.setProductKey(productKey);
|
||||
|
||||
QueryThingModelResponse response = acsClient.getAcsResponse(request);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("success", response.getSuccess());
|
||||
result.put("data", response.getData());
|
||||
result.put("errorMessage", response.getErrorMessage());
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import com.intc.iot.handler.DeviceDataHandler;
|
||||
import com.intc.iot.service.DeviceStatusService;
|
||||
import com.intc.iot.service.MqttService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -9,6 +10,7 @@ import org.eclipse.paho.client.mqttv3.MqttClient;
|
||||
import org.eclipse.paho.client.mqttv3.MqttMessage;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
@@ -19,6 +21,7 @@ import org.springframework.stereotype.Service;
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnClass(MqttClient.class)
|
||||
@ConditionalOnBean(MqttClient.class)
|
||||
public class MqttServiceImpl implements MqttService {
|
||||
|
||||
@@ -27,6 +30,9 @@ public class MqttServiceImpl implements MqttService {
|
||||
@Autowired(required = false)
|
||||
private DeviceDataHandler deviceDataHandler;
|
||||
|
||||
@Autowired(required = false)
|
||||
private DeviceStatusService deviceStatusService;
|
||||
|
||||
@Override
|
||||
public void publish(String topic, String payload, int qos) throws Exception {
|
||||
if (!mqttClient.isConnected()) {
|
||||
@@ -67,6 +73,11 @@ public class MqttServiceImpl implements MqttService {
|
||||
log.debug("未匹配的 Topic 类型: {}", topic);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理设备状态变更(对应 C# 的 /as/mqtt/status/ Topic)
|
||||
if (deviceStatusService != null && topic.contains("/mqtt/status/")) {
|
||||
handleDeviceStatus(topic, payload);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -84,4 +95,52 @@ public class MqttServiceImpl implements MqttService {
|
||||
log.info("MQTT 主题取消订阅成功,Topic: {}", topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备状态变更
|
||||
* 对应 C# IOTTopicDeviceStatus 的处理逻辑
|
||||
*
|
||||
* @param topic Topic
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
private void handleDeviceStatus(String topic, String payload) {
|
||||
try {
|
||||
log.debug("[设备状态] 收到状态变更消息 - Topic: {}", topic);
|
||||
|
||||
// 解析 JSON 消息
|
||||
cn.hutool.json.JSONObject jsonObject = cn.hutool.json.JSONUtil.parseObj(payload);
|
||||
|
||||
// 从 Topic 中提取 productKey 和 deviceName
|
||||
// Topic 格式: /as/mqtt/status/{productKey}/{deviceName}
|
||||
String[] parts = topic.split("/");
|
||||
if (parts.length >= 6) { // 需要至少 6 个元素
|
||||
String productKey = parts[4];
|
||||
String deviceName = parts[5];
|
||||
|
||||
java.util.Map<String, Object> statusData = new java.util.HashMap<>();
|
||||
statusData.put("productKey", productKey);
|
||||
statusData.put("deviceName", deviceName);
|
||||
statusData.put("status", jsonObject.getStr("status")); // online/offline
|
||||
statusData.put("statusTime", jsonObject.getLong("statusTime"));
|
||||
statusData.put("lastTime", jsonObject.getLong("lastTime"));
|
||||
statusData.put("clientIp", jsonObject.getStr("clientIp"));
|
||||
statusData.put("iotId", jsonObject.getStr("iotId"));
|
||||
|
||||
// 调用服务处理
|
||||
deviceStatusService.handleStatusChange(statusData);
|
||||
} else {
|
||||
log.warn("[设备状态] Topic 格式不正确: {}", topic);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[设备状态] 处理设备状态变更异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribeDeviceStatus(String productKey, String deviceName) throws Exception {
|
||||
// 订阅设备状态 Topic(对应 C# 的 /as/mqtt/status/{productKey}/{deviceName})
|
||||
String statusTopic = String.format("/as/mqtt/status/%s/%s", productKey, deviceName);
|
||||
subscribe(statusTopic, 1);
|
||||
log.info("订阅设备状态 Topic 成功: {}", statusTopic);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
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.intc.iot.config.AliyunIotProperties;
|
||||
import com.intc.iot.domain.VmsCallback;
|
||||
import com.intc.iot.mapper.VmsCallbackMapper;
|
||||
import com.intc.iot.service.VmsMnsConsumerService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* VMS MNS 回执消费服务实现
|
||||
* 注意:直接使用 AccessKey/Secret 访问 MNS,不再依赖 dybaseapi
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnProperty(prefix = "aliyun.living-iot.vms", name = "mns-enabled", havingValue = "true")
|
||||
@ConditionalOnBean(AliyunIotProperties.class)
|
||||
@Slf4j
|
||||
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/";
|
||||
private static final int MNS_THREAD_COUNT = 2;
|
||||
private static final int POLL_WAIT_SECONDS = 30;
|
||||
|
||||
/**
|
||||
* 应用标识(用于过滤 out_id)
|
||||
*/
|
||||
private String appName;
|
||||
|
||||
/**
|
||||
* 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 回执消费服务...");
|
||||
|
||||
// 从配置读取应用名称
|
||||
appName = aliyunIotProperties.getAppKey() != null && !aliyunIotProperties.getAppKey().isEmpty()
|
||||
? aliyunIotProperties.getAppKey()
|
||||
: "fishery-backend";
|
||||
log.info("VMS 回执过滤应用名称: {}", appName);
|
||||
|
||||
// 直接使用 AccessKey/Secret 创建 MNS 客户端
|
||||
CloudAccount account = new CloudAccount(
|
||||
aliyunIotProperties.getAccessKeyId(),
|
||||
aliyunIotProperties.getAccessKeySecret(),
|
||||
MNS_ACCOUNT_ENDPOINT
|
||||
);
|
||||
mnsClient = account.getMNSClient();
|
||||
log.info("MNS 客户端初始化成功,Endpoint: {}", MNS_ACCOUNT_ENDPOINT);
|
||||
|
||||
// 创建线程池
|
||||
executorService = Executors.newFixedThreadPool(MNS_THREAD_COUNT);
|
||||
|
||||
// 启动消费线程(对应 C# 的 MNSHandle 循环)
|
||||
for (int i = 0; i < MNS_THREAD_COUNT; i++) {
|
||||
executorService.submit(this::mnsConsumeLoop);
|
||||
}
|
||||
|
||||
log.info("VMS MNS 回执消费服务启动成功,线程数: {}", MNS_THREAD_COUNT);
|
||||
} else {
|
||||
log.warn("VMS MNS 回执消费服务已在运行中,无需重复启动");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (running.compareAndSet(true, false)) {
|
||||
log.info("停止 VMS MNS 回执消费服务...");
|
||||
|
||||
if (executorService != null) {
|
||||
executorService.shutdown();
|
||||
try {
|
||||
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executorService.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭 MNS 客户端
|
||||
if (mnsClient != null) {
|
||||
mnsClient.close();
|
||||
}
|
||||
|
||||
log.info("VMS MNS 回执消费服务已停止");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return running.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* MNS 消费循环
|
||||
*/
|
||||
private void mnsConsumeLoop() {
|
||||
log.info("MNS 消费线程 {} 启动", Thread.currentThread().getName());
|
||||
|
||||
CloudQueue queue = null;
|
||||
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;
|
||||
}
|
||||
|
||||
while (running.get()) {
|
||||
try {
|
||||
// 批量接收消息
|
||||
List<Message> messages = queue.batchPopMessage(16, POLL_WAIT_SECONDS);
|
||||
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
log.debug("接收到 {} 条 VMS 回执消息", messages.size());
|
||||
|
||||
// 处理消息
|
||||
for (Message message : messages) {
|
||||
try {
|
||||
processMessage(message, queue);
|
||||
} catch (Exception e) {
|
||||
log.error("处理 VMS 回执消息失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// 检查是否为中断异常
|
||||
if (e instanceof InterruptedException || e.getCause() instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.info("MNS 消费线程被中断");
|
||||
break;
|
||||
}
|
||||
|
||||
log.error("MNS 消费循环异常: {}", e.getMessage(), e);
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(5);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("MNS 消费线程 {} 退出", Thread.currentThread().getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单条消息
|
||||
*
|
||||
* @param message MNS 消息
|
||||
* @param queue MNS 队列
|
||||
*/
|
||||
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);
|
||||
|
||||
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());
|
||||
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();
|
||||
callback.setCallId((String) map.get("call_id"));
|
||||
callback.setOutId((String) map.get("out_id"));
|
||||
callback.setStatusCode((String) map.get("status_code"));
|
||||
callback.setStatusMsg((String) map.get("status_msg"));
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.aliyuncs.CommonRequest;
|
||||
import com.aliyuncs.CommonResponse;
|
||||
import com.aliyuncs.DefaultAcsClient;
|
||||
import com.aliyuncs.IAcsClient;
|
||||
import com.aliyuncs.http.MethodType;
|
||||
import com.aliyuncs.profile.DefaultProfile;
|
||||
import com.intc.iot.config.AliyunIotProperties;
|
||||
import com.intc.iot.domain.VmsNoticeResponse;
|
||||
import com.intc.iot.service.VmsNoticeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 阿里云语音通知服务实现(基于稳定版 SDK)
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(AliyunIotProperties.class)
|
||||
public class VmsNoticeServiceImpl implements VmsNoticeService {
|
||||
|
||||
/** 默认主叫显号(可以根据实际情况调整或改为配置注入) */
|
||||
private static final String CALL_SHOW_NUMBER = "0571000013978";
|
||||
|
||||
/** 默认 TTS 模板编码(当前取 C# 版本中的正式模板编码,可按环境调整) */
|
||||
private static final String TTS_CODE = "TTS_299550017";
|
||||
|
||||
private final AliyunIotProperties aliyunIotProperties;
|
||||
|
||||
/**
|
||||
* VMS 客户端单例,避免重复创建
|
||||
*/
|
||||
private volatile IAcsClient vmsClient;
|
||||
|
||||
private IAcsClient getClient() {
|
||||
if (vmsClient == null) {
|
||||
synchronized (this) {
|
||||
if (vmsClient == null) {
|
||||
DefaultProfile profile = DefaultProfile.getProfile(
|
||||
"cn-hangzhou",
|
||||
aliyunIotProperties.getAccessKeyId(),
|
||||
aliyunIotProperties.getAccessKeySecret()
|
||||
);
|
||||
vmsClient = new DefaultAcsClient(profile);
|
||||
log.info("VMS 客户端初始化成功");
|
||||
}
|
||||
}
|
||||
}
|
||||
return vmsClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VmsNoticeResponse sendTtsCall(String phoneNum, Map<String, String> params, String outId) throws Exception {
|
||||
log.info("[VmsNoticeService] 发送语音通知, phoneNum={}, outId={}, params={}", phoneNum, outId, params);
|
||||
|
||||
IAcsClient client = getClient();
|
||||
|
||||
// 使用 CommonRequest 构建请求
|
||||
CommonRequest request = new CommonRequest();
|
||||
request.setSysMethod(MethodType.POST);
|
||||
request.setSysDomain("dyvmsapi.aliyuncs.com");
|
||||
request.setSysVersion("2017-05-25");
|
||||
request.setSysAction("SingleCallByTts");
|
||||
|
||||
// 设置请求参数
|
||||
request.putQueryParameter("CalledNumber", phoneNum);
|
||||
request.putQueryParameter("CalledShowNumber", CALL_SHOW_NUMBER);
|
||||
request.putQueryParameter("TtsCode", TTS_CODE);
|
||||
request.putQueryParameter("TtsParam", JSONUtil.toJsonStr(params));
|
||||
request.putQueryParameter("PlayTimes", "3");
|
||||
request.putQueryParameter("Volume", "100");
|
||||
request.putQueryParameter("Speed", "5");
|
||||
if (outId != null && !outId.isEmpty()) {
|
||||
request.putQueryParameter("OutId", outId);
|
||||
}
|
||||
|
||||
try {
|
||||
CommonResponse response = client.getCommonResponse(request);
|
||||
|
||||
if (response == null || response.getData() == null) {
|
||||
log.error("[VmsNoticeService] 语音呼叫请求异常, response 为空");
|
||||
VmsNoticeResponse result = new VmsNoticeResponse();
|
||||
result.setSuccess(false);
|
||||
result.setMessage("呼叫请求异常");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
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");
|
||||
|
||||
VmsNoticeResponse result = new VmsNoticeResponse();
|
||||
result.setCode(code);
|
||||
result.setMessage(message);
|
||||
result.setCallId(callId);
|
||||
result.setSuccess("OK".equalsIgnoreCase(code));
|
||||
|
||||
if (!result.isSuccess()) {
|
||||
log.warn("[VmsNoticeService] 语音呼叫失败, phoneNum={}, code={}, message={}, callId={}",
|
||||
phoneNum, code, message, callId);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("[VmsNoticeService] 语音呼叫异常", e);
|
||||
VmsNoticeResponse result = new VmsNoticeResponse();
|
||||
result.setSuccess(false);
|
||||
result.setMessage("调用异常: " + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
package com.intc.iot.service.impl;
|
||||
|
||||
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.mapper.AquWarnCallNoticeMapper;
|
||||
import com.intc.iot.mapper.VmsCallbackMapper;
|
||||
import com.intc.iot.service.WarnCallNoticeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 告警电话通知服务实现
|
||||
* 对应 C# AlarmProcessor 中的告警通知处理逻辑
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean({AquWarnCallNoticeMapper.class, VmsCallbackMapper.class})
|
||||
@Slf4j
|
||||
public class WarnCallNoticeServiceImpl implements WarnCallNoticeService {
|
||||
|
||||
private final AquWarnCallNoticeMapper warnCallNoticeMapper;
|
||||
private final VmsCallbackMapper vmsCallbackMapper;
|
||||
|
||||
/**
|
||||
* 呼叫状态枚举(对应 C# EnumCallNoticeStatus)
|
||||
*/
|
||||
private static final int CALL_STATUS_NO_CALL = 0; // 未呼叫
|
||||
private static final int CALL_STATUS_CALL_SUCCESS = 1; // 呼叫成功,等待结果反馈
|
||||
private static final int CALL_STATUS_CALL_FAIL = 2; // 呼叫失败
|
||||
private static final int CALL_STATUS_CALLBACK_SUCCESS = 3; // 呼叫反馈成功
|
||||
private static final int CALL_STATUS_CALLBACK_FAIL = 4; // 呼叫反馈失败
|
||||
|
||||
/**
|
||||
* VMS 成功状态码
|
||||
*/
|
||||
private static final String VMS_STATUS_CODE_SUCCESS = "200000"; // 用户听完语音
|
||||
private static final String VMS_STATUS_CODE_HANGUP = "200001"; // 用户未听完挂断但已收听
|
||||
|
||||
/**
|
||||
* 回执超时时间(分钟)
|
||||
* 对应 C# CallNoticeMinuteInterval = 3
|
||||
*/
|
||||
private static final int CALLBACK_TIMEOUT_MINUTES = 3;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean processVmsCallback(VmsCallback callback) {
|
||||
if (callback == null || callback.getCallId() == null) {
|
||||
log.warn("[告警通知] 回执数据为空或缺少 CallId");
|
||||
return false;
|
||||
}
|
||||
|
||||
log.debug("[告警通知] 处理回执信息 - 呼叫ID: {}, 状态码: {}", callback.getCallId(), callback.getStatusCode());
|
||||
|
||||
// 根据 CallId 查找对应的通知记录
|
||||
AquWarnCallNotice 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());
|
||||
return false;
|
||||
}
|
||||
|
||||
log.debug("[告警通知] 找到对应的通知记录 - 记录ID: {}, 设备ID: {}, 电话: {}",
|
||||
callNotice.getId(), callNotice.getDeviceId(), callNotice.getMobilePhone());
|
||||
|
||||
// 更新通知记录(对应 C# UpdateCallNoticeFromCallback)
|
||||
updateCallNoticeFromCallback(callNotice, callback);
|
||||
|
||||
// 标记回执为已处理
|
||||
callback.setProcessed(1);
|
||||
vmsCallbackMapper.updateById(callback);
|
||||
|
||||
log.info("[告警通知] 回执处理完成 - 呼叫ID: {}, 设备ID: {}, 状态: {}",
|
||||
callback.getCallId(), callNotice.getDeviceId(),
|
||||
callNotice.getCallStatus() == CALL_STATUS_CALLBACK_SUCCESS ? "成功" : "失败");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int processUnhandledCallbacks() {
|
||||
log.debug("[告警通知] 开始批量处理未处理的 VMS 回执");
|
||||
|
||||
// 查询所有未处理的回执
|
||||
List<VmsCallback> unhandledCallbacks = vmsCallbackMapper.selectList(
|
||||
new LambdaQueryWrapper<VmsCallback>()
|
||||
.eq(VmsCallback::getProcessed, 0)
|
||||
.orderByAsc(VmsCallback::getCreatedTime)
|
||||
);
|
||||
|
||||
if (unhandledCallbacks.isEmpty()) {
|
||||
log.debug("[告警通知] 没有未处理的回执");
|
||||
return 0;
|
||||
}
|
||||
|
||||
log.info("[告警通知] 发现 {} 条未处理的回执,开始处理", unhandledCallbacks.size());
|
||||
|
||||
int processedCount = 0;
|
||||
for (VmsCallback callback : unhandledCallbacks) {
|
||||
try {
|
||||
if (processVmsCallback(callback)) {
|
||||
processedCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[告警通知] 处理回执异常 - 呼叫ID: {}, 异常: {}", callback.getCallId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[告警通知] 批量处理完成 - 成功: {}, 总数: {}", processedCount, unhandledCallbacks.size());
|
||||
return processedCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
|
||||
public int removePendingNotificationsByDevice(Long deviceId) {
|
||||
if (deviceId == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
log.debug("[告警通知] 移除设备 {} 的待通知记录", deviceId);
|
||||
|
||||
// 删除该设备所有未呼叫的通知记录(对应 C# RemovePendingNotificationsForDevice)
|
||||
int count = warnCallNoticeMapper.delete(
|
||||
new LambdaQueryWrapper<AquWarnCallNotice>()
|
||||
.eq(AquWarnCallNotice::getDeviceId, deviceId)
|
||||
.eq(AquWarnCallNotice::getCallStatus, CALL_STATUS_NO_CALL)
|
||||
);
|
||||
|
||||
if (count > 0) {
|
||||
log.info("[告警通知] 成功移除设备 {} 的 {} 条待通知记录", deviceId, count);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int cleanupExpiredNotifications() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime timeoutThreshold = now.minusMinutes(CALLBACK_TIMEOUT_MINUTES);
|
||||
|
||||
log.debug("[告警通知] 开始清理超时通知数据 - 超时时间: {} 分钟", CALLBACK_TIMEOUT_MINUTES);
|
||||
|
||||
// 查询所有超时的通知记录
|
||||
// 条件:
|
||||
// 1. callStatus = 1(呼叫成功,等待结果反馈)
|
||||
// 2. callTime < 现在时间 - 3分钟
|
||||
List<AquWarnCallNotice> expiredNotices = warnCallNoticeMapper.selectList(
|
||||
new LambdaQueryWrapper<AquWarnCallNotice>()
|
||||
.eq(AquWarnCallNotice::getCallStatus, CALL_STATUS_CALL_SUCCESS)
|
||||
.lt(AquWarnCallNotice::getCallTime, timeoutThreshold)
|
||||
);
|
||||
|
||||
if (expiredNotices.isEmpty()) {
|
||||
log.debug("[告警通知] 无超时回执记录需要清理");
|
||||
return 0;
|
||||
}
|
||||
|
||||
log.info("[告警通知] 发现 {} 条超时回执记录,开始清理", expiredNotices.size());
|
||||
|
||||
int cleanedCount = 0;
|
||||
for (AquWarnCallNotice notice : expiredNotices) {
|
||||
try {
|
||||
log.warn("[告警通知] 电话通知回执超时 - 呼叫ID: {}, 电话: {}, 设备ID: {}, 超时时间: {}分钟",
|
||||
notice.getCallId(), notice.getMobilePhone(), notice.getDeviceId(), CALLBACK_TIMEOUT_MINUTES);
|
||||
|
||||
// 更新状态为回执失败
|
||||
notice.setCallStatus(CALL_STATUS_CALLBACK_FAIL);
|
||||
notice.setStatusMsg("回执超时,未在规定时间内收到回执");
|
||||
notice.setUpdatedTime(now);
|
||||
warnCallNoticeMapper.updateById(notice);
|
||||
|
||||
cleanedCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("[告警通知] 清理超时记录异常 - 呼叫ID: {}, 异常: {}", notice.getCallId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
log.info("[告警通知] 清理了 {} 个超时的回执记录", cleanedCount);
|
||||
}
|
||||
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新电话通知的回执信息
|
||||
* 对应 C# UpdateCallNoticeFromCallback
|
||||
*
|
||||
* @param callNotice 通知记录
|
||||
* @param callback VMS 回执
|
||||
*/
|
||||
private void updateCallNoticeFromCallback(AquWarnCallNotice callNotice, VmsCallback callback) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// 更新回执详细信息(直接映射字段)
|
||||
callNotice.setStatusCode(callback.getStatusCode());
|
||||
callNotice.setHangupDirection(callback.getHangupDirection());
|
||||
callNotice.setCaller(callback.getCaller());
|
||||
callNotice.setRingTime(convertToString(callback.getRingTime()));
|
||||
callNotice.setDuration(convertToString(callback.getDuration()));
|
||||
callNotice.setVoiceType(callback.getVoiceType());
|
||||
callNotice.setOriginateTime(convertToString(callback.getOriginateTime()));
|
||||
callNotice.setStartTime(convertToString(callback.getStartTime()));
|
||||
callNotice.setEndTime(convertToString(callback.getEndTime()));
|
||||
callNotice.setStatusMsg(callback.getStatusMsg());
|
||||
callNotice.setOutId(callback.getOutId());
|
||||
callNotice.setTollType(callback.getTollType());
|
||||
callNotice.setUpdatedTime(now);
|
||||
|
||||
// 根据状态码判断通话结果(对应 C# 的状态码判断逻辑)
|
||||
if (VMS_STATUS_CODE_SUCCESS.equals(callback.getStatusCode()) ||
|
||||
VMS_STATUS_CODE_HANGUP.equals(callback.getStatusCode())) {
|
||||
// 用户听完语音或提前挂机但已收听
|
||||
callNotice.setCallStatus(CALL_STATUS_CALLBACK_SUCCESS);
|
||||
|
||||
log.info("[告警通知] 电话通知成功 - 设备ID: {}, 电话: {}, 状态码: {}",
|
||||
callNotice.getDeviceId(), callNotice.getMobilePhone(), callback.getStatusCode());
|
||||
|
||||
// 通话成功后,移除该设备的其他待通知记录
|
||||
// 注意:removePendingNotificationsByDevice 使用 REQUIRES_NEW 事务传播,独立于当前事务
|
||||
try {
|
||||
removePendingNotificationsByDevice(callNotice.getDeviceId());
|
||||
} catch (Exception e) {
|
||||
// 失败也不影响主事务
|
||||
log.warn("[告警通知] 移除待通知记录失败: {}", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 通话失败
|
||||
callNotice.setCallStatus(CALL_STATUS_CALLBACK_FAIL);
|
||||
|
||||
log.warn("[告警通知] 电话通知失败 - 设备ID: {}, 电话: {}, 状态码: {}",
|
||||
callNotice.getDeviceId(), callNotice.getMobilePhone(), callback.getStatusCode());
|
||||
}
|
||||
|
||||
// 更新到数据库
|
||||
warnCallNoticeMapper.updateById(callNotice);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Long/Integer 转换为 String
|
||||
* C# 中这些字段都是 string 类型
|
||||
*
|
||||
* @param value 数值
|
||||
* @return 字符串
|
||||
*/
|
||||
private String convertToString(Object value) {
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.intc.iot.task;
|
||||
|
||||
import com.intc.iot.service.WarnCallNoticeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* VMS 回执处理定时任务
|
||||
* 对应 C# AlarmProcessor 中的 ProcessVmsCallbacks + CleanupExpiredNotifications 定时逻辑
|
||||
*
|
||||
* @author intc-iot
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@ConditionalOnBean(WarnCallNoticeService.class)
|
||||
@Slf4j
|
||||
public class VmsCallbackProcessTask {
|
||||
|
||||
private final WarnCallNoticeService warnCallNoticeService;
|
||||
|
||||
/**
|
||||
* 定时处理 VMS 回执
|
||||
* 每 5 秒执行一次(对应 C# 的处理频率)
|
||||
*/
|
||||
@Scheduled(fixedDelay = 5000)
|
||||
public void processVmsCallbacks() {
|
||||
try {
|
||||
int count = warnCallNoticeService.processUnhandledCallbacks();
|
||||
if (count > 0) {
|
||||
log.debug("[VMS回执处理] 本次处理了 {} 条回执", count);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[VMS回执处理] 处理回执异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时清理超时的通知记录
|
||||
* 每 30 秒执行一次(对应 C# CleanupExpiredNotifications 逻辑)
|
||||
*/
|
||||
@Scheduled(fixedDelay = 30000)
|
||||
public void cleanupExpiredNotifications() {
|
||||
try {
|
||||
int count = warnCallNoticeService.cleanupExpiredNotifications();
|
||||
if (count > 0) {
|
||||
log.info("[VMS超时清理] 本次清理了 {} 条超时记录", count);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[VMS超时清理] 清理超时记录异常: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.intc.iot.utils;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 阿里云 IoT AMQP 签名工具类
|
||||
*
|
||||
* 用于生成 AMQP 服务端订阅所需的认证信息
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class AliyunAmqpSignUtil {
|
||||
|
||||
/**
|
||||
* 生成 AMQP 用户名
|
||||
*
|
||||
* @param accessKeyId 阿里云 AccessKey ID
|
||||
* @return AMQP 用户名
|
||||
*/
|
||||
public static String generateUsername(String accessKeyId) {
|
||||
return accessKeyId + "|authMode=aksign|";
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 AMQP 密码
|
||||
* 使用 HMAC-SHA1 算法对消费组 ID 进行签名
|
||||
*
|
||||
* @param accessKeySecret 阿里云 AccessKey Secret
|
||||
* @param consumerGroupId 消费组 ID
|
||||
* @return AMQP 密码(Base64 编码)
|
||||
*/
|
||||
public static String generatePassword(String accessKeySecret, String consumerGroupId) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(
|
||||
accessKeySecret.getBytes(StandardCharsets.UTF_8),
|
||||
"HmacSHA1"
|
||||
);
|
||||
mac.init(secretKeySpec);
|
||||
|
||||
byte[] signData = mac.doFinal(consumerGroupId.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(signData);
|
||||
} catch (Exception e) {
|
||||
log.error("生成 AMQP 密码失败", e);
|
||||
throw new RuntimeException("生成 AMQP 密码失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 AMQP 虚拟主机名
|
||||
* 默认为 AccessKey ID 的前 8 位
|
||||
*
|
||||
* @param accessKeyId 阿里云 AccessKey ID
|
||||
* @return 虚拟主机名
|
||||
*/
|
||||
public static String generateVirtualHost(String accessKeyId) {
|
||||
if (accessKeyId == null || accessKeyId.length() < 8) {
|
||||
throw new IllegalArgumentException("AccessKey ID 长度不足 8 位");
|
||||
}
|
||||
return accessKeyId.substring(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 AMQP 接入点地址
|
||||
*
|
||||
* @param uid 阿里云账号 UID
|
||||
* @param regionId 地域 ID(如:cn-shanghai)
|
||||
* @return AMQP 接入点地址
|
||||
*/
|
||||
public static String generateHost(String uid, String regionId) {
|
||||
return String.format("%s.iot-amqp.%s.aliyuncs.com", uid, regionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印完整的 AMQP 配置信息(用于调试)
|
||||
*
|
||||
* @param accessKeyId AccessKey ID
|
||||
* @param accessKeySecret AccessKey Secret
|
||||
* @param consumerGroupId 消费组 ID
|
||||
* @param uid 阿里云账号 UID
|
||||
* @param regionId 地域 ID
|
||||
*/
|
||||
public static void printAmqpConfig(String accessKeyId, String accessKeySecret,
|
||||
String consumerGroupId, String uid, String regionId) {
|
||||
String host = generateHost(uid, regionId);
|
||||
String virtualHost = generateVirtualHost(accessKeyId);
|
||||
String username = generateUsername(accessKeyId);
|
||||
String password = generatePassword(accessKeySecret, consumerGroupId);
|
||||
|
||||
log.info("========== AMQP 配置信息 ==========");
|
||||
log.info("host: {}", host);
|
||||
log.info("port: 5672");
|
||||
log.info("virtual-host: {}", virtualHost);
|
||||
log.info("username: {}", username);
|
||||
log.info("password: {}", password);
|
||||
log.info("consumer-group-id: {}", consumerGroupId);
|
||||
log.info("==================================");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.intc.iot.utils;
|
||||
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 阿里云IoT平台MQTT连接签名工具类
|
||||
*
|
||||
* @author intc
|
||||
*/
|
||||
@Slf4j
|
||||
public class AliyunIotSignUtil {
|
||||
|
||||
/**
|
||||
* 生成MQTT ClientId
|
||||
* 格式:{ClientId}|securemode=2,signmethod=hmacsha1,timestamp={timestamp}|
|
||||
*
|
||||
* @param clientId 客户端ID(可自定义,建议使用设备名称)
|
||||
* @return 完整的ClientId
|
||||
*/
|
||||
public static String generateClientId(String clientId) {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
return String.format("%s|securemode=2,signmethod=hmacsha1,timestamp=%d|", clientId, timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MQTT Username
|
||||
* 格式:{DeviceName}&{ProductKey}
|
||||
*
|
||||
* @param deviceName 设备名称
|
||||
* @param productKey 产品Key
|
||||
* @return Username
|
||||
*/
|
||||
public static String generateUsername(String deviceName, String productKey) {
|
||||
return String.format("%s&%s", deviceName, productKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MQTT Password(使用HMAC-SHA1签名)
|
||||
* 签名内容:clientId{ClientId}deviceName{DeviceName}productKey{ProductKey}timestamp{timestamp}
|
||||
*
|
||||
* @param clientId 客户端ID
|
||||
* @param deviceName 设备名称
|
||||
* @param productKey 产品Key
|
||||
* @param deviceSecret 设备密钥
|
||||
* @return Password
|
||||
*/
|
||||
public static String generatePassword(String clientId, String deviceName, String productKey, String deviceSecret) {
|
||||
try {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
// 构造签名内容
|
||||
String content = String.format("clientId%sdeviceName%sproductKey%stimestamp%d",
|
||||
clientId, deviceName, productKey, timestamp);
|
||||
|
||||
log.debug("签名内容: {}", content);
|
||||
|
||||
// 使用HMAC-SHA1算法签名
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
SecretKeySpec secretKeySpec = new SecretKeySpec(deviceSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA1");
|
||||
mac.init(secretKeySpec);
|
||||
byte[] hash = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// 转换为十六进制字符串
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
|
||||
return hexString.toString().toUpperCase();
|
||||
} catch (Exception e) {
|
||||
log.error("生成MQTT密码失败", e);
|
||||
throw new RuntimeException("生成MQTT密码失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成阿里云IoT平台MQTT Broker地址
|
||||
* 格式:{ProductKey}.iot-as-mqtt.{RegionId}.aliyuncs.com:1883
|
||||
* SSL格式:ssl://{ProductKey}.iot-as-mqtt.{RegionId}.aliyuncs.com:8883
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param regionId 地域ID(如:cn-shanghai)
|
||||
* @param useSsl 是否使用SSL
|
||||
* @return Broker地址
|
||||
*/
|
||||
public static String generateBrokerUrl(String productKey, String regionId, boolean useSsl) {
|
||||
String protocol = useSsl ? "ssl://" : "tcp://";
|
||||
int port = useSsl ? 8883 : 1883;
|
||||
return String.format("%s%s.iot-as-mqtt.%s.aliyuncs.com:%d", protocol, productKey, regionId, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成设备属性上报Topic
|
||||
* 格式:/sys/{ProductKey}/{DeviceName}/thing/event/property/post
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @return Topic
|
||||
*/
|
||||
public static String generatePropertyTopic(String productKey, String deviceName) {
|
||||
return String.format("/sys/%s/%s/thing/event/property/post", productKey, deviceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成所有设备属性上报Topic(使用通配符)
|
||||
* 格式:/sys/{ProductKey}/+/thing/event/property/post
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @return Topic
|
||||
*/
|
||||
public static String generateAllDevicesPropertyTopic(String productKey) {
|
||||
return String.format("/sys/%s/+/thing/event/property/post", productKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成设备事件上报Topic
|
||||
* 格式:/sys/{ProductKey}/{DeviceName}/thing/event/{EventIdentifier}/post
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @param eventIdentifier 事件标识符(使用+通配符可订阅所有事件)
|
||||
* @return Topic
|
||||
*/
|
||||
public static String generateEventTopic(String productKey, String deviceName, String eventIdentifier) {
|
||||
return String.format("/sys/%s/%s/thing/event/%s/post", productKey, deviceName, eventIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成设备服务调用Topic
|
||||
* 格式:/sys/{ProductKey}/{DeviceName}/thing/service/{ServiceIdentifier}
|
||||
*
|
||||
* @param productKey 产品Key
|
||||
* @param deviceName 设备名称
|
||||
* @param serviceIdentifier 服务标识符
|
||||
* @return Topic
|
||||
*/
|
||||
public static String generateServiceTopic(String productKey, String deviceName, String serviceIdentifier) {
|
||||
return String.format("/sys/%s/%s/thing/service/%s", productKey, deviceName, serviceIdentifier);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +1,73 @@
|
||||
# 阿里云生活物联网平台(飞燕平台)配置
|
||||
# 使用说明:
|
||||
# 1. 替换下面的占位符为实际值
|
||||
# 2. AccessKey/Secret 建议使用环境变量或配置中心管理
|
||||
# 3. 推荐使用 AMQP 服务端订阅接收所有设备数据
|
||||
aliyun:
|
||||
living-iot:
|
||||
# 阿里云 AccessKey ID(必填)
|
||||
# ========== 基础配置 ==========
|
||||
# 阿里云 AccessKey ID(必填)- 从阿里云控制台获取
|
||||
# 安全提示:请通过环境变量配置,不要在代码中暴露真实密钥
|
||||
access-key-id: LTAI5tRnPowmTLjH181nSbsR
|
||||
# 阿里云 AccessKey Secret(必填)
|
||||
# 阿里云 AccessKey Secret(必填)- 从阿里云控制台获取
|
||||
# 安全提示:请通过环境变量配置,不要在代码中暴露真实密钥
|
||||
access-key-secret: Vh2LoAM1t3XuMUVy2wTWSACJ97kOUW
|
||||
# 地域节点(必填,如:cn-shanghai)
|
||||
region-id: cn-shanghai
|
||||
# 飞燕平台项目ID(Project ID,必填)
|
||||
project-id: a123nMibvh0q4UnU
|
||||
# 控制器key
|
||||
controller-product-key: a1Xj9dagTIx,
|
||||
# 检测仪key
|
||||
detector-product-key: a15hA3oBPmB,
|
||||
# App Key(必填)
|
||||
app-key: 334224397
|
||||
# App Secret(必填)
|
||||
app-secret: 70de3018ec39423e9ca1e1b6a6a84ad6
|
||||
# 品类Key(选填)
|
||||
category-key:
|
||||
|
||||
# MQTT 配置(可选)
|
||||
# ========== AMQP 服务端订阅配置(推荐) ==========
|
||||
# 说明:AMQP 服务端订阅用于接收所有设备的数据,无需为每个设备配置 MQTT
|
||||
# 配置步骤:
|
||||
# 1. 在阿里云IoT控制台创建消费组
|
||||
# 路径:IoT控制台 -> 规则引擎 -> 服务端订阅 -> 消费组管理 -> 创建消费组
|
||||
# 2. 获取 AMQP 接入点信息
|
||||
# 路径:IoT控制台 -> 实例详情 -> 开发配置 -> 服务端订阅 -> 查看 AMQP 接入点
|
||||
# 3. 使用工具生成认证信息:
|
||||
# username: {accessKeyId}|authMode=aksign|
|
||||
# password: 使用 HMAC-SHA1 签名生成,可使用阿里云提供的在线工具
|
||||
amqp:
|
||||
# 是否启用 AMQP 订阅
|
||||
enabled: true
|
||||
# AMQP 接入点地址(格式:${uid}.iot-amqp.${region}.aliyuncs.com)
|
||||
# 示例:1234567890.iot-amqp.cn-shanghai.aliyuncs.com
|
||||
host: 1572610294777992.iot-amqp.cn-shanghai.aliyuncs.com
|
||||
# AMQP 端口(默认 5672)
|
||||
port: 5672
|
||||
# 虚拟主机(默认为 AccessKey 的前 8 位)
|
||||
virtual-host:
|
||||
# 用户名(格式:${accessKeyId}|authMode=aksign|)
|
||||
username:
|
||||
# 密码(使用 HMAC-SHA1 签名生成)
|
||||
password:
|
||||
# 消费组 ID(在阿里云 IoT 控制台创建)
|
||||
consumer-group-id: HPaJu3YgmDnUAP43Z7xd000100
|
||||
# 连接超时时间(毫秒)
|
||||
connection-timeout: 30000
|
||||
# 自动重连
|
||||
auto-reconnect: true
|
||||
# 预取数量(每次批量拉取的消息数)
|
||||
prefetch-count: 50
|
||||
|
||||
# ========== MQTT 配置(可选,用于设备直连) ==========
|
||||
# 说明:MQTT 适用于单个设备直连,不推荐用于大量设备场景
|
||||
# 如需启用,请确保以下配置正确
|
||||
# 注:大量设备请使用 AMQP 服务端订阅
|
||||
mqtt:
|
||||
# MQTT Broker 地址(格式:ssl://实例ID.iot-as-mqtt.cn-shanghai.aliyuncs.com:1883)
|
||||
broker-url: ssl://1572610294777992.iot-amqp.cn-shanghai.aliyuncs.com
|
||||
# 客户端ID(格式:{ClientID}|securemode=2,signmethod=hmacsha1|)
|
||||
client-id: your_client_id|securemode=2,signmethod=hmacsha1|
|
||||
# 用户名(设备名称&产品Key)
|
||||
username: DeviceName&ProductKey
|
||||
# 密码(通过MQTT密码工具生成)
|
||||
password: your_mqtt_password_here
|
||||
# MQTT Broker 地址(格式:tcp://{ProductKey}.iot-as-mqtt.{RegionId}.aliyuncs.com:1883)
|
||||
# 示例:tcp://a1Xj9dagTIx.iot-as-mqtt.cn-shanghai.aliyuncs.com:1883
|
||||
broker-url:
|
||||
# 客户端ID
|
||||
client-id:
|
||||
# 用户名(格式:{DeviceName}&{ProductKey})
|
||||
username:
|
||||
# 密码(使用 AliyunIotSignUtil.generatePassword() 方法生成)
|
||||
password:
|
||||
# 连接超时时间(秒)
|
||||
connection-timeout: 30
|
||||
# 保活时间(秒)
|
||||
@@ -38,3 +76,24 @@ aliyun:
|
||||
auto-reconnect: true
|
||||
# 清除会话
|
||||
clean-session: true
|
||||
|
||||
# ========== 配置说明 ==========
|
||||
#
|
||||
# 1. AMQP 服务端订阅配置步骤:
|
||||
# a. 登录阿里云 IoT 控制台
|
||||
# b. 进入实例管理 -> 选择你的实例 -> 规则引擎 -> 服务端订阅
|
||||
# c. 创建消费组(记住消费组 ID)
|
||||
# d. 查看 AMQP 接入点信息,获取 host 和 virtualHost
|
||||
# e. 生成认证信息:
|
||||
# username: {accessKeyId}|authMode=aksign|
|
||||
# password: 使用 HMAC-SHA1("{accessKeySecret}", "{consumerGroupId}") 计算
|
||||
# 可使用阿里云提供的在线签名工具
|
||||
#
|
||||
# 2. AMQP vs MQTT 选择:
|
||||
# - AMQP:推荐,适用于服务端接收所有设备数据,无需为每个设备配置
|
||||
# - MQTT:适用于单个设备直连,需要设备三元组(ProductKey/DeviceName/DeviceSecret)
|
||||
#
|
||||
# 3. 关键信息获取路径:
|
||||
# - AMQP 接入点:IoT控制台 -> 实例详情 -> 开发配置 -> 服务端订阅
|
||||
# - 消费组 ID:IoT控制台 -> 规则引擎 -> 服务端订阅 -> 消费组管理
|
||||
# - AccessKey:阿里云控制台 -> AccessKey 管理
|
||||
|
||||
@@ -74,11 +74,12 @@ public class DeviceSensorData {
|
||||
}
|
||||
|
||||
// 标签字段(元数据,不随时间频繁变化)
|
||||
private String tenantId = "111111"; // 租户ID(默认值)
|
||||
private String serialNum; // 设备序列号
|
||||
private long deviceId; // 设备ID(对应TDengine的BIGINT)
|
||||
private long userId; // 用户ID(对应TDengine的BIGINT)
|
||||
private Long deviceId; // 设备ID(对应TDengine的BIGINT)
|
||||
private Long userId; // 用户ID(对应TDengine的BIGINT)
|
||||
private String userName; // 用户名
|
||||
private String mobilePhone; // 手机号
|
||||
private String deviceName; // 设备名称
|
||||
private int deviceType; // 设备类型
|
||||
private Integer deviceType; // 设备类型
|
||||
}
|
||||
|
||||
@@ -41,6 +41,16 @@ public interface DeviceSensorDataMapper {
|
||||
*/
|
||||
void createTable(DeviceSensorData deviceSensorData);
|
||||
|
||||
/**
|
||||
* 检查子表是否存在
|
||||
*
|
||||
* @param serialNum 设备序列号
|
||||
* @return 存在返回1,不存在返回0
|
||||
*/
|
||||
@DS("taos")
|
||||
@InterceptorIgnore(tenantLine = "true")
|
||||
Integer checkTableExists(@Param("serialNum") String serialNum);
|
||||
|
||||
/**
|
||||
* 批量插入数据
|
||||
*
|
||||
|
||||
@@ -18,6 +18,74 @@ public class DeviceSensorDataService implements IDeviceSensorDataService {
|
||||
@Resource
|
||||
private DeviceSensorDataMapper deviceSensorDataMapper;
|
||||
|
||||
/**
|
||||
* 已创建的子表缓存(设备序列号)
|
||||
* 使用 ConcurrentHashMap 保证线程安全
|
||||
*/
|
||||
private static final java.util.concurrent.ConcurrentHashMap<String, Boolean> CREATED_TABLES = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 数据缓冲队列,用于批量插入优化
|
||||
*/
|
||||
private static final java.util.concurrent.BlockingQueue<DeviceSensorData> DATA_BUFFER = new java.util.concurrent.LinkedBlockingQueue<>(10000);
|
||||
|
||||
/**
|
||||
* 批量插入配置
|
||||
*/
|
||||
private static final int BATCH_SIZE = 100; // 每批次插入数量
|
||||
private static final long BATCH_TIMEOUT_MS = 1000; // 批次超时时间(毫秒)
|
||||
|
||||
/**
|
||||
* 启动批量插入线程
|
||||
*/
|
||||
@jakarta.annotation.PostConstruct
|
||||
public void startBatchInsertThread() {
|
||||
Thread batchThread = new Thread(() -> {
|
||||
List<DeviceSensorData> batch = new ArrayList<>(BATCH_SIZE);
|
||||
long lastInsertTime = System.currentTimeMillis();
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
// 从队列中取数据,最多等待100ms
|
||||
DeviceSensorData data = DATA_BUFFER.poll(100, java.util.concurrent.TimeUnit.MILLISECONDS);
|
||||
|
||||
if (data != null) {
|
||||
batch.add(data);
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
boolean shouldInsert = batch.size() >= BATCH_SIZE ||
|
||||
(batch.size() > 0 && (now - lastInsertTime) >= BATCH_TIMEOUT_MS);
|
||||
|
||||
if (shouldInsert) {
|
||||
batchInsertDeviceSensorDataInternal(new ArrayList<>(batch));
|
||||
batch.clear();
|
||||
lastInsertTime = now;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
log.warn("批量插入线程被中断");
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
log.error("批量插入线程异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 线程结束前,插入剩余数据
|
||||
if (!batch.isEmpty()) {
|
||||
try {
|
||||
batchInsertDeviceSensorDataInternal(batch);
|
||||
} catch (Exception e) {
|
||||
log.error("插入剩余数据失败", e);
|
||||
}
|
||||
}
|
||||
}, "TDengine-BatchInsert-Thread");
|
||||
|
||||
batchThread.setDaemon(true);
|
||||
batchThread.start();
|
||||
log.info("✅ TDengine 批量插入线程已启动");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建子表
|
||||
@@ -31,14 +99,95 @@ public class DeviceSensorDataService implements IDeviceSensorDataService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入数据
|
||||
* 批量插入数据(异步,支持批量优化)
|
||||
*
|
||||
* @param dataList 数据列表
|
||||
* @return 影响行数
|
||||
*/
|
||||
@Override
|
||||
public void batchInsertDeviceSensorData(List<DeviceSensorData> dataList) {
|
||||
deviceSensorDataMapper.batchInsertDeviceSensorData(dataList);
|
||||
if (dataList == null || dataList.isEmpty()) {
|
||||
log.warn("批量插入数据列表为空,跳过插入");
|
||||
return;
|
||||
}
|
||||
|
||||
// 将数据加入缓冲队列,由后台线程批量插入
|
||||
for (DeviceSensorData data : dataList) {
|
||||
boolean added = DATA_BUFFER.offer(data);
|
||||
if (!added) {
|
||||
log.warn("⚠️ 数据缓冲队列已满,直接同步插入");
|
||||
// 队列满了,直接同步插入
|
||||
batchInsertDeviceSensorDataInternal(dataList);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部批量插入方法(同步执行)
|
||||
*
|
||||
* @param dataList 数据列表
|
||||
*/
|
||||
private void batchInsertDeviceSensorDataInternal(List<DeviceSensorData> dataList) {
|
||||
if (dataList == null || dataList.isEmpty()) {
|
||||
log.warn("批量插入数据列表为空,跳过插入");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 打印第一条数据的详细信息
|
||||
if (!dataList.isEmpty()) {
|
||||
DeviceSensorData firstData = dataList.get(0);
|
||||
// 检查子表是否存在(使用缓存优化)
|
||||
String serialNum = firstData.getSerialNum();
|
||||
String tableName = "t_" + serialNum;
|
||||
|
||||
// 先检查缓存,避免重复查询数据库
|
||||
if (!CREATED_TABLES.containsKey(serialNum)) {
|
||||
try {
|
||||
Integer exists = deviceSensorDataMapper.checkTableExists(serialNum);
|
||||
if (exists == null || exists == 0) {
|
||||
deviceSensorDataMapper.createTable(firstData);
|
||||
log.debug("子表 {} 已创建", tableName);
|
||||
}
|
||||
// 加入缓存
|
||||
CREATED_TABLES.put(serialNum, true);
|
||||
} catch (Exception e) {
|
||||
// 检查表不存在是正常情况,使用 USING 语法会自动创建
|
||||
String errorMsg = e.getMessage();
|
||||
if (errorMsg != null && errorMsg.contains("Table does not exist")) {
|
||||
log.debug("子表 {} 不存在,将自动创建", tableName);
|
||||
} else {
|
||||
log.warn("检查子表失败: {}", errorMsg != null && errorMsg.length() > 100 ? errorMsg.substring(0, 100) : errorMsg);
|
||||
}
|
||||
// 即使失败也加入缓存,避免重复尝试
|
||||
CREATED_TABLES.put(serialNum, true);
|
||||
}
|
||||
} else {
|
||||
log.debug("子表 {} 已在缓存中", tableName);
|
||||
}
|
||||
}
|
||||
|
||||
deviceSensorDataMapper.batchInsertDeviceSensorData(dataList);
|
||||
|
||||
log.debug("插入 {} 条数据", dataList.size());
|
||||
} catch (Exception e) {
|
||||
log.error("批量插入失败: {}", e.getMessage());
|
||||
|
||||
// 判断错误类型
|
||||
String errorMsg = e.getMessage();
|
||||
if (errorMsg != null) {
|
||||
if (errorMsg.contains("超级表") || errorMsg.contains("stable") || errorMsg.contains("not exist")) {
|
||||
log.error("⚠️ 可能原因:超级表 'fishery.device_sensor_data' 不存在!");
|
||||
log.error("请执行建表 SQL 创建超级表");
|
||||
} else if (errorMsg.contains("数据库") || errorMsg.contains("database")) {
|
||||
log.error("⚠️ 可能原因:数据库 'fishery' 不存在!");
|
||||
} else if (errorMsg.contains("字段") || errorMsg.contains("column")) {
|
||||
log.error("⚠️ 可能原因:字段不匹配或类型错误");
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("插入 TDengine 数据失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,23 +32,29 @@
|
||||
|
||||
create table if not exists
|
||||
|
||||
`fishery`.t_#{serialNum}
|
||||
`fishery`.`t_${serialNum}`
|
||||
|
||||
using fishery.device_sensor_data
|
||||
|
||||
tags(#{tenantId},#{serialNum},#{deviceId},#{userId},#{userName},#{mobilePhone},#{deviceName},#{deviceType})
|
||||
</update>
|
||||
|
||||
<select id="checkTableExists" resultType="java.lang.Integer">
|
||||
SELECT COUNT(*) FROM information_schema.ins_tables
|
||||
WHERE db_name = 'fishery' AND stable_name = 'device_sensor_data' AND table_name = 't_${serialNum}'
|
||||
</select>
|
||||
|
||||
<insert id="batchInsertDeviceSensorData">
|
||||
insert into
|
||||
<foreach collection="dataList" item="data" open="" close="" separator=" ">
|
||||
`fishery`.t_#{data.serialNum}
|
||||
`fishery`.`t_${data.serialNum}`
|
||||
|
||||
using fishery.device_sensor_data
|
||||
|
||||
tags(#{data.tenantId},#{data.serialNum},#{data.deviceId},#{data.userId},#{data.userName},#{data.mobilePhone},#{data.deviceName},#{data.deviceType})
|
||||
|
||||
(time, createTime, dissolvedOxygen, temperature, saturability, ph, salinity, treference, tfluorescence, phaseDifference, battery) values (#{data.time}, ${data.createTime}, ${data.dissolvedOxygen}, ${data.temperature}, ${data.saturability}, ${data.ph}, ${data.salinity}, ${data.treference}, ${data.tfluorescence}, ${data.phaseDifference}, ${data.battery})
|
||||
(time, createTime, dissolvedOxygen, temperature, saturability, ph, salinity, treference, tfluorescence, phaseDifference, battery)
|
||||
values (#{data.time}, #{data.createTime}, #{data.dissolvedOxygen}, #{data.temperature}, #{data.saturability}, #{data.ph}, #{data.salinity}, #{data.treference}, #{data.tfluorescence}, #{data.phaseDifference}, #{data.battery})
|
||||
</foreach>
|
||||
</insert>
|
||||
|
||||
|
||||
37
pom.xml
37
pom.xml
@@ -99,6 +99,32 @@
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<!-- Maven 仓库配置 -->
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>huaweicloud</id>
|
||||
<name>Huawei Cloud Repository</name>
|
||||
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>aliyun</id>
|
||||
<name>Aliyun Maven Repository</name>
|
||||
<url>https://maven.aliyun.com/repository/public</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<!-- 依赖声明 -->
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
@@ -477,17 +503,6 @@
|
||||
</resources>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>public</id>
|
||||
<name>huawei nexus</name>
|
||||
<url>https://mirrors.huaweicloud.com/repository/maven/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>public</id>
|
||||
|
||||
Reference in New Issue
Block a user