diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java index 68a8fd699b..b42e1c0a42 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotAbstractDataSinkConfig.java +++ b/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; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", visible = true) @JsonSubTypes({ @JsonSubTypes.Type(value = IotDataSinkHttpConfig.class, name = "1"), + @JsonSubTypes.Type(value = IotDataSinkTcpConfig.class, name = "2"), + @JsonSubTypes.Type(value = IotDataSinkWebSocketConfig.class, name = "3"), @JsonSubTypes.Type(value = IotDataSinkMqttConfig.class, name = "10"), @JsonSubTypes.Type(value = IotDataSinkRedisConfig.class, name = "21"), @JsonSubTypes.Type(value = IotDataSinkRocketMQConfig.class, name = "30"), diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java new file mode 100644 index 0000000000..3d96f11ceb --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkTcpConfig.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; + +import lombok.Data; + +/** + * IoT TCP 配置 {@link IotAbstractDataSinkConfig} 实现类 + * + * @author HUIHUI + */ +@Data +public class IotDataSinkTcpConfig extends IotAbstractDataSinkConfig { + + /** + * TCP 服务器地址 + */ + private String host; + + /** + * TCP 服务器端口 + */ + private Integer port; + + /** + * 连接超时时间(毫秒) + */ + private Integer connectTimeoutMs = 5000; + + /** + * 读取超时时间(毫秒) + */ + private Integer readTimeoutMs = 10000; + + /** + * 是否启用 SSL + */ + private Boolean ssl = false; + + /** + * SSL 证书路径(当 ssl=true 时需要) + */ + private String sslCertPath; + + /** + * 数据格式:JSON 或 BINARY + */ + private String dataFormat = "JSON"; + + /** + * 心跳间隔时间(毫秒),0 表示不启用心跳 + */ + private Long heartbeatIntervalMs = 30000L; + + /** + * 重连间隔时间(毫秒) + */ + private Long reconnectIntervalMs = 5000L; + + /** + * 最大重连次数 + */ + private Integer maxReconnectAttempts = 3; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java new file mode 100644 index 0000000000..f1b7e86d86 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/config/IotDataSinkWebSocketConfig.java @@ -0,0 +1,87 @@ +package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config; + +import lombok.Data; + +/** + * IoT WebSocket 配置 {@link IotAbstractDataSinkConfig} 实现类 + *

+ * 配置设备消息通过 WebSocket 协议发送到外部 WebSocket 服务器 + * 支持 WebSocket (ws://) 和 WebSocket Secure (wss://) 连接 + * + * @author HUIHUI + */ +@Data +public class IotDataSinkWebSocketConfig extends IotAbstractDataSinkConfig { + + /** + * WebSocket 服务器地址 + * 例如:ws://localhost:8080/ws 或 wss://example.com/ws + */ + private String serverUrl; + + /** + * 连接超时时间(毫秒) + */ + private Integer connectTimeoutMs = 5000; + + /** + * 发送超时时间(毫秒) + */ + private Integer sendTimeoutMs = 10000; + + /** + * 心跳间隔时间(毫秒),0 表示不启用心跳 + */ + private Long heartbeatIntervalMs = 30000L; + + /** + * 心跳消息内容(JSON 格式) + */ + private String heartbeatMessage = "{\"type\":\"heartbeat\"}"; + + /** + * 子协议列表(逗号分隔) + */ + private String subprotocols; + + /** + * 自定义请求头(JSON 格式) + */ + private String customHeaders; + + /** + * 是否启用 SSL 证书验证(仅对 wss:// 生效) + */ + private Boolean verifySslCert = true; + + /** + * 数据格式:JSON 或 TEXT + */ + private String dataFormat = "JSON"; + + /** + * 重连间隔时间(毫秒) + */ + private Long reconnectIntervalMs = 5000L; + + /** + * 最大重连次数 + */ + private Integer maxReconnectAttempts = 3; + + /** + * 是否启用压缩 + */ + private Boolean enableCompression = false; + + /** + * 消息发送重试次数 + */ + private Integer sendRetryCount = 1; + + /** + * 消息发送重试间隔(毫秒) + */ + private Long sendRetryIntervalMs = 1000L; + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java index 1187677e54..c8041a673c 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/redis/RedisKeyConstants.java +++ b/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 { */ String DATA_SINK = "iot:data_sink"; + /** + * 场景联动规则的数据缓存,使用 Spring Cache 操作 + *

+ * KEY 格式:scene_rule_list_${productId}_${deviceId} + * VALUE 数据类型:String 数组(JSON),即 {@link cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO} 列表 + */ + String SCENE_RULE_LIST = "iot:scene_rule_list"; + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java new file mode 100644 index 0000000000..e9810eb08d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleAction.java @@ -0,0 +1,97 @@ +package cn.iocoder.yudao.module.iot.service.rule.data.action; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig; +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +/** + * TCP 的 {@link IotDataRuleAction} 实现类 + *

+ * 负责将设备消息发送到外部 TCP 服务器 + * 支持普通 TCP 和 SSL TCP 连接,支持 JSON 和 BINARY 数据格式 + * 使用连接池管理 TCP 连接,提高性能和资源利用率 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class IotTcpDataRuleAction extends + IotDataRuleCacheableAction { + + private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5); + private static final Duration SEND_TIMEOUT = Duration.ofSeconds(10); + + @Override + public Integer getType() { + return IotDataSinkTypeEnum.TCP.getType(); + } + + @Override + protected IotTcpClient initProducer(IotDataSinkTcpConfig config) throws Exception { + // 1. 参数校验 + if (config.getHost() == null || config.getHost().trim().isEmpty()) { + throw new IllegalArgumentException("TCP 服务器地址不能为空"); + } + if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) { + throw new IllegalArgumentException("TCP 服务器端口无效"); + } + + // 2. 创建 TCP 客户端 + IotTcpClient tcpClient = new IotTcpClient( + config.getHost(), + config.getPort(), + config.getConnectTimeoutMs(), + config.getReadTimeoutMs(), + config.getSsl(), + config.getSslCertPath(), + config.getDataFormat() + ); + + // 3. 连接服务器 + tcpClient.connect(); + + log.info("[initProducer][TCP 客户端创建并连接成功,服务器: {}:{},SSL: {},数据格式: {}]", + config.getHost(), config.getPort(), config.getSsl(), config.getDataFormat()); + + return tcpClient; + } + + @Override + protected void closeProducer(IotTcpClient producer) throws Exception { + if (producer != null) { + producer.close(); + } + } + + @Override + protected void execute(IotDeviceMessage message, IotDataSinkTcpConfig config) throws Exception { + try { + // 1. 获取或创建 TCP 客户端 + IotTcpClient tcpClient = getProducer(config); + + // 2. 检查连接状态,如果断开则重新连接 + if (!tcpClient.isConnected()) { + log.warn("[execute][TCP 连接已断开,尝试重新连接,服务器: {}:{}]", config.getHost(), config.getPort()); + tcpClient.connect(); + } + + // 3. 发送消息并等待结果 + tcpClient.sendMessage(message); + + // 4. 记录发送成功日志 + log.info("[execute][message({}) config({}) 发送成功,TCP 服务器: {}:{}]", + message, config, config.getHost(), config.getPort()); + + } catch (Exception e) { + log.error("[execute][message({}) config({}) 发送失败,TCP 服务器: {}:{}]", + message, config, config.getHost(), config.getPort(), e); + throw e; + } + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java new file mode 100644 index 0000000000..052c43ed4c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/tcp/IotTcpClient.java @@ -0,0 +1,184 @@ +package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import lombok.extern.slf4j.Slf4j; + +import javax.net.ssl.SSLSocketFactory; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * IoT TCP 客户端 + *

+ * 负责与外部 TCP 服务器建立连接并发送设备消息 + * 支持 JSON 和 BINARY 两种数据格式,支持 SSL 加密连接 + * + * @author HUIHUI + */ +@Slf4j +public class IotTcpClient { + + private final String host; + private final Integer port; + private final Integer connectTimeoutMs; + private final Integer readTimeoutMs; + private final Boolean ssl; + private final String sslCertPath; + private final String dataFormat; + + private Socket socket; + private OutputStream outputStream; + private BufferedReader reader; + private final AtomicBoolean connected = new AtomicBoolean(false); + + public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs, + Boolean ssl, String sslCertPath, String dataFormat) { + this.host = host; + this.port = port; + this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : 5000; + this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : 10000; + this.ssl = ssl != null ? ssl : false; + this.sslCertPath = sslCertPath; + this.dataFormat = dataFormat != null ? dataFormat : "JSON"; + } + + /** + * 连接到 TCP 服务器 + */ + public void connect() throws Exception { + if (connected.get()) { + log.warn("[connect][TCP 客户端已经连接,无需重复连接]"); + return; + } + + try { + if (ssl) { + // SSL 连接 + SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault(); + socket = sslSocketFactory.createSocket(); + } else { + // 普通连接 + socket = new Socket(); + } + + // 连接服务器 + socket.connect(new InetSocketAddress(host, port), connectTimeoutMs); + socket.setSoTimeout(readTimeoutMs); + + // 获取输入输出流 + outputStream = socket.getOutputStream(); + reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8)); + + connected.set(true); + log.info("[connect][TCP 客户端连接成功,服务器地址: {}:{}]", host, port); + + } catch (Exception e) { + close(); + log.error("[connect][TCP 客户端连接失败,服务器地址: {}:{}]", host, port, e); + throw e; + } + } + + /** + * 发送设备消息 + * + * @param message 设备消息 + * @throws Exception 发送异常 + */ + public void sendMessage(IotDeviceMessage message) throws Exception { + if (!connected.get()) { + throw new IllegalStateException("TCP 客户端未连接"); + } + + try { + String messageData; + if ("JSON".equalsIgnoreCase(dataFormat)) { + // JSON 格式 + messageData = JsonUtils.toJsonString(message); + } else { + // BINARY 格式(这里简化为字符串,实际可能需要自定义二进制协议) + messageData = message.toString(); + } + + // 发送消息 + outputStream.write(messageData.getBytes(StandardCharsets.UTF_8)); + outputStream.write('\n'); // 添加换行符作为消息分隔符 + outputStream.flush(); + + log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]", + message.getDeviceId(), messageData.length()); + + } catch (Exception e) { + log.error("[sendMessage][发送消息失败,设备 ID: {}]", message.getDeviceId(), e); + throw e; + } + } + + /** + * 关闭连接 + */ + public void close() { + if (!connected.get()) { + return; + } + + try { + // 关闭资源 + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + log.warn("[close][关闭输入流失败]", e); + } + } + if (outputStream != null) { + try { + outputStream.close(); + } catch (IOException e) { + log.warn("[close][关闭输出流失败]", e); + } + } + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + log.warn("[close][关闭 Socket 失败]", e); + } + } + + connected.set(false); + log.info("[close][TCP 客户端连接已关闭,服务器地址: {}:{}]", host, port); + + } catch (Exception e) { + log.error("[close][关闭 TCP 客户端连接异常]", e); + } + } + + /** + * 检查连接状态 + * + * @return 是否已连接 + */ + public boolean isConnected() { + return connected.get() && socket != null && !socket.isClosed(); + } + + @Override + public String toString() { + return "IotTcpClient{" + + "host='" + host + '\'' + + ", port=" + port + + ", ssl=" + ssl + + ", dataFormat='" + dataFormat + '\'' + + ", connected=" + connected.get() + + '}'; + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java index c631e34586..a29ff98616 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java +++ b/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; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper; +import cn.iocoder.yudao.module.iot.dal.redis.RedisKeyConstants; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.product.IotProductService; @@ -24,6 +25,8 @@ import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatche import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; @@ -60,6 +63,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { private IotSceneRuleTimerHandler timerHandler; @Override + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) public Long createSceneRule(IotSceneRuleSaveReqVO createReqVO) { IotSceneRuleDO sceneRule = BeanUtils.toBean(createReqVO, IotSceneRuleDO.class); sceneRuleMapper.insert(sceneRule); @@ -71,6 +75,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { } @Override + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) public void updateSceneRule(IotSceneRuleSaveReqVO updateReqVO) { // 校验存在 validateSceneRuleExists(updateReqVO.getId()); @@ -83,6 +88,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { } @Override + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) public void updateSceneRuleStatus(Long id, Integer status) { // 1. 校验存在 validateSceneRuleExists(id); @@ -105,6 +111,7 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { } @Override + @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) public void deleteSceneRule(Long id) { // 1. 校验存在 validateSceneRuleExists(id); @@ -149,15 +156,12 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { return sceneRuleMapper.selectListByStatus(status); } - // TODO @puhui999:缓存待实现 @Override + @Cacheable(value = RedisKeyConstants.SCENE_RULE_LIST, key = "#productId + '_' + #deviceId ") @TenantIgnore // 忽略租户隔离:因为 IotSceneRuleMessageHandler 调用时,一般未传递租户,所以需要忽略 public List getSceneRuleListByProductIdAndDeviceIdFromCache(Long productId, Long deviceId) { // 1. 查询启用状态的规则场景 - // TODO @puhui999:这里查询 enable 的; - List list = sceneRuleMapper.selectList(); - List enabledList = filterList(list, - sceneRule -> CommonStatusEnum.isEnable(sceneRule.getStatus())); + List enabledList = sceneRuleMapper.selectList(IotSceneRuleDO::getStatus, CommonStatusEnum.ENABLE.getStatus()); // 2. 根据 productKey 和 deviceName 进行匹配 return filterList(enabledList, sceneRule -> { @@ -190,9 +194,10 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @Override public void executeSceneRuleByDevice(IotDeviceMessage message) { - // TODO @puhui999:这里的 tenantId,通过设备获取; - TenantUtils.execute(message.getTenantId(), () -> { - // 1. 获得设备匹配的规则场景 + // 1.1 这里的 tenantId,通过设备获取; + IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId()); + TenantUtils.execute(device.getTenantId(), () -> { + // 1.2 获得设备匹配的规则场景 List sceneRules = getMatchedSceneRuleListByMessage(message); if (CollUtil.isEmpty(sceneRules)) { return; @@ -238,14 +243,14 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { // 1. 匹配设备 // TODO 缓存 @puhui999:可能需要 getSelf() // 1.1 通过 deviceId 获取设备信息 - IotDeviceDO device = deviceService.getDeviceFromCache(message.getDeviceId()); + IotDeviceDO device = getSelf().deviceService.getDeviceFromCache(message.getDeviceId()); if (device == null) { log.warn("[getMatchedSceneRuleListByMessage][设备({}) 不存在]", message.getDeviceId()); return List.of(); } // 1.2 通过 productId 获取产品信息 - IotProductDO product = productService.getProductFromCache(device.getProductId()); + IotProductDO product = getSelf().productService.getProductFromCache(device.getProductId()); if (product == null) { log.warn("[getMatchedSceneRuleListByMessage][产品({}) 不存在]", device.getProductId()); return List.of(); diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java similarity index 98% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java index cb85f36203..2083bebac9 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java +++ b/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; import java.time.format.DateTimeFormatter; import java.util.List; -// TODO @puhui999:是不是 IoT 的前缀,都加下哈; /** * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑 * @@ -24,7 +23,7 @@ import java.util.List; */ @Component @Slf4j -public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher { +public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher { /** * 时间格式化器 - HH:mm:ss diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcher.java index f4316c0b5d..c130c55438 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java +++ b/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; * @author HUIHUI */ @Component -public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher { +public class IotDevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher { @Override public IotSceneRuleConditionTypeEnum getSupportedConditionType() { diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcher.java index ceb1ee8c8b..232812270f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java +++ b/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; * @author HUIHUI */ @Component -public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher { +public class IotDeviceStateConditionMatcher implements IotSceneRuleConditionMatcher { @Override public IotSceneRuleConditionTypeEnum getSupportedConditionType() { diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcher.java index 2a843d1c2c..825b5eae1d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java +++ b/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; * @author HUIHUI */ @Component -public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher { +public class IotDeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher { @Override public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcher.java index fdaf68e3f1..27cb02a1a5 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java +++ b/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; * @author HUIHUI */ @Component -public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher { +public class IotDevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher { @Override public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java similarity index 95% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcher.java index 2c357fb1e2..b5fa0330dc 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java +++ b/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; * @author HUIHUI */ @Component -public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher { +public class IotDeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher { @Override public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { @@ -44,7 +44,7 @@ public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMat // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可 // 不需要检查操作符和值,因为服务调用本身就是触发条件 - // TODO @puhui999: 服务调用时校验输入参数是否匹配条件 + // TODO @puhui999: 服务调用时校验输入参数是否匹配条件? IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); return true; } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcher.java index b51716f4e3..6b8c73a501 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java +++ b/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; * @author HUIHUI */ @Component -public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher { +public class IotDeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher { @Override public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcher.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java rename to yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcher.java index 34cc59507d..f980c2471b 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java +++ b/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; * @author HUIHUI */ @Component -public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher { +public class IotTimerTriggerMatcher implements IotSceneRuleTriggerMatcher { @Override public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java new file mode 100644 index 0000000000..3a7ee1cf73 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotTcpDataRuleActionTest.java @@ -0,0 +1,161 @@ +package cn.iocoder.yudao.module.iot.service.rule.data.action; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig; +import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link IotTcpDataRuleAction} 的单元测试 + * + * @author HUIHUI + */ +class IotTcpDataRuleActionTest { + + private IotTcpDataRuleAction tcpDataRuleAction; + + @Mock + private IotTcpClient mockTcpClient; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + tcpDataRuleAction = new IotTcpDataRuleAction(); + } + + @Test + void testGetType() { + // 准备参数 + Integer expectedType = 2; // 数据接收类型枚举中 TCP 类型的值 + + // 调用方法 + Integer actualType = tcpDataRuleAction.getType(); + + // 断言结果 + assertEquals(expectedType, actualType); + } + + @Test + void testInitProducer_Success() throws Exception { + // 准备参数 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); + config.setHost("localhost"); + config.setPort(8080); + config.setDataFormat("JSON"); + config.setSsl(false); + + // 调用方法 & 断言结果 + // 此测试需要实际的 TCP 连接,在单元测试中可能不可用 + // 目前我们只验证配置是否有效 + assertNotNull(config.getHost()); + assertTrue(config.getPort() > 0 && config.getPort() <= 65535); + } + + @Test + void testInitProducer_InvalidHost() { + // 准备参数 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); + config.setHost(""); + config.setPort(8080); + + // 调用方法 & 断言结果 + IotTcpDataRuleAction action = new IotTcpDataRuleAction(); + + // 测试验证逻辑(通常在 initProducer 方法中) + assertThrows(IllegalArgumentException.class, () -> { + if (config.getHost() == null || config.getHost().trim().isEmpty()) { + throw new IllegalArgumentException("TCP 服务器地址不能为空"); + } + }); + } + + @Test + void testInitProducer_InvalidPort() { + // 准备参数 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); + config.setHost("localhost"); + config.setPort(-1); + + // 调用方法 & 断言结果 + assertThrows(IllegalArgumentException.class, () -> { + if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) { + throw new IllegalArgumentException("TCP 服务器端口无效"); + } + }); + } + + @Test + void testCloseProducer() throws Exception { + // 准备参数 + IotTcpClient client = mock(IotTcpClient.class); + + // 调用方法 + tcpDataRuleAction.closeProducer(client); + + // 断言结果 + verify(client, times(1)).close(); + } + + @Test + void testExecute_WithValidConfig() { + // 准备参数 + IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report", + "{\"temperature\": 25.5, \"humidity\": 60}"); + + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); + config.setHost("localhost"); + config.setPort(8080); + config.setDataFormat("JSON"); + + // 调用方法 & 断言结果 + // 通常这需要实际的 TCP 连接 + // 在单元测试中,我们只验证输入参数 + assertNotNull(message); + assertNotNull(config); + assertNotNull(config.getHost()); + assertTrue(config.getPort() > 0); + } + + @Test + void testConfig_DefaultValues() { + // 准备参数 + IotDataSinkTcpConfig config = new IotDataSinkTcpConfig(); + + // 调用方法 & 断言结果 + // 验证默认值 + assertEquals("JSON", config.getDataFormat()); + assertEquals(5000, config.getConnectTimeoutMs()); + assertEquals(10000, config.getReadTimeoutMs()); + assertEquals(false, config.getSsl()); + assertEquals(30000L, config.getHeartbeatIntervalMs()); + assertEquals(5000L, config.getReconnectIntervalMs()); + assertEquals(3, config.getMaxReconnectAttempts()); + } + + @Test + void testMessageSerialization() { + // 准备参数 + IotDeviceMessage message = IotDeviceMessage.builder() + .deviceId(123L) + .method("thing.property.report") + .params("{\"temperature\": 25.5}") + .build(); + + // 调用方法 + String json = JsonUtils.toJsonString(message); + + // 断言结果 + assertNotNull(json); + assertTrue(json.contains("\"deviceId\":123")); + assertTrue(json.contains("\"method\":\"thing.property.report\"")); + assertTrue(json.contains("\"temperature\":25.5")); + } + +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/BaseMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java similarity index 88% rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/BaseMatcherTest.java rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotBaseConditionMatcherTest.java index b53e48da3a..5be63b57d2 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/BaseMatcherTest.java +++ b/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; import org.springframework.context.annotation.Configuration; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -// TODO @puhui999:建议改成 IotBaseConditionMatcherTest /** * Matcher 测试基类 * 提供通用的 Spring 测试配置 @@ -14,7 +13,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; * @author HUIHUI */ @SpringJUnitConfig -public abstract class BaseMatcherTest { +public abstract class IotBaseConditionMatcherTest { /** * 注入一下 SpringUtil,解析 EL 表达式时需要 diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcherTest.java index f14da99a56..586d948cd0 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java +++ b/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; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -16,17 +16,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString import static org.junit.jupiter.api.Assertions.*; /** - * {@link CurrentTimeConditionMatcher} 的单元测试 + * {@link IotCurrentTimeConditionMatcher} 的单元测试 * * @author HUIHUI */ -public class CurrentTimeConditionMatcherTest extends BaseMatcherTest { +public class IotCurrentTimeConditionMatcherTest extends IotBaseConditionMatcherTest { - private CurrentTimeConditionMatcher matcher; + private IotCurrentTimeConditionMatcher matcher; @BeforeEach public void setUp() { - matcher = new CurrentTimeConditionMatcher(); + matcher = new IotCurrentTimeConditionMatcher(); } @Test diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDevicePropertyConditionMatcherTest.java index ddfebc66be..5a40995567 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java +++ b/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; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,17 +15,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId import static org.junit.jupiter.api.Assertions.*; /** - * {@link DevicePropertyConditionMatcher} 的单元测试 + * {@link IotDevicePropertyConditionMatcher} 的单元测试 * * @author HUIHUI */ -public class DevicePropertyConditionMatcherTest extends BaseMatcherTest { +public class IotDevicePropertyConditionMatcherTest extends IotBaseConditionMatcherTest { - private DevicePropertyConditionMatcher matcher; + private IotDevicePropertyConditionMatcher matcher; @BeforeEach public void setUp() { - matcher = new DevicePropertyConditionMatcher(); + matcher = new IotDevicePropertyConditionMatcher(); } @Test diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotDeviceStateConditionMatcherTest.java index d54575ba41..da59077a6e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java +++ b/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; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,17 +14,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString import static org.junit.jupiter.api.Assertions.*; /** - * {@link DeviceStateConditionMatcher} 的单元测试 + * {@link IotDeviceStateConditionMatcher} 的单元测试 * * @author HUIHUI */ -public class DeviceStateConditionMatcherTest extends BaseMatcherTest { +public class IotDeviceStateConditionMatcherTest extends IotBaseConditionMatcherTest { - private DeviceStateConditionMatcher matcher; + private IotDeviceStateConditionMatcher matcher; @BeforeEach public void setUp() { - matcher = new DeviceStateConditionMatcher(); + matcher = new IotDeviceStateConditionMatcher(); } @Test diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceEventPostTriggerMatcherTest.java index 4569d439cc..9cf51421fe 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java +++ b/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; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -18,17 +18,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString import static org.junit.jupiter.api.Assertions.*; /** - * {@link DeviceEventPostTriggerMatcher} 的单元测试 + * {@link IotDeviceEventPostTriggerMatcher} 的单元测试 * * @author HUIHUI */ -public class DeviceEventPostTriggerMatcherTest extends BaseMatcherTest { +public class IotDeviceEventPostTriggerMatcherTest extends IotBaseConditionMatcherTest { - private DeviceEventPostTriggerMatcher matcher; + private IotDeviceEventPostTriggerMatcher matcher; @BeforeEach public void setUp() { - matcher = new DeviceEventPostTriggerMatcher(); + matcher = new IotDeviceEventPostTriggerMatcher(); } @Test diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDevicePropertyPostTriggerMatcherTest.java index eb191eb927..fb155763aa 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java +++ b/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; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,17 +20,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString import static org.junit.jupiter.api.Assertions.*; /** - * {@link DevicePropertyPostTriggerMatcher} 的单元测试 + * {@link IotDevicePropertyPostTriggerMatcher} 的单元测试 * * @author HUIHUI */ -public class DevicePropertyPostTriggerMatcherTest extends BaseMatcherTest { +public class IotDevicePropertyPostTriggerMatcherTest extends IotBaseConditionMatcherTest { - private DevicePropertyPostTriggerMatcher matcher; + private IotDevicePropertyPostTriggerMatcher matcher; @BeforeEach public void setUp() { - matcher = new DevicePropertyPostTriggerMatcher(); + matcher = new IotDevicePropertyPostTriggerMatcher(); } @Test diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java similarity index 97% rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceServiceInvokeTriggerMatcherTest.java index cfa36605f8..a515f1268e 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java +++ b/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; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -18,17 +18,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString import static org.junit.jupiter.api.Assertions.*; /** - * {@link DeviceServiceInvokeTriggerMatcher} 的单元测试 + * {@link IotDeviceServiceInvokeTriggerMatcher} 的单元测试 * * @author HUIHUI */ -public class DeviceServiceInvokeTriggerMatcherTest extends BaseMatcherTest { +public class IotDeviceServiceInvokeTriggerMatcherTest extends IotBaseConditionMatcherTest { - private DeviceServiceInvokeTriggerMatcher matcher; + private IotDeviceServiceInvokeTriggerMatcher matcher; @BeforeEach public void setUp() { - matcher = new DeviceServiceInvokeTriggerMatcher(); + matcher = new IotDeviceServiceInvokeTriggerMatcher(); } @Test diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java similarity index 92% rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java index bee6e072e4..2e8b17a353 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotDeviceStateUpdateTriggerMatcherTest.java @@ -1,42 +1,30 @@ package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; -import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomLongId; import static org.junit.jupiter.api.Assertions.*; /** - * {@link DeviceStateUpdateTriggerMatcher} 的单元测试 + * {@link IotDeviceStateUpdateTriggerMatcher} 的单元测试 * * @author HUIHUI */ -@SpringJUnitConfig(DeviceStateUpdateTriggerMatcherTest.TestConfig.class) -public class DeviceStateUpdateTriggerMatcherTest { +public class IotDeviceStateUpdateTriggerMatcherTest extends IotBaseConditionMatcherTest { - @Configuration - static class TestConfig { - @Bean - public SpringUtil springUtil() { - return new SpringUtil(); - } - } - - private DeviceStateUpdateTriggerMatcher matcher; + private IotDeviceStateUpdateTriggerMatcher matcher; @BeforeEach public void setUp() { - matcher = new DeviceStateUpdateTriggerMatcher(); + matcher = new IotDeviceStateUpdateTriggerMatcher(); } @Test diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java similarity index 96% rename from yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java rename to yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotTimerTriggerMatcherTest.java index f332f3097b..df47e6c28f 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java +++ b/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; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; -import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.BaseMatcherTest; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotBaseConditionMatcherTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -12,17 +12,17 @@ import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString import static org.junit.jupiter.api.Assertions.*; /** - * {@link TimerTriggerMatcher} 的单元测试 + * {@link IotTimerTriggerMatcher} 的单元测试 * * @author HUIHUI */ -public class TimerTriggerMatcherTest extends BaseMatcherTest { +public class IotTimerTriggerMatcherTest extends IotBaseConditionMatcherTest { - private TimerTriggerMatcher matcher; + private IotTimerTriggerMatcher matcher; @BeforeEach public void setUp() { - matcher = new TimerTriggerMatcher(); + matcher = new IotTimerTriggerMatcher(); } @Test diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java new file mode 100644 index 0000000000..3e38e93616 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotSceneRuleTimerHandlerTest.java @@ -0,0 +1,126 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.timer; + +import cn.hutool.core.collection.ListUtil; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; +import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.quartz.SchedulerException; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * {@link IotSceneRuleTimerHandler} 的单元测试类 + * + * @author HUIHUI + */ +@ExtendWith(MockitoExtension.class) +public class IotSceneRuleTimerHandlerTest { + + @Mock + private IotSchedulerManager schedulerManager; + + @InjectMocks + private IotSceneRuleTimerHandler timerHandler; + + @BeforeEach + void setUp() { + // 重置 Mock 对象 + reset(schedulerManager); + } + + @Test + public void testRegisterTimerTriggers_success() throws SchedulerException { + // 准备参数 + Long sceneRuleId = 1L; + IotSceneRuleDO sceneRule = new IotSceneRuleDO(); + sceneRule.setId(sceneRuleId); + sceneRule.setStatus(0); // 0 表示启用 + // 创建定时触发器 + IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger(); + timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); + timerTrigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点 + sceneRule.setTriggers(ListUtil.toList(timerTrigger)); + + // 调用 + timerHandler.registerTimerTriggers(sceneRule); + + // 验证 + verify(schedulerManager, times(1)).addOrUpdateJob( + eq(IotSceneRuleJob.class), + eq("iot_scene_rule_timer_" + sceneRuleId), + eq("0 0 12 * * ?"), + eq(IotSceneRuleJob.buildJobDataMap(sceneRuleId)) + ); + } + + @Test + public void testRegisterTimerTriggers_noTimerTrigger() throws SchedulerException { + // 准备参数 - 没有定时触发器 + IotSceneRuleDO sceneRule = new IotSceneRuleDO(); + sceneRule.setStatus(0); // 0 表示启用 + // 创建非定时触发器 + IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger(); + deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType()); + sceneRule.setTriggers(ListUtil.toList(deviceTrigger)); + + // 调用 + timerHandler.registerTimerTriggers(sceneRule); + + // 验证 - 不应该调用调度器 + verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any()); + } + + @Test + public void testRegisterTimerTriggers_emptyCronExpression() throws SchedulerException { + // 准备参数 - CRON 表达式为空 + Long sceneRuleId = 2L; + IotSceneRuleDO sceneRule = new IotSceneRuleDO(); + sceneRule.setId(sceneRuleId); + sceneRule.setStatus(0); // 0 表示启用 + // 创建定时触发器但没有 CRON 表达式 + IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger(); + timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); + timerTrigger.setCronExpression(""); // 空的 CRON 表达式 + sceneRule.setTriggers(ListUtil.toList(timerTrigger)); + + // 调用 + timerHandler.registerTimerTriggers(sceneRule); + + // 验证 - 不应该调用调度器 + verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any()); + } + + @Test + public void testUnregisterTimerTriggers_success() throws SchedulerException { + // 准备参数 + Long sceneRuleId = 3L; + + // 调用 + timerHandler.unregisterTimerTriggers(sceneRuleId); + + // 验证 + verify(schedulerManager, times(1)).deleteJob("iot_scene_rule_timer_" + sceneRuleId); + } + + @Test + public void testPauseTimerTriggers_success() throws SchedulerException { + // 准备参数 + Long sceneRuleId = 4L; + + // 调用 + timerHandler.pauseTimerTriggers(sceneRuleId); + + // 验证 + verify(schedulerManager, times(1)).pauseJob("iot_scene_rule_timer_" + sceneRuleId); + } + +}