From 4901912ece006720db02fc828bef4f0a810967e2 Mon Sep 17 00:00:00 2001 From: puhui999 Date: Sun, 25 Jan 2026 17:30:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(iot):=E3=80=90=E5=9C=BA=E6=99=AF=E8=81=94?= =?UTF-8?q?=E5=8A=A8=E3=80=91=E5=AE=9A=E6=97=B6=E8=A7=A6=E5=8F=91=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=9D=A1=E4=BB=B6=E7=BB=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rule/scene/IotSceneRuleServiceImpl.java | 89 ++- .../rule/scene/IotSceneRuleTimeHelper.java | 213 ++++++ .../IotCurrentTimeConditionMatcher.java | 162 +---- .../timer/IotTimerConditionEvaluator.java | 189 ++++++ ...ceneRuleTimerConditionIntegrationTest.java | 611 ++++++++++++++++++ 5 files changed, 1102 insertions(+), 162 deletions(-) create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotTimerConditionEvaluator.java create mode 100644 yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java 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 f96bc9f450..4ea7338e33 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 @@ -23,6 +23,7 @@ import cn.iocoder.yudao.module.iot.service.product.IotProductService; import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager; import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler; +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; @@ -62,6 +63,8 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { private List sceneRuleActions; @Resource private IotSceneRuleTimerHandler timerHandler; + @Resource + private IotTimerConditionEvaluator timerConditionEvaluator; @Override @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) @@ -222,18 +225,98 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { return; } // 1.2 判断是否有定时触发器,避免脏数据 - IotSceneRuleDO.Trigger config = CollUtil.findOne(scene.getTriggers(), + IotSceneRuleDO.Trigger timerTrigger = CollUtil.findOne(scene.getTriggers(), trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType())); - if (config == null) { + if (timerTrigger == null) { log.error("[executeSceneRuleByTimer][规则场景({}) 不存在定时触发器]", scene); return; } - // 2. 执行规则场景 + // 2. 评估条件组(新增逻辑) + log.info("[executeSceneRuleByTimer][规则场景({}) 开始评估条件组]", id); + if (!evaluateTimerConditionGroups(scene, timerTrigger)) { + log.info("[executeSceneRuleByTimer][规则场景({}) 条件组不满足,跳过执行]", id); + return; + } + log.info("[executeSceneRuleByTimer][规则场景({}) 条件组评估通过,准备执行动作]", id); + + // 3. 执行规则场景 TenantUtils.execute(scene.getTenantId(), () -> executeSceneRuleAction(null, ListUtil.toList(scene))); } + /** + * 评估定时触发器的条件组 + * + * @param scene 场景规则 + * @param trigger 定时触发器 + * @return 是否满足条件 + */ + private boolean evaluateTimerConditionGroups(IotSceneRuleDO scene, IotSceneRuleDO.Trigger trigger) { + // 1. 如果没有条件组,直接返回 true(直接执行动作) + if (CollUtil.isEmpty(trigger.getConditionGroups())) { + log.debug("[evaluateTimerConditionGroups][规则场景({}) 无条件组配置,直接执行]", scene.getId()); + return true; + } + + // 2. 条件组之间是 OR 关系,任一条件组满足即可 + for (List conditionGroup : trigger.getConditionGroups()) { + if (evaluateSingleConditionGroup(scene, conditionGroup)) { + log.debug("[evaluateTimerConditionGroups][规则场景({}) 条件组匹配成功]", scene.getId()); + return true; + } + } + + // 3. 所有条件组都不满足 + log.debug("[evaluateTimerConditionGroups][规则场景({}) 所有条件组都不满足]", scene.getId()); + return false; + } + + /** + * 评估单个条件组 + * + * @param scene 场景规则 + * @param conditionGroup 条件组 + * @return 是否满足条件 + */ + private boolean evaluateSingleConditionGroup(IotSceneRuleDO scene, + List conditionGroup) { + // 1. 空条件组视为满足 + if (CollUtil.isEmpty(conditionGroup)) { + return true; + } + + // 2. 条件之间是 AND 关系,所有条件都必须满足 + for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) { + if (!evaluateTimerCondition(scene, condition)) { + log.debug("[evaluateSingleConditionGroup][规则场景({}) 条件({}) 不满足]", + scene.getId(), condition); + return false; + } + } + + return true; + } + + /** + * 评估单个条件(定时触发器专用) + * + * @param scene 场景规则 + * @param condition 条件 + * @return 是否满足条件 + */ + private boolean evaluateTimerCondition(IotSceneRuleDO scene, IotSceneRuleDO.TriggerCondition condition) { + try { + boolean result = timerConditionEvaluator.evaluate(condition); + log.debug("[evaluateTimerCondition][规则场景({}) 条件类型({}) 评估结果: {}]", + scene.getId(), condition.getType(), result); + return result; + } catch (Exception e) { + log.error("[evaluateTimerCondition][规则场景({}) 条件评估异常]", scene.getId(), e); + return false; + } + } + /** * 基于消息,获得匹配的规则场景列表 * diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java new file mode 100644 index 0000000000..8d1c1f6292 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java @@ -0,0 +1,213 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.CharPool; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.List; + +/** + * IoT 场景规则时间匹配工具类 + *

+ * 提供时间条件匹配的通用方法,供 {@link cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotCurrentTimeConditionMatcher} + * 和 {@link cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator} 共同使用。 + * + * @author HUIHUI + */ +@Slf4j +public class IotSceneRuleTimeHelper { + + /** + * 时间格式化器 - 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"); + + private IotSceneRuleTimeHelper() { + // 工具类,禁止实例化 + } + + /** + * 判断是否为日期时间操作符 + * + * @param operatorEnum 操作符枚举 + * @return 是否为日期时间操作符 + */ + public static boolean isDateTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) { + return operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN + || operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_LESS_THAN + || operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN; + } + + /** + * 判断是否为时间操作符(包括日期时间操作符和当日时间操作符) + * + * @param operatorEnum 操作符枚举 + * @return 是否为时间操作符 + */ + public static boolean isTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) { + return operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN + && operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_LESS_THAN + && operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_BETWEEN + && !isDateTimeOperator(operatorEnum); + } + + /** + * 执行时间匹配逻辑 + * + * @param operatorEnum 操作符枚举 + * @param param 参数值 + * @return 是否匹配 + */ + public static boolean executeTimeMatching(IotSceneRuleConditionOperatorEnum operatorEnum, String param) { + try { + LocalDateTime now = LocalDateTime.now(); + if (isDateTimeOperator(operatorEnum)) { + // 日期时间匹配(时间戳,秒级) + long currentTimestamp = now.atZone(ZoneId.systemDefault()).toEpochSecond(); + 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; + } + } + + /** + * 匹配日期时间(时间戳,秒级) + * + * @param currentTimestamp 当前时间戳 + * @param operatorEnum 操作符枚举 + * @param param 参数值 + * @return 是否匹配 + */ + public static boolean matchDateTime(long currentTimestamp, IotSceneRuleConditionOperatorEnum operatorEnum, + String param) { + try { + // DATE_TIME_BETWEEN 需要解析两个时间戳,单独处理 + if (operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN) { + return matchDateTimeBetween(currentTimestamp, param); + } + // 其他操作符只需要解析一个时间戳 + long targetTimestamp = Long.parseLong(param); + switch (operatorEnum) { + case DATE_TIME_GREATER_THAN: + return currentTimestamp > targetTimestamp; + case DATE_TIME_LESS_THAN: + return currentTimestamp < targetTimestamp; + default: + log.warn("[matchDateTime][operatorEnum({}) 不支持的日期时间操作符]", operatorEnum); + return false; + } + } catch (Exception e) { + log.error("[matchDateTime][operatorEnum({}) param({}) 日期时间匹配异常]", operatorEnum, param, e); + return false; + } + } + + /** + * 匹配日期时间区间 + * + * @param currentTimestamp 当前时间戳 + * @param param 参数值(格式:startTimestamp,endTimestamp) + * @return 是否匹配 + */ + public static 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 或 HH:mm) + * + * @param currentTime 当前时间 + * @param operatorEnum 操作符枚举 + * @param param 参数值 + * @return 是否匹配 + */ + public static boolean matchTime(LocalTime currentTime, IotSceneRuleConditionOperatorEnum operatorEnum, + String param) { + try { + // TIME_BETWEEN 需要解析两个时间,单独处理 + if (operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_BETWEEN) { + return matchTimeBetween(currentTime, param); + } + // 其他操作符只需要解析一个时间 + LocalTime targetTime = parseTime(param); + switch (operatorEnum) { + case TIME_GREATER_THAN: + return currentTime.isAfter(targetTime); + case TIME_LESS_THAN: + return currentTime.isBefore(targetTime); + default: + log.warn("[matchTime][operatorEnum({}) 不支持的时间操作符]", operatorEnum); + return false; + } + } catch (Exception e) { + log.error("[matchTime][operatorEnum({}) param({}) 时间解析异常]", operatorEnum, param, e); + return false; + } + } + + /** + * 匹配时间区间 + * + * @param currentTime 当前时间 + * @param param 参数值(格式:startTime,endTime) + * @return 是否匹配 + */ + public static 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 两种格式 + * + * @param timeStr 时间字符串 + * @return 解析后的 LocalTime + */ + public static 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); + } + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java index 2083bebac9..a54785ad69 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java @@ -1,21 +1,14 @@ 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.IotSceneRuleTimeHelper; 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; - /** * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑 * @@ -25,16 +18,6 @@ import java.util.List; @Slf4j public class IotCurrentTimeConditionMatcher 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; @@ -62,13 +45,13 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc return false; } - if (!isTimeOperator(operatorEnum)) { + if (IotSceneRuleTimeHelper.isTimeOperator(operatorEnum)) { IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "不支持的时间操作符: " + operator); return false; } // 2.1 执行时间匹配 - boolean matched = executeTimeMatching(operatorEnum, condition.getParam()); + boolean matched = IotSceneRuleTimeHelper.executeTimeMatching(operatorEnum, condition.getParam()); // 2.2 记录匹配结果 if (matched) { @@ -80,145 +63,6 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc 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); - } - - /** - * 匹配日期时间(时间戳) - * 直接实现时间戳比较逻辑 - */ - private boolean matchDateTime(long currentTimestamp, IotSceneRuleConditionOperatorEnum operatorEnum, String param) { - try { - long targetTimestamp = Long.parseLong(param); - switch (operatorEnum) { - case DATE_TIME_GREATER_THAN: - return currentTimestamp > targetTimestamp; - case DATE_TIME_LESS_THAN: - return currentTimestamp < targetTimestamp; - case DATE_TIME_BETWEEN: - return matchDateTimeBetween(currentTimestamp, param); - default: - log.warn("[matchDateTime][operatorEnum({}) 不支持的日期时间操作符]", operatorEnum); - return 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); - switch (operatorEnum) { - case TIME_GREATER_THAN: - return currentTime.isAfter(targetTime); - case TIME_LESS_THAN: - return currentTime.isBefore(targetTime); - case TIME_BETWEEN: - return matchTimeBetween(currentTime, param); - default: - log.warn("[matchTime][operatorEnum({}) 不支持的时间操作符]", operatorEnum); - return 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/timer/IotTimerConditionEvaluator.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotTimerConditionEvaluator.java new file mode 100644 index 0000000000..d8fe8183bf --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotTimerConditionEvaluator.java @@ -0,0 +1,189 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene.timer; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +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.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleTimeHelper; +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * IoT 定时触发器条件评估器 + *

+ * 与设备触发器不同,定时触发器没有设备消息上下文, + * 需要主动查询设备属性和状态来评估条件。 + * + * @author HUIHUI + */ +@Component +@Slf4j +public class IotTimerConditionEvaluator { + + @Resource + private IotDevicePropertyService devicePropertyService; + + @Resource + private IotDeviceService deviceService; + + /** + * 评估条件 + * + * @param condition 条件配置 + * @return 是否满足条件 + */ + public boolean evaluate(IotSceneRuleDO.TriggerCondition condition) { + // 1. 基础参数校验 + if (condition == null || condition.getType() == null) { + log.warn("[evaluate][条件为空或类型为空]"); + return false; + } + + // 2. 根据条件类型分发到具体的评估方法 + IotSceneRuleConditionTypeEnum conditionType = + IotSceneRuleConditionTypeEnum.typeOf(condition.getType()); + if (conditionType == null) { + log.warn("[evaluate][未知的条件类型: {}]", condition.getType()); + return false; + } + + switch (conditionType) { + case DEVICE_PROPERTY: + return evaluateDevicePropertyCondition(condition); + case DEVICE_STATE: + return evaluateDeviceStateCondition(condition); + case CURRENT_TIME: + return evaluateCurrentTimeCondition(condition); + default: + log.warn("[evaluate][未知的条件类型: {}]", conditionType); + return false; + } + } + + /** + * 评估设备属性条件 + * + * @param condition 条件配置 + * @return 是否满足条件 + */ + private boolean evaluateDevicePropertyCondition(IotSceneRuleDO.TriggerCondition condition) { + // 1. 校验必要参数 + if (condition.getDeviceId() == null) { + log.debug("[evaluateDevicePropertyCondition][设备ID为空]"); + return false; + } + if (StrUtil.isBlank(condition.getIdentifier())) { + log.debug("[evaluateDevicePropertyCondition][属性标识符为空]"); + return false; + } + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { + log.debug("[evaluateDevicePropertyCondition][操作符或参数无效]"); + return false; + } + + // 2. 获取设备最新属性值 + Map properties = + devicePropertyService.getLatestDeviceProperties(condition.getDeviceId()); + if (properties == null || properties.isEmpty()) { + log.debug("[evaluateDevicePropertyCondition][设备({}) 无属性数据]", condition.getDeviceId()); + return false; + } + + // 3. 获取指定属性 + IotDevicePropertyDO property = properties.get(condition.getIdentifier()); + if (property == null || property.getValue() == null) { + log.debug("[evaluateDevicePropertyCondition][设备({}) 属性({}) 不存在或值为空]", + condition.getDeviceId(), condition.getIdentifier()); + return false; + } + + // 4. 使用现有的条件评估逻辑进行比较 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition( + property.getValue(), condition.getOperator(), condition.getParam()); + log.debug("[evaluateDevicePropertyCondition][设备({}) 属性({}) 值({}) 操作符({}) 参数({}) 匹配结果: {}]", + condition.getDeviceId(), condition.getIdentifier(), property.getValue(), + condition.getOperator(), condition.getParam(), matched); + return matched; + } + + /** + * 评估设备状态条件 + * + * @param condition 条件配置 + * @return 是否满足条件 + */ + private boolean evaluateDeviceStateCondition(IotSceneRuleDO.TriggerCondition condition) { + // 1. 校验必要参数 + if (condition.getDeviceId() == null) { + log.debug("[evaluateDeviceStateCondition][设备ID为空]"); + return false; + } + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { + log.debug("[evaluateDeviceStateCondition][操作符或参数无效]"); + return false; + } + + // 2. 获取设备信息 + IotDeviceDO device = deviceService.getDevice(condition.getDeviceId()); + if (device == null) { + log.debug("[evaluateDeviceStateCondition][设备({}) 不存在]", condition.getDeviceId()); + return false; + } + + // 3. 获取设备状态 + Integer state = device.getState(); + if (state == null) { + log.debug("[evaluateDeviceStateCondition][设备({}) 状态为空]", condition.getDeviceId()); + return false; + } + + // 4. 比较状态 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition( + state.toString(), condition.getOperator(), condition.getParam()); + log.debug("[evaluateDeviceStateCondition][设备({}) 状态({}) 操作符({}) 参数({}) 匹配结果: {}]", + condition.getDeviceId(), state, condition.getOperator(), condition.getParam(), matched); + return matched; + } + + /** + * 评估当前时间条件 + * + * @param condition 条件配置 + * @return 是否满足条件 + */ + private boolean evaluateCurrentTimeCondition(IotSceneRuleDO.TriggerCondition condition) { + // 1. 校验必要参数 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) { + log.debug("[evaluateCurrentTimeCondition][操作符或参数无效]"); + return false; + } + + // 2. 验证操作符是否为支持的时间操作符 + IotSceneRuleConditionOperatorEnum operatorEnum = + IotSceneRuleConditionOperatorEnum.operatorOf(condition.getOperator()); + if (operatorEnum == null) { + log.debug("[evaluateCurrentTimeCondition][无效的操作符: {}]", condition.getOperator()); + return false; + } + + if (IotSceneRuleTimeHelper.isTimeOperator(operatorEnum)) { + log.debug("[evaluateCurrentTimeCondition][不支持的时间操作符: {}]", condition.getOperator()); + return false; + } + + // 3. 执行时间匹配 + boolean matched = IotSceneRuleTimeHelper.executeTimeMatching(operatorEnum, condition.getParam()); + log.debug("[evaluateCurrentTimeCondition][操作符({}) 参数({}) 匹配结果: {}]", + condition.getOperator(), condition.getParam(), matched); + return matched; + } + +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java new file mode 100644 index 0000000000..75319b9c21 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java @@ -0,0 +1,611 @@ +package cn.iocoder.yudao.module.iot.service.rule.scene; + +import cn.hutool.core.collection.ListUtil; +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO; +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.IotSceneRuleConditionTypeEnum; +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum; +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; +import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler; +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * {@link IotSceneRuleServiceImpl} 定时触发器条件组集成测试 + *

+ * 测试定时触发器的条件组评估功能: + * - 空条件组直接执行动作 + * - 条件组评估后决定是否执行动作 + * - 条件组之间的 OR 逻辑 + * - 条件组内的 AND 逻辑 + * - 所有条件组不满足时跳过执行 + *

+ * Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5 + * + * @author HUIHUI + */ +public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTest { + + @InjectMocks + private IotSceneRuleServiceImpl sceneRuleService; + + @Mock + private IotSceneRuleMapper sceneRuleMapper; + + @Mock + private IotDeviceService deviceService; + + @Mock + private IotDevicePropertyService devicePropertyService; + + @Mock + private List sceneRuleActions; + + @Mock + private IotSceneRuleTimerHandler timerHandler; + + private IotTimerConditionEvaluator timerConditionEvaluator; + + // 测试常量 + private static final Long SCENE_RULE_ID = 1L; + private static final Long TENANT_ID = 1L; + private static final Long DEVICE_ID = 100L; + private static final String PROPERTY_IDENTIFIER = "temperature"; + + @BeforeEach + void setUp() { + // 创建并注入 timerConditionEvaluator 的依赖 + timerConditionEvaluator = new IotTimerConditionEvaluator(); + try { + var devicePropertyServiceField = IotTimerConditionEvaluator.class.getDeclaredField("devicePropertyService"); + devicePropertyServiceField.setAccessible(true); + devicePropertyServiceField.set(timerConditionEvaluator, devicePropertyService); + + var deviceServiceField = IotTimerConditionEvaluator.class.getDeclaredField("deviceService"); + deviceServiceField.setAccessible(true); + deviceServiceField.set(timerConditionEvaluator, deviceService); + + var evaluatorField = IotSceneRuleServiceImpl.class.getDeclaredField("timerConditionEvaluator"); + evaluatorField.setAccessible(true); + evaluatorField.set(sceneRuleService, timerConditionEvaluator); + } catch (Exception e) { + throw new RuntimeException("Failed to inject dependencies", e); + } + } + + // ========== 辅助方法 ========== + + private IotSceneRuleDO createBaseSceneRule() { + IotSceneRuleDO sceneRule = new IotSceneRuleDO(); + sceneRule.setId(SCENE_RULE_ID); + sceneRule.setTenantId(TENANT_ID); + sceneRule.setName("测试定时触发器"); + sceneRule.setStatus(CommonStatusEnum.ENABLE.getStatus()); + sceneRule.setActions(Collections.emptyList()); + return sceneRule; + } + + private IotSceneRuleDO.Trigger createTimerTrigger(String cronExpression, + List> conditionGroups) { + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger(); + trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType()); + trigger.setCronExpression(cronExpression); + trigger.setConditionGroups(conditionGroups); + return trigger; + } + + private IotSceneRuleDO.TriggerCondition createDevicePropertyCondition(Long deviceId, String identifier, + String operator, String param) { + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType()); + condition.setDeviceId(deviceId); + condition.setIdentifier(identifier); + condition.setOperator(operator); + condition.setParam(param); + return condition; + } + + private IotSceneRuleDO.TriggerCondition createDeviceStateCondition(Long deviceId, String operator, String param) { + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition(); + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType()); + condition.setDeviceId(deviceId); + condition.setOperator(operator); + condition.setParam(param); + return condition; + } + + private void mockDeviceProperty(Long deviceId, String identifier, Object value) { + Map properties = new HashMap<>(); + IotDevicePropertyDO property = new IotDevicePropertyDO(); + property.setValue(value); + properties.put(identifier, property); + when(devicePropertyService.getLatestDeviceProperties(deviceId)).thenReturn(properties); + } + + private void mockDeviceState(Long deviceId, Integer state) { + IotDeviceDO device = new IotDeviceDO(); + device.setId(deviceId); + device.setState(state); + when(deviceService.getDevice(deviceId)).thenReturn(device); + } + + /** + * 创建单条件的条件组列表 + */ + private List> createSingleConditionGroups( + IotSceneRuleDO.TriggerCondition condition) { + List group = new ArrayList<>(); + group.add(condition); + List> groups = new ArrayList<>(); + groups.add(group); + return groups; + } + + /** + * 创建两个单条件组的条件组列表 + */ + private List> createTwoSingleConditionGroups( + IotSceneRuleDO.TriggerCondition cond1, IotSceneRuleDO.TriggerCondition cond2) { + List group1 = new ArrayList<>(); + group1.add(cond1); + List group2 = new ArrayList<>(); + group2.add(cond2); + List> groups = new ArrayList<>(); + groups.add(group1); + groups.add(group2); + return groups; + } + + /** + * 创建单个多条件组的条件组列表 + */ + private List> createSingleGroupWithMultipleConditions( + IotSceneRuleDO.TriggerCondition... conditions) { + List group = new ArrayList<>(Arrays.asList(conditions)); + List> groups = new ArrayList<>(); + groups.add(group); + return groups; + } + + // ========== 测试用例 ========== + + @Nested + @DisplayName("空条件组测试 - Validates: Requirement 2.1") + class EmptyConditionGroupsTest { + + @Test + @DisplayName("定时触发器无条件组时,应直接执行动作") + void testTimerTrigger_withNullConditionGroups_shouldExecuteActions() { + // 准备数据 + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", null); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(sceneRuleMapper, times(1)).selectById(SCENE_RULE_ID); + verify(devicePropertyService, never()).getLatestDeviceProperties(any()); + verify(deviceService, never()).getDevice(any()); + } + + @Test + @DisplayName("定时触发器条件组为空列表时,应直接执行动作") + void testTimerTrigger_withEmptyConditionGroups_shouldExecuteActions() { + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", Collections.emptyList()); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(sceneRuleMapper, times(1)).selectById(SCENE_RULE_ID); + verify(devicePropertyService, never()).getLatestDeviceProperties(any()); + } + } + + @Nested + @DisplayName("条件组 OR 逻辑测试 - Validates: Requirements 2.2, 2.3") + class ConditionGroupOrLogicTest { + + @Test + @DisplayName("多个条件组中第一个满足时,应执行动作") + void testMultipleConditionGroups_firstGroupMatches_shouldExecuteActions() { + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition( + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition( + DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50"); + + List> conditionGroups = + createTwoSingleConditionGroups(condition1, condition2); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); + mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); + } + + @Test + @DisplayName("多个条件组中第二个满足时,应执行动作") + void testMultipleConditionGroups_secondGroupMatches_shouldExecuteActions() { + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition( + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50"); + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition( + DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); + + List> conditionGroups = + createTwoSingleConditionGroups(condition1, condition2); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); + mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID + 1); + } + } + + @Nested + @DisplayName("条件组内 AND 逻辑测试 - Validates: Requirement 2.4") + class ConditionGroupAndLogicTest { + + @Test + @DisplayName("条件组内所有条件都满足时,该组应匹配成功") + void testSingleConditionGroup_allConditionsMatch_shouldPass() { + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition( + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition( + DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "80"); + + List> conditionGroups = + createSingleGroupWithMultipleConditions(condition1, condition2); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + Map properties = new HashMap<>(); + IotDevicePropertyDO tempProperty = new IotDevicePropertyDO(); + tempProperty.setValue(30); + properties.put("temperature", tempProperty); + IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO(); + humidityProperty.setValue(60); + properties.put("humidity", humidityProperty); + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); + } + + @Test + @DisplayName("条件组内有一个条件不满足时,该组应匹配失败") + void testSingleConditionGroup_oneConditionFails_shouldFail() { + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition( + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition( + DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "50"); + + List> conditionGroups = + createSingleGroupWithMultipleConditions(condition1, condition2); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + Map properties = new HashMap<>(); + IotDevicePropertyDO tempProperty = new IotDevicePropertyDO(); + tempProperty.setValue(30); + properties.put("temperature", tempProperty); + IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO(); + humidityProperty.setValue(60); // 不满足 < 50 + properties.put("humidity", humidityProperty); + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); + } + } + + @Nested + @DisplayName("所有条件组不满足测试 - Validates: Requirement 2.5") + class AllConditionGroupsFailTest { + + @Test + @DisplayName("所有条件组都不满足时,应跳过动作执行") + void testAllConditionGroups_allFail_shouldSkipExecution() { + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition( + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50"); + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition( + DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50"); + + List> conditionGroups = + createTwoSingleConditionGroups(condition1, condition2); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); + mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID + 1); + } + } + + @Nested + @DisplayName("设备状态条件测试 - Validates: Requirements 4.1, 4.2") + class DeviceStateConditionTest { + + @Test + @DisplayName("设备在线状态条件满足时,应匹配成功") + void testDeviceStateCondition_online_shouldMatch() { + IotSceneRuleDO.TriggerCondition condition = createDeviceStateCondition( + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + String.valueOf(IotDeviceStateEnum.ONLINE.getState())); + + List> conditionGroups = createSingleConditionGroups(condition); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState()); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID); + } + + @Test + @DisplayName("设备不存在时,条件应不匹配") + void testDeviceStateCondition_deviceNotExists_shouldNotMatch() { + IotSceneRuleDO.TriggerCondition condition = createDeviceStateCondition( + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + String.valueOf(IotDeviceStateEnum.ONLINE.getState())); + + List> conditionGroups = createSingleConditionGroups(condition); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + when(deviceService.getDevice(DEVICE_ID)).thenReturn(null); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID); + } + } + + @Nested + @DisplayName("设备属性条件测试 - Validates: Requirements 3.1, 3.2, 3.3") + class DevicePropertyConditionTest { + + @Test + @DisplayName("设备属性条件满足时,应匹配成功") + void testDevicePropertyCondition_match_shouldPass() { + IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition( + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "25"); + + List> conditionGroups = createSingleConditionGroups(condition); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); + } + + @Test + @DisplayName("设备属性不存在时,条件应不匹配") + void testDevicePropertyCondition_propertyNotExists_shouldNotMatch() { + IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition( + DEVICE_ID, "nonexistent", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "25"); + + List> conditionGroups = createSingleConditionGroups(condition); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(Collections.emptyMap()); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); + } + + @Test + @DisplayName("设备属性等于条件测试") + void testDevicePropertyCondition_equals_shouldMatch() { + IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition( + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), "30"); + + List> conditionGroups = createSingleConditionGroups(condition); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); + } + } + + @Nested + @DisplayName("场景规则状态测试") + class SceneRuleStatusTest { + + @Test + @DisplayName("场景规则不存在时,应直接返回") + void testSceneRule_notExists_shouldReturn() { + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(null); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, never()).getLatestDeviceProperties(any()); + } + + @Test + @DisplayName("场景规则已禁用时,应直接返回") + void testSceneRule_disabled_shouldReturn() { + IotSceneRuleDO sceneRule = createBaseSceneRule(); + sceneRule.setStatus(CommonStatusEnum.DISABLE.getStatus()); + + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, never()).getLatestDeviceProperties(any()); + } + + @Test + @DisplayName("场景规则无定时触发器时,应直接返回") + void testSceneRule_noTimerTrigger_shouldReturn() { + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger(); + deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType()); + sceneRule.setTriggers(ListUtil.toList(deviceTrigger)); + + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, never()).getLatestDeviceProperties(any()); + } + } + + @Nested + @DisplayName("复杂条件组合测试") + class ComplexConditionCombinationTest { + + @Test + @DisplayName("混合条件类型测试:设备属性 + 设备状态") + void testMixedConditionTypes_propertyAndState() { + IotSceneRuleDO.TriggerCondition propertyCondition = createDevicePropertyCondition( + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); + IotSceneRuleDO.TriggerCondition stateCondition = createDeviceStateCondition( + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + String.valueOf(IotDeviceStateEnum.ONLINE.getState())); + + List> conditionGroups = + createSingleGroupWithMultipleConditions(propertyCondition, stateCondition); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30); + mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState()); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); + verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID); + } + + @Test + @DisplayName("多条件组 OR 逻辑 + 组内 AND 逻辑综合测试") + void testComplexOrAndLogic() { + // 条件组1:温度 > 30 AND 湿度 < 50(不满足) + // 条件组2:温度 > 20 AND 设备在线(满足) + IotSceneRuleDO.TriggerCondition group1Cond1 = createDevicePropertyCondition( + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "30"); + IotSceneRuleDO.TriggerCondition group1Cond2 = createDevicePropertyCondition( + DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "50"); + + IotSceneRuleDO.TriggerCondition group2Cond1 = createDevicePropertyCondition( + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20"); + IotSceneRuleDO.TriggerCondition group2Cond2 = createDeviceStateCondition( + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), + String.valueOf(IotDeviceStateEnum.ONLINE.getState())); + + // 创建两个条件组 + List group1 = new ArrayList<>(); + group1.add(group1Cond1); + group1.add(group1Cond2); + List group2 = new ArrayList<>(); + group2.add(group2Cond1); + group2.add(group2Cond2); + List> conditionGroups = new ArrayList<>(); + conditionGroups.add(group1); + conditionGroups.add(group2); + + IotSceneRuleDO sceneRule = createBaseSceneRule(); + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups); + sceneRule.setTriggers(ListUtil.toList(trigger)); + + // Mock:温度 25,湿度 60,设备在线 + Map properties = new HashMap<>(); + IotDevicePropertyDO tempProperty = new IotDevicePropertyDO(); + tempProperty.setValue(25); + properties.put("temperature", tempProperty); + IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO(); + humidityProperty.setValue(60); + properties.put("humidity", humidityProperty); + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties); + mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState()); + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule); + + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID)); + + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID); + } + } + +}