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 17 @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true)
18 18 @JsonSubTypes({
19 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 22 @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"),
21 23 @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"),
22 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 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 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 76 */
77 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 19 @Getter
20 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 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 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 16 import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
17 17 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
18 18 import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
  19 +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants;
19 20 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
20   -import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
21 21 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
22 22 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
23 23 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
24 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 26 import jakarta.annotation.Resource;
26 27 import lombok.extern.slf4j.Slf4j;
  28 +import org.springframework.cache.annotation.CacheEvict;
  29 +import org.springframework.cache.annotation.Cacheable;
27 30 import org.springframework.stereotype.Service;
28 31 import org.springframework.validation.annotation.Validated;
29 32  
... ... @@ -47,9 +50,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
47 50 @Resource
48 51 private IotSceneRuleMapper sceneRuleMapper;
49 52  
50   - // TODO @puhui999:定时任务,基于它调度;
51   - @Resource(name = "iotSchedulerManager")
52   - private IotSchedulerManager schedulerManager;
53 53 @Resource
54 54 private IotProductService productService;
55 55 @Resource
... ... @@ -59,38 +59,68 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
59 59 private IotSceneRuleMatcherManager sceneRuleMatcherManager;
60 60 @Resource
61 61 private List<IotSceneRuleAction> sceneRuleActions;
  62 + @Resource
  63 + private IotSceneRuleTimerHandler timerHandler;
62 64  
63 65 @Override
  66 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
64 67 public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) {
65 68 IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class);
66 69 sceneRuleMapper.insert(sceneRule);
  70 +
  71 + // 注册定时触发器
  72 + timerHandler.registerTimerTriggers(sceneRule);
  73 +
67 74 return sceneRule.getId();
68 75 }
69 76  
70 77 @Override
  78 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
71 79 public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) {
72 80 // 校验存在
73 81 validateSceneRuleExists(updateReqVO.getId());
74 82 // 更新
75 83 IotSceneRuleDO updateObj = BeanUtils.toBean(updateReqVO, IotSceneRuleDO.class);
76 84 sceneRuleMapper.updateById(updateObj);
  85 +
  86 + // 更新定时触发器
  87 + timerHandler.updateTimerTriggers(updateObj);
77 88 }
78 89  
79 90 @Override
  91 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
80 92 public void updateSceneRuleStatus(Long id, Integer status) {
81   - // 校验存在
  93 + // 1. 校验存在
82 94 validateSceneRuleExists(id);
83   - // 更新状态
  95 +
  96 + // 2. 更新状态
84 97 IotSceneRuleDO updateObj = new IotSceneRuleDO().setId(id).setStatus(status);
85 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 113 @Override
  114 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
89 115 public void deleteSceneRule(Long id) {
90   - // 校验存在
  116 + // 1. 校验存在
91 117 validateSceneRuleExists(id);
92   - // 删除
  118 +
  119 + // 2. 删除
93 120 sceneRuleMapper.deleteById(id);
  121 +
  122 + // 3. 删除定时触发器
  123 + timerHandler.unregisterTimerTriggers(id);
94 124 }
95 125  
96 126 private void validateSceneRuleExists(Long id) {
... ... @@ -126,16 +156,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
126 156 return sceneRuleMapper.selectListByStatus(status);
127 157 }
128 158  
129   - // TODO 芋艿,缓存待实现 @puhui999
130 159 @Override
  160 + @Cacheable(value = RedisKeyConstants.SCENE_RULE_LIST, key = "#productId + '_' + #deviceId ")
131 161 @TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略
132 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 167 return filterList(enabledList, sceneRule -> {
140 168 if (CollUtil.isEmpty(sceneRule.getTriggers())) {
141 169 return false;
... ... @@ -144,21 +172,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
144 172 for (IotSceneRuleDO.Trigger trigger : sceneRule.getTriggers()) {
145 173 // 检查触发器是否匹配指定的产品和设备
146 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 177 return false;
153 178 }
154 179 // 检查是否是全部设备的特殊标识
155 180 if (IotDeviceDO.DEVICE_ID_ALL.equals(trigger.getDeviceId())) {
156   - return true; // 匹配所有设备
  181 + return true;
157 182 }
158 183 // 检查具体设备 ID 是否匹配
159 184 return ObjUtil.equal(productId, trigger.getProductId()) && ObjUtil.equal(deviceId, trigger.getDeviceId());
160 185 } catch (Exception e) {
161   - log.warn("[isMatchProductAndDevice][产品({}) 设备({}) 匹配触发器异常]", productId, deviceId, e);
  186 + log.warn("[getSceneRuleListByProductIdAndDeviceIdFromCache][产品({}) 设备({}) 匹配触发器异常]",
  187 + productId, deviceId, e);
162 188 return false;
163 189 }
164 190 }
... ... @@ -168,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
168 194  
169 195 @Override
170 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 201 List<IotSceneRuleDO> sceneRules = getMatchedSceneRuleListByMessage(message);
175 202 if (CollUtil.isEmpty(sceneRules)) {
176 203 return;
... ... @@ -214,16 +241,16 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
214 241 */
215 242 private List<IotSceneRuleDO> getMatchedSceneRuleListByMessage(IotDeviceMessage message) {
216 243 // 1. 匹配设备
217   - // TODO @芋艿:可能需要 getSelf(); 缓存 @puhui999;
  244 + // TODO 缓存 @puhui999:可能需要 getSelf()
218 245 // 1.1 通过 deviceId 获取设备信息
219   - IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
  246 + IotDeviceDO device = getSelf().deviceService.getDeviceFromCache(message.getDeviceId());
220 247 if (device == null) {
221 248 log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId());
222 249 return List.of();
223 250 }
224 251  
225 252 // 1.2 通过 productId 获取产品信息
226   - IotProductDO product = productService.getProductFromCache(device.getProductId());
  253 + IotProductDO product = getSelf().productService.getProductFromCache(device.getProductId());
227 254 if (product == null) {
228 255 log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId());
229 256 return List.of();
... ... @@ -273,7 +300,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
273 300 */
274 301 private boolean matchSingleTrigger(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, IotSceneRuleDO sceneRule) {
275 302 try {
276   - // 2. 检查触发器的条件分组
277 303 return sceneRuleMatcherManager.isMatched(message, trigger) && isTriggerConditionGroupsMatched(message, trigger, sceneRule);
278 304 } catch (Exception e) {
279 305 log.error("[matchSingleTrigger][触发器匹配异常] sceneRuleId: {}, triggerType: {}, message: {}",
... ... @@ -290,18 +316,19 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
290 316 * @param sceneRule 场景规则(用于日志)
291 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 323 if (CollUtil.isEmpty(trigger.getConditionGroups())) {
296 324 return true;
297 325 }
298 326  
299   - // 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系
  327 + // 2. 检查条件分组:分组与分组之间是"或"的关系,条件与条件之间是"且"的关系
300 328 for (List<IotSceneRuleDO.TriggerCondition> conditionGroup : trigger.getConditionGroups()) {
301 329 if (CollUtil.isEmpty(conditionGroup)) {
302 330 continue;
303 331 }
304   -
305 332 // 检查当前分组中的所有条件是否都匹配(且关系)
306 333 boolean allConditionsMatched = true;
307 334 for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) {
... ... @@ -310,14 +337,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
310 337 break;
311 338 }
312 339 }
313   -
314 340 // 如果当前分组的所有条件都匹配,则整个触发器匹配成功
315 341 if (allConditionsMatched) {
316 342 return true;
317 343 }
318 344 }
319 345  
320   - // 所有分组都不匹配
  346 + // 3. 所有分组都不匹配
321 347 return false;
322 348 }
323 349  
... ... @@ -352,13 +378,13 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
352 378 sceneRules.forEach(sceneRule -> {
353 379 // 2. 遍历规则场景的动作
354 380 sceneRule.getActions().forEach(actionConfig -> {
355   - // 3.1 获取对应的动作 Action 数组
  381 + // 2.1 获取对应的动作 Action 数组
356 382 List<IotSceneRuleAction> actions = filterList(sceneRuleActions,
357 383 action -> action.getType().getType().equals(actionConfig.getType()));
358 384 if (CollUtil.isEmpty(actions)) {
359 385 return;
360 386 }
361   - // 3.2 执行动作
  387 + // 2.2 执行动作
362 388 actions.forEach(action -> {
363 389 try {
364 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 14  
15 15 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
16 16  
17   -// TODO @puhui999、@芋艿:未测试;需要场景联动开发完
18 17 /**
19 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 17 import javax.annotation.Nullable;
18 18 import java.util.List;
19 19  
20   -// TODO @puhui999、@芋艿:未测试;需要场景联动开发完
21 20 /**
22 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 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 6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
4 8 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
6 10 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
... ... @@ -9,8 +13,11 @@ import jakarta.annotation.Resource;
9 13 import lombok.extern.slf4j.Slf4j;
10 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 22 * @author 芋道源码
16 23 */
... ... @@ -23,28 +30,108 @@ public class IotDeviceControlSceneRuleAction implements IotSceneRuleAction {
23 30 @Resource
24 31 private IotDeviceMessageService deviceMessageService;
25 32  
26   - // TODO @puhui999:这里
27 33 @Override
28 34 public void execute(IotDeviceMessage message,
29 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 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 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 9 * - {@link IotSceneRuleTriggerMatcher} 触发器匹配器
12 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 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 25 * @author HUIHUI
... ... @@ -91,7 +89,7 @@ public final class IotSceneRuleMatcherHelper {
91 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 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 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 21 * @author HUIHUI
24 22 */
... ... @@ -44,13 +42,12 @@ public class IotSceneRuleMatcherManager {
44 42 return;
45 43 }
46 44  
47   - // 按优先级排序并过滤启用的匹配器
  45 + // 1.1 按优先级排序并过滤启用的匹配器
48 46 List<IotSceneRuleMatcher> allMatchers = matchers.stream()
49 47 .filter(IotSceneRuleMatcher::isEnabled)
50 48 .sorted(Comparator.comparing(IotSceneRuleMatcher::getPriority))
51 49 .toList();
52   -
53   - // 分离触发器匹配器和条件匹配器
  50 + // 1.2 分离触发器匹配器和条件匹配器
54 51 List<IotSceneRuleTriggerMatcher> triggerMatchers = allMatchers.stream()
55 52 .filter(matcher -> matcher instanceof IotSceneRuleTriggerMatcher)
56 53 .map(matcher -> (IotSceneRuleTriggerMatcher) matcher)
... ... @@ -60,7 +57,7 @@ public class IotSceneRuleMatcherManager {
60 57 .map(matcher -> (IotSceneRuleConditionMatcher) matcher)
61 58 .toList();
62 59  
63   - // 构建触发器匹配器映射表
  60 + // 2.1 构建触发器匹配器映射表
64 61 this.triggerMatchers = convertMap(triggerMatchers, IotSceneRuleTriggerMatcher::getSupportedTriggerType,
65 62 Function.identity(),
66 63 (existing, replacement) -> {
... ... @@ -70,7 +67,7 @@ public class IotSceneRuleMatcherManager {
70 67 existing.getSupportedTriggerType() : replacement.getSupportedTriggerType());
71 68 return existing.getPriority() <= replacement.getPriority() ? existing : replacement;
72 69 }, LinkedHashMap::new);
73   - // 构建条件匹配器映射表
  70 + // 2.2 构建条件匹配器映射表
74 71 this.conditionMatchers = convertMap(conditionMatchers, IotSceneRuleConditionMatcher::getSupportedConditionType,
75 72 Function.identity(),
76 73 (existing, replacement) -> {
... ... @@ -82,7 +79,7 @@ public class IotSceneRuleMatcherManager {
82 79 },
83 80 LinkedHashMap::new);
84 81  
85   - // 日志输出初始化信息
  82 + // 3. 日志输出初始化信息
86 83 log.info("[IotSceneRuleMatcherManager][初始化完成,共加载({})个匹配器,其中触发器匹配器({})个,条件匹配器({})个]",
87 84 allMatchers.size(), this.triggerMatchers.size(), this.conditionMatchers.size());
88 85 this.triggerMatchers.forEach((type, matcher) ->
... ... @@ -135,7 +132,7 @@ public class IotSceneRuleMatcherManager {
135 132 return false;
136 133 }
137 134  
138   - // 根据条件类型查找对应的匹配器
  135 + // 1. 根据条件类型查找对应的匹配器
139 136 IotSceneRuleConditionTypeEnum conditionType = IotSceneRuleConditionTypeEnum.typeOf(condition.getType());
140 137 if (conditionType == null) {
141 138 log.warn("[isConditionMatched][conditionType({}) 未知的条件类型]", condition.getType());
... ... @@ -147,7 +144,7 @@ public class IotSceneRuleMatcherManager {
147 144 return false;
148 145 }
149 146  
150   - // 执行匹配逻辑
  147 + // 2. 执行匹配逻辑
151 148 try {
152 149 return matcher.matches(message, condition);
153 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 17 import java.util.List;
18 18  
19 19 /**
20   - * 当前时间条件匹配器
21   - * <p>
22   - * 处理时间相关的子条件匹配逻辑
  20 + * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑
23 21 *
24 22 * @author HUIHUI
25 23 */
26 24 @Component
27 25 @Slf4j
28   -public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
  26 +public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
29 27  
30 28 /**
31 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 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2  
  3 +
3 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 5 import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
5 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 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
8 9 import org.springframework.stereotype.Component;
9 10  
  11 +
10 12 /**
11   - * 设备属性条件匹配器
12   - * <p>
13   - * 处理设备属性相关的子条件匹配逻辑
  13 + * 设备属性条件匹配器:处理设备属性相关的子条件匹配逻辑
14 14 *
15 15 * @author HUIHUI
16 16 */
17 17 @Component
18   -public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher {
  18 +public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher {
19 19  
20 20 @Override
21 21 public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
... ... @@ -43,10 +43,10 @@ public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatc
43 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 48 if (propertyValue == null) {
49   - IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空");
  49 + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空或未找到指定属性");
50 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 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.util.IotDeviceMessageUtils;
4 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
6 7 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
7 8 import org.springframework.stereotype.Component;
8 9  
9 10 /**
10   - * 设备状态条件匹配器
11   - * <p>
12   - * 处理设备状态相关的子条件匹配逻辑
  11 + * 设备状态条件匹配器:处理设备状态相关的子条件匹配逻辑
13 12 *
14 13 * @author HUIHUI
15 14 */
16 15 @Component
17   -public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
  16 +public class IotDeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
18 17  
19 18 @Override
20 19 public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
... ... @@ -35,8 +34,9 @@ public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher
35 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 40 if (stateValue == null) {
41 41 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空");
42 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 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 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 10 import org.springframework.stereotype.Component;
11 11  
12 12 /**
13   - * 设备事件上报触发器匹配器
14   - * <p>
15   - * 处理设备事件上报的触发器匹配逻辑
  13 + * 设备事件上报触发器匹配器:处理设备事件上报的触发器匹配逻辑
16 14 *
17 15 * @author HUIHUI
18 16 */
19 17 @Component
20   -public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
  18 +public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
21 19  
22 20 @Override
23 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 9 import org.springframework.stereotype.Component;
10 10  
11 11 /**
12   - * 设备属性上报触发器匹配器
13   - * <p>
14   - * 处理设备属性数据上报的触发器匹配逻辑
  12 + * 设备属性上报触发器匹配器:处理设备属性数据上报的触发器匹配逻辑
15 13 *
16 14 * @author HUIHUI
17 15 */
18 16 @Component
19   -public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
  17 +public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
20 18  
21 19 @Override
22 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
... ... @@ -52,10 +50,10 @@ public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatc
52 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 55 if (propertyValue == null) {
58   - IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空");
  56 + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空或未找到指定属性");
59 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 9 import org.springframework.stereotype.Component;
10 10  
11 11 /**
12   - * 设备服务调用触发器匹配器
13   - * <p>
14   - * 处理设备服务调用的触发器匹配逻辑
  12 + * 设备服务调用触发器匹配器:处理设备服务调用的触发器匹配逻辑
15 13 *
16 14 * @author HUIHUI
17 15 */
18 16 @Component
19   -public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
  17 +public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
20 18  
21 19 @Override
22 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
... ... @@ -46,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat
46 44  
47 45 // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可
48 46 // 不需要检查操作符和值,因为服务调用本身就是触发条件
49   - // TODO @puhui999: 服务调用时校验输入参数是否匹配条件
  47 + // TODO @puhui999: 服务调用时校验输入参数是否匹配条件?
50 48 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
51 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 2  
3 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
4 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
5 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
7 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
8 9 import org.springframework.stereotype.Component;
9 10  
10 11 /**
11   - * 设备状态更新触发器匹配器
12   - * <p>
13   - * 处理设备上下线状态变更的触发器匹配逻辑
  12 + * 设备状态更新触发器匹配器:处理设备上下线状态变更的触发器匹配逻辑
14 13 *
15 14 * @author HUIHUI
16 15 */
17 16 @Component
18   -public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
  17 +public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
19 18  
20 19 @Override
21 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
... ... @@ -43,16 +42,17 @@ public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatch
43 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 49 IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空");
50 50 return false;
51 51 }
52 52  
53 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 56 if (matched) {
57 57 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
58 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 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 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 9 import org.springframework.stereotype.Component;
10 10  
11 11 /**
12   - * 定时触发器匹配器
13   - * <p>
14   - * 处理定时触发的触发器匹配逻辑
  12 + * 定时触发器匹配器:处理定时触发的触发器匹配逻辑
  13 + *
15 14 * 注意:定时触发器不依赖设备消息,主要用于定时任务场景
16 15 *
17 16 * @author HUIHUI
18 17 */
19 18 @Component
20   -public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
  19 +public class IotTimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
21 20  
22 21 @Override
23 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 72 FROM device_property_${reqVO.deviceId}
73 73 WHERE ${@cn.hutool.core.util.StrUtil@toUnderlineCase(reqVO.identifier)} IS NOT NULL
74 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 76 ORDER BY ts DESC
77 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 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 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 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
7 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 9 import org.junit.jupiter.api.Test;
9   -import org.mockito.InjectMocks;
10 10  
11 11 import java.time.LocalDateTime;
12 12 import java.time.ZoneOffset;
... ... @@ -16,14 +16,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
16 16 import static org.junit.jupiter.api.Assertions.*;
17 17  
18 18 /**
19   - * {@link CurrentTimeConditionMatcher} 的单元测试
  19 + * {@link IotCurrentTimeConditionMatcher} 的单元测试
20 20 *
21 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 32 @Test
29 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 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 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 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 9 import org.junit.jupiter.api.Test;
10   -import org.mockito.InjectMocks;
11 10  
12 11 import java.util.HashMap;
13 12 import java.util.Map;
14 13  
15 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 15 import static org.junit.jupiter.api.Assertions.*;
18 16  
19 17 /**
20   - * {@link DevicePropertyConditionMatcher} 的单元测试
  18 + * {@link IotDevicePropertyConditionMatcher} 的单元测试
21 19 *
22 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 31 @Test
30 32 public void testGetSupportedConditionType() {
... ... @@ -41,27 +43,17 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
41 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 49 @Test
57 50 public void testMatches_temperatureEquals_success() {
58   - // 准备参数
59   - String propertyName = "temperature";
  51 + // 准备参数:创建属性上报消息
  52 + String propertyIdentifier = "temperature";
60 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 55 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
64   - propertyName,
  56 + propertyIdentifier,
65 57 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
66 58 String.valueOf(propertyValue)
67 59 );
... ... @@ -75,14 +67,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
75 67  
76 68 @Test
77 69 public void testMatches_humidityGreaterThan_success() {
78   - // 准备参数
79   - String propertyName = "humidity";
  70 + // 准备参数:创建属性上报消息
  71 + String propertyIdentifier = "humidity";
80 72 Integer propertyValue = 75;
81 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 75 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
85   - propertyName,
  76 + propertyIdentifier,
86 77 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
87 78 String.valueOf(compareValue)
88 79 );
... ... @@ -96,14 +87,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
96 87  
97 88 @Test
98 89 public void testMatches_pressureLessThan_success() {
99   - // 准备参数
100   - String propertyName = "pressure";
  90 + // 准备参数:创建属性上报消息
  91 + String propertyIdentifier = "pressure";
101 92 Double propertyValue = 1010.5;
102 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 95 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
106   - propertyName,
  96 + propertyIdentifier,
107 97 IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(),
108 98 String.valueOf(compareValue)
109 99 );
... ... @@ -117,14 +107,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
117 107  
118 108 @Test
119 109 public void testMatches_statusNotEquals_success() {
120   - // 准备参数
121   - String propertyName = "status";
  110 + // 准备参数:创建属性上报消息
  111 + String propertyIdentifier = "status";
122 112 String propertyValue = "active";
123 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 115 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
127   - propertyName,
  116 + propertyIdentifier,
128 117 IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(),
129 118 compareValue
130 119 );
... ... @@ -138,14 +127,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
138 127  
139 128 @Test
140 129 public void testMatches_propertyMismatch_fail() {
141   - // 准备参数
142   - String propertyName = "temperature";
  130 + // 准备参数:创建属性上报消息,值不满足条件
  131 + String propertyIdentifier = "temperature";
143 132 Double propertyValue = 15.0;
144 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 135 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
148   - propertyName,
  136 + propertyIdentifier,
149 137 IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(),
150 138 String.valueOf(compareValue)
151 139 );
... ... @@ -158,14 +146,16 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
158 146 }
159 147  
160 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 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 168 @Test
179 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 174 boolean result = matcher.matches(message, null);
... ... @@ -191,8 +180,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
191 180 @Test
192 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 184 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
197 185 condition.setType(null);
198 186  
... ... @@ -206,8 +194,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
206 194 @Test
207 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 198 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
212 199 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
213 200 condition.setIdentifier(null); // 缺少标识符
... ... @@ -224,8 +211,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
224 211 @Test
225 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 215 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
230 216 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
231 217 condition.setIdentifier("temperature");
... ... @@ -242,8 +228,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
242 228 @Test
243 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 232 IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
248 233 condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
249 234 condition.setIdentifier("temperature");
... ... @@ -275,7 +260,7 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
275 260  
276 261 @Test
277 262 public void testMatches_nullDeviceProperties_fail() {
278   - // 准备参数
  263 + // 准备参数:消息的 params 为 null
279 264 IotDeviceMessage message = new IotDeviceMessage();
280 265 message.setParams(null);
281 266 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
... ... @@ -292,14 +277,79 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
292 277 }
293 278  
294 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 346 public void testMatches_voltageGreaterThanOrEquals_success() {
296   - // 准备参数
297   - String propertyName = "voltage";
  347 + // 准备参数:创建属性上报消息
  348 + String propertyIdentifier = "voltage";
298 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 351 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
302   - propertyName,
  352 + propertyIdentifier,
303 353 IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(),
304 354 String.valueOf(propertyValue)
305 355 );
... ... @@ -313,14 +363,13 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
313 363  
314 364 @Test
315 365 public void testMatches_currentLessThanOrEquals_success() {
316   - // 准备参数
317   - String propertyName = "current";
  366 + // 准备参数:创建属性上报消息
  367 + String propertyIdentifier = "current";
318 368 Double propertyValue = 2.5;
319 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 371 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
323   - propertyName,
  372 + propertyIdentifier,
324 373 IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(),
325 374 String.valueOf(compareValue)
326 375 );
... ... @@ -334,13 +383,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
334 383  
335 384 @Test
336 385 public void testMatches_stringProperty_success() {
337   - // 准备参数
338   - String propertyName = "mode";
  386 + // 准备参数:创建属性上报消息
  387 + String propertyIdentifier = "mode";
339 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 390 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
343   - propertyName,
  391 + propertyIdentifier,
344 392 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
345 393 propertyValue
346 394 );
... ... @@ -354,13 +402,12 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
354 402  
355 403 @Test
356 404 public void testMatches_booleanProperty_success() {
357   - // 准备参数
358   - String propertyName = "enabled";
  405 + // 准备参数:创建属性上报消息
  406 + String propertyIdentifier = "enabled";
359 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 409 IotSceneRuleDO.TriggerCondition condition = createValidCondition(
363   - propertyName,
  410 + propertyIdentifier,
364 411 IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
365 412 String.valueOf(propertyValue)
366 413 );
... ... @@ -372,40 +419,61 @@ public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest {
372 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 471 IotDeviceMessage message = new IotDeviceMessage();
407 472 message.setDeviceId(randomLongId());
408   - message.setParams(properties);
  473 + message.setMethod("thing.property.post");
  474 + // 直接将属性值作为 params
  475 + message.setParams(value);
  476 +
409 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 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 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
5 4 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 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 10 import org.junit.jupiter.api.Test;
10   -import org.mockito.InjectMocks;
11 11  
12 12 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
13 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
14 14 import static org.junit.jupiter.api.Assertions.*;
15 15  
16 16 /**
17   - * {@link DeviceStateConditionMatcher} 的单元测试
  17 + * {@link IotDeviceStateConditionMatcher} 的单元测试
18 18 *
19 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 30 @Test
27 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 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4   -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
5 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 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 10 import org.junit.jupiter.api.Test;
10   -import org.mockito.InjectMocks;
11 11  
12 12 import java.util.HashMap;
13 13 import java.util.Map;
... ... @@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
18 18 import static org.junit.jupiter.api.Assertions.*;
19 19  
20 20 /**
21   - * {@link DeviceEventPostTriggerMatcher} 的单元测试
  21 + * {@link IotDeviceEventPostTriggerMatcher} 的单元测试
22 22 *
23 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 34 @Test
31 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 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4   -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
5 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
9 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 11 import org.junit.jupiter.api.Test;
11   -import org.mockito.InjectMocks;
12 12  
13 13 import java.util.HashMap;
14 14 import java.util.Map;
... ... @@ -20,14 +20,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
20 20 import static org.junit.jupiter.api.Assertions.*;
21 21  
22 22 /**
23   - * {@link DevicePropertyPostTriggerMatcher} 的单元测试
  23 + * {@link IotDevicePropertyPostTriggerMatcher} 的单元测试
24 24 *
25 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 36 @Test
33 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 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
2 2  
3 3 import cn.hutool.core.map.MapUtil;
4   -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
5 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
6 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 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 10 import org.junit.jupiter.api.Test;
10   -import org.mockito.InjectMocks;
11 11  
12 12 import java.util.HashMap;
13 13 import java.util.Map;
... ... @@ -18,14 +18,18 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString
18 18 import static org.junit.jupiter.api.Assertions.*;
19 19  
20 20 /**
21   - * {@link DeviceServiceInvokeTriggerMatcher} 的单元测试
  21 + * {@link IotDeviceServiceInvokeTriggerMatcher} 的单元测试
22 22 *
23 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 34 @Test
31 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 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 3 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
6 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
9 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 11 import org.junit.jupiter.api.Test;
11   -import org.mockito.InjectMocks;
12 12  
13 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
14 14 import static org.junit.jupiter.api.Assertions.*;
15 15  
16 16 /**
17   - * {@link DeviceStateUpdateTriggerMatcher} 的单元测试
  17 + * {@link IotDeviceStateUpdateTriggerMatcher} 的单元测试
18 18 *
19 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 30 @Test
27 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 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 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 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 8 import org.junit.jupiter.api.Test;
8   -import org.mockito.InjectMocks;
9 9  
10 10 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
11 11 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
12 12 import static org.junit.jupiter.api.Assertions.*;
13 13  
14 14 /**
15   - * {@link TimerTriggerMatcher} 的单元测试
  15 + * {@link IotTimerTriggerMatcher} 的单元测试
16 16 *
17 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 28 @Test
25 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 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 149 // ========== Topic 相关 ==========
73 150  
74 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 68  
69 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 78 endpoint.exceptionHandler(ex -> {
73 79 log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, connectionManager.getEndpointAddress(endpoint));
74 80 cleanupConnection(endpoint);
... ... @@ -77,7 +83,7 @@ public class IotMqttUpstreamHandler {
77 83 cleanupConnection(endpoint);
78 84 });
79 85  
80   - // 3. 设置消息处理器
  86 + // 4. 设置消息处理器
81 87 endpoint.publishHandler(message -> {
82 88 try {
83 89 processMessage(clientId, message.topicName(), message.payload().getBytes());
... ... @@ -100,7 +106,7 @@ public class IotMqttUpstreamHandler {
100 106 }
101 107 });
102 108  
103   - // 4. 设置订阅处理器
  109 + // 5. 设置订阅处理器
104 110 endpoint.subscribeHandler(subscribe -> {
105 111 // 提取主题名称列表用于日志显示
106 112 List<String> topicNames = subscribe.topicSubscriptions().stream()
... ... @@ -115,22 +121,22 @@ public class IotMqttUpstreamHandler {
115 121 endpoint.subscribeAcknowledge(subscribe.messageId(), grantedQoSLevels);
116 122 });
117 123  
118   - // 5. 设置取消订阅处理器
  124 + // 6. 设置取消订阅处理器
119 125 endpoint.unsubscribeHandler(unsubscribe -> {
120 126 log.debug("[handle][设备取消订阅,客户端 ID: {},主题: {}]", clientId, unsubscribe.topics());
121 127 endpoint.unsubscribeAcknowledge(unsubscribe.messageId());
122 128 });
123 129  
124   - // 6. 设置 QoS 2消息的 PUBREL 处理器
  130 + // 7. 设置 QoS 2消息的 PUBREL 处理器
125 131 endpoint.publishReleaseHandler(endpoint::publishComplete);
126 132  
127   - // 7. 设置断开连接处理器
  133 + // 8. 设置断开连接处理器
128 134 endpoint.disconnectHandler(v -> {
129 135 log.debug("[handle][设备断开连接,客户端 ID: {}]", clientId);
130 136 cleanupConnection(endpoint);
131 137 });
132 138  
133   - // 8. 接受连接
  139 + // 9. 接受连接
134 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 103 port: 1883
104 104 max-message-size: 8192
105 105 connect-timeout-seconds: 60
106   - keep-alive-timeout-seconds: 300
107 106 ssl-enabled: false
108 107  
109 108 --- #################### 日志相关配置 ####################
... ...