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 216584ec20..bfc84c9f60 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 @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.iot.enums.rule; +import cn.hutool.core.util.ArrayUtil; import cn.iocoder.yudao.framework.common.core.ArrayValuable; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -60,18 +61,8 @@ public enum IotSceneRuleTriggerTypeEnum implements ArrayValuable { return ARRAYS; } - // TODO @puhui999:可以参考下别的枚举哈,方法名,和实现都可以更简洁;of(String type) { firstMatch - /** - * 根据类型值查找触发器类型枚举 - * - * @param typeValue 类型值 - * @return 触发器类型枚举 - */ - public static IotSceneRuleTriggerTypeEnum findTriggerTypeEnum(Integer typeValue) { - return Arrays.stream(IotSceneRuleTriggerTypeEnum.values()) - .filter(type -> type.getType().equals(typeValue)) - .findFirst() - .orElse(null); + public static IotSceneRuleTriggerTypeEnum typeOf(Integer type) { + return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); } } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java index ee56310fba..ba48afc5c2 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleServiceImpl.java @@ -2,18 +2,11 @@ package cn.iocoder.yudao.module.iot.service.rule.scene; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.text.CharPool; -import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; -import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; -import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; -import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.iot.controller.admin.rule.vo.scene.IotSceneRulePageReqVO; @@ -23,7 +16,6 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO; import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; @@ -36,12 +28,9 @@ import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import java.util.Collection; -import java.util.HashMap; import java.util.List; -import java.util.Map; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.RULE_SCENE_NOT_EXISTS; @@ -353,65 +342,6 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { } } - // TODO @puhui999:下面还需要么? - /** - * 判断触发器的条件参数是否匹配 - * - * @param message 设备消息 - * @param condition 触发条件 - * @param sceneRule 规则场景(用于日志,无其它作用) - * @param trigger 触发器(用于日志,无其它作用) - * @return 是否匹配 - */ - @SuppressWarnings({"unchecked", "DataFlowIssue"}) - private boolean isTriggerConditionParameterMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition, - IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) { - // 1.1 校验操作符是否合法 - IotSceneRuleConditionOperatorEnum operator = - IotSceneRuleConditionOperatorEnum.operatorOf(condition.getOperator()); - if (operator == null) { - log.error("[isTriggerConditionParameterMatched][规则场景编号({}) 的触发器({}) 存在错误的操作符({})]", - sceneRule.getId(), trigger, condition.getOperator()); - return false; - } - // 1.2 校验消息是否包含对应的值 - String messageValue = MapUtil.getStr((Map) message.getData(), condition.getIdentifier()); - if (messageValue == null) { - return false; - } - - // 2.1 构建 Spring 表达式的变量 - Map springExpressionVariables = new HashMap<>(); - try { - springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, messageValue); - springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE, condition.getParam()); - List parameterValues = StrUtil.splitTrim(condition.getParam(), CharPool.COMMA); - springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE_LIST, parameterValues); - // 特殊:解决数字的比较。因为 Spring 是基于它的 compareTo 方法,对数字的比较存在问题! - if (ObjectUtils.equalsAny(operator, IotSceneRuleConditionOperatorEnum.BETWEEN, - IotSceneRuleConditionOperatorEnum.NOT_BETWEEN, - IotSceneRuleConditionOperatorEnum.GREATER_THAN, - IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS, - IotSceneRuleConditionOperatorEnum.LESS_THAN, - IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS) - && NumberUtil.isNumber(messageValue) - && NumberUtils.isAllNumber(parameterValues)) { - springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, - NumberUtil.parseDouble(messageValue)); - springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE, - NumberUtil.parseDouble(condition.getParam())); - springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE_LIST, - convertList(parameterValues, NumberUtil::parseDouble)); - } - // 2.2 计算 Spring 表达式 - return (Boolean) SpringExpressionUtils.parseExpression(operator.getSpringExpression(), springExpressionVariables); - } catch (Exception e) { - log.error("[isTriggerConditionParameterMatched][消息({}) 规则场景编号({}) 的触发器({}) 的匹配表达式({}/{}) 计算异常]", - message, sceneRule.getId(), trigger, operator, springExpressionVariables, e); - return false; - } - } - /** * 执行规则场景的动作 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/AbstractIotSceneRuleMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/AbstractIotSceneRuleMatcher.java deleted file mode 100644 index 5d48bba293..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/AbstractIotSceneRuleMatcher.java +++ /dev/null @@ -1,174 +0,0 @@ -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 AbstractIotSceneRuleMatcher implements IotSceneRuleMatcher { - - // TODO @puhui999:这个是不是也是【通用】条件哈? - - /** - * 评估条件是否匹配 - * - * @param sourceValue 源值(来自消息) - * @param operator 操作符 - * @param paramValue 参数值(来自条件配置) - * @return 是否匹配 - */ - @SuppressWarnings("DataFlowIssue") - 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 操作符) - // TODO @puhui999:使用这个,会不会有问题?例如说:string 恰好有 , 分隔? - 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 isTriggerOperatorAndValueValid(IotSceneRuleDO.Trigger trigger) { - return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue()); - } - - /** - * 记录触发器匹配成功日志 - * - * @param message 设备消息 - * @param trigger 触发器配置 - */ - protected void logTriggerMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { - log.debug("[{}][消息({}) 匹配触发器({}) 成功]", getMatcherName(), message.getRequestId(), trigger.getType()); - } - - /** - * 记录触发器匹配失败日志 - * - * @param message 设备消息 - * @param trigger 触发器配置 - * @param reason 失败原因 - */ - protected void logTriggerMatchFailure(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, String reason) { - log.debug("[{}][消息({}) 匹配触发器({}) 失败: {}]", getMatcherName(), message.getRequestId(), trigger.getType(), reason); - } - - // ========== 【条件】相关工具方法 ========== - - /** - * 检查基础条件参数是否有效 - * - * @param condition 触发条件 - * @return 是否有效 - */ - protected boolean isBasicConditionValid(IotSceneRuleDO.TriggerCondition condition) { - return condition != null && condition.getType() != null; - } - - /** - * 检查条件操作符和参数是否有效 - * - * @param condition 触发条件 - * @return 是否有效 - */ - protected boolean isConditionOperatorAndParamValid(IotSceneRuleDO.TriggerCondition condition) { - return StrUtil.isNotBlank(condition.getOperator()) && StrUtil.isNotBlank(condition.getParam()); - } - - /** - * 记录条件匹配成功日志 - * - * @param message 设备消息 - * @param condition 触发条件 - */ - protected void logConditionMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { - log.debug("[{}][消息({}) 匹配条件({}) 成功]", getMatcherName(), message.getRequestId(), condition.getType()); - } - - /** - * 记录条件匹配失败日志 - * - * @param message 设备消息 - * @param condition 触发条件 - * @param reason 失败原因 - */ - protected void logConditionMatchFailure(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition, String reason) { - log.debug("[{}][消息({}) 匹配条件({}) 失败: {}]", getMatcherName(), message.getRequestId(), condition.getType(), reason); - } - - // ========== 【通用】工具方法 ========== - - /** - * 检查标识符是否匹配 - * - * @param expectedIdentifier 期望的标识符 - * @param actualIdentifier 实际的标识符 - * @return 是否匹配 - */ - protected boolean isIdentifierMatched(String expectedIdentifier, String actualIdentifier) { - return StrUtil.isNotBlank(expectedIdentifier) && expectedIdentifier.equals(actualIdentifier); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/CurrentTimeConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/CurrentTimeConditionMatcher.java deleted file mode 100644 index 94e7401b63..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/CurrentTimeConditionMatcher.java +++ /dev/null @@ -1,165 +0,0 @@ -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.IotSceneRuleConditionTypeEnum; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.format.DateTimeFormatter; - -/** - * 当前时间条件匹配器 - *

- * 处理时间相关的子条件匹配逻辑 - * - * @author HUIHUI - */ -@Component -@Slf4j -public class CurrentTimeConditionMatcher extends AbstractIotSceneRuleMatcher { - - /** - * 时间格式化器 - HH:mm:ss - */ - private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); - - /** - * 时间格式化器 - HH:mm - */ - private static final DateTimeFormatter TIME_FORMATTER_SHORT = DateTimeFormatter.ofPattern("HH:mm"); - - @Override - public MatcherType getMatcherType() { - return MatcherType.CONDITION; - } - - @Override - public IotSceneRuleConditionTypeEnum getSupportedConditionType() { - return IotSceneRuleConditionTypeEnum.CURRENT_TIME; - } - - @Override - public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { - // 1. 基础参数校验 - if (!isBasicConditionValid(condition)) { - logConditionMatchFailure(message, condition, "条件基础参数无效"); - return false; - } - - // 2. 检查操作符和参数是否有效 - if (!isConditionOperatorAndParamValid(condition)) { - logConditionMatchFailure(message, condition, "操作符或参数无效"); - return false; - } - - // 3. 根据操作符类型进行不同的时间匹配 - LocalDateTime now = LocalDateTime.now(); - String operator = condition.getOperator(); - String param = condition.getParam(); - boolean matched; - try { - if (operator.startsWith("date_time_")) { - // 日期时间匹配(时间戳) - matched = matchDateTime(now, operator, param); - } else if (operator.startsWith("time_")) { - // 当日时间匹配(HH:mm:ss) - matched = matchTime(now.toLocalTime(), operator, param); - } else { - // 其他操作符,使用通用条件评估器 - matched = evaluateCondition(now.toEpochSecond(java.time.ZoneOffset.of("+8")), operator, param); - } - - if (matched) { - logConditionMatchSuccess(message, condition); - } else { - logConditionMatchFailure(message, condition, "时间条件不匹配"); - } - } catch (Exception e) { - log.error("[CurrentTimeConditionMatcher][时间条件匹配异常] operator: {}, param: {}", operator, param, e); - logConditionMatchFailure(message, condition, "时间条件匹配异常: " + e.getMessage()); - matched = false; - } - return matched; - } - - /** - * 匹配日期时间(时间戳) - */ - private boolean matchDateTime(LocalDateTime now, String operator, String param) { - long currentTimestamp = now.toEpochSecond(java.time.ZoneOffset.of("+8")); - return evaluateCondition(currentTimestamp, operator.substring("date_time_".length()), param); - } - - /** - * 匹配当日时间(HH:mm:ss) - */ - private boolean matchTime(LocalTime currentTime, String operator, String param) { - try { - String actualOperator = operator.substring("time_".length()); - - // TODO @puhui999:if return 简化; - if ("between".equals(actualOperator)) { - // 时间区间匹配 - String[] timeRange = param.split(","); - if (timeRange.length != 2) { - return false; - } - LocalTime startTime = parseTime(timeRange[0].trim()); - LocalTime endTime = parseTime(timeRange[1].trim()); - return !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime); - } else { - // 单个时间比较 - LocalTime targetTime = parseTime(param); - // TODO @puhui999:枚举类; - switch (actualOperator) { - case ">": - return currentTime.isAfter(targetTime); - case "<": - return currentTime.isBefore(targetTime); - case ">=": - return !currentTime.isBefore(targetTime); - case "<=": - return !currentTime.isAfter(targetTime); - case "=": - return currentTime.equals(targetTime); - default: - return false; - } - } - } catch (Exception e) { - // TODO @puhui999:1)日志格式 [][];2)方法名不对哈; - log.error("[CurrentTimeConditionMatcher][时间解析异常] param: {}", param, e); - return false; - } - } - - /** - * 解析时间字符串 - */ - private LocalTime parseTime(String timeStr) { - // TODO @puhui999:可以用 hutool Assert 类简化 - if (StrUtil.isBlank(timeStr)) { - throw new IllegalArgumentException("时间字符串不能为空"); - } - // 尝试不同的时间格式 - try { - if (timeStr.length() == 5) { // HH:mm - return LocalTime.parse(timeStr, TIME_FORMATTER_SHORT); - } else { // HH:mm:ss - return LocalTime.parse(timeStr, TIME_FORMATTER); - } - } catch (Exception e) { - throw new IllegalArgumentException("时间格式无效: " + timeStr, e); - } - } - - @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/DeviceEventPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceEventPostTriggerMatcher.java deleted file mode 100644 index 3c832f6553..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceEventPostTriggerMatcher.java +++ /dev/null @@ -1,82 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; - -import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.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 AbstractIotSceneRuleMatcher { - - /** - * 设备事件上报消息方法 - */ - private static final String DEVICE_EVENT_POST_METHOD = IotDeviceMessageMethodEnum.EVENT_POST.getMethod(); - - @Override - public MatcherType getMatcherType() { - return MatcherType.TRIGGER; - } - - @Override - public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { - return IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST; - } - - @Override - public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { - // 1. 基础参数校验 - if (!isBasicTriggerValid(trigger)) { - logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); - return false; - } - - // 2. 检查消息方法是否匹配 - if (!DEVICE_EVENT_POST_METHOD.equals(message.getMethod())) { - logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_EVENT_POST_METHOD + ", 实际: " + message.getMethod()); - return false; - } - - // 3. 检查标识符是否匹配 - String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); - if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { - logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier); - return false; - } - - // 4. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配 - // 但如果配置了操作符和值,则需要进行条件匹配 - if (StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue())) { - Object eventData = message.getData(); - if (eventData == null) { - logTriggerMatchFailure(message, trigger, "消息中事件数据为空"); - return false; - } - - boolean matched = evaluateCondition(eventData, trigger.getOperator(), trigger.getValue()); - if (!matched) { - logTriggerMatchFailure(message, trigger, "事件数据条件不匹配"); - return false; - } - } - - logTriggerMatchSuccess(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/DevicePropertyConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DevicePropertyConditionMatcher.java deleted file mode 100644 index 37381500b9..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DevicePropertyConditionMatcher.java +++ /dev/null @@ -1,73 +0,0 @@ -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.IotSceneRuleConditionTypeEnum; -import org.springframework.stereotype.Component; - -/** - * 设备属性条件匹配器 - *

- * 处理设备属性相关的子条件匹配逻辑 - * - * @author HUIHUI - */ -@Component -public class DevicePropertyConditionMatcher extends AbstractIotSceneRuleMatcher { - - @Override - public MatcherType getMatcherType() { - return MatcherType.CONDITION; - } - - @Override - public IotSceneRuleConditionTypeEnum getSupportedConditionType() { - return IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY; - } - - // TODO @puhui999:参数校验的,要不要 1.1 1.2 1.3 1.4 ?这样最终看到 2. 3. 就是核心逻辑列; - @Override - public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { - // 1. 基础参数校验 - if (!isBasicConditionValid(condition)) { - logConditionMatchFailure(message, condition, "条件基础参数无效"); - return false; - } - - // 2. 检查标识符是否匹配 - String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); - if (!isIdentifierMatched(condition.getIdentifier(), messageIdentifier)) { - logConditionMatchFailure(message, condition, "标识符不匹配,期望: " + condition.getIdentifier() + ", 实际: " + messageIdentifier); - return false; - } - - // 3. 检查操作符和参数是否有效 - if (!isConditionOperatorAndParamValid(condition)) { - logConditionMatchFailure(message, condition, "操作符或参数无效"); - return false; - } - - // 4. 获取属性值 - Object propertyValue = message.getData(); - if (propertyValue == null) { - logConditionMatchFailure(message, condition, "消息中属性值为空"); - return false; - } - - // 5. 使用条件评估器进行匹配 - boolean matched = evaluateCondition(propertyValue, condition.getOperator(), condition.getParam()); - if (matched) { - logConditionMatchSuccess(message, condition); - } else { - logConditionMatchFailure(message, condition, "设备属性条件不匹配"); - } - return matched; - } - - @Override - public int getPriority() { - return 25; // 中等优先级 - } - -} 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 deleted file mode 100644 index ed228fc72d..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DevicePropertyPostTriggerMatcher.java +++ /dev/null @@ -1,85 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; - -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.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 AbstractIotSceneRuleMatcher { - - /** - * 设备属性上报消息方法 - */ - // TODO @puhui999:是不是不用枚举哈?直接使用 IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod() - private static final String DEVICE_PROPERTY_POST_METHOD = IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod(); - - @Override - public MatcherType getMatcherType() { - return MatcherType.TRIGGER; - } - - @Override - public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { - return IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST; - } - - @Override - public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { - // 1. 基础参数校验 - if (!isBasicTriggerValid(trigger)) { - logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); - return false; - } - - // 2. 检查消息方法是否匹配 - if (!DEVICE_PROPERTY_POST_METHOD.equals(message.getMethod())) { - logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_PROPERTY_POST_METHOD + ", 实际: " + message.getMethod()); - return false; - } - - // 3. 检查标识符是否匹配 - String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); - if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { - logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier); - return false; - } - - // 4. 检查操作符和值是否有效 - if (!isTriggerOperatorAndValueValid(trigger)) { - logTriggerMatchFailure(message, trigger, "操作符或值无效"); - return false; - } - - // 5. 获取属性值 - Object propertyValue = message.getData(); - if (propertyValue == null) { - logTriggerMatchFailure(message, trigger, "消息中属性值为空"); - return false; - } - - // 6. 使用条件评估器进行匹配 - boolean matched = evaluateCondition(propertyValue, trigger.getOperator(), trigger.getValue()); - if (matched) { - logTriggerMatchSuccess(message, trigger); - } else { - logTriggerMatchFailure(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 deleted file mode 100644 index c2b7e4ef82..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceServiceInvokeTriggerMatcher.java +++ /dev/null @@ -1,68 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; - -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.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 AbstractIotSceneRuleMatcher { - - /** - * 设备服务调用消息方法 - */ - private static final String DEVICE_SERVICE_INVOKE_METHOD = IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(); - - @Override - public MatcherType getMatcherType() { - return MatcherType.TRIGGER; - } - - @Override - public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { - return IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE; - } - - @Override - public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { - // 1. 基础参数校验 - if (!isBasicTriggerValid(trigger)) { - logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); - return false; - } - - // 2. 检查消息方法是否匹配 - if (!DEVICE_SERVICE_INVOKE_METHOD.equals(message.getMethod())) { - logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_SERVICE_INVOKE_METHOD + ", 实际: " + message.getMethod()); - return false; - } - - // 3. 检查标识符是否匹配 - String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); - if (!isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { - logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier); - return false; - } - - // 4. 对于服务调用触发器,通常只需要匹配服务标识符即可 - // 不需要检查操作符和值,因为服务调用本身就是触发条件 - - logTriggerMatchSuccess(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/DeviceStateConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceStateConditionMatcher.java deleted file mode 100644 index 69d3a7dcb7..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceStateConditionMatcher.java +++ /dev/null @@ -1,65 +0,0 @@ -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.IotSceneRuleConditionTypeEnum; -import org.springframework.stereotype.Component; - -/** - * 设备状态条件匹配器 - *

- * 处理设备状态相关的子条件匹配逻辑 - * - * @author HUIHUI - */ -@Component -public class DeviceStateConditionMatcher extends AbstractIotSceneRuleMatcher { - - @Override - public MatcherType getMatcherType() { - return MatcherType.CONDITION; - } - - @Override - public IotSceneRuleConditionTypeEnum getSupportedConditionType() { - return IotSceneRuleConditionTypeEnum.DEVICE_STATE; - } - - @Override - public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { - // 1. 基础参数校验 - if (!isBasicConditionValid(condition)) { - logConditionMatchFailure(message, condition, "条件基础参数无效"); - return false; - } - - // 2. 检查操作符和参数是否有效 - if (!isConditionOperatorAndParamValid(condition)) { - logConditionMatchFailure(message, condition, "操作符或参数无效"); - return false; - } - - // 3. 获取设备状态值 - // 设备状态通常在消息的 data 字段中 - Object stateValue = message.getData(); - if (stateValue == null) { - logConditionMatchFailure(message, condition, "消息中设备状态值为空"); - return false; - } - - // 4. 使用条件评估器进行匹配 - boolean matched = evaluateCondition(stateValue, condition.getOperator(), condition.getParam()); - if (matched) { - logConditionMatchSuccess(message, condition); - } else { - logConditionMatchFailure(message, condition, "设备状态条件不匹配"); - } - return matched; - } - - @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/DeviceStateUpdateTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceStateUpdateTriggerMatcher.java deleted file mode 100644 index 3a2a0e712f..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/DeviceStateUpdateTriggerMatcher.java +++ /dev/null @@ -1,77 +0,0 @@ -package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; - -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; -import org.springframework.stereotype.Component; - -/** - * 设备状态更新触发器匹配器 - *

- * 处理设备上下线状态变更的触发器匹配逻辑 - * - * @author HUIHUI - */ -@Component -public class DeviceStateUpdateTriggerMatcher extends AbstractIotSceneRuleMatcher { - - // TODO @puhui999:是不是不用枚举哈; - /** - * 设备状态更新消息方法 - */ - private static final String DEVICE_STATE_UPDATE_METHOD = IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod(); - - @Override - public MatcherType getMatcherType() { - return MatcherType.TRIGGER; - } - - @Override - public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { - return IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE; - } - - @Override - public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { - // 1. 基础参数校验 - if (!isBasicTriggerValid(trigger)) { - logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); - return false; - } - - // 2. 检查消息方法是否匹配 - if (!DEVICE_STATE_UPDATE_METHOD.equals(message.getMethod())) { - logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + DEVICE_STATE_UPDATE_METHOD + ", 实际: " + message.getMethod()); - return false; - } - - // 3. 检查操作符和值是否有效 - if (!isTriggerOperatorAndValueValid(trigger)) { - logTriggerMatchFailure(message, trigger, "操作符或值无效"); - return false; - } - - // 4. 获取设备状态值 - Object stateValue = message.getData(); - if (stateValue == null) { - logTriggerMatchFailure(message, trigger, "消息中设备状态值为空"); - return false; - } - - // 5. 使用条件评估器进行匹配 - boolean matched = evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue()); - if (matched) { - logTriggerMatchSuccess(message, trigger); - } else { - logTriggerMatchFailure(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/IotSceneRuleMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java index b9b439c786..84795d9fe5 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcher.java @@ -1,93 +1,20 @@ 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.IotSceneRuleConditionTypeEnum; -import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotSceneRuleConditionMatcher; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger.IotSceneRuleTriggerMatcher; /** - * IoT 场景规则匹配器统一接口 + * IoT 场景规则匹配器基础接口 *

- * 支持触发器匹配和条件匹配两种类型,遵循策略模式设计 + * 定义所有匹配器的通用行为,包括优先级、名称和启用状态 *

- * 匹配器类型说明: - * - 触发器匹配器:用于匹配主触发条件(如设备消息类型、定时器等) - * - 条件匹配器:用于匹配子条件(如设备状态、属性值、时间条件等) + * - {@link IotSceneRuleTriggerMatcher} 触发器匹配器 + * - {@link IotSceneRuleConditionMatcher} 条件匹配器 * * @author HUIHUI */ public interface IotSceneRuleMatcher { - // TODO @puhui999:MatcherTypeEnum; - // TODO @puhui999:可以考虑根据类型,新建 trigger、condition 包,然后把对应的实现类放进去哈; - /** - * 匹配器类型枚举 - */ - enum MatcherType { - - /** - * 触发器匹配器 - 用于匹配主触发条件 - */ - TRIGGER, - - /** - * 条件匹配器 - 用于匹配子条件 - */ - CONDITION - - } - - /** - * 获取匹配器类型 - * - * @return 匹配器类型 - */ - MatcherType getMatcherType(); - - // TODO @puhui999:【重要】有个思路,IotSceneRuleMatcher 拆分成 2 种 mather 接口;然后 AbstractIotSceneRuleMatcher 是个 Helper 工具类; - - // TODO @puhui999:是不是和 AbstractSceneRuleMatcher 一样,分下块; - - /** - * 获取支持的触发器类型(仅触发器匹配器需要实现) - * - * @return 触发器类型枚举,条件匹配器返回 null - */ - default IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { - return null; - } - - /** - * 获取支持的条件类型(仅条件匹配器需要实现) - * - * @return 条件类型枚举,触发器匹配器返回 null - */ - default IotSceneRuleConditionTypeEnum getSupportedConditionType() { - return null; - } - - /** - * 检查触发器是否匹配消息(仅触发器匹配器需要实现) - * - * @param message 设备消息 - * @param trigger 触发器配置 - * @return 是否匹配 - */ - default boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { - throw new UnsupportedOperationException("触发器匹配方法仅支持触发器匹配器"); - } - - /** - * 检查条件是否匹配消息(仅条件匹配器需要实现) - * - * @param message 设备消息 - * @param condition 触发条件 - * @return 是否匹配 - */ - default boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { - throw new UnsupportedOperationException("条件匹配方法仅支持条件匹配器"); - } - /** * 获取匹配优先级(数值越小优先级越高) *

@@ -99,16 +26,6 @@ public interface IotSceneRuleMatcher { return 100; } - // TODO @puhui999:如果目前没自定义,体感可以删除哈; - /** - * 获取匹配器名称,用于日志和调试 - * - * @return 匹配器名称 - */ - default String getMatcherName() { - return this.getClass().getSimpleName(); - } - /** * 是否启用该匹配器 *

diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java new file mode 100644 index 0000000000..7175e37a7e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherHelper.java @@ -0,0 +1,238 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher; + +import cn.hutool.core.text.CharPool; +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; +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 final class IotSceneRuleMatcherHelper { + + /** + * 私有构造函数,防止实例化 + */ + private IotSceneRuleMatcherHelper() { + } + + /** + * 评估条件是否匹配 + * + * @param sourceValue 源值(来自消息) + * @param operator 操作符 + * @param paramValue 参数值(来自条件配置) + * @return 是否匹配 + */ + public static boolean evaluateCondition(Object sourceValue, String operator, String paramValue) { + try { + // 1. 校验操作符是否合法 + IotSceneRuleConditionOperatorEnum operatorEnum = IotSceneRuleConditionOperatorEnum.operatorOf(operator); + if (operatorEnum == null) { + log.warn("[evaluateCondition][operator({}) 操作符无效]", operator); + return false; + } + + // 2. 构建 Spring 表达式变量 + return evaluateConditionWithOperatorEnum(sourceValue, operatorEnum, paramValue); + } catch (Exception e) { + log.error("[evaluateCondition][sourceValue({}) operator({}) paramValue({}) 条件评估异常]", + sourceValue, operator, paramValue, e); + return false; + } + } + + /** + * 使用操作符枚举评估条件是否匹配 + * + * @param sourceValue 源值(来自消息) + * @param operatorEnum 操作符枚举 + * @param paramValue 参数值(来自条件配置) + * @return 是否匹配 + */ + @SuppressWarnings("DataFlowIssue") + public static boolean evaluateConditionWithOperatorEnum(Object sourceValue, IotSceneRuleConditionOperatorEnum operatorEnum, String paramValue) { + try { + // 1. 构建 Spring 表达式变量 + Map springExpressionVariables = buildSpringExpressionVariables(sourceValue, operatorEnum, paramValue); + + // 2. 计算 Spring 表达式 + return (Boolean) SpringExpressionUtils.parseExpression(operatorEnum.getSpringExpression(), springExpressionVariables); + } catch (Exception e) { + log.error("[evaluateConditionWithOperatorEnum][sourceValue({}) operatorEnum({}) paramValue({}) 条件评估异常]", + sourceValue, operatorEnum, paramValue, e); + return false; + } + } + + /** + * 构建 Spring 表达式变量 + */ + private static Map buildSpringExpressionVariables(Object sourceValue, IotSceneRuleConditionOperatorEnum operatorEnum, String paramValue) { + Map springExpressionVariables = new HashMap<>(); + + // 设置源值 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, sourceValue); + + // 处理参数值 + if (StrUtil.isNotBlank(paramValue)) { + List parameterValues = StrUtil.splitTrim(paramValue, CharPool.COMMA); + + // 设置原始参数值 + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE, paramValue); + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE_LIST, parameterValues); + + // 特殊处理:解决数字比较问题 + // Spring 表达式基于 compareTo 方法,对数字的比较存在问题,需要转换为数字类型 + if (isNumericComparisonOperator(operatorEnum) && isNumericComparison(sourceValue, parameterValues)) { + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_SOURCE, + NumberUtil.parseDouble(String.valueOf(sourceValue))); + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE, + NumberUtil.parseDouble(paramValue)); + springExpressionVariables.put(IotSceneRuleConditionOperatorEnum.SPRING_EXPRESSION_VALUE_LIST, + convertList(parameterValues, NumberUtil::parseDouble)); + } + } + + return springExpressionVariables; + } + + /** + * 判断是否为数字比较操作符 + */ + private static boolean isNumericComparisonOperator(IotSceneRuleConditionOperatorEnum operatorEnum) { + return ObjectUtils.equalsAny(operatorEnum, + IotSceneRuleConditionOperatorEnum.BETWEEN, + IotSceneRuleConditionOperatorEnum.NOT_BETWEEN, + IotSceneRuleConditionOperatorEnum.GREATER_THAN, + IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS, + IotSceneRuleConditionOperatorEnum.LESS_THAN, + IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS); + } + + /** + * 判断是否为数字比较场景 + */ + private static boolean isNumericComparison(Object sourceValue, List parameterValues) { + return NumberUtil.isNumber(String.valueOf(sourceValue)) && NumberUtils.isAllNumber(parameterValues); + } + + // ========== 【触发器】相关工具方法 ========== + + /** + * 检查基础触发器参数是否有效 + * + * @param trigger 触发器配置 + * @return 是否有效 + */ + public static boolean isBasicTriggerValid(IotSceneRuleDO.Trigger trigger) { + return trigger != null && trigger.getType() != null; + } + + /** + * 检查触发器操作符和值是否有效 + * + * @param trigger 触发器配置 + * @return 是否有效 + */ + public static boolean isTriggerOperatorAndValueValid(IotSceneRuleDO.Trigger trigger) { + return StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue()); + } + + /** + * 记录触发器匹配成功日志 + * + * @param message 设备消息 + * @param trigger 触发器配置 + */ + public static void logTriggerMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + log.debug("[isMatched][message({}) trigger({}) 匹配触发器成功]", message.getRequestId(), trigger.getType()); + } + + /** + * 记录触发器匹配失败日志 + * + * @param message 设备消息 + * @param trigger 触发器配置 + * @param reason 失败原因 + */ + public static void logTriggerMatchFailure(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger, String reason) { + log.debug("[isMatched][message({}) trigger({}) reason({}) 匹配触发器失败]", message.getRequestId(), trigger.getType(), reason); + } + + // ========== 【条件】相关工具方法 ========== + + /** + * 检查基础条件参数是否有效 + * + * @param condition 触发条件 + * @return 是否有效 + */ + public static boolean isBasicConditionValid(IotSceneRuleDO.TriggerCondition condition) { + return condition != null && condition.getType() != null; + } + + /** + * 检查条件操作符和参数是否有效 + * + * @param condition 触发条件 + * @return 是否有效 + */ + public static boolean isConditionOperatorAndParamValid(IotSceneRuleDO.TriggerCondition condition) { + return StrUtil.isNotBlank(condition.getOperator()) && StrUtil.isNotBlank(condition.getParam()); + } + + /** + * 记录条件匹配成功日志 + * + * @param message 设备消息 + * @param condition 触发条件 + */ + public static void logConditionMatchSuccess(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { + log.debug("[isMatched][message({}) condition({}) 匹配条件成功]", message.getRequestId(), condition.getType()); + } + + /** + * 记录条件匹配失败日志 + * + * @param message 设备消息 + * @param condition 触发条件 + * @param reason 失败原因 + */ + public static void logConditionMatchFailure(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition, String reason) { + log.debug("[isMatched][message({}) condition({}) reason({}) 匹配条件失败]", message.getRequestId(), condition.getType(), reason); + } + + // ========== 【通用】工具方法 ========== + + /** + * 检查标识符是否匹配 + * + * @param expectedIdentifier 期望的标识符 + * @param actualIdentifier 实际的标识符 + * @return 是否匹配 + */ + public static boolean isIdentifierMatched(String expectedIdentifier, String actualIdentifier) { + return StrUtil.isNotBlank(expectedIdentifier) && expectedIdentifier.equals(actualIdentifier); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java index e95e553cab..103c09a1eb 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleMatcherManager.java @@ -5,6 +5,8 @@ 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.IotSceneRuleConditionTypeEnum; import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotSceneRuleConditionMatcher; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger.IotSceneRuleTriggerMatcher; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -12,7 +14,7 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; -import static cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum.findTriggerTypeEnum; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; /** * IoT 场景规则匹配器统一管理器 @@ -27,21 +29,18 @@ public class IotSceneRuleMatcherManager { /** * 触发器匹配器映射表 - * Key: 触发器类型枚举 - * Value: 对应的匹配器实例 */ - private final Map triggerMatchers; + private final Map triggerMatchers; /** * 条件匹配器映射表 - * Key: 条件类型枚举 - * Value: 对应的匹配器实例 */ - private final Map conditionMatchers; + private final Map conditionMatchers; /** * 所有匹配器列表(按优先级排序) */ + // TODO @puhui999:貌似 local variable 也可以 private final List allMatchers; public IotSceneRuleMatcherManager(List matchers) { @@ -60,50 +59,44 @@ public class IotSceneRuleMatcherManager { .collect(Collectors.toList()); // 分离触发器匹配器和条件匹配器 - List triggerMatchers = this.allMatchers.stream() - .filter(matcher -> matcher.getMatcherType() == IotSceneRuleMatcher.MatcherType.TRIGGER) + List triggerMatchers = this.allMatchers.stream() + .filter(matcher -> matcher instanceof IotSceneRuleTriggerMatcher) + .map(matcher -> (IotSceneRuleTriggerMatcher) matcher) .toList(); - List conditionMatchers = this.allMatchers.stream() - .filter(matcher -> matcher.getMatcherType() == IotSceneRuleMatcher.MatcherType.CONDITION) + List conditionMatchers = this.allMatchers.stream() + .filter(matcher -> matcher instanceof IotSceneRuleConditionMatcher) + .map(matcher -> (IotSceneRuleConditionMatcher) matcher) .toList(); // 构建触发器匹配器映射表 - // TODO @puhui999:convertMap() - this.triggerMatchers = triggerMatchers.stream() - .collect(Collectors.toMap( - IotSceneRuleMatcher::getSupportedTriggerType, - Function.identity(), - (existing, replacement) -> { - log.warn("[IotSceneRuleMatcherManager][触发器类型({})存在多个匹配器,使用优先级更高的: {}]", - existing.getSupportedTriggerType(), - existing.getPriority() <= replacement.getPriority() ? existing.getMatcherName() : replacement.getMatcherName()); - return existing.getPriority() <= replacement.getPriority() ? existing : replacement; - }, - LinkedHashMap::new - )); + this.triggerMatchers = convertMap(triggerMatchers, IotSceneRuleTriggerMatcher::getSupportedTriggerType, + Function.identity(), + (existing, replacement) -> { + log.warn("[IotSceneRuleMatcherManager][触发器类型({})存在多个匹配器,使用优先级更高的: {}]", + existing.getSupportedTriggerType(), + existing.getPriority() <= replacement.getPriority() ? + existing.getSupportedTriggerType() : replacement.getSupportedTriggerType()); + return existing.getPriority() <= replacement.getPriority() ? existing : replacement; + }, LinkedHashMap::new); // 构建条件匹配器映射表 - this.conditionMatchers = conditionMatchers.stream() - .collect(Collectors.toMap( - IotSceneRuleMatcher::getSupportedConditionType, - Function.identity(), - (existing, replacement) -> { - log.warn("[IotSceneRuleMatcherManager][条件类型({})存在多个匹配器,使用优先级更高的: {}]", - existing.getSupportedConditionType(), - existing.getPriority() <= replacement.getPriority() ? existing.getMatcherName() : replacement.getMatcherName()); - return existing.getPriority() <= replacement.getPriority() ? existing : replacement; - }, - LinkedHashMap::new - )); + this.conditionMatchers = convertMap(conditionMatchers, IotSceneRuleConditionMatcher::getSupportedConditionType, + Function.identity(), + (existing, replacement) -> { + log.warn("[IotSceneRuleMatcherManager][条件类型({})存在多个匹配器,使用优先级更高的: {}]", + existing.getSupportedConditionType(), + existing.getPriority() <= replacement.getPriority() ? + existing.getSupportedConditionType() : replacement.getSupportedConditionType()); + return existing.getPriority() <= replacement.getPriority() ? existing : replacement; + }, + LinkedHashMap::new); // 日志输出初始化信息 - log.info("[IotSceneRuleMatcherManager][初始化完成,共加载 {} 个匹配器,其中触发器匹配器 {} 个,条件匹配器 {} 个]", + log.info("[IotSceneRuleMatcherManager][初始化完成,共加载({})个匹配器,其中触发器匹配器({})个,条件匹配器({})个]", this.allMatchers.size(), this.triggerMatchers.size(), this.conditionMatchers.size()); this.triggerMatchers.forEach((type, matcher) -> - log.info("[IotSceneRuleMatcherManager][触发器匹配器] 类型: {}, 匹配器: {}, 优先级: {}", - type, matcher.getMatcherName(), matcher.getPriority())); + log.info("[IotSceneRuleMatcherManager][触发器匹配器类型: ({}), 优先级: ({})] ", type, matcher.getPriority())); this.conditionMatchers.forEach((type, matcher) -> - log.info("[IotSceneRuleMatcherManager][条件匹配器] 类型: {}, 匹配器: {}, 优先级: {}", - type, matcher.getMatcherName(), matcher.getPriority())); + log.info("[IotSceneRuleMatcherManager][条件匹配器类型: ({}), 优先级: ({})]", type, matcher.getPriority())); } /** @@ -114,27 +107,25 @@ public class IotSceneRuleMatcherManager { * @return 是否匹配 */ public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { - // TODO @puhui999:日志优化下;claude 打出来的日志风格,和项目有点不一样哈; if (message == null || trigger == null || trigger.getType() == null) { - log.debug("[isMatched][参数无效] message: {}, trigger: {}", message, trigger); + log.debug("[isMatched][message({}) trigger({}) 参数无效]", message, trigger); return false; } - IotSceneRuleTriggerTypeEnum triggerType = findTriggerTypeEnum(trigger.getType()); + IotSceneRuleTriggerTypeEnum triggerType = IotSceneRuleTriggerTypeEnum.typeOf(trigger.getType()); if (triggerType == null) { - log.warn("[isMatched][未知的触发器类型: {}]", trigger.getType()); + log.warn("[isMatched][triggerType({}) 未知的触发器类型]", trigger.getType()); return false; } - IotSceneRuleMatcher matcher = triggerMatchers.get(triggerType); + IotSceneRuleTriggerMatcher matcher = triggerMatchers.get(triggerType); if (matcher == null) { - log.warn("[isMatched][触发器类型({})没有对应的匹配器]", triggerType); + log.warn("[isMatched][triggerType({}) 没有对应的匹配器]", triggerType); return false; } try { return matcher.isMatched(message, trigger); } catch (Exception e) { - log.error("[isMatched][触发器匹配异常] message: {}, trigger: {}, matcher: {}", - message, trigger, matcher.getMatcherName(), e); + log.error("[isMatched][触发器匹配异常] message: {}, trigger: {}", message, trigger, e); return false; } } @@ -148,28 +139,27 @@ public class IotSceneRuleMatcherManager { */ public boolean isConditionMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { if (message == null || condition == null || condition.getType() == null) { - log.debug("[isConditionMatched][参数无效] message: {}, condition: {}", message, condition); + log.debug("[isConditionMatched][message({}) condition({}) 参数无效]", message, condition); return false; } // 根据条件类型查找对应的匹配器 IotSceneRuleConditionTypeEnum conditionType = findConditionTypeEnum(condition.getType()); if (conditionType == null) { - log.warn("[isConditionMatched][未知的条件类型: {}]", condition.getType()); + log.warn("[isConditionMatched][conditionType({}) 未知的条件类型]", condition.getType()); return false; } - - IotSceneRuleMatcher matcher = conditionMatchers.get(conditionType); + IotSceneRuleConditionMatcher matcher = conditionMatchers.get(conditionType); if (matcher == null) { - log.warn("[isConditionMatched][条件类型({})没有对应的匹配器]", conditionType); + log.warn("[isConditionMatched][conditionType({}) 没有对应的匹配器]", conditionType); return false; } + // 执行匹配逻辑 try { return matcher.isMatched(message, condition); } catch (Exception e) { - log.error("[isConditionMatched][条件匹配异常] message: {}, condition: {}, matcher: {}", - message, condition, matcher.getMatcherName(), e); + log.error("[isConditionMatched][message({}) condition({}) 条件匹配异常]", message, condition, e); return false; } } @@ -181,12 +171,15 @@ public class IotSceneRuleMatcherManager { * @return 条件类型枚举 */ private IotSceneRuleConditionTypeEnum findConditionTypeEnum(Integer typeValue) { + // TODO @puhui999:是不是搞到枚举类里? return Arrays.stream(IotSceneRuleConditionTypeEnum.values()) .filter(type -> type.getType().equals(typeValue)) .findFirst() .orElse(null); } + // TODO @puhui999:下面两个方法,是不是也可以删除哈? + /** * 获取所有支持的触发器类型 * @@ -205,65 +198,4 @@ public class IotSceneRuleMatcherManager { return new HashSet<>(conditionMatchers.keySet()); } - // TODO @puhui999:用不到的方法,可以去掉先哈; - - /** - * 获取指定触发器类型的匹配器 - * - * @param triggerType 触发器类型 - * @return 匹配器实例,如果不存在则返回 null - */ - public IotSceneRuleMatcher getTriggerMatcher(IotSceneRuleTriggerTypeEnum triggerType) { - return triggerMatchers.get(triggerType); - } - - /** - * 获取指定条件类型的匹配器 - * - * @param conditionType 条件类型 - * @return 匹配器实例,如果不存在则返回 null - */ - public IotSceneRuleMatcher getConditionMatcher(IotSceneRuleConditionTypeEnum conditionType) { - return conditionMatchers.get(conditionType); - } - - // TODO @puhui999:是不是不用这个哈;直接 @Getter,单测直接处理; - /** - * 获取所有匹配器的统计信息 - * - * @return 统计信息映射表 - */ - public Map getMatcherStatistics() { - Map statistics = new HashMap<>(); - statistics.put("totalMatchers", allMatchers.size()); - statistics.put("triggerMatchers", triggerMatchers.size()); - statistics.put("conditionMatchers", conditionMatchers.size()); - statistics.put("supportedTriggerTypes", getSupportedTriggerTypes()); - statistics.put("supportedConditionTypes", getSupportedConditionTypes()); - - // 触发器匹配器详情 - Map triggerMatcherDetails = new HashMap<>(); - triggerMatchers.forEach((type, matcher) -> { - Map detail = new HashMap<>(); - detail.put("matcherName", matcher.getMatcherName()); - detail.put("priority", matcher.getPriority()); - detail.put("enabled", matcher.isEnabled()); - triggerMatcherDetails.put(type.name(), detail); - }); - statistics.put("triggerMatcherDetails", triggerMatcherDetails); - - // 条件匹配器详情 - Map conditionMatcherDetails = new HashMap<>(); - conditionMatchers.forEach((type, matcher) -> { - Map detail = new HashMap<>(); - detail.put("matcherName", matcher.getMatcherName()); - detail.put("priority", matcher.getPriority()); - detail.put("enabled", matcher.isEnabled()); - conditionMatcherDetails.put(type.name(), detail); - }); - statistics.put("conditionMatcherDetails", conditionMatcherDetails); - - 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 deleted file mode 100644 index edc18771a3..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/TimerTriggerMatcher.java +++ /dev/null @@ -1,86 +0,0 @@ -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 AbstractIotSceneRuleMatcher { - - @Override - public MatcherType getMatcherType() { - return MatcherType.TRIGGER; - } - - @Override - public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { - return IotSceneRuleTriggerTypeEnum.TIMER; - } - - @Override - public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { - // 1. 基础参数校验 - if (!isBasicTriggerValid(trigger)) { - logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); - return false; - } - - // 2. 检查 CRON 表达式是否存在 - if (StrUtil.isBlank(trigger.getCronExpression())) { - logTriggerMatchFailure(message, trigger, "定时触发器缺少 CRON 表达式"); - return false; - } - - // 3. 定时触发器通常不依赖具体的设备消息 - // 它是通过定时任务调度器触发的,这里主要是验证配置的有效性 - - // 4. 可以添加 CRON 表达式格式验证 - if (!isValidCronExpression(trigger.getCronExpression())) { - logTriggerMatchFailure(message, trigger, "CRON 表达式格式无效: " + trigger.getCronExpression()); - return false; - } - - logTriggerMatchSuccess(message, trigger); - return true; - } - - /** - * 验证 CRON 表达式格式是否有效 - * - * @param cronExpression CRON 表达式 - * @return 是否有效 - */ - private boolean isValidCronExpression(String cronExpression) { - // TODO @puhui999:CronExpression.isValidExpression(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/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java new file mode 100644 index 0000000000..0daf4eefd5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcher.java @@ -0,0 +1,226 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharPool; +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.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * 当前时间条件匹配器 + *

+ * 处理时间相关的子条件匹配逻辑 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class CurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher { + + /** + * 时间格式化器 - HH:mm:ss + */ + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); + + /** + * 时间格式化器 - HH:mm + */ + private static final DateTimeFormatter TIME_FORMATTER_SHORT = DateTimeFormatter.ofPattern("HH:mm"); + + @Override + public IotSceneRuleConditionTypeEnum getSupportedConditionType() { + return IotSceneRuleConditionTypeEnum.CURRENT_TIME; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicConditionValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "条件基础参数无效"); + return false; + } + + // 1.2 检查操作符和参数是否有效 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "操作符或参数无效"); + return false; + } + + // 1.3 验证操作符是否为支持的时间操作符 + String operator = condition.getOperator(); + IotSceneRuleConditionOperatorEnum operatorEnum = IotSceneRuleConditionOperatorEnum.operatorOf(operator); + if (operatorEnum == null) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "无效的操作符: " + operator); + return false; + } + + if (!isTimeOperator(operatorEnum)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "不支持的时间操作符: " + operator); + return false; + } + + // 2.1 执行时间匹配 + boolean matched = executeTimeMatching(operatorEnum, condition.getParam()); + + // 2.2 记录匹配结果 + if (matched) { + IotSceneRuleMatcherHelper.logConditionMatchSuccess(message, condition); + } else { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "时间条件不匹配"); + } + + return matched; + } + + /** + * 执行时间匹配逻辑 + * 直接实现时间条件匹配,不使用 Spring EL 表达式 + */ + private boolean executeTimeMatching(IotSceneRuleConditionOperatorEnum operatorEnum, String param) { + try { + LocalDateTime now = LocalDateTime.now(); + + if (isDateTimeOperator(operatorEnum)) { + // 日期时间匹配(时间戳) + long currentTimestamp = now.toEpochSecond(java.time.ZoneOffset.of("+8")); + return matchDateTime(currentTimestamp, operatorEnum, param); + } else { + // 当日时间匹配(HH:mm:ss) + return matchTime(now.toLocalTime(), operatorEnum, param); + } + } catch (Exception e) { + log.error("[executeTimeMatching][operatorEnum({}) param({}) 时间匹配异常]", operatorEnum, param, e); + return false; + } + } + + /** + * 判断是否为日期时间操作符 + */ + private boolean isDateTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) { + return operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN || + operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_LESS_THAN || + operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN; + } + + /** + * 判断是否为时间操作符 + */ + private boolean isTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) { + return operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN || + operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_LESS_THAN || + operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_BETWEEN || + isDateTimeOperator(operatorEnum); + } + + // TODO @puhui999:switch 兼容下 jdk8 + /** + * 匹配日期时间(时间戳) + * 直接实现时间戳比较逻辑 + */ + private boolean matchDateTime(long currentTimestamp, IotSceneRuleConditionOperatorEnum operatorEnum, String param) { + try { + long targetTimestamp = Long.parseLong(param); + return switch (operatorEnum) { + case DATE_TIME_GREATER_THAN -> currentTimestamp > targetTimestamp; + case DATE_TIME_LESS_THAN -> currentTimestamp < targetTimestamp; + case DATE_TIME_BETWEEN -> matchDateTimeBetween(currentTimestamp, param); + default -> { + log.warn("[matchDateTime][operatorEnum({}) 不支持的日期时间操作符]", operatorEnum); + yield false; + } + }; + } catch (Exception e) { + log.error("[matchDateTime][operatorEnum({}) param({}) 日期时间匹配异常]", operatorEnum, param, e); + return false; + } + } + + /** + * 匹配日期时间区间 + */ + private boolean matchDateTimeBetween(long currentTimestamp, String param) { + List timestampRange = StrUtil.splitTrim(param, CharPool.COMMA); + if (timestampRange.size() != 2) { + log.warn("[matchDateTimeBetween][param({}) 时间戳区间参数格式错误]", param); + return false; + } + long startTimestamp = Long.parseLong(timestampRange.get(0).trim()); + long endTimestamp = Long.parseLong(timestampRange.get(1).trim()); + return currentTimestamp >= startTimestamp && currentTimestamp <= endTimestamp; + } + + /** + * 匹配当日时间(HH:mm:ss) + * 直接实现时间比较逻辑 + */ + private boolean matchTime(LocalTime currentTime, IotSceneRuleConditionOperatorEnum operatorEnum, String param) { + try { + LocalTime targetTime = parseTime(param); + return switch (operatorEnum) { + case TIME_GREATER_THAN -> currentTime.isAfter(targetTime); + case TIME_LESS_THAN -> currentTime.isBefore(targetTime); + case TIME_BETWEEN -> matchTimeBetween(currentTime, param); + default -> { + log.warn("[matchTime][operatorEnum({}) 不支持的时间操作符]", operatorEnum); + yield false; + } + }; + } catch (Exception e) { + log.error("[matchTime][][operatorEnum({}) param({}) 时间解析异常]", operatorEnum, param, e); + return false; + } + } + + /** + * 匹配时间区间 + */ + private boolean matchTimeBetween(LocalTime currentTime, String param) { + List timeRange = StrUtil.splitTrim(param, CharPool.COMMA); + if (timeRange.size() != 2) { + log.warn("[matchTimeBetween][param({}) 时间区间参数格式错误]", param); + return false; + } + LocalTime startTime = parseTime(timeRange.get(0).trim()); + LocalTime endTime = parseTime(timeRange.get(1).trim()); + return !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime); + } + + /** + * 解析时间字符串 + * 支持 HH:mm 和 HH:mm:ss 两种格式 + */ + private LocalTime parseTime(String timeStr) { + Assert.isFalse(StrUtil.isBlank(timeStr), "时间字符串不能为空"); + + try { + // 尝试不同的时间格式 + if (timeStr.length() == 5) { // HH:mm + return LocalTime.parse(timeStr, TIME_FORMATTER_SHORT); + } else if (timeStr.length() == 8) { // HH:mm:ss + return LocalTime.parse(timeStr, TIME_FORMATTER); + } else { + throw new IllegalArgumentException("时间格式长度不正确,期望 HH:mm 或 HH:mm:ss 格式"); + } + } catch (Exception e) { + log.error("[parseTime][timeStr({}) 时间格式解析失败]", timeStr, e); + throw new IllegalArgumentException("时间格式无效: " + timeStr, e); + } + } + + @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/condition/DevicePropertyConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java new file mode 100644 index 0000000000..e6fe043d0a --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcher.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +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.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备属性条件匹配器 + *

+ * 处理设备属性相关的子条件匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DevicePropertyConditionMatcher implements IotSceneRuleConditionMatcher { + + @Override + public IotSceneRuleConditionTypeEnum getSupportedConditionType() { + return IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY; + } + + // TODO @puhui999:matches 会不会更好?参考的 org.hamcrest.Matcher jdk 接口 + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicConditionValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "条件基础参数无效"); + return false; + } + + // 1.2 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!IotSceneRuleMatcherHelper.isIdentifierMatched(condition.getIdentifier(), messageIdentifier)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "标识符不匹配,期望: " + condition.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 1.3 检查操作符和参数是否有效 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "操作符或参数无效"); + return false; + } + + // 2.1. 获取属性值 + Object propertyValue = message.getParams(); + if (propertyValue == null) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中属性值为空"); + return false; + } + + // 2.2 使用条件评估器进行匹配 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(propertyValue, condition.getOperator(), condition.getParam()); + if (matched) { + IotSceneRuleMatcherHelper.logConditionMatchSuccess(message, condition); + } else { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "设备属性条件不匹配"); + } + return matched; + } + + @Override + public int getPriority() { + return 25; // 中等优先级 + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java new file mode 100644 index 0000000000..a25bef467f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcher.java @@ -0,0 +1,60 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +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.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备状态条件匹配器 + *

+ * 处理设备状态相关的子条件匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceStateConditionMatcher implements IotSceneRuleConditionMatcher { + + @Override + public IotSceneRuleConditionTypeEnum getSupportedConditionType() { + return IotSceneRuleConditionTypeEnum.DEVICE_STATE; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicConditionValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "条件基础参数无效"); + return false; + } + + // 1.2 检查操作符和参数是否有效 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "操作符或参数无效"); + return false; + } + + // 2.1 获取设备状态值 + Object stateValue = message.getParams(); + if (stateValue == null) { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "消息中设备状态值为空"); + return false; + } + + // 2.2 使用条件评估器进行匹配 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateValue, condition.getOperator(), condition.getParam()); + if (matched) { + IotSceneRuleMatcherHelper.logConditionMatchSuccess(message, condition); + } else { + IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "设备状态条件不匹配"); + } + return matched; + } + + @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/condition/IotSceneRuleConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java new file mode 100644 index 0000000000..2e44b1174d --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotSceneRuleConditionMatcher.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +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.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher; + +/** + * IoT 场景规则条件匹配器接口 + *

+ * 专门处理子条件的匹配逻辑,如设备状态、属性值、时间条件等 + *

+ * 条件匹配器负责判断设备消息是否满足场景规则的附加条件, + * 在触发器匹配成功后进行进一步的条件筛选 + * + * @author HUIHUI + */ +public interface IotSceneRuleConditionMatcher extends IotSceneRuleMatcher { + + /** + * 获取支持的条件类型 + * + * @return 条件类型枚举 + */ + IotSceneRuleConditionTypeEnum getSupportedConditionType(); + + /** + * 检查条件是否匹配消息 + *

+ * 判断设备消息是否满足指定的触发条件 + * + * @param message 设备消息 + * @param condition 触发条件 + * @return 是否匹配 + */ + boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.TriggerCondition condition); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java new file mode 100644 index 0000000000..8d0d156851 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcher.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备事件上报触发器匹配器 + *

+ * 处理设备事件上报的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceEventPostTriggerMatcher implements IotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicTriggerValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 1.2 检查消息方法是否匹配 + if (!IotDeviceMessageMethodEnum.EVENT_POST.getMethod().equals(message.getMethod())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + + IotDeviceMessageMethodEnum.EVENT_POST.getMethod() + ", 实际: " + message.getMethod()); + return false; + } + + // 1.3 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!IotSceneRuleMatcherHelper.isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + + trigger.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 2. 对于事件触发器,通常不需要检查操作符和值,只要事件发生即匹配 + // 但如果配置了操作符和值,则需要进行条件匹配 + if (StrUtil.isNotBlank(trigger.getOperator()) && StrUtil.isNotBlank(trigger.getValue())) { + Object eventData = message.getData(); + if (eventData == null) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中事件数据为空"); + return false; + } + + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(eventData, trigger.getOperator(), trigger.getValue()); + if (!matched) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "事件数据条件不匹配"); + return false; + } + } + + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(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/trigger/DevicePropertyPostTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java new file mode 100644 index 0000000000..654305c858 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcher.java @@ -0,0 +1,77 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备属性上报触发器匹配器 + *

+ * 处理设备属性数据上报的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DevicePropertyPostTriggerMatcher implements IotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicTriggerValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 1.2 检查消息方法是否匹配 + if (!IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod().equals(message.getMethod())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + + IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod() + ", 实际: " + message.getMethod()); + return false; + } + + // 1.3 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!IotSceneRuleMatcherHelper.isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + + trigger.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 1.4 检查操作符和值是否有效 + if (!IotSceneRuleMatcherHelper.isTriggerOperatorAndValueValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "操作符或值无效"); + return false; + } + + // 2.1 获取属性值 + Object propertyValue = message.getParams(); + if (propertyValue == null) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中属性值为空"); + return false; + } + + // 2.2 使用条件评估器进行匹配 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(propertyValue, trigger.getOperator(), trigger.getValue()); + if (matched) { + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); + } else { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(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/trigger/DeviceServiceInvokeTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java new file mode 100644 index 0000000000..da72bdaf3c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcher.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备服务调用触发器匹配器 + *

+ * 处理设备服务调用的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceServiceInvokeTriggerMatcher implements IotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicTriggerValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 1.2 检查消息方法是否匹配 + if (!IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod().equals(message.getMethod())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod() + ", 实际: " + message.getMethod()); + return false; + } + + // 1.3 检查标识符是否匹配 + String messageIdentifier = IotDeviceMessageUtils.getIdentifier(message); + if (!IotSceneRuleMatcherHelper.isIdentifierMatched(trigger.getIdentifier(), messageIdentifier)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "标识符不匹配,期望: " + trigger.getIdentifier() + ", 实际: " + messageIdentifier); + return false; + } + + // 2. 对于服务调用触发器,通常只需要匹配服务标识符即可 + // 不需要检查操作符和值,因为服务调用本身就是触发条件 + // TODO @puhui999: 服务调用时校验输入参数是否匹配条件 + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(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/trigger/DeviceStateUpdateTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java new file mode 100644 index 0000000000..139b47ac7c --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcher.java @@ -0,0 +1,69 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.springframework.stereotype.Component; + +/** + * 设备状态更新触发器匹配器 + *

+ * 处理设备上下线状态变更的触发器匹配逻辑 + * + * @author HUIHUI + */ +@Component +public class DeviceStateUpdateTriggerMatcher implements IotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicTriggerValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 1.2 检查消息方法是否匹配 + if (!IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod().equals(message.getMethod())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息方法不匹配,期望: " + + IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod() + ", 实际: " + message.getMethod()); + return false; + } + + // 1.3 检查操作符和值是否有效 + if (!IotSceneRuleMatcherHelper.isTriggerOperatorAndValueValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "操作符或值无效"); + return false; + } + + // 2.1 获取设备状态值 + Object stateValue = message.getParams(); + if (stateValue == null) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "消息中设备状态值为空"); + return false; + } + + // 2.2 使用条件评估器进行匹配 + // TODO @puhui999: 状态匹配重新实现 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(stateValue, trigger.getOperator(), trigger.getValue()); + if (matched) { + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); + } else { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(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/trigger/IotSceneRuleTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java new file mode 100644 index 0000000000..322421738e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/IotSceneRuleTriggerMatcher.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcher; + +/** + * IoT 场景规则触发器匹配器接口 + *

+ * 专门处理主触发条件的匹配逻辑,如设备消息类型、定时器等 + *

+ * 触发器匹配器负责判断设备消息是否满足场景规则的主触发条件, + * 是场景规则执行的第一道门槛 + * + * @author HUIHUI + */ +public interface IotSceneRuleTriggerMatcher extends IotSceneRuleMatcher { + + /** + * 获取支持的触发器类型 + * + * @return 触发器类型枚举 + */ + IotSceneRuleTriggerTypeEnum getSupportedTriggerType(); + + /** + * 检查触发器是否匹配消息 + *

+ * 判断设备消息是否满足指定的触发器条件 + * + * @param message 设备消息 + * @param trigger 触发器配置 + * @return 是否匹配 + */ + boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger); + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java new file mode 100644 index 0000000000..5c9ac13cf4 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcher.java @@ -0,0 +1,57 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +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 cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import org.quartz.CronExpression; +import org.springframework.stereotype.Component; + +/** + * 定时触发器匹配器 + *

+ * 处理定时触发的触发器匹配逻辑 + * 注意:定时触发器不依赖设备消息,主要用于定时任务场景 + * + * @author HUIHUI + */ +@Component +public class TimerTriggerMatcher implements IotSceneRuleTriggerMatcher { + + @Override + public IotSceneRuleTriggerTypeEnum getSupportedTriggerType() { + return IotSceneRuleTriggerTypeEnum.TIMER; + } + + @Override + public boolean isMatched(IotDeviceMessage message, IotSceneRuleDO.Trigger trigger) { + // 1.1 基础参数校验 + if (!IotSceneRuleMatcherHelper.isBasicTriggerValid(trigger)) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "触发器基础参数无效"); + return false; + } + + // 1.2 检查 CRON 表达式是否存在 + if (StrUtil.isBlank(trigger.getCronExpression())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "定时触发器缺少 CRON 表达式"); + return false; + } + + // 1.3 定时触发器通常不依赖具体的设备消息 + // 它是通过定时任务调度器触发的,这里主要是验证配置的有效性 + if (!CronExpression.isValidExpression(trigger.getCronExpression())) { + IotSceneRuleMatcherHelper.logTriggerMatchFailure(message, trigger, "CRON 表达式格式无效: " + trigger.getCronExpression()); + return false; + } + + IotSceneRuleMatcherHelper.logTriggerMatchSuccess(message, trigger); + return true; + } + + @Override + public int getPriority() { + return 50; // 最低优先级,因为定时触发器不依赖消息 + } + +} 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 deleted file mode 100644 index ff5e28397a..0000000000 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/IotSceneRuleTriggerMatcherTest.java +++ /dev/null @@ -1,202 +0,0 @@ -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 { - - // TODO @puhui999:public 都加下哈; - - private IotSceneRuleMatcherManager matcherManager; - - @BeforeEach - void setUp() { - // 创建所有匹配器实例 - List matchers = Arrays.asList( - new DeviceStateUpdateTriggerMatcher(), - new DevicePropertyPostTriggerMatcher(), - new DeviceEventPostTriggerMatcher(), - new DeviceServiceInvokeTriggerMatcher(), - new TimerTriggerMatcher() - ); - - // 初始化匹配器管理器 - matcherManager = new IotSceneRuleMatcherManager(matchers); - } - - @Test - void testDeviceStateUpdateTriggerMatcher() { - // 1. 准备测试数据 - IotDeviceMessage message = IotDeviceMessage.builder() - .requestId("test-001") - .method("thing.state.update") // TODO @puhui999:这里的枚举; - .data(1) // 在线状态 TODO @puhui999:这里的枚举; - .build(); - - IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); - trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE.getType()); - trigger.setOperator("="); // TODO @puhui999:这里的枚举;下面也是类似; - 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)); - } - -} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java new file mode 100644 index 0000000000..88e948ea0f --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/CurrentTimeConditionMatcherTest.java @@ -0,0 +1,321 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +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.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link CurrentTimeConditionMatcher} 的单元测试类 + * + * @author HUIHUI + */ +public class CurrentTimeConditionMatcherTest extends BaseMockitoUnitTest { + + private CurrentTimeConditionMatcher matcher; + + @BeforeEach + public void setUp() { + matcher = new CurrentTimeConditionMatcher(); + } + + @Test + public void testGetSupportedConditionType() { + // when & then + assertEquals(IotSceneRuleConditionTypeEnum.CURRENT_TIME, matcher.getSupportedConditionType()); + } + + @Test + public void testGetPriority() { + // when & then + assertEquals(40, matcher.getPriority()); + } + + @Test + public void testIsEnabled() { + // when & then + assertTrue(matcher.isEnabled()); + } + + // ========== 时间戳条件测试 ========== + + @Test + public void testIsMatched_DateTimeGreaterThan_Success() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + long pastTimestamp = LocalDateTime.now().minusHours(1).toEpochSecond(ZoneOffset.of("+8")); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN.getOperator(), + String.valueOf(pastTimestamp) + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_DateTimeGreaterThan_Failure() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + long futureTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN.getOperator(), + String.valueOf(futureTimestamp) + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_DateTimeLessThan_Success() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + long futureTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_LESS_THAN.getOperator(), + String.valueOf(futureTimestamp) + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_DateTimeBetween_Success() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + long startTimestamp = LocalDateTime.now().minusHours(1).toEpochSecond(ZoneOffset.of("+8")); + long endTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN.getOperator(), + startTimestamp + "," + endTimestamp + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_DateTimeBetween_Failure() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + long startTimestamp = LocalDateTime.now().plusHours(1).toEpochSecond(ZoneOffset.of("+8")); + long endTimestamp = LocalDateTime.now().plusHours(2).toEpochSecond(ZoneOffset.of("+8")); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN.getOperator(), + startTimestamp + "," + endTimestamp + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + // ========== 当日时间条件测试 ========== + + @Test + public void testIsMatched_TimeGreaterThan_EarlyMorning() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN.getOperator(), + "06:00:00" // 早上6点 + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + // 结果取决于当前时间,如果当前时间大于6点则为true + assertNotNull(result); + } + + @Test + public void testIsMatched_TimeLessThan_LateNight() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_LESS_THAN.getOperator(), + "23:59:59" // 晚上11点59分59秒 + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + // 大部分情况下应该为true,除非在午夜前1秒运行测试 + assertNotNull(result); + } + + @Test + public void testIsMatched_TimeBetween_AllDay() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_BETWEEN.getOperator(), + "00:00:00,23:59:59" // 全天 + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); // 全天范围应该总是匹配 + } + + @Test + public void testIsMatched_TimeBetween_WorkingHours() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_BETWEEN.getOperator(), + "09:00:00,17:00:00" // 工作时间 + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + // 结果取决于当前时间是否在工作时间内 + assertNotNull(result); + } + + // ========== 异常情况测试 ========== + + @Test + public void testIsMatched_NullCondition() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + + // when + boolean result = matcher.isMatched(message, null); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_NullConditionType() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(null); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_InvalidOperator() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.CURRENT_TIME.getType()); + condition.setOperator("invalid_operator"); + condition.setParam("12:00:00"); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_InvalidTimeFormat() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN.getOperator(), + "invalid-time-format" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_InvalidTimestampFormat() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createDateTimeCondition( + IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN.getOperator(), + "invalid-timestamp" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_InvalidBetweenFormat() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.TriggerCondition condition = createTimeCondition( + IotSceneRuleConditionOperatorEnum.TIME_BETWEEN.getOperator(), + "09:00:00" // 缺少结束时间 + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建日期时间条件 + */ + private IotSceneRuleDO.TriggerCondition createDateTimeCondition(String operator, String param) { + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.CURRENT_TIME.getType()); + condition.setOperator(operator); + condition.setParam(param); + return condition; + } + + /** + * 创建当日时间条件 + */ + private IotSceneRuleDO.TriggerCondition createTimeCondition(String operator, String param) { + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.CURRENT_TIME.getType()); + condition.setOperator(operator); + condition.setParam(param); + return condition; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java new file mode 100644 index 0000000000..209893d1c8 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DevicePropertyConditionMatcherTest.java @@ -0,0 +1,391 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +import cn.hutool.core.map.MapUtil; +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.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DevicePropertyConditionMatcher} 的单元测试类 + * + * @author HUIHUI + */ +public class DevicePropertyConditionMatcherTest extends BaseMockitoUnitTest { + + private DevicePropertyConditionMatcher matcher; + + @BeforeEach + public void setUp() { + matcher = new DevicePropertyConditionMatcher(); + } + + @Test + public void testGetSupportedConditionType() { + // when & then + assertEquals(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY, matcher.getSupportedConditionType()); + } + + @Test + public void testGetPriority() { + // when & then + assertEquals(20, matcher.getPriority()); + } + + @Test + public void testIsEnabled() { + // when & then + assertTrue(matcher.isEnabled()); + } + + @Test + public void testIsMatched_Success_TemperatureEquals() { + // given + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "temperature", + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + "25.5" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_HumidityGreaterThan() { + // given + Map properties = MapUtil.of("humidity", 75); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "humidity", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "70" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_PressureLessThan() { + // given + Map properties = MapUtil.of("pressure", 1010.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "pressure", + IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), + "1020" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_StatusNotEquals() { + // given + Map properties = MapUtil.of("status", "active"); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "status", + IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), + "inactive" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_PropertyMismatch() { + // given + Map properties = MapUtil.of("temperature", 15.0); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "temperature", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "20" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_PropertyNotFound() { + // given + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "humidity", // 不存在的属性 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "50" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullCondition() { + // given + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + + // when + boolean result = matcher.isMatched(message, null); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullConditionType() { + // given + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(null); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingIdentifier() { + // given + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); + condition.setIdentifier(null); // 缺少标识符 + condition.setOperator(IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator()); + condition.setParam("20"); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingOperator() { + // given + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); + condition.setIdentifier("temperature"); + condition.setOperator(null); // 缺少操作符 + condition.setParam("20"); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingParam() { + // given + Map properties = MapUtil.of("temperature", 25.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); + condition.setIdentifier("temperature"); + condition.setOperator(IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator()); + condition.setParam(null); // 缺少参数 + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullMessage() { + // given + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "temperature", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "20" + ); + + // when + boolean result = matcher.isMatched(null, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullDeviceProperties() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + message.setParams(null); + + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "temperature", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "20" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Success_GreaterThanOrEquals() { + // given + Map properties = MapUtil.of("voltage", 12.0); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "voltage", + IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(), + "12.0" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_LessThanOrEquals() { + // given + Map properties = MapUtil.of("current", 2.5); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "current", + IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(), + "3.0" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_StringProperty() { + // given + Map properties = MapUtil.of("mode", "auto"); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "mode", + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + "auto" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_BooleanProperty() { + // given + Map properties = MapUtil.of("enabled", true); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "enabled", + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + "true" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_MultipleProperties() { + // given + Map properties = MapUtil.builder(new HashMap()) + .put("temperature", 25.5) + .put("humidity", 60) + .put("status", "active") + .put("enabled", true) + .build(); + IotDeviceMessage message = createDeviceMessage(properties); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + "humidity", + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + "60" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建设备消息 + */ + private IotDeviceMessage createDeviceMessage(Map properties) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setParams(properties); + return message; + } + + /** + * 创建有效的条件 + */ + private IotSceneRuleDO.TriggerCondition createValidCondition(String identifier, String operator, String param) { + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); + condition.setIdentifier(identifier); + condition.setOperator(operator); + condition.setParam(param); + return condition; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java new file mode 100644 index 0000000000..8eaf3c4af5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/DeviceStateConditionMatcherTest.java @@ -0,0 +1,334 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DeviceStateConditionMatcher} 的单元测试类 + * + * @author HUIHUI + */ +public class DeviceStateConditionMatcherTest extends BaseMockitoUnitTest { + + private DeviceStateConditionMatcher matcher; + + @BeforeEach + public void setUp() { + matcher = new DeviceStateConditionMatcher(); + } + + @Test + public void testGetSupportedConditionType() { + // when & then + assertEquals(IotSceneRuleConditionTypeEnum.DEVICE_STATE, matcher.getSupportedConditionType()); + } + + @Test + public void testGetPriority() { + // when & then + assertEquals(30, matcher.getPriority()); + } + + @Test + public void testIsEnabled() { + // when & then + assertTrue(matcher.isEnabled()); + } + + @Test + public void testIsMatched_Success_OnlineState() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_OfflineState() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.OFFLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.OFFLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_InactiveState() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.INACTIVE.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.INACTIVE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_StateMismatch() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.OFFLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Success_NotEqualsOperator() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), + IotDeviceStateEnum.OFFLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_GreaterThanOperator() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.OFFLINE.getState()); // 2 + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() // 1 + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_LessThanOperator() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.INACTIVE.getState()); // 0 + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() // 1 + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_NullCondition() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + + // when + boolean result = matcher.isMatched(message, null); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullConditionType() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(null); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingOperator() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType()); + condition.setOperator(null); + condition.setParam(IotDeviceStateEnum.ONLINE.getState().toString()); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingParam() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType()); + condition.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + condition.setParam(null); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullMessage() { + // given + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(null, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullDeviceState() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + message.setParams(null); + + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Success_GreaterThanOrEqualsOperator() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); // 1 + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() // 1 + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_LessThanOrEqualsOperator() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); // 1 + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.LESS_THAN_OR_EQUALS.getOperator(), + IotDeviceStateEnum.OFFLINE.getState().toString() // 2 + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_InvalidOperator() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType()); + condition.setOperator("invalid_operator"); + condition.setParam(IotDeviceStateEnum.ONLINE.getState().toString()); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_InvalidParamFormat() { + // given + IotDeviceMessage message = createDeviceMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.TriggerCondition condition = createValidCondition( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + "invalid_state_value" + ); + + // when + boolean result = matcher.isMatched(message, condition); + + // then + assertFalse(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建设备消息 + */ + private IotDeviceMessage createDeviceMessage(Integer deviceState) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setParams(deviceState); + return message; + } + + /** + * 创建有效的条件 + */ + private IotSceneRuleDO.TriggerCondition createValidCondition(String operator, String param) { + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType()); + condition.setOperator(operator); + condition.setParam(param); + return condition; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java new file mode 100644 index 0000000000..acba2332c1 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceEventPostTriggerMatcherTest.java @@ -0,0 +1,341 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DeviceEventPostTriggerMatcher} 的单元测试类 + * + * @author HUIHUI + */ +public class DeviceEventPostTriggerMatcherTest extends BaseMockitoUnitTest { + + private DeviceEventPostTriggerMatcher matcher; + + @BeforeEach + public void setUp() { + matcher = new DeviceEventPostTriggerMatcher(); + } + + @Test + public void testGetSupportedTriggerType() { + // when & then + assertEquals(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST, matcher.getSupportedTriggerType()); + } + + @Test + public void testGetPriority() { + // when & then + assertEquals(30, matcher.getPriority()); + } + + @Test + public void testIsEnabled() { + // when & then + assertTrue(matcher.isEnabled()); + } + + @Test + public void testIsMatched_Success_AlarmEvent() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "alarm") + .put("value", MapUtil.builder(new HashMap()) + .put("level", "high") + .put("message", "Temperature too high") + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("alarm"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_ErrorEvent() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "error") + .put("value", MapUtil.builder(new HashMap()) + .put("code", 500) + .put("description", "System error") + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("error"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_InfoEvent() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "info") + .put("value", MapUtil.builder(new HashMap()) + .put("status", "normal") + .put("timestamp", System.currentTimeMillis()) + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("info"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_EventIdentifierMismatch() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "alarm") + .put("value", MapUtil.builder(new HashMap()) + .put("level", "high") + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("error"); // 不匹配的事件标识符 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_WrongMessageMethod() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "alarm") + .put("value", MapUtil.builder(new HashMap()) + .put("level", "high") + .build()) + .build(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); // 错误的方法 + message.setParams(eventParams); + + IotSceneRuleDO.Trigger trigger = createValidTrigger("alarm"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingIdentifier() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "alarm") + .put("value", MapUtil.builder(new HashMap()) + .put("level", "high") + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST.getType()); + trigger.setIdentifier(null); // 缺少标识符 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullMessageParams() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod()); + message.setParams(null); + + IotSceneRuleDO.Trigger trigger = createValidTrigger("alarm"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_InvalidMessageParams() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod()); + message.setParams("invalid-params"); // 不是 Map 类型 + + IotSceneRuleDO.Trigger trigger = createValidTrigger("alarm"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingEventIdentifierInParams() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("value", MapUtil.builder(new HashMap()) + .put("level", "high") + .build()) // 缺少 identifier 字段 + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("alarm"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullTrigger() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "alarm") + .put("value", MapUtil.builder(new HashMap()) + .put("level", "high") + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + + // when + boolean result = matcher.isMatched(message, null); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullTriggerType() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "alarm") + .put("value", MapUtil.builder(new HashMap()) + .put("level", "high") + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(null); + trigger.setIdentifier("alarm"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Success_ComplexEventValue() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "maintenance") + .put("value", MapUtil.builder(new HashMap()) + .put("type", "scheduled") + .put("duration", 120) + .put("components", new String[]{"motor", "sensor"}) + .put("priority", "medium") + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("maintenance"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_EmptyEventValue() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "heartbeat") + .put("value", MapUtil.of()) // 空的事件值 + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("heartbeat"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_CaseInsensitiveIdentifier() { + // given + Map eventParams = MapUtil.builder(new HashMap()) + .put("identifier", "ALARM") // 大写 + .put("value", MapUtil.builder(new HashMap()) + .put("level", "high") + .build()) + .build(); + IotDeviceMessage message = createEventPostMessage(eventParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("alarm"); // 小写 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + // 根据实际实现,这里可能需要调整期望结果 + // 如果实现是大小写敏感的,则应该为 false + assertFalse(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建事件上报消息 + */ + private IotDeviceMessage createEventPostMessage(Map eventParams) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.EVENT_POST.getMethod()); + message.setParams(eventParams); + return message; + } + + /** + * 创建有效的触发器 + */ + private IotSceneRuleDO.Trigger createValidTrigger(String identifier) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_EVENT_POST.getType()); + trigger.setIdentifier(identifier); + return trigger; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java new file mode 100644 index 0000000000..0744c9a272 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DevicePropertyPostTriggerMatcherTest.java @@ -0,0 +1,298 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DevicePropertyPostTriggerMatcher} 的单元测试类 + * + * @author HUIHUI + */ +public class DevicePropertyPostTriggerMatcherTest extends BaseMockitoUnitTest { + + private DevicePropertyPostTriggerMatcher matcher; + + @BeforeEach + public void setUp() { + matcher = new DevicePropertyPostTriggerMatcher(); + } + + @Test + public void testGetSupportedTriggerType() { + // when & then + assertEquals(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST, matcher.getSupportedTriggerType()); + } + + @Test + public void testGetPriority() { + // when & then + assertEquals(20, matcher.getPriority()); + } + + @Test + public void testIsEnabled() { + // when & then + assertTrue(matcher.isEnabled()); + } + + @Test + public void testIsMatched_Success_TemperatureProperty() { + // given + Map properties = MapUtil.builder(new HashMap()) + .put("temperature", 25.5) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + "temperature", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "20" + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_HumidityProperty() { + // given + Map properties = MapUtil.builder(new HashMap()) + .put("humidity", 60) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + "humidity", + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + "60" + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_PropertyMismatch() { + // given + Map properties = MapUtil.builder(new HashMap()) + .put("temperature", 15.0) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + "temperature", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "20" + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_PropertyNotFound() { + // given + Map properties = MapUtil.builder(new HashMap()) + .put("temperature", 25.5) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + "humidity", // 不存在的属性 + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "50" + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_WrongMessageMethod() { + // given + Map properties = MapUtil.builder(new HashMap()) + .put("temperature", 25.5) + .build(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); + message.setParams(properties); + + IotSceneRuleDO.Trigger trigger = createValidTrigger( + "temperature", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "20" + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingIdentifier() { + // given + Map properties = MapUtil.builder(new HashMap()) + .put("temperature", 25.5) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType()); + trigger.setIdentifier(null); // 缺少标识符 + trigger.setOperator(IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator()); + trigger.setValue("20"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullMessageParams() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); + message.setParams(null); + + IotSceneRuleDO.Trigger trigger = createValidTrigger( + "temperature", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "20" + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_InvalidMessageParams() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); + message.setParams("invalid-params"); // 不是 Map 类型 + + IotSceneRuleDO.Trigger trigger = createValidTrigger( + "temperature", + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + "20" + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Success_LessThanOperator() { + // given + Map properties = MapUtil.builder(new HashMap()) + .put("temperature", 15.0) + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + "temperature", + IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), + "20" + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_NotEqualsOperator() { + // given + Map properties = MapUtil.builder(new HashMap()) + .put("status", "active") + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + "status", + IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), + "inactive" + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_MultipleProperties() { + // given + Map properties = MapUtil.builder(new HashMap()) + .put("temperature", 25.5) + .put("humidity", 60) + .put("status", "active") + .build(); + IotDeviceMessage message = createPropertyPostMessage(properties); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + "humidity", + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + "60" + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建属性上报消息 + */ + private IotDeviceMessage createPropertyPostMessage(Map properties) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); + message.setParams(properties); + return message; + } + + /** + * 创建有效的触发器 + */ + private IotSceneRuleDO.Trigger createValidTrigger(String identifier, String operator, String value) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType()); + trigger.setIdentifier(identifier); + trigger.setOperator(operator); + trigger.setValue(value); + return trigger; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java new file mode 100644 index 0000000000..6aef51cf71 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceServiceInvokeTriggerMatcherTest.java @@ -0,0 +1,363 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.hutool.core.map.MapUtil; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DeviceServiceInvokeTriggerMatcher} 的单元测试类 + * + * @author HUIHUI + */ +public class DeviceServiceInvokeTriggerMatcherTest extends BaseMockitoUnitTest { + + private DeviceServiceInvokeTriggerMatcher matcher; + + @BeforeEach + public void setUp() { + matcher = new DeviceServiceInvokeTriggerMatcher(); + } + + @Test + public void testGetSupportedTriggerType() { + // when & then + // TODO @puhui999:单测按照现有项目的注释风格哈;类似 // 调用;// 断言 + assertEquals(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE, matcher.getSupportedTriggerType()); + } + + @Test + public void testGetPriority() { + // when & then + assertEquals(40, matcher.getPriority()); + } + + @Test + public void testIsEnabled() { + // when & then + assertTrue(matcher.isEnabled()); + } + + @Test + public void testIsMatched_Success_RestartService() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "restart") + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", "soft") + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("restart"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_ConfigService() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "config") + .put("inputData", MapUtil.builder(new HashMap()) + .put("interval", 30) + .put("enabled", true) + .put("threshold", 75.5) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("config"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_UpdateService() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "update") + .put("inputData", MapUtil.builder(new HashMap()) + .put("version", "1.2.3") + .put("url", "http://example.com/firmware.bin") + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("update"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_ServiceIdentifierMismatch() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "restart") + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", "soft") + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("config"); // 不匹配的服务标识符 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_WrongMessageMethod() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "restart") + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", "soft") + .build()) + .build(); + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); // 错误的方法 + message.setParams(serviceParams); + + IotSceneRuleDO.Trigger trigger = createValidTrigger("restart"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingIdentifier() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "restart") + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", "soft") + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType()); + trigger.setIdentifier(null); // 缺少标识符 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullMessageParams() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod()); + message.setParams(null); + + IotSceneRuleDO.Trigger trigger = createValidTrigger("restart"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_InvalidMessageParams() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod()); + message.setParams("invalid-params"); // 不是 Map 类型 + + IotSceneRuleDO.Trigger trigger = createValidTrigger("restart"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingServiceIdentifierInParams() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", "soft") + .build()) // 缺少 identifier 字段 + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("restart"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullTrigger() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "restart") + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", "soft") + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + + // when + boolean result = matcher.isMatched(message, null); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullTriggerType() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "restart") + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", "soft") + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(null); + trigger.setIdentifier("restart"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Success_EmptyInputData() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "ping") + .put("inputData", MapUtil.of()) // 空的输入数据 + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("ping"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_NoInputData() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "status") + // 没有 inputData 字段 + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("status"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_ComplexInputData() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "calibrate") + .put("inputData", MapUtil.builder(new HashMap()) + .put("sensors", new String[]{"temperature", "humidity", "pressure"}) + .put("precision", 0.01) + .put("duration", 300) + .put("autoSave", true) + .put("config", MapUtil.builder(new HashMap()) + .put("mode", "auto") + .put("level", "high") + .build()) + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("calibrate"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_CaseInsensitiveIdentifier() { + // given + Map serviceParams = MapUtil.builder(new HashMap()) + .put("identifier", "RESTART") // 大写 + .put("inputData", MapUtil.builder(new HashMap()) + .put("mode", "soft") + .build()) + .build(); + IotDeviceMessage message = createServiceInvokeMessage(serviceParams); + IotSceneRuleDO.Trigger trigger = createValidTrigger("restart"); // 小写 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + // 根据实际实现,这里可能需要调整期望结果 + // 如果实现是大小写敏感的,则应该为 false + assertFalse(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建服务调用消息 + */ + private IotDeviceMessage createServiceInvokeMessage(Map serviceParams) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod()); + message.setParams(serviceParams); + return message; + } + + /** + * 创建有效的触发器 + */ + private IotSceneRuleDO.Trigger createValidTrigger(String identifier) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_SERVICE_INVOKE.getType()); + trigger.setIdentifier(identifier); + return trigger; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java new file mode 100644 index 0000000000..2f101b2b08 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/DeviceStateUpdateTriggerMatcherTest.java @@ -0,0 +1,245 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DeviceStateUpdateTriggerMatcher} 的单元测试类 + * + * @author HUIHUI + */ +public class DeviceStateUpdateTriggerMatcherTest extends BaseMockitoUnitTest { + + private DeviceStateUpdateTriggerMatcher matcher; + + @BeforeEach + public void setUp() { + matcher = new DeviceStateUpdateTriggerMatcher(); + } + + @Test + public void testGetSupportedTriggerType() { + // when & then + assertEquals(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE, matcher.getSupportedTriggerType()); + } + + @Test + public void testGetPriority() { + // when & then + assertEquals(10, matcher.getPriority()); + } + + @Test + public void testIsEnabled() { + // when & then + assertTrue(matcher.isEnabled()); + } + + @Test + public void testIsMatched_Success_OnlineState() { + // given + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_OfflineState() { + // given + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.OFFLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.OFFLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_StateMismatch() { + // given + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.OFFLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullTrigger() { + // given + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + + // when + boolean result = matcher.isMatched(message, null); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullTriggerType() { + // given + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(null); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_WrongMessageMethod() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); + message.setParams(IotDeviceStateEnum.ONLINE.getState()); + + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingOperator() { + // given + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE.getType()); + trigger.setOperator(null); + trigger.setValue(IotDeviceStateEnum.ONLINE.getState().toString()); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_MissingValue() { + // given + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE.getType()); + trigger.setOperator(IotSceneRuleConditionOperatorEnum.EQUALS.getOperator()); + trigger.setValue(null); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullMessageParams() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); + message.setParams(null); + + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + IotDeviceStateEnum.ONLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Success_GreaterThanOperator() { + // given + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), + IotDeviceStateEnum.INACTIVE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_NotEqualsOperator() { + // given + IotDeviceMessage message = createStateUpdateMessage(IotDeviceStateEnum.ONLINE.getState()); + IotSceneRuleDO.Trigger trigger = createValidTrigger( + IotSceneRuleConditionOperatorEnum.NOT_EQUALS.getOperator(), + IotDeviceStateEnum.OFFLINE.getState().toString() + ); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建设备状态更新消息 + */ + private IotDeviceMessage createStateUpdateMessage(Integer state) { + IotDeviceMessage message = new IotDeviceMessage(); + message.setMethod(IotDeviceMessageMethodEnum.STATE_UPDATE.getMethod()); + message.setParams(state); + return message; + } + + /** + * 创建有效的触发器 + */ + private IotSceneRuleDO.Trigger createValidTrigger(String operator, String value) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_STATE_UPDATE.getType()); + trigger.setOperator(operator); + trigger.setValue(value); + return trigger; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java new file mode 100644 index 0000000000..13fe587e14 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/trigger/TimerTriggerMatcherTest.java @@ -0,0 +1,240 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger; + +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 static org.junit.jupiter.api.Assertions.*; + +/** + * {@link TimerTriggerMatcher} 的单元测试类 + * + * @author HUIHUI + */ +public class TimerTriggerMatcherTest extends BaseMockitoUnitTest { + + private TimerTriggerMatcher matcher; + + @BeforeEach + public void setUp() { + matcher = new TimerTriggerMatcher(); + } + + @Test + public void testGetSupportedTriggerType() { + // when & then + assertEquals(IotSceneRuleTriggerTypeEnum.TIMER, matcher.getSupportedTriggerType()); + } + + @Test + public void testGetPriority() { + // when & then + assertEquals(50, matcher.getPriority()); + } + + @Test + public void testIsEnabled() { + // when & then + assertTrue(matcher.isEnabled()); + } + + @Test + public void testIsMatched_Success_ValidCronExpression() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 12 * * ?"); // 每天中午12点 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_EveryMinuteCron() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 * * * * ?"); // 每分钟 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_WeekdaysCron() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 9 ? * MON-FRI"); // 工作日上午9点 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_InvalidCronExpression() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("invalid-cron-expression"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_EmptyCronExpression() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger(""); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullCronExpression() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); + trigger.setCronExpression(null); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullTrigger() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + + // when + boolean result = matcher.isMatched(message, null); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Failure_NullTriggerType() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(null); + trigger.setCronExpression("0 0 12 * * ?"); + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Success_ComplexCronExpression() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 15 10 ? * 6#3"); // 每月第三个星期五上午10:15 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_IncorrectCronFormat() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 12 * *"); // 缺少字段 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Success_SpecificDateCron() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 0 1 1 ? 2025"); // 2025年1月1日午夜 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Success_EverySecondCron() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("* * * * * ?"); // 每秒 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + @Test + public void testIsMatched_Failure_InvalidCharactersCron() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 12 * * @ #"); // 包含无效字符 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertFalse(result); + } + + @Test + public void testIsMatched_Success_RangeCron() { + // given + IotDeviceMessage message = new IotDeviceMessage(); + IotSceneRuleDO.Trigger trigger = createValidTrigger("0 0 9-17 * * MON-FRI"); // 工作日9-17点 + + // when + boolean result = matcher.isMatched(message, trigger); + + // then + assertTrue(result); + } + + // ========== 辅助方法 ========== + + /** + * 创建有效的定时触发器 + */ + private IotSceneRuleDO.Trigger createValidTrigger(String cronExpression) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); + trigger.setCronExpression(cronExpression); + return trigger; + } +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/resources/application-unit-test.yaml b/yudao-module-iot/yudao-module-iot-biz/src/test/resources/application-unit-test.yaml index 7eecc88a4b..3966a274d4 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/resources/application-unit-test.yaml +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/resources/application-unit-test.yaml @@ -20,6 +20,15 @@ mybatis-plus: lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject +# 日志配置 +logging: + level: + cn.iocoder.yudao.module.iot.service.rule.scene.matcher: DEBUG + cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager: INFO + cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition: DEBUG + cn.iocoder.yudao.module.iot.service.rule.scene.matcher.trigger: DEBUG + root: WARN + --- #################### 定时任务相关配置 #################### --- #################### 配置中心相关配置 #################### diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/resources/logback.xml b/yudao-module-iot/yudao-module-iot-biz/src/test/resources/logback.xml index 1d071e4799..b68931dc1c 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/resources/logback.xml +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/resources/logback.xml @@ -1,4 +1,37 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + UTF-8 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index b12c0bbd04..6d97229e9b 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -185,6 +185,7 @@ logging: cn.iocoder.yudao.module.erp.dal.mysql: debug cn.iocoder.yudao.module.iot.dal.mysql: debug cn.iocoder.yudao.module.iot.dal.tdengine: DEBUG + cn.iocoder.yudao.module.iot.service.rule: debug cn.iocoder.yudao.module.ai.dal.mysql: debug org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示