From 49bf744b740150c722297ed161702c44ccdf4ac3 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Fri, 15 Aug 2025 15:02:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E3=80=90IoT=20=E7=89=A9=E8=81=94=E7=BD=91?= =?UTF-8?q?=E3=80=91=E6=96=B0=E5=A2=9E=E5=9C=BA=E6=99=AF=E8=A7=84=E5=88=99?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E5=99=A8=E5=8C=B9=E9=85=8D=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=92=8C=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rule/IotSceneRuleTriggerTypeEnum.java | 14 ++ .../AbstractIotSceneRuleTriggerMatcher.java | 122 +++++++++++ .../DeviceEventPostTriggerMatcher.java | 75 +++++++ .../DevicePropertyPostTriggerMatcher.java | 79 +++++++ .../DeviceServiceInvokeTriggerMatcher.java | 61 ++++++ .../DeviceStateUpdateTriggerMatcher.java | 71 +++++++ .../matcher/IotSceneRuleTriggerMatcher.java | 62 ++++++ .../IotSceneRuleTriggerMatcherManager.java | 150 +++++++++++++ .../scene/matcher/TimerTriggerMatcher.java | 79 +++++++ .../IotSceneRuleTriggerMatcherTest.java | 200 ++++++++++++++++++ 10 files changed, 913 insertions(+) create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/AbstractIotSceneRuleTriggerMatcher.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceEventPostTriggerMatcher.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DevicePropertyPostTriggerMatcher.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceServiceInvokeTriggerMatcher.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceStateUpdateTriggerMatcher.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcher.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherManager.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/TimerTriggerMatcher.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherTest.java diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java index 5e502b59d4..b8e37e80c4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotSceneRuleTriggerTypeEnum.java @@ -63,4 +63,18 @@ public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable { return ARRAYS; } + + /** + * 根据类型值查找触发器类型枚举 + * + * @param typeValue 类型值 + * @return 触发器类型枚举 + */ + public static IotSceneRuleTriggerTypeEnum findTriggerTypeEnum(Integer typeValue) { + return Arrays.stream(IotSceneRuleTriggerTypeEnum.values()) + .filter(type -> type.getType().equals(typeValue)) + .findFirst() + .orElse(null); + } + } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/AbstractIotSceneRuleTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/AbstractIotSceneRuleTriggerMatcher.java new file mode 100644 index 0000000000..f2775964fd --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/AbstractIotSceneRuleTriggerMatcher.java @@ -0,0 +1,122 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; + +/** + * IoT 场景规则触发器匹配器抽象基类 + *

+ * 提供通用的条件评估逻辑和工具方法 + * + * @author HUIHUI + */ +@Slf4j +public abstract class AbstractIotSceneRuleTriggerMatcher implements IotSceneRuleTriggerMatcher { + + /** + * 评估条件是否匹配 + * + * @param sourceValue 源值(来自消息) + * @param operator 操作符 + * @param paramValue 参数值(来自条件配置) + * @return 是否匹配 + */ + protected boolean evaluateCondition(Object sourceValue, String operator, String paramValue) { + try { + // 1. 校验操作符是否合法 + IotSceneRuleConditionOperatorEnum operatorEnum = IotSceneRuleConditionOperatorEnum.operatorOf(operator); + if (operatorEnum == null) { + log.warn("[evaluateCondition][存在错误的操作符({})]", operator); + return false; + } + + // 2. 构建 Spring 表达式变量 + Map springExpressionVariables = new HashMap<>(); + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue); + + // 处理参数值 + if (StrUtil.isNotBlank(paramValue)) { + // 处理多值情况(如 IN、BETWEEN 操作符) + if (paramValue.contains(",")) { + List paramValues = StrUtil.split(paramValue, ','); + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE_LIST, + convertList(paramValues, NumberUtil::parseDouble)); + } else { + // 处理单值情况 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE, + NumberUtil.parseDouble(paramValue)); + } + } + + // 3. 计算 Spring 表达式 + return (Boolean) SpringExpressionUtils.parseExpression(operatorEnum.getSpringExpression(), springExpressionVariables); + } catch (Exception e) { + log.error("[evaluateCondition][条件评估异常] sourceValue: {}, operator: {}, paramValue: {}", + sourceValue, operator, paramValue, e); + return false; + } + } + + /** + * 检查基础触发器参数是否有效 + * + * @param trigger 触发器配置 + * @return 是否有效 + */ + protected boolean isBasicTriggerValid(IotSceneRuleDO.Trigger trigger) { + return trigger != null && trigger.getType() != null; + } + + /** + * 检查操作符和值是否有效 + * + * @param trigger 触发器配置 + * @return 是否有效 + */ + protected boolean isOperatorAndValueValid(IotSceneRuleDO.Trigger trigger) { + return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue()); + } + + /** + * 检查标识符是否匹配 + * + * @param expectedIdentifier 期望的标识符 + * @param actualIdentifier 实际的标识符 + * @return 是否匹配 + */ + protected boolean isIdentifierMatched(String expectedIdentifier, String actualIdentifier) { + return StrUtil.isNotBlank(expectedIdentifier) && expectedIdentifier.equals(actualIdentifier); + } + + /** + * 记录匹配成功日志 + * + * @param message 设备消息 + * @param trigger 触发器配置 + */ + protected void logMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + log.debug("[{}][消息({}) 匹配触发器({}) 成功]", getMatcherName(), message.getRequestId(), trigger.getType()); + } + + /** + * 记录匹配失败日志 + * + * @param message 设备消息 + * @param trigger 触发器配置 + * @param reason 失败原因 + */ + protected void logMatchFailure(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, String reason) { + log.debug("[{}][消息({}) 匹配触发器({}) 失败: {}]", getMatcherName(), message.getRequestId(), trigger.getType(), reason); + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceEventPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceEventPostTriggerMatcher.java new file mode 100644 index 0000000000..dd30b068d4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceEventPostTriggerMatcher.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.springframework.stereotype.Component; + +/** + * 设备事件上报触发器匹配器 + *

+ * 处理设备事件上报的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceEventPostTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher { + + /** + * 设备事件上报消息方法 + */ + private static final String DEVICE_EVENT_POST_METHOD = "thing.event.post"; + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1. 基础参数校验 + if (!isBasicTriggerValid(trigger)) { + logMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 2. 检查消息方法是否匹配 + if (!DEVICE_EVENT_POST_METHOD.equals(message.getMethod())) { + logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_EVENT_POST_METHOD + ", 实际: " + message.getMethod()); + return false; + } + + // 3. 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { + logMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 4. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配 + // 但如果配置了操作符和值,则需要进行条件匹配 + if (StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue())) { + Object eventData = message.getData(); + if (eventData == null) { + logMatchFailure(message, trigger, "消息中事件数据为空"); + return false; + } + + boolean matched = evaluateCondition(eventData, trigger.getOperator(), trigger.getValue()); + if (!matched) { + logMatchFailure(message, trigger, "事件数据条件不匹配"); + return false; + } + } + + logMatchSuccess(message, trigger); + return true; + } + + @Override + public int getPriority() { + return 30; // 中等优先级 + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DevicePropertyPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DevicePropertyPostTriggerMatcher.java new file mode 100644 index 0000000000..b508612b6f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DevicePropertyPostTriggerMatcher.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.springframework.stereotype.Component; + +/** + * 设备属性上报触发器匹配器 + *

+ * 处理设备属性数据上报的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DevicePropertyPostTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher { + + /** + * 设备属性上报消息方法 + */ + private static final String DEVICE_PROPERTY_POST_METHOD = "thing.property.post"; + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1. 基础参数校验 + if (!isBasicTriggerValid(trigger)) { + logMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 2. 检查消息方法是否匹配 + if (!DEVICE_PROPERTY_POST_METHOD.equals(message.getMethod())) { + logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_PROPERTY_POST_METHOD + ", 实际: " + message.getMethod()); + return false; + } + + // 3. 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { + logMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 4. 检查操作符和值是否有效 + if (!isOperatorAndValueValid(trigger)) { + logMatchFailure(message, trigger, "操作符或值无效"); + return false; + } + + // 5. 获取属性值 + Object propertyValue = message.getData(); + if (propertyValue == null) { + logMatchFailure(message, trigger, "消息中属性值为空"); + return false; + } + + // 6. 使用条件评估器进行匹配 + boolean matched = evaluateCondition(propertyValue, trigger.getOperator(), trigger.getValue()); + + if (matched) { + logMatchSuccess(message, trigger); + } else { + logMatchFailure(message, trigger, "属性值条件不匹配"); + } + + return matched; + } + + @Override + public int getPriority() { + return 20; // 中等优先级 + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceServiceInvokeTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceServiceInvokeTriggerMatcher.java new file mode 100644 index 0000000000..0f77e0b4e6 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceServiceInvokeTriggerMatcher.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.springframework.stereotype.Component; + +/** + * 设备服务调用触发器匹配器 + *

+ * 处理设备服务调用的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceServiceInvokeTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher { + + /** + * 设备服务调用消息方法 + */ + private static final String DEVICE_SERVICE_INVOKE_METHOD = "thing.service.invoke"; + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1. 基础参数校验 + if (!isBasicTriggerValid(trigger)) { + logMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 2. 检查消息方法是否匹配 + if (!DEVICE_SERVICE_INVOKE_METHOD.equals(message.getMethod())) { + logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_SERVICE_INVOKE_METHOD + ", 实际: " + message.getMethod()); + return false; + } + + // 3. 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { + logMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 4. 对于服务调用触发器,通常只需要匹配服务标识符即可 + // 不需要检查操作符和值,因为服务调用本身就是触发条件 + + logMatchSuccess(message, trigger); + return true; + } + + @Override + public int getPriority() { + return 40; // 较低优先级 + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceStateUpdateTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceStateUpdateTriggerMatcher.java new file mode 100644 index 0000000000..2e73081242 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceStateUpdateTriggerMatcher.java @@ -0,0 +1,71 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.springframework.stereotype.Component; + +/** + * 设备状态更新触发器匹配器 + *

+ * 处理设备上下线状态变更的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceStateUpdateTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher { + + /** + * 设备状态更新消息方法 + */ + private static final String DEVICE_STATE_UPDATE_METHOD = "thing.state.update"; + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1. 基础参数校验 + if (!isBasicTriggerValid(trigger)) { + logMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 2. 检查消息方法是否匹配 + if (!DEVICE_STATE_UPDATE_METHOD.equals(message.getMethod())) { + logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_STATE_UPDATE_METHOD + ", 实际: " + message.getMethod()); + return false; + } + + // 3. 检查操作符和值是否有效 + if (!isOperatorAndValueValid(trigger)) { + logMatchFailure(message, trigger, "操作符或值无效"); + return false; + } + + // 4. 获取设备状态值 + Object stateValue = message.getData(); + if (stateValue == null) { + logMatchFailure(message, trigger, "消息中设备状态值为空"); + return false; + } + + // 5. 使用条件评估器进行匹配 + boolean matched = evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue()); + + if (matched) { + logMatchSuccess(message, trigger); + } else { + logMatchFailure(message, trigger, "状态值条件不匹配"); + } + + return matched; + } + + @Override + public int getPriority() { + return 10; // 高优先级 + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcher.java new file mode 100644 index 0000000000..56f7772817 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcher.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; + +/** + * IoT 场景规则触发器匹配策略接口 + *

+ * 用于实现不同类型触发器的匹配逻辑,遵循策略模式设计 + * + * @author HUIHUI + */ +public interface IotSceneRuleTriggerMatcher { + + /** + * 获取支持的触发器类型 + * + * @return 触发器类型枚举 + */ + IotSceneRuleTriggerTypeEnum getSupportedTriggerType(); + + /** + * 检查触发器是否匹配消息 + * + * @param message 设备消息 + * @param trigger 触发器配置 + * @return 是否匹配 + */ + boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger); + + /** + * 获取匹配优先级(数值越小优先级越高) + *

+ * 用于在多个匹配器支持同一触发器类型时确定优先级 + * + * @return 优先级数值 + */ + default int getPriority() { + return 100; + } + + /** + * 获取匹配器名称,用于日志和调试 + * + * @return 匹配器名称 + */ + default String getMatcherName() { + return this.getClass().getSimpleName(); + } + + /** + * 是否启用该匹配器 + *

+ * 可用于动态开关某些匹配器 + * + * @return 是否启用 + */ + default boolean isEnabled() { + return true; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherManager.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherManager.java new file mode 100644 index 0000000000..2c300b1871 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherManager.java @@ -0,0 +1,150 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.hutool.core.collection.CollUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum.findTriggerTypeEnum; + +/** + * IoT 场景规则触发器匹配管理器 + *

+ * 负责管理所有触发器匹配器,并提供统一的匹配入口 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class IotSceneRuleTriggerMatcherManager { + + /** + * 触发器匹配器映射表 + * Key: 触发器类型枚举 + * Value: 对应的匹配器实例 + */ + private final Map matcherMap; + + /** + * 所有匹配器列表(按优先级排序) + */ + private final List allMatchers; + + public IotSceneRuleTriggerMatcherManager(List matchers) { + if (CollUtil.isEmpty(matchers)) { + log.warn("[IotSceneRuleTriggerMatcherManager][没有找到任何触发器匹配器]"); + this.matcherMap = new HashMap<>(); + this.allMatchers = new ArrayList<>(); + return; + } + + // 按优先级排序并过滤启用的匹配器 + this.allMatchers = matchers.stream() + .filter(IotSceneRuleTriggerMatcher::isEnabled) + .sorted(Comparator.comparing(IotSceneRuleTriggerMatcher::getPriority)) + .collect(Collectors.toList()); + + // 构建匹配器映射表 + this.matcherMap = this.allMatchers.stream() + .collect(Collectors.toMap( + IotSceneRuleTriggerMatcher::getSupportedTriggerType, + Function.identity(), + (existing, replacement) -> { + log.warn("[IotSceneRuleTriggerMatcherManager][触发器类型({})存在多个匹配器,使用优先级更高的: {}]", + existing.getSupportedTriggerType(), + existing.getPriority() <= replacement.getPriority() ? existing.getMatcherName() : replacement.getMatcherName()); + return existing.getPriority() <= replacement.getPriority() ? existing : replacement; + }, + LinkedHashMap::new + )); + + log.info("[IotSceneRuleTriggerMatcherManager][初始化完成,共加载 {} 个触发器匹配器]", this.matcherMap.size()); + this.matcherMap.forEach((type, matcher) -> + log.info("[IotSceneRuleTriggerMatcherManager][触发器类型: {}, 匹配器: {}, 优先级: {}]", + type, matcher.getMatcherName(), matcher.getPriority())); + } + + /** + * 检查触发器是否匹配消息 + * + * @param message 设备消息 + * @param trigger 触发器配置 + * @return 是否匹配 + */ + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + if (message == null || trigger == null || trigger.getType() == null) { + log.debug("[isMatched][参数无效] message: {}, trigger: {}", message, trigger); + return false; + } + + // 根据触发器类型查找对应的匹配器 + IotSceneRuleTriggerTypeEnum triggerType = findTriggerTypeEnum(trigger.getType()); + if (triggerType == null) { + log.warn("[isMatched][未知的触发器类型: {}]", trigger.getType()); + return false; + } + + IotSceneRuleTriggerMatcher matcher = matcherMap.get(triggerType); + if (matcher == null) { + log.warn("[isMatched][触发器类型({})没有对应的匹配器]", triggerType); + return false; + } + + try { + return matcher.isMatched(message, trigger); + } catch (Exception e) { + log.error("[isMatched][触发器匹配异常] message: {}, trigger: {}, matcher: {}", + message, trigger, matcher.getMatcherName(), e); + return false; + } + } + + /** + * 获取所有支持的触发器类型 + * + * @return 支持的触发器类型列表 + */ + public Set getSupportedTriggerTypes() { + return new HashSet<>(matcherMap.keySet()); + } + + /** + * 获取指定触发器类型的匹配器 + * + * @param triggerType 触发器类型 + * @return 匹配器实例,如果不存在则返回 null + */ + public IotSceneRuleTriggerMatcher getMatcher(IotSceneRuleTriggerTypeEnum triggerType) { + return matcherMap.get(triggerType); + } + + /** + * 获取所有匹配器的统计信息 + * + * @return 统计信息映射表 + */ + public Map getMatcherStatistics() { + Map statistics = new HashMap<>(); + statistics.put("totalMatchers", allMatchers.size()); + statistics.put("enabledMatchers", matcherMap.size()); + statistics.put("supportedTriggerTypes", getSupportedTriggerTypes()); + + Map matcherDetails = new HashMap<>(); + matcherMap.forEach((type, matcher) -> { + Map detail = new HashMap<>(); + detail.put("matcherName", matcher.getMatcherName()); + detail.put("priority", matcher.getPriority()); + detail.put("enabled", matcher.isEnabled()); + matcherDetails.put(type.name(), detail); + }); + statistics.put("matcherDetails", matcherDetails); + + return statistics; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/TimerTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/TimerTriggerMatcher.java new file mode 100644 index 0000000000..a0605ca65f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/TimerTriggerMatcher.java @@ -0,0 +1,79 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.springframework.stereotype.Component; + +/** + * 定时触发器匹配器 + *

+ * 处理定时触发的触发器匹配逻辑 + * 注意:定时触发器不依赖设备消息,主要用于定时任务场景 + * + * @author HUIHUI + */ +@Component +public class TimerTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.TIMER; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1. 基础参数校验 + if (!isBasicTriggerValid(trigger)) { + logMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 2. 检查 CRON 表达式是否存在 + if (StrUtil.isBlank(trigger.getCronExpression())) { + logMatchFailure(message, trigger, "定时触发器缺少 CRON 表达式"); + return false; + } + + // 3. 定时触发器通常不依赖具体的设备消息 + // 它是通过定时任务调度器触发的,这里主要是验证配置的有效性 + + // 4. 可以添加 CRON 表达式格式验证 + if (!isValidCronExpression(trigger.getCronExpression())) { + logMatchFailure(message, trigger, "CRON 表达式格式无效: " + trigger.getCronExpression()); + return false; + } + + logMatchSuccess(message, trigger); + return true; + } + + /** + * 验证 CRON 表达式格式是否有效 + * + * @param cronExpression CRON 表达式 + * @return 是否有效 + */ + private boolean isValidCronExpression(String cronExpression) { + try { + // 简单的 CRON 表达式格式验证 + // 标准 CRON 表达式应该有 6 或 7 个字段(秒 分 时 日 月 周 [年]) + String[] fields = cronExpression.trim().split("\\s+"); + return fields.length >= 6 && fields.length <= 7; + } catch (Exception e) { + return false; + } + } + + @Override + public int getPriority() { + return 50; // 最低优先级,因为定时触发器不依赖消息 + } + + @Override + public boolean isEnabled() { + // 定时触发器可以根据配置动态启用/禁用 + return true; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherTest.java new file mode 100644 index 0000000000..9903e8cc2f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherTest.java @@ -0,0 +1,200 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * IoT 场景规则触发器匹配器测试类 + * + * @author HUIHUI + */ +public class IotSceneRuleTriggerMatcherTest extends BaseMockitoUnitTest { + + private IotSceneRuleTriggerMatcherManager matcherManager; + private List matchers; + + @BeforeEach + void setUp() { + // 创建所有匹配器实例 + matchers = Arrays.asList( + new DeviceStateUpdateTriggerMatcher(), + new DevicePropertyPostTriggerMatcher(), + new DeviceEventPostTriggerMatcher(), + new DeviceServiceInvokeTriggerMatcher(), + new TimerTriggerMatcher() + ); + + // 初始化匹配器管理器 + matcherManager = new IotSceneRuleTriggerMatcherManager(matchers); + } + + @Test + void testDeviceStateUpdateTriggerMatcher() { + // 1. 准备测试数据 + IotDeviceMessage message = IotDeviceMessage.builder() + .requestId("test-001") + .method("thing.state.update") + .data(1) // 在线状态 + .build(); + + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE.getType()); + trigger.setOperator("="); + trigger.setValue("1"); + + // 2. 执行测试 + boolean matched = matcherManager.isMatched(message, trigger); + + // 3. 验证结果 + assertTrue(matched, "设备状态更新触发器应该匹配"); + } + + @Test + void testDevicePropertyPostTriggerMatcher() { + // 1. 准备测试数据 + HashMap params = new HashMap<>(); + IotDeviceMessage message = IotDeviceMessage.builder() + .requestId("test-002") + .method("thing.property.post") + .data(25.5) // 温度值 + .params(params) + .build(); + // 模拟标识符 + params.put("identifier", "temperature"); + + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType()); + trigger.setIdentifier("temperature"); + trigger.setOperator(">"); + trigger.setValue("20"); + + // 2. 执行测试 + boolean matched = matcherManager.isMatched(message, trigger); + + // 3. 验证结果 + assertTrue(matched, "设备属性上报触发器应该匹配"); + } + + @Test + void testDeviceEventPostTriggerMatcher() { + // 1. 准备测试数据 + HashMap params = new HashMap<>(); + IotDeviceMessage message = IotDeviceMessage.builder() + .requestId("test-003") + .method("thing.event.post") + .data("alarm_data") + .params(params) + .build(); + // 模拟标识符 + params.put("identifier", "high_temperature_alarm"); + + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST.getType()); + trigger.setIdentifier("high_temperature_alarm"); + + // 2. 执行测试 + boolean matched = matcherManager.isMatched(message, trigger); + + // 3. 验证结果 + assertTrue(matched, "设备事件上报触发器应该匹配"); + } + + @Test + void testDeviceServiceInvokeTriggerMatcher() { + // 1. 准备测试数据 + HashMap params = new HashMap<>(); + IotDeviceMessage message = IotDeviceMessage.builder() + .requestId("test-004") + .method("thing.service.invoke") + .msg("alarm_data") + .params(params) + .build(); + // 模拟标识符 + params.put("identifier", "restart_device"); + + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType()); + trigger.setIdentifier("restart_device"); + + // 2. 执行测试 + boolean matched = matcherManager.isMatched(message, trigger); + + // 3. 验证结果 + assertTrue(matched, "设备服务调用触发器应该匹配"); + } + + @Test + void testTimerTriggerMatcher() { + // 1. 准备测试数据 + IotDeviceMessage message = IotDeviceMessage.builder() + .requestId("test-005") + .method("timer.trigger") // 定时触发器不依赖具体消息方法 + .build(); + + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); + trigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点 + + // 2. 执行测试 + boolean matched = matcherManager.isMatched(message, trigger); + + // 3. 验证结果 + assertTrue(matched, "定时触发器应该匹配"); + } + + @Test + void testInvalidTriggerType() { + // 1. 准备测试数据 + IotDeviceMessage message = IotDeviceMessage.builder() + .requestId("test-006") + .method("unknown.method") + .build(); + + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(999); // 无效的触发器类型 + + // 2. 执行测试 + boolean matched = matcherManager.isMatched(message, trigger); + + // 3. 验证结果 + assertFalse(matched, "无效的触发器类型应该不匹配"); + } + + @Test + void testMatcherManagerStatistics() { + // 1. 执行测试 + var statistics = matcherManager.getMatcherStatistics(); + + // 2. 验证结果 + assertNotNull(statistics); + assertEquals(5, statistics.get("totalMatchers")); + assertEquals(5, statistics.get("enabledMatchers")); + assertNotNull(statistics.get("supportedTriggerTypes")); + assertNotNull(statistics.get("matcherDetails")); + } + + @Test + void testGetSupportedTriggerTypes() { + // 1. 执行测试 + var supportedTypes = matcherManager.getSupportedTriggerTypes(); + + // 2. 验证结果 + assertNotNull(supportedTypes); + assertEquals(5, supportedTypes.size()); + assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE)); + assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST)); + assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST)); + assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE)); + assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.TIMER)); + } +}