Commit d668af19b72c29ac2f09adaf64368ae93dc3f4ca
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
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<Integer> { |
| 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<String> { |
| 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<Config, Producer> 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
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); | ... | ... |