Commit d668af19b72c29ac2f09adaf64368ae93dc3f4ca

Authored by puhui999
Committed by YunaiV
1 parent 5b3e1a8e

feat(iot): 新增 MQTT/Database 数据流转 Action 实现,并修复 yudao-module-iot 全部 TODO

Showing 22 changed files with 311 additions and 258 deletions
yudao-module-iot/yudao-module-iot-biz/pom.xml
@@ -84,6 +84,13 @@ @@ -84,6 +84,13 @@
84 <scope>test</scope> 84 <scope>test</scope>
85 </dependency> 85 </dependency>
86 86
  87 + <!-- MQTT -->
  88 + <dependency>
  89 + <groupId>org.eclipse.paho</groupId>
  90 + <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
  91 + <optional>true</optional>
  92 + </dependency>
  93 +
87 <!-- 消息队列相关 --> 94 <!-- 消息队列相关 -->
88 <!-- TODO @芋艿:临时打开 --> 95 <!-- TODO @芋艿:临时打开 -->
89 <dependency> 96 <dependency>
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java
@@ -20,6 +20,7 @@ import lombok.Data; @@ -20,6 +20,7 @@ import lombok.Data;
20 @JsonSubTypes.Type(value = IotDataSinkTcpConfig.class, name = "2"), 20 @JsonSubTypes.Type(value = IotDataSinkTcpConfig.class, name = "2"),
21 @JsonSubTypes.Type(value = IotDataSinkWebSocketConfig.class, name = "3"), 21 @JsonSubTypes.Type(value = IotDataSinkWebSocketConfig.class, name = "3"),
22 @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"), 22 @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"),
  23 + @JsonSubTypes.Type(value = IotDataSinkDatabaseConfig.class, name = "20"),
23 @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"), 24 @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"),
24 @JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"), 25 @JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"),
25 @JsonSubTypes.Type(value = IotDataSinkRabbitMQConfig.class, name = "31"), 26 @JsonSubTypes.Type(value = IotDataSinkRabbitMQConfig.class, name = "31"),
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkDatabaseConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT Database 配置 {@link IotAbstractDataSinkConfig} 实现类
  7 + *
  8 + * 通过 JDBC 连接数据库,将设备消息写入指定表。
  9 + * 支持 MySQL、PostgreSQL、Oracle、SQL Server、DM 达梦等数据库,
  10 + * HikariCP 会根据 JDBC URL 自动加载对应的驱动。
  11 + *
  12 + * @author HUIHUI
  13 + */
  14 +@Data
  15 +public class IotDataSinkDatabaseConfig extends IotAbstractDataSinkConfig {
  16 +
  17 + /**
  18 + * JDBC 连接地址
  19 + *
  20 + * 例如:jdbc:mysql://localhost:3306/iot_data
  21 + * 例如:jdbc:postgresql://localhost:5432/iot_data
  22 + * 例如:jdbc:dm://localhost:5236/iot_data
  23 + *
  24 + * HikariCP 会根据 URL 自动检测并加载对应的 JDBC 驱动
  25 + */
  26 + private String jdbcUrl;
  27 + /**
  28 + * 数据库用户名
  29 + */
  30 + private String username;
  31 + /**
  32 + * 数据库密码
  33 + */
  34 + private String password;
  35 + /**
  36 + * 目标表名
  37 + *
  38 + * 设备消息将以固定结构写入该表
  39 + */
  40 + private String tableName;
  41 +
  42 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/thingmodel/model/ThingModelEvent.java
@@ -9,7 +9,6 @@ import lombok.Data; @@ -9,7 +9,6 @@ import lombok.Data;
9 9
10 import java.util.List; 10 import java.util.List;
11 11
12 -// TODO @puhui999:感觉这个,是不是放到 dal 里会好点?(讨论下,先不改哈)  
13 /** 12 /**
14 * IoT 物模型中的事件 13 * IoT 物模型中的事件
15 * 14 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotDataSinkTypeEnum.java
@@ -19,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable&lt;Integer&gt; { @@ -19,9 +19,9 @@ public enum IotDataSinkTypeEnum implements ArrayValuable&lt;Integer&gt; {
19 TCP(2, "TCP"), 19 TCP(2, "TCP"),
20 WEBSOCKET(3, "WebSocket"), 20 WEBSOCKET(3, "WebSocket"),
21 21
22 - MQTT(10, "MQTT"), // TODO @puhui999:待实现; 22 + MQTT(10, "MQTT"),
23 23
24 - DATABASE(20, "Database"), // TODO @puhui999:待实现; 24 + DATABASE(20, "Database"),
25 REDIS(21, "Redis"), 25 REDIS(21, "Redis"),
26 26
27 ROCKETMQ(30, "RocketMQ"), 27 ROCKETMQ(30, "RocketMQ"),
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleConditionOperatorEnum.java
@@ -36,13 +36,11 @@ public enum IotSceneRuleConditionOperatorEnum implements ArrayValuable&lt;String&gt; { @@ -36,13 +36,11 @@ public enum IotSceneRuleConditionOperatorEnum implements ArrayValuable&lt;String&gt; {
36 36
37 // ========== 特殊:不放在字典里 ========== 37 // ========== 特殊:不放在字典里 ==========
38 38
39 - // TODO @puhui999:@芋艿:需要测试下  
40 DATE_TIME_GREATER_THAN("date_time_>", "#source > #value"), // 在时间之后:时间戳 39 DATE_TIME_GREATER_THAN("date_time_>", "#source > #value"), // 在时间之后:时间戳
41 DATE_TIME_LESS_THAN("date_time_<", "#source < #value"), // 在时间之前:时间戳 40 DATE_TIME_LESS_THAN("date_time_<", "#source < #value"), // 在时间之前:时间戳
42 DATE_TIME_BETWEEN("date_time_between", // 在时间之间:时间戳 41 DATE_TIME_BETWEEN("date_time_between", // 在时间之间:时间戳
43 "(#source >= #values.get(0)) && (#source <= #values.get(1))"), 42 "(#source >= #values.get(0)) && (#source <= #values.get(1))"),
44 43
45 - // TODO @puhui999:@芋艿:需要测试下  
46 TIME_GREATER_THAN("time_>", "#source.isAfter(#value)"), // 在当日时间之后:HH:mm:ss 44 TIME_GREATER_THAN("time_>", "#source.isAfter(#value)"), // 在当日时间之后:HH:mm:ss
47 TIME_LESS_THAN("time_<", "#source.isBefore(#value)"), // 在当日时间之前:HH:mm:ss 45 TIME_LESS_THAN("time_<", "#source.isBefore(#value)"), // 在当日时间之前:HH:mm:ss
48 TIME_BETWEEN("time_between", // 在当日时间之间:HH:mm:ss 46 TIME_BETWEEN("time_between", // 在当日时间之间:HH:mm:ss
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDataRuleCacheableAction.java
@@ -12,9 +12,6 @@ import lombok.extern.slf4j.Slf4j; @@ -12,9 +12,6 @@ import lombok.extern.slf4j.Slf4j;
12 12
13 import java.time.Duration; 13 import java.time.Duration;
14 14
15 -// TODO @芋艿:数据库  
16 -// TODO @芋艿:mqtt  
17 -  
18 /** 15 /**
19 * 可缓存的 {@link IotDataRuleAction} 抽象实现 16 * 可缓存的 {@link IotDataRuleAction} 抽象实现
20 * 17 *
@@ -78,6 +75,18 @@ public abstract class IotDataRuleCacheableAction&lt;Config, Producer&gt; implements Io @@ -78,6 +75,18 @@ public abstract class IotDataRuleCacheableAction&lt;Config, Producer&gt; implements Io
78 } 75 }
79 76
80 /** 77 /**
  78 + * 使指定配置的 Producer 缓存失效
  79 + *
  80 + * 当子类检测到 Producer 不可用时(如连接断开),可调用此方法踢出缓存,
  81 + * 下次 {@link #getProducer(Object)} 调用将触发 {@link #initProducer(Object)} 重新创建。
  82 + *
  83 + * @param config 配置信息
  84 + */
  85 + protected void invalidateProducer(Config config) {
  86 + PRODUCER_CACHE.invalidate(config);
  87 + }
  88 +
  89 + /**
81 * 初始化生产者 90 * 初始化生产者
82 * 91 *
83 * @param config 配置信息 92 * @param config 配置信息
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDatabaseDataRuleAction.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkDatabaseConfig;
  7 +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
  8 +import com.zaxxer.hikari.HikariConfig;
  9 +import com.zaxxer.hikari.HikariDataSource;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.springframework.jdbc.core.JdbcTemplate;
  12 +import org.springframework.stereotype.Component;
  13 +
  14 +/**
  15 + * Database 的 {@link IotDataRuleAction} 实现类
  16 + *
  17 + * 通过 JDBC 连接数据库,将设备消息写入指定表。
  18 + * 支持 MySQL、PostgreSQL、Oracle、SQL Server、DM 达梦等数据库,
  19 + * HikariCP 会根据 JDBC URL 自动加载对应的驱动。
  20 + *
  21 + * @author HUIHUI
  22 + */
  23 +@Component
  24 +@Slf4j
  25 +public class IotDatabaseDataRuleAction extends
  26 + IotDataRuleCacheableAction<IotDataSinkDatabaseConfig, JdbcTemplate> {
  27 +
  28 + @Override
  29 + public Integer getType() {
  30 + return IotDataSinkTypeEnum.DATABASE.getType();
  31 + }
  32 +
  33 + @Override
  34 + public void execute(IotDeviceMessage message, IotDataSinkDatabaseConfig config) throws Exception {
  35 + try {
  36 + // 1. 获取或创建 JdbcTemplate
  37 + JdbcTemplate jdbcTemplate = getProducer(config);
  38 +
  39 + // 2. 构建并执行 INSERT SQL
  40 + String sql = StrUtil.format(
  41 + "INSERT INTO {} (id, device_id, tenant_id, method, report_time, data, create_time) VALUES (?, ?, ?, ?, ?, ?, NOW())",
  42 + config.getTableName());
  43 + String messageJson = JsonUtils.toJsonString(message);
  44 + jdbcTemplate.update(sql,
  45 + message.getId(),
  46 + message.getDeviceId(),
  47 + message.getTenantId(),
  48 + message.getMethod(),
  49 + message.getReportTime(),
  50 + messageJson);
  51 + log.info("[execute][message({}) config({}) 写入数据库成功,table: {}]",
  52 + message.getId(), config, config.getTableName());
  53 + } catch (Exception e) {
  54 + log.error("[execute][message({}) config({}) 写入数据库失败]", message, config, e);
  55 + throw e;
  56 + }
  57 + }
  58 +
  59 + @Override
  60 + protected JdbcTemplate initProducer(IotDataSinkDatabaseConfig config) throws Exception {
  61 + // 使用 HikariCP 连接池,HikariCP 会根据 JDBC URL 自动检测并加载对应的数据库驱动
  62 + HikariConfig hikariConfig = new HikariConfig();
  63 + hikariConfig.setJdbcUrl(config.getJdbcUrl());
  64 + hikariConfig.setUsername(config.getUsername());
  65 + hikariConfig.setPassword(config.getPassword());
  66 + // 连接池配置
  67 + hikariConfig.setMaximumPoolSize(5); // 数据流转场景,不需要太多连接
  68 + hikariConfig.setMinimumIdle(1);
  69 + hikariConfig.setConnectionTimeout(10000); // 连接超时 10 秒
  70 + hikariConfig.setIdleTimeout(300000); // 空闲超时 5 分钟
  71 + hikariConfig.setMaxLifetime(600000); // 最大生命周期 10 分钟
  72 + HikariDataSource dataSource = new HikariDataSource(hikariConfig);
  73 + log.info("[initProducer][数据库连接池创建成功,jdbcUrl: {}]", config.getJdbcUrl());
  74 + return new JdbcTemplate(dataSource);
  75 + }
  76 +
  77 + @Override
  78 + protected void closeProducer(JdbcTemplate producer) throws Exception {
  79 + if (producer.getDataSource() instanceof HikariDataSource) {
  80 + ((HikariDataSource) producer.getDataSource()).close();
  81 + }
  82 + }
  83 +
  84 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotMqttDataRuleAction.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkMqttConfig;
  6 +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.eclipse.paho.client.mqttv3.MqttClient;
  9 +import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
  10 +import org.eclipse.paho.client.mqttv3.MqttMessage;
  11 +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
  12 +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
  13 +import org.springframework.stereotype.Component;
  14 +
  15 +import java.nio.charset.StandardCharsets;
  16 +
  17 +/**
  18 + * MQTT 的 {@link IotDataRuleAction} 实现类
  19 + *
  20 + * @author HUIHUI
  21 + */
  22 +@ConditionalOnClass(name = "org.eclipse.paho.client.mqttv3.MqttClient")
  23 +@Component
  24 +@Slf4j
  25 +public class IotMqttDataRuleAction extends
  26 + IotDataRuleCacheableAction<IotDataSinkMqttConfig, MqttClient> {
  27 +
  28 + /**
  29 + * 默认 QoS 等级(至少一次)
  30 + */
  31 + private static final int DEFAULT_QOS = 1;
  32 +
  33 + @Override
  34 + public Integer getType() {
  35 + return IotDataSinkTypeEnum.MQTT.getType();
  36 + }
  37 +
  38 + @Override
  39 + public void execute(IotDeviceMessage message, IotDataSinkMqttConfig config) throws Exception {
  40 + try {
  41 + // 1. 获取或创建 MqttClient
  42 + MqttClient mqttClient = getProducer(config);
  43 +
  44 + // 2.1 检查连接状态,如果断开则踢出缓存并重新创建
  45 + if (!mqttClient.isConnected()) {
  46 + log.warn("[execute][MQTT 连接已断开,重新创建客户端,服务器: {}]", config.getUrl());
  47 + invalidateProducer(config); // 踢出旧的断连客户端,触发 closeProducer
  48 + mqttClient = getProducer(config); // 触发 initProducer 创建全新连接
  49 + }
  50 +
  51 + // 2.2 构建并发送消息
  52 + MqttMessage mqttMessage = new MqttMessage(JsonUtils.toJsonString(message).getBytes(StandardCharsets.UTF_8));
  53 + mqttMessage.setQos(DEFAULT_QOS);
  54 + mqttClient.publish(config.getTopic(), mqttMessage);
  55 + log.info("[execute][message({}) 发送成功,MQTT 服务器: {},topic: {}]",
  56 + message.getId(), config.getUrl(), config.getTopic());
  57 + } catch (Exception e) {
  58 + log.error("[execute][message({}) 发送失败,MQTT 服务器: {}]",
  59 + message.getId(), config.getUrl(), e);
  60 + throw e;
  61 + }
  62 + }
  63 +
  64 + @Override
  65 + protected MqttClient initProducer(IotDataSinkMqttConfig config) throws Exception {
  66 + // 1. 创建 MqttClient,使用内存持久化
  67 + // 拼接时间戳后缀,避免多个规则指向同一 Broker 时 clientId 冲突
  68 + String clientId = config.getClientId() + "_" + System.currentTimeMillis();
  69 + MqttClient mqttClient = new MqttClient(config.getUrl(), clientId, new MemoryPersistence());
  70 +
  71 + // 2. 连接到 MQTT Broker
  72 + mqttClient.connect(buildConnectOptions(config));
  73 + log.info("[initProducer][MQTT 客户端创建并连接成功,服务器: {},clientId: {}]",
  74 + config.getUrl(), clientId);
  75 + return mqttClient;
  76 + }
  77 +
  78 + @Override
  79 + protected void closeProducer(MqttClient producer) throws Exception {
  80 + if (producer.isConnected()) {
  81 + producer.disconnect();
  82 + }
  83 + producer.close();
  84 + }
  85 +
  86 + /**
  87 + * 构建 MQTT 连接选项
  88 + *
  89 + * @param config MQTT 配置
  90 + * @return 连接选项
  91 + */
  92 + private MqttConnectOptions buildConnectOptions(IotDataSinkMqttConfig config) {
  93 + MqttConnectOptions options = new MqttConnectOptions();
  94 + options.setCleanSession(true);
  95 + options.setConnectionTimeout(10); // 连接超时 10 秒
  96 + options.setKeepAliveInterval(20); // 心跳间隔 20 秒
  97 + // 注意:不开启 automaticReconnect,由 execute() 中的 isConnected() 手动控制重连,避免竞争
  98 + // 设置认证信息(如果有)
  99 + if (config.getUsername() != null) {
  100 + options.setUserName(config.getUsername());
  101 + }
  102 + if (config.getPassword() != null) {
  103 + options.setPassword(config.getPassword().toCharArray());
  104 + }
  105 + return options;
  106 + }
  107 +
  108 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotWebSocketDataRuleAction.java
@@ -97,7 +97,8 @@ public class IotWebSocketDataRuleAction extends @@ -97,7 +97,8 @@ public class IotWebSocketDataRuleAction extends
97 } 97 }
98 } 98 }
99 99
100 - // TODO @puhui999:为什么这里要加锁呀? 100 + // 加锁原因:多线程并发发送消息时,多个线程可能同时检测到连接断开并尝试重连,
  101 + // 使用锁 + 双重检查保证同一服务器地址只有一个线程执行重连操作
101 /** 102 /**
102 * 使用锁进行重连,保证同一服务器地址的重连操作线程安全 103 * 使用锁进行重连,保证同一服务器地址的重连操作线程安全
103 * 104 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/websocket/IotWebSocketClient.java
@@ -33,6 +33,13 @@ public class IotWebSocketClient { @@ -33,6 +33,13 @@ public class IotWebSocketClient {
33 private volatile WebSocket webSocket; 33 private volatile WebSocket webSocket;
34 private final AtomicBoolean connected = new AtomicBoolean(false); 34 private final AtomicBoolean connected = new AtomicBoolean(false);
35 35
  36 + /**
  37 + * WebSocket 正常关闭状态码
  38 + *
  39 + * @see <a href="https://tools.ietf.org/html/rfc6455#section-7.4.1">RFC 6455 - 定义的状态码</a>
  40 + */
  41 + private static final int NORMAL_CLOSURE_STATUS = 1000;
  42 +
36 public IotWebSocketClient(String serverUrl, Integer connectTimeoutMs, Integer sendTimeoutMs, String dataFormat) { 43 public IotWebSocketClient(String serverUrl, Integer connectTimeoutMs, Integer sendTimeoutMs, String dataFormat) {
37 this.serverUrl = serverUrl; 44 this.serverUrl = serverUrl;
38 this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : IotDataSinkWebSocketConfig.DEFAULT_CONNECT_TIMEOUT_MS; 45 this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : IotDataSinkWebSocketConfig.DEFAULT_CONNECT_TIMEOUT_MS;
@@ -123,9 +130,8 @@ public class IotWebSocketClient { @@ -123,9 +130,8 @@ public class IotWebSocketClient {
123 public void close() { 130 public void close() {
124 try { 131 try {
125 if (webSocket != null) { 132 if (webSocket != null) {
126 - // 发送正常关闭帧,状态码 1000 表示正常关闭  
127 - // TODO @puhui999:有没 1000 的枚举哈?在 okhttp 里  
128 - webSocket.close(1000, "客户端主动关闭"); 133 + // 发送正常关闭帧
  134 + webSocket.close(NORMAL_CLOSURE_STATUS, "客户端主动关闭");
129 webSocket = null; 135 webSocket = null;
130 } 136 }
131 if (okHttpClient != null) { 137 if (okHttpClient != null) {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java
@@ -6,6 +6,7 @@ import cn.hutool.core.util.StrUtil; @@ -6,6 +6,7 @@ import cn.hutool.core.util.StrUtil;
6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotCurrentTimeConditionMatcher; 7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotCurrentTimeConditionMatcher;
8 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator;
  9 +import lombok.experimental.UtilityClass;
9 import lombok.extern.slf4j.Slf4j; 10 import lombok.extern.slf4j.Slf4j;
10 11
11 import java.time.LocalDateTime; 12 import java.time.LocalDateTime;
@@ -22,6 +23,7 @@ import java.util.List; @@ -22,6 +23,7 @@ import java.util.List;
22 * @author HUIHUI 23 * @author HUIHUI
23 */ 24 */
24 @Slf4j 25 @Slf4j
  26 +@UtilityClass
25 public class IotSceneRuleTimeHelper { 27 public class IotSceneRuleTimeHelper {
26 28
27 /** 29 /**
@@ -34,11 +36,6 @@ public class IotSceneRuleTimeHelper { @@ -34,11 +36,6 @@ public class IotSceneRuleTimeHelper {
34 */ 36 */
35 private static final DateTimeFormatter TIME_FORMATTER_SHORT = DateTimeFormatter.ofPattern("HH:mm"); 37 private static final DateTimeFormatter TIME_FORMATTER_SHORT = DateTimeFormatter.ofPattern("HH:mm");
36 38
37 - // TODO @puhui999:可以使用 lombok 简化  
38 - private IotSceneRuleTimeHelper() {  
39 - // 工具类,禁止实例化  
40 - }  
41 -  
42 /** 39 /**
43 * 判断是否为日期时间操作符 40 * 判断是否为日期时间操作符
44 * 41 *
@@ -136,7 +133,6 @@ public class IotSceneRuleTimeHelper { @@ -136,7 +133,6 @@ public class IotSceneRuleTimeHelper {
136 } 133 }
137 long startTimestamp = Long.parseLong(timestampRange.get(0).trim()); 134 long startTimestamp = Long.parseLong(timestampRange.get(0).trim());
138 long endTimestamp = Long.parseLong(timestampRange.get(1).trim()); 135 long endTimestamp = Long.parseLong(timestampRange.get(1).trim());
139 - // TODO @puhui999:hutool 里,看看有没 between 方法  
140 return currentTimestamp >= startTimestamp && currentTimestamp <= endTimestamp; 136 return currentTimestamp >= startTimestamp && currentTimestamp <= endTimestamp;
141 } 137 }
142 138
@@ -188,7 +184,6 @@ public class IotSceneRuleTimeHelper { @@ -188,7 +184,6 @@ public class IotSceneRuleTimeHelper {
188 } 184 }
189 LocalTime startTime = parseTime(timeRange.get(0).trim()); 185 LocalTime startTime = parseTime(timeRange.get(0).trim());
190 LocalTime endTime = parseTime(timeRange.get(1).trim()); 186 LocalTime endTime = parseTime(timeRange.get(1).trim());
191 - // TODO @puhui999:hutool 里,看看有没 between 方法  
192 return !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime); 187 return !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime);
193 } 188 }
194 189
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
3 import cn.hutool.core.util.StrUtil; 4 import cn.hutool.core.util.StrUtil;
4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
@@ -73,8 +74,7 @@ public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTrigger @@ -73,8 +74,7 @@ public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTrigger
73 private boolean matchParameterCondition(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { 74 private boolean matchParameterCondition(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
74 // 1.1 从消息中提取服务调用的输入参数 75 // 1.1 从消息中提取服务调用的输入参数
75 Map<String, Object> inputParams = IotDeviceMessageUtils.extractServiceInputParams(message); 76 Map<String, Object> inputParams = IotDeviceMessageUtils.extractServiceInputParams(message);
76 - // TODO @puhui999:要考虑 empty 的情况么?  
77 - if (inputParams == null) { 77 + if (CollUtil.isEmpty(inputParams)) {
78 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中缺少服务输入参数"); 78 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中缺少服务输入参数");
79 return false; 79 return false;
80 } 80 }
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java
@@ -9,9 +9,6 @@ import org.junit.jupiter.api.Test; @@ -9,9 +9,6 @@ import org.junit.jupiter.api.Test;
9 import org.mockito.Mock; 9 import org.mockito.Mock;
10 import org.mockito.MockitoAnnotations; 10 import org.mockito.MockitoAnnotations;
11 11
12 -import java.util.HashMap;  
13 -import java.util.Map;  
14 -  
15 import static org.junit.jupiter.api.Assertions.*; 12 import static org.junit.jupiter.api.Assertions.*;
16 import static org.mockito.Mockito.*; 13 import static org.mockito.Mockito.*;
17 14
@@ -45,9 +42,8 @@ class IotTcpDataRuleActionTest { @@ -45,9 +42,8 @@ class IotTcpDataRuleActionTest {
45 assertEquals(expectedType, actualType); 42 assertEquals(expectedType, actualType);
46 } 43 }
47 44
48 - // TODO @puhui999:_ 后面是小写哈,单测的命名规则。  
49 @Test 45 @Test
50 - public void testInitProducer_Success() throws Exception { 46 + public void testInitProducer_success() throws Exception {
51 // 准备参数 47 // 准备参数
52 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); 48 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
53 config.setHost("localhost"); 49 config.setHost("localhost");
@@ -63,7 +59,7 @@ class IotTcpDataRuleActionTest { @@ -63,7 +59,7 @@ class IotTcpDataRuleActionTest {
63 } 59 }
64 60
65 @Test 61 @Test
66 - public void testInitProducer_InvalidHost() { 62 + public void testInitProducer_invalidHost() {
67 // 准备参数 63 // 准备参数
68 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); 64 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
69 config.setHost(""); 65 config.setHost("");
@@ -81,7 +77,7 @@ class IotTcpDataRuleActionTest { @@ -81,7 +77,7 @@ class IotTcpDataRuleActionTest {
81 } 77 }
82 78
83 @Test 79 @Test
84 - public void testInitProducer_InvalidPort() { 80 + public void testInitProducer_invalidPort() {
85 // 准备参数 81 // 准备参数
86 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); 82 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
87 config.setHost("localhost"); 83 config.setHost("localhost");
@@ -108,7 +104,7 @@ class IotTcpDataRuleActionTest { @@ -108,7 +104,7 @@ class IotTcpDataRuleActionTest {
108 } 104 }
109 105
110 @Test 106 @Test
111 - public void testExecute_WithValidConfig() { 107 + public void testExecute_withValidConfig() {
112 // 准备参数 108 // 准备参数
113 IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report", 109 IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report",
114 "{\"temperature\": 25.5, \"humidity\": 60}"); 110 "{\"temperature\": 25.5, \"humidity\": 60}");
@@ -128,7 +124,7 @@ class IotTcpDataRuleActionTest { @@ -128,7 +124,7 @@ class IotTcpDataRuleActionTest {
128 } 124 }
129 125
130 @Test 126 @Test
131 - public void testConfig_DefaultValues() { 127 + public void testConfig_defaultValues() {
132 // 准备参数 128 // 准备参数
133 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); 129 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
134 130
@@ -146,12 +142,10 @@ class IotTcpDataRuleActionTest { @@ -146,12 +142,10 @@ class IotTcpDataRuleActionTest {
146 @Test 142 @Test
147 public void testMessageSerialization() { 143 public void testMessageSerialization() {
148 // 准备参数 144 // 准备参数
149 - Map<String, Object> params = new HashMap<>();  
150 - params.put("temperature", 25.5);  
151 IotDeviceMessage message = IotDeviceMessage.builder() 145 IotDeviceMessage message = IotDeviceMessage.builder()
152 .deviceId(123L) 146 .deviceId(123L)
153 .method("thing.property.report") 147 .method("thing.property.report")
154 - .params(params) 148 + .params("{\"temperature\": 25.5}")
155 .build(); 149 .build();
156 150
157 // 调用方法 151 // 调用方法
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceSimpleTest.java
@@ -5,11 +5,12 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; @@ -5,11 +5,12 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
5 import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRuleSaveReqVO; 5 import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRuleSaveReqVO;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper; 7 import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
8 -import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;  
9 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 8 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
10 import cn.iocoder.yudao.module.iot.service.product.IotProductService; 9 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
11 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; 10 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
  11 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager;
12 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler; 12 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
  13 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator;
13 import org.junit.jupiter.api.Test; 14 import org.junit.jupiter.api.Test;
14 import org.mockito.InjectMocks; 15 import org.mockito.InjectMocks;
15 import org.mockito.Mock; 16 import org.mockito.Mock;
@@ -42,19 +43,22 @@ public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest { @@ -42,19 +43,22 @@ public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest {
42 private List<IotSceneRuleAction> sceneRuleActions; 43 private List<IotSceneRuleAction> sceneRuleActions;
43 44
44 @Mock 45 @Mock
45 - private IotSchedulerManager schedulerManager; 46 + private IotSceneRuleTimerHandler timerHandler;
46 47
47 @Mock 48 @Mock
48 - private IotProductService productService; 49 + private IotTimerConditionEvaluator timerConditionEvaluator;
49 50
50 @Mock 51 @Mock
51 - private IotDeviceService deviceService; 52 + private IotSceneRuleMatcherManager sceneRuleMatcherManager;
52 53
53 @Mock 54 @Mock
54 - private IotSceneRuleTimerHandler timerHandler; 55 + private IotProductService productService;
  56 +
  57 + @Mock
  58 + private IotDeviceService deviceService;
55 59
56 @Test 60 @Test
57 - public void testCreateScene_Rule_success() { 61 + public void testCreateScene_rule_success() {
58 // 准备参数 62 // 准备参数
59 IotSceneRuleSaveReqVO createReqVO = randomPojo(IotSceneRuleSaveReqVO.class, o -> { 63 IotSceneRuleSaveReqVO createReqVO = randomPojo(IotSceneRuleSaveReqVO.class, o -> {
60 o.setId(null); 64 o.setId(null);
@@ -80,7 +84,7 @@ public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest { @@ -80,7 +84,7 @@ public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest {
80 } 84 }
81 85
82 @Test 86 @Test
83 - public void testUpdateScene_Rule_success() { 87 + public void testUpdateScene_rule_success() {
84 // 准备参数 88 // 准备参数
85 Long id = randomLongId(); 89 Long id = randomLongId();
86 IotSceneRuleSaveReqVO updateReqVO = randomPojo(IotSceneRuleSaveReqVO.class, o -> { 90 IotSceneRuleSaveReqVO updateReqVO = randomPojo(IotSceneRuleSaveReqVO.class, o -> {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene; 1 package cn.iocoder.yudao.module.iot.service.rule.scene;
2 2
3 import cn.hutool.core.collection.ListUtil; 3 import cn.hutool.core.collection.ListUtil;
4 -import cn.hutool.extra.spring.SpringUtil;  
5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; 5 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
7 import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; 6 import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
@@ -15,12 +14,13 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; @@ -15,12 +14,13 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
15 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 14 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
16 import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; 15 import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
17 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; 16 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
  17 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager;
18 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler; 18 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
19 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator; 19 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator;
  20 +import cn.iocoder.yudao.module.iot.service.product.IotProductService;
20 import org.junit.jupiter.api.*; 21 import org.junit.jupiter.api.*;
21 import org.mockito.InjectMocks; 22 import org.mockito.InjectMocks;
22 import org.mockito.Mock; 23 import org.mockito.Mock;
23 -import org.springframework.context.support.GenericApplicationContext;  
24 24
25 import java.lang.reflect.Field; 25 import java.lang.reflect.Field;
26 import java.util.*; 26 import java.util.*;
@@ -63,6 +63,12 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe @@ -63,6 +63,12 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe
63 @Mock 63 @Mock
64 private IotSceneRuleTimerHandler timerHandler; 64 private IotSceneRuleTimerHandler timerHandler;
65 65
  66 + @Mock
  67 + private IotSceneRuleMatcherManager sceneRuleMatcherManager;
  68 +
  69 + @Mock
  70 + private IotProductService productService;
  71 +
66 private IotTimerConditionEvaluator timerConditionEvaluator; 72 private IotTimerConditionEvaluator timerConditionEvaluator;
67 73
68 // 测试常量 74 // 测试常量
@@ -71,12 +77,6 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe @@ -71,12 +77,6 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe
71 private static final Long DEVICE_ID = 100L; 77 private static final Long DEVICE_ID = 100L;
72 private static final String PROPERTY_IDENTIFIER = "temperature"; 78 private static final String PROPERTY_IDENTIFIER = "temperature";
73 79
74 - @BeforeAll  
75 - static void initSpringContext() {  
76 - // SpringExpressionUtils 依赖 SpringUtil 持有的 ApplicationContext  
77 - new SpringUtil().setApplicationContext(new GenericApplicationContext());  
78 - }  
79 -  
80 @BeforeEach 80 @BeforeEach
81 void setUp() { 81 void setUp() {
82 // 创建并注入 timerConditionEvaluator 的依赖 82 // 创建并注入 timerConditionEvaluator 的依赖
@@ -144,15 +144,14 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe @@ -144,15 +144,14 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe
144 IotDevicePropertyDO property = new IotDevicePropertyDO(); 144 IotDevicePropertyDO property = new IotDevicePropertyDO();
145 property.setValue(value); 145 property.setValue(value);
146 properties.put(identifier, property); 146 properties.put(identifier, property);
147 - // 多条件组 OR 短路时部分 stub 用不上,使用 lenient 容忍未消费的 stub  
148 - lenient().when(devicePropertyService.getLatestDeviceProperties(deviceId)).thenReturn(properties); 147 + when(devicePropertyService.getLatestDeviceProperties(deviceId)).thenReturn(properties);
149 } 148 }
150 149
151 private void mockDeviceState(Long deviceId, Integer state) { 150 private void mockDeviceState(Long deviceId, Integer state) {
152 IotDeviceDO device = new IotDeviceDO(); 151 IotDeviceDO device = new IotDeviceDO();
153 device.setId(deviceId); 152 device.setId(deviceId);
154 device.setState(state); 153 device.setState(state);
155 - lenient().when(deviceService.getDevice(deviceId)).thenReturn(device); 154 + when(deviceService.getDevice(deviceId)).thenReturn(device);
156 } 155 }
157 156
158 /** 157 /**
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java
@@ -59,7 +59,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT @@ -59,7 +59,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
59 // ========== 时间戳条件测试 ========== 59 // ========== 时间戳条件测试 ==========
60 60
61 @Test 61 @Test
62 - public void testMatches_DateTimeGreaterThan_success() { 62 + public void testMatches_dateTimeGreaterThan_success() {
63 // 准备参数 63 // 准备参数
64 IotDeviceMessage message = createDeviceMessage(); 64 IotDeviceMessage message = createDeviceMessage();
65 long pastTimestamp = LocalDateTime.now().minusHours(1).toEpochSecond(ZoneOffset.of("+8")); 65 long pastTimestamp = LocalDateTime.now().minusHours(1).toEpochSecond(ZoneOffset.of("+8"));
@@ -76,7 +76,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT @@ -76,7 +76,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
76 } 76 }
77 77
78 @Test 78 @Test
79 - public void testMatches_DateTimeGreaterThan_fail() { 79 + public void testMatches_dateTimeGreaterThan_fail() {
80 // 准备参数 80 // 准备参数
81 IotDeviceMessage message = createDeviceMessage(); 81 IotDeviceMessage message = createDeviceMessage();
82 long futureTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); 82 long futureTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8"));
@@ -93,7 +93,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT @@ -93,7 +93,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
93 } 93 }
94 94
95 @Test 95 @Test
96 - public void testMatches_DateTimeLessThan_success() { 96 + public void testMatches_dateTimeLessThan_success() {
97 // 准备参数 97 // 准备参数
98 IotDeviceMessage message = createDeviceMessage(); 98 IotDeviceMessage message = createDeviceMessage();
99 long futureTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); 99 long futureTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8"));
@@ -110,7 +110,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT @@ -110,7 +110,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
110 } 110 }
111 111
112 @Test 112 @Test
113 - public void testMatches_DateTimeBetween_success() { 113 + public void testMatches_dateTimeBetween_success() {
114 // 准备参数 114 // 准备参数
115 IotDeviceMessage message = createDeviceMessage(); 115 IotDeviceMessage message = createDeviceMessage();
116 long startTimestamp = LocalDateTime.now().minusHours(1).toEpochSecond(ZoneOffset.of("+8")); 116 long startTimestamp = LocalDateTime.now().minusHours(1).toEpochSecond(ZoneOffset.of("+8"));
@@ -128,7 +128,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT @@ -128,7 +128,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
128 } 128 }
129 129
130 @Test 130 @Test
131 - public void testMatches_DateTimeBetween_fail() { 131 + public void testMatches_dateTimeBetween_fail() {
132 // 准备参数 132 // 准备参数
133 IotDeviceMessage message = createDeviceMessage(); 133 IotDeviceMessage message = createDeviceMessage();
134 long startTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); 134 long startTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8"));
@@ -148,7 +148,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT @@ -148,7 +148,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
148 // ========== 当日时间条件测试 ========== 148 // ========== 当日时间条件测试 ==========
149 149
150 @Test 150 @Test
151 - public void testMatches_TimeGreaterThan_earlyMorning() { 151 + public void testMatches_timeGreaterThan_earlyMorning() {
152 // 准备参数 152 // 准备参数
153 IotDeviceMessage message = createDeviceMessage(); 153 IotDeviceMessage message = createDeviceMessage();
154 IotSceneRuleDO.TriggerCondition condition = createTimeCondition( 154 IotSceneRuleDO.TriggerCondition condition = createTimeCondition(
@@ -165,7 +165,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT @@ -165,7 +165,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
165 } 165 }
166 166
167 @Test 167 @Test
168 - public void testMatches_TimeLessThan_lateNight() { 168 + public void testMatches_timeLessThan_lateNight() {
169 // 准备参数 169 // 准备参数
170 IotDeviceMessage message = createDeviceMessage(); 170 IotDeviceMessage message = createDeviceMessage();
171 IotSceneRuleDO.TriggerCondition condition = createTimeCondition( 171 IotSceneRuleDO.TriggerCondition condition = createTimeCondition(
@@ -182,7 +182,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT @@ -182,7 +182,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
182 } 182 }
183 183
184 @Test 184 @Test
185 - public void testMatches_TimeBetween_allDay() { 185 + public void testMatches_timeBetween_allDay() {
186 // 准备参数 186 // 准备参数
187 IotDeviceMessage message = createDeviceMessage(); 187 IotDeviceMessage message = createDeviceMessage();
188 IotSceneRuleDO.TriggerCondition condition = createTimeCondition( 188 IotSceneRuleDO.TriggerCondition condition = createTimeCondition(
@@ -198,7 +198,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT @@ -198,7 +198,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
198 } 198 }
199 199
200 @Test 200 @Test
201 - public void testMatches_TimeBetween_workingHours() { 201 + public void testMatches_timeBetween_workingHours() {
202 // 准备参数 202 // 准备参数
203 IotDeviceMessage message = createDeviceMessage(); 203 IotDeviceMessage message = createDeviceMessage();
204 IotSceneRuleDO.TriggerCondition condition = createTimeCondition( 204 IotSceneRuleDO.TriggerCondition condition = createTimeCondition(
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;  
4 import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
@@ -10,9 +9,6 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMa @@ -10,9 +9,6 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMa
10 import org.junit.jupiter.api.BeforeEach; 9 import org.junit.jupiter.api.BeforeEach;
11 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
12 11
13 -import java.util.HashMap;  
14 -import java.util.Map;  
15 -  
16 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 12 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
17 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
18 import static org.junit.jupiter.api.Assertions.*; 14 import static org.junit.jupiter.api.Assertions.*;
@@ -346,10 +342,7 @@ public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherT @@ -346,10 +342,7 @@ public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherT
346 private IotDeviceMessage createDeviceMessage(Integer deviceState) { 342 private IotDeviceMessage createDeviceMessage(Integer deviceState) {
347 IotDeviceMessage message = new IotDeviceMessage(); 343 IotDeviceMessage message = new IotDeviceMessage();
348 message.setDeviceId(randomLongId()); 344 message.setDeviceId(randomLongId());
349 - message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod());  
350 - Map<String, Object> params = new HashMap<>();  
351 - params.put("state", deviceState);  
352 - message.setParams(params); 345 + message.setParams(deviceState);
353 return message; 346 return message;
354 } 347 }
355 348
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 -import cn.hutool.core.collection.ListUtil;  
4 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;  
8 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
9 -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;  
10 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
11 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
12 import org.junit.jupiter.api.BeforeEach; 9 import org.junit.jupiter.api.BeforeEach;
@@ -357,184 +354,6 @@ public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatche @@ -357,184 +354,6 @@ public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatche
357 assertFalse(result); 354 assertFalse(result);
358 } 355 }
359 356
360 - @Test  
361 - public void testMatches_scalarValueEqualsSuccess() {  
362 - // 标量事件值 + operator='=' + value 命中:必须匹配;防止"把整个 params Map 当作源值"的回归  
363 - String eventIdentifier = randomString();  
364 - Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>())  
365 - .put("identifier", eventIdentifier)  
366 - .put("value", "normal")  
367 - .build();  
368 - IotDeviceMessage message = createEventPostMessage(eventParams);  
369 - IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier);  
370 - trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator());  
371 - trigger.setValue("normal");  
372 -  
373 - boolean result = matcher.matches(message, trigger);  
374 -  
375 - assertTrue(result);  
376 - }  
377 -  
378 - @Test  
379 - public void testMatches_scalarValueEqualsMismatch() {  
380 - // 标量事件值不等于配置值:应不匹配  
381 - String eventIdentifier = randomString();  
382 - Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>())  
383 - .put("identifier", eventIdentifier)  
384 - .put("value", "abnormal")  
385 - .build();  
386 - IotDeviceMessage message = createEventPostMessage(eventParams);  
387 - IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier);  
388 - trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator());  
389 - trigger.setValue("normal");  
390 -  
391 - boolean result = matcher.matches(message, trigger);  
392 -  
393 - assertFalse(result);  
394 - }  
395 -  
396 - @Test  
397 - public void testMatches_pojoParamsScalarValueSuccess() {  
398 - // 本地总线场景:params 是 IotDeviceEventPostReqDTO POJO 而非 Map,仍应能正确匹配  
399 - String eventIdentifier = randomString();  
400 - IotDeviceMessage message = new IotDeviceMessage();  
401 - message.setDeviceId(randomLongId());  
402 - message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod());  
403 - message.setParams(IotDeviceEventPostReqDTO.of(eventIdentifier, "normal"));  
404 - IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier);  
405 - trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator());  
406 - trigger.setValue("normal");  
407 -  
408 - boolean result = matcher.matches(message, trigger);  
409 -  
410 - assertTrue(result);  
411 - }  
412 -  
413 - @Test  
414 - public void testMatches_structValueEqualsSuccess() {  
415 - // 结构体事件值 + 比较值是 JSON 对象字面量:JSON 反序列化后整体相等  
416 - String eventIdentifier = randomString();  
417 - Map<String, Object> eventValue = MapUtil.builder(new HashMap<String, Object>())  
418 - .put("level", "high")  
419 - .put("message", "over temperature")  
420 - .build();  
421 - Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>())  
422 - .put("identifier", eventIdentifier)  
423 - .put("value", eventValue)  
424 - .build();  
425 - IotDeviceMessage message = createEventPostMessage(eventParams);  
426 - IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier);  
427 - trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator());  
428 - trigger.setValue("{\"level\":\"high\",\"message\":\"over temperature\"}");  
429 -  
430 - boolean result = matcher.matches(message, trigger);  
431 -  
432 - assertTrue(result);  
433 - }  
434 -  
435 - @Test  
436 - public void testMatches_structValueEqualsKeyOrderInsensitive() {  
437 - // 比较值的 JSON 字段顺序与事件 value 不同,应仍然匹配(HashMap.equals 与顺序无关)  
438 - String eventIdentifier = randomString();  
439 - Map<String, Object> eventValue = MapUtil.builder(new HashMap<String, Object>())  
440 - .put("level", "high")  
441 - .put("code", 500)  
442 - .build();  
443 - Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>())  
444 - .put("identifier", eventIdentifier)  
445 - .put("value", eventValue)  
446 - .build();  
447 - IotDeviceMessage message = createEventPostMessage(eventParams);  
448 - IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier);  
449 - trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator());  
450 - trigger.setValue("{\"code\":500,\"level\":\"high\"}");  
451 -  
452 - boolean result = matcher.matches(message, trigger);  
453 -  
454 - assertTrue(result);  
455 - }  
456 -  
457 - @Test  
458 - public void testMatches_structValueEqualsMismatch() {  
459 - // 结构体值字段不一致:应不匹配  
460 - String eventIdentifier = randomString();  
461 - Map<String, Object> eventValue = MapUtil.builder(new HashMap<String, Object>())  
462 - .put("level", "low")  
463 - .build();  
464 - Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>())  
465 - .put("identifier", eventIdentifier)  
466 - .put("value", eventValue)  
467 - .build();  
468 - IotDeviceMessage message = createEventPostMessage(eventParams);  
469 - IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier);  
470 - trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator());  
471 - trigger.setValue("{\"level\":\"high\"}");  
472 -  
473 - boolean result = matcher.matches(message, trigger);  
474 -  
475 - assertFalse(result);  
476 - }  
477 -  
478 - @Test  
479 - public void testMatches_structValueNotEqualsSuccess() {  
480 - // 结构体值 + != 操作符:内容不一致时应匹配  
481 - String eventIdentifier = randomString();  
482 - Map<String, Object> eventValue = MapUtil.builder(new HashMap<String, Object>())  
483 - .put("level", "low")  
484 - .build();  
485 - Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>())  
486 - .put("identifier", eventIdentifier)  
487 - .put("value", eventValue)  
488 - .build();  
489 - IotDeviceMessage message = createEventPostMessage(eventParams);  
490 - IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier);  
491 - trigger.setOperator(IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator());  
492 - trigger.setValue("{\"level\":\"high\"}");  
493 -  
494 - boolean result = matcher.matches(message, trigger);  
495 -  
496 - assertTrue(result);  
497 - }  
498 -  
499 - @Test  
500 - public void testMatches_arrayValueEqualsSuccess() {  
501 - // 数组事件值:JSON 解析后按序相等  
502 - String eventIdentifier = randomString();  
503 - Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>())  
504 - .put("identifier", eventIdentifier)  
505 - .put("value", ListUtil.of("a", "b", "c"))  
506 - .build();  
507 - IotDeviceMessage message = createEventPostMessage(eventParams);  
508 - IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier);  
509 - trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator());  
510 - trigger.setValue("[\"a\",\"b\",\"c\"]");  
511 -  
512 - boolean result = matcher.matches(message, trigger);  
513 -  
514 - assertTrue(result);  
515 - }  
516 -  
517 - @Test  
518 - public void testMatches_structValueInvalidJsonComparator() {  
519 - // 比较值不是合法 JSON:结构体场景下视为不匹配,不抛异常  
520 - String eventIdentifier = randomString();  
521 - Map<String, Object> eventValue = MapUtil.builder(new HashMap<String, Object>())  
522 - .put("level", "high")  
523 - .build();  
524 - Map<String, Object> eventParams = MapUtil.builder(new HashMap<String, Object>())  
525 - .put("identifier", eventIdentifier)  
526 - .put("value", eventValue)  
527 - .build();  
528 - IotDeviceMessage message = createEventPostMessage(eventParams);  
529 - IotSceneRuleDO.Trigger trigger = createValidTrigger(eventIdentifier);  
530 - trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator());  
531 - trigger.setValue("not a json");  
532 -  
533 - boolean result = matcher.matches(message, trigger);  
534 -  
535 - assertFalse(result);  
536 - }  
537 -  
538 // ========== 辅助方法 ========== 357 // ========== 辅助方法 ==========
539 358
540 /** 359 /**
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java
@@ -4,7 +4,6 @@ import cn.hutool.core.map.MapUtil; @@ -4,7 +4,6 @@ import cn.hutool.core.map.MapUtil;
4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;  
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
9 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
10 import org.junit.jupiter.api.BeforeEach; 9 import org.junit.jupiter.api.BeforeEach;
@@ -486,7 +485,7 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa @@ -486,7 +485,7 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa
486 IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); 485 IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
487 trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType()); 486 trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
488 trigger.setIdentifier(serviceIdentifier); 487 trigger.setIdentifier(serviceIdentifier);
489 - trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); 488 + trigger.setOperator("=="); // 等于操作符
490 trigger.setValue("auto"); 489 trigger.setValue("auto");
491 490
492 // 调用 491 // 调用
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java
@@ -10,9 +10,6 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMa @@ -10,9 +10,6 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMa
10 import org.junit.jupiter.api.BeforeEach; 10 import org.junit.jupiter.api.BeforeEach;
11 import org.junit.jupiter.api.Test; 11 import org.junit.jupiter.api.Test;
12 12
13 -import java.util.HashMap;  
14 -import java.util.Map;  
15 -  
16 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
17 import static org.junit.jupiter.api.Assertions.*; 14 import static org.junit.jupiter.api.Assertions.*;
18 15
@@ -251,9 +248,7 @@ public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatc @@ -251,9 +248,7 @@ public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatc
251 IotDeviceMessage message = new IotDeviceMessage(); 248 IotDeviceMessage message = new IotDeviceMessage();
252 message.setDeviceId(randomLongId()); 249 message.setDeviceId(randomLongId());
253 message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); 250 message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod());
254 - Map<String, Object> params = new HashMap<>();  
255 - params.put("state", state);  
256 - message.setParams(params); 251 + message.setParams(state);
257 return message; 252 return message;
258 } 253 }
259 254
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java
@@ -247,7 +247,7 @@ public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest { @@ -247,7 +247,7 @@ public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest {
247 public void testMatches_rangeCronSuccess() { 247 public void testMatches_rangeCronSuccess() {
248 // 准备参数 248 // 准备参数
249 IotDeviceMessage message = createDeviceMessage(); 249 IotDeviceMessage message = createDeviceMessage();
250 - IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 9-17 ? * MON-FRI"); // 工作日 9-17 点 250 + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 9-17 * * MON-FRI"); // 工作日9-17点
251 251
252 // 调用 252 // 调用
253 boolean result = matcher.matches(message, trigger); 253 boolean result = matcher.matches(message, trigger);