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