Commit c1657149a85d8c5719ec444da6ff3529c9077e3b

Authored by 芋道源码
Committed by Gitee
2 parents e639dbfe c5b4968e

!1434 完善 code review 提到的一些问题

Merge pull request !1434 from puhui999/feature/iot
Showing 26 changed files with 793 additions and 74 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/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. 参数校验
  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. 创建 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 +
  55 + // 3. 连接服务器
  56 + tcpClient.connect();
  57 +
  58 + log.info("[initProducer][TCP 客户端创建并连接成功,服务器: {}:{},SSL: {},数据格式: {}]",
  59 + config.getHost(), config.getPort(), config.getSsl(), config.getDataFormat());
  60 +
  61 + return tcpClient;
  62 + }
  63 +
  64 + @Override
  65 + protected void closeProducer(IotTcpClient producer) throws Exception {
  66 + if (producer != null) {
  67 + producer.close();
  68 + }
  69 + }
  70 +
  71 + @Override
  72 + protected void execute(IotDeviceMessage message, IotDataSinkTcpConfig config) throws Exception {
  73 + try {
  74 + // 1. 获取或创建 TCP 客户端
  75 + IotTcpClient tcpClient = getProducer(config);
  76 +
  77 + // 2. 检查连接状态,如果断开则重新连接
  78 + if (!tcpClient.isConnected()) {
  79 + log.warn("[execute][TCP 连接已断开,尝试重新连接,服务器: {}:{}]", config.getHost(), config.getPort());
  80 + tcpClient.connect();
  81 + }
  82 +
  83 + // 3. 发送消息并等待结果
  84 + tcpClient.sendMessage(message);
  85 +
  86 + // 4. 记录发送成功日志
  87 + log.info("[execute][message({}) config({}) 发送成功,TCP 服务器: {}:{}]",
  88 + message, config, config.getHost(), config.getPort());
  89 +
  90 + } catch (Exception e) {
  91 + log.error("[execute][message({}) config({}) 发送失败,TCP 服务器: {}:{}]",
  92 + message, config, config.getHost(), config.getPort(), e);
  93 + throw e;
  94 + }
  95 + }
  96 +
  97 +}
0 98 \ 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 + public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs,
  42 + Boolean ssl, String sslCertPath, String dataFormat) {
  43 + this.host = host;
  44 + this.port = port;
  45 + this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : 5000;
  46 + this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : 10000;
  47 + this.ssl = ssl != null ? ssl : false;
  48 + this.sslCertPath = sslCertPath;
  49 + this.dataFormat = dataFormat != null ? dataFormat : "JSON";
  50 + }
  51 +
  52 + /**
  53 + * 连接到 TCP 服务器
  54 + */
  55 + public void connect() throws Exception {
  56 + if (connected.get()) {
  57 + log.warn("[connect][TCP 客户端已经连接,无需重复连接]");
  58 + return;
  59 + }
  60 +
  61 + try {
  62 + if (ssl) {
  63 + // SSL 连接
  64 + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
  65 + socket = sslSocketFactory.createSocket();
  66 + } else {
  67 + // 普通连接
  68 + socket = new Socket();
  69 + }
  70 +
  71 + // 连接服务器
  72 + socket.connect(new InetSocketAddress(host, port), connectTimeoutMs);
  73 + socket.setSoTimeout(readTimeoutMs);
  74 +
  75 + // 获取输入输出流
  76 + outputStream = socket.getOutputStream();
  77 + reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
  78 +
  79 + connected.set(true);
  80 + log.info("[connect][TCP 客户端连接成功,服务器地址: {}:{}]", host, port);
  81 +
  82 + } catch (Exception e) {
  83 + close();
  84 + log.error("[connect][TCP 客户端连接失败,服务器地址: {}:{}]", host, port, e);
  85 + throw e;
  86 + }
  87 + }
  88 +
  89 + /**
  90 + * 发送设备消息
  91 + *
  92 + * @param message 设备消息
  93 + * @throws Exception 发送异常
  94 + */
  95 + public void sendMessage(IotDeviceMessage message) throws Exception {
  96 + if (!connected.get()) {
  97 + throw new IllegalStateException("TCP 客户端未连接");
  98 + }
  99 +
  100 + try {
  101 + String messageData;
  102 + if ("JSON".equalsIgnoreCase(dataFormat)) {
  103 + // JSON 格式
  104 + messageData = JsonUtils.toJsonString(message);
  105 + } else {
  106 + // BINARY 格式(这里简化为字符串,实际可能需要自定义二进制协议)
  107 + messageData = message.toString();
  108 + }
  109 +
  110 + // 发送消息
  111 + outputStream.write(messageData.getBytes(StandardCharsets.UTF_8));
  112 + outputStream.write('\n'); // 添加换行符作为消息分隔符
  113 + outputStream.flush();
  114 +
  115 + log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]",
  116 + message.getDeviceId(), messageData.length());
  117 +
  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 + connected.set(false);
  157 + log.info("[close][TCP 客户端连接已关闭,服务器地址: {}:{}]", host, port);
  158 +
  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,6 +16,7 @@ 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 21 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
21 22 import cn.iocoder.yudao.module.iot.service.product.IotProductService;
... ... @@ -24,6 +25,8 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche
24 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  
... ... @@ -60,6 +63,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
60 63 private IotSceneRuleTimerHandler timerHandler;
61 64  
62 65 @Override
  66 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
63 67 public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) {
64 68 IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class);
65 69 sceneRuleMapper.insert(sceneRule);
... ... @@ -71,6 +75,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
71 75 }
72 76  
73 77 @Override
  78 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
74 79 public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) {
75 80 // 校验存在
76 81 validateSceneRuleExists(updateReqVO.getId());
... ... @@ -83,6 +88,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
83 88 }
84 89  
85 90 @Override
  91 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
86 92 public void updateSceneRuleStatus(Long id, Integer status) {
87 93 // 1. 校验存在
88 94 validateSceneRuleExists(id);
... ... @@ -105,6 +111,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
105 111 }
106 112  
107 113 @Override
  114 + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
108 115 public void deleteSceneRule(Long id) {
109 116 // 1. 校验存在
110 117 validateSceneRuleExists(id);
... ... @@ -149,15 +156,12 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
149 156 return sceneRuleMapper.selectListByStatus(status);
150 157 }
151 158  
152   - // TODO @puhui999:缓存待实现
153 159 @Override
  160 + @Cacheable(value = RedisKeyConstants.SCENE_RULE_LIST, key = "#productId + '_' + #deviceId ")
154 161 @TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略
155 162 public List<IotSceneRuleDO> getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId) {
156 163 // 1. 查询启用状态的规则场景
157   - // TODO @puhui999:这里查询 enable 的;
158   - List<IotSceneRuleDO> list = sceneRuleMapper.selectList();
159   - List<IotSceneRuleDO> enabledList = filterList(list,
160   - sceneRule -> CommonStatusEnum.isEnable(sceneRule.getStatus()));
  164 + List<IotSceneRuleDO> enabledList = sceneRuleMapper.selectList(IotSceneRuleDO::getStatus, CommonStatusEnum.ENABLE.getStatus());
161 165  
162 166 // 2. 根据 productKey 和 deviceName 进行匹配
163 167 return filterList(enabledList, sceneRule -> {
... ... @@ -190,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
190 194  
191 195 @Override
192 196 public void executeSceneRuleByDevice(IotDeviceMessage message) {
193   - // TODO @puhui999:这里的 tenantId,通过设备获取;
194   - TenantUtils.execute(message.getTenantId(), () -> {
195   - // 1. 获得设备匹配的规则场景
  197 + // 1.1 这里的 tenantId,通过设备获取;
  198 + IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
  199 + TenantUtils.execute(device.getTenantId(), () -> {
  200 + // 1.2 获得设备匹配的规则场景
196 201 List<IotSceneRuleDO> sceneRules = getMatchedSceneRuleListByMessage(message);
197 202 if (CollUtil.isEmpty(sceneRules)) {
198 203 return;
... ... @@ -238,14 +243,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
238 243 // 1. 匹配设备
239 244 // TODO 缓存 @puhui999:可能需要 getSelf()
240 245 // 1.1 通过 deviceId 获取设备信息
241   - IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId());
  246 + IotDeviceDO device = getSelf().deviceService.getDeviceFromCache(message.getDeviceId());
242 247 if (device == null) {
243 248 log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId());
244 249 return List.of();
245 250 }
246 251  
247 252 // 1.2 通过 productId 获取产品信息
248   - IotProductDO product = productService.getProductFromCache(device.getProductId());
  253 + IotProductDO product = getSelf().productService.getProductFromCache(device.getProductId());
249 254 if (product == null) {
250 255 log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId());
251 256 return List.of();
... ...
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
... ... @@ -16,7 +16,6 @@ import java.time.LocalTime;
16 16 import java.time.format.DateTimeFormatter;
17 17 import java.util.List;
18 18  
19   -// TODO @puhui999:是不是 IoT 的前缀,都加下哈;
20 19 /**
21 20 * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑
22 21 *
... ... @@ -24,7 +23,7 @@ import java.util.List;
24 23 */
25 24 @Component
26 25 @Slf4j
27   -public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
  26 +public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
28 27  
29 28 /**
30 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
... ... @@ -15,7 +15,7 @@ import org.springframework.stereotype.Component;
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() {
... ...
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
... ... @@ -13,7 +13,7 @@ import org.springframework.stereotype.Component;
13 13 * @author HUIHUI
14 14 */
15 15 @Component
16   -public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
  16 +public class IotDeviceStateConditionMatcher implements IotSceneRuleConditionMatcher {
17 17  
18 18 @Override
19 19 public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
... ...
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
... ... @@ -15,7 +15,7 @@ import org.springframework.stereotype.Component;
15 15 * @author HUIHUI
16 16 */
17 17 @Component
18   -public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
  18 +public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
19 19  
20 20 @Override
21 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
... ... @@ -14,7 +14,7 @@ import org.springframework.stereotype.Component;
14 14 * @author HUIHUI
15 15 */
16 16 @Component
17   -public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
  17 +public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher {
18 18  
19 19 @Override
20 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
... ...
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
... ... @@ -14,7 +14,7 @@ import org.springframework.stereotype.Component;
14 14 * @author HUIHUI
15 15 */
16 16 @Component
17   -public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
  17 +public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher {
18 18  
19 19 @Override
20 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
... ... @@ -44,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat
44 44  
45 45 // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可
46 46 // 不需要检查操作符和值,因为服务调用本身就是触发条件
47   - // TODO @puhui999: 服务调用时校验输入参数是否匹配条件
  47 + // TODO @puhui999: 服务调用时校验输入参数是否匹配条件?
48 48 IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger);
49 49 return true;
50 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
... ... @@ -14,7 +14,7 @@ import org.springframework.stereotype.Component;
14 14 * @author HUIHUI
15 15 */
16 16 @Component
17   -public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
  17 +public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher {
18 18  
19 19 @Override
20 20 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
... ...
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
... ... @@ -16,7 +16,7 @@ import org.springframework.stereotype.Component;
16 16 * @author HUIHUI
17 17 */
18 18 @Component
19   -public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
  19 +public class IotTimerTriggerMatcher implements IotSceneRuleTriggerMatcher {
20 20  
21 21 @Override
22 22 public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
... ...
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 + void setUp() {
  29 + MockitoAnnotations.openMocks(this);
  30 + tcpDataRuleAction = new IotTcpDataRuleAction();
  31 + }
  32 +
  33 + @Test
  34 + void testGetType() {
  35 + // 准备参数
  36 + Integer expectedType = 2; // 数据接收类型枚举中 TCP 类型的值
  37 +
  38 + // 调用方法
  39 + Integer actualType = tcpDataRuleAction.getType();
  40 +
  41 + // 断言结果
  42 + assertEquals(expectedType, actualType);
  43 + }
  44 +
  45 + @Test
  46 + void testInitProducer_Success() throws Exception {
  47 + // 准备参数
  48 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  49 + config.setHost("localhost");
  50 + config.setPort(8080);
  51 + config.setDataFormat("JSON");
  52 + config.setSsl(false);
  53 +
  54 + // 调用方法 & 断言结果
  55 + // 此测试需要实际的 TCP 连接,在单元测试中可能不可用
  56 + // 目前我们只验证配置是否有效
  57 + assertNotNull(config.getHost());
  58 + assertTrue(config.getPort() > 0 && config.getPort() <= 65535);
  59 + }
  60 +
  61 + @Test
  62 + void testInitProducer_InvalidHost() {
  63 + // 准备参数
  64 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  65 + config.setHost("");
  66 + config.setPort(8080);
  67 +
  68 + // 调用方法 & 断言结果
  69 + IotTcpDataRuleAction action = new IotTcpDataRuleAction();
  70 +
  71 + // 测试验证逻辑(通常在 initProducer 方法中)
  72 + assertThrows(IllegalArgumentException.class, () -> {
  73 + if (config.getHost() == null || config.getHost().trim().isEmpty()) {
  74 + throw new IllegalArgumentException("TCP 服务器地址不能为空");
  75 + }
  76 + });
  77 + }
  78 +
  79 + @Test
  80 + void testInitProducer_InvalidPort() {
  81 + // 准备参数
  82 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  83 + config.setHost("localhost");
  84 + config.setPort(-1);
  85 +
  86 + // 调用方法 & 断言结果
  87 + assertThrows(IllegalArgumentException.class, () -> {
  88 + if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
  89 + throw new IllegalArgumentException("TCP 服务器端口无效");
  90 + }
  91 + });
  92 + }
  93 +
  94 + @Test
  95 + void testCloseProducer() throws Exception {
  96 + // 准备参数
  97 + IotTcpClient client = mock(IotTcpClient.class);
  98 +
  99 + // 调用方法
  100 + tcpDataRuleAction.closeProducer(client);
  101 +
  102 + // 断言结果
  103 + verify(client, times(1)).close();
  104 + }
  105 +
  106 + @Test
  107 + void testExecute_WithValidConfig() {
  108 + // 准备参数
  109 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report",
  110 + "{\"temperature\": 25.5, \"humidity\": 60}");
  111 +
  112 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  113 + config.setHost("localhost");
  114 + config.setPort(8080);
  115 + config.setDataFormat("JSON");
  116 +
  117 + // 调用方法 & 断言结果
  118 + // 通常这需要实际的 TCP 连接
  119 + // 在单元测试中,我们只验证输入参数
  120 + assertNotNull(message);
  121 + assertNotNull(config);
  122 + assertNotNull(config.getHost());
  123 + assertTrue(config.getPort() > 0);
  124 + }
  125 +
  126 + @Test
  127 + void testConfig_DefaultValues() {
  128 + // 准备参数
  129 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
  130 +
  131 + // 调用方法 & 断言结果
  132 + // 验证默认值
  133 + assertEquals("JSON", config.getDataFormat());
  134 + assertEquals(5000, config.getConnectTimeoutMs());
  135 + assertEquals(10000, config.getReadTimeoutMs());
  136 + assertEquals(false, config.getSsl());
  137 + assertEquals(30000L, config.getHeartbeatIntervalMs());
  138 + assertEquals(5000L, config.getReconnectIntervalMs());
  139 + assertEquals(3, config.getMaxReconnectAttempts());
  140 + }
  141 +
  142 + @Test
  143 + void testMessageSerialization() {
  144 + // 准备参数
  145 + IotDeviceMessage message = IotDeviceMessage.builder()
  146 + .deviceId(123L)
  147 + .method("thing.property.report")
  148 + .params("{\"temperature\": 25.5}")
  149 + .build();
  150 +
  151 + // 调用方法
  152 + String json = JsonUtils.toJsonString(message);
  153 +
  154 + // 断言结果
  155 + assertNotNull(json);
  156 + assertTrue(json.contains("\"deviceId\":123"));
  157 + assertTrue(json.contains("\"method\":\"thing.property.report\""));
  158 + assertTrue(json.contains("\"temperature\":25.5"));
  159 + }
  160 +
  161 +}
0 162 \ 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/BaseMatcherTest.java renamed to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java
... ... @@ -6,7 +6,6 @@ import org.springframework.context.annotation.Bean;
6 6 import org.springframework.context.annotation.Configuration;
7 7 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
8 8  
9   -// TODO @puhui999:建议改成 IotBaseConditionMatcherTest
10 9 /**
11 10 * Matcher 测试基类
12 11 * 提供通用的 Spring 测试配置
... ... @@ -14,7 +13,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
14 13 * @author HUIHUI
15 14 */
16 15 @SpringJUnitConfig
17   -public abstract class BaseMatcherTest {
  16 +public abstract class IotBaseConditionMatcherTest {
18 17  
19 18 /**
20 19 * 注入一下 SpringUtil,解析 EL 表达式时需要
... ...
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
... ... @@ -4,7 +4,7 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
6 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
7   -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
  7 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
8 8 import org.junit.jupiter.api.BeforeEach;
9 9 import org.junit.jupiter.api.Test;
10 10  
... ... @@ -16,17 +16,17 @@ 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 BaseMatcherTest {
  23 +public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest {
24 24  
25   - private CurrentTimeConditionMatcher matcher;
  25 + private IotCurrentTimeConditionMatcher matcher;
26 26  
27 27 @BeforeEach
28 28 public void setUp() {
29   - matcher = new CurrentTimeConditionMatcher();
  29 + matcher = new IotCurrentTimeConditionMatcher();
30 30 }
31 31  
32 32 @Test
... ...
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
... ... @@ -4,7 +4,7 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
6 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
7   -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
  7 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
8 8 import org.junit.jupiter.api.BeforeEach;
9 9 import org.junit.jupiter.api.Test;
10 10  
... ... @@ -15,17 +15,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId
15 15 import static org.junit.jupiter.api.Assertions.*;
16 16  
17 17 /**
18   - * {@link DevicePropertyConditionMatcher} 的单元测试
  18 + * {@link IotDevicePropertyConditionMatcher} 的单元测试
19 19 *
20 20 * @author HUIHUI
21 21 */
22   -public class DevicePropertyConditionMatcherTest extends BaseMatcherTest {
  22 +public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest {
23 23  
24   - private DevicePropertyConditionMatcher matcher;
  24 + private IotDevicePropertyConditionMatcher matcher;
25 25  
26 26 @BeforeEach
27 27 public void setUp() {
28   - matcher = new DevicePropertyConditionMatcher();
  28 + matcher = new IotDevicePropertyConditionMatcher();
29 29 }
30 30  
31 31 @Test
... ...
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
... ... @@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
5 5 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
6 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
7 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
8   -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
9 9 import org.junit.jupiter.api.BeforeEach;
10 10 import org.junit.jupiter.api.Test;
11 11  
... ... @@ -14,17 +14,17 @@ 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 BaseMatcherTest {
  21 +public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest {
22 22  
23   - private DeviceStateConditionMatcher matcher;
  23 + private IotDeviceStateConditionMatcher matcher;
24 24  
25 25 @BeforeEach
26 26 public void setUp() {
27   - matcher = new DeviceStateConditionMatcher();
  27 + matcher = new IotDeviceStateConditionMatcher();
28 28 }
29 29  
30 30 @Test
... ...
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
... ... @@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
8   -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
9 9 import org.junit.jupiter.api.BeforeEach;
10 10 import org.junit.jupiter.api.Test;
11 11  
... ... @@ -18,17 +18,17 @@ 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 BaseMatcherTest {
  25 +public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
26 26  
27   - private DeviceEventPostTriggerMatcher matcher;
  27 + private IotDeviceEventPostTriggerMatcher matcher;
28 28  
29 29 @BeforeEach
30 30 public void setUp() {
31   - matcher = new DeviceEventPostTriggerMatcher();
  31 + matcher = new IotDeviceEventPostTriggerMatcher();
32 32 }
33 33  
34 34 @Test
... ...
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
... ... @@ -6,7 +6,7 @@ import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
8 8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
9   -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
  9 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
10 10 import org.junit.jupiter.api.BeforeEach;
11 11 import org.junit.jupiter.api.Test;
12 12  
... ... @@ -20,17 +20,17 @@ 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 BaseMatcherTest {
  27 +public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest {
28 28  
29   - private DevicePropertyPostTriggerMatcher matcher;
  29 + private IotDevicePropertyPostTriggerMatcher matcher;
30 30  
31 31 @BeforeEach
32 32 public void setUp() {
33   - matcher = new DevicePropertyPostTriggerMatcher();
  33 + matcher = new IotDevicePropertyPostTriggerMatcher();
34 34 }
35 35  
36 36 @Test
... ...
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
... ... @@ -5,7 +5,7 @@ import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
7 7 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
8   -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
  8 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
9 9 import org.junit.jupiter.api.BeforeEach;
10 10 import org.junit.jupiter.api.Test;
11 11  
... ... @@ -18,17 +18,17 @@ 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 BaseMatcherTest {
  25 +public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest {
26 26  
27   - private DeviceServiceInvokeTriggerMatcher matcher;
  27 + private IotDeviceServiceInvokeTriggerMatcher matcher;
28 28  
29 29 @BeforeEach
30 30 public void setUp() {
31   - matcher = new DeviceServiceInvokeTriggerMatcher();
  31 + matcher = new IotDeviceServiceInvokeTriggerMatcher();
32 32 }
33 33  
34 34 @Test
... ...
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.hutool.extra.spring.SpringUtil;
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 10 import org.junit.jupiter.api.BeforeEach;
11 11 import org.junit.jupiter.api.Test;
12   -import org.springframework.context.annotation.Bean;
13   -import org.springframework.context.annotation.Configuration;
14   -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
15 12  
16 13 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId;
17 14 import static org.junit.jupiter.api.Assertions.*;
18 15  
19 16 /**
20   - * {@link DeviceStateUpdateTriggerMatcher} 的单元测试
  17 + * {@link IotDeviceStateUpdateTriggerMatcher} 的单元测试
21 18 *
22 19 * @author HUIHUI
23 20 */
24   -@SpringJUnitConfig(DeviceStateUpdateTriggerMatcherTest.TestConfig.class)
25   -public class DeviceStateUpdateTriggerMatcherTest {
26   -
27   - @Configuration
28   - static class TestConfig {
29   - @Bean
30   - public SpringUtil springUtil() {
31   - return new SpringUtil();
32   - }
33   - }
  21 +public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatcherTest {
34 22  
35   - private DeviceStateUpdateTriggerMatcher matcher;
  23 + private IotDeviceStateUpdateTriggerMatcher matcher;
36 24  
37 25 @BeforeEach
38 26 public void setUp() {
39   - matcher = new DeviceStateUpdateTriggerMatcher();
  27 + matcher = new IotDeviceStateUpdateTriggerMatcher();
40 28 }
41 29  
42 30 @Test
... ...
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
... ... @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger;
3 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
4 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
5 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
6   -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest;
  6 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest;
7 7 import org.junit.jupiter.api.BeforeEach;
8 8 import org.junit.jupiter.api.Test;
9 9  
... ... @@ -12,17 +12,17 @@ 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 BaseMatcherTest {
  19 +public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest {
20 20  
21   - private TimerTriggerMatcher matcher;
  21 + private IotTimerTriggerMatcher matcher;
22 22  
23 23 @BeforeEach
24 24 public void setUp() {
25   - matcher = new TimerTriggerMatcher();
  25 + matcher = new IotTimerTriggerMatcher();
26 26 }
27 27  
28 28 @Test
... ...
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 +}
... ...