Commit 32f34335778805b7d1848b4994a7b60c76166a6b

Authored by YunaiV
2 parents 24c90401 3f83569e

Merge branch 'feature/iot' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into develop

Showing 42 changed files with 1755 additions and 294 deletions
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java
@@ -17,6 +17,8 @@ import lombok.Data; @@ -17,6 +17,8 @@ import lombok.Data;
17 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) 17 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true)
18 @JsonSubTypes({ 18 @JsonSubTypes({
19 @JsonSubTypes.Type(value = IotDataSinkHttpConfig.class, name = "1"), 19 @JsonSubTypes.Type(value = IotDataSinkHttpConfig.class, name = "1"),
  20 + @JsonSubTypes.Type(value = IotDataSinkTcpConfig.class, name = "2"),
  21 + @JsonSubTypes.Type(value = IotDataSinkWebSocketConfig.class, name = "3"),
20 @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"), 22 @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"),
21 @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"), 23 @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"),
22 @JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"), 24 @JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"),
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT TCP 配置 {@link IotAbstractDataSinkConfig} 实现类
  7 + *
  8 + * @author HUIHUI
  9 + */
  10 +@Data
  11 +public class IotDataSinkTcpConfig extends IotAbstractDataSinkConfig {
  12 +
  13 + /**
  14 + * TCP 服务器地址
  15 + */
  16 + private String host;
  17 +
  18 + /**
  19 + * TCP 服务器端口
  20 + */
  21 + private Integer port;
  22 +
  23 + /**
  24 + * 连接超时时间(毫秒)
  25 + */
  26 + private Integer connectTimeoutMs = 5000;
  27 +
  28 + /**
  29 + * 读取超时时间(毫秒)
  30 + */
  31 + private Integer readTimeoutMs = 10000;
  32 +
  33 + /**
  34 + * 是否启用 SSL
  35 + */
  36 + private Boolean ssl = false;
  37 +
  38 + /**
  39 + * SSL 证书路径(当 ssl=true 时需要)
  40 + */
  41 + private String sslCertPath;
  42 +
  43 + /**
  44 + * 数据格式:JSON 或 BINARY
  45 + */
  46 + private String dataFormat = "JSON";
  47 +
  48 + /**
  49 + * 心跳间隔时间(毫秒),0 表示不启用心跳
  50 + */
  51 + private Long heartbeatIntervalMs = 30000L;
  52 +
  53 + /**
  54 + * 重连间隔时间(毫秒)
  55 + */
  56 + private Long reconnectIntervalMs = 5000L;
  57 +
  58 + /**
  59 + * 最大重连次数
  60 + */
  61 + private Integer maxReconnectAttempts = 3;
  62 +
  63 +}
0 \ No newline at end of file 64 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * IoT WebSocket 配置 {@link IotAbstractDataSinkConfig} 实现类
  7 + * <p>
  8 + * 配置设备消息通过 WebSocket 协议发送到外部 WebSocket 服务器
  9 + * 支持 WebSocket (ws://) 和 WebSocket Secure (wss://) 连接
  10 + *
  11 + * @author HUIHUI
  12 + */
  13 +@Data
  14 +public class IotDataSinkWebSocketConfig extends IotAbstractDataSinkConfig {
  15 +
  16 + /**
  17 + * WebSocket 服务器地址
  18 + * 例如:ws://localhost:8080/ws 或 wss://example.com/ws
  19 + */
  20 + private String serverUrl;
  21 +
  22 + /**
  23 + * 连接超时时间(毫秒)
  24 + */
  25 + private Integer connectTimeoutMs = 5000;
  26 +
  27 + /**
  28 + * 发送超时时间(毫秒)
  29 + */
  30 + private Integer sendTimeoutMs = 10000;
  31 +
  32 + /**
  33 + * 心跳间隔时间(毫秒),0 表示不启用心跳
  34 + */
  35 + private Long heartbeatIntervalMs = 30000L;
  36 +
  37 + /**
  38 + * 心跳消息内容(JSON 格式)
  39 + */
  40 + private String heartbeatMessage = "{\"type\":\"heartbeat\"}";
  41 +
  42 + /**
  43 + * 子协议列表(逗号分隔)
  44 + */
  45 + private String subprotocols;
  46 +
  47 + /**
  48 + * 自定义请求头(JSON 格式)
  49 + */
  50 + private String customHeaders;
  51 +
  52 + /**
  53 + * 是否启用 SSL 证书验证(仅对 wss:// 生效)
  54 + */
  55 + private Boolean verifySslCert = true;
  56 +
  57 + /**
  58 + * 数据格式:JSON 或 TEXT
  59 + */
  60 + private String dataFormat = "JSON";
  61 +
  62 + /**
  63 + * 重连间隔时间(毫秒)
  64 + */
  65 + private Long reconnectIntervalMs = 5000L;
  66 +
  67 + /**
  68 + * 最大重连次数
  69 + */
  70 + private Integer maxReconnectAttempts = 3;
  71 +
  72 + /**
  73 + * 是否启用压缩
  74 + */
  75 + private Boolean enableCompression = false;
  76 +
  77 + /**
  78 + * 消息发送重试次数
  79 + */
  80 + private Integer sendRetryCount = 1;
  81 +
  82 + /**
  83 + * 消息发送重试间隔(毫秒)
  84 + */
  85 + private Long sendRetryIntervalMs = 1000L;
  86 +
  87 +}
0 \ No newline at end of file 88 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java
@@ -76,4 +76,12 @@ public interface RedisKeyConstants { @@ -76,4 +76,12 @@ public interface RedisKeyConstants {
76 */ 76 */
77 String DATA_SINK = "iot:data_sink"; 77 String DATA_SINK = "iot:data_sink";
78 78
  79 + /**
  80 + * 场景联动规则的数据缓存,使用 Spring Cache 操作
  81 + * <p>
  82 + * KEY 格式:scene_rule_list_${productId}_${deviceId}
  83 + * VALUE 数据类型:String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO} 列表
  84 + */
  85 + String SCENE_RULE_LIST = "iot:scene_rule_list";
  86 +
79 } 87 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java
@@ -19,7 +19,6 @@ import java.util.Arrays; @@ -19,7 +19,6 @@ import java.util.Arrays;
19 @Getter 19 @Getter
20 public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable<Integer> { 20 public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable<Integer> {
21 21
22 - // TODO @芋艿:后续“对应”部分,要 @下,等包结构梳理完;  
23 /** 22 /**
24 * 设备上下线变更 23 * 设备上下线变更
25 * 24 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
  5 +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +import java.time.Duration;
  11 +
  12 +/**
  13 + * TCP 的 {@link IotDataRuleAction} 实现类
  14 + * <p>
  15 + * 负责将设备消息发送到外部 TCP 服务器
  16 + * 支持普通 TCP 和 SSL TCP 连接,支持 JSON 和 BINARY 数据格式
  17 + * 使用连接池管理 TCP 连接,提高性能和资源利用率
  18 + *
  19 + * @author HUIHUI
  20 + */
  21 +@Component
  22 +@Slf4j
  23 +public class IotTcpDataRuleAction extends
  24 + IotDataRuleCacheableAction<IotDataSinkTcpConfig, IotTcpClient> {
  25 +
  26 + private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
  27 + private static final Duration SEND_TIMEOUT = Duration.ofSeconds(10);
  28 +
  29 + @Override
  30 + public Integer getType() {
  31 + return IotDataSinkTypeEnum.TCP.getType();
  32 + }
  33 +
  34 + @Override
  35 + protected IotTcpClient initProducer(IotDataSinkTcpConfig config) throws Exception {
  36 + // 1.1 参数校验
  37 + if (config.getHost() == null || config.getHost().trim().isEmpty()) {
  38 + throw new IllegalArgumentException("TCP 服务器地址不能为空");
  39 + }
  40 + if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
  41 + throw new IllegalArgumentException("TCP 服务器端口无效");
  42 + }
  43 +
  44 + // 2.1 创建 TCP 客户端
  45 + IotTcpClient tcpClient = new IotTcpClient(
  46 + config.getHost(),
  47 + config.getPort(),
  48 + config.getConnectTimeoutMs(),
  49 + config.getReadTimeoutMs(),
  50 + config.getSsl(),
  51 + config.getSslCertPath(),
  52 + config.getDataFormat()
  53 + );
  54 + // 2.2 连接服务器
  55 + tcpClient.connect();
  56 + log.info("[initProducer][TCP 客户端创建并连接成功,服务器: {}:{},SSL: {},数据格式: {}]",
  57 + config.getHost(), config.getPort(), config.getSsl(), config.getDataFormat());
  58 + return tcpClient;
  59 + }
  60 +
  61 + @Override
  62 + protected void closeProducer(IotTcpClient producer) throws Exception {
  63 + if (producer != null) {
  64 + producer.close();
  65 + }
  66 + }
  67 +
  68 + @Override
  69 + protected void execute(IotDeviceMessage message, IotDataSinkTcpConfig config) throws Exception {
  70 + try {
  71 + // 1.1 获取或创建 TCP 客户端
  72 + IotTcpClient tcpClient = getProducer(config);
  73 + // 1.2 检查连接状态,如果断开则重新连接
  74 + if (!tcpClient.isConnected()) {
  75 + log.warn("[execute][TCP 连接已断开,尝试重新连接,服务器: {}:{}]", config.getHost(), config.getPort());
  76 + tcpClient.connect();
  77 + }
  78 +
  79 + // 2.1 发送消息并等待结果
  80 + tcpClient.sendMessage(message);
  81 + // 2.2 记录发送成功日志
  82 + log.info("[execute][message({}) config({}) 发送成功,TCP 服务器: {}:{}]",
  83 + message, config, config.getHost(), config.getPort());
  84 + } catch (Exception e) {
  85 + log.error("[execute][message({}) config({}) 发送失败,TCP 服务器: {}:{}]",
  86 + message, config, config.getHost(), config.getPort(), e);
  87 + throw e;
  88 + }
  89 + }
  90 +
  91 +}
0 \ No newline at end of file 92 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import lombok.extern.slf4j.Slf4j;
  6 +
  7 +import javax.net.ssl.SSLSocketFactory;
  8 +import java.io.BufferedReader;
  9 +import java.io.IOException;
  10 +import java.io.InputStreamReader;
  11 +import java.io.OutputStream;
  12 +import java.net.InetSocketAddress;
  13 +import java.net.Socket;
  14 +import java.nio.charset.StandardCharsets;
  15 +import java.util.concurrent.atomic.AtomicBoolean;
  16 +
  17 +/**
  18 + * IoT TCP 客户端
  19 + * <p>
  20 + * 负责与外部 TCP 服务器建立连接并发送设备消息
  21 + * 支持 JSON 和 BINARY 两种数据格式,支持 SSL 加密连接
  22 + *
  23 + * @author HUIHUI
  24 + */
  25 +@Slf4j
  26 +public class IotTcpClient {
  27 +
  28 + private final String host;
  29 + private final Integer port;
  30 + private final Integer connectTimeoutMs;
  31 + private final Integer readTimeoutMs;
  32 + private final Boolean ssl;
  33 + private final String sslCertPath;
  34 + private final String dataFormat;
  35 +
  36 + private Socket socket;
  37 + private OutputStream outputStream;
  38 + private BufferedReader reader;
  39 + private final AtomicBoolean connected = new AtomicBoolean(false);
  40 +
  41 + // TODO @puhui999:default 值,IotDataSinkTcpConfig.java 枚举起来哈;
  42 + public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs,
  43 + Boolean ssl, String sslCertPath, String dataFormat) {
  44 + this.host = host;
  45 + this.port = port;
  46 + this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : 5000;
  47 + this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : 10000;
  48 + this.ssl = ssl != null ? ssl : false;
  49 + this.sslCertPath = sslCertPath;
  50 + this.dataFormat = dataFormat != null ? dataFormat : "JSON";
  51 + }
  52 +
  53 + /**
  54 + * 连接到 TCP 服务器
  55 + */
  56 + public void connect() throws Exception {
  57 + if (connected.get()) {
  58 + log.warn("[connect][TCP 客户端已经连接,无需重复连接]");
  59 + return;
  60 + }
  61 +
  62 + try {
  63 + if (ssl) {
  64 + // SSL 连接
  65 + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
  66 + socket = sslSocketFactory.createSocket();
  67 + } else {
  68 + // 普通连接
  69 + socket = new Socket();
  70 + }
  71 +
  72 + // 连接服务器
  73 + socket.connect(new InetSocketAddress(host, port), connectTimeoutMs);
  74 + socket.setSoTimeout(readTimeoutMs);
  75 +
  76 + // 获取输入输出流
  77 + outputStream = socket.getOutputStream();
  78 + reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
  79 +
  80 + // 更新状态
  81 + connected.set(true);
  82 + log.info("[connect][TCP 客户端连接成功,服务器地址: {}:{}]", host, port);
  83 + } catch (Exception e) {
  84 + close();
  85 + log.error("[connect][TCP 客户端连接失败,服务器地址: {}:{}]", host, port, e);
  86 + throw e;
  87 + }
  88 + }
  89 +
  90 + /**
  91 + * 发送设备消息
  92 + *
  93 + * @param message 设备消息
  94 + * @throws Exception 发送异常
  95 + */
  96 + public void sendMessage(IotDeviceMessage message) throws Exception {
  97 + if (!connected.get()) {
  98 + throw new IllegalStateException("TCP 客户端未连接");
  99 + }
  100 +
  101 + try {
  102 + // TODO @puhui999:枚举值
  103 + String messageData;
  104 + if ("JSON".equalsIgnoreCase(dataFormat)) {
  105 + // JSON 格式
  106 + messageData = JsonUtils.toJsonString(message);
  107 + } else {
  108 + // BINARY 格式(这里简化为字符串,实际可能需要自定义二进制协议)
  109 + messageData = message.toString();
  110 + }
  111 +
  112 + // 发送消息
  113 + outputStream.write(messageData.getBytes(StandardCharsets.UTF_8));
  114 + outputStream.write('\n'); // 添加换行符作为消息分隔符
  115 + outputStream.flush();
  116 + log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]",
  117 + message.getDeviceId(), messageData.length());
  118 + } catch (Exception e) {
  119 + log.error("[sendMessage][发送消息失败,设备 ID: {}]", message.getDeviceId(), e);
  120 + throw e;
  121 + }
  122 + }
  123 +
  124 + /**
  125 + * 关闭连接
  126 + */
  127 + public void close() {
  128 + if (!connected.get()) {
  129 + return;
  130 + }
  131 +
  132 + try {
  133 + // 关闭资源
  134 + if (reader != null) {
  135 + try {
  136 + reader.close();
  137 + } catch (IOException e) {
  138 + log.warn("[close][关闭输入流失败]", e);
  139 + }
  140 + }
  141 + if (outputStream != null) {
  142 + try {
  143 + outputStream.close();
  144 + } catch (IOException e) {
  145 + log.warn("[close][关闭输出流失败]", e);
  146 + }
  147 + }
  148 + if (socket != null) {
  149 + try {
  150 + socket.close();
  151 + } catch (IOException e) {
  152 + log.warn("[close][关闭 Socket 失败]", e);
  153 + }
  154 + }
  155 +
  156 + // 更新状态
  157 + connected.set(false);
  158 + log.info("[close][TCP 客户端连接已关闭,服务器地址: {}:{}]", host, port);
  159 + } catch (Exception e) {
  160 + log.error("[close][关闭 TCP 客户端连接异常]", e);
  161 + }
  162 + }
  163 +
  164 + /**
  165 + * 检查连接状态
  166 + *
  167 + * @return 是否已连接
  168 + */
  169 + public boolean isConnected() {
  170 + return connected.get() && socket != null && !socket.isClosed();
  171 + }
  172 +
  173 + @Override
  174 + public String toString() {
  175 + return "IotTcpClient{" +
  176 + "host='" + host + '\'' +
  177 + ", port=" + port +
  178 + ", ssl=" + ssl +
  179 + ", dataFormat='" + dataFormat + '\'' +
  180 + ", connected=" + connected.get() +
  181 + '}';
  182 + }
  183 +
  184 +}
0 \ No newline at end of file 185 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java
@@ -16,14 +16,17 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; @@ -16,14 +16,17 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
16 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; 16 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
17 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 17 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
18 import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper; 18 import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
  19 +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
19 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 20 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
20 -import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;  
21 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 21 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
22 import cn.iocoder.yudao.module.iot.service.product.IotProductService; 22 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
23 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; 23 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
24 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager; 24 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager;
  25 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
25 import jakarta.annotation.Resource; 26 import jakarta.annotation.Resource;
26 import lombok.extern.slf4j.Slf4j; 27 import lombok.extern.slf4j.Slf4j;
  28 +import org.springframework.cache.annotation.CacheEvict;
  29 +import org.springframework.cache.annotation.Cacheable;
27 import org.springframework.stereotype.Service; 30 import org.springframework.stereotype.Service;
28 import org.springframework.validation.annotation.Validated; 31 import org.springframework.validation.annotation.Validated;
29 32
@@ -47,9 +50,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -47,9 +50,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
47 @Resource 50 @Resource
48 private IotSceneRuleMapper sceneRuleMapper; 51 private IotSceneRuleMapper sceneRuleMapper;
49 52
50 - // TODO @puhui999:定时任务,基于它调度;  
51 - @Resource(name = "iotSchedulerManager")  
52 - private IotSchedulerManager schedulerManager;  
53 @Resource 53 @Resource
54 private IotProductService productService; 54 private IotProductService productService;
55 @Resource 55 @Resource
@@ -59,38 +59,68 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -59,38 +59,68 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
59 private IotSceneRuleMatcherManager sceneRuleMatcherManager; 59 private IotSceneRuleMatcherManager sceneRuleMatcherManager;
60 @Resource 60 @Resource
61 private List<IotSceneRuleAction> sceneRuleActions; 61 private List<IotSceneRuleAction> sceneRuleActions;
  62 + @Resource
  63 + private IotSceneRuleTimerHandler timerHandler;
62 64
63 @Override 65 @Override
  66 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
64 public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) { 67 public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) {
65 IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class); 68 IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class);
66 sceneRuleMapper.insert(sceneRule); 69 sceneRuleMapper.insert(sceneRule);
  70 +
  71 + // 注册定时触发器
  72 + timerHandler.registerTimerTriggers(sceneRule);
  73 +
67 return sceneRule.getId(); 74 return sceneRule.getId();
68 } 75 }
69 76
70 @Override 77 @Override
  78 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
71 public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) { 79 public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) {
72 // 校验存在 80 // 校验存在
73 validateSceneRuleExists(updateReqVO.getId()); 81 validateSceneRuleExists(updateReqVO.getId());
74 // 更新 82 // 更新
75 IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class); 83 IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class);
76 sceneRuleMapper.updateById(updateObj); 84 sceneRuleMapper.updateById(updateObj);
  85 +
  86 + // 更新定时触发器
  87 + timerHandler.updateTimerTriggers(updateObj);
77 } 88 }
78 89
79 @Override 90 @Override
  91 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
80 public void updateSceneRuleStatus(Long id, Integer status) { 92 public void updateSceneRuleStatus(Long id, Integer status) {
81 - // 校验存在 93 + // 1. 校验存在
82 validateSceneRuleExists(id); 94 validateSceneRuleExists(id);
83 - // 更新状态 95 +
  96 + // 2. 更新状态
84 IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status); 97 IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status);
85 sceneRuleMapper.updateById(updateObj); 98 sceneRuleMapper.updateById(updateObj);
  99 +
  100 + // 3. 根据状态管理定时触发器
  101 + if (CommonStatusEnum.isEnable(status)) {
  102 + // 启用时,获取完整的场景规则信息并注册定时触发器
  103 + IotSceneRuleDO sceneRule = sceneRuleMapper.selectById(id);
  104 + if (sceneRule != null) {
  105 + timerHandler.registerTimerTriggers(sceneRule);
  106 + }
  107 + } else {
  108 + // 禁用时,暂停定时触发器
  109 + timerHandler.pauseTimerTriggers(id);
  110 + }
86 } 111 }
87 112
88 @Override 113 @Override
  114 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
89 public void deleteSceneRule(Long id) { 115 public void deleteSceneRule(Long id) {
90 - // 校验存在 116 + // 1. 校验存在
91 validateSceneRuleExists(id); 117 validateSceneRuleExists(id);
92 - // 删除 118 +
  119 + // 2. 删除
93 sceneRuleMapper.deleteById(id); 120 sceneRuleMapper.deleteById(id);
  121 +
  122 + // 3. 删除定时触发器
  123 + timerHandler.unregisterTimerTriggers(id);
94 } 124 }
95 125
96 private void validateSceneRuleExists(Long id) { 126 private void validateSceneRuleExists(Long id) {
@@ -126,16 +156,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -126,16 +156,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
126 return sceneRuleMapper.selectListByStatus(status); 156 return sceneRuleMapper.selectListByStatus(status);
127 } 157 }
128 158
129 - // TODO 芋艿,缓存待实现 @puhui999  
130 @Override 159 @Override
  160 + @Cacheable(value = RedisKeyConstants.SCENE_RULE_LIST, key = "#productId + '_' + #deviceId ")
131 @TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略 161 @TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略
132 public List<IotSceneRuleDO> getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId) { 162 public List<IotSceneRuleDO> getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId) {
133 - List<IotSceneRuleDO> list = sceneRuleMapper.selectList();  
134 - // 只返回启用状态的规则场景  
135 - List<IotSceneRuleDO> enabledList = filterList(list,  
136 - sceneRule -> CommonStatusEnum.isEnable(sceneRule.getStatus())); 163 + // 1. 查询启用状态的规则场景
  164 + List<IotSceneRuleDO> enabledList = sceneRuleMapper.selectList(IotSceneRuleDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
137 165
138 - // 根据 productKey 和 deviceName 进行匹配 166 + // 2. 根据 productKey 和 deviceName 进行匹配
139 return filterList(enabledList, sceneRule -> { 167 return filterList(enabledList, sceneRule -> {
140 if (CollUtil.isEmpty(sceneRule.getTriggers())) { 168 if (CollUtil.isEmpty(sceneRule.getTriggers())) {
141 return false; 169 return false;
@@ -144,21 +172,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -144,21 +172,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
144 for (IotSceneRuleDO.Trigger trigger : sceneRule.getTriggers()) { 172 for (IotSceneRuleDO.Trigger trigger : sceneRule.getTriggers()) {
145 // 检查触发器是否匹配指定的产品和设备 173 // 检查触发器是否匹配指定的产品和设备
146 try { 174 try {
147 - // 1. 检查产品是否匹配  
148 - if (trigger.getProductId() == null) {  
149 - return false;  
150 - }  
151 - if (trigger.getDeviceId() == null) { 175 + // 检查产品是否匹配
  176 + if (trigger.getProductId() == null || trigger.getDeviceId() == null) {
152 return false; 177 return false;
153 } 178 }
154 // 检查是否是全部设备的特殊标识 179 // 检查是否是全部设备的特殊标识
155 if (IotDeviceDO.DEVICE_ID_ALL.equals(trigger.getDeviceId())) { 180 if (IotDeviceDO.DEVICE_ID_ALL.equals(trigger.getDeviceId())) {
156 - return true; // 匹配所有设备 181 + return true;
157 } 182 }
158 // 检查具体设备 ID 是否匹配 183 // 检查具体设备 ID 是否匹配
159 return ObjUtil.equal(productId, trigger.getProductId()) && ObjUtil.equal(deviceId, trigger.getDeviceId()); 184 return ObjUtil.equal(productId, trigger.getProductId()) && ObjUtil.equal(deviceId, trigger.getDeviceId());
160 } catch (Exception e) { 185 } catch (Exception e) {
161 - log.warn("[isMatchProductAndDevice][产品({}) 设备({}) 匹配触发器异常]", productId, deviceId, e); 186 + log.warn("[getSceneRuleListByProductIdAndDeviceIdFromCache][产品({}) 设备({}) 匹配触发器异常]",
  187 + productId, deviceId, e);
162 return false; 188 return false;
163 } 189 }
164 } 190 }
@@ -168,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -168,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
168 194
169 @Override 195 @Override
170 public void executeSceneRuleByDevice(IotDeviceMessage message) { 196 public void executeSceneRuleByDevice(IotDeviceMessage message) {
171 - // TODO @芋艿:这里的 tenantId,通过设备获取;@puhui999:  
172 - TenantUtils.execute(message.getTenantId(), () -> {  
173 - // 1. 获得设备匹配的规则场景 197 + // 1.1 这里的 tenantId,通过设备获取;
  198 + IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
  199 + TenantUtils.execute(device.getTenantId(), () -> {
  200 + // 1.2 获得设备匹配的规则场景
174 List<IotSceneRuleDO> sceneRules = getMatchedSceneRuleListByMessage(message); 201 List<IotSceneRuleDO> sceneRules = getMatchedSceneRuleListByMessage(message);
175 if (CollUtil.isEmpty(sceneRules)) { 202 if (CollUtil.isEmpty(sceneRules)) {
176 return; 203 return;
@@ -214,16 +241,16 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -214,16 +241,16 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
214 */ 241 */
215 private List<IotSceneRuleDO> getMatchedSceneRuleListByMessage(IotDeviceMessage message) { 242 private List<IotSceneRuleDO> getMatchedSceneRuleListByMessage(IotDeviceMessage message) {
216 // 1. 匹配设备 243 // 1. 匹配设备
217 - // TODO @芋艿:可能需要 getSelf(); 缓存 @puhui999; 244 + // TODO 缓存 @puhui999:可能需要 getSelf()
218 // 1.1 通过 deviceId 获取设备信息 245 // 1.1 通过 deviceId 获取设备信息
219 - IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId()); 246 + IotDeviceDO device = getSelf().deviceService.getDeviceFromCache(message.getDeviceId());
220 if (device == null) { 247 if (device == null) {
221 log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId()); 248 log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId());
222 return List.of(); 249 return List.of();
223 } 250 }
224 251
225 // 1.2 通过 productId 获取产品信息 252 // 1.2 通过 productId 获取产品信息
226 - IotProductDO product = productService.getProductFromCache(device.getProductId()); 253 + IotProductDO product = getSelf().productService.getProductFromCache(device.getProductId());
227 if (product == null) { 254 if (product == null) {
228 log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId()); 255 log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId());
229 return List.of(); 256 return List.of();
@@ -273,7 +300,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -273,7 +300,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
273 */ 300 */
274 private boolean matchSingleTrigger(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) { 301 private boolean matchSingleTrigger(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) {
275 try { 302 try {
276 - // 2. 检查触发器的条件分组  
277 return sceneRuleMatcherManager.isMatched(message, trigger) && isTriggerConditionGroupsMatched(message, trigger, sceneRule); 303 return sceneRuleMatcherManager.isMatched(message, trigger) && isTriggerConditionGroupsMatched(message, trigger, sceneRule);
278 } catch (Exception e) { 304 } catch (Exception e) {
279 log.error("[matchSingleTrigger][触发器匹配异常] sceneRuleId: {}, triggerType: {}, message: {}", 305 log.error("[matchSingleTrigger][触发器匹配异常] sceneRuleId: {}, triggerType: {}, message: {}",
@@ -290,18 +316,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -290,18 +316,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
290 * @param sceneRule 场景规则(用于日志) 316 * @param sceneRule 场景规则(用于日志)
291 * @return 是否匹配 317 * @return 是否匹配
292 */ 318 */
293 - private boolean isTriggerConditionGroupsMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) {  
294 - // 如果没有条件分组,则认为匹配成功(只依赖基础触发器匹配) 319 + private boolean isTriggerConditionGroupsMatched(IotDeviceMessage message,
  320 + IotSceneRuleDO.Trigger trigger,
  321 + IotSceneRuleDO sceneRule) {
  322 + // 1. 如果没有条件分组,则认为匹配成功(只依赖基础触发器匹配)
295 if (CollUtil.isEmpty(trigger.getConditionGroups())) { 323 if (CollUtil.isEmpty(trigger.getConditionGroups())) {
296 return true; 324 return true;
297 } 325 }
298 326
299 - // 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系 327 + // 2. 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系
300 for (List<IotSceneRuleDO.TriggerCondition> conditionGroup : trigger.getConditionGroups()) { 328 for (List<IotSceneRuleDO.TriggerCondition> conditionGroup : trigger.getConditionGroups()) {
301 if (CollUtil.isEmpty(conditionGroup)) { 329 if (CollUtil.isEmpty(conditionGroup)) {
302 continue; 330 continue;
303 } 331 }
304 -  
305 // 检查当前分组中的所有条件是否都匹配(且关系) 332 // 检查当前分组中的所有条件是否都匹配(且关系)
306 boolean allConditionsMatched = true; 333 boolean allConditionsMatched = true;
307 for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) { 334 for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) {
@@ -310,14 +337,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -310,14 +337,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
310 break; 337 break;
311 } 338 }
312 } 339 }
313 -  
314 // 如果当前分组的所有条件都匹配,则整个触发器匹配成功 340 // 如果当前分组的所有条件都匹配,则整个触发器匹配成功
315 if (allConditionsMatched) { 341 if (allConditionsMatched) {
316 return true; 342 return true;
317 } 343 }
318 } 344 }
319 345
320 - // 所有分组都不匹配 346 + // 3. 所有分组都不匹配
321 return false; 347 return false;
322 } 348 }
323 349
@@ -352,13 +378,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -352,13 +378,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
352 sceneRules.forEach(sceneRule -> { 378 sceneRules.forEach(sceneRule -> {
353 // 2. 遍历规则场景的动作 379 // 2. 遍历规则场景的动作
354 sceneRule.getActions().forEach(actionConfig -> { 380 sceneRule.getActions().forEach(actionConfig -> {
355 - // 3.1 获取对应的动作 Action 数组 381 + // 2.1 获取对应的动作 Action 数组
356 List<IotSceneRuleAction> actions = filterList(sceneRuleActions, 382 List<IotSceneRuleAction> actions = filterList(sceneRuleActions,
357 action -> action.getType().getType().equals(actionConfig.getType())); 383 action -> action.getType().getType().equals(actionConfig.getType()));
358 if (CollUtil.isEmpty(actions)) { 384 if (CollUtil.isEmpty(actions)) {
359 return; 385 return;
360 } 386 }
361 - // 3.2 执行动作 387 + // 2.2 执行动作
362 actions.forEach(action -> { 388 actions.forEach(action -> {
363 try { 389 try {
364 action.execute(message, sceneRule, actionConfig); 390 action.execute(message, sceneRule, actionConfig);
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertRecoverSceneRuleAction.java
@@ -14,7 +14,6 @@ import java.util.List; @@ -14,7 +14,6 @@ import java.util.List;
14 14
15 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 15 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
16 16
17 -// TODO @puhui999、@芋艿:未测试;需要场景联动开发完  
18 /** 17 /**
19 * IoT 告警恢复的 {@link IotSceneRuleAction} 实现类 18 * IoT 告警恢复的 {@link IotSceneRuleAction} 实现类
20 * 19 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotAlertTriggerSceneRuleAction.java
@@ -17,7 +17,6 @@ import org.springframework.stereotype.Component; @@ -17,7 +17,6 @@ import org.springframework.stereotype.Component;
17 import javax.annotation.Nullable; 17 import javax.annotation.Nullable;
18 import java.util.List; 18 import java.util.List;
19 19
20 -// TODO @puhui999、@芋艿:未测试;需要场景联动开发完  
21 /** 20 /**
22 * IoT 告警触发的 {@link IotSceneRuleAction} 实现类 21 * IoT 告警触发的 {@link IotSceneRuleAction} 实现类
23 * 22 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceControlSceneRuleAction.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.action; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.action;
2 2
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 8 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum; 9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
6 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 10 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
@@ -9,8 +13,11 @@ import jakarta.annotation.Resource; @@ -9,8 +13,11 @@ import jakarta.annotation.Resource;
9 import lombok.extern.slf4j.Slf4j; 13 import lombok.extern.slf4j.Slf4j;
10 import org.springframework.stereotype.Component; 14 import org.springframework.stereotype.Component;
11 15
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +
12 /** 19 /**
13 - * IoT 设备控制的 {@link IotSceneRuleAction} 实现类 20 + * IoT 设备属性设置的 {@link IotSceneRuleAction} 实现类
14 * 21 *
15 * @author 芋道源码 22 * @author 芋道源码
16 */ 23 */
@@ -23,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction { @@ -23,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction {
23 @Resource 30 @Resource
24 private IotDeviceMessageService deviceMessageService; 31 private IotDeviceMessageService deviceMessageService;
25 32
26 - // TODO @puhui999:这里  
27 @Override 33 @Override
28 public void execute(IotDeviceMessage message, 34 public void execute(IotDeviceMessage message,
29 IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) { 35 IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
30 - //IotSceneRuleDO.ActionDeviceControl control = actionConfig.getDeviceControl();  
31 - //Assert.notNull(control, "设备控制配置不能为空");  
32 - //// 遍历每个设备,下发消息  
33 - //control.getDeviceNames().forEach(deviceName -> {  
34 - // IotDeviceDO device = deviceService.getDeviceFromCache(control.getProductKey(), deviceName);  
35 - // if (device == null) {  
36 - // log.error("[execute][message({}) actionConfig({}) 对应的设备不存在]", message, actionConfig);  
37 - // return;  
38 - // }  
39 - // try {  
40 - // // TODO @芋艿:@puhui999:这块可能要改,从 type => method  
41 - // IotDeviceMessage downstreamMessage = deviceMessageService.sendDeviceMessage(IotDeviceMessage.requestOf(  
42 - // control.getType() + control.getIdentifier(), control.getData()).setDeviceId(device.getId()));  
43 - // log.info("[execute][message({}) actionConfig({}) 下发消息({})成功]", message, actionConfig, downstreamMessage);  
44 - // } catch (Exception e) {  
45 - // log.error("[execute][message({}) actionConfig({}) 下发消息失败]", message, actionConfig, e);  
46 - // }  
47 - //}); 36 + // 1. 参数校验
  37 + if (actionConfig.getDeviceId() == null) {
  38 + log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
  39 + return;
  40 + }
  41 + if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
  42 + log.error("[execute][规则场景({}) 动作配置({}) 属性标识符不能为空]", rule.getId(), actionConfig);
  43 + return;
  44 + }
  45 +
  46 + // 2. 判断是否为全部设备
  47 + if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
  48 + executeForAllDevices(message, rule, actionConfig);
  49 + } else {
  50 + executeForSingleDevice(message, rule, actionConfig);
  51 + }
  52 + }
  53 +
  54 + /**
  55 + * 为单个设备执行属性设置
  56 + */
  57 + private void executeForSingleDevice(IotDeviceMessage message,
  58 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  59 + // 1. 获取设备信息
  60 + IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
  61 + if (device == null) {
  62 + log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
  63 + rule.getId(), actionConfig, actionConfig.getDeviceId());
  64 + return;
  65 + }
  66 +
  67 + // 2. 执行属性设置
  68 + executePropertySetForDevice(rule, actionConfig, device);
  69 + }
  70 +
  71 + /**
  72 + * 为产品下的所有设备执行属性设置
  73 + */
  74 + private void executeForAllDevices(IotDeviceMessage message,
  75 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  76 + // 1. 参数校验
  77 + if (actionConfig.getProductId() == null) {
  78 + log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
  79 + return;
  80 + }
  81 +
  82 + // 2. 获取产品下的所有设备
  83 + List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
  84 + if (CollUtil.isEmpty(devices)) {
  85 + log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
  86 + rule.getId(), actionConfig, actionConfig.getProductId());
  87 + return;
  88 + }
  89 +
  90 + // 3. 遍历所有设备执行属性设置
  91 + for (IotDeviceDO device : devices) {
  92 + executePropertySetForDevice(rule, actionConfig, device);
  93 + }
  94 + }
  95 +
  96 + /**
  97 + * 为指定设备执行属性设置
  98 + */
  99 + private void executePropertySetForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  100 + // 1. 构建属性设置消息
  101 + IotDeviceMessage downstreamMessage = buildPropertySetMessage(actionConfig, device);
  102 + if (downstreamMessage == null) {
  103 + log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 构建属性设置消息失败]",
  104 + rule.getId(), actionConfig, device.getId());
  105 + return;
  106 + }
  107 +
  108 + // 2. 发送设备消息
  109 + try {
  110 + IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
  111 + log.info("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息({}) 发送成功]",
  112 + rule.getId(), actionConfig, device.getId(), result.getId());
  113 + } catch (Exception e) {
  114 + log.error("[executePropertySetForDevice][规则场景({}) 动作配置({}) 设备({}) 属性设置消息发送失败]",
  115 + rule.getId(), actionConfig, device.getId(), e);
  116 + }
  117 + }
  118 +
  119 + /**
  120 + * 构建属性设置消息
  121 + *
  122 + * @param actionConfig 动作配置
  123 + * @param device 设备信息
  124 + * @return 设备消息
  125 + */
  126 + private IotDeviceMessage buildPropertySetMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  127 + try {
  128 + // 属性设置参数格式: {"properties": {"identifier": value}}
  129 + Object params = Map.of("properties", Map.of(actionConfig.getIdentifier(), actionConfig.getParams()));
  130 + return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.PROPERTY_SET.getMethod(), params);
  131 + } catch (Exception e) {
  132 + log.error("[buildPropertySetMessage][构建属性设置消息异常]", e);
  133 + return null;
  134 + }
48 } 135 }
49 136
50 @Override 137 @Override
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/action/IotDeviceServiceInvokeSceneRuleAction.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.action;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  6 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  8 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  9 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
  10 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
  11 +import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
  12 +import jakarta.annotation.Resource;
  13 +import lombok.extern.slf4j.Slf4j;
  14 +import org.springframework.stereotype.Component;
  15 +
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +
  19 +/**
  20 + * IoT 设备服务调用的 {@link IotSceneRuleAction} 实现类
  21 + *
  22 + * @author HUIHUI
  23 + */
  24 +@Component
  25 +@Slf4j
  26 +public class IotDeviceServiceInvokeSceneRuleAction implements IotSceneRuleAction {
  27 +
  28 + @Resource
  29 + private IotDeviceService deviceService;
  30 + @Resource
  31 + private IotDeviceMessageService deviceMessageService;
  32 +
  33 + @Override
  34 + public void execute(IotDeviceMessage message,
  35 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  36 + // 1. 参数校验
  37 + if (actionConfig.getDeviceId() == null) {
  38 + log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
  39 + return;
  40 + }
  41 + if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
  42 + log.error("[execute][规则场景({}) 动作配置({}) 服务标识符不能为空]", rule.getId(), actionConfig);
  43 + return;
  44 + }
  45 +
  46 + // 2. 判断是否为全部设备
  47 + if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
  48 + executeForAllDevices(message, rule, actionConfig);
  49 + } else {
  50 + executeForSingleDevice(message, rule, actionConfig);
  51 + }
  52 + }
  53 +
  54 + /**
  55 + * 为单个设备执行服务调用
  56 + */
  57 + private void executeForSingleDevice(IotDeviceMessage message,
  58 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  59 + // 1. 获取设备信息
  60 + IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
  61 + if (device == null) {
  62 + log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
  63 + rule.getId(), actionConfig, actionConfig.getDeviceId());
  64 + return;
  65 + }
  66 +
  67 + // 2. 执行服务调用
  68 + executeServiceInvokeForDevice(rule, actionConfig, device);
  69 + }
  70 +
  71 + /**
  72 + * 为产品下的所有设备执行服务调用
  73 + */
  74 + private void executeForAllDevices(IotDeviceMessage message,
  75 + IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
  76 + // 1. 参数校验
  77 + if (actionConfig.getProductId() == null) {
  78 + log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
  79 + return;
  80 + }
  81 +
  82 + // 2. 获取产品下的所有设备
  83 + List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
  84 + if (CollUtil.isEmpty(devices)) {
  85 + log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
  86 + rule.getId(), actionConfig, actionConfig.getProductId());
  87 + return;
  88 + }
  89 +
  90 + // 3. 遍历所有设备执行服务调用
  91 + for (IotDeviceDO device : devices) {
  92 + executeServiceInvokeForDevice(rule, actionConfig, device);
  93 + }
  94 + }
  95 +
  96 + /**
  97 + * 为指定设备执行服务调用
  98 + */
  99 + private void executeServiceInvokeForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  100 + // 1. 构建服务调用消息
  101 + IotDeviceMessage downstreamMessage = buildServiceInvokeMessage(actionConfig, device);
  102 + if (downstreamMessage == null) {
  103 + log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 构建服务调用消息失败]",
  104 + rule.getId(), actionConfig, device.getId());
  105 + return;
  106 + }
  107 +
  108 + // 2. 发送设备消息
  109 + try {
  110 + IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
  111 + log.info("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息({}) 发送成功]",
  112 + rule.getId(), actionConfig, device.getId(), result.getId());
  113 + } catch (Exception e) {
  114 + log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息发送失败]",
  115 + rule.getId(), actionConfig, device.getId(), e);
  116 + }
  117 + }
  118 +
  119 + /**
  120 + * 构建服务调用消息
  121 + *
  122 + * @param actionConfig 动作配置
  123 + * @param device 设备信息
  124 + * @return 设备消息
  125 + */
  126 + private IotDeviceMessage buildServiceInvokeMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
  127 + try {
  128 + // 服务调用参数格式: {"identifier": "serviceId", "params": {...}}
  129 + Object params = Map.of(
  130 + "identifier", actionConfig.getIdentifier(),
  131 + "params", actionConfig.getParams() != null ? actionConfig.getParams() : Map.of()
  132 + );
  133 + return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), params);
  134 + } catch (Exception e) {
  135 + log.error("[buildServiceInvokeMessage][构建服务调用消息异常]", e);
  136 + return null;
  137 + }
  138 + }
  139 +
  140 + @Override
  141 + public IotSceneRuleActionTypeEnum getType() {
  142 + return IotSceneRuleActionTypeEnum.DEVICE_SERVICE_INVOKE;
  143 + }
  144 +
  145 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java
@@ -4,10 +4,8 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotScene @@ -4,10 +4,8 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotScene
4 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger.IotSceneRuleTriggerMatcher; 4 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger.IotSceneRuleTriggerMatcher;
5 5
6 /** 6 /**
7 - * IoT 场景规则匹配器基础接口  
8 - * <p>  
9 - * 定义所有匹配器的通用行为,包括优先级、名称和启用状态  
10 - * <p> 7 + * IoT 场景规则匹配器基础接口:定义所有匹配器的通用行为,包括优先级、名称和启用状态
  8 + *
11 * - {@link IotSceneRuleTriggerMatcher} 触发器匹配器 9 * - {@link IotSceneRuleTriggerMatcher} 触发器匹配器
12 * - {@link IotSceneRuleConditionMatcher} 条件匹配器 10 * - {@link IotSceneRuleConditionMatcher} 条件匹配器
13 * 11 *
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java
@@ -18,10 +18,8 @@ import java.util.Map; @@ -18,10 +18,8 @@ import java.util.Map;
18 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; 18 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
19 19
20 /** 20 /**
21 - * IoT 场景规则匹配器工具类  
22 - * <p>  
23 - * 提供通用的条件评估逻辑和工具方法,供触发器和条件匹配器使用  
24 - * <p> 21 + * IoT 场景规则匹配器工具类:提供通用的条件评估逻辑和工具方法,供触发器和条件匹配器使用
  22 + *
25 * 该类包含了匹配器实现中常用的工具方法,如条件评估、参数校验、日志记录等 23 * 该类包含了匹配器实现中常用的工具方法,如条件评估、参数校验、日志记录等
26 * 24 *
27 * @author HUIHUI 25 * @author HUIHUI
@@ -91,7 +89,7 @@ public final class IotSceneRuleMatcherHelper { @@ -91,7 +89,7 @@ public final class IotSceneRuleMatcherHelper {
91 Map<String, Object> springExpressionVariables = new HashMap<>(); 89 Map<String, Object> springExpressionVariables = new HashMap<>();
92 90
93 // 设置源值 91 // 设置源值
94 - springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue); 92 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, StrUtil.toString(sourceValue));
95 93
96 // 处理参数值 94 // 处理参数值
97 if (StrUtil.isNotBlank(paramValue)) { 95 if (StrUtil.isNotBlank(paramValue)) {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java
@@ -16,9 +16,7 @@ import java.util.function.Function; @@ -16,9 +16,7 @@ import java.util.function.Function;
16 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; 16 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
17 17
18 /** 18 /**
19 - * IoT 场景规则匹配器统一管理器  
20 - * <p>  
21 - * 负责管理所有匹配器(触发器匹配器和条件匹配器),并提供统一的匹配入口 19 + * IoT 场景规则匹配器统一管理器:负责管理所有匹配器(触发器匹配器和条件匹配器),并提供统一的匹配入口
22 * 20 *
23 * @author HUIHUI 21 * @author HUIHUI
24 */ 22 */
@@ -44,13 +42,12 @@ public class IotSceneRuleMatcherManager { @@ -44,13 +42,12 @@ public class IotSceneRuleMatcherManager {
44 return; 42 return;
45 } 43 }
46 44
47 - // 按优先级排序并过滤启用的匹配器 45 + // 1.1 按优先级排序并过滤启用的匹配器
48 List<IotSceneRuleMatcher> allMatchers = matchers.stream() 46 List<IotSceneRuleMatcher> allMatchers = matchers.stream()
49 .filter(IotSceneRuleMatcher::isEnabled) 47 .filter(IotSceneRuleMatcher::isEnabled)
50 .sorted(Comparator.comparing(IotSceneRuleMatcher::getPriority)) 48 .sorted(Comparator.comparing(IotSceneRuleMatcher::getPriority))
51 .toList(); 49 .toList();
52 -  
53 - // 分离触发器匹配器和条件匹配器 50 + // 1.2 分离触发器匹配器和条件匹配器
54 List<IotSceneRuleTriggerMatcher> triggerMatchers = allMatchers.stream() 51 List<IotSceneRuleTriggerMatcher> triggerMatchers = allMatchers.stream()
55 .filter(matcher -> matcher instanceof IotSceneRuleTriggerMatcher) 52 .filter(matcher -> matcher instanceof IotSceneRuleTriggerMatcher)
56 .map(matcher -> (IotSceneRuleTriggerMatcher) matcher) 53 .map(matcher -> (IotSceneRuleTriggerMatcher) matcher)
@@ -60,7 +57,7 @@ public class IotSceneRuleMatcherManager { @@ -60,7 +57,7 @@ public class IotSceneRuleMatcherManager {
60 .map(matcher -> (IotSceneRuleConditionMatcher) matcher) 57 .map(matcher -> (IotSceneRuleConditionMatcher) matcher)
61 .toList(); 58 .toList();
62 59
63 - // 构建触发器匹配器映射表 60 + // 2.1 构建触发器匹配器映射表
64 this.triggerMatchers = convertMap(triggerMatchers, IotSceneRuleTriggerMatcher::getSupportedTriggerType, 61 this.triggerMatchers = convertMap(triggerMatchers, IotSceneRuleTriggerMatcher::getSupportedTriggerType,
65 Function.identity(), 62 Function.identity(),
66 (existing, replacement) -> { 63 (existing, replacement) -> {
@@ -70,7 +67,7 @@ public class IotSceneRuleMatcherManager { @@ -70,7 +67,7 @@ public class IotSceneRuleMatcherManager {
70 existing.getSupportedTriggerType() : replacement.getSupportedTriggerType()); 67 existing.getSupportedTriggerType() : replacement.getSupportedTriggerType());
71 return existing.getPriority() <= replacement.getPriority() ? existing : replacement; 68 return existing.getPriority() <= replacement.getPriority() ? existing : replacement;
72 }, LinkedHashMap::new); 69 }, LinkedHashMap::new);
73 - // 构建条件匹配器映射表 70 + // 2.2 构建条件匹配器映射表
74 this.conditionMatchers = convertMap(conditionMatchers, IotSceneRuleConditionMatcher::getSupportedConditionType, 71 this.conditionMatchers = convertMap(conditionMatchers, IotSceneRuleConditionMatcher::getSupportedConditionType,
75 Function.identity(), 72 Function.identity(),
76 (existing, replacement) -> { 73 (existing, replacement) -> {
@@ -82,7 +79,7 @@ public class IotSceneRuleMatcherManager { @@ -82,7 +79,7 @@ public class IotSceneRuleMatcherManager {
82 }, 79 },
83 LinkedHashMap::new); 80 LinkedHashMap::new);
84 81
85 - // 日志输出初始化信息 82 + // 3. 日志输出初始化信息
86 log.info("[IotSceneRuleMatcherManager][初始化完成,共加载({})个匹配器,其中触发器匹配器({})个,条件匹配器({})个]", 83 log.info("[IotSceneRuleMatcherManager][初始化完成,共加载({})个匹配器,其中触发器匹配器({})个,条件匹配器({})个]",
87 allMatchers.size(), this.triggerMatchers.size(), this.conditionMatchers.size()); 84 allMatchers.size(), this.triggerMatchers.size(), this.conditionMatchers.size());
88 this.triggerMatchers.forEach((type, matcher) -> 85 this.triggerMatchers.forEach((type, matcher) ->
@@ -135,7 +132,7 @@ public class IotSceneRuleMatcherManager { @@ -135,7 +132,7 @@ public class IotSceneRuleMatcherManager {
135 return false; 132 return false;
136 } 133 }
137 134
138 - // 根据条件类型查找对应的匹配器 135 + // 1. 根据条件类型查找对应的匹配器
139 IotSceneRuleConditionTypeEnum conditionType = IotSceneRuleConditionTypeEnum.typeOf(condition.getType()); 136 IotSceneRuleConditionTypeEnum conditionType = IotSceneRuleConditionTypeEnum.typeOf(condition.getType());
140 if (conditionType == null) { 137 if (conditionType == null) {
141 log.warn("[isConditionMatched][conditionType({}) 未知的条件类型]", condition.getType()); 138 log.warn("[isConditionMatched][conditionType({}) 未知的条件类型]", condition.getType());
@@ -147,7 +144,7 @@ public class IotSceneRuleMatcherManager { @@ -147,7 +144,7 @@ public class IotSceneRuleMatcherManager {
147 return false; 144 return false;
148 } 145 }
149 146
150 - // 执行匹配逻辑 147 + // 2. 执行匹配逻辑
151 try { 148 try {
152 return matcher.matches(message, condition); 149 return matcher.matches(message, condition);
153 } catch (Exception e) { 150 } catch (Exception e) {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java
@@ -17,15 +17,13 @@ import java.time.format.DateTimeFormatter; @@ -17,15 +17,13 @@ import java.time.format.DateTimeFormatter;
17 import java.util.List; 17 import java.util.List;
18 18
19 /** 19 /**
20 - * 当前时间条件匹配器  
21 - * <p>  
22 - * 处理时间相关的子条件匹配逻辑 20 + * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑
23 * 21 *
24 * @author HUIHUI 22 * @author HUIHUI
25 */ 23 */
26 @Component 24 @Component
27 @Slf4j 25 @Slf4j
28 -public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher { 26 +public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
29 27
30 /** 28 /**
31 * 时间格式化器 - HH:mm:ss 29 * 时间格式化器 - HH:mm:ss
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
  3 +
3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; 5 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
@@ -7,15 +8,14 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; @@ -7,15 +8,14 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
8 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
9 10
  11 +
10 /** 12 /**
11 - * 设备属性条件匹配器  
12 - * <p>  
13 - * 处理设备属性相关的子条件匹配逻辑 13 + * 设备属性条件匹配器:处理设备属性相关的子条件匹配逻辑
14 * 14 *
15 * @author HUIHUI 15 * @author HUIHUI
16 */ 16 */
17 @Component 17 @Component
18 -public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher { 18 +public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher {
19 19
20 @Override 20 @Override
21 public IotSceneRuleConditionTypeEnum getSupportedConditionType() { 21 public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
@@ -43,10 +43,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc @@ -43,10 +43,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc
43 return false; 43 return false;
44 } 44 }
45 45
46 - // 2.1. 获取属性值  
47 - Object propertyValue = message.getParams(); 46 + // 2.1. 获取属性值 - 使用工具类方法正确提取属性值
  47 + Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, condition.getIdentifier());
48 if (propertyValue == null) { 48 if (propertyValue == null) {
49 - IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空"); 49 + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空或未找到指定属性");
50 return false; 50 return false;
51 } 51 }
52 52
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
7 import org.springframework.stereotype.Component; 8 import org.springframework.stereotype.Component;
8 9
9 /** 10 /**
10 - * 设备状态条件匹配器  
11 - * <p>  
12 - * 处理设备状态相关的子条件匹配逻辑 11 + * 设备状态条件匹配器:处理设备状态相关的子条件匹配逻辑
13 * 12 *
14 * @author HUIHUI 13 * @author HUIHUI
15 */ 14 */
16 @Component 15 @Component
17 -public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher { 16 +public class IotDeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
18 17
19 @Override 18 @Override
20 public IotSceneRuleConditionTypeEnum getSupportedConditionType() { 19 public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
@@ -35,8 +34,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher @@ -35,8 +34,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher
35 return false; 34 return false;
36 } 35 }
37 36
38 - // 2.1 获取设备状态值  
39 - Object stateValue = message.getParams(); 37 + // 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
  38 + // 对于设备状态条件,状态值通过 getIdentifier 获取(实际是从 params.state 字段)
  39 + String stateValue = IotDeviceMessageUtils.getIdentifier(message);
40 if (stateValue == null) { 40 if (stateValue == null) {
41 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空"); 41 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空");
42 return false; 42 return false;
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java
@@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; @@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher; 6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher;
7 7
8 /** 8 /**
9 - * IoT 场景规则条件匹配器接口  
10 - * <p>  
11 - * 专门处理子条件的匹配逻辑,如设备状态、属性值、时间条件等  
12 - * <p>  
13 - * 条件匹配器负责判断设备消息是否满足场景规则的附加条件,  
14 - * 在触发器匹配成功后进行进一步的条件筛选 9 + * IoT 场景规则条件匹配器接口:专门处理子条件的匹配逻辑,如设备状态、属性值、时间条件等
  10 + *
  11 + * 条件匹配器负责判断设备消息是否满足场景规则的附加条件,在触发器匹配成功后进行进一步的条件筛选
15 * 12 *
16 * @author HUIHUI 13 * @author HUIHUI
17 */ 14 */
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java
@@ -10,14 +10,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche @@ -10,14 +10,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
10 import org.springframework.stereotype.Component; 10 import org.springframework.stereotype.Component;
11 11
12 /** 12 /**
13 - * 设备事件上报触发器匹配器  
14 - * <p>  
15 - * 处理设备事件上报的触发器匹配逻辑 13 + * 设备事件上报触发器匹配器:处理设备事件上报的触发器匹配逻辑
16 * 14 *
17 * @author HUIHUI 15 * @author HUIHUI
18 */ 16 */
19 @Component 17 @Component
20 -public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher { 18 +public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
21 19
22 @Override 20 @Override
23 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { 21 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java
@@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche @@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
9 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
10 10
11 /** 11 /**
12 - * 设备属性上报触发器匹配器  
13 - * <p>  
14 - * 处理设备属性数据上报的触发器匹配逻辑 12 + * 设备属性上报触发器匹配器:处理设备属性数据上报的触发器匹配逻辑
15 * 13 *
16 * @author HUIHUI 14 * @author HUIHUI
17 */ 15 */
18 @Component 16 @Component
19 -public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher { 17 +public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
20 18
21 @Override 19 @Override
22 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -52,10 +50,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc @@ -52,10 +50,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc
52 return false; 50 return false;
53 } 51 }
54 52
55 - // 2.1 获取属性值  
56 - Object propertyValue = message.getParams(); 53 + // 2.1 获取属性值 - 使用工具类方法正确提取属性值
  54 + Object propertyValue = IotDeviceMessageUtils.extractPropertyValue(message, trigger.getIdentifier());
57 if (propertyValue == null) { 55 if (propertyValue == null) {
58 - IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空"); 56 + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空或未找到指定属性");
59 return false; 57 return false;
60 } 58 }
61 59
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java
@@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche @@ -9,14 +9,12 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
9 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
10 10
11 /** 11 /**
12 - * 设备服务调用触发器匹配器  
13 - * <p>  
14 - * 处理设备服务调用的触发器匹配逻辑 12 + * 设备服务调用触发器匹配器:处理设备服务调用的触发器匹配逻辑
15 * 13 *
16 * @author HUIHUI 14 * @author HUIHUI
17 */ 15 */
18 @Component 16 @Component
19 -public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher { 17 +public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
20 18
21 @Override 19 @Override
22 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -46,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat @@ -46,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat
46 44
47 // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可 45 // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可
48 // 不需要检查操作符和值,因为服务调用本身就是触发条件 46 // 不需要检查操作符和值,因为服务调用本身就是触发条件
49 - // TODO @puhui999: 服务调用时校验输入参数是否匹配条件 47 + // TODO @puhui999: 服务调用时校验输入参数是否匹配条件?
50 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); 48 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
51 return true; 49 return true;
52 } 50 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java
@@ -2,20 +2,19 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; @@ -2,20 +2,19 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
8 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
9 10
10 /** 11 /**
11 - * 设备状态更新触发器匹配器  
12 - * <p>  
13 - * 处理设备上下线状态变更的触发器匹配逻辑 12 + * 设备状态更新触发器匹配器:处理设备上下线状态变更的触发器匹配逻辑
14 * 13 *
15 * @author HUIHUI 14 * @author HUIHUI
16 */ 15 */
17 @Component 16 @Component
18 -public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher { 17 +public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
19 18
20 @Override 19 @Override
21 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
@@ -43,16 +42,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch @@ -43,16 +42,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch
43 return false; 42 return false;
44 } 43 }
45 44
46 - // 2.1 获取设备状态值  
47 - Object stateValue = message.getParams();  
48 - if (stateValue == null) { 45 + // 2.1 获取设备状态值 - 使用工具类方法正确提取状态值
  46 + // 对于状态更新消息,状态值通过 getIdentifier 获取(实际是从 params.state 字段)
  47 + String stateIdentifier = IotDeviceMessageUtils.getIdentifier(message);
  48 + if (stateIdentifier == null) {
49 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空"); 49 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空");
50 return false; 50 return false;
51 } 51 }
52 52
53 // 2.2 使用条件评估器进行匹配 53 // 2.2 使用条件评估器进行匹配
54 - // TODO @puhui999: 状态匹配重新实现  
55 - boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue()); 54 + // 状态值通常是字符串或数字,直接使用标识符作为状态值
  55 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateIdentifier, trigger.getOperator(), trigger.getValue());
56 if (matched) { 56 if (matched) {
57 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); 57 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
58 } else { 58 } else {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java
@@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; @@ -6,12 +6,9 @@ import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher; 6 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher;
7 7
8 /** 8 /**
9 - * IoT 场景规则触发器匹配器接口  
10 - * <p>  
11 - * 专门处理主触发条件的匹配逻辑,如设备消息类型、定时器等  
12 - * <p>  
13 - * 触发器匹配器负责判断设备消息是否满足场景规则的主触发条件,  
14 - * 是场景规则执行的第一道门槛 9 + * IoT 场景规则触发器匹配器接口:专门处理主触发条件的匹配逻辑,如设备消息类型、定时器等
  10 + *
  11 + * 触发器匹配器负责判断设备消息是否满足场景规则的主触发条件,是场景规则执行的第一道门槛
15 * 12 *
16 * @author HUIHUI 13 * @author HUIHUI
17 */ 14 */
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java renamed to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcher.java
@@ -9,15 +9,14 @@ import org.quartz.CronExpression; @@ -9,15 +9,14 @@ import org.quartz.CronExpression;
9 import org.springframework.stereotype.Component; 9 import org.springframework.stereotype.Component;
10 10
11 /** 11 /**
12 - * 定时触发器匹配器  
13 - * <p>  
14 - * 处理定时触发的触发器匹配逻辑 12 + * 定时触发器匹配器:处理定时触发的触发器匹配逻辑
  13 + *
15 * 注意:定时触发器不依赖设备消息,主要用于定时任务场景 14 * 注意:定时触发器不依赖设备消息,主要用于定时任务场景
16 * 15 *
17 * @author HUIHUI 16 * @author HUIHUI
18 */ 17 */
19 @Component 18 @Component
20 -public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher { 19 +public class IotTimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
21 20
22 @Override 21 @Override
23 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { 22 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.ObjUtil;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  8 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
  10 +import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
  11 +import jakarta.annotation.Resource;
  12 +import lombok.extern.slf4j.Slf4j;
  13 +import org.quartz.SchedulerException;
  14 +import org.springframework.stereotype.Component;
  15 +
  16 +import java.util.List;
  17 +
  18 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
  19 +
  20 +/**
  21 + * IoT 场景规则定时触发器处理器:负责管理定时触发器的注册、更新、删除等操作
  22 + *
  23 + * @author HUIHUI
  24 + */
  25 +@Component
  26 +@Slf4j
  27 +public class IotSceneRuleTimerHandler {
  28 +
  29 + @Resource(name = "iotSchedulerManager")
  30 + private IotSchedulerManager schedulerManager;
  31 +
  32 + /**
  33 + * 注册场景规则的定时触发器
  34 + *
  35 + * @param sceneRule 场景规则
  36 + */
  37 + public void registerTimerTriggers(IotSceneRuleDO sceneRule) {
  38 + // 1. 过滤出定时触发器
  39 + if (sceneRule == null || CollUtil.isEmpty(sceneRule.getTriggers())) {
  40 + return;
  41 + }
  42 + List<IotSceneRuleDO.Trigger> timerTriggers = filterList(sceneRule.getTriggers(),
  43 + trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType()));
  44 + if (CollUtil.isEmpty(timerTriggers)) {
  45 + return;
  46 + }
  47 +
  48 + // 2. 注册每个定时触发器
  49 + timerTriggers.forEach(trigger -> registerSingleTimerTrigger(sceneRule, trigger));
  50 + }
  51 +
  52 + /**
  53 + * 更新场景规则的定时触发器
  54 + *
  55 + * @param sceneRule 场景规则
  56 + */
  57 + public void updateTimerTriggers(IotSceneRuleDO sceneRule) {
  58 + if (sceneRule == null) {
  59 + return;
  60 + }
  61 +
  62 + // 1. 先删除旧的定时任务
  63 + unregisterTimerTriggers(sceneRule.getId());
  64 +
  65 + // 2.1 如果场景规则已禁用,则不重新注册
  66 + if (CommonStatusEnum.isDisable(sceneRule.getStatus())) {
  67 + log.info("[updateTimerTriggers][场景规则({}) 已禁用,不注册定时触发器]", sceneRule.getId());
  68 + return;
  69 + }
  70 +
  71 + // 2.2 重新注册定时触发器
  72 + registerTimerTriggers(sceneRule);
  73 + }
  74 +
  75 + /**
  76 + * 注销场景规则的定时触发器
  77 + *
  78 + * @param sceneRuleId 场景规则 ID
  79 + */
  80 + public void unregisterTimerTriggers(Long sceneRuleId) {
  81 + if (sceneRuleId == null) {
  82 + return;
  83 + }
  84 +
  85 + String jobName = buildJobName(sceneRuleId);
  86 + try {
  87 + schedulerManager.deleteJob(jobName);
  88 + log.info("[unregisterTimerTriggers][场景规则({}) 定时触发器注销成功]", sceneRuleId);
  89 + } catch (SchedulerException e) {
  90 + log.error("[unregisterTimerTriggers][场景规则({}) 定时触发器注销失败]", sceneRuleId, e);
  91 + }
  92 + }
  93 +
  94 + /**
  95 + * 暂停场景规则的定时触发器
  96 + *
  97 + * @param sceneRuleId 场景规则 ID
  98 + */
  99 + public void pauseTimerTriggers(Long sceneRuleId) {
  100 + if (sceneRuleId == null) {
  101 + return;
  102 + }
  103 +
  104 + String jobName = buildJobName(sceneRuleId);
  105 + try {
  106 + schedulerManager.pauseJob(jobName);
  107 + log.info("[pauseTimerTriggers][场景规则({}) 定时触发器暂停成功]", sceneRuleId);
  108 + } catch (SchedulerException e) {
  109 + log.error("[pauseTimerTriggers][场景规则({}) 定时触发器暂停失败]", sceneRuleId, e);
  110 + }
  111 + }
  112 +
  113 + /**
  114 + * 注册单个定时触发器
  115 + *
  116 + * @param sceneRule 场景规则
  117 + * @param trigger 定时触发器配置
  118 + */
  119 + private void registerSingleTimerTrigger(IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) {
  120 + // 1. 参数校验
  121 + if (StrUtil.isBlank(trigger.getCronExpression())) {
  122 + log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器缺少 CRON 表达式]", sceneRule.getId());
  123 + return;
  124 + }
  125 +
  126 + try {
  127 + // 2.1 构建任务名称和数据
  128 + String jobName = buildJobName(sceneRule.getId());
  129 + // 2.2 注册定时任务
  130 + schedulerManager.addOrUpdateJob(
  131 + IotSceneRuleJob.class,
  132 + jobName,
  133 + trigger.getCronExpression(),
  134 + IotSceneRuleJob.buildJobDataMap(sceneRule.getId())
  135 + );
  136 + log.info("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册成功,CRON: {}]",
  137 + sceneRule.getId(), trigger.getCronExpression());
  138 + } catch (SchedulerException e) {
  139 + log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册失败,CRON: {}]",
  140 + sceneRule.getId(), trigger.getCronExpression(), e);
  141 + }
  142 + }
  143 +
  144 + /**
  145 + * 构建任务名称
  146 + *
  147 + * @param sceneRuleId 场景规则 ID
  148 + * @return 任务名称
  149 + */
  150 + private String buildJobName(Long sceneRuleId) {
  151 + return "iot_scene_rule_timer_" + sceneRuleId;
  152 + }
  153 +
  154 +}
yudao-module-iot/yudao-module-iot-biz/src/main/resources/mapper/device/IotDevicePropertyMapper.xml
@@ -72,7 +72,7 @@ @@ -72,7 +72,7 @@
72 FROM device_property_${reqVO.deviceId} 72 FROM device_property_${reqVO.deviceId}
73 WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL 73 WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL
74 AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])} 74 AND ts BETWEEN ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[0])}
75 - AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])} 75 + AND ${@cn.hutool.core.date.LocalDateTimeUtil@toEpochMilli(reqVO.times[1])}
76 ORDER BY ts DESC 76 ORDER BY ts DESC
77 </select> 77 </select>
78 78
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.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.IotDataSinkTcpConfig;
  6 +import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
  7 +import org.junit.jupiter.api.BeforeEach;
  8 +import org.junit.jupiter.api.Test;
  9 +import org.mockito.Mock;
  10 +import org.mockito.MockitoAnnotations;
  11 +
  12 +import static org.junit.jupiter.api.Assertions.*;
  13 +import static org.mockito.Mockito.*;
  14 +
  15 +/**
  16 + * {@link IotTcpDataRuleAction} 的单元测试
  17 + *
  18 + * @author HUIHUI
  19 + */
  20 +class IotTcpDataRuleActionTest {
  21 +
  22 + private IotTcpDataRuleAction tcpDataRuleAction;
  23 +
  24 + @Mock
  25 + private IotTcpClient mockTcpClient;
  26 +
  27 + @BeforeEach
  28 + public void setUp() {
  29 + MockitoAnnotations.openMocks(this);
  30 + tcpDataRuleAction = new IotTcpDataRuleAction();
  31 + }
  32 +
  33 + @Test
  34 + public void testGetType() {
  35 + // 准备参数
  36 + Integer expectedType = 2; // 数据接收类型枚举中 TCP 类型的值
  37 +
  38 + // 调用方法
  39 + Integer actualType = tcpDataRuleAction.getType();
  40 +
  41 + // 断言结果
  42 + assertEquals(expectedType, actualType);
  43 + }
  44 +
  45 + // TODO @puhui999:_ 后面是小写哈,单测的命名规则。
  46 + @Test
  47 + public void testInitProducer_Success() throws Exception {
  48 + // 准备参数
  49 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  50 + config.setHost("localhost");
  51 + config.setPort(8080);
  52 + config.setDataFormat("JSON");
  53 + config.setSsl(false);
  54 +
  55 + // 调用方法 & 断言结果
  56 + // 此测试需要实际的 TCP 连接,在单元测试中可能不可用
  57 + // 目前我们只验证配置是否有效
  58 + assertNotNull(config.getHost());
  59 + assertTrue(config.getPort() > 0 && config.getPort() <= 65535);
  60 + }
  61 +
  62 + @Test
  63 + public void testInitProducer_InvalidHost() {
  64 + // 准备参数
  65 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  66 + config.setHost("");
  67 + config.setPort(8080);
  68 +
  69 + // 调用方法 & 断言结果
  70 + IotTcpDataRuleAction action = new IotTcpDataRuleAction();
  71 +
  72 + // 测试验证逻辑(通常在 initProducer 方法中)
  73 + assertThrows(IllegalArgumentException.class, () -> {
  74 + if (config.getHost() == null || config.getHost().trim().isEmpty()) {
  75 + throw new IllegalArgumentException("TCP 服务器地址不能为空");
  76 + }
  77 + });
  78 + }
  79 +
  80 + @Test
  81 + public void testInitProducer_InvalidPort() {
  82 + // 准备参数
  83 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  84 + config.setHost("localhost");
  85 + config.setPort(-1);
  86 +
  87 + // 调用方法 & 断言结果
  88 + assertThrows(IllegalArgumentException.class, () -> {
  89 + if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
  90 + throw new IllegalArgumentException("TCP 服务器端口无效");
  91 + }
  92 + });
  93 + }
  94 +
  95 + @Test
  96 + public void testCloseProducer() throws Exception {
  97 + // 准备参数
  98 + IotTcpClient client = mock(IotTcpClient.class);
  99 +
  100 + // 调用方法
  101 + tcpDataRuleAction.closeProducer(client);
  102 +
  103 + // 断言结果
  104 + verify(client, times(1)).close();
  105 + }
  106 +
  107 + @Test
  108 + public void testExecute_WithValidConfig() {
  109 + // 准备参数
  110 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report",
  111 + "{\"temperature\": 25.5, \"humidity\": 60}");
  112 +
  113 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  114 + config.setHost("localhost");
  115 + config.setPort(8080);
  116 + config.setDataFormat("JSON");
  117 +
  118 + // 调用方法 & 断言结果
  119 + // 通常这需要实际的 TCP 连接
  120 + // 在单元测试中,我们只验证输入参数
  121 + assertNotNull(message);
  122 + assertNotNull(config);
  123 + assertNotNull(config.getHost());
  124 + assertTrue(config.getPort() > 0);
  125 + }
  126 +
  127 + @Test
  128 + public void testConfig_DefaultValues() {
  129 + // 准备参数
  130 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  131 +
  132 + // 调用方法 & 断言结果
  133 + // 验证默认值
  134 + assertEquals("JSON", config.getDataFormat());
  135 + assertEquals(5000, config.getConnectTimeoutMs());
  136 + assertEquals(10000, config.getReadTimeoutMs());
  137 + assertEquals(false, config.getSsl());
  138 + assertEquals(30000L, config.getHeartbeatIntervalMs());
  139 + assertEquals(5000L, config.getReconnectIntervalMs());
  140 + assertEquals(3, config.getMaxReconnectAttempts());
  141 + }
  142 +
  143 + @Test
  144 + public void testMessageSerialization() {
  145 + // 准备参数
  146 + IotDeviceMessage message = IotDeviceMessage.builder()
  147 + .deviceId(123L)
  148 + .method("thing.property.report")
  149 + .params("{\"temperature\": 25.5}")
  150 + .build();
  151 +
  152 + // 调用方法
  153 + String json = JsonUtils.toJsonString(message);
  154 +
  155 + // 断言结果
  156 + assertNotNull(json);
  157 + assertTrue(json.contains("\"deviceId\":123"));
  158 + assertTrue(json.contains("\"method\":\"thing.property.report\""));
  159 + assertTrue(json.contains("\"temperature\":25.5"));
  160 + }
  161 +
  162 +}
0 \ No newline at end of file 163 \ No newline at end of file
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
  5 +import org.springframework.context.annotation.Bean;
  6 +import org.springframework.context.annotation.Configuration;
  7 +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
  8 +
  9 +/**
  10 + * Matcher 测试基类
  11 + * 提供通用的 Spring 测试配置
  12 + *
  13 + * @author HUIHUI
  14 + */
  15 +@SpringJUnitConfig
  16 +public abstract class IotBaseConditionMatcherTest {
  17 +
  18 + /**
  19 + * 注入一下 SpringUtil,解析 EL 表达式时需要
  20 + * {@link SpringExpressionUtils#parseExpression}
  21 + */
  22 + @Configuration
  23 + static class TestConfig {
  24 +
  25 + @Bean
  26 + public SpringUtil springUtil() {
  27 + return new SpringUtil();
  28 + }
  29 +
  30 + }
  31 +
  32 +}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  7 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  8 +import org.junit.jupiter.api.BeforeEach;
8 import org.junit.jupiter.api.Test; 9 import org.junit.jupiter.api.Test;
9 -import org.mockito.InjectMocks;  
10 10
11 import java.time.LocalDateTime; 11 import java.time.LocalDateTime;
12 import java.time.ZoneOffset; 12 import java.time.ZoneOffset;
@@ -16,14 +16,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString @@ -16,14 +16,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
16 import static org.junit.jupiter.api.Assertions.*; 16 import static org.junit.jupiter.api.Assertions.*;
17 17
18 /** 18 /**
19 - * {@link CurrentTimeConditionMatcher} 的单元测试 19 + * {@link IotCurrentTimeConditionMatcher} 的单元测试
20 * 20 *
21 * @author HUIHUI 21 * @author HUIHUI
22 */ 22 */
23 -public class CurrentTimeConditionMatcherTest extends BaseMockitoUnitTest { 23 +public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest {
24 24
25 - @InjectMocks  
26 - private CurrentTimeConditionMatcher matcher; 25 + private IotCurrentTimeConditionMatcher matcher;
  26 +
  27 + @BeforeEach
  28 + public void setUp() {
  29 + matcher = new IotCurrentTimeConditionMatcher();
  30 + }
27 31
28 @Test 32 @Test
29 public void testGetSupportedConditionType() { 33 public void testGetSupportedConditionType() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 -import cn.hutool.core.map.MapUtil;  
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  7 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  8 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 9 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 10
12 import java.util.HashMap; 11 import java.util.HashMap;
13 import java.util.Map; 12 import java.util.Map;
14 13
15 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 14 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
16 -import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;  
17 import static org.junit.jupiter.api.Assertions.*; 15 import static org.junit.jupiter.api.Assertions.*;
18 16
19 /** 17 /**
20 - * {@link DevicePropertyConditionMatcher} 的单元测试 18 + * {@link IotDevicePropertyConditionMatcher} 的单元测试
21 * 19 *
22 * @author HUIHUI 20 * @author HUIHUI
23 */ 21 */
24 -public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { 22 +public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest {
25 23
26 - @InjectMocks  
27 - private DevicePropertyConditionMatcher matcher; 24 + private IotDevicePropertyConditionMatcher matcher;
  25 +
  26 + @BeforeEach
  27 + public void setUp() {
  28 + matcher = new IotDevicePropertyConditionMatcher();
  29 + }
28 30
29 @Test 31 @Test
30 public void testGetSupportedConditionType() { 32 public void testGetSupportedConditionType() {
@@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
41 int result = matcher.getPriority(); 43 int result = matcher.getPriority();
42 44
43 // 断言 45 // 断言
44 - assertEquals(20, result);  
45 - }  
46 -  
47 - @Test  
48 - public void testIsEnabled() {  
49 - // 调用  
50 - boolean result = matcher.isEnabled();  
51 -  
52 - // 断言  
53 - assertTrue(result); 46 + assertEquals(25, result); // 修正:实际返回值是 25
54 } 47 }
55 48
56 @Test 49 @Test
57 public void testMatches_temperatureEquals_success() { 50 public void testMatches_temperatureEquals_success() {
58 - // 准备参数  
59 - String propertyName = "temperature"; 51 + // 准备参数:创建属性上报消息
  52 + String propertyIdentifier = "temperature";
60 Double propertyValue = 25.5; 53 Double propertyValue = 25.5;
61 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
62 - IotDeviceMessage message = createDeviceMessage(properties); 54 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
63 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 55 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
64 - propertyName, 56 + propertyIdentifier,
65 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), 57 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
66 String.valueOf(propertyValue) 58 String.valueOf(propertyValue)
67 ); 59 );
@@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
75 67
76 @Test 68 @Test
77 public void testMatches_humidityGreaterThan_success() { 69 public void testMatches_humidityGreaterThan_success() {
78 - // 准备参数  
79 - String propertyName = "humidity"; 70 + // 准备参数:创建属性上报消息
  71 + String propertyIdentifier = "humidity";
80 Integer propertyValue = 75; 72 Integer propertyValue = 75;
81 Integer compareValue = 70; 73 Integer compareValue = 70;
82 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
83 - IotDeviceMessage message = createDeviceMessage(properties); 74 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
84 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 75 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
85 - propertyName, 76 + propertyIdentifier,
86 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), 77 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
87 String.valueOf(compareValue) 78 String.valueOf(compareValue)
88 ); 79 );
@@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
96 87
97 @Test 88 @Test
98 public void testMatches_pressureLessThan_success() { 89 public void testMatches_pressureLessThan_success() {
99 - // 准备参数  
100 - String propertyName = "pressure"; 90 + // 准备参数:创建属性上报消息
  91 + String propertyIdentifier = "pressure";
101 Double propertyValue = 1010.5; 92 Double propertyValue = 1010.5;
102 Integer compareValue = 1020; 93 Integer compareValue = 1020;
103 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
104 - IotDeviceMessage message = createDeviceMessage(properties); 94 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
105 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 95 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
106 - propertyName, 96 + propertyIdentifier,
107 IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), 97 IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(),
108 String.valueOf(compareValue) 98 String.valueOf(compareValue)
109 ); 99 );
@@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
117 107
118 @Test 108 @Test
119 public void testMatches_statusNotEquals_success() { 109 public void testMatches_statusNotEquals_success() {
120 - // 准备参数  
121 - String propertyName = "status"; 110 + // 准备参数:创建属性上报消息
  111 + String propertyIdentifier = "status";
122 String propertyValue = "active"; 112 String propertyValue = "active";
123 String compareValue = "inactive"; 113 String compareValue = "inactive";
124 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
125 - IotDeviceMessage message = createDeviceMessage(properties); 114 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
126 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 115 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
127 - propertyName, 116 + propertyIdentifier,
128 IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), 117 IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(),
129 compareValue 118 compareValue
130 ); 119 );
@@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
138 127
139 @Test 128 @Test
140 public void testMatches_propertyMismatch_fail() { 129 public void testMatches_propertyMismatch_fail() {
141 - // 准备参数  
142 - String propertyName = "temperature"; 130 + // 准备参数:创建属性上报消息,值不满足条件
  131 + String propertyIdentifier = "temperature";
143 Double propertyValue = 15.0; 132 Double propertyValue = 15.0;
144 Integer compareValue = 20; 133 Integer compareValue = 20;
145 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
146 - IotDeviceMessage message = createDeviceMessage(properties); 134 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
147 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 135 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
148 - propertyName, 136 + propertyIdentifier,
149 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), 137 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
150 String.valueOf(compareValue) 138 String.valueOf(compareValue)
151 ); 139 );
@@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
158 } 146 }
159 147
160 @Test 148 @Test
161 - public void testMatches_propertyNotFound_fail() {  
162 - // 准备参数  
163 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
164 - IotDeviceMessage message = createDeviceMessage(properties); 149 + public void testMatches_identifierMismatch_fail() {
  150 + // 准备参数:标识符不匹配
  151 + String messageIdentifier = "temperature";
  152 + String conditionIdentifier = "humidity";
  153 + Double propertyValue = 25.5;
  154 + IotDeviceMessage message = createPropertyPostMessage(messageIdentifier, propertyValue);
165 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 155 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
166 - randomString(), // 随机不存在的属性名  
167 - IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),  
168 - "50" 156 + conditionIdentifier,
  157 + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  158 + String.valueOf(propertyValue)
169 ); 159 );
170 160
171 // 调用 161 // 调用
@@ -178,8 +168,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -178,8 +168,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
178 @Test 168 @Test
179 public void testMatches_nullCondition_fail() { 169 public void testMatches_nullCondition_fail() {
180 // 准备参数 170 // 准备参数
181 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
182 - IotDeviceMessage message = createDeviceMessage(properties); 171 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
183 172
184 // 调用 173 // 调用
185 boolean result = matcher.matches(message, null); 174 boolean result = matcher.matches(message, null);
@@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
191 @Test 180 @Test
192 public void testMatches_nullConditionType_fail() { 181 public void testMatches_nullConditionType_fail() {
193 // 准备参数 182 // 准备参数
194 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
195 - IotDeviceMessage message = createDeviceMessage(properties); 183 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
196 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 184 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
197 condition.setType(null); 185 condition.setType(null);
198 186
@@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
206 @Test 194 @Test
207 public void testMatches_missingIdentifier_fail() { 195 public void testMatches_missingIdentifier_fail() {
208 // 准备参数 196 // 准备参数
209 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
210 - IotDeviceMessage message = createDeviceMessage(properties); 197 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
211 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 198 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
212 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); 199 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
213 condition.setIdentifier(null); // 缺少标识符 200 condition.setIdentifier(null); // 缺少标识符
@@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
224 @Test 211 @Test
225 public void testMatches_missingOperator_fail() { 212 public void testMatches_missingOperator_fail() {
226 // 准备参数 213 // 准备参数
227 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
228 - IotDeviceMessage message = createDeviceMessage(properties); 214 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
229 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 215 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
230 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); 216 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
231 condition.setIdentifier("temperature"); 217 condition.setIdentifier("temperature");
@@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
242 @Test 228 @Test
243 public void testMatches_missingParam_fail() { 229 public void testMatches_missingParam_fail() {
244 // 准备参数 230 // 准备参数
245 - Map<String, Object> properties = MapUtil.of("temperature", 25.5);  
246 - IotDeviceMessage message = createDeviceMessage(properties); 231 + IotDeviceMessage message = createPropertyPostMessage("temperature", 25.5);
247 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); 232 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
248 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); 233 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
249 condition.setIdentifier("temperature"); 234 condition.setIdentifier("temperature");
@@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
275 260
276 @Test 261 @Test
277 public void testMatches_nullDeviceProperties_fail() { 262 public void testMatches_nullDeviceProperties_fail() {
278 - // 准备参数 263 + // 准备参数:消息的 params 为 null
279 IotDeviceMessage message = new IotDeviceMessage(); 264 IotDeviceMessage message = new IotDeviceMessage();
280 message.setParams(null); 265 message.setParams(null);
281 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 266 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
@@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
292 } 277 }
293 278
294 @Test 279 @Test
  280 + public void testMatches_propertiesStructure_success() {
  281 + // 测试使用 properties 结构的消息(真实的属性上报场景)
  282 + String identifier = "temperature";
  283 + Double propertyValue = 25.5;
  284 + IotDeviceMessage message = createPropertyPostMessageWithProperties(identifier, propertyValue);
  285 + IotSceneRuleDO.TriggerCondition condition = createValidCondition(
  286 + identifier,
  287 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
  288 + "20"
  289 + );
  290 +
  291 + // 调用
  292 + boolean result = matcher.matches(message, condition);
  293 +
  294 + // 断言:修复后的实现应该能正确从 properties 中提取属性值
  295 + assertTrue(result);
  296 + }
  297 +
  298 + @Test
  299 + public void testMatches_simpleValueMessage_success() {
  300 + // 测试简单值消息(params 直接是属性值)
  301 + Double propertyValue = 25.5;
  302 + IotDeviceMessage message = createSimpleValueMessage(propertyValue);
  303 + IotSceneRuleDO.TriggerCondition condition = createValidCondition(
  304 + "any", // 对于简单值消息,标识符匹配会被跳过
  305 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
  306 + "20"
  307 + );
  308 +
  309 + // 调用
  310 + boolean result = matcher.matches(message, condition);
  311 +
  312 + // 断言:修复后的实现应该能处理简单值消息
  313 + // 但由于标识符匹配失败,结果为 false
  314 + assertFalse(result);
  315 + }
  316 +
  317 + @Test
  318 + public void testMatches_valueFieldStructure_success() {
  319 + // 测试使用 value 字段的消息结构
  320 + String identifier = "temperature";
  321 + Double propertyValue = 25.5;
  322 +
  323 + IotDeviceMessage message = new IotDeviceMessage();
  324 + message.setDeviceId(randomLongId());
  325 + message.setMethod("thing.event.post");
  326 +
  327 + Map<String, Object> params = new HashMap<>();
  328 + params.put("identifier", identifier);
  329 + params.put("value", propertyValue);
  330 + message.setParams(params);
  331 +
  332 + IotSceneRuleDO.TriggerCondition condition = createValidCondition(
  333 + identifier,
  334 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
  335 + "20"
  336 + );
  337 +
  338 + // 调用
  339 + boolean result = matcher.matches(message, condition);
  340 +
  341 + // 断言:修复后的实现应该能从 value 字段提取属性值
  342 + assertTrue(result);
  343 + }
  344 +
  345 + @Test
295 public void testMatches_voltageGreaterThanOrEquals_success() { 346 public void testMatches_voltageGreaterThanOrEquals_success() {
296 - // 准备参数  
297 - String propertyName = "voltage"; 347 + // 准备参数:创建属性上报消息
  348 + String propertyIdentifier = "voltage";
298 Double propertyValue = 12.0; 349 Double propertyValue = 12.0;
299 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
300 - IotDeviceMessage message = createDeviceMessage(properties); 350 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
301 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 351 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
302 - propertyName, 352 + propertyIdentifier,
303 IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(), 353 IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(),
304 String.valueOf(propertyValue) 354 String.valueOf(propertyValue)
305 ); 355 );
@@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
313 363
314 @Test 364 @Test
315 public void testMatches_currentLessThanOrEquals_success() { 365 public void testMatches_currentLessThanOrEquals_success() {
316 - // 准备参数  
317 - String propertyName = "current"; 366 + // 准备参数:创建属性上报消息
  367 + String propertyIdentifier = "current";
318 Double propertyValue = 2.5; 368 Double propertyValue = 2.5;
319 Double compareValue = 3.0; 369 Double compareValue = 3.0;
320 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
321 - IotDeviceMessage message = createDeviceMessage(properties); 370 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
322 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 371 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
323 - propertyName, 372 + propertyIdentifier,
324 IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(), 373 IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(),
325 String.valueOf(compareValue) 374 String.valueOf(compareValue)
326 ); 375 );
@@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
334 383
335 @Test 384 @Test
336 public void testMatches_stringProperty_success() { 385 public void testMatches_stringProperty_success() {
337 - // 准备参数  
338 - String propertyName = "mode"; 386 + // 准备参数:创建属性上报消息
  387 + String propertyIdentifier = "mode";
339 String propertyValue = "auto"; 388 String propertyValue = "auto";
340 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
341 - IotDeviceMessage message = createDeviceMessage(properties); 389 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
342 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 390 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
343 - propertyName, 391 + propertyIdentifier,
344 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), 392 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
345 propertyValue 393 propertyValue
346 ); 394 );
@@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
354 402
355 @Test 403 @Test
356 public void testMatches_booleanProperty_success() { 404 public void testMatches_booleanProperty_success() {
357 - // 准备参数  
358 - String propertyName = "enabled"; 405 + // 准备参数:创建属性上报消息
  406 + String propertyIdentifier = "enabled";
359 Boolean propertyValue = true; 407 Boolean propertyValue = true;
360 - Map<String, Object> properties = MapUtil.of(propertyName, propertyValue);  
361 - IotDeviceMessage message = createDeviceMessage(properties); 408 + IotDeviceMessage message = createPropertyPostMessage(propertyIdentifier, propertyValue);
362 IotSceneRuleDO.TriggerCondition condition = createValidCondition( 409 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
363 - propertyName, 410 + propertyIdentifier,
364 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), 411 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
365 String.valueOf(propertyValue) 412 String.valueOf(propertyValue)
366 ); 413 );
@@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { @@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
372 assertTrue(result); 419 assertTrue(result);
373 } 420 }
374 421
375 - @Test  
376 - public void testMatches_multipleProperties_success() {  
377 - // 准备参数  
378 - Map<String, Object> properties = MapUtil.builder(new HashMap<String, Object>())  
379 - .put("temperature", 25.5)  
380 - .put("humidity", 60)  
381 - .put("status", "active")  
382 - .put("enabled", true)  
383 - .build();  
384 - IotDeviceMessage message = createDeviceMessage(properties);  
385 - String targetProperty = "humidity";  
386 - Integer targetValue = 60;  
387 - IotSceneRuleDO.TriggerCondition condition = createValidCondition(  
388 - targetProperty,  
389 - IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),  
390 - String.valueOf(targetValue)  
391 - ); 422 + // ========== 辅助方法 ==========
392 423
393 - // 调用  
394 - boolean result = matcher.matches(message, condition); 424 + /**
  425 + * 创建设备消息用于测试
  426 + *
  427 + * 支持的消息格式:
  428 + * 1. 直接属性值:params 直接是属性值(适用于简单消息)
  429 + * 2. 标识符+值:params 包含 identifier 和对应的属性值
  430 + * 3. properties 结构:params.properties[identifier] = value
  431 + * 4. data 结构:params.data[identifier] = value
  432 + * 5. value 字段:params.value = value
  433 + */
  434 + private IotDeviceMessage createPropertyPostMessage(String identifier, Object value) {
  435 + IotDeviceMessage message = new IotDeviceMessage();
  436 + message.setDeviceId(randomLongId());
  437 + message.setMethod("thing.event.post"); // 使用事件上报方法
395 438
396 - // 断言  
397 - assertTrue(result); 439 + // 创建符合修复后逻辑的 params 结构
  440 + Map<String, Object> params = new HashMap<>();
  441 + params.put("identifier", identifier);
  442 + // 直接将属性值放在标识符对应的字段中
  443 + params.put(identifier, value);
  444 + message.setParams(params);
  445 +
  446 + return message;
398 } 447 }
399 448
400 - // ========== 辅助方法 ========== 449 + /**
  450 + * 创建使用 properties 结构的消息(模拟真实的属性上报消息)
  451 + */
  452 + private IotDeviceMessage createPropertyPostMessageWithProperties(String identifier, Object value) {
  453 + IotDeviceMessage message = new IotDeviceMessage();
  454 + message.setDeviceId(randomLongId());
  455 + message.setMethod("thing.property.post"); // 属性上报方法
  456 +
  457 + Map<String, Object> properties = new HashMap<>();
  458 + properties.put(identifier, value);
  459 +
  460 + Map<String, Object> params = new HashMap<>();
  461 + params.put("properties", properties);
  462 + message.setParams(params);
  463 +
  464 + return message;
  465 + }
401 466
402 /** 467 /**
403 - * 创建设备消息 468 + * 创建简单值消息(params 直接是属性值)
404 */ 469 */
405 - private IotDeviceMessage createDeviceMessage(Map<String, Object> properties) { 470 + private IotDeviceMessage createSimpleValueMessage(Object value) {
406 IotDeviceMessage message = new IotDeviceMessage(); 471 IotDeviceMessage message = new IotDeviceMessage();
407 message.setDeviceId(randomLongId()); 472 message.setDeviceId(randomLongId());
408 - message.setParams(properties); 473 + message.setMethod("thing.property.post");
  474 + // 直接将属性值作为 params
  475 + message.setParams(value);
  476 +
409 return message; 477 return message;
410 } 478 }
411 479
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  9 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 11
12 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 12 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
14 import static org.junit.jupiter.api.Assertions.*; 14 import static org.junit.jupiter.api.Assertions.*;
15 15
16 /** 16 /**
17 - * {@link DeviceStateConditionMatcher} 的单元测试 17 + * {@link IotDeviceStateConditionMatcher} 的单元测试
18 * 18 *
19 * @author HUIHUI 19 * @author HUIHUI
20 */ 20 */
21 -public class DeviceStateConditionMatcherTest extends BaseMockitoUnitTest { 21 +public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest {
22 22
23 - @InjectMocks  
24 - private DeviceStateConditionMatcher matcher; 23 + private IotDeviceStateConditionMatcher matcher;
  24 +
  25 + @BeforeEach
  26 + public void setUp() {
  27 + matcher = new IotDeviceStateConditionMatcher();
  28 + }
25 29
26 @Test 30 @Test
27 public void testGetSupportedConditionType() { 31 public void testGetSupportedConditionType() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  9 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 11
12 import java.util.HashMap; 12 import java.util.HashMap;
13 import java.util.Map; 13 import java.util.Map;
@@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString @@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
18 import static org.junit.jupiter.api.Assertions.*; 18 import static org.junit.jupiter.api.Assertions.*;
19 19
20 /** 20 /**
21 - * {@link DeviceEventPostTriggerMatcher} 的单元测试 21 + * {@link IotDeviceEventPostTriggerMatcher} 的单元测试
22 * 22 *
23 * @author HUIHUI 23 * @author HUIHUI
24 */ 24 */
25 -public class DeviceEventPostTriggerMatcherTest extends BaseMockitoUnitTest { 25 +public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
26 26
27 - @InjectMocks  
28 - private DeviceEventPostTriggerMatcher matcher; 27 + private IotDeviceEventPostTriggerMatcher matcher;
  28 +
  29 + @BeforeEach
  30 + public void setUp() {
  31 + matcher = new IotDeviceEventPostTriggerMatcher();
  32 + }
29 33
30 @Test 34 @Test
31 public void testGetSupportedTriggerType_success() { 35 public void testGetSupportedTriggerType_success() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  10 +import org.junit.jupiter.api.BeforeEach;
10 import org.junit.jupiter.api.Test; 11 import org.junit.jupiter.api.Test;
11 -import org.mockito.InjectMocks;  
12 12
13 import java.util.HashMap; 13 import java.util.HashMap;
14 import java.util.Map; 14 import java.util.Map;
@@ -20,14 +20,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString @@ -20,14 +20,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
20 import static org.junit.jupiter.api.Assertions.*; 20 import static org.junit.jupiter.api.Assertions.*;
21 21
22 /** 22 /**
23 - * {@link DevicePropertyPostTriggerMatcher} 的单元测试 23 + * {@link IotDevicePropertyPostTriggerMatcher} 的单元测试
24 * 24 *
25 * @author HUIHUI 25 * @author HUIHUI
26 */ 26 */
27 -public class DevicePropertyPostTriggerMatcherTest extends BaseMockitoUnitTest { 27 +public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
28 28
29 - @InjectMocks  
30 - private DevicePropertyPostTriggerMatcher matcher; 29 + private IotDevicePropertyPostTriggerMatcher matcher;
  30 +
  31 + @BeforeEach
  32 + public void setUp() {
  33 + matcher = new IotDevicePropertyPostTriggerMatcher();
  34 + }
31 35
32 @Test 36 @Test
33 public void testGetSupportedTriggerType_success() { 37 public void testGetSupportedTriggerType_success() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 import cn.hutool.core.map.MapUtil; 3 import cn.hutool.core.map.MapUtil;
4 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  9 +import org.junit.jupiter.api.BeforeEach;
9 import org.junit.jupiter.api.Test; 10 import org.junit.jupiter.api.Test;
10 -import org.mockito.InjectMocks;  
11 11
12 import java.util.HashMap; 12 import java.util.HashMap;
13 import java.util.Map; 13 import java.util.Map;
@@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString @@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
18 import static org.junit.jupiter.api.Assertions.*; 18 import static org.junit.jupiter.api.Assertions.*;
19 19
20 /** 20 /**
21 - * {@link DeviceServiceInvokeTriggerMatcher} 的单元测试 21 + * {@link IotDeviceServiceInvokeTriggerMatcher} 的单元测试
22 * 22 *
23 * @author HUIHUI 23 * @author HUIHUI
24 */ 24 */
25 -public class DeviceServiceInvokeTriggerMatcherTest extends BaseMockitoUnitTest { 25 +public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest {
26 26
27 - @InjectMocks  
28 - private DeviceServiceInvokeTriggerMatcher matcher; 27 + private IotDeviceServiceInvokeTriggerMatcher matcher;
  28 +
  29 + @BeforeEach
  30 + public void setUp() {
  31 + matcher = new IotDeviceServiceInvokeTriggerMatcher();
  32 + }
29 33
30 @Test 34 @Test
31 public void testGetSupportedTriggerType_success() { 35 public void testGetSupportedTriggerType_success() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  10 +import org.junit.jupiter.api.BeforeEach;
10 import org.junit.jupiter.api.Test; 11 import org.junit.jupiter.api.Test;
11 -import org.mockito.InjectMocks;  
12 12
13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
14 import static org.junit.jupiter.api.Assertions.*; 14 import static org.junit.jupiter.api.Assertions.*;
15 15
16 /** 16 /**
17 - * {@link DeviceStateUpdateTriggerMatcher} 的单元测试 17 + * {@link IotDeviceStateUpdateTriggerMatcher} 的单元测试
18 * 18 *
19 * @author HUIHUI 19 * @author HUIHUI
20 */ 20 */
21 -public class DeviceStateUpdateTriggerMatcherTest extends BaseMockitoUnitTest { 21 +public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatcherTest {
22 22
23 - @InjectMocks  
24 - private DeviceStateUpdateTriggerMatcher matcher; 23 + private IotDeviceStateUpdateTriggerMatcher matcher;
  24 +
  25 + @BeforeEach
  26 + public void setUp() {
  27 + matcher = new IotDeviceStateUpdateTriggerMatcher();
  28 + }
25 29
26 @Test 30 @Test
27 public void testGetSupportedTriggerType_success() { 31 public void testGetSupportedTriggerType_success() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2
3 -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;  
4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
  7 +import org.junit.jupiter.api.BeforeEach;
7 import org.junit.jupiter.api.Test; 8 import org.junit.jupiter.api.Test;
8 -import org.mockito.InjectMocks;  
9 9
10 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; 10 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
11 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; 11 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
12 import static org.junit.jupiter.api.Assertions.*; 12 import static org.junit.jupiter.api.Assertions.*;
13 13
14 /** 14 /**
15 - * {@link TimerTriggerMatcher} 的单元测试 15 + * {@link IotTimerTriggerMatcher} 的单元测试
16 * 16 *
17 * @author HUIHUI 17 * @author HUIHUI
18 */ 18 */
19 -public class TimerTriggerMatcherTest extends BaseMockitoUnitTest { 19 +public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest {
20 20
21 - @InjectMocks  
22 - private TimerTriggerMatcher matcher; 21 + private IotTimerTriggerMatcher matcher;
  22 +
  23 + @BeforeEach
  24 + public void setUp() {
  25 + matcher = new IotTimerTriggerMatcher();
  26 + }
23 27
24 @Test 28 @Test
25 public void testGetSupportedTriggerType_success() { 29 public void testGetSupportedTriggerType_success() {
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
  2 +
  3 +import cn.hutool.core.collection.ListUtil;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  5 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  6 +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
  7 +import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
  8 +import org.junit.jupiter.api.BeforeEach;
  9 +import org.junit.jupiter.api.Test;
  10 +import org.junit.jupiter.api.extension.ExtendWith;
  11 +import org.mockito.InjectMocks;
  12 +import org.mockito.Mock;
  13 +import org.mockito.junit.jupiter.MockitoExtension;
  14 +import org.quartz.SchedulerException;
  15 +
  16 +import static org.mockito.ArgumentMatchers.any;
  17 +import static org.mockito.ArgumentMatchers.eq;
  18 +import static org.mockito.Mockito.*;
  19 +
  20 +/**
  21 + * {@link IotSceneRuleTimerHandler} 的单元测试类
  22 + *
  23 + * @author HUIHUI
  24 + */
  25 +@ExtendWith(MockitoExtension.class)
  26 +public class IotSceneRuleTimerHandlerTest {
  27 +
  28 + @Mock
  29 + private IotSchedulerManager schedulerManager;
  30 +
  31 + @InjectMocks
  32 + private IotSceneRuleTimerHandler timerHandler;
  33 +
  34 + @BeforeEach
  35 + void setUp() {
  36 + // 重置 Mock 对象
  37 + reset(schedulerManager);
  38 + }
  39 +
  40 + @Test
  41 + public void testRegisterTimerTriggers_success() throws SchedulerException {
  42 + // 准备参数
  43 + Long sceneRuleId = 1L;
  44 + IotSceneRuleDO sceneRule = new IotSceneRuleDO();
  45 + sceneRule.setId(sceneRuleId);
  46 + sceneRule.setStatus(0); // 0 表示启用
  47 + // 创建定时触发器
  48 + IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
  49 + timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
  50 + timerTrigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点
  51 + sceneRule.setTriggers(ListUtil.toList(timerTrigger));
  52 +
  53 + // 调用
  54 + timerHandler.registerTimerTriggers(sceneRule);
  55 +
  56 + // 验证
  57 + verify(schedulerManager, times(1)).addOrUpdateJob(
  58 + eq(IotSceneRuleJob.class),
  59 + eq("iot_scene_rule_timer_" + sceneRuleId),
  60 + eq("0 0 12 * * ?"),
  61 + eq(IotSceneRuleJob.buildJobDataMap(sceneRuleId))
  62 + );
  63 + }
  64 +
  65 + @Test
  66 + public void testRegisterTimerTriggers_noTimerTrigger() throws SchedulerException {
  67 + // 准备参数 - 没有定时触发器
  68 + IotSceneRuleDO sceneRule = new IotSceneRuleDO();
  69 + sceneRule.setStatus(0); // 0 表示启用
  70 + // 创建非定时触发器
  71 + IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger();
  72 + deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType());
  73 + sceneRule.setTriggers(ListUtil.toList(deviceTrigger));
  74 +
  75 + // 调用
  76 + timerHandler.registerTimerTriggers(sceneRule);
  77 +
  78 + // 验证 - 不应该调用调度器
  79 + verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
  80 + }
  81 +
  82 + @Test
  83 + public void testRegisterTimerTriggers_emptyCronExpression() throws SchedulerException {
  84 + // 准备参数 - CRON 表达式为空
  85 + Long sceneRuleId = 2L;
  86 + IotSceneRuleDO sceneRule = new IotSceneRuleDO();
  87 + sceneRule.setId(sceneRuleId);
  88 + sceneRule.setStatus(0); // 0 表示启用
  89 + // 创建定时触发器但没有 CRON 表达式
  90 + IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
  91 + timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
  92 + timerTrigger.setCronExpression(""); // 空的 CRON 表达式
  93 + sceneRule.setTriggers(ListUtil.toList(timerTrigger));
  94 +
  95 + // 调用
  96 + timerHandler.registerTimerTriggers(sceneRule);
  97 +
  98 + // 验证 - 不应该调用调度器
  99 + verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
  100 + }
  101 +
  102 + @Test
  103 + public void testUnregisterTimerTriggers_success() throws SchedulerException {
  104 + // 准备参数
  105 + Long sceneRuleId = 3L;
  106 +
  107 + // 调用
  108 + timerHandler.unregisterTimerTriggers(sceneRuleId);
  109 +
  110 + // 验证
  111 + verify(schedulerManager, times(1)).deleteJob("iot_scene_rule_timer_" + sceneRuleId);
  112 + }
  113 +
  114 + @Test
  115 + public void testPauseTimerTriggers_success() throws SchedulerException {
  116 + // 准备参数
  117 + Long sceneRuleId = 4L;
  118 +
  119 + // 调用
  120 + timerHandler.pauseTimerTriggers(sceneRuleId);
  121 +
  122 + // 验证
  123 + verify(schedulerManager, times(1)).pauseJob("iot_scene_rule_timer_" + sceneRuleId);
  124 + }
  125 +
  126 +}
yudao-module-iot/yudao-module-iot-core/src/main/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtils.java
@@ -69,6 +69,83 @@ public class IotDeviceMessageUtils { @@ -69,6 +69,83 @@ public class IotDeviceMessageUtils {
69 return null; 69 return null;
70 } 70 }
71 71
  72 + /**
  73 + * 从设备消息中提取指定标识符的属性值
  74 + * - 支持多种消息格式和属性值提取策略
  75 + * - 兼容现有的消息结构
  76 + * - 提供统一的属性值提取接口
  77 + * <p>
  78 + * 支持的提取策略(按优先级顺序):
  79 + * 1. 直接值:如果 params 不是 Map,直接返回该值(适用于简单消息)
  80 + * 2. 标识符字段:从 params[identifier] 获取
  81 + * 3. properties 结构:从 params.properties[identifier] 获取(标准属性上报)
  82 + * 4. data 结构:从 params.data[identifier] 获取
  83 + * 5. value 字段:从 params.value 获取(单值消息)
  84 + * 6. 单值 Map:如果 Map 只包含 identifier 和一个值,返回该值
  85 + *
  86 + * @param message 设备消息
  87 + * @param identifier 属性标识符
  88 + * @return 属性值,如果未找到则返回 null
  89 + */
  90 + @SuppressWarnings("unchecked")
  91 + public static Object extractPropertyValue(IotDeviceMessage message, String identifier) {
  92 + Object params = message.getParams();
  93 + if (params == null) {
  94 + return null;
  95 + }
  96 +
  97 + // 策略1:如果 params 不是 Map,直接返回该值(适用于简单的单属性消息)
  98 + if (!(params instanceof Map)) {
  99 + return params;
  100 + }
  101 +
  102 + Map<String, Object> paramsMap = (Map<String, Object>) params;
  103 +
  104 + // 策略2:直接通过标识符获取属性值
  105 + Object directValue = paramsMap.get(identifier);
  106 + if (directValue != null) {
  107 + return directValue;
  108 + }
  109 +
  110 + // 策略3:从 properties 字段中获取(适用于标准属性上报消息)
  111 + Object properties = paramsMap.get("properties");
  112 + if (properties instanceof Map) {
  113 + Map<String, Object> propertiesMap = (Map<String, Object>) properties;
  114 + Object propertyValue = propertiesMap.get(identifier);
  115 + if (propertyValue != null) {
  116 + return propertyValue;
  117 + }
  118 + }
  119 +
  120 + // 策略4:从 data 字段中获取(适用于某些消息格式)
  121 + Object data = paramsMap.get("data");
  122 + if (data instanceof Map) {
  123 + Map<String, Object> dataMap = (Map<String, Object>) data;
  124 + Object dataValue = dataMap.get(identifier);
  125 + if (dataValue != null) {
  126 + return dataValue;
  127 + }
  128 + }
  129 +
  130 + // 策略5:从 value 字段中获取(适用于单值消息)
  131 + Object value = paramsMap.get("value");
  132 + if (value != null) {
  133 + return value;
  134 + }
  135 +
  136 + // 策略6:如果 Map 只有两个字段且包含 identifier,返回另一个字段的值
  137 + if (paramsMap.size() == 2 && paramsMap.containsKey("identifier")) {
  138 + for (Map.Entry<String, Object> entry : paramsMap.entrySet()) {
  139 + if (!"identifier".equals(entry.getKey())) {
  140 + return entry.getValue();
  141 + }
  142 + }
  143 + }
  144 +
  145 + // 未找到对应的属性值
  146 + return null;
  147 + }
  148 +
72 // ========== Topic 相关 ========== 149 // ========== Topic 相关 ==========
73 150
74 public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) { 151 public static String buildMessageBusGatewayDeviceMessageTopic(String serverId) {
yudao-module-iot/yudao-module-iot-core/src/test/java/cn/iocoder/yudao/module/iot/core/util/IotDeviceMessageUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.core.util;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import org.junit.jupiter.api.Test;
  5 +
  6 +import java.util.HashMap;
  7 +import java.util.Map;
  8 +
  9 +import static org.junit.jupiter.api.Assertions.assertEquals;
  10 +import static org.junit.jupiter.api.Assertions.assertNull;
  11 +
  12 +/**
  13 + * {@link IotDeviceMessageUtils} 的单元测试
  14 + *
  15 + * @author HUIHUI
  16 + */
  17 +public class IotDeviceMessageUtilsTest {
  18 +
  19 + @Test
  20 + public void testExtractPropertyValue_directValue() {
  21 + // 测试直接值(非 Map)
  22 + IotDeviceMessage message = new IotDeviceMessage();
  23 + message.setParams(25.5);
  24 +
  25 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  26 + assertEquals(25.5, result);
  27 + }
  28 +
  29 + @Test
  30 + public void testExtractPropertyValue_directIdentifier() {
  31 + // 测试直接通过标识符获取
  32 + IotDeviceMessage message = new IotDeviceMessage();
  33 + Map<String, Object> params = new HashMap<>();
  34 + params.put("temperature", 25.5);
  35 + message.setParams(params);
  36 +
  37 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  38 + assertEquals(25.5, result);
  39 + }
  40 +
  41 + @Test
  42 + public void testExtractPropertyValue_propertiesStructure() {
  43 + // 测试 properties 结构
  44 + IotDeviceMessage message = new IotDeviceMessage();
  45 + Map<String, Object> properties = new HashMap<>();
  46 + properties.put("temperature", 25.5);
  47 + properties.put("humidity", 60);
  48 +
  49 + Map<String, Object> params = new HashMap<>();
  50 + params.put("properties", properties);
  51 + message.setParams(params);
  52 +
  53 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  54 + assertEquals(25.5, result);
  55 + }
  56 +
  57 + @Test
  58 + public void testExtractPropertyValue_dataStructure() {
  59 + // 测试 data 结构
  60 + IotDeviceMessage message = new IotDeviceMessage();
  61 + Map<String, Object> data = new HashMap<>();
  62 + data.put("temperature", 25.5);
  63 +
  64 + Map<String, Object> params = new HashMap<>();
  65 + params.put("data", data);
  66 + message.setParams(params);
  67 +
  68 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  69 + assertEquals(25.5, result);
  70 + }
  71 +
  72 + @Test
  73 + public void testExtractPropertyValue_valueField() {
  74 + // 测试 value 字段
  75 + IotDeviceMessage message = new IotDeviceMessage();
  76 + Map<String, Object> params = new HashMap<>();
  77 + params.put("identifier", "temperature");
  78 + params.put("value", 25.5);
  79 + message.setParams(params);
  80 +
  81 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  82 + assertEquals(25.5, result);
  83 + }
  84 +
  85 + @Test
  86 + public void testExtractPropertyValue_singleValueMap() {
  87 + // 测试单值 Map(包含 identifier 和一个值)
  88 + IotDeviceMessage message = new IotDeviceMessage();
  89 + Map<String, Object> params = new HashMap<>();
  90 + params.put("identifier", "temperature");
  91 + params.put("actualValue", 25.5);
  92 + message.setParams(params);
  93 +
  94 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  95 + assertEquals(25.5, result);
  96 + }
  97 +
  98 + @Test
  99 + public void testExtractPropertyValue_notFound() {
  100 + // 测试未找到属性值
  101 + IotDeviceMessage message = new IotDeviceMessage();
  102 + Map<String, Object> params = new HashMap<>();
  103 + params.put("humidity", 60);
  104 + message.setParams(params);
  105 +
  106 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  107 + assertNull(result);
  108 + }
  109 +
  110 + @Test
  111 + public void testExtractPropertyValue_nullParams() {
  112 + // 测试 params 为 null
  113 + IotDeviceMessage message = new IotDeviceMessage();
  114 + message.setParams(null);
  115 +
  116 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  117 + assertNull(result);
  118 + }
  119 +
  120 + @Test
  121 + public void testExtractPropertyValue_priorityOrder() {
  122 + // 测试优先级顺序:直接标识符 > properties > data > value
  123 + IotDeviceMessage message = new IotDeviceMessage();
  124 +
  125 + Map<String, Object> properties = new HashMap<>();
  126 + properties.put("temperature", 20.0);
  127 +
  128 + Map<String, Object> data = new HashMap<>();
  129 + data.put("temperature", 30.0);
  130 +
  131 + Map<String, Object> params = new HashMap<>();
  132 + params.put("temperature", 25.5); // 最高优先级
  133 + params.put("properties", properties);
  134 + params.put("data", data);
  135 + params.put("value", 40.0);
  136 + message.setParams(params);
  137 +
  138 + Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
  139 + assertEquals(25.5, result); // 应该返回直接标识符的值
  140 + }
  141 +}
yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/mqtt/router/IotMqttUpstreamHandler.java
@@ -68,7 +68,13 @@ public class IotMqttUpstreamHandler { @@ -68,7 +68,13 @@ public class IotMqttUpstreamHandler {
68 68
69 log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username); 69 log.info("[handle][设备认证成功,建立连接,客户端 ID: {},用户名: {}]", clientId, username);
70 70
71 - // 2. 设置异常和关闭处理器 71 + // 2. 设置心跳处理器(监听客户端的 PINGREQ 消息)
  72 + endpoint.pingHandler(v -> {
  73 + log.debug("[handle][收到客户端心跳,客户端 ID: {}]", clientId);
  74 + // Vert.x 会自动发送 PINGRESP 响应,无需手动处理
  75 + });
  76 +
  77 + // 3. 设置异常和关闭处理器
72 endpoint.exceptionHandler(ex -> { 78 endpoint.exceptionHandler(ex -> {
73 log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint)); 79 log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint));
74 cleanupConnection(endpoint); 80 cleanupConnection(endpoint);
@@ -77,7 +83,7 @@ public class IotMqttUpstreamHandler { @@ -77,7 +83,7 @@ public class IotMqttUpstreamHandler {
77 cleanupConnection(endpoint); 83 cleanupConnection(endpoint);
78 }); 84 });
79 85
80 - // 3. 设置消息处理器 86 + // 4. 设置消息处理器
81 endpoint.publishHandler(message -> { 87 endpoint.publishHandler(message -> {
82 try { 88 try {
83 processMessage(clientId, message.topicName(), message.payload().getBytes()); 89 processMessage(clientId, message.topicName(), message.payload().getBytes());
@@ -100,7 +106,7 @@ public class IotMqttUpstreamHandler { @@ -100,7 +106,7 @@ public class IotMqttUpstreamHandler {
100 } 106 }
101 }); 107 });
102 108
103 - // 4. 设置订阅处理器 109 + // 5. 设置订阅处理器
104 endpoint.subscribeHandler(subscribe -> { 110 endpoint.subscribeHandler(subscribe -> {
105 // 提取主题名称列表用于日志显示 111 // 提取主题名称列表用于日志显示
106 List<String> topicNames = subscribe.topicSubscriptions().stream() 112 List<String> topicNames = subscribe.topicSubscriptions().stream()
@@ -115,22 +121,22 @@ public class IotMqttUpstreamHandler { @@ -115,22 +121,22 @@ public class IotMqttUpstreamHandler {
115 endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels); 121 endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
116 }); 122 });
117 123
118 - // 5. 设置取消订阅处理器 124 + // 6. 设置取消订阅处理器
119 endpoint.unsubscribeHandler(unsubscribe -> { 125 endpoint.unsubscribeHandler(unsubscribe -> {
120 log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics()); 126 log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
121 endpoint.unsubscribeAcknowledge(unsubscribe.messageId()); 127 endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
122 }); 128 });
123 129
124 - // 6. 设置 QoS 2消息的 PUBREL 处理器 130 + // 7. 设置 QoS 2消息的 PUBREL 处理器
125 endpoint.publishReleaseHandler(endpoint::publishComplete); 131 endpoint.publishReleaseHandler(endpoint::publishComplete);
126 132
127 - // 7. 设置断开连接处理器 133 + // 8. 设置断开连接处理器
128 endpoint.disconnectHandler(v -> { 134 endpoint.disconnectHandler(v -> {
129 log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId); 135 log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId);
130 cleanupConnection(endpoint); 136 cleanupConnection(endpoint);
131 }); 137 });
132 138
133 - // 8. 接受连接 139 + // 9. 接受连接
134 endpoint.accept(false); 140 endpoint.accept(false);
135 } 141 }
136 142
yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml
@@ -103,7 +103,6 @@ yudao: @@ -103,7 +103,6 @@ yudao:
103 port: 1883 103 port: 1883
104 max-message-size: 8192 104 max-message-size: 8192
105 connect-timeout-seconds: 60 105 connect-timeout-seconds: 60
106 - keep-alive-timeout-seconds: 300  
107 ssl-enabled: false 106 ssl-enabled: false
108 107
109 --- #################### 日志相关配置 #################### 108 --- #################### 日志相关配置 ####################