fix: 物联网平台,amqp数据接入并插入TD数据库相关逻辑编码。

This commit is contained in:
tianyongbao
2026-01-10 01:20:51 +08:00
parent 28c33874f0
commit 0167de4156
56 changed files with 4842 additions and 158 deletions

View File

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

View 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 # 告警通知间隔时间(分钟),同一设备在此时间内不会重复发送通知

View File

@@ -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
# 飞燕平台项目IDProject 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:
# 前端外网访问地址

View File

@@ -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:

View File

@@ -17,4 +17,4 @@ databaseDialectTimestampFormat=yyyy-MM-dd HH:mm:ss
# 是否过滤 Log
filter=true
# 过滤 Log 时所排除的 sql 关键字,以逗号分隔
exclude=
exclude=information_schema.ins_tables

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = "地域 IDcn-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());
}
}
}

View File

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

View File

@@ -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;
/**
* 呼叫的CallIdCallStatus为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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: 更新设备在线状态
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 地域 IDcn-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("==================================");
}
}

View File

@@ -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 地域IDcn-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);
}
}

View File

@@ -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
# 飞燕平台项目IDProject 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控制台 -> 实例详情 -> 开发配置 -> 服务端订阅
# - 消费组 IDIoT控制台 -> 规则引擎 -> 服务端订阅 -> 消费组管理
# - AccessKey阿里云控制台 -> AccessKey 管理

View File

@@ -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; // 设备类型
}

View File

@@ -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);
/**
* 批量插入数据
*

View File

@@ -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);
}
}
/**

View File

@@ -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
View File

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