Commit 49bf744b740150c722297ed161702c44ccdf4ac3

Authored by puhui999
1 parent 0d288077

feat:【IoT 物联网】新增场景规则触发器匹配策略接口和实现

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<Integer> { @@ -63,4 +63,18 @@ public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable<Integer> {
63 return ARRAYS; 63 return ARRAYS;
64 } 64 }
65 65
  66 +
  67 + /**
  68 + * 根据类型值查找触发器类型枚举
  69 + *
  70 + * @param typeValue 类型值
  71 + * @return 触发器类型枚举
  72 + */
  73 + public static IotSceneRuleTriggerTypeEnum findTriggerTypeEnum(Integer typeValue) {
  74 + return Arrays.stream(IotSceneRuleTriggerTypeEnum.values())
  75 + .filter(type -> type.getType().equals(typeValue))
  76 + .findFirst()
  77 + .orElse(null);
  78 + }
  79 +
66 } 80 }
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/AbstractIotSceneRuleTriggerMatcher.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.hutool.core.util.NumberUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
  6 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  8 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +
  11 +import java.util.HashMap;
  12 +import java.util.List;
  13 +import java.util.Map;
  14 +
  15 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  16 +
  17 +/**
  18 + * IoT 场景规则触发器匹配器抽象基类
  19 + * <p>
  20 + * 提供通用的条件评估逻辑和工具方法
  21 + *
  22 + * @author HUIHUI
  23 + */
  24 +@Slf4j
  25 +public abstract class AbstractIotSceneRuleTriggerMatcher implements IotSceneRuleTriggerMatcher {
  26 +
  27 + /**
  28 + * 评估条件是否匹配
  29 + *
  30 + * @param sourceValue 源值(来自消息)
  31 + * @param operator 操作符
  32 + * @param paramValue 参数值(来自条件配置)
  33 + * @return 是否匹配
  34 + */
  35 + protected boolean evaluateCondition(Object sourceValue, String operator, String paramValue) {
  36 + try {
  37 + // 1. 校验操作符是否合法
  38 + IotSceneRuleConditionOperatorEnum operatorEnum = IotSceneRuleConditionOperatorEnum.operatorOf(operator);
  39 + if (operatorEnum == null) {
  40 + log.warn("[evaluateCondition][存在错误的操作符({})]", operator);
  41 + return false;
  42 + }
  43 +
  44 + // 2. 构建 Spring 表达式变量
  45 + Map<String, Object> springExpressionVariables = new HashMap<>();
  46 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue);
  47 +
  48 + // 处理参数值
  49 + if (StrUtil.isNotBlank(paramValue)) {
  50 + // 处理多值情况(如 IN、BETWEEN 操作符)
  51 + if (paramValue.contains(",")) {
  52 + List<String> paramValues = StrUtil.split(paramValue, ',');
  53 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE_LIST,
  54 + convertList(paramValues, NumberUtil::parseDouble));
  55 + } else {
  56 + // 处理单值情况
  57 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE,
  58 + NumberUtil.parseDouble(paramValue));
  59 + }
  60 + }
  61 +
  62 + // 3. 计算 Spring 表达式
  63 + return (Boolean) SpringExpressionUtils.parseExpression(operatorEnum.getSpringExpression(), springExpressionVariables);
  64 + } catch (Exception e) {
  65 + log.error("[evaluateCondition][条件评估异常] sourceValue: {}, operator: {}, paramValue: {}",
  66 + sourceValue, operator, paramValue, e);
  67 + return false;
  68 + }
  69 + }
  70 +
  71 + /**
  72 + * 检查基础触发器参数是否有效
  73 + *
  74 + * @param trigger 触发器配置
  75 + * @return 是否有效
  76 + */
  77 + protected boolean isBasicTriggerValid(IotSceneRuleDO.Trigger trigger) {
  78 + return trigger != null && trigger.getType() != null;
  79 + }
  80 +
  81 + /**
  82 + * 检查操作符和值是否有效
  83 + *
  84 + * @param trigger 触发器配置
  85 + * @return 是否有效
  86 + */
  87 + protected boolean isOperatorAndValueValid(IotSceneRuleDO.Trigger trigger) {
  88 + return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue());
  89 + }
  90 +
  91 + /**
  92 + * 检查标识符是否匹配
  93 + *
  94 + * @param expectedIdentifier 期望的标识符
  95 + * @param actualIdentifier 实际的标识符
  96 + * @return 是否匹配
  97 + */
  98 + protected boolean isIdentifierMatched(String expectedIdentifier, String actualIdentifier) {
  99 + return StrUtil.isNotBlank(expectedIdentifier) && expectedIdentifier.equals(actualIdentifier);
  100 + }
  101 +
  102 + /**
  103 + * 记录匹配成功日志
  104 + *
  105 + * @param message 设备消息
  106 + * @param trigger 触发器配置
  107 + */
  108 + protected void logMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
  109 + log.debug("[{}][消息({}) 匹配触发器({}) 成功]", getMatcherName(), message.getRequestId(), trigger.getType());
  110 + }
  111 +
  112 + /**
  113 + * 记录匹配失败日志
  114 + *
  115 + * @param message 设备消息
  116 + * @param trigger 触发器配置
  117 + * @param reason 失败原因
  118 + */
  119 + protected void logMatchFailure(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, String reason) {
  120 + log.debug("[{}][消息({}) 匹配触发器({}) 失败: {}]", getMatcherName(), message.getRequestId(), trigger.getType(), reason);
  121 + }
  122 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceEventPostTriggerMatcher.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  7 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +/**
  11 + * 设备事件上报触发器匹配器
  12 + * <p>
  13 + * 处理设备事件上报的触发器匹配逻辑
  14 + *
  15 + * @author HUIHUI
  16 + */
  17 +@Component
  18 +public class DeviceEventPostTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher {
  19 +
  20 + /**
  21 + * 设备事件上报消息方法
  22 + */
  23 + private static final String DEVICE_EVENT_POST_METHOD = "thing.event.post";
  24 +
  25 + @Override
  26 + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
  27 + return IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST;
  28 + }
  29 +
  30 + @Override
  31 + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
  32 + // 1. 基础参数校验
  33 + if (!isBasicTriggerValid(trigger)) {
  34 + logMatchFailure(message, trigger, "触发器基础参数无效");
  35 + return false;
  36 + }
  37 +
  38 + // 2. 检查消息方法是否匹配
  39 + if (!DEVICE_EVENT_POST_METHOD.equals(message.getMethod())) {
  40 + logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_EVENT_POST_METHOD + ", 实际: " + message.getMethod());
  41 + return false;
  42 + }
  43 +
  44 + // 3. 检查标识符是否匹配
  45 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message);
  46 + if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) {
  47 + logMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier);
  48 + return false;
  49 + }
  50 +
  51 + // 4. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配
  52 + // 但如果配置了操作符和值,则需要进行条件匹配
  53 + if (StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue())) {
  54 + Object eventData = message.getData();
  55 + if (eventData == null) {
  56 + logMatchFailure(message, trigger, "消息中事件数据为空");
  57 + return false;
  58 + }
  59 +
  60 + boolean matched = evaluateCondition(eventData, trigger.getOperator(), trigger.getValue());
  61 + if (!matched) {
  62 + logMatchFailure(message, trigger, "事件数据条件不匹配");
  63 + return false;
  64 + }
  65 + }
  66 +
  67 + logMatchSuccess(message, trigger);
  68 + return true;
  69 + }
  70 +
  71 + @Override
  72 + public int getPriority() {
  73 + return 30; // 中等优先级
  74 + }
  75 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DevicePropertyPostTriggerMatcher.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  6 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +/**
  10 + * 设备属性上报触发器匹配器
  11 + * <p>
  12 + * 处理设备属性数据上报的触发器匹配逻辑
  13 + *
  14 + * @author HUIHUI
  15 + */
  16 +@Component
  17 +public class DevicePropertyPostTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher {
  18 +
  19 + /**
  20 + * 设备属性上报消息方法
  21 + */
  22 + private static final String DEVICE_PROPERTY_POST_METHOD = "thing.property.post";
  23 +
  24 + @Override
  25 + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
  26 + return IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST;
  27 + }
  28 +
  29 + @Override
  30 + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
  31 + // 1. 基础参数校验
  32 + if (!isBasicTriggerValid(trigger)) {
  33 + logMatchFailure(message, trigger, "触发器基础参数无效");
  34 + return false;
  35 + }
  36 +
  37 + // 2. 检查消息方法是否匹配
  38 + if (!DEVICE_PROPERTY_POST_METHOD.equals(message.getMethod())) {
  39 + logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_PROPERTY_POST_METHOD + ", 实际: " + message.getMethod());
  40 + return false;
  41 + }
  42 +
  43 + // 3. 检查标识符是否匹配
  44 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message);
  45 + if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) {
  46 + logMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier);
  47 + return false;
  48 + }
  49 +
  50 + // 4. 检查操作符和值是否有效
  51 + if (!isOperatorAndValueValid(trigger)) {
  52 + logMatchFailure(message, trigger, "操作符或值无效");
  53 + return false;
  54 + }
  55 +
  56 + // 5. 获取属性值
  57 + Object propertyValue = message.getData();
  58 + if (propertyValue == null) {
  59 + logMatchFailure(message, trigger, "消息中属性值为空");
  60 + return false;
  61 + }
  62 +
  63 + // 6. 使用条件评估器进行匹配
  64 + boolean matched = evaluateCondition(propertyValue, trigger.getOperator(), trigger.getValue());
  65 +
  66 + if (matched) {
  67 + logMatchSuccess(message, trigger);
  68 + } else {
  69 + logMatchFailure(message, trigger, "属性值条件不匹配");
  70 + }
  71 +
  72 + return matched;
  73 + }
  74 +
  75 + @Override
  76 + public int getPriority() {
  77 + return 20; // 中等优先级
  78 + }
  79 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceServiceInvokeTriggerMatcher.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  6 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +/**
  10 + * 设备服务调用触发器匹配器
  11 + * <p>
  12 + * 处理设备服务调用的触发器匹配逻辑
  13 + *
  14 + * @author HUIHUI
  15 + */
  16 +@Component
  17 +public class DeviceServiceInvokeTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher {
  18 +
  19 + /**
  20 + * 设备服务调用消息方法
  21 + */
  22 + private static final String DEVICE_SERVICE_INVOKE_METHOD = "thing.service.invoke";
  23 +
  24 + @Override
  25 + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
  26 + return IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE;
  27 + }
  28 +
  29 + @Override
  30 + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
  31 + // 1. 基础参数校验
  32 + if (!isBasicTriggerValid(trigger)) {
  33 + logMatchFailure(message, trigger, "触发器基础参数无效");
  34 + return false;
  35 + }
  36 +
  37 + // 2. 检查消息方法是否匹配
  38 + if (!DEVICE_SERVICE_INVOKE_METHOD.equals(message.getMethod())) {
  39 + logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_SERVICE_INVOKE_METHOD + ", 实际: " + message.getMethod());
  40 + return false;
  41 + }
  42 +
  43 + // 3. 检查标识符是否匹配
  44 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message);
  45 + if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) {
  46 + logMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier);
  47 + return false;
  48 + }
  49 +
  50 + // 4. 对于服务调用触发器,通常只需要匹配服务标识符即可
  51 + // 不需要检查操作符和值,因为服务调用本身就是触发条件
  52 +
  53 + logMatchSuccess(message, trigger);
  54 + return true;
  55 + }
  56 +
  57 + @Override
  58 + public int getPriority() {
  59 + return 40; // 较低优先级
  60 + }
  61 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceStateUpdateTriggerMatcher.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  5 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  6 +import org.springframework.stereotype.Component;
  7 +
  8 +/**
  9 + * 设备状态更新触发器匹配器
  10 + * <p>
  11 + * 处理设备上下线状态变更的触发器匹配逻辑
  12 + *
  13 + * @author HUIHUI
  14 + */
  15 +@Component
  16 +public class DeviceStateUpdateTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher {
  17 +
  18 + /**
  19 + * 设备状态更新消息方法
  20 + */
  21 + private static final String DEVICE_STATE_UPDATE_METHOD = "thing.state.update";
  22 +
  23 + @Override
  24 + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
  25 + return IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE;
  26 + }
  27 +
  28 + @Override
  29 + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
  30 + // 1. 基础参数校验
  31 + if (!isBasicTriggerValid(trigger)) {
  32 + logMatchFailure(message, trigger, "触发器基础参数无效");
  33 + return false;
  34 + }
  35 +
  36 + // 2. 检查消息方法是否匹配
  37 + if (!DEVICE_STATE_UPDATE_METHOD.equals(message.getMethod())) {
  38 + logMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_STATE_UPDATE_METHOD + ", 实际: " + message.getMethod());
  39 + return false;
  40 + }
  41 +
  42 + // 3. 检查操作符和值是否有效
  43 + if (!isOperatorAndValueValid(trigger)) {
  44 + logMatchFailure(message, trigger, "操作符或值无效");
  45 + return false;
  46 + }
  47 +
  48 + // 4. 获取设备状态值
  49 + Object stateValue = message.getData();
  50 + if (stateValue == null) {
  51 + logMatchFailure(message, trigger, "消息中设备状态值为空");
  52 + return false;
  53 + }
  54 +
  55 + // 5. 使用条件评估器进行匹配
  56 + boolean matched = evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue());
  57 +
  58 + if (matched) {
  59 + logMatchSuccess(message, trigger);
  60 + } else {
  61 + logMatchFailure(message, trigger, "状态值条件不匹配");
  62 + }
  63 +
  64 + return matched;
  65 + }
  66 +
  67 + @Override
  68 + public int getPriority() {
  69 + return 10; // 高优先级
  70 + }
  71 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcher.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  5 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  6 +
  7 +/**
  8 + * IoT 场景规则触发器匹配策略接口
  9 + * <p>
  10 + * 用于实现不同类型触发器的匹配逻辑,遵循策略模式设计
  11 + *
  12 + * @author HUIHUI
  13 + */
  14 +public interface IotSceneRuleTriggerMatcher {
  15 +
  16 + /**
  17 + * 获取支持的触发器类型
  18 + *
  19 + * @return 触发器类型枚举
  20 + */
  21 + IotSceneRuleTriggerTypeEnum getSupportedTriggerType();
  22 +
  23 + /**
  24 + * 检查触发器是否匹配消息
  25 + *
  26 + * @param message 设备消息
  27 + * @param trigger 触发器配置
  28 + * @return 是否匹配
  29 + */
  30 + boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger);
  31 +
  32 + /**
  33 + * 获取匹配优先级(数值越小优先级越高)
  34 + * <p>
  35 + * 用于在多个匹配器支持同一触发器类型时确定优先级
  36 + *
  37 + * @return 优先级数值
  38 + */
  39 + default int getPriority() {
  40 + return 100;
  41 + }
  42 +
  43 + /**
  44 + * 获取匹配器名称,用于日志和调试
  45 + *
  46 + * @return 匹配器名称
  47 + */
  48 + default String getMatcherName() {
  49 + return this.getClass().getSimpleName();
  50 + }
  51 +
  52 + /**
  53 + * 是否启用该匹配器
  54 + * <p>
  55 + * 可用于动态开关某些匹配器
  56 + *
  57 + * @return 是否启用
  58 + */
  59 + default boolean isEnabled() {
  60 + return true;
  61 + }
  62 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherManager.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  6 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +import org.springframework.stereotype.Component;
  9 +
  10 +import java.util.*;
  11 +import java.util.function.Function;
  12 +import java.util.stream.Collectors;
  13 +
  14 +import static cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum.findTriggerTypeEnum;
  15 +
  16 +/**
  17 + * IoT 场景规则触发器匹配管理器
  18 + * <p>
  19 + * 负责管理所有触发器匹配器,并提供统一的匹配入口
  20 + *
  21 + * @author HUIHUI
  22 + */
  23 +@Component
  24 +@Slf4j
  25 +public class IotSceneRuleTriggerMatcherManager {
  26 +
  27 + /**
  28 + * 触发器匹配器映射表
  29 + * Key: 触发器类型枚举
  30 + * Value: 对应的匹配器实例
  31 + */
  32 + private final Map<IotSceneRuleTriggerTypeEnum, IotSceneRuleTriggerMatcher> matcherMap;
  33 +
  34 + /**
  35 + * 所有匹配器列表(按优先级排序)
  36 + */
  37 + private final List<IotSceneRuleTriggerMatcher> allMatchers;
  38 +
  39 + public IotSceneRuleTriggerMatcherManager(List<IotSceneRuleTriggerMatcher> matchers) {
  40 + if (CollUtil.isEmpty(matchers)) {
  41 + log.warn("[IotSceneRuleTriggerMatcherManager][没有找到任何触发器匹配器]");
  42 + this.matcherMap = new HashMap<>();
  43 + this.allMatchers = new ArrayList<>();
  44 + return;
  45 + }
  46 +
  47 + // 按优先级排序并过滤启用的匹配器
  48 + this.allMatchers = matchers.stream()
  49 + .filter(IotSceneRuleTriggerMatcher::isEnabled)
  50 + .sorted(Comparator.comparing(IotSceneRuleTriggerMatcher::getPriority))
  51 + .collect(Collectors.toList());
  52 +
  53 + // 构建匹配器映射表
  54 + this.matcherMap = this.allMatchers.stream()
  55 + .collect(Collectors.toMap(
  56 + IotSceneRuleTriggerMatcher::getSupportedTriggerType,
  57 + Function.identity(),
  58 + (existing, replacement) -> {
  59 + log.warn("[IotSceneRuleTriggerMatcherManager][触发器类型({})存在多个匹配器,使用优先级更高的: {}]",
  60 + existing.getSupportedTriggerType(),
  61 + existing.getPriority() <= replacement.getPriority() ? existing.getMatcherName() : replacement.getMatcherName());
  62 + return existing.getPriority() <= replacement.getPriority() ? existing : replacement;
  63 + },
  64 + LinkedHashMap::new
  65 + ));
  66 +
  67 + log.info("[IotSceneRuleTriggerMatcherManager][初始化完成,共加载 {} 个触发器匹配器]", this.matcherMap.size());
  68 + this.matcherMap.forEach((type, matcher) ->
  69 + log.info("[IotSceneRuleTriggerMatcherManager][触发器类型: {}, 匹配器: {}, 优先级: {}]",
  70 + type, matcher.getMatcherName(), matcher.getPriority()));
  71 + }
  72 +
  73 + /**
  74 + * 检查触发器是否匹配消息
  75 + *
  76 + * @param message 设备消息
  77 + * @param trigger 触发器配置
  78 + * @return 是否匹配
  79 + */
  80 + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
  81 + if (message == null || trigger == null || trigger.getType() == null) {
  82 + log.debug("[isMatched][参数无效] message: {}, trigger: {}", message, trigger);
  83 + return false;
  84 + }
  85 +
  86 + // 根据触发器类型查找对应的匹配器
  87 + IotSceneRuleTriggerTypeEnum triggerType = findTriggerTypeEnum(trigger.getType());
  88 + if (triggerType == null) {
  89 + log.warn("[isMatched][未知的触发器类型: {}]", trigger.getType());
  90 + return false;
  91 + }
  92 +
  93 + IotSceneRuleTriggerMatcher matcher = matcherMap.get(triggerType);
  94 + if (matcher == null) {
  95 + log.warn("[isMatched][触发器类型({})没有对应的匹配器]", triggerType);
  96 + return false;
  97 + }
  98 +
  99 + try {
  100 + return matcher.isMatched(message, trigger);
  101 + } catch (Exception e) {
  102 + log.error("[isMatched][触发器匹配异常] message: {}, trigger: {}, matcher: {}",
  103 + message, trigger, matcher.getMatcherName(), e);
  104 + return false;
  105 + }
  106 + }
  107 +
  108 + /**
  109 + * 获取所有支持的触发器类型
  110 + *
  111 + * @return 支持的触发器类型列表
  112 + */
  113 + public Set<IotSceneRuleTriggerTypeEnum> getSupportedTriggerTypes() {
  114 + return new HashSet<>(matcherMap.keySet());
  115 + }
  116 +
  117 + /**
  118 + * 获取指定触发器类型的匹配器
  119 + *
  120 + * @param triggerType 触发器类型
  121 + * @return 匹配器实例,如果不存在则返回 null
  122 + */
  123 + public IotSceneRuleTriggerMatcher getMatcher(IotSceneRuleTriggerTypeEnum triggerType) {
  124 + return matcherMap.get(triggerType);
  125 + }
  126 +
  127 + /**
  128 + * 获取所有匹配器的统计信息
  129 + *
  130 + * @return 统计信息映射表
  131 + */
  132 + public Map<String, Object> getMatcherStatistics() {
  133 + Map<String, Object> statistics = new HashMap<>();
  134 + statistics.put("totalMatchers", allMatchers.size());
  135 + statistics.put("enabledMatchers", matcherMap.size());
  136 + statistics.put("supportedTriggerTypes", getSupportedTriggerTypes());
  137 +
  138 + Map<String, Object> matcherDetails = new HashMap<>();
  139 + matcherMap.forEach((type, matcher) -> {
  140 + Map<String, Object> detail = new HashMap<>();
  141 + detail.put("matcherName", matcher.getMatcherName());
  142 + detail.put("priority", matcher.getPriority());
  143 + detail.put("enabled", matcher.isEnabled());
  144 + matcherDetails.put(type.name(), detail);
  145 + });
  146 + statistics.put("matcherDetails", matcherDetails);
  147 +
  148 + return statistics;
  149 + }
  150 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/TimerTriggerMatcher.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  6 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  7 +import org.springframework.stereotype.Component;
  8 +
  9 +/**
  10 + * 定时触发器匹配器
  11 + * <p>
  12 + * 处理定时触发的触发器匹配逻辑
  13 + * 注意:定时触发器不依赖设备消息,主要用于定时任务场景
  14 + *
  15 + * @author HUIHUI
  16 + */
  17 +@Component
  18 +public class TimerTriggerMatcher extends AbstractIotSceneRuleTriggerMatcher {
  19 +
  20 + @Override
  21 + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() {
  22 + return IotSceneRuleTriggerTypeEnum.TIMER;
  23 + }
  24 +
  25 + @Override
  26 + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) {
  27 + // 1. 基础参数校验
  28 + if (!isBasicTriggerValid(trigger)) {
  29 + logMatchFailure(message, trigger, "触发器基础参数无效");
  30 + return false;
  31 + }
  32 +
  33 + // 2. 检查 CRON 表达式是否存在
  34 + if (StrUtil.isBlank(trigger.getCronExpression())) {
  35 + logMatchFailure(message, trigger, "定时触发器缺少 CRON 表达式");
  36 + return false;
  37 + }
  38 +
  39 + // 3. 定时触发器通常不依赖具体的设备消息
  40 + // 它是通过定时任务调度器触发的,这里主要是验证配置的有效性
  41 +
  42 + // 4. 可以添加 CRON 表达式格式验证
  43 + if (!isValidCronExpression(trigger.getCronExpression())) {
  44 + logMatchFailure(message, trigger, "CRON 表达式格式无效: " + trigger.getCronExpression());
  45 + return false;
  46 + }
  47 +
  48 + logMatchSuccess(message, trigger);
  49 + return true;
  50 + }
  51 +
  52 + /**
  53 + * 验证 CRON 表达式格式是否有效
  54 + *
  55 + * @param cronExpression CRON 表达式
  56 + * @return 是否有效
  57 + */
  58 + private boolean isValidCronExpression(String cronExpression) {
  59 + try {
  60 + // 简单的 CRON 表达式格式验证
  61 + // 标准 CRON 表达式应该有 6 或 7 个字段(秒 分 时 日 月 周 [年])
  62 + String[] fields = cronExpression.trim().split("\\s+");
  63 + return fields.length >= 6 && fields.length <= 7;
  64 + } catch (Exception e) {
  65 + return false;
  66 + }
  67 + }
  68 +
  69 + @Override
  70 + public int getPriority() {
  71 + return 50; // 最低优先级,因为定时触发器不依赖消息
  72 + }
  73 +
  74 + @Override
  75 + public boolean isEnabled() {
  76 + // 定时触发器可以根据配置动态启用/禁用
  77 + return true;
  78 + }
  79 +}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  6 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  7 +import org.junit.jupiter.api.BeforeEach;
  8 +import org.junit.jupiter.api.Test;
  9 +
  10 +import java.util.Arrays;
  11 +import java.util.HashMap;
  12 +import java.util.List;
  13 +
  14 +import static org.junit.jupiter.api.Assertions.*;
  15 +
  16 +/**
  17 + * IoT 场景规则触发器匹配器测试类
  18 + *
  19 + * @author HUIHUI
  20 + */
  21 +public class IotSceneRuleTriggerMatcherTest extends BaseMockitoUnitTest {
  22 +
  23 + private IotSceneRuleTriggerMatcherManager matcherManager;
  24 + private List<IotSceneRuleTriggerMatcher> matchers;
  25 +
  26 + @BeforeEach
  27 + void setUp() {
  28 + // 创建所有匹配器实例
  29 + matchers = Arrays.asList(
  30 + new DeviceStateUpdateTriggerMatcher(),
  31 + new DevicePropertyPostTriggerMatcher(),
  32 + new DeviceEventPostTriggerMatcher(),
  33 + new DeviceServiceInvokeTriggerMatcher(),
  34 + new TimerTriggerMatcher()
  35 + );
  36 +
  37 + // 初始化匹配器管理器
  38 + matcherManager = new IotSceneRuleTriggerMatcherManager(matchers);
  39 + }
  40 +
  41 + @Test
  42 + void testDeviceStateUpdateTriggerMatcher() {
  43 + // 1. 准备测试数据
  44 + IotDeviceMessage message = IotDeviceMessage.builder()
  45 + .requestId("test-001")
  46 + .method("thing.state.update")
  47 + .data(1) // 在线状态
  48 + .build();
  49 +
  50 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  51 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE.getType());
  52 + trigger.setOperator("=");
  53 + trigger.setValue("1");
  54 +
  55 + // 2. 执行测试
  56 + boolean matched = matcherManager.isMatched(message, trigger);
  57 +
  58 + // 3. 验证结果
  59 + assertTrue(matched, "设备状态更新触发器应该匹配");
  60 + }
  61 +
  62 + @Test
  63 + void testDevicePropertyPostTriggerMatcher() {
  64 + // 1. 准备测试数据
  65 + HashMap<String, Object> params = new HashMap<>();
  66 + IotDeviceMessage message = IotDeviceMessage.builder()
  67 + .requestId("test-002")
  68 + .method("thing.property.post")
  69 + .data(25.5) // 温度值
  70 + .params(params)
  71 + .build();
  72 + // 模拟标识符
  73 + params.put("identifier", "temperature");
  74 +
  75 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  76 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType());
  77 + trigger.setIdentifier("temperature");
  78 + trigger.setOperator(">");
  79 + trigger.setValue("20");
  80 +
  81 + // 2. 执行测试
  82 + boolean matched = matcherManager.isMatched(message, trigger);
  83 +
  84 + // 3. 验证结果
  85 + assertTrue(matched, "设备属性上报触发器应该匹配");
  86 + }
  87 +
  88 + @Test
  89 + void testDeviceEventPostTriggerMatcher() {
  90 + // 1. 准备测试数据
  91 + HashMap<String, Object> params = new HashMap<>();
  92 + IotDeviceMessage message = IotDeviceMessage.builder()
  93 + .requestId("test-003")
  94 + .method("thing.event.post")
  95 + .data("alarm_data")
  96 + .params(params)
  97 + .build();
  98 + // 模拟标识符
  99 + params.put("identifier", "high_temperature_alarm");
  100 +
  101 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  102 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST.getType());
  103 + trigger.setIdentifier("high_temperature_alarm");
  104 +
  105 + // 2. 执行测试
  106 + boolean matched = matcherManager.isMatched(message, trigger);
  107 +
  108 + // 3. 验证结果
  109 + assertTrue(matched, "设备事件上报触发器应该匹配");
  110 + }
  111 +
  112 + @Test
  113 + void testDeviceServiceInvokeTriggerMatcher() {
  114 + // 1. 准备测试数据
  115 + HashMap<String, Object> params = new HashMap<>();
  116 + IotDeviceMessage message = IotDeviceMessage.builder()
  117 + .requestId("test-004")
  118 + .method("thing.service.invoke")
  119 + .msg("alarm_data")
  120 + .params(params)
  121 + .build();
  122 + // 模拟标识符
  123 + params.put("identifier", "restart_device");
  124 +
  125 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  126 + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType());
  127 + trigger.setIdentifier("restart_device");
  128 +
  129 + // 2. 执行测试
  130 + boolean matched = matcherManager.isMatched(message, trigger);
  131 +
  132 + // 3. 验证结果
  133 + assertTrue(matched, "设备服务调用触发器应该匹配");
  134 + }
  135 +
  136 + @Test
  137 + void testTimerTriggerMatcher() {
  138 + // 1. 准备测试数据
  139 + IotDeviceMessage message = IotDeviceMessage.builder()
  140 + .requestId("test-005")
  141 + .method("timer.trigger") // 定时触发器不依赖具体消息方法
  142 + .build();
  143 +
  144 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  145 + trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
  146 + trigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点
  147 +
  148 + // 2. 执行测试
  149 + boolean matched = matcherManager.isMatched(message, trigger);
  150 +
  151 + // 3. 验证结果
  152 + assertTrue(matched, "定时触发器应该匹配");
  153 + }
  154 +
  155 + @Test
  156 + void testInvalidTriggerType() {
  157 + // 1. 准备测试数据
  158 + IotDeviceMessage message = IotDeviceMessage.builder()
  159 + .requestId("test-006")
  160 + .method("unknown.method")
  161 + .build();
  162 +
  163 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  164 + trigger.setType(999); // 无效的触发器类型
  165 +
  166 + // 2. 执行测试
  167 + boolean matched = matcherManager.isMatched(message, trigger);
  168 +
  169 + // 3. 验证结果
  170 + assertFalse(matched, "无效的触发器类型应该不匹配");
  171 + }
  172 +
  173 + @Test
  174 + void testMatcherManagerStatistics() {
  175 + // 1. 执行测试
  176 + var statistics = matcherManager.getMatcherStatistics();
  177 +
  178 + // 2. 验证结果
  179 + assertNotNull(statistics);
  180 + assertEquals(5, statistics.get("totalMatchers"));
  181 + assertEquals(5, statistics.get("enabledMatchers"));
  182 + assertNotNull(statistics.get("supportedTriggerTypes"));
  183 + assertNotNull(statistics.get("matcherDetails"));
  184 + }
  185 +
  186 + @Test
  187 + void testGetSupportedTriggerTypes() {
  188 + // 1. 执行测试
  189 + var supportedTypes = matcherManager.getSupportedTriggerTypes();
  190 +
  191 + // 2. 验证结果
  192 + assertNotNull(supportedTypes);
  193 + assertEquals(5, supportedTypes.size());
  194 + assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE));
  195 + assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST));
  196 + assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST));
  197 + assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE));
  198 + assertTrue(supportedTypes.contains(IotSceneRuleTriggerTypeEnum.TIMER));
  199 + }
  200 +}