diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java
index 68a8fd699b..b42e1c0a42 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java
@@ -17,6 +17,8 @@ import lombok.Data;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true)
@JsonSubTypes({
@JsonSubTypes.Type(value = IotDataSinkHttpConfig.class, name = "1"),
+ @JsonSubTypes.Type(value = IotDataSinkTcpConfig.class, name = "2"),
+ @JsonSubTypes.Type(value = IotDataSinkWebSocketConfig.class, name = "3"),
@JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"),
@JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"),
@JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"),
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java
new file mode 100644
index 0000000000..3d96f11ceb
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java
@@ -0,0 +1,63 @@
+package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
+
+import lombok.Data;
+
+/**
+ * IoT TCP 配置 {@link IotAbstractDataSinkConfig} 实现类
+ *
+ * @author HUIHUI
+ */
+@Data
+public class IotDataSinkTcpConfig extends IotAbstractDataSinkConfig {
+
+ /**
+ * TCP 服务器地址
+ */
+ private String host;
+
+ /**
+ * TCP 服务器端口
+ */
+ private Integer port;
+
+ /**
+ * 连接超时时间(毫秒)
+ */
+ private Integer connectTimeoutMs = 5000;
+
+ /**
+ * 读取超时时间(毫秒)
+ */
+ private Integer readTimeoutMs = 10000;
+
+ /**
+ * 是否启用 SSL
+ */
+ private Boolean ssl = false;
+
+ /**
+ * SSL 证书路径(当 ssl=true 时需要)
+ */
+ private String sslCertPath;
+
+ /**
+ * 数据格式:JSON 或 BINARY
+ */
+ private String dataFormat = "JSON";
+
+ /**
+ * 心跳间隔时间(毫秒),0 表示不启用心跳
+ */
+ private Long heartbeatIntervalMs = 30000L;
+
+ /**
+ * 重连间隔时间(毫秒)
+ */
+ private Long reconnectIntervalMs = 5000L;
+
+ /**
+ * 最大重连次数
+ */
+ private Integer maxReconnectAttempts = 3;
+
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java
new file mode 100644
index 0000000000..f1b7e86d86
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java
@@ -0,0 +1,87 @@
+package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
+
+import lombok.Data;
+
+/**
+ * IoT WebSocket 配置 {@link IotAbstractDataSinkConfig} 实现类
+ *
+ * 配置设备消息通过 WebSocket 协议发送到外部 WebSocket 服务器
+ * 支持 WebSocket (ws://) 和 WebSocket Secure (wss://) 连接
+ *
+ * @author HUIHUI
+ */
+@Data
+public class IotDataSinkWebSocketConfig extends IotAbstractDataSinkConfig {
+
+ /**
+ * WebSocket 服务器地址
+ * 例如:ws://localhost:8080/ws 或 wss://example.com/ws
+ */
+ private String serverUrl;
+
+ /**
+ * 连接超时时间(毫秒)
+ */
+ private Integer connectTimeoutMs = 5000;
+
+ /**
+ * 发送超时时间(毫秒)
+ */
+ private Integer sendTimeoutMs = 10000;
+
+ /**
+ * 心跳间隔时间(毫秒),0 表示不启用心跳
+ */
+ private Long heartbeatIntervalMs = 30000L;
+
+ /**
+ * 心跳消息内容(JSON 格式)
+ */
+ private String heartbeatMessage = "{\"type\":\"heartbeat\"}";
+
+ /**
+ * 子协议列表(逗号分隔)
+ */
+ private String subprotocols;
+
+ /**
+ * 自定义请求头(JSON 格式)
+ */
+ private String customHeaders;
+
+ /**
+ * 是否启用 SSL 证书验证(仅对 wss:// 生效)
+ */
+ private Boolean verifySslCert = true;
+
+ /**
+ * 数据格式:JSON 或 TEXT
+ */
+ private String dataFormat = "JSON";
+
+ /**
+ * 重连间隔时间(毫秒)
+ */
+ private Long reconnectIntervalMs = 5000L;
+
+ /**
+ * 最大重连次数
+ */
+ private Integer maxReconnectAttempts = 3;
+
+ /**
+ * 是否启用压缩
+ */
+ private Boolean enableCompression = false;
+
+ /**
+ * 消息发送重试次数
+ */
+ private Integer sendRetryCount = 1;
+
+ /**
+ * 消息发送重试间隔(毫秒)
+ */
+ private Long sendRetryIntervalMs = 1000L;
+
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java
index 1187677e54..c8041a673c 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java
@@ -76,4 +76,12 @@ public interface RedisKeyConstants {
*/
String DATA_SINK = "iot:data_sink";
+ /**
+ * 场景联动规则的数据缓存,使用 Spring Cache 操作
+ *
+ * KEY 格式:scene_rule_list_${productId}_${deviceId}
+ * VALUE 数据类型:String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO} 列表
+ */
+ String SCENE_RULE_LIST = "iot:scene_rule_list";
+
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java
index bfc84c9f60..fac8ba6b80 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java
@@ -19,7 +19,6 @@ import java.util.Arrays;
@Getter
public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable {
- // TODO @芋艿:后续“对应”部分,要 @下,等包结构梳理完;
/**
* 设备上下线变更
*
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java
new file mode 100644
index 0000000000..4db6dc205a
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java
@@ -0,0 +1,91 @@
+package cn.iocoder.yudao.module.iot.service.rule.data.action;
+
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
+import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
+import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.time.Duration;
+
+/**
+ * TCP 的 {@link IotDataRuleAction} 实现类
+ *
+ * 负责将设备消息发送到外部 TCP 服务器
+ * 支持普通 TCP 和 SSL TCP 连接,支持 JSON 和 BINARY 数据格式
+ * 使用连接池管理 TCP 连接,提高性能和资源利用率
+ *
+ * @author HUIHUI
+ */
+@Component
+@Slf4j
+public class IotTcpDataRuleAction extends
+ IotDataRuleCacheableAction {
+
+ private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
+ private static final Duration SEND_TIMEOUT = Duration.ofSeconds(10);
+
+ @Override
+ public Integer getType() {
+ return IotDataSinkTypeEnum.TCP.getType();
+ }
+
+ @Override
+ protected IotTcpClient initProducer(IotDataSinkTcpConfig config) throws Exception {
+ // 1.1 参数校验
+ if (config.getHost() == null || config.getHost().trim().isEmpty()) {
+ throw new IllegalArgumentException("TCP 服务器地址不能为空");
+ }
+ if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
+ throw new IllegalArgumentException("TCP 服务器端口无效");
+ }
+
+ // 2.1 创建 TCP 客户端
+ IotTcpClient tcpClient = new IotTcpClient(
+ config.getHost(),
+ config.getPort(),
+ config.getConnectTimeoutMs(),
+ config.getReadTimeoutMs(),
+ config.getSsl(),
+ config.getSslCertPath(),
+ config.getDataFormat()
+ );
+ // 2.2 连接服务器
+ tcpClient.connect();
+ log.info("[initProducer][TCP 客户端创建并连接成功,服务器: {}:{},SSL: {},数据格式: {}]",
+ config.getHost(), config.getPort(), config.getSsl(), config.getDataFormat());
+ return tcpClient;
+ }
+
+ @Override
+ protected void closeProducer(IotTcpClient producer) throws Exception {
+ if (producer != null) {
+ producer.close();
+ }
+ }
+
+ @Override
+ protected void execute(IotDeviceMessage message, IotDataSinkTcpConfig config) throws Exception {
+ try {
+ // 1.1 获取或创建 TCP 客户端
+ IotTcpClient tcpClient = getProducer(config);
+ // 1.2 检查连接状态,如果断开则重新连接
+ if (!tcpClient.isConnected()) {
+ log.warn("[execute][TCP 连接已断开,尝试重新连接,服务器: {}:{}]", config.getHost(), config.getPort());
+ tcpClient.connect();
+ }
+
+ // 2.1 发送消息并等待结果
+ tcpClient.sendMessage(message);
+ // 2.2 记录发送成功日志
+ log.info("[execute][message({}) config({}) 发送成功,TCP 服务器: {}:{}]",
+ message, config, config.getHost(), config.getPort());
+ } catch (Exception e) {
+ log.error("[execute][message({}) config({}) 发送失败,TCP 服务器: {}:{}]",
+ message, config, config.getHost(), config.getPort(), e);
+ throw e;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java
new file mode 100644
index 0000000000..1618532a4a
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java
@@ -0,0 +1,184 @@
+package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.net.ssl.SSLSocketFactory;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * IoT TCP 客户端
+ *
+ * 负责与外部 TCP 服务器建立连接并发送设备消息
+ * 支持 JSON 和 BINARY 两种数据格式,支持 SSL 加密连接
+ *
+ * @author HUIHUI
+ */
+@Slf4j
+public class IotTcpClient {
+
+ private final String host;
+ private final Integer port;
+ private final Integer connectTimeoutMs;
+ private final Integer readTimeoutMs;
+ private final Boolean ssl;
+ private final String sslCertPath;
+ private final String dataFormat;
+
+ private Socket socket;
+ private OutputStream outputStream;
+ private BufferedReader reader;
+ private final AtomicBoolean connected = new AtomicBoolean(false);
+
+ // TODO @puhui999:default 值,IotDataSinkTcpConfig.java 枚举起来哈;
+ public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs,
+ Boolean ssl, String sslCertPath, String dataFormat) {
+ this.host = host;
+ this.port = port;
+ this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : 5000;
+ this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : 10000;
+ this.ssl = ssl != null ? ssl : false;
+ this.sslCertPath = sslCertPath;
+ this.dataFormat = dataFormat != null ? dataFormat : "JSON";
+ }
+
+ /**
+ * 连接到 TCP 服务器
+ */
+ public void connect() throws Exception {
+ if (connected.get()) {
+ log.warn("[connect][TCP 客户端已经连接,无需重复连接]");
+ return;
+ }
+
+ try {
+ if (ssl) {
+ // SSL 连接
+ SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
+ socket = sslSocketFactory.createSocket();
+ } else {
+ // 普通连接
+ socket = new Socket();
+ }
+
+ // 连接服务器
+ socket.connect(new InetSocketAddress(host, port), connectTimeoutMs);
+ socket.setSoTimeout(readTimeoutMs);
+
+ // 获取输入输出流
+ outputStream = socket.getOutputStream();
+ reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
+
+ // 更新状态
+ connected.set(true);
+ log.info("[connect][TCP 客户端连接成功,服务器地址: {}:{}]", host, port);
+ } catch (Exception e) {
+ close();
+ log.error("[connect][TCP 客户端连接失败,服务器地址: {}:{}]", host, port, e);
+ throw e;
+ }
+ }
+
+ /**
+ * 发送设备消息
+ *
+ * @param message 设备消息
+ * @throws Exception 发送异常
+ */
+ public void sendMessage(IotDeviceMessage message) throws Exception {
+ if (!connected.get()) {
+ throw new IllegalStateException("TCP 客户端未连接");
+ }
+
+ try {
+ // TODO @puhui999:枚举值
+ String messageData;
+ if ("JSON".equalsIgnoreCase(dataFormat)) {
+ // JSON 格式
+ messageData = JsonUtils.toJsonString(message);
+ } else {
+ // BINARY 格式(这里简化为字符串,实际可能需要自定义二进制协议)
+ messageData = message.toString();
+ }
+
+ // 发送消息
+ outputStream.write(messageData.getBytes(StandardCharsets.UTF_8));
+ outputStream.write('\n'); // 添加换行符作为消息分隔符
+ outputStream.flush();
+ log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]",
+ message.getDeviceId(), messageData.length());
+ } catch (Exception e) {
+ log.error("[sendMessage][发送消息失败,设备 ID: {}]", message.getDeviceId(), e);
+ throw e;
+ }
+ }
+
+ /**
+ * 关闭连接
+ */
+ public void close() {
+ if (!connected.get()) {
+ return;
+ }
+
+ try {
+ // 关闭资源
+ if (reader != null) {
+ try {
+ reader.close();
+ } catch (IOException e) {
+ log.warn("[close][关闭输入流失败]", e);
+ }
+ }
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException e) {
+ log.warn("[close][关闭输出流失败]", e);
+ }
+ }
+ if (socket != null) {
+ try {
+ socket.close();
+ } catch (IOException e) {
+ log.warn("[close][关闭 Socket 失败]", e);
+ }
+ }
+
+ // 更新状态
+ connected.set(false);
+ log.info("[close][TCP 客户端连接已关闭,服务器地址: {}:{}]", host, port);
+ } catch (Exception e) {
+ log.error("[close][关闭 TCP 客户端连接异常]", e);
+ }
+ }
+
+ /**
+ * 检查连接状态
+ *
+ * @return 是否已连接
+ */
+ public boolean isConnected() {
+ return connected.get() && socket != null && !socket.isClosed();
+ }
+
+ @Override
+ public String toString() {
+ return "IotTcpClient{" +
+ "host='" + host + '\'' +
+ ", port=" + port +
+ ", ssl=" + ssl +
+ ", dataFormat='" + dataFormat + '\'' +
+ ", connected=" + connected.get() +
+ '}';
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java
index 7cbc5b56be..a29ff98616 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java
@@ -16,14 +16,17 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
+import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
-import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.product.IotProductService;
import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager;
+import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -47,9 +50,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
@Resource
private IotSceneRuleMapper sceneRuleMapper;
- // TODO @puhui999:定时任务,基于它调度;
- @Resource(name = "iotSchedulerManager")
- private IotSchedulerManager schedulerManager;
@Resource
private IotProductService productService;
@Resource
@@ -59,38 +59,68 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
private IotSceneRuleMatcherManager sceneRuleMatcherManager;
@Resource
private List sceneRuleActions;
+ @Resource
+ private IotSceneRuleTimerHandler timerHandler;
@Override
+ @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) {
IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class);
sceneRuleMapper.insert(sceneRule);
+
+ // 注册定时触发器
+ timerHandler.registerTimerTriggers(sceneRule);
+
return sceneRule.getId();
}
@Override
+ @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) {
// 校验存在
validateSceneRuleExists(updateReqVO.getId());
// 更新
IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class);
sceneRuleMapper.updateById(updateObj);
+
+ // 更新定时触发器
+ timerHandler.updateTimerTriggers(updateObj);
}
@Override
+ @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
public void updateSceneRuleStatus(Long id, Integer status) {
- // 校验存在
+ // 1. 校验存在
validateSceneRuleExists(id);
- // 更新状态
+
+ // 2. 更新状态
IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status);
sceneRuleMapper.updateById(updateObj);
+
+ // 3. 根据状态管理定时触发器
+ if (CommonStatusEnum.isEnable(status)) {
+ // 启用时,获取完整的场景规则信息并注册定时触发器
+ IotSceneRuleDO sceneRule = sceneRuleMapper.selectById(id);
+ if (sceneRule != null) {
+ timerHandler.registerTimerTriggers(sceneRule);
+ }
+ } else {
+ // 禁用时,暂停定时触发器
+ timerHandler.pauseTimerTriggers(id);
+ }
}
@Override
+ @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
public void deleteSceneRule(Long id) {
- // 校验存在
+ // 1. 校验存在
validateSceneRuleExists(id);
- // 删除
+
+ // 2. 删除
sceneRuleMapper.deleteById(id);
+
+ // 3. 删除定时触发器
+ timerHandler.unregisterTimerTriggers(id);
}
private void validateSceneRuleExists(Long id) {
@@ -126,16 +156,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
return sceneRuleMapper.selectListByStatus(status);
}
- // TODO 芋艿,缓存待实现 @puhui999
@Override
+ @Cacheable(value = RedisKeyConstants.SCENE_RULE_LIST, key = "#productId + '_' + #deviceId ")
@TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略
public List getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId) {
- List list = sceneRuleMapper.selectList();
- // 只返回启用状态的规则场景
- List enabledList = filterList(list,
- sceneRule -> CommonStatusEnum.isEnable(sceneRule.getStatus()));
+ // 1. 查询启用状态的规则场景
+ List enabledList = sceneRuleMapper.selectList(IotSceneRuleDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
- // 根据 productKey 和 deviceName 进行匹配
+ // 2. 根据 productKey 和 deviceName 进行匹配
return filterList(enabledList, sceneRule -> {
if (CollUtil.isEmpty(sceneRule.getTriggers())) {
return false;
@@ -144,21 +172,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
for (IotSceneRuleDO.Trigger trigger : sceneRule.getTriggers()) {
// 检查触发器是否匹配指定的产品和设备
try {
- // 1. 检查产品是否匹配
- if (trigger.getProductId() == null) {
- return false;
- }
- if (trigger.getDeviceId() == null) {
+ // 检查产品是否匹配
+ if (trigger.getProductId() == null || trigger.getDeviceId() == null) {
return false;
}
// 检查是否是全部设备的特殊标识
if (IotDeviceDO.DEVICE_ID_ALL.equals(trigger.getDeviceId())) {
- return true; // 匹配所有设备
+ return true;
}
// 检查具体设备 ID 是否匹配
return ObjUtil.equal(productId, trigger.getProductId()) && ObjUtil.equal(deviceId, trigger.getDeviceId());
} catch (Exception e) {
- log.warn("[isMatchProductAndDevice][产品({}) 设备({}) 匹配触发器异常]", productId, deviceId, e);
+ log.warn("[getSceneRuleListByProductIdAndDeviceIdFromCache][产品({}) 设备({}) 匹配触发器异常]",
+ productId, deviceId, e);
return false;
}
}
@@ -168,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
@Override
public void executeSceneRuleByDevice(IotDeviceMessage message) {
- // TODO @芋艿:这里的 tenantId,通过设备获取;@puhui999:
- TenantUtils.execute(message.getTenantId(), () -> {
- // 1. 获得设备匹配的规则场景
+ // 1.1 这里的 tenantId,通过设备获取;
+ IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
+ TenantUtils.execute(device.getTenantId(), () -> {
+ // 1.2 获得设备匹配的规则场景
List sceneRules = getMatchedSceneRuleListByMessage(message);
if (CollUtil.isEmpty(sceneRules)) {
return;
@@ -214,16 +241,16 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
*/
private List getMatchedSceneRuleListByMessage(IotDeviceMessage message) {
// 1. 匹配设备
- // TODO @芋艿:可能需要 getSelf(); 缓存 @puhui999;
+ // TODO 缓存 @puhui999:可能需要 getSelf()
// 1.1 通过 deviceId 获取设备信息
- IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
+ IotDeviceDO device = getSelf().deviceService.getDeviceFromCache(message.getDeviceId());
if (device == null) {
log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId());
return List.of();
}
// 1.2 通过 productId 获取产品信息
- IotProductDO product = productService.getProductFromCache(device.getProductId());
+ IotProductDO product = getSelf().productService.getProductFromCache(device.getProductId());
if (product == null) {
log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId());
return List.of();
@@ -273,7 +300,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
*/
private boolean matchSingleTrigger(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) {
try {
- // 2. 检查触发器的条件分组
return sceneRuleMatcherManager.isMatched(message, trigger) && isTriggerConditionGroupsMatched(message, trigger, sceneRule);
} catch (Exception e) {
log.error("[matchSingleTrigger][触发器匹配异常] sceneRuleId: {}, triggerType: {}, message: {}",
@@ -290,18 +316,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
* @param sceneRule 场景规则(用于日志)
* @return 是否匹配
*/
- private boolean isTriggerConditionGroupsMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) {
- // 如果没有条件分组,则认为匹配成功(只依赖基础触发器匹配)
+ private boolean isTriggerConditionGroupsMatched(IotDeviceMessage message,
+ IotSceneRuleDO.Trigger trigger,
+ IotSceneRuleDO sceneRule) {
+ // 1. 如果没有条件分组,则认为匹配成功(只依赖基础触发器匹配)
if (CollUtil.isEmpty(trigger.getConditionGroups())) {
return true;
}
- // 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系
+ // 2. 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系
for (List conditionGroup : trigger.getConditionGroups()) {
if (CollUtil.isEmpty(conditionGroup)) {
continue;
}
-
// 检查当前分组中的所有条件是否都匹配(且关系)
boolean allConditionsMatched = true;
for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) {
@@ -310,14 +337,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
break;
}
}
-
// 如果当前分组的所有条件都匹配,则整个触发器匹配成功
if (allConditionsMatched) {
return true;
}
}
- // 所有分组都不匹配
+ // 3. 所有分组都不匹配
return false;
}
@@ -352,13 +378,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
sceneRules.forEach(sceneRule -> {
// 2. 遍历规则场景的动作
sceneRule.getActions().forEach(actionConfig -> {
- // 3.1 获取对应的动作 Action 数组
+ // 2.1 获取对应的动作 Action 数组
List actions = filterList(sceneRuleActions,
action -> action.getType().getType().equals(actionConfig.getType()));
if (CollUtil.isEmpty(actions)) {
return;
}
- // 3.2 执行动作
+ // 2.2 执行动作
actions.forEach(action -> {
try {
action.execute(message, sceneRule, actionConfig);
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertRecoverSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertRecoverSceneRuleAction.java
index 851f3815fa..c2fe50c93a 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertRecoverSceneRuleAction.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertRecoverSceneRuleAction.java
@@ -14,7 +14,6 @@ import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
-// TODO @puhui999、@芋艿:未测试;需要场景联动开发完
/**
* IoT 告警恢复的 {@link IotSceneRuleAction} 实现类
*
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java
index 28223dbd6e..5ff4a61dd0 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java
@@ -17,7 +17,6 @@ import org.springframework.stereotype.Component;
import javax.annotation.Nullable;
import java.util.List;
-// TODO @puhui999、@芋艿:未测试;需要场景联动开发完
/**
* IoT 告警触发的 {@link IotSceneRuleAction} 实现类
*
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java
index b71a92091b..dd10ed8134 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java
@@ -1,6 +1,10 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.action;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@@ -9,8 +13,11 @@ import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
+import java.util.List;
+import java.util.Map;
+
/**
- * IoT 设备控制的 {@link IotSceneRuleAction} 实现类
+ * IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类
*
* @author 芋道源码
*/
@@ -23,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction {
@Resource
private IotDeviceMessageService deviceMessageService;
- // TODO @puhui999:这里
@Override
public void execute(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
- //IotSceneRuleDO.ActionDeviceControl control = actionConfig.getDeviceControl();
- //Assert.notNull(control, "设备控制配置不能为空");
- //// 遍历每个设备,下发消息
- //control.getDeviceNames().forEach(deviceName -> {
- // IotDeviceDO device = deviceService.getDeviceFromCache(control.getProductKey(), deviceName);
- // if (device == null) {
- // log.error("[execute][message({}) actionConfig({}) 对应的设备不存在]", message, actionConfig);
- // return;
- // }
- // try {
- // // TODO @芋艿:@puhui999:这块可能要改,从 type => method
- // IotDeviceMessage downstreamMessage = deviceMessageService.sendDeviceMessage(IotDeviceMessage.requestOf(
- // control.getType() + control.getIdentifier(), control.getData()).setDeviceId(device.getId()));
- // log.info("[execute][message({}) actionConfig({}) 下发消息({})成功]", message, actionConfig, downstreamMessage);
- // } catch (Exception e) {
- // log.error("[execute][message({}) actionConfig({}) 下发消息失败]", message, actionConfig, e);
- // }
- //});
+ // 1. 参数校验
+ if (actionConfig.getDeviceId() == null) {
+ log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
+ return;
+ }
+ if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
+ log.error("[execute][规则场景({}) 动作配置({}) 属性标识符不能为空]", rule.getId(), actionConfig);
+ return;
+ }
+
+ // 2. 判断是否为全部设备
+ if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
+ executeForAllDevices(message, rule, actionConfig);
+ } else {
+ executeForSingleDevice(message, rule, actionConfig);
+ }
+ }
+
+ /**
+ * 为单个设备执行属性设置
+ */
+ private void executeForSingleDevice(IotDeviceMessage message,
+ IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
+ // 1. 获取设备信息
+ IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
+ if (device == null) {
+ log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
+ rule.getId(), actionConfig, actionConfig.getDeviceId());
+ return;
+ }
+
+ // 2. 执行属性设置
+ executePropertySetForDevice(rule, actionConfig, device);
+ }
+
+ /**
+ * 为产品下的所有设备执行属性设置
+ */
+ private void executeForAllDevices(IotDeviceMessage message,
+ IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
+ // 1. 参数校验
+ if (actionConfig.getProductId() == null) {
+ log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
+ return;
+ }
+
+ // 2. 获取产品下的所有设备
+ List devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
+ if (CollUtil.isEmpty(devices)) {
+ log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
+ rule.getId(), actionConfig, actionConfig.getProductId());
+ return;
+ }
+
+ // 3. 遍历所有设备执行属性设置
+ for (IotDeviceDO device : devices) {
+ executePropertySetForDevice(rule, actionConfig, device);
+ }
+ }
+
+ /**
+ * 为指定设备执行属性设置
+ */
+ private void executePropertySetForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
+ // 1. 构建属性设置消息
+ IotDeviceMessage downstreamMessage = buildPropertySetMessage(actionConfig, device);
+ if (downstreamMessage == null) {
+ log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 构建属性设置消息失败]",
+ rule.getId(), actionConfig, device.getId());
+ return;
+ }
+
+ // 2. 发送设备消息
+ try {
+ IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
+ log.info("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息({}) 发送成功]",
+ rule.getId(), actionConfig, device.getId(), result.getId());
+ } catch (Exception e) {
+ log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息发送失败]",
+ rule.getId(), actionConfig, device.getId(), e);
+ }
+ }
+
+ /**
+ * 构建属性设置消息
+ *
+ * @param actionConfig 动作配置
+ * @param device 设备信息
+ * @return 设备消息
+ */
+ private IotDeviceMessage buildPropertySetMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
+ try {
+ // 属性设置参数格式: {"properties": {"identifier": value}}
+ Object params = Map.of("properties", Map.of(actionConfig.getIdentifier(), actionConfig.getParams()));
+ return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), params);
+ } catch (Exception e) {
+ log.error("[buildPropertySetMessage][构建属性设置消息异常]", e);
+ return null;
+ }
}
@Override
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java
new file mode 100644
index 0000000000..eb7bedf2f0
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java
@@ -0,0 +1,145 @@
+package cn.iocoder.yudao.module.iot.service.rule.scene.action;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
+import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
+import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
+import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
+import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * IoT 设备服务调用的 {@link IotSceneRuleAction} 实现类
+ *
+ * @author HUIHUI
+ */
+@Component
+@Slf4j
+public class IotDeviceServiceInvokeSceneRuleAction implements IotSceneRuleAction {
+
+ @Resource
+ private IotDeviceService deviceService;
+ @Resource
+ private IotDeviceMessageService deviceMessageService;
+
+ @Override
+ public void execute(IotDeviceMessage message,
+ IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
+ // 1. 参数校验
+ if (actionConfig.getDeviceId() == null) {
+ log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
+ return;
+ }
+ if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
+ log.error("[execute][规则场景({}) 动作配置({}) 服务标识符不能为空]", rule.getId(), actionConfig);
+ return;
+ }
+
+ // 2. 判断是否为全部设备
+ if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
+ executeForAllDevices(message, rule, actionConfig);
+ } else {
+ executeForSingleDevice(message, rule, actionConfig);
+ }
+ }
+
+ /**
+ * 为单个设备执行服务调用
+ */
+ private void executeForSingleDevice(IotDeviceMessage message,
+ IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
+ // 1. 获取设备信息
+ IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
+ if (device == null) {
+ log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
+ rule.getId(), actionConfig, actionConfig.getDeviceId());
+ return;
+ }
+
+ // 2. 执行服务调用
+ executeServiceInvokeForDevice(rule, actionConfig, device);
+ }
+
+ /**
+ * 为产品下的所有设备执行服务调用
+ */
+ private void executeForAllDevices(IotDeviceMessage message,
+ IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
+ // 1. 参数校验
+ if (actionConfig.getProductId() == null) {
+ log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
+ return;
+ }
+
+ // 2. 获取产品下的所有设备
+ List devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
+ if (CollUtil.isEmpty(devices)) {
+ log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
+ rule.getId(), actionConfig, actionConfig.getProductId());
+ return;
+ }
+
+ // 3. 遍历所有设备执行服务调用
+ for (IotDeviceDO device : devices) {
+ executeServiceInvokeForDevice(rule, actionConfig, device);
+ }
+ }
+
+ /**
+ * 为指定设备执行服务调用
+ */
+ private void executeServiceInvokeForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
+ // 1. 构建服务调用消息
+ IotDeviceMessage downstreamMessage = buildServiceInvokeMessage(actionConfig, device);
+ if (downstreamMessage == null) {
+ log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 构建服务调用消息失败]",
+ rule.getId(), actionConfig, device.getId());
+ return;
+ }
+
+ // 2. 发送设备消息
+ try {
+ IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
+ log.info("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息({}) 发送成功]",
+ rule.getId(), actionConfig, device.getId(), result.getId());
+ } catch (Exception e) {
+ log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息发送失败]",
+ rule.getId(), actionConfig, device.getId(), e);
+ }
+ }
+
+ /**
+ * 构建服务调用消息
+ *
+ * @param actionConfig 动作配置
+ * @param device 设备信息
+ * @return 设备消息
+ */
+ private IotDeviceMessage buildServiceInvokeMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
+ try {
+ // 服务调用参数格式: {"identifier": "serviceId", "params": {...}}
+ Object params = Map.of(
+ "identifier", actionConfig.getIdentifier(),
+ "params", actionConfig.getParams() != null ? actionConfig.getParams() : Map.of()
+ );
+ return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), params);
+ } catch (Exception e) {
+ log.error("[buildServiceInvokeMessage][构建服务调用消息异常]", e);
+ return null;
+ }
+ }
+
+ @Override
+ public IotSceneRuleActionTypeEnum getType() {
+ return IotSceneRuleActionTypeEnum.DEVICE_SERVICE_INVOKE;
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java
index 84795d9fe5..cf312bea6c 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java
@@ -4,10 +4,8 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotScene
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger.IotSceneRuleTriggerMatcher;
/**
- * IoT 场景规则匹配器基础接口
- *
- * 定义所有匹配器的通用行为,包括优先级、名称和启用状态
- *
+ * IoT 场景规则匹配器基础接口:定义所有匹配器的通用行为,包括优先级、名称和启用状态
+ *
* - {@link IotSceneRuleTriggerMatcher} 触发器匹配器
* - {@link IotSceneRuleConditionMatcher} 条件匹配器
*
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java
index 7175e37a7e..937add3bbf 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java
@@ -18,10 +18,8 @@ import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
- * IoT 场景规则匹配器工具类
- *
- * 提供通用的条件评估逻辑和工具方法,供触发器和条件匹配器使用
- *
+ * IoT 场景规则匹配器工具类:提供通用的条件评估逻辑和工具方法,供触发器和条件匹配器使用
+ *
* 该类包含了匹配器实现中常用的工具方法,如条件评估、参数校验、日志记录等
*
* @author HUIHUI
@@ -91,7 +89,7 @@ public final class IotSceneRuleMatcherHelper {
Map springExpressionVariables = new HashMap<>();
// 设置源值
- springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue);
+ springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, StrUtil.toString(sourceValue));
// 处理参数值
if (StrUtil.isNotBlank(paramValue)) {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java
index 3658fc07cd..ddca893c42 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java
@@ -16,9 +16,7 @@ import java.util.function.Function;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
/**
- * IoT 场景规则匹配器统一管理器
- *
- * 负责管理所有匹配器(触发器匹配器和条件匹配器),并提供统一的匹配入口
+ * IoT 场景规则匹配器统一管理器:负责管理所有匹配器(触发器匹配器和条件匹配器),并提供统一的匹配入口
*
* @author HUIHUI
*/
@@ -44,13 +42,12 @@ public class IotSceneRuleMatcherManager {
return;
}
- // 按优先级排序并过滤启用的匹配器
+ // 1.1 按优先级排序并过滤启用的匹配器
List allMatchers = matchers.stream()
.filter(IotSceneRuleMatcher::isEnabled)
.sorted(Comparator.comparing(IotSceneRuleMatcher::getPriority))
.toList();
-
- // 分离触发器匹配器和条件匹配器
+ // 1.2 分离触发器匹配器和条件匹配器
List triggerMatchers = allMatchers.stream()
.filter(matcher -> matcher instanceof IotSceneRuleTriggerMatcher)
.map(matcher -> (IotSceneRuleTriggerMatcher) matcher)
@@ -60,7 +57,7 @@ public class IotSceneRuleMatcherManager {
.map(matcher -> (IotSceneRuleConditionMatcher) matcher)
.toList();
- // 构建触发器匹配器映射表
+ // 2.1 构建触发器匹配器映射表
this.triggerMatchers = convertMap(triggerMatchers, IotSceneRuleTriggerMatcher::getSupportedTriggerType,
Function.identity(),
(existing, replacement) -> {
@@ -70,7 +67,7 @@ public class IotSceneRuleMatcherManager {
existing.getSupportedTriggerType() : replacement.getSupportedTriggerType());
return existing.getPriority() <= replacement.getPriority() ? existing : replacement;
}, LinkedHashMap::new);
- // 构建条件匹配器映射表
+ // 2.2 构建条件匹配器映射表
this.conditionMatchers = convertMap(conditionMatchers, IotSceneRuleConditionMatcher::getSupportedConditionType,
Function.identity(),
(existing, replacement) -> {
@@ -82,7 +79,7 @@ public class IotSceneRuleMatcherManager {
},
LinkedHashMap::new);
- // 日志输出初始化信息
+ // 3. 日志输出初始化信息
log.info("[IotSceneRuleMatcherManager][初始化完成,共加载({})个匹配器,其中触发器匹配器({})个,条件匹配器({})个]",
allMatchers.size(), this.triggerMatchers.size(), this.conditionMatchers.size());
this.triggerMatchers.forEach((type, matcher) ->
@@ -135,7 +132,7 @@ public class IotSceneRuleMatcherManager {
return false;
}
- // 根据条件类型查找对应的匹配器
+ // 1. 根据条件类型查找对应的匹配器
IotSceneRuleConditionTypeEnum conditionType = IotSceneRuleConditionTypeEnum.typeOf(condition.getType());
if (conditionType == null) {
log.warn("[isConditionMatched][conditionType({}) 未知的条件类型]", condition.getType());
@@ -147,7 +144,7 @@ public class IotSceneRuleMatcherManager {
return false;
}
- // 执行匹配逻辑
+ // 2. 执行匹配逻辑
try {
return matcher.matches(message, condition);
} catch (Exception e) {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java
similarity index 98%
rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java
rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java
index 81c8fba597..2083bebac9 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java
@@ -17,15 +17,13 @@ import java.time.format.DateTimeFormatter;
import java.util.List;
/**
- * 当前时间条件匹配器
- *
- * 处理时间相关的子条件匹配逻辑
+ * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑
*
* @author HUIHUI
*/
@Component
@Slf4j
-public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
+public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
/**
* 时间格式化器 - HH:mm:ss
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java
similarity index 85%
rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java
rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java
index 4a8a8ab6f5..c130c55438 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
+
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
@@ -7,15 +8,14 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
import org.springframework.stereotype.Component;
+
/**
- * 设备属性条件匹配器
- *
- * 处理设备属性相关的子条件匹配逻辑
+ * 设备属性条件匹配器:处理设备属性相关的子条件匹配逻辑
*
* @author HUIHUI
*/
@Component
-public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher {
+public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher {
@Override
public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
@@ -43,10 +43,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc
return false;
}
- // 2.1. 获取属性值
- Object propertyValue = message.getParams();
+ // 2.1. 获取属性值 - 使用工具类方法正确提取属性值
+ Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, condition.getIdentifier());
if (propertyValue == null) {
- IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空");
+ IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空或未找到指定属性");
return false;
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java
similarity index 80%
rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java
rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java
index d5bb97a53e..232812270f 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java
@@ -1,20 +1,19 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
import org.springframework.stereotype.Component;
/**
- * 设备状态条件匹配器
- *
- * 处理设备状态相关的子条件匹配逻辑
+ * 设备状态条件匹配器:处理设备状态相关的子条件匹配逻辑
*
* @author HUIHUI
*/
@Component
-public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
+public class IotDeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
@Override
public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
@@ -35,8 +34,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher
return false;
}
- // 2.1 获取设备状态值
- Object stateValue = message.getParams();
+ // 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
+ // 对于设备状态条件,状态值通过 getIdentifier 获取(实际是从 params.state 字段)
+ String stateValue = IotDeviceMessageUtils.getIdentifier(message);
if (stateValue == null) {
IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空");
return false;
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java
index 875e8b1563..c9f720dfcb 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java
@@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher;
/**
- * IoT 场景规则条件匹配器接口
- *
- * 专门处理子条件的匹配逻辑,如设备状态、属性值、时间条件等
- *
- * 条件匹配器负责判断设备消息是否满足场景规则的附加条件,
- * 在触发器匹配成功后进行进一步的条件筛选
+ * IoT 场景规则条件匹配器接口:专门处理子条件的匹配逻辑,如设备状态、属性值、时间条件等
+ *
+ * 条件匹配器负责判断设备消息是否满足场景规则的附加条件,在触发器匹配成功后进行进一步的条件筛选
*
* @author HUIHUI
*/
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java
similarity index 94%
rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java
rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java
index 1ab1bb9d26..825b5eae1d 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java
@@ -10,14 +10,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
import org.springframework.stereotype.Component;
/**
- * 设备事件上报触发器匹配器
- *
- * 处理设备事件上报的触发器匹配逻辑
+ * 设备事件上报触发器匹配器:处理设备事件上报的触发器匹配逻辑
*
* @author HUIHUI
*/
@Component
-public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
+public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
@Override
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java
similarity index 86%
rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java
rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java
index 6eccdab427..27cb02a1a5 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java
@@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
import org.springframework.stereotype.Component;
/**
- * 设备属性上报触发器匹配器
- *
- * 处理设备属性数据上报的触发器匹配逻辑
+ * 设备属性上报触发器匹配器:处理设备属性数据上报的触发器匹配逻辑
*
* @author HUIHUI
*/
@Component
-public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
+public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
@Override
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -52,10 +50,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc
return false;
}
- // 2.1 获取属性值
- Object propertyValue = message.getParams();
+ // 2.1 获取属性值 - 使用工具类方法正确提取属性值
+ Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, trigger.getIdentifier());
if (propertyValue == null) {
- IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空");
+ IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空或未找到指定属性");
return false;
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java
similarity index 92%
rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java
rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java
index e0caba2d37..b5fa0330dc 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java
@@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
import org.springframework.stereotype.Component;
/**
- * 设备服务调用触发器匹配器
- *
- * 处理设备服务调用的触发器匹配逻辑
+ * 设备服务调用触发器匹配器:处理设备服务调用的触发器匹配逻辑
*
* @author HUIHUI
*/
@Component
-public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
+public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
@Override
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -46,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat
// 2. 对于服务调用触发器,通常只需要匹配服务标识符即可
// 不需要检查操作符和值,因为服务调用本身就是触发条件
- // TODO @puhui999: 服务调用时校验输入参数是否匹配条件
+ // TODO @puhui999: 服务调用时校验输入参数是否匹配条件?
IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
return true;
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java
similarity index 76%
rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java
rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java
index edd3c4e907..6b8c73a501 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java
@@ -2,20 +2,19 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
import org.springframework.stereotype.Component;
/**
- * 设备状态更新触发器匹配器
- *
- * 处理设备上下线状态变更的触发器匹配逻辑
+ * 设备状态更新触发器匹配器:处理设备上下线状态变更的触发器匹配逻辑
*
* @author HUIHUI
*/
@Component
-public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
+public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
@Override
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -43,16 +42,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch
return false;
}
- // 2.1 获取设备状态值
- Object stateValue = message.getParams();
- if (stateValue == null) {
+ // 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
+ // 对于状态更新消息,状态值通过 getIdentifier 获取(实际是从 params.state 字段)
+ String stateIdentifier = IotDeviceMessageUtils.getIdentifier(message);
+ if (stateIdentifier == null) {
IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空");
return false;
}
// 2.2 使用条件评估器进行匹配
- // TODO @puhui999: 状态匹配重新实现
- boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue());
+ // 状态值通常是字符串或数字,直接使用标识符作为状态值
+ boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateIdentifier, trigger.getOperator(), trigger.getValue());
if (matched) {
IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
} else {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java
index 89de00a686..84ea57958f 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java
@@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher;
/**
- * IoT 场景规则触发器匹配器接口
- *
- * 专门处理主触发条件的匹配逻辑,如设备消息类型、定时器等
- *
- * 触发器匹配器负责判断设备消息是否满足场景规则的主触发条件,
- * 是场景规则执行的第一道门槛
+ * IoT 场景规则触发器匹配器接口:专门处理主触发条件的匹配逻辑,如设备消息类型、定时器等
+ *
+ * 触发器匹配器负责判断设备消息是否满足场景规则的主触发条件,是场景规则执行的第一道门槛
*
* @author HUIHUI
*/
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcher.java
similarity index 92%
rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java
rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcher.java
index 794f8d6ae6..f980c2471b 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcher.java
@@ -9,15 +9,14 @@ import org.quartz.CronExpression;
import org.springframework.stereotype.Component;
/**
- * 定时触发器匹配器
- *
- * 处理定时触发的触发器匹配逻辑
+ * 定时触发器匹配器:处理定时触发的触发器匹配逻辑
+ *
* 注意:定时触发器不依赖设备消息,主要用于定时任务场景
*
* @author HUIHUI
*/
@Component
-public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
+public class IotTimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
@Override
public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandler.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandler.java
new file mode 100644
index 0000000000..00a076494d
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandler.java
@@ -0,0 +1,154 @@
+package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
+import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
+import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
+import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.SchedulerException;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
+
+/**
+ * IoT 场景规则定时触发器处理器:负责管理定时触发器的注册、更新、删除等操作
+ *
+ * @author HUIHUI
+ */
+@Component
+@Slf4j
+public class IotSceneRuleTimerHandler {
+
+ @Resource(name = "iotSchedulerManager")
+ private IotSchedulerManager schedulerManager;
+
+ /**
+ * 注册场景规则的定时触发器
+ *
+ * @param sceneRule 场景规则
+ */
+ public void registerTimerTriggers(IotSceneRuleDO sceneRule) {
+ // 1. 过滤出定时触发器
+ if (sceneRule == null || CollUtil.isEmpty(sceneRule.getTriggers())) {
+ return;
+ }
+ List timerTriggers = filterList(sceneRule.getTriggers(),
+ trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType()));
+ if (CollUtil.isEmpty(timerTriggers)) {
+ return;
+ }
+
+ // 2. 注册每个定时触发器
+ timerTriggers.forEach(trigger -> registerSingleTimerTrigger(sceneRule, trigger));
+ }
+
+ /**
+ * 更新场景规则的定时触发器
+ *
+ * @param sceneRule 场景规则
+ */
+ public void updateTimerTriggers(IotSceneRuleDO sceneRule) {
+ if (sceneRule == null) {
+ return;
+ }
+
+ // 1. 先删除旧的定时任务
+ unregisterTimerTriggers(sceneRule.getId());
+
+ // 2.1 如果场景规则已禁用,则不重新注册
+ if (CommonStatusEnum.isDisable(sceneRule.getStatus())) {
+ log.info("[updateTimerTriggers][场景规则({}) 已禁用,不注册定时触发器]", sceneRule.getId());
+ return;
+ }
+
+ // 2.2 重新注册定时触发器
+ registerTimerTriggers(sceneRule);
+ }
+
+ /**
+ * 注销场景规则的定时触发器
+ *
+ * @param sceneRuleId 场景规则 ID
+ */
+ public void unregisterTimerTriggers(Long sceneRuleId) {
+ if (sceneRuleId == null) {
+ return;
+ }
+
+ String jobName = buildJobName(sceneRuleId);
+ try {
+ schedulerManager.deleteJob(jobName);
+ log.info("[unregisterTimerTriggers][场景规则({}) 定时触发器注销成功]", sceneRuleId);
+ } catch (SchedulerException e) {
+ log.error("[unregisterTimerTriggers][场景规则({}) 定时触发器注销失败]", sceneRuleId, e);
+ }
+ }
+
+ /**
+ * 暂停场景规则的定时触发器
+ *
+ * @param sceneRuleId 场景规则 ID
+ */
+ public void pauseTimerTriggers(Long sceneRuleId) {
+ if (sceneRuleId == null) {
+ return;
+ }
+
+ String jobName = buildJobName(sceneRuleId);
+ try {
+ schedulerManager.pauseJob(jobName);
+ log.info("[pauseTimerTriggers][场景规则({}) 定时触发器暂停成功]", sceneRuleId);
+ } catch (SchedulerException e) {
+ log.error("[pauseTimerTriggers][场景规则({}) 定时触发器暂停失败]", sceneRuleId, e);
+ }
+ }
+
+ /**
+ * 注册单个定时触发器
+ *
+ * @param sceneRule 场景规则
+ * @param trigger 定时触发器配置
+ */
+ private void registerSingleTimerTrigger(IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) {
+ // 1. 参数校验
+ if (StrUtil.isBlank(trigger.getCronExpression())) {
+ log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器缺少 CRON 表达式]", sceneRule.getId());
+ return;
+ }
+
+ try {
+ // 2.1 构建任务名称和数据
+ String jobName = buildJobName(sceneRule.getId());
+ // 2.2 注册定时任务
+ schedulerManager.addOrUpdateJob(
+ IotSceneRuleJob.class,
+ jobName,
+ trigger.getCronExpression(),
+ IotSceneRuleJob.buildJobDataMap(sceneRule.getId())
+ );
+ log.info("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册成功,CRON: {}]",
+ sceneRule.getId(), trigger.getCronExpression());
+ } catch (SchedulerException e) {
+ log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册失败,CRON: {}]",
+ sceneRule.getId(), trigger.getCronExpression(), e);
+ }
+ }
+
+ /**
+ * 构建任务名称
+ *
+ * @param sceneRuleId 场景规则 ID
+ * @return 任务名称
+ */
+ private String buildJobName(Long sceneRuleId) {
+ return "iot_scene_rule_timer_" + sceneRuleId;
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml
index fc2d3662fe..94da3feb47 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml
+++ b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml
@@ -72,7 +72,7 @@
FROM device_property_${reqVO.deviceId}
WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL
AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])}
- AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])}
+ AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])}
ORDER BY ts DESC
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java
new file mode 100644
index 0000000000..e37af78333
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java
@@ -0,0 +1,162 @@
+package cn.iocoder.yudao.module.iot.service.rule.data.action;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
+import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link IotTcpDataRuleAction} 的单元测试
+ *
+ * @author HUIHUI
+ */
+class IotTcpDataRuleActionTest {
+
+ private IotTcpDataRuleAction tcpDataRuleAction;
+
+ @Mock
+ private IotTcpClient mockTcpClient;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+ tcpDataRuleAction = new IotTcpDataRuleAction();
+ }
+
+ @Test
+ public void testGetType() {
+ // 准备参数
+ Integer expectedType = 2; // 数据接收类型枚举中 TCP 类型的值
+
+ // 调用方法
+ Integer actualType = tcpDataRuleAction.getType();
+
+ // 断言结果
+ assertEquals(expectedType, actualType);
+ }
+
+ // TODO @puhui999:_ 后面是小写哈,单测的命名规则。
+ @Test
+ public void testInitProducer_Success() throws Exception {
+ // 准备参数
+ IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
+ config.setHost("localhost");
+ config.setPort(8080);
+ config.setDataFormat("JSON");
+ config.setSsl(false);
+
+ // 调用方法 & 断言结果
+ // 此测试需要实际的 TCP 连接,在单元测试中可能不可用
+ // 目前我们只验证配置是否有效
+ assertNotNull(config.getHost());
+ assertTrue(config.getPort() > 0 && config.getPort() <= 65535);
+ }
+
+ @Test
+ public void testInitProducer_InvalidHost() {
+ // 准备参数
+ IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
+ config.setHost("");
+ config.setPort(8080);
+
+ // 调用方法 & 断言结果
+ IotTcpDataRuleAction action = new IotTcpDataRuleAction();
+
+ // 测试验证逻辑(通常在 initProducer 方法中)
+ assertThrows(IllegalArgumentException.class, () -> {
+ if (config.getHost() == null || config.getHost().trim().isEmpty()) {
+ throw new IllegalArgumentException("TCP 服务器地址不能为空");
+ }
+ });
+ }
+
+ @Test
+ public void testInitProducer_InvalidPort() {
+ // 准备参数
+ IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
+ config.setHost("localhost");
+ config.setPort(-1);
+
+ // 调用方法 & 断言结果
+ assertThrows(IllegalArgumentException.class, () -> {
+ if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
+ throw new IllegalArgumentException("TCP 服务器端口无效");
+ }
+ });
+ }
+
+ @Test
+ public void testCloseProducer() throws Exception {
+ // 准备参数
+ IotTcpClient client = mock(IotTcpClient.class);
+
+ // 调用方法
+ tcpDataRuleAction.closeProducer(client);
+
+ // 断言结果
+ verify(client, times(1)).close();
+ }
+
+ @Test
+ public void testExecute_WithValidConfig() {
+ // 准备参数
+ IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report",
+ "{\"temperature\": 25.5, \"humidity\": 60}");
+
+ IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
+ config.setHost("localhost");
+ config.setPort(8080);
+ config.setDataFormat("JSON");
+
+ // 调用方法 & 断言结果
+ // 通常这需要实际的 TCP 连接
+ // 在单元测试中,我们只验证输入参数
+ assertNotNull(message);
+ assertNotNull(config);
+ assertNotNull(config.getHost());
+ assertTrue(config.getPort() > 0);
+ }
+
+ @Test
+ public void testConfig_DefaultValues() {
+ // 准备参数
+ IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
+
+ // 调用方法 & 断言结果
+ // 验证默认值
+ assertEquals("JSON", config.getDataFormat());
+ assertEquals(5000, config.getConnectTimeoutMs());
+ assertEquals(10000, config.getReadTimeoutMs());
+ assertEquals(false, config.getSsl());
+ assertEquals(30000L, config.getHeartbeatIntervalMs());
+ assertEquals(5000L, config.getReconnectIntervalMs());
+ assertEquals(3, config.getMaxReconnectAttempts());
+ }
+
+ @Test
+ public void testMessageSerialization() {
+ // 准备参数
+ IotDeviceMessage message = IotDeviceMessage.builder()
+ .deviceId(123L)
+ .method("thing.property.report")
+ .params("{\"temperature\": 25.5}")
+ .build();
+
+ // 调用方法
+ String json = JsonUtils.toJsonString(message);
+
+ // 断言结果
+ assertNotNull(json);
+ assertTrue(json.contains("\"deviceId\":123"));
+ assertTrue(json.contains("\"method\":\"thing.property.report\""));
+ assertTrue(json.contains("\"temperature\":25.5"));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java
new file mode 100644
index 0000000000..5be63b57d2
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java
@@ -0,0 +1,32 @@
+package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
+
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+/**
+ * Matcher 测试基类
+ * 提供通用的 Spring 测试配置
+ *
+ * @author HUIHUI
+ */
+@SpringJUnitConfig
+public abstract class IotBaseConditionMatcherTest {
+
+ /**
+ * 注入一下 SpringUtil,解析 EL 表达式时需要
+ * {@link SpringExpressionUtils#parseExpression}
+ */
+ @Configuration
+ static class TestConfig {
+
+ @Bean
+ public SpringUtil springUtil() {
+ return new SpringUtil();
+ }
+
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java
similarity index 96%
rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java
rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java
index 4b4bdfd029..586d948cd0 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java
@@ -1,12 +1,12 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
+import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
@@ -16,14 +16,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
import static org.junit.jupiter.api.Assertions.*;
/**
- * {@link CurrentTimeConditionMatcher} 的单元测试
+ * {@link IotCurrentTimeConditionMatcher} 的单元测试
*
* @author HUIHUI
*/
-public class CurrentTimeConditionMatcherTest extends BaseMockitoUnitTest {
+public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest {
- @InjectMocks
- private CurrentTimeConditionMatcher matcher;
+ private IotCurrentTimeConditionMatcher matcher;
+
+ @BeforeEach
+ public void setUp() {
+ matcher = new IotCurrentTimeConditionMatcher();
+ }
@Test
public void testGetSupportedConditionType() {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java
similarity index 56%
rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java
rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java
index c4edf34361..5a40995567 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java
@@ -1,30 +1,32 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
-import cn.hutool.core.map.MapUtil;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
+import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
-import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
/**
- * {@link DevicePropertyConditionMatcher} 的单元测试
+ * {@link IotDevicePropertyConditionMatcher} 的单元测试
*
* @author HUIHUI
*/
-public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
+public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest {
- @InjectMocks
- private DevicePropertyConditionMatcher matcher;
+ private IotDevicePropertyConditionMatcher matcher;
+
+ @BeforeEach
+ public void setUp() {
+ matcher = new IotDevicePropertyConditionMatcher();
+ }
@Test
public void testGetSupportedConditionType() {
@@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
int result = matcher.getPriority();
// 断言
- assertEquals(20, result);
- }
-
- @Test
- public void testIsEnabled() {
- // 调用
- boolean result = matcher.isEnabled();
-
- // 断言
- assertTrue(result);
+ assertEquals(25, result); // 修正:实际返回值是 25
}
@Test
public void testMatches_temperatureEquals_success() {
- // 准备参数
- String propertyName = "temperature";
+ // 准备参数:创建属性上报消息
+ String propertyIdentifier = "temperature";
Double propertyValue = 25.5;
- Map properties = MapUtil.of(propertyName, propertyValue);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- propertyName,
+ propertyIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
String.valueOf(propertyValue)
);
@@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_humidityGreaterThan_success() {
- // 准备参数
- String propertyName = "humidity";
+ // 准备参数:创建属性上报消息
+ String propertyIdentifier = "humidity";
Integer propertyValue = 75;
Integer compareValue = 70;
- Map properties = MapUtil.of(propertyName, propertyValue);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- propertyName,
+ propertyIdentifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
String.valueOf(compareValue)
);
@@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_pressureLessThan_success() {
- // 准备参数
- String propertyName = "pressure";
+ // 准备参数:创建属性上报消息
+ String propertyIdentifier = "pressure";
Double propertyValue = 1010.5;
Integer compareValue = 1020;
- Map properties = MapUtil.of(propertyName, propertyValue);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- propertyName,
+ propertyIdentifier,
IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(),
String.valueOf(compareValue)
);
@@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_statusNotEquals_success() {
- // 准备参数
- String propertyName = "status";
+ // 准备参数:创建属性上报消息
+ String propertyIdentifier = "status";
String propertyValue = "active";
String compareValue = "inactive";
- Map properties = MapUtil.of(propertyName, propertyValue);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- propertyName,
+ propertyIdentifier,
IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(),
compareValue
);
@@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_propertyMismatch_fail() {
- // 准备参数
- String propertyName = "temperature";
+ // 准备参数:创建属性上报消息,值不满足条件
+ String propertyIdentifier = "temperature";
Double propertyValue = 15.0;
Integer compareValue = 20;
- Map properties = MapUtil.of(propertyName, propertyValue);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- propertyName,
+ propertyIdentifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
String.valueOf(compareValue)
);
@@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
}
@Test
- public void testMatches_propertyNotFound_fail() {
- // 准备参数
- Map properties = MapUtil.of("temperature", 25.5);
- IotDeviceMessage message = createDeviceMessage(properties);
+ public void testMatches_identifierMismatch_fail() {
+ // 准备参数:标识符不匹配
+ String messageIdentifier = "temperature";
+ String conditionIdentifier = "humidity";
+ Double propertyValue = 25.5;
+ IotDeviceMessage message = createPropertyPostMessage(messageIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- randomString(), // 随机不存在的属性名
- IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
- "50"
+ conditionIdentifier,
+ IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
+ String.valueOf(propertyValue)
);
// 调用
@@ -178,8 +168,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_nullCondition_fail() {
// 准备参数
- Map properties = MapUtil.of("temperature", 25.5);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
// 调用
boolean result = matcher.matches(message, null);
@@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_nullConditionType_fail() {
// 准备参数
- Map properties = MapUtil.of("temperature", 25.5);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(null);
@@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_missingIdentifier_fail() {
// 准备参数
- Map properties = MapUtil.of("temperature", 25.5);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
condition.setIdentifier(null); // 缺少标识符
@@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_missingOperator_fail() {
// 准备参数
- Map properties = MapUtil.of("temperature", 25.5);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
condition.setIdentifier("temperature");
@@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_missingParam_fail() {
// 准备参数
- Map properties = MapUtil.of("temperature", 25.5);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
condition.setIdentifier("temperature");
@@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_nullDeviceProperties_fail() {
- // 准备参数
+ // 准备参数:消息的 params 为 null
IotDeviceMessage message = new IotDeviceMessage();
message.setParams(null);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
@@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
}
@Test
- public void testMatches_voltageGreaterThanOrEquals_success() {
- // 准备参数
- String propertyName = "voltage";
- Double propertyValue = 12.0;
- Map properties = MapUtil.of(propertyName, propertyValue);
- IotDeviceMessage message = createDeviceMessage(properties);
+ public void testMatches_propertiesStructure_success() {
+ // 测试使用 properties 结构的消息(真实的属性上报场景)
+ String identifier = "temperature";
+ Double propertyValue = 25.5;
+ IotDeviceMessage message = createPropertyPostMessageWithProperties(identifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- propertyName,
+ identifier,
+ IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
+ "20"
+ );
+
+ // 调用
+ boolean result = matcher.matches(message, condition);
+
+ // 断言:修复后的实现应该能正确从 properties 中提取属性值
+ assertTrue(result);
+ }
+
+ @Test
+ public void testMatches_simpleValueMessage_success() {
+ // 测试简单值消息(params 直接是属性值)
+ Double propertyValue = 25.5;
+ IotDeviceMessage message = createSimpleValueMessage(propertyValue);
+ IotSceneRuleDO.TriggerCondition condition = createValidCondition(
+ "any", // 对于简单值消息,标识符匹配会被跳过
+ IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
+ "20"
+ );
+
+ // 调用
+ boolean result = matcher.matches(message, condition);
+
+ // 断言:修复后的实现应该能处理简单值消息
+ // 但由于标识符匹配失败,结果为 false
+ assertFalse(result);
+ }
+
+ @Test
+ public void testMatches_valueFieldStructure_success() {
+ // 测试使用 value 字段的消息结构
+ String identifier = "temperature";
+ Double propertyValue = 25.5;
+
+ IotDeviceMessage message = new IotDeviceMessage();
+ message.setDeviceId(randomLongId());
+ message.setMethod("thing.event.post");
+
+ Map params = new HashMap<>();
+ params.put("identifier", identifier);
+ params.put("value", propertyValue);
+ message.setParams(params);
+
+ IotSceneRuleDO.TriggerCondition condition = createValidCondition(
+ identifier,
+ IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
+ "20"
+ );
+
+ // 调用
+ boolean result = matcher.matches(message, condition);
+
+ // 断言:修复后的实现应该能从 value 字段提取属性值
+ assertTrue(result);
+ }
+
+ @Test
+ public void testMatches_voltageGreaterThanOrEquals_success() {
+ // 准备参数:创建属性上报消息
+ String propertyIdentifier = "voltage";
+ Double propertyValue = 12.0;
+ IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
+ IotSceneRuleDO.TriggerCondition condition = createValidCondition(
+ propertyIdentifier,
IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(),
String.valueOf(propertyValue)
);
@@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_currentLessThanOrEquals_success() {
- // 准备参数
- String propertyName = "current";
+ // 准备参数:创建属性上报消息
+ String propertyIdentifier = "current";
Double propertyValue = 2.5;
Double compareValue = 3.0;
- Map properties = MapUtil.of(propertyName, propertyValue);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- propertyName,
+ propertyIdentifier,
IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(),
String.valueOf(compareValue)
);
@@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_stringProperty_success() {
- // 准备参数
- String propertyName = "mode";
+ // 准备参数:创建属性上报消息
+ String propertyIdentifier = "mode";
String propertyValue = "auto";
- Map properties = MapUtil.of(propertyName, propertyValue);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- propertyName,
+ propertyIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
propertyValue
);
@@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
@Test
public void testMatches_booleanProperty_success() {
- // 准备参数
- String propertyName = "enabled";
+ // 准备参数:创建属性上报消息
+ String propertyIdentifier = "enabled";
Boolean propertyValue = true;
- Map properties = MapUtil.of(propertyName, propertyValue);
- IotDeviceMessage message = createDeviceMessage(properties);
+ IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- propertyName,
+ propertyIdentifier,
IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
String.valueOf(propertyValue)
);
@@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
assertTrue(result);
}
- @Test
- public void testMatches_multipleProperties_success() {
- // 准备参数
- Map properties = MapUtil.builder(new HashMap())
- .put("temperature", 25.5)
- .put("humidity", 60)
- .put("status", "active")
- .put("enabled", true)
- .build();
- IotDeviceMessage message = createDeviceMessage(properties);
- String targetProperty = "humidity";
- Integer targetValue = 60;
- IotSceneRuleDO.TriggerCondition condition = createValidCondition(
- targetProperty,
- IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
- String.valueOf(targetValue)
- );
-
- // 调用
- boolean result = matcher.matches(message, condition);
-
- // 断言
- assertTrue(result);
- }
-
// ========== 辅助方法 ==========
/**
- * 创建设备消息
+ * 创建设备消息用于测试
+ *
+ * 支持的消息格式:
+ * 1. 直接属性值:params 直接是属性值(适用于简单消息)
+ * 2. 标识符+值:params 包含 identifier 和对应的属性值
+ * 3. properties 结构:params.properties[identifier] = value
+ * 4. data 结构:params.data[identifier] = value
+ * 5. value 字段:params.value = value
*/
- private IotDeviceMessage createDeviceMessage(Map properties) {
+ private IotDeviceMessage createPropertyPostMessage(String identifier, Object value) {
IotDeviceMessage message = new IotDeviceMessage();
message.setDeviceId(randomLongId());
- message.setParams(properties);
+ message.setMethod("thing.event.post"); // 使用事件上报方法
+
+ // 创建符合修复后逻辑的 params 结构
+ Map params = new HashMap<>();
+ params.put("identifier", identifier);
+ // 直接将属性值放在标识符对应的字段中
+ params.put(identifier, value);
+ message.setParams(params);
+
+ return message;
+ }
+
+ /**
+ * 创建使用 properties 结构的消息(模拟真实的属性上报消息)
+ */
+ private IotDeviceMessage createPropertyPostMessageWithProperties(String identifier, Object value) {
+ IotDeviceMessage message = new IotDeviceMessage();
+ message.setDeviceId(randomLongId());
+ message.setMethod("thing.property.post"); // 属性上报方法
+
+ Map properties = new HashMap<>();
+ properties.put(identifier, value);
+
+ Map params = new HashMap<>();
+ params.put("properties", properties);
+ message.setParams(params);
+
+ return message;
+ }
+
+ /**
+ * 创建简单值消息(params 直接是属性值)
+ */
+ private IotDeviceMessage createSimpleValueMessage(Object value) {
+ IotDeviceMessage message = new IotDeviceMessage();
+ message.setDeviceId(randomLongId());
+ message.setMethod("thing.property.post");
+ // 直接将属性值作为 params
+ message.setParams(value);
+
return message;
}
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java
similarity index 96%
rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java
rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java
index 25ea571528..da59077a6e 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java
@@ -1,27 +1,31 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
+import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
/**
- * {@link DeviceStateConditionMatcher} 的单元测试
+ * {@link IotDeviceStateConditionMatcher} 的单元测试
*
* @author HUIHUI
*/
-public class DeviceStateConditionMatcherTest extends BaseMockitoUnitTest {
+public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest {
- @InjectMocks
- private DeviceStateConditionMatcher matcher;
+ private IotDeviceStateConditionMatcher matcher;
+
+ @BeforeEach
+ public void setUp() {
+ matcher = new IotDeviceStateConditionMatcher();
+ }
@Test
public void testGetSupportedConditionType() {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java
similarity index 96%
rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java
rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java
index 1ed8f1c48f..9cf51421fe 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java
@@ -1,13 +1,13 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.hutool.core.map.MapUtil;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
+import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
@@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
import static org.junit.jupiter.api.Assertions.*;
/**
- * {@link DeviceEventPostTriggerMatcher} 的单元测试
+ * {@link IotDeviceEventPostTriggerMatcher} 的单元测试
*
* @author HUIHUI
*/
-public class DeviceEventPostTriggerMatcherTest extends BaseMockitoUnitTest {
+public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
- @InjectMocks
- private DeviceEventPostTriggerMatcher matcher;
+ private IotDeviceEventPostTriggerMatcher matcher;
+
+ @BeforeEach
+ public void setUp() {
+ matcher = new IotDeviceEventPostTriggerMatcher();
+ }
@Test
public void testGetSupportedTriggerType_success() {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java
similarity index 96%
rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java
rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java
index 2bed7fa631..fb155763aa 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java
@@ -1,14 +1,14 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.hutool.core.map.MapUtil;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
+import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
@@ -20,14 +20,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
import static org.junit.jupiter.api.Assertions.*;
/**
- * {@link DevicePropertyPostTriggerMatcher} 的单元测试
+ * {@link IotDevicePropertyPostTriggerMatcher} 的单元测试
*
* @author HUIHUI
*/
-public class DevicePropertyPostTriggerMatcherTest extends BaseMockitoUnitTest {
+public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
- @InjectMocks
- private DevicePropertyPostTriggerMatcher matcher;
+ private IotDevicePropertyPostTriggerMatcher matcher;
+
+ @BeforeEach
+ public void setUp() {
+ matcher = new IotDevicePropertyPostTriggerMatcher();
+ }
@Test
public void testGetSupportedTriggerType_success() {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java
similarity index 96%
rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java
rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java
index a9348456f4..a515f1268e 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java
@@ -1,13 +1,13 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
import cn.hutool.core.map.MapUtil;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
+import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
import java.util.HashMap;
import java.util.Map;
@@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
import static org.junit.jupiter.api.Assertions.*;
/**
- * {@link DeviceServiceInvokeTriggerMatcher} 的单元测试
+ * {@link IotDeviceServiceInvokeTriggerMatcher} 的单元测试
*
* @author HUIHUI
*/
-public class DeviceServiceInvokeTriggerMatcherTest extends BaseMockitoUnitTest {
+public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest {
- @InjectMocks
- private DeviceServiceInvokeTriggerMatcher matcher;
+ private IotDeviceServiceInvokeTriggerMatcher matcher;
+
+ @BeforeEach
+ public void setUp() {
+ matcher = new IotDeviceServiceInvokeTriggerMatcher();
+ }
@Test
public void testGetSupportedTriggerType_success() {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java
similarity index 94%
rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java
rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java
index b1e095ea3b..2e8b17a353 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java
@@ -1,27 +1,31 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
+import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static org.junit.jupiter.api.Assertions.*;
/**
- * {@link DeviceStateUpdateTriggerMatcher} 的单元测试
+ * {@link IotDeviceStateUpdateTriggerMatcher} 的单元测试
*
* @author HUIHUI
*/
-public class DeviceStateUpdateTriggerMatcherTest extends BaseMockitoUnitTest {
+public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatcherTest {
- @InjectMocks
- private DeviceStateUpdateTriggerMatcher matcher;
+ private IotDeviceStateUpdateTriggerMatcher matcher;
+
+ @BeforeEach
+ public void setUp() {
+ matcher = new IotDeviceStateUpdateTriggerMatcher();
+ }
@Test
public void testGetSupportedTriggerType_success() {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java
similarity index 95%
rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java
rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java
index 52ed5ec3de..df47e6c28f 100644
--- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java
@@ -1,25 +1,29 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
-import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
+import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.mockito.InjectMocks;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.junit.jupiter.api.Assertions.*;
/**
- * {@link TimerTriggerMatcher} 的单元测试
+ * {@link IotTimerTriggerMatcher} 的单元测试
*
* @author HUIHUI
*/
-public class TimerTriggerMatcherTest extends BaseMockitoUnitTest {
+public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest {
- @InjectMocks
- private TimerTriggerMatcher matcher;
+ private IotTimerTriggerMatcher matcher;
+
+ @BeforeEach
+ public void setUp() {
+ matcher = new IotTimerTriggerMatcher();
+ }
@Test
public void testGetSupportedTriggerType_success() {
diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java
new file mode 100644
index 0000000000..3e38e93616
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java
@@ -0,0 +1,126 @@
+package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
+
+import cn.hutool.core.collection.ListUtil;
+import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
+import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
+import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
+import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.quartz.SchedulerException;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link IotSceneRuleTimerHandler} 的单元测试类
+ *
+ * @author HUIHUI
+ */
+@ExtendWith(MockitoExtension.class)
+public class IotSceneRuleTimerHandlerTest {
+
+ @Mock
+ private IotSchedulerManager schedulerManager;
+
+ @InjectMocks
+ private IotSceneRuleTimerHandler timerHandler;
+
+ @BeforeEach
+ void setUp() {
+ // 重置 Mock 对象
+ reset(schedulerManager);
+ }
+
+ @Test
+ public void testRegisterTimerTriggers_success() throws SchedulerException {
+ // 准备参数
+ Long sceneRuleId = 1L;
+ IotSceneRuleDO sceneRule = new IotSceneRuleDO();
+ sceneRule.setId(sceneRuleId);
+ sceneRule.setStatus(0); // 0 表示启用
+ // 创建定时触发器
+ IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
+ timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
+ timerTrigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点
+ sceneRule.setTriggers(ListUtil.toList(timerTrigger));
+
+ // 调用
+ timerHandler.registerTimerTriggers(sceneRule);
+
+ // 验证
+ verify(schedulerManager, times(1)).addOrUpdateJob(
+ eq(IotSceneRuleJob.class),
+ eq("iot_scene_rule_timer_" + sceneRuleId),
+ eq("0 0 12 * * ?"),
+ eq(IotSceneRuleJob.buildJobDataMap(sceneRuleId))
+ );
+ }
+
+ @Test
+ public void testRegisterTimerTriggers_noTimerTrigger() throws SchedulerException {
+ // 准备参数 - 没有定时触发器
+ IotSceneRuleDO sceneRule = new IotSceneRuleDO();
+ sceneRule.setStatus(0); // 0 表示启用
+ // 创建非定时触发器
+ IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger();
+ deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType());
+ sceneRule.setTriggers(ListUtil.toList(deviceTrigger));
+
+ // 调用
+ timerHandler.registerTimerTriggers(sceneRule);
+
+ // 验证 - 不应该调用调度器
+ verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
+ }
+
+ @Test
+ public void testRegisterTimerTriggers_emptyCronExpression() throws SchedulerException {
+ // 准备参数 - CRON 表达式为空
+ Long sceneRuleId = 2L;
+ IotSceneRuleDO sceneRule = new IotSceneRuleDO();
+ sceneRule.setId(sceneRuleId);
+ sceneRule.setStatus(0); // 0 表示启用
+ // 创建定时触发器但没有 CRON 表达式
+ IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
+ timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
+ timerTrigger.setCronExpression(""); // 空的 CRON 表达式
+ sceneRule.setTriggers(ListUtil.toList(timerTrigger));
+
+ // 调用
+ timerHandler.registerTimerTriggers(sceneRule);
+
+ // 验证 - 不应该调用调度器
+ verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
+ }
+
+ @Test
+ public void testUnregisterTimerTriggers_success() throws SchedulerException {
+ // 准备参数
+ Long sceneRuleId = 3L;
+
+ // 调用
+ timerHandler.unregisterTimerTriggers(sceneRuleId);
+
+ // 验证
+ verify(schedulerManager, times(1)).deleteJob("iot_scene_rule_timer_" + sceneRuleId);
+ }
+
+ @Test
+ public void testPauseTimerTriggers_success() throws SchedulerException {
+ // 准备参数
+ Long sceneRuleId = 4L;
+
+ // 调用
+ timerHandler.pauseTimerTriggers(sceneRuleId);
+
+ // 验证
+ verify(schedulerManager, times(1)).pauseJob("iot_scene_rule_timer_" + sceneRuleId);
+ }
+
+}
diff --git a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
index 5b7778ea0c..65165425c8 100644
--- a/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
+++ b/yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
@@ -69,6 +69,83 @@ public class IotDeviceMessageUtils {
return null;
}
+ /**
+ * 从设备消息中提取指定标识符的属性值
+ * - 支持多种消息格式和属性值提取策略
+ * - 兼容现有的消息结构
+ * - 提供统一的属性值提取接口
+ *
+ * 支持的提取策略(按优先级顺序):
+ * 1. 直接值:如果 params 不是 Map,直接返回该值(适用于简单消息)
+ * 2. 标识符字段:从 params[identifier] 获取
+ * 3. properties 结构:从 params.properties[identifier] 获取(标准属性上报)
+ * 4. data 结构:从 params.data[identifier] 获取
+ * 5. value 字段:从 params.value 获取(单值消息)
+ * 6. 单值 Map:如果 Map 只包含 identifier 和一个值,返回该值
+ *
+ * @param message 设备消息
+ * @param identifier 属性标识符
+ * @return 属性值,如果未找到则返回 null
+ */
+ @SuppressWarnings("unchecked")
+ public static Object extractPropertyValue(IotDeviceMessage message, String identifier) {
+ Object params = message.getParams();
+ if (params == null) {
+ return null;
+ }
+
+ // 策略1:如果 params 不是 Map,直接返回该值(适用于简单的单属性消息)
+ if (!(params instanceof Map)) {
+ return params;
+ }
+
+ Map paramsMap = (Map) params;
+
+ // 策略2:直接通过标识符获取属性值
+ Object directValue = paramsMap.get(identifier);
+ if (directValue != null) {
+ return directValue;
+ }
+
+ // 策略3:从 properties 字段中获取(适用于标准属性上报消息)
+ Object properties = paramsMap.get("properties");
+ if (properties instanceof Map) {
+ Map propertiesMap = (Map) properties;
+ Object propertyValue = propertiesMap.get(identifier);
+ if (propertyValue != null) {
+ return propertyValue;
+ }
+ }
+
+ // 策略4:从 data 字段中获取(适用于某些消息格式)
+ Object data = paramsMap.get("data");
+ if (data instanceof Map) {
+ Map dataMap = (Map) data;
+ Object dataValue = dataMap.get(identifier);
+ if (dataValue != null) {
+ return dataValue;
+ }
+ }
+
+ // 策略5:从 value 字段中获取(适用于单值消息)
+ Object value = paramsMap.get("value");
+ if (value != null) {
+ return value;
+ }
+
+ // 策略6:如果 Map 只有两个字段且包含 identifier,返回另一个字段的值
+ if (paramsMap.size() == 2 && paramsMap.containsKey("identifier")) {
+ for (Map.Entry entry : paramsMap.entrySet()) {
+ if (!"identifier".equals(entry.getKey())) {
+ return entry.getValue();
+ }
+ }
+ }
+
+ // 未找到对应的属性值
+ return null;
+ }
+
// ========== Topic 相关 ==========
public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {
diff --git a/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java
new file mode 100644
index 0000000000..a6d669d170
--- /dev/null
+++ b/yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java
@@ -0,0 +1,141 @@
+package cn.iocoder.yudao.module.iot.core.util;
+
+import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * {@link IotDeviceMessageUtils} 的单元测试
+ *
+ * @author HUIHUI
+ */
+public class IotDeviceMessageUtilsTest {
+
+ @Test
+ public void testExtractPropertyValue_directValue() {
+ // 测试直接值(非 Map)
+ IotDeviceMessage message = new IotDeviceMessage();
+ message.setParams(25.5);
+
+ Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
+ assertEquals(25.5, result);
+ }
+
+ @Test
+ public void testExtractPropertyValue_directIdentifier() {
+ // 测试直接通过标识符获取
+ IotDeviceMessage message = new IotDeviceMessage();
+ Map params = new HashMap<>();
+ params.put("temperature", 25.5);
+ message.setParams(params);
+
+ Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
+ assertEquals(25.5, result);
+ }
+
+ @Test
+ public void testExtractPropertyValue_propertiesStructure() {
+ // 测试 properties 结构
+ IotDeviceMessage message = new IotDeviceMessage();
+ Map properties = new HashMap<>();
+ properties.put("temperature", 25.5);
+ properties.put("humidity", 60);
+
+ Map params = new HashMap<>();
+ params.put("properties", properties);
+ message.setParams(params);
+
+ Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
+ assertEquals(25.5, result);
+ }
+
+ @Test
+ public void testExtractPropertyValue_dataStructure() {
+ // 测试 data 结构
+ IotDeviceMessage message = new IotDeviceMessage();
+ Map data = new HashMap<>();
+ data.put("temperature", 25.5);
+
+ Map params = new HashMap<>();
+ params.put("data", data);
+ message.setParams(params);
+
+ Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
+ assertEquals(25.5, result);
+ }
+
+ @Test
+ public void testExtractPropertyValue_valueField() {
+ // 测试 value 字段
+ IotDeviceMessage message = new IotDeviceMessage();
+ Map params = new HashMap<>();
+ params.put("identifier", "temperature");
+ params.put("value", 25.5);
+ message.setParams(params);
+
+ Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
+ assertEquals(25.5, result);
+ }
+
+ @Test
+ public void testExtractPropertyValue_singleValueMap() {
+ // 测试单值 Map(包含 identifier 和一个值)
+ IotDeviceMessage message = new IotDeviceMessage();
+ Map params = new HashMap<>();
+ params.put("identifier", "temperature");
+ params.put("actualValue", 25.5);
+ message.setParams(params);
+
+ Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
+ assertEquals(25.5, result);
+ }
+
+ @Test
+ public void testExtractPropertyValue_notFound() {
+ // 测试未找到属性值
+ IotDeviceMessage message = new IotDeviceMessage();
+ Map params = new HashMap<>();
+ params.put("humidity", 60);
+ message.setParams(params);
+
+ Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
+ assertNull(result);
+ }
+
+ @Test
+ public void testExtractPropertyValue_nullParams() {
+ // 测试 params 为 null
+ IotDeviceMessage message = new IotDeviceMessage();
+ message.setParams(null);
+
+ Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
+ assertNull(result);
+ }
+
+ @Test
+ public void testExtractPropertyValue_priorityOrder() {
+ // 测试优先级顺序:直接标识符 > properties > data > value
+ IotDeviceMessage message = new IotDeviceMessage();
+
+ Map properties = new HashMap<>();
+ properties.put("temperature", 20.0);
+
+ Map data = new HashMap<>();
+ data.put("temperature", 30.0);
+
+ Map params = new HashMap<>();
+ params.put("temperature", 25.5); // 最高优先级
+ params.put("properties", properties);
+ params.put("data", data);
+ params.put("value", 40.0);
+ message.setParams(params);
+
+ Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
+ assertEquals(25.5, result); // 应该返回直接标识符的值
+ }
+}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
index c19053f144..4c0eb6e612 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
@@ -68,7 +68,13 @@ public class IotMqttUpstreamHandler {
log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
- // 2. 设置异常和关闭处理器
+ // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息)
+ endpoint.pingHandler(v -> {
+ log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId);
+ // Vert.x 会自动发送 PINGRESP 响应,无需手动处理
+ });
+
+ // 3. 设置异常和关闭处理器
endpoint.exceptionHandler(ex -> {
log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint));
cleanupConnection(endpoint);
@@ -77,7 +83,7 @@ public class IotMqttUpstreamHandler {
cleanupConnection(endpoint);
});
- // 3. 设置消息处理器
+ // 4. 设置消息处理器
endpoint.publishHandler(message -> {
try {
processMessage(clientId, message.topicName(), message.payload().getBytes());
@@ -100,7 +106,7 @@ public class IotMqttUpstreamHandler {
}
});
- // 4. 设置订阅处理器
+ // 5. 设置订阅处理器
endpoint.subscribeHandler(subscribe -> {
// 提取主题名称列表用于日志显示
List topicNames = subscribe.topicSubscriptions().stream()
@@ -115,22 +121,22 @@ public class IotMqttUpstreamHandler {
endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
});
- // 5. 设置取消订阅处理器
+ // 6. 设置取消订阅处理器
endpoint.unsubscribeHandler(unsubscribe -> {
log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
});
- // 6. 设置 QoS 2消息的 PUBREL 处理器
+ // 7. 设置 QoS 2消息的 PUBREL 处理器
endpoint.publishReleaseHandler(endpoint::publishComplete);
- // 7. 设置断开连接处理器
+ // 8. 设置断开连接处理器
endpoint.disconnectHandler(v -> {
log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId);
cleanupConnection(endpoint);
});
- // 8. 接受连接
+ // 9. 接受连接
endpoint.accept(false);
}
diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
index 322748d46d..b85e84c170 100644
--- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
+++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
@@ -103,7 +103,6 @@ yudao:
port: 1883
max-message-size: 8192
connect-timeout-seconds: 60
- keep-alive-timeout-seconds: 300
ssl-enabled: false
--- #################### 日志相关配置 ####################