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 84 <scope>test</scope>
85 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 95 <!-- TODO @芋艿:临时打开 -->
89 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 20 @JsonSubTypes.Type(value = IotDataSinkTcpConfig.class, name = "2"),
21 21 @JsonSubTypes.Type(value = IotDataSinkWebSocketConfig.class, name = "3"),
22 22 @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"),
  23 + @JsonSubTypes.Type(value = IotDataSinkDatabaseConfig.class, name = "20"),
23 24 @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"),
24 25 @JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"),
25 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 9  
10 10 import java.util.List;
11 11  
12   -// TODO @puhui999:感觉这个,是不是放到 dal 里会好点?(讨论下,先不改哈)
13 12 /**
14 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 19 TCP(2, "TCP"),
20 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 25 REDIS(21, "Redis"),
26 26  
27 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 36  
37 37 // ========== 特殊:不放在字典里 ==========
38 38  
39   - // TODO @puhui999:@芋艿:需要测试下
40 39 DATE_TIME_GREATER_THAN("date_time_>", "#source > #value"), // 在时间之后:时间戳
41 40 DATE_TIME_LESS_THAN("date_time_<", "#source < #value"), // 在时间之前:时间戳
42 41 DATE_TIME_BETWEEN("date_time_between", // 在时间之间:时间戳
43 42 "(#source >= #values.get(0)) && (#source <= #values.get(1))"),
44 43  
45   - // TODO @puhui999:@芋艿:需要测试下
46 44 TIME_GREATER_THAN("time_>", "#source.isAfter(#value)"), // 在当日时间之后:HH:mm:ss
47 45 TIME_LESS_THAN("time_<", "#source.isBefore(#value)"), // 在当日时间之前:HH:mm:ss
48 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 12  
13 13 import java.time.Duration;
14 14  
15   -// TODO @芋艿:数据库
16   -// TODO @芋艿:mqtt
17   -
18 15 /**
19 16 * 可缓存的 {@link IotDataRuleAction} 抽象实现
20 17 *
... ... @@ -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 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 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 33 private volatile WebSocket webSocket;
34 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 43 public IotWebSocketClient(String serverUrl, Integer connectTimeoutMs, Integer sendTimeoutMs, String dataFormat) {
37 44 this.serverUrl = serverUrl;
38 45 this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : IotDataSinkWebSocketConfig.DEFAULT_CONNECT_TIMEOUT_MS;
... ... @@ -123,9 +130,8 @@ public class IotWebSocketClient {
123 130 public void close() {
124 131 try {
125 132 if (webSocket != null) {
126   - // 发送正常关闭帧,状态码 1000 表示正常关闭
127   - // TODO @puhui999:有没 1000 的枚举哈?在 okhttp 里
128   - webSocket.close(1000, "客户端主动关闭");
  133 + // 发送正常关闭帧
  134 + webSocket.close(NORMAL_CLOSURE_STATUS, "客户端主动关闭");
129 135 webSocket = null;
130 136 }
131 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 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
7 7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotCurrentTimeConditionMatcher;
8 8 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator;
  9 +import lombok.experimental.UtilityClass;
9 10 import lombok.extern.slf4j.Slf4j;
10 11  
11 12 import java.time.LocalDateTime;
... ... @@ -22,6 +23,7 @@ import java.util.List;
22 23 * @author HUIHUI
23 24 */
24 25 @Slf4j
  26 +@UtilityClass
25 27 public class IotSceneRuleTimeHelper {
26 28  
27 29 /**
... ... @@ -34,11 +36,6 @@ public class IotSceneRuleTimeHelper {
34 36 */
35 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 133 }
137 134 long startTimestamp = Long.parseLong(timestampRange.get(0).trim());
138 135 long endTimestamp = Long.parseLong(timestampRange.get(1).trim());
139   - // TODO @puhui999:hutool 里,看看有没 between 方法
140 136 return currentTimestamp >= startTimestamp && currentTimestamp <= endTimestamp;
141 137 }
142 138  
... ... @@ -188,7 +184,6 @@ public class IotSceneRuleTimeHelper {
188 184 }
189 185 LocalTime startTime = parseTime(timeRange.get(0).trim());
190 186 LocalTime endTime = parseTime(timeRange.get(1).trim());
191   - // TODO @puhui999:hutool 里,看看有没 between 方法
192 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 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2  
  3 +import cn.hutool.core.collection.CollUtil;
3 4 import cn.hutool.core.util.StrUtil;
4 5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
... ... @@ -73,8 +74,7 @@ public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTrigger
73 74 private boolean matchParameterCondition(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
74 75 // 1.1 从消息中提取服务调用的输入参数
75 76 Map<String, Object> inputParams = IotDeviceMessageUtils.extractServiceInputParams(message);
76   - // TODO @puhui999:要考虑 empty 的情况么?
77   - if (inputParams == null) {
  77 + if (CollUtil.isEmpty(inputParams)) {
78 78 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中缺少服务输入参数");
79 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 import org.mockito.Mock;
10 10 import org.mockito.MockitoAnnotations;
11 11  
12   -import java.util.HashMap;
13   -import java.util.Map;
14   -
15 12 import static org.junit.jupiter.api.Assertions.*;
16 13 import static org.mockito.Mockito.*;
17 14  
... ... @@ -45,9 +42,8 @@ class IotTcpDataRuleActionTest {
45 42 assertEquals(expectedType, actualType);
46 43 }
47 44  
48   - // TODO @puhui999:_ 后面是小写哈,单测的命名规则。
49 45 @Test
50   - public void testInitProducer_Success() throws Exception {
  46 + public void testInitProducer_success() throws Exception {
51 47 // 准备参数
52 48 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
53 49 config.setHost("localhost");
... ... @@ -63,7 +59,7 @@ class IotTcpDataRuleActionTest {
63 59 }
64 60  
65 61 @Test
66   - public void testInitProducer_InvalidHost() {
  62 + public void testInitProducer_invalidHost() {
67 63 // 准备参数
68 64 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
69 65 config.setHost("");
... ... @@ -81,7 +77,7 @@ class IotTcpDataRuleActionTest {
81 77 }
82 78  
83 79 @Test
84   - public void testInitProducer_InvalidPort() {
  80 + public void testInitProducer_invalidPort() {
85 81 // 准备参数
86 82 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
87 83 config.setHost("localhost");
... ... @@ -108,7 +104,7 @@ class IotTcpDataRuleActionTest {
108 104 }
109 105  
110 106 @Test
111   - public void testExecute_WithValidConfig() {
  107 + public void testExecute_withValidConfig() {
112 108 // 准备参数
113 109 IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report",
114 110 "{\"temperature\": 25.5, \"humidity\": 60}");
... ... @@ -128,7 +124,7 @@ class IotTcpDataRuleActionTest {
128 124 }
129 125  
130 126 @Test
131   - public void testConfig_DefaultValues() {
  127 + public void testConfig_defaultValues() {
132 128 // 准备参数
133 129 IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
134 130  
... ... @@ -146,12 +142,10 @@ class IotTcpDataRuleActionTest {
146 142 @Test
147 143 public void testMessageSerialization() {
148 144 // 准备参数
149   - Map<String, Object> params = new HashMap<>();
150   - params.put("temperature", 25.5);
151 145 IotDeviceMessage message = IotDeviceMessage.builder()
152 146 .deviceId(123L)
153 147 .method("thing.property.report")
154   - .params(params)
  148 + .params("{\"temperature\": 25.5}")
155 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 5 import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRuleSaveReqVO;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 7 import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
8   -import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
9 8 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
10 9 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
11 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 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 14 import org.junit.jupiter.api.Test;
14 15 import org.mockito.InjectMocks;
15 16 import org.mockito.Mock;
... ... @@ -42,19 +43,22 @@ public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest {
42 43 private List<IotSceneRuleAction> sceneRuleActions;
43 44  
44 45 @Mock
45   - private IotSchedulerManager schedulerManager;
  46 + private IotSceneRuleTimerHandler timerHandler;
46 47  
47 48 @Mock
48   - private IotProductService productService;
  49 + private IotTimerConditionEvaluator timerConditionEvaluator;
49 50  
50 51 @Mock
51   - private IotDeviceService deviceService;
  52 + private IotSceneRuleMatcherManager sceneRuleMatcherManager;
52 53  
53 54 @Mock
54   - private IotSceneRuleTimerHandler timerHandler;
  55 + private IotProductService productService;
  56 +
  57 + @Mock
  58 + private IotDeviceService deviceService;
55 59  
56 60 @Test
57   - public void testCreateScene_Rule_success() {
  61 + public void testCreateScene_rule_success() {
58 62 // 准备参数
59 63 IotSceneRuleSaveReqVO createReqVO = randomPojo(IotSceneRuleSaveReqVO.class, o -> {
60 64 o.setId(null);
... ... @@ -80,7 +84,7 @@ public class IotSceneRuleServiceSimpleTest extends BaseMockitoUnitTest {
80 84 }
81 85  
82 86 @Test
83   - public void testUpdateScene_Rule_success() {
  87 + public void testUpdateScene_rule_success() {
84 88 // 准备参数
85 89 Long id = randomLongId();
86 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 1 package cn.iocoder.yudao.module.iot.service.rule.scene;
2 2  
3 3 import cn.hutool.core.collection.ListUtil;
4   -import cn.hutool.extra.spring.SpringUtil;
5 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
6 5 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
7 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 14 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
16 15 import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
17 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 18 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
19 19 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator;
  20 +import cn.iocoder.yudao.module.iot.service.product.IotProductService;
20 21 import org.junit.jupiter.api.*;
21 22 import org.mockito.InjectMocks;
22 23 import org.mockito.Mock;
23   -import org.springframework.context.support.GenericApplicationContext;
24 24  
25 25 import java.lang.reflect.Field;
26 26 import java.util.*;
... ... @@ -63,6 +63,12 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe
63 63 @Mock
64 64 private IotSceneRuleTimerHandler timerHandler;
65 65  
  66 + @Mock
  67 + private IotSceneRuleMatcherManager sceneRuleMatcherManager;
  68 +
  69 + @Mock
  70 + private IotProductService productService;
  71 +
66 72 private IotTimerConditionEvaluator timerConditionEvaluator;
67 73  
68 74 // 测试常量
... ... @@ -71,12 +77,6 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe
71 77 private static final Long DEVICE_ID = 100L;
72 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 80 @BeforeEach
81 81 void setUp() {
82 82 // 创建并注入 timerConditionEvaluator 的依赖
... ... @@ -144,15 +144,14 @@ public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTe
144 144 IotDevicePropertyDO property = new IotDevicePropertyDO();
145 145 property.setValue(value);
146 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 150 private void mockDeviceState(Long deviceId, Integer state) {
152 151 IotDeviceDO device = new IotDeviceDO();
153 152 device.setId(deviceId);
154 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 59 // ========== 时间戳条件测试 ==========
60 60  
61 61 @Test
62   - public void testMatches_DateTimeGreaterThan_success() {
  62 + public void testMatches_dateTimeGreaterThan_success() {
63 63 // 准备参数
64 64 IotDeviceMessage message = createDeviceMessage();
65 65 long pastTimestamp = LocalDateTime.now().minusHours(1).toEpochSecond(ZoneOffset.of("+8"));
... ... @@ -76,7 +76,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
76 76 }
77 77  
78 78 @Test
79   - public void testMatches_DateTimeGreaterThan_fail() {
  79 + public void testMatches_dateTimeGreaterThan_fail() {
80 80 // 准备参数
81 81 IotDeviceMessage message = createDeviceMessage();
82 82 long futureTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8"));
... ... @@ -93,7 +93,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
93 93 }
94 94  
95 95 @Test
96   - public void testMatches_DateTimeLessThan_success() {
  96 + public void testMatches_dateTimeLessThan_success() {
97 97 // 准备参数
98 98 IotDeviceMessage message = createDeviceMessage();
99 99 long futureTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8"));
... ... @@ -110,7 +110,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
110 110 }
111 111  
112 112 @Test
113   - public void testMatches_DateTimeBetween_success() {
  113 + public void testMatches_dateTimeBetween_success() {
114 114 // 准备参数
115 115 IotDeviceMessage message = createDeviceMessage();
116 116 long startTimestamp = LocalDateTime.now().minusHours(1).toEpochSecond(ZoneOffset.of("+8"));
... ... @@ -128,7 +128,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
128 128 }
129 129  
130 130 @Test
131   - public void testMatches_DateTimeBetween_fail() {
  131 + public void testMatches_dateTimeBetween_fail() {
132 132 // 准备参数
133 133 IotDeviceMessage message = createDeviceMessage();
134 134 long startTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8"));
... ... @@ -148,7 +148,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
148 148 // ========== 当日时间条件测试 ==========
149 149  
150 150 @Test
151   - public void testMatches_TimeGreaterThan_earlyMorning() {
  151 + public void testMatches_timeGreaterThan_earlyMorning() {
152 152 // 准备参数
153 153 IotDeviceMessage message = createDeviceMessage();
154 154 IotSceneRuleDO.TriggerCondition condition = createTimeCondition(
... ... @@ -165,7 +165,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
165 165 }
166 166  
167 167 @Test
168   - public void testMatches_TimeLessThan_lateNight() {
  168 + public void testMatches_timeLessThan_lateNight() {
169 169 // 准备参数
170 170 IotDeviceMessage message = createDeviceMessage();
171 171 IotSceneRuleDO.TriggerCondition condition = createTimeCondition(
... ... @@ -182,7 +182,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
182 182 }
183 183  
184 184 @Test
185   - public void testMatches_TimeBetween_allDay() {
  185 + public void testMatches_timeBetween_allDay() {
186 186 // 准备参数
187 187 IotDeviceMessage message = createDeviceMessage();
188 188 IotSceneRuleDO.TriggerCondition condition = createTimeCondition(
... ... @@ -198,7 +198,7 @@ public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherT
198 198 }
199 199  
200 200 @Test
201   - public void testMatches_TimeBetween_workingHours() {
  201 + public void testMatches_timeBetween_workingHours() {
202 202 // 准备参数
203 203 IotDeviceMessage message = createDeviceMessage();
204 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 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 3 import cn.iocoder.yudao.module.iot.core.enums.device.IotDeviceStateEnum;
5 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 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 import org.junit.jupiter.api.BeforeEach;
11 10 import org.junit.jupiter.api.Test;
12 11  
13   -import java.util.HashMap;
14   -import java.util.Map;
15   -
16 12 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
17 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
18 14 import static org.junit.jupiter.api.Assertions.*;
... ... @@ -346,10 +342,7 @@ public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherT
346 342 private IotDeviceMessage createDeviceMessage(Integer deviceState) {
347 343 IotDeviceMessage message = new IotDeviceMessage();
348 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 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 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2  
3   -import cn.hutool.core.collection.ListUtil;
4 3 import cn.hutool.core.map.MapUtil;
5 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7   -import cn.iocoder.yudao.module.iot.core.topic.event.IotDeviceEventPostReqDTO;
8 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
9   -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
10 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
11 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
12 9 import org.junit.jupiter.api.BeforeEach;
... ... @@ -357,184 +354,6 @@ public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatche
357 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 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7   -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
9 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
10 9 import org.junit.jupiter.api.BeforeEach;
... ... @@ -486,7 +485,7 @@ public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMa
486 485 IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
487 486 trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
488 487 trigger.setIdentifier(serviceIdentifier);
489   - trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator());
  488 + trigger.setOperator("=="); // 等于操作符
490 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 10 import org.junit.jupiter.api.BeforeEach;
11 11 import org.junit.jupiter.api.Test;
12 12  
13   -import java.util.HashMap;
14   -import java.util.Map;
15   -
16 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
17 14 import static org.junit.jupiter.api.Assertions.*;
18 15  
... ... @@ -251,9 +248,7 @@ public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatc
251 248 IotDeviceMessage message = new IotDeviceMessage();
252 249 message.setDeviceId(randomLongId());
253 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 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 247 public void testMatches_rangeCronSuccess() {
248 248 // 准备参数
249 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 253 boolean result = matcher.matches(message, trigger);
... ...