Browse Source

feat(iot):【场景联动】定时触发,增加条件组

puhui999 8 months ago
parent
commit
4901912ece

+ 86 - 3
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<IotSceneRuleAction> 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<IotSceneRuleDO.TriggerCondition> 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<IotSceneRuleDO.TriggerCondition> 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;
+        }
+    }
+
     /**
      * 基于消息,获得匹配的规则场景列表
      *

+ 213 - 0
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 场景规则时间匹配工具类
+ * <p>
+ * 提供时间条件匹配的通用方法,供 {@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<String> 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<String> 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);
+        }
+    }
+
+}

+ 3 - 159
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<String> 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<String> 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; // 较低优先级

+ 189 - 0
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 定时触发器条件评估器
+ * <p>
+ * 与设备触发器不同,定时触发器没有设备消息上下文,
+ * 需要主动查询设备属性和状态来评估条件。
+ *
+ * @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<String, IotDevicePropertyDO> 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;
+    }
+
+}

+ 611 - 0
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} 定时触发器条件组集成测试
+ * <p>
+ * 测试定时触发器的条件组评估功能:
+ * - 空条件组直接执行动作
+ * - 条件组评估后决定是否执行动作
+ * - 条件组之间的 OR 逻辑
+ * - 条件组内的 AND 逻辑
+ * - 所有条件组不满足时跳过执行
+ * <p>
+ * 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<IotSceneRuleAction> 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<List<IotSceneRuleDO.TriggerCondition>> 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<String, IotDevicePropertyDO> 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<List<IotSceneRuleDO.TriggerCondition>> createSingleConditionGroups(
+            IotSceneRuleDO.TriggerCondition condition) {
+        List<IotSceneRuleDO.TriggerCondition> group = new ArrayList<>();
+        group.add(condition);
+        List<List<IotSceneRuleDO.TriggerCondition>> groups = new ArrayList<>();
+        groups.add(group);
+        return groups;
+    }
+
+    /**
+     * 创建两个单条件组的条件组列表
+     */
+    private List<List<IotSceneRuleDO.TriggerCondition>> createTwoSingleConditionGroups(
+            IotSceneRuleDO.TriggerCondition cond1, IotSceneRuleDO.TriggerCondition cond2) {
+        List<IotSceneRuleDO.TriggerCondition> group1 = new ArrayList<>();
+        group1.add(cond1);
+        List<IotSceneRuleDO.TriggerCondition> group2 = new ArrayList<>();
+        group2.add(cond2);
+        List<List<IotSceneRuleDO.TriggerCondition>> groups = new ArrayList<>();
+        groups.add(group1);
+        groups.add(group2);
+        return groups;
+    }
+
+    /**
+     * 创建单个多条件组的条件组列表
+     */
+    private List<List<IotSceneRuleDO.TriggerCondition>> createSingleGroupWithMultipleConditions(
+            IotSceneRuleDO.TriggerCondition... conditions) {
+        List<IotSceneRuleDO.TriggerCondition> group = new ArrayList<>(Arrays.asList(conditions));
+        List<List<IotSceneRuleDO.TriggerCondition>> 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<List<IotSceneRuleDO.TriggerCondition>> 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<List<IotSceneRuleDO.TriggerCondition>> 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<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
+                    createSingleGroupWithMultipleConditions(condition1, condition2);
+
+            IotSceneRuleDO sceneRule = createBaseSceneRule();
+            IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
+            sceneRule.setTriggers(ListUtil.toList(trigger));
+
+            Map<String, IotDevicePropertyDO> 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<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
+                    createSingleGroupWithMultipleConditions(condition1, condition2);
+
+            IotSceneRuleDO sceneRule = createBaseSceneRule();
+            IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
+            sceneRule.setTriggers(ListUtil.toList(trigger));
+
+            Map<String, IotDevicePropertyDO> 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<List<IotSceneRuleDO.TriggerCondition>> 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<List<IotSceneRuleDO.TriggerCondition>> 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<List<IotSceneRuleDO.TriggerCondition>> 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<List<IotSceneRuleDO.TriggerCondition>> 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<List<IotSceneRuleDO.TriggerCondition>> 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<List<IotSceneRuleDO.TriggerCondition>> 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<List<IotSceneRuleDO.TriggerCondition>> 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<IotSceneRuleDO.TriggerCondition> group1 = new ArrayList<>();
+            group1.add(group1Cond1);
+            group1.add(group1Cond2);
+            List<IotSceneRuleDO.TriggerCondition> group2 = new ArrayList<>();
+            group2.add(group2Cond1);
+            group2.add(group2Cond2);
+            List<List<IotSceneRuleDO.TriggerCondition>> 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<String, IotDevicePropertyDO> 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);
+        }
+    }
+
+}