Commit 4901912ece006720db02fc828bef4f0a810967e2

Authored by puhui999
1 parent e765099a

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

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; @@ -23,6 +23,7 @@ import cn.iocoder.yudao.module.iot.service.product.IotProductService;
23 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; 23 import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
24 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager; 24 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager;
25 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler; 25 import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
  26 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator;
26 import jakarta.annotation.Resource; 27 import jakarta.annotation.Resource;
27 import lombok.extern.slf4j.Slf4j; 28 import lombok.extern.slf4j.Slf4j;
28 import org.springframework.cache.annotation.CacheEvict; 29 import org.springframework.cache.annotation.CacheEvict;
@@ -62,6 +63,8 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -62,6 +63,8 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
62 private List<IotSceneRuleAction> sceneRuleActions; 63 private List<IotSceneRuleAction> sceneRuleActions;
63 @Resource 64 @Resource
64 private IotSceneRuleTimerHandler timerHandler; 65 private IotSceneRuleTimerHandler timerHandler;
  66 + @Resource
  67 + private IotTimerConditionEvaluator timerConditionEvaluator;
65 68
66 @Override 69 @Override
67 @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) 70 @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true)
@@ -222,19 +225,99 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { @@ -222,19 +225,99 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService {
222 return; 225 return;
223 } 226 }
224 // 1.2 判断是否有定时触发器,避免脏数据 227 // 1.2 判断是否有定时触发器,避免脏数据
225 - IotSceneRuleDO.Trigger config = CollUtil.findOne(scene.getTriggers(), 228 + IotSceneRuleDO.Trigger timerTrigger = CollUtil.findOne(scene.getTriggers(),
226 trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType())); 229 trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType()));
227 - if (config == null) { 230 + if (timerTrigger == null) {
228 log.error("[executeSceneRuleByTimer][规则场景({}) 不存在定时触发器]", scene); 231 log.error("[executeSceneRuleByTimer][规则场景({}) 不存在定时触发器]", scene);
229 return; 232 return;
230 } 233 }
231 234
232 - // 2. 执行规则场景 235 + // 2. 评估条件组(新增逻辑)
  236 + log.info("[executeSceneRuleByTimer][规则场景({}) 开始评估条件组]", id);
  237 + if (!evaluateTimerConditionGroups(scene, timerTrigger)) {
  238 + log.info("[executeSceneRuleByTimer][规则场景({}) 条件组不满足,跳过执行]", id);
  239 + return;
  240 + }
  241 + log.info("[executeSceneRuleByTimer][规则场景({}) 条件组评估通过,准备执行动作]", id);
  242 +
  243 + // 3. 执行规则场景
233 TenantUtils.execute(scene.getTenantId(), 244 TenantUtils.execute(scene.getTenantId(),
234 () -> executeSceneRuleAction(null, ListUtil.toList(scene))); 245 () -> executeSceneRuleAction(null, ListUtil.toList(scene)));
235 } 246 }
236 247
237 /** 248 /**
  249 + * 评估定时触发器的条件组
  250 + *
  251 + * @param scene 场景规则
  252 + * @param trigger 定时触发器
  253 + * @return 是否满足条件
  254 + */
  255 + private boolean evaluateTimerConditionGroups(IotSceneRuleDO scene, IotSceneRuleDO.Trigger trigger) {
  256 + // 1. 如果没有条件组,直接返回 true(直接执行动作)
  257 + if (CollUtil.isEmpty(trigger.getConditionGroups())) {
  258 + log.debug("[evaluateTimerConditionGroups][规则场景({}) 无条件组配置,直接执行]", scene.getId());
  259 + return true;
  260 + }
  261 +
  262 + // 2. 条件组之间是 OR 关系,任一条件组满足即可
  263 + for (List<IotSceneRuleDO.TriggerCondition> conditionGroup : trigger.getConditionGroups()) {
  264 + if (evaluateSingleConditionGroup(scene, conditionGroup)) {
  265 + log.debug("[evaluateTimerConditionGroups][规则场景({}) 条件组匹配成功]", scene.getId());
  266 + return true;
  267 + }
  268 + }
  269 +
  270 + // 3. 所有条件组都不满足
  271 + log.debug("[evaluateTimerConditionGroups][规则场景({}) 所有条件组都不满足]", scene.getId());
  272 + return false;
  273 + }
  274 +
  275 + /**
  276 + * 评估单个条件组
  277 + *
  278 + * @param scene 场景规则
  279 + * @param conditionGroup 条件组
  280 + * @return 是否满足条件
  281 + */
  282 + private boolean evaluateSingleConditionGroup(IotSceneRuleDO scene,
  283 + List<IotSceneRuleDO.TriggerCondition> conditionGroup) {
  284 + // 1. 空条件组视为满足
  285 + if (CollUtil.isEmpty(conditionGroup)) {
  286 + return true;
  287 + }
  288 +
  289 + // 2. 条件之间是 AND 关系,所有条件都必须满足
  290 + for (IotSceneRuleDO.TriggerCondition condition : conditionGroup) {
  291 + if (!evaluateTimerCondition(scene, condition)) {
  292 + log.debug("[evaluateSingleConditionGroup][规则场景({}) 条件({}) 不满足]",
  293 + scene.getId(), condition);
  294 + return false;
  295 + }
  296 + }
  297 +
  298 + return true;
  299 + }
  300 +
  301 + /**
  302 + * 评估单个条件(定时触发器专用)
  303 + *
  304 + * @param scene 场景规则
  305 + * @param condition 条件
  306 + * @return 是否满足条件
  307 + */
  308 + private boolean evaluateTimerCondition(IotSceneRuleDO scene, IotSceneRuleDO.TriggerCondition condition) {
  309 + try {
  310 + boolean result = timerConditionEvaluator.evaluate(condition);
  311 + log.debug("[evaluateTimerCondition][规则场景({}) 条件类型({}) 评估结果: {}]",
  312 + scene.getId(), condition.getType(), result);
  313 + return result;
  314 + } catch (Exception e) {
  315 + log.error("[evaluateTimerCondition][规则场景({}) 条件评估异常]", scene.getId(), e);
  316 + return false;
  317 + }
  318 + }
  319 +
  320 + /**
238 * 基于消息,获得匹配的规则场景列表 321 * 基于消息,获得匹配的规则场景列表
239 * 322 *
240 * @param message 设备消息 323 * @param message 设备消息
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimeHelper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene;
  2 +
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.hutool.core.text.CharPool;
  5 +import cn.hutool.core.util.StrUtil;
  6 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +import java.time.LocalDateTime;
  10 +import java.time.LocalTime;
  11 +import java.time.ZoneId;
  12 +import java.time.format.DateTimeFormatter;
  13 +import java.util.List;
  14 +
  15 +/**
  16 + * IoT 场景规则时间匹配工具类
  17 + * <p>
  18 + * 提供时间条件匹配的通用方法,供 {@link cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition.IotCurrentTimeConditionMatcher}
  19 + * 和 {@link cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator} 共同使用。
  20 + *
  21 + * @author HUIHUI
  22 + */
  23 +@Slf4j
  24 +public class IotSceneRuleTimeHelper {
  25 +
  26 + /**
  27 + * 时间格式化器 - HH:mm:ss
  28 + */
  29 + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
  30 +
  31 + /**
  32 + * 时间格式化器 - HH:mm
  33 + */
  34 + private static final DateTimeFormatter TIME_FORMATTER_SHORT = DateTimeFormatter.ofPattern("HH:mm");
  35 +
  36 + private IotSceneRuleTimeHelper() {
  37 + // 工具类,禁止实例化
  38 + }
  39 +
  40 + /**
  41 + * 判断是否为日期时间操作符
  42 + *
  43 + * @param operatorEnum 操作符枚举
  44 + * @return 是否为日期时间操作符
  45 + */
  46 + public static boolean isDateTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) {
  47 + return operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN
  48 + || operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_LESS_THAN
  49 + || operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN;
  50 + }
  51 +
  52 + /**
  53 + * 判断是否为时间操作符(包括日期时间操作符和当日时间操作符)
  54 + *
  55 + * @param operatorEnum 操作符枚举
  56 + * @return 是否为时间操作符
  57 + */
  58 + public static boolean isTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) {
  59 + return operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN
  60 + && operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_LESS_THAN
  61 + && operatorEnum != IotSceneRuleConditionOperatorEnum.TIME_BETWEEN
  62 + && !isDateTimeOperator(operatorEnum);
  63 + }
  64 +
  65 + /**
  66 + * 执行时间匹配逻辑
  67 + *
  68 + * @param operatorEnum 操作符枚举
  69 + * @param param 参数值
  70 + * @return 是否匹配
  71 + */
  72 + public static boolean executeTimeMatching(IotSceneRuleConditionOperatorEnum operatorEnum, String param) {
  73 + try {
  74 + LocalDateTime now = LocalDateTime.now();
  75 + if (isDateTimeOperator(operatorEnum)) {
  76 + // 日期时间匹配(时间戳,秒级)
  77 + long currentTimestamp = now.atZone(ZoneId.systemDefault()).toEpochSecond();
  78 + return matchDateTime(currentTimestamp, operatorEnum, param);
  79 + } else {
  80 + // 当日时间匹配(HH:mm:ss)
  81 + return matchTime(now.toLocalTime(), operatorEnum, param);
  82 + }
  83 + } catch (Exception e) {
  84 + log.error("[executeTimeMatching][operatorEnum({}) param({}) 时间匹配异常]", operatorEnum, param, e);
  85 + return false;
  86 + }
  87 + }
  88 +
  89 + /**
  90 + * 匹配日期时间(时间戳,秒级)
  91 + *
  92 + * @param currentTimestamp 当前时间戳
  93 + * @param operatorEnum 操作符枚举
  94 + * @param param 参数值
  95 + * @return 是否匹配
  96 + */
  97 + public static boolean matchDateTime(long currentTimestamp, IotSceneRuleConditionOperatorEnum operatorEnum,
  98 + String param) {
  99 + try {
  100 + // DATE_TIME_BETWEEN 需要解析两个时间戳,单独处理
  101 + if (operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN) {
  102 + return matchDateTimeBetween(currentTimestamp, param);
  103 + }
  104 + // 其他操作符只需要解析一个时间戳
  105 + long targetTimestamp = Long.parseLong(param);
  106 + switch (operatorEnum) {
  107 + case DATE_TIME_GREATER_THAN:
  108 + return currentTimestamp > targetTimestamp;
  109 + case DATE_TIME_LESS_THAN:
  110 + return currentTimestamp < targetTimestamp;
  111 + default:
  112 + log.warn("[matchDateTime][operatorEnum({}) 不支持的日期时间操作符]", operatorEnum);
  113 + return false;
  114 + }
  115 + } catch (Exception e) {
  116 + log.error("[matchDateTime][operatorEnum({}) param({}) 日期时间匹配异常]", operatorEnum, param, e);
  117 + return false;
  118 + }
  119 + }
  120 +
  121 + /**
  122 + * 匹配日期时间区间
  123 + *
  124 + * @param currentTimestamp 当前时间戳
  125 + * @param param 参数值(格式:startTimestamp,endTimestamp)
  126 + * @return 是否匹配
  127 + */
  128 + public static boolean matchDateTimeBetween(long currentTimestamp, String param) {
  129 + List<String> timestampRange = StrUtil.splitTrim(param, CharPool.COMMA);
  130 + if (timestampRange.size() != 2) {
  131 + log.warn("[matchDateTimeBetween][param({}) 时间戳区间参数格式错误]", param);
  132 + return false;
  133 + }
  134 + long startTimestamp = Long.parseLong(timestampRange.get(0).trim());
  135 + long endTimestamp = Long.parseLong(timestampRange.get(1).trim());
  136 + return currentTimestamp >= startTimestamp && currentTimestamp <= endTimestamp;
  137 + }
  138 +
  139 + /**
  140 + * 匹配当日时间(HH:mm:ss 或 HH:mm)
  141 + *
  142 + * @param currentTime 当前时间
  143 + * @param operatorEnum 操作符枚举
  144 + * @param param 参数值
  145 + * @return 是否匹配
  146 + */
  147 + public static boolean matchTime(LocalTime currentTime, IotSceneRuleConditionOperatorEnum operatorEnum,
  148 + String param) {
  149 + try {
  150 + // TIME_BETWEEN 需要解析两个时间,单独处理
  151 + if (operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_BETWEEN) {
  152 + return matchTimeBetween(currentTime, param);
  153 + }
  154 + // 其他操作符只需要解析一个时间
  155 + LocalTime targetTime = parseTime(param);
  156 + switch (operatorEnum) {
  157 + case TIME_GREATER_THAN:
  158 + return currentTime.isAfter(targetTime);
  159 + case TIME_LESS_THAN:
  160 + return currentTime.isBefore(targetTime);
  161 + default:
  162 + log.warn("[matchTime][operatorEnum({}) 不支持的时间操作符]", operatorEnum);
  163 + return false;
  164 + }
  165 + } catch (Exception e) {
  166 + log.error("[matchTime][operatorEnum({}) param({}) 时间解析异常]", operatorEnum, param, e);
  167 + return false;
  168 + }
  169 + }
  170 +
  171 + /**
  172 + * 匹配时间区间
  173 + *
  174 + * @param currentTime 当前时间
  175 + * @param param 参数值(格式:startTime,endTime)
  176 + * @return 是否匹配
  177 + */
  178 + public static boolean matchTimeBetween(LocalTime currentTime, String param) {
  179 + List<String> timeRange = StrUtil.splitTrim(param, CharPool.COMMA);
  180 + if (timeRange.size() != 2) {
  181 + log.warn("[matchTimeBetween][param({}) 时间区间参数格式错误]", param);
  182 + return false;
  183 + }
  184 + LocalTime startTime = parseTime(timeRange.get(0).trim());
  185 + LocalTime endTime = parseTime(timeRange.get(1).trim());
  186 + return !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime);
  187 + }
  188 +
  189 + /**
  190 + * 解析时间字符串
  191 + * 支持 HH:mm 和 HH:mm:ss 两种格式
  192 + *
  193 + * @param timeStr 时间字符串
  194 + * @return 解析后的 LocalTime
  195 + */
  196 + public static LocalTime parseTime(String timeStr) {
  197 + Assert.isFalse(StrUtil.isBlank(timeStr), "时间字符串不能为空");
  198 + try {
  199 + // 尝试不同的时间格式
  200 + if (timeStr.length() == 5) { // HH:mm
  201 + return LocalTime.parse(timeStr, TIME_FORMATTER_SHORT);
  202 + } else if (timeStr.length() == 8) { // HH:mm:ss
  203 + return LocalTime.parse(timeStr, TIME_FORMATTER);
  204 + } else {
  205 + throw new IllegalArgumentException("时间格式长度不正确,期望 HH:mm 或 HH:mm:ss 格式");
  206 + }
  207 + } catch (Exception e) {
  208 + log.error("[parseTime][timeStr({}) 时间格式解析失败]", timeStr, e);
  209 + throw new IllegalArgumentException("时间格式无效: " + timeStr, e);
  210 + }
  211 + }
  212 +
  213 +}
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/matcher/condition/IotCurrentTimeConditionMatcher.java
1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition; 1 package cn.iocoder.yudao.module.iot.service.rule.scene.matcher.condition;
2 2
3 -import cn.hutool.core.lang.Assert;  
4 -import cn.hutool.core.text.CharPool;  
5 -import cn.hutool.core.util.StrUtil;  
6 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 3 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; 4 import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
8 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; 5 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
9 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; 6 import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  7 +import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleTimeHelper;
10 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; 8 import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
11 import lombok.extern.slf4j.Slf4j; 9 import lombok.extern.slf4j.Slf4j;
12 import org.springframework.stereotype.Component; 10 import org.springframework.stereotype.Component;
13 11
14 -import java.time.LocalDateTime;  
15 -import java.time.LocalTime;  
16 -import java.time.format.DateTimeFormatter;  
17 -import java.util.List;  
18 -  
19 /** 12 /**
20 * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑 13 * 当前时间条件匹配器:处理时间相关的子条件匹配逻辑
21 * 14 *
@@ -25,16 +18,6 @@ import java.util.List; @@ -25,16 +18,6 @@ import java.util.List;
25 @Slf4j 18 @Slf4j
26 public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher { 19 public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatcher {
27 20
28 - /**  
29 - * 时间格式化器 - HH:mm:ss  
30 - */  
31 - private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");  
32 -  
33 - /**  
34 - * 时间格式化器 - HH:mm  
35 - */  
36 - private static final DateTimeFormatter TIME_FORMATTER_SHORT = DateTimeFormatter.ofPattern("HH:mm");  
37 -  
38 @Override 21 @Override
39 public IotSceneRuleConditionTypeEnum getSupportedConditionType() { 22 public IotSceneRuleConditionTypeEnum getSupportedConditionType() {
40 return IotSceneRuleConditionTypeEnum.CURRENT_TIME; 23 return IotSceneRuleConditionTypeEnum.CURRENT_TIME;
@@ -62,13 +45,13 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc @@ -62,13 +45,13 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc
62 return false; 45 return false;
63 } 46 }
64 47
65 - if (!isTimeOperator(operatorEnum)) { 48 + if (IotSceneRuleTimeHelper.isTimeOperator(operatorEnum)) {
66 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "不支持的时间操作符: " + operator); 49 IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "不支持的时间操作符: " + operator);
67 return false; 50 return false;
68 } 51 }
69 52
70 // 2.1 执行时间匹配 53 // 2.1 执行时间匹配
71 - boolean matched = executeTimeMatching(operatorEnum, condition.getParam()); 54 + boolean matched = IotSceneRuleTimeHelper.executeTimeMatching(operatorEnum, condition.getParam());
72 55
73 // 2.2 记录匹配结果 56 // 2.2 记录匹配结果
74 if (matched) { 57 if (matched) {
@@ -80,145 +63,6 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc @@ -80,145 +63,6 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc
80 return matched; 63 return matched;
81 } 64 }
82 65
83 - /**  
84 - * 执行时间匹配逻辑  
85 - * 直接实现时间条件匹配,不使用 Spring EL 表达式  
86 - */  
87 - private boolean executeTimeMatching(IotSceneRuleConditionOperatorEnum operatorEnum, String param) {  
88 - try {  
89 - LocalDateTime now = LocalDateTime.now();  
90 -  
91 - if (isDateTimeOperator(operatorEnum)) {  
92 - // 日期时间匹配(时间戳)  
93 - long currentTimestamp = now.toEpochSecond(java.time.ZoneOffset.of("+8"));  
94 - return matchDateTime(currentTimestamp, operatorEnum, param);  
95 - } else {  
96 - // 当日时间匹配(HH:mm:ss)  
97 - return matchTime(now.toLocalTime(), operatorEnum, param);  
98 - }  
99 - } catch (Exception e) {  
100 - log.error("[executeTimeMatching][operatorEnum({}) param({}) 时间匹配异常]", operatorEnum, param, e);  
101 - return false;  
102 - }  
103 - }  
104 -  
105 - /**  
106 - * 判断是否为日期时间操作符  
107 - */  
108 - private boolean isDateTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) {  
109 - return operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_GREATER_THAN ||  
110 - operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_LESS_THAN ||  
111 - operatorEnum == IotSceneRuleConditionOperatorEnum.DATE_TIME_BETWEEN;  
112 - }  
113 -  
114 - /**  
115 - * 判断是否为时间操作符  
116 - */  
117 - private boolean isTimeOperator(IotSceneRuleConditionOperatorEnum operatorEnum) {  
118 - return operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_GREATER_THAN ||  
119 - operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_LESS_THAN ||  
120 - operatorEnum == IotSceneRuleConditionOperatorEnum.TIME_BETWEEN ||  
121 - isDateTimeOperator(operatorEnum);  
122 - }  
123 -  
124 - /**  
125 - * 匹配日期时间(时间戳)  
126 - * 直接实现时间戳比较逻辑  
127 - */  
128 - private boolean matchDateTime(long currentTimestamp, IotSceneRuleConditionOperatorEnum operatorEnum, String param) {  
129 - try {  
130 - long targetTimestamp = Long.parseLong(param);  
131 - switch (operatorEnum) {  
132 - case DATE_TIME_GREATER_THAN:  
133 - return currentTimestamp > targetTimestamp;  
134 - case DATE_TIME_LESS_THAN:  
135 - return currentTimestamp < targetTimestamp;  
136 - case DATE_TIME_BETWEEN:  
137 - return matchDateTimeBetween(currentTimestamp, param);  
138 - default:  
139 - log.warn("[matchDateTime][operatorEnum({}) 不支持的日期时间操作符]", operatorEnum);  
140 - return false;  
141 - }  
142 - } catch (Exception e) {  
143 - log.error("[matchDateTime][operatorEnum({}) param({}) 日期时间匹配异常]", operatorEnum, param, e);  
144 - return false;  
145 - }  
146 - }  
147 -  
148 - /**  
149 - * 匹配日期时间区间  
150 - */  
151 - private boolean matchDateTimeBetween(long currentTimestamp, String param) {  
152 - List<String> timestampRange = StrUtil.splitTrim(param, CharPool.COMMA);  
153 - if (timestampRange.size() != 2) {  
154 - log.warn("[matchDateTimeBetween][param({}) 时间戳区间参数格式错误]", param);  
155 - return false;  
156 - }  
157 - long startTimestamp = Long.parseLong(timestampRange.get(0).trim());  
158 - long endTimestamp = Long.parseLong(timestampRange.get(1).trim());  
159 - return currentTimestamp >= startTimestamp && currentTimestamp <= endTimestamp;  
160 - }  
161 -  
162 - /**  
163 - * 匹配当日时间(HH:mm:ss)  
164 - * 直接实现时间比较逻辑  
165 - */  
166 - private boolean matchTime(LocalTime currentTime, IotSceneRuleConditionOperatorEnum operatorEnum, String param) {  
167 - try {  
168 - LocalTime targetTime = parseTime(param);  
169 - switch (operatorEnum) {  
170 - case TIME_GREATER_THAN:  
171 - return currentTime.isAfter(targetTime);  
172 - case TIME_LESS_THAN:  
173 - return currentTime.isBefore(targetTime);  
174 - case TIME_BETWEEN:  
175 - return matchTimeBetween(currentTime, param);  
176 - default:  
177 - log.warn("[matchTime][operatorEnum({}) 不支持的时间操作符]", operatorEnum);  
178 - return false;  
179 - }  
180 - } catch (Exception e) {  
181 - log.error("[matchTime][][operatorEnum({}) param({}) 时间解析异常]", operatorEnum, param, e);  
182 - return false;  
183 - }  
184 - }  
185 -  
186 - /**  
187 - * 匹配时间区间  
188 - */  
189 - private boolean matchTimeBetween(LocalTime currentTime, String param) {  
190 - List<String> timeRange = StrUtil.splitTrim(param, CharPool.COMMA);  
191 - if (timeRange.size() != 2) {  
192 - log.warn("[matchTimeBetween][param({}) 时间区间参数格式错误]", param);  
193 - return false;  
194 - }  
195 - LocalTime startTime = parseTime(timeRange.get(0).trim());  
196 - LocalTime endTime = parseTime(timeRange.get(1).trim());  
197 - return !currentTime.isBefore(startTime) && !currentTime.isAfter(endTime);  
198 - }  
199 -  
200 - /**  
201 - * 解析时间字符串  
202 - * 支持 HH:mm 和 HH:mm:ss 两种格式  
203 - */  
204 - private LocalTime parseTime(String timeStr) {  
205 - Assert.isFalse(StrUtil.isBlank(timeStr), "时间字符串不能为空");  
206 -  
207 - try {  
208 - // 尝试不同的时间格式  
209 - if (timeStr.length() == 5) { // HH:mm  
210 - return LocalTime.parse(timeStr, TIME_FORMATTER_SHORT);  
211 - } else if (timeStr.length() == 8) { // HH:mm:ss  
212 - return LocalTime.parse(timeStr, TIME_FORMATTER);  
213 - } else {  
214 - throw new IllegalArgumentException("时间格式长度不正确,期望 HH:mm 或 HH:mm:ss 格式");  
215 - }  
216 - } catch (Exception e) {  
217 - log.error("[parseTime][timeStr({}) 时间格式解析失败]", timeStr, e);  
218 - throw new IllegalArgumentException("时间格式无效: " + timeStr, e);  
219 - }  
220 - }  
221 -  
222 @Override 66 @Override
223 public int getPriority() { 67 public int getPriority() {
224 return 40; // 较低优先级 68 return 40; // 较低优先级
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/scene/timer/IotTimerConditionEvaluator.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  5 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
  6 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  7 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
  8 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
  10 +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
  11 +import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleTimeHelper;
  12 +import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper;
  13 +import jakarta.annotation.Resource;
  14 +import lombok.extern.slf4j.Slf4j;
  15 +import org.springframework.stereotype.Component;
  16 +
  17 +import java.util.Map;
  18 +
  19 +/**
  20 + * IoT 定时触发器条件评估器
  21 + * <p>
  22 + * 与设备触发器不同,定时触发器没有设备消息上下文,
  23 + * 需要主动查询设备属性和状态来评估条件。
  24 + *
  25 + * @author HUIHUI
  26 + */
  27 +@Component
  28 +@Slf4j
  29 +public class IotTimerConditionEvaluator {
  30 +
  31 + @Resource
  32 + private IotDevicePropertyService devicePropertyService;
  33 +
  34 + @Resource
  35 + private IotDeviceService deviceService;
  36 +
  37 + /**
  38 + * 评估条件
  39 + *
  40 + * @param condition 条件配置
  41 + * @return 是否满足条件
  42 + */
  43 + public boolean evaluate(IotSceneRuleDO.TriggerCondition condition) {
  44 + // 1. 基础参数校验
  45 + if (condition == null || condition.getType() == null) {
  46 + log.warn("[evaluate][条件为空或类型为空]");
  47 + return false;
  48 + }
  49 +
  50 + // 2. 根据条件类型分发到具体的评估方法
  51 + IotSceneRuleConditionTypeEnum conditionType =
  52 + IotSceneRuleConditionTypeEnum.typeOf(condition.getType());
  53 + if (conditionType == null) {
  54 + log.warn("[evaluate][未知的条件类型: {}]", condition.getType());
  55 + return false;
  56 + }
  57 +
  58 + switch (conditionType) {
  59 + case DEVICE_PROPERTY:
  60 + return evaluateDevicePropertyCondition(condition);
  61 + case DEVICE_STATE:
  62 + return evaluateDeviceStateCondition(condition);
  63 + case CURRENT_TIME:
  64 + return evaluateCurrentTimeCondition(condition);
  65 + default:
  66 + log.warn("[evaluate][未知的条件类型: {}]", conditionType);
  67 + return false;
  68 + }
  69 + }
  70 +
  71 + /**
  72 + * 评估设备属性条件
  73 + *
  74 + * @param condition 条件配置
  75 + * @return 是否满足条件
  76 + */
  77 + private boolean evaluateDevicePropertyCondition(IotSceneRuleDO.TriggerCondition condition) {
  78 + // 1. 校验必要参数
  79 + if (condition.getDeviceId() == null) {
  80 + log.debug("[evaluateDevicePropertyCondition][设备ID为空]");
  81 + return false;
  82 + }
  83 + if (StrUtil.isBlank(condition.getIdentifier())) {
  84 + log.debug("[evaluateDevicePropertyCondition][属性标识符为空]");
  85 + return false;
  86 + }
  87 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) {
  88 + log.debug("[evaluateDevicePropertyCondition][操作符或参数无效]");
  89 + return false;
  90 + }
  91 +
  92 + // 2. 获取设备最新属性值
  93 + Map<String, IotDevicePropertyDO> properties =
  94 + devicePropertyService.getLatestDeviceProperties(condition.getDeviceId());
  95 + if (properties == null || properties.isEmpty()) {
  96 + log.debug("[evaluateDevicePropertyCondition][设备({}) 无属性数据]", condition.getDeviceId());
  97 + return false;
  98 + }
  99 +
  100 + // 3. 获取指定属性
  101 + IotDevicePropertyDO property = properties.get(condition.getIdentifier());
  102 + if (property == null || property.getValue() == null) {
  103 + log.debug("[evaluateDevicePropertyCondition][设备({}) 属性({}) 不存在或值为空]",
  104 + condition.getDeviceId(), condition.getIdentifier());
  105 + return false;
  106 + }
  107 +
  108 + // 4. 使用现有的条件评估逻辑进行比较
  109 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(
  110 + property.getValue(), condition.getOperator(), condition.getParam());
  111 + log.debug("[evaluateDevicePropertyCondition][设备({}) 属性({}) 值({}) 操作符({}) 参数({}) 匹配结果: {}]",
  112 + condition.getDeviceId(), condition.getIdentifier(), property.getValue(),
  113 + condition.getOperator(), condition.getParam(), matched);
  114 + return matched;
  115 + }
  116 +
  117 + /**
  118 + * 评估设备状态条件
  119 + *
  120 + * @param condition 条件配置
  121 + * @return 是否满足条件
  122 + */
  123 + private boolean evaluateDeviceStateCondition(IotSceneRuleDO.TriggerCondition condition) {
  124 + // 1. 校验必要参数
  125 + if (condition.getDeviceId() == null) {
  126 + log.debug("[evaluateDeviceStateCondition][设备ID为空]");
  127 + return false;
  128 + }
  129 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) {
  130 + log.debug("[evaluateDeviceStateCondition][操作符或参数无效]");
  131 + return false;
  132 + }
  133 +
  134 + // 2. 获取设备信息
  135 + IotDeviceDO device = deviceService.getDevice(condition.getDeviceId());
  136 + if (device == null) {
  137 + log.debug("[evaluateDeviceStateCondition][设备({}) 不存在]", condition.getDeviceId());
  138 + return false;
  139 + }
  140 +
  141 + // 3. 获取设备状态
  142 + Integer state = device.getState();
  143 + if (state == null) {
  144 + log.debug("[evaluateDeviceStateCondition][设备({}) 状态为空]", condition.getDeviceId());
  145 + return false;
  146 + }
  147 +
  148 + // 4. 比较状态
  149 + boolean matched = IotSceneRuleMatcherHelper.evaluateCondition(
  150 + state.toString(), condition.getOperator(), condition.getParam());
  151 + log.debug("[evaluateDeviceStateCondition][设备({}) 状态({}) 操作符({}) 参数({}) 匹配结果: {}]",
  152 + condition.getDeviceId(), state, condition.getOperator(), condition.getParam(), matched);
  153 + return matched;
  154 + }
  155 +
  156 + /**
  157 + * 评估当前时间条件
  158 + *
  159 + * @param condition 条件配置
  160 + * @return 是否满足条件
  161 + */
  162 + private boolean evaluateCurrentTimeCondition(IotSceneRuleDO.TriggerCondition condition) {
  163 + // 1. 校验必要参数
  164 + if (!IotSceneRuleMatcherHelper.isConditionOperatorAndParamValid(condition)) {
  165 + log.debug("[evaluateCurrentTimeCondition][操作符或参数无效]");
  166 + return false;
  167 + }
  168 +
  169 + // 2. 验证操作符是否为支持的时间操作符
  170 + IotSceneRuleConditionOperatorEnum operatorEnum =
  171 + IotSceneRuleConditionOperatorEnum.operatorOf(condition.getOperator());
  172 + if (operatorEnum == null) {
  173 + log.debug("[evaluateCurrentTimeCondition][无效的操作符: {}]", condition.getOperator());
  174 + return false;
  175 + }
  176 +
  177 + if (IotSceneRuleTimeHelper.isTimeOperator(operatorEnum)) {
  178 + log.debug("[evaluateCurrentTimeCondition][不支持的时间操作符: {}]", condition.getOperator());
  179 + return false;
  180 + }
  181 +
  182 + // 3. 执行时间匹配
  183 + boolean matched = IotSceneRuleTimeHelper.executeTimeMatching(operatorEnum, condition.getParam());
  184 + log.debug("[evaluateCurrentTimeCondition][操作符({}) 参数({}) 匹配结果: {}]",
  185 + condition.getOperator(), condition.getParam(), matched);
  186 + return matched;
  187 + }
  188 +
  189 +}
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/scene/IotSceneRuleTimerConditionIntegrationTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.scene;
  2 +
  3 +import cn.hutool.core.collection.ListUtil;
  4 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceStateEnum;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
  8 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDevicePropertyDO;
  9 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
  10 +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotSceneRuleMapper;
  11 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum;
  12 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum;
  13 +import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
  14 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
  15 +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
  16 +import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction;
  17 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotSceneRuleTimerHandler;
  18 +import cn.iocoder.yudao.module.iot.service.rule.scene.timer.IotTimerConditionEvaluator;
  19 +import org.junit.jupiter.api.BeforeEach;
  20 +import org.junit.jupiter.api.DisplayName;
  21 +import org.junit.jupiter.api.Nested;
  22 +import org.junit.jupiter.api.Test;
  23 +import org.mockito.InjectMocks;
  24 +import org.mockito.Mock;
  25 +
  26 +import java.util.*;
  27 +
  28 +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
  29 +import static org.mockito.ArgumentMatchers.any;
  30 +import static org.mockito.Mockito.*;
  31 +
  32 +/**
  33 + * {@link IotSceneRuleServiceImpl} 定时触发器条件组集成测试
  34 + * <p>
  35 + * 测试定时触发器的条件组评估功能:
  36 + * - 空条件组直接执行动作
  37 + * - 条件组评估后决定是否执行动作
  38 + * - 条件组之间的 OR 逻辑
  39 + * - 条件组内的 AND 逻辑
  40 + * - 所有条件组不满足时跳过执行
  41 + * <p>
  42 + * Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5
  43 + *
  44 + * @author HUIHUI
  45 + */
  46 +public class IotSceneRuleTimerConditionIntegrationTest extends BaseMockitoUnitTest {
  47 +
  48 + @InjectMocks
  49 + private IotSceneRuleServiceImpl sceneRuleService;
  50 +
  51 + @Mock
  52 + private IotSceneRuleMapper sceneRuleMapper;
  53 +
  54 + @Mock
  55 + private IotDeviceService deviceService;
  56 +
  57 + @Mock
  58 + private IotDevicePropertyService devicePropertyService;
  59 +
  60 + @Mock
  61 + private List<IotSceneRuleAction> sceneRuleActions;
  62 +
  63 + @Mock
  64 + private IotSceneRuleTimerHandler timerHandler;
  65 +
  66 + private IotTimerConditionEvaluator timerConditionEvaluator;
  67 +
  68 + // 测试常量
  69 + private static final Long SCENE_RULE_ID = 1L;
  70 + private static final Long TENANT_ID = 1L;
  71 + private static final Long DEVICE_ID = 100L;
  72 + private static final String PROPERTY_IDENTIFIER = "temperature";
  73 +
  74 + @BeforeEach
  75 + void setUp() {
  76 + // 创建并注入 timerConditionEvaluator 的依赖
  77 + timerConditionEvaluator = new IotTimerConditionEvaluator();
  78 + try {
  79 + var devicePropertyServiceField = IotTimerConditionEvaluator.class.getDeclaredField("devicePropertyService");
  80 + devicePropertyServiceField.setAccessible(true);
  81 + devicePropertyServiceField.set(timerConditionEvaluator, devicePropertyService);
  82 +
  83 + var deviceServiceField = IotTimerConditionEvaluator.class.getDeclaredField("deviceService");
  84 + deviceServiceField.setAccessible(true);
  85 + deviceServiceField.set(timerConditionEvaluator, deviceService);
  86 +
  87 + var evaluatorField = IotSceneRuleServiceImpl.class.getDeclaredField("timerConditionEvaluator");
  88 + evaluatorField.setAccessible(true);
  89 + evaluatorField.set(sceneRuleService, timerConditionEvaluator);
  90 + } catch (Exception e) {
  91 + throw new RuntimeException("Failed to inject dependencies", e);
  92 + }
  93 + }
  94 +
  95 + // ========== 辅助方法 ==========
  96 +
  97 + private IotSceneRuleDO createBaseSceneRule() {
  98 + IotSceneRuleDO sceneRule = new IotSceneRuleDO();
  99 + sceneRule.setId(SCENE_RULE_ID);
  100 + sceneRule.setTenantId(TENANT_ID);
  101 + sceneRule.setName("测试定时触发器");
  102 + sceneRule.setStatus(CommonStatusEnum.ENABLE.getStatus());
  103 + sceneRule.setActions(Collections.emptyList());
  104 + return sceneRule;
  105 + }
  106 +
  107 + private IotSceneRuleDO.Trigger createTimerTrigger(String cronExpression,
  108 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups) {
  109 + IotSceneRuleDO.Trigger trigger = new IotSceneRuleDO.Trigger();
  110 + trigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
  111 + trigger.setCronExpression(cronExpression);
  112 + trigger.setConditionGroups(conditionGroups);
  113 + return trigger;
  114 + }
  115 +
  116 + private IotSceneRuleDO.TriggerCondition createDevicePropertyCondition(Long deviceId, String identifier,
  117 + String operator, String param) {
  118 + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
  119 + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_PROPERTY.getType());
  120 + condition.setDeviceId(deviceId);
  121 + condition.setIdentifier(identifier);
  122 + condition.setOperator(operator);
  123 + condition.setParam(param);
  124 + return condition;
  125 + }
  126 +
  127 + private IotSceneRuleDO.TriggerCondition createDeviceStateCondition(Long deviceId, String operator, String param) {
  128 + IotSceneRuleDO.TriggerCondition condition = new IotSceneRuleDO.TriggerCondition();
  129 + condition.setType(IotSceneRuleConditionTypeEnum.DEVICE_STATE.getType());
  130 + condition.setDeviceId(deviceId);
  131 + condition.setOperator(operator);
  132 + condition.setParam(param);
  133 + return condition;
  134 + }
  135 +
  136 + private void mockDeviceProperty(Long deviceId, String identifier, Object value) {
  137 + Map<String, IotDevicePropertyDO> properties = new HashMap<>();
  138 + IotDevicePropertyDO property = new IotDevicePropertyDO();
  139 + property.setValue(value);
  140 + properties.put(identifier, property);
  141 + when(devicePropertyService.getLatestDeviceProperties(deviceId)).thenReturn(properties);
  142 + }
  143 +
  144 + private void mockDeviceState(Long deviceId, Integer state) {
  145 + IotDeviceDO device = new IotDeviceDO();
  146 + device.setId(deviceId);
  147 + device.setState(state);
  148 + when(deviceService.getDevice(deviceId)).thenReturn(device);
  149 + }
  150 +
  151 + /**
  152 + * 创建单条件的条件组列表
  153 + */
  154 + private List<List<IotSceneRuleDO.TriggerCondition>> createSingleConditionGroups(
  155 + IotSceneRuleDO.TriggerCondition condition) {
  156 + List<IotSceneRuleDO.TriggerCondition> group = new ArrayList<>();
  157 + group.add(condition);
  158 + List<List<IotSceneRuleDO.TriggerCondition>> groups = new ArrayList<>();
  159 + groups.add(group);
  160 + return groups;
  161 + }
  162 +
  163 + /**
  164 + * 创建两个单条件组的条件组列表
  165 + */
  166 + private List<List<IotSceneRuleDO.TriggerCondition>> createTwoSingleConditionGroups(
  167 + IotSceneRuleDO.TriggerCondition cond1, IotSceneRuleDO.TriggerCondition cond2) {
  168 + List<IotSceneRuleDO.TriggerCondition> group1 = new ArrayList<>();
  169 + group1.add(cond1);
  170 + List<IotSceneRuleDO.TriggerCondition> group2 = new ArrayList<>();
  171 + group2.add(cond2);
  172 + List<List<IotSceneRuleDO.TriggerCondition>> groups = new ArrayList<>();
  173 + groups.add(group1);
  174 + groups.add(group2);
  175 + return groups;
  176 + }
  177 +
  178 + /**
  179 + * 创建单个多条件组的条件组列表
  180 + */
  181 + private List<List<IotSceneRuleDO.TriggerCondition>> createSingleGroupWithMultipleConditions(
  182 + IotSceneRuleDO.TriggerCondition... conditions) {
  183 + List<IotSceneRuleDO.TriggerCondition> group = new ArrayList<>(Arrays.asList(conditions));
  184 + List<List<IotSceneRuleDO.TriggerCondition>> groups = new ArrayList<>();
  185 + groups.add(group);
  186 + return groups;
  187 + }
  188 +
  189 + // ========== 测试用例 ==========
  190 +
  191 + @Nested
  192 + @DisplayName("空条件组测试 - Validates: Requirement 2.1")
  193 + class EmptyConditionGroupsTest {
  194 +
  195 + @Test
  196 + @DisplayName("定时触发器无条件组时,应直接执行动作")
  197 + void testTimerTrigger_withNullConditionGroups_shouldExecuteActions() {
  198 + // 准备数据
  199 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  200 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", null);
  201 + sceneRule.setTriggers(ListUtil.toList(trigger));
  202 +
  203 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  204 +
  205 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  206 +
  207 + verify(sceneRuleMapper, times(1)).selectById(SCENE_RULE_ID);
  208 + verify(devicePropertyService, never()).getLatestDeviceProperties(any());
  209 + verify(deviceService, never()).getDevice(any());
  210 + }
  211 +
  212 + @Test
  213 + @DisplayName("定时触发器条件组为空列表时,应直接执行动作")
  214 + void testTimerTrigger_withEmptyConditionGroups_shouldExecuteActions() {
  215 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  216 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", Collections.emptyList());
  217 + sceneRule.setTriggers(ListUtil.toList(trigger));
  218 +
  219 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  220 +
  221 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  222 +
  223 + verify(sceneRuleMapper, times(1)).selectById(SCENE_RULE_ID);
  224 + verify(devicePropertyService, never()).getLatestDeviceProperties(any());
  225 + }
  226 + }
  227 +
  228 + @Nested
  229 + @DisplayName("条件组 OR 逻辑测试 - Validates: Requirements 2.2, 2.3")
  230 + class ConditionGroupOrLogicTest {
  231 +
  232 + @Test
  233 + @DisplayName("多个条件组中第一个满足时,应执行动作")
  234 + void testMultipleConditionGroups_firstGroupMatches_shouldExecuteActions() {
  235 + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition(
  236 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  237 + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition(
  238 + DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50");
  239 +
  240 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  241 + createTwoSingleConditionGroups(condition1, condition2);
  242 +
  243 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  244 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  245 + sceneRule.setTriggers(ListUtil.toList(trigger));
  246 +
  247 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  248 + mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30);
  249 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  250 +
  251 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  252 +
  253 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  254 + }
  255 +
  256 + @Test
  257 + @DisplayName("多个条件组中第二个满足时,应执行动作")
  258 + void testMultipleConditionGroups_secondGroupMatches_shouldExecuteActions() {
  259 + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition(
  260 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50");
  261 + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition(
  262 + DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  263 +
  264 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  265 + createTwoSingleConditionGroups(condition1, condition2);
  266 +
  267 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  268 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  269 + sceneRule.setTriggers(ListUtil.toList(trigger));
  270 +
  271 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  272 + mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30);
  273 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  274 +
  275 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  276 +
  277 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  278 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID + 1);
  279 + }
  280 + }
  281 +
  282 + @Nested
  283 + @DisplayName("条件组内 AND 逻辑测试 - Validates: Requirement 2.4")
  284 + class ConditionGroupAndLogicTest {
  285 +
  286 + @Test
  287 + @DisplayName("条件组内所有条件都满足时,该组应匹配成功")
  288 + void testSingleConditionGroup_allConditionsMatch_shouldPass() {
  289 + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition(
  290 + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  291 + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition(
  292 + DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "80");
  293 +
  294 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  295 + createSingleGroupWithMultipleConditions(condition1, condition2);
  296 +
  297 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  298 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  299 + sceneRule.setTriggers(ListUtil.toList(trigger));
  300 +
  301 + Map<String, IotDevicePropertyDO> properties = new HashMap<>();
  302 + IotDevicePropertyDO tempProperty = new IotDevicePropertyDO();
  303 + tempProperty.setValue(30);
  304 + properties.put("temperature", tempProperty);
  305 + IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO();
  306 + humidityProperty.setValue(60);
  307 + properties.put("humidity", humidityProperty);
  308 + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties);
  309 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  310 +
  311 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  312 +
  313 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  314 + }
  315 +
  316 + @Test
  317 + @DisplayName("条件组内有一个条件不满足时,该组应匹配失败")
  318 + void testSingleConditionGroup_oneConditionFails_shouldFail() {
  319 + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition(
  320 + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  321 + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition(
  322 + DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "50");
  323 +
  324 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  325 + createSingleGroupWithMultipleConditions(condition1, condition2);
  326 +
  327 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  328 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  329 + sceneRule.setTriggers(ListUtil.toList(trigger));
  330 +
  331 + Map<String, IotDevicePropertyDO> properties = new HashMap<>();
  332 + IotDevicePropertyDO tempProperty = new IotDevicePropertyDO();
  333 + tempProperty.setValue(30);
  334 + properties.put("temperature", tempProperty);
  335 + IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO();
  336 + humidityProperty.setValue(60); // 不满足 < 50
  337 + properties.put("humidity", humidityProperty);
  338 + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties);
  339 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  340 +
  341 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  342 +
  343 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  344 + }
  345 + }
  346 +
  347 + @Nested
  348 + @DisplayName("所有条件组不满足测试 - Validates: Requirement 2.5")
  349 + class AllConditionGroupsFailTest {
  350 +
  351 + @Test
  352 + @DisplayName("所有条件组都不满足时,应跳过动作执行")
  353 + void testAllConditionGroups_allFail_shouldSkipExecution() {
  354 + IotSceneRuleDO.TriggerCondition condition1 = createDevicePropertyCondition(
  355 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50");
  356 + IotSceneRuleDO.TriggerCondition condition2 = createDevicePropertyCondition(
  357 + DEVICE_ID + 1, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "50");
  358 +
  359 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  360 + createTwoSingleConditionGroups(condition1, condition2);
  361 +
  362 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  363 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  364 + sceneRule.setTriggers(ListUtil.toList(trigger));
  365 +
  366 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  367 + mockDeviceProperty(DEVICE_ID + 1, PROPERTY_IDENTIFIER, 30);
  368 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  369 +
  370 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  371 +
  372 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  373 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID + 1);
  374 + }
  375 + }
  376 +
  377 + @Nested
  378 + @DisplayName("设备状态条件测试 - Validates: Requirements 4.1, 4.2")
  379 + class DeviceStateConditionTest {
  380 +
  381 + @Test
  382 + @DisplayName("设备在线状态条件满足时,应匹配成功")
  383 + void testDeviceStateCondition_online_shouldMatch() {
  384 + IotSceneRuleDO.TriggerCondition condition = createDeviceStateCondition(
  385 + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  386 + String.valueOf(IotDeviceStateEnum.ONLINE.getState()));
  387 +
  388 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = createSingleConditionGroups(condition);
  389 +
  390 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  391 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  392 + sceneRule.setTriggers(ListUtil.toList(trigger));
  393 +
  394 + mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState());
  395 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  396 +
  397 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  398 +
  399 + verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID);
  400 + }
  401 +
  402 + @Test
  403 + @DisplayName("设备不存在时,条件应不匹配")
  404 + void testDeviceStateCondition_deviceNotExists_shouldNotMatch() {
  405 + IotSceneRuleDO.TriggerCondition condition = createDeviceStateCondition(
  406 + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  407 + String.valueOf(IotDeviceStateEnum.ONLINE.getState()));
  408 +
  409 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = createSingleConditionGroups(condition);
  410 +
  411 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  412 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  413 + sceneRule.setTriggers(ListUtil.toList(trigger));
  414 +
  415 + when(deviceService.getDevice(DEVICE_ID)).thenReturn(null);
  416 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  417 +
  418 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  419 +
  420 + verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID);
  421 + }
  422 + }
  423 +
  424 + @Nested
  425 + @DisplayName("设备属性条件测试 - Validates: Requirements 3.1, 3.2, 3.3")
  426 + class DevicePropertyConditionTest {
  427 +
  428 + @Test
  429 + @DisplayName("设备属性条件满足时,应匹配成功")
  430 + void testDevicePropertyCondition_match_shouldPass() {
  431 + IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition(
  432 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "25");
  433 +
  434 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = createSingleConditionGroups(condition);
  435 +
  436 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  437 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  438 + sceneRule.setTriggers(ListUtil.toList(trigger));
  439 +
  440 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  441 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  442 +
  443 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  444 +
  445 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  446 + }
  447 +
  448 + @Test
  449 + @DisplayName("设备属性不存在时,条件应不匹配")
  450 + void testDevicePropertyCondition_propertyNotExists_shouldNotMatch() {
  451 + IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition(
  452 + DEVICE_ID, "nonexistent", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "25");
  453 +
  454 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = createSingleConditionGroups(condition);
  455 +
  456 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  457 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  458 + sceneRule.setTriggers(ListUtil.toList(trigger));
  459 +
  460 + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(Collections.emptyMap());
  461 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  462 +
  463 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  464 +
  465 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  466 + }
  467 +
  468 + @Test
  469 + @DisplayName("设备属性等于条件测试")
  470 + void testDevicePropertyCondition_equals_shouldMatch() {
  471 + IotSceneRuleDO.TriggerCondition condition = createDevicePropertyCondition(
  472 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(), "30");
  473 +
  474 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = createSingleConditionGroups(condition);
  475 +
  476 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  477 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  478 + sceneRule.setTriggers(ListUtil.toList(trigger));
  479 +
  480 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  481 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  482 +
  483 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  484 +
  485 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  486 + }
  487 + }
  488 +
  489 + @Nested
  490 + @DisplayName("场景规则状态测试")
  491 + class SceneRuleStatusTest {
  492 +
  493 + @Test
  494 + @DisplayName("场景规则不存在时,应直接返回")
  495 + void testSceneRule_notExists_shouldReturn() {
  496 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(null);
  497 +
  498 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  499 +
  500 + verify(devicePropertyService, never()).getLatestDeviceProperties(any());
  501 + }
  502 +
  503 + @Test
  504 + @DisplayName("场景规则已禁用时,应直接返回")
  505 + void testSceneRule_disabled_shouldReturn() {
  506 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  507 + sceneRule.setStatus(CommonStatusEnum.DISABLE.getStatus());
  508 +
  509 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  510 +
  511 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  512 +
  513 + verify(devicePropertyService, never()).getLatestDeviceProperties(any());
  514 + }
  515 +
  516 + @Test
  517 + @DisplayName("场景规则无定时触发器时,应直接返回")
  518 + void testSceneRule_noTimerTrigger_shouldReturn() {
  519 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  520 + IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger();
  521 + deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType());
  522 + sceneRule.setTriggers(ListUtil.toList(deviceTrigger));
  523 +
  524 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  525 +
  526 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  527 +
  528 + verify(devicePropertyService, never()).getLatestDeviceProperties(any());
  529 + }
  530 + }
  531 +
  532 + @Nested
  533 + @DisplayName("复杂条件组合测试")
  534 + class ComplexConditionCombinationTest {
  535 +
  536 + @Test
  537 + @DisplayName("混合条件类型测试:设备属性 + 设备状态")
  538 + void testMixedConditionTypes_propertyAndState() {
  539 + IotSceneRuleDO.TriggerCondition propertyCondition = createDevicePropertyCondition(
  540 + DEVICE_ID, PROPERTY_IDENTIFIER, IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  541 + IotSceneRuleDO.TriggerCondition stateCondition = createDeviceStateCondition(
  542 + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  543 + String.valueOf(IotDeviceStateEnum.ONLINE.getState()));
  544 +
  545 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups =
  546 + createSingleGroupWithMultipleConditions(propertyCondition, stateCondition);
  547 +
  548 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  549 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  550 + sceneRule.setTriggers(ListUtil.toList(trigger));
  551 +
  552 + mockDeviceProperty(DEVICE_ID, PROPERTY_IDENTIFIER, 30);
  553 + mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState());
  554 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  555 +
  556 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  557 +
  558 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  559 + verify(deviceService, atLeastOnce()).getDevice(DEVICE_ID);
  560 + }
  561 +
  562 + @Test
  563 + @DisplayName("多条件组 OR 逻辑 + 组内 AND 逻辑综合测试")
  564 + void testComplexOrAndLogic() {
  565 + // 条件组1:温度 > 30 AND 湿度 < 50(不满足)
  566 + // 条件组2:温度 > 20 AND 设备在线(满足)
  567 + IotSceneRuleDO.TriggerCondition group1Cond1 = createDevicePropertyCondition(
  568 + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "30");
  569 + IotSceneRuleDO.TriggerCondition group1Cond2 = createDevicePropertyCondition(
  570 + DEVICE_ID, "humidity", IotSceneRuleConditionOperatorEnum.LESS_THAN.getOperator(), "50");
  571 +
  572 + IotSceneRuleDO.TriggerCondition group2Cond1 = createDevicePropertyCondition(
  573 + DEVICE_ID, "temperature", IotSceneRuleConditionOperatorEnum.GREATER_THAN.getOperator(), "20");
  574 + IotSceneRuleDO.TriggerCondition group2Cond2 = createDeviceStateCondition(
  575 + DEVICE_ID, IotSceneRuleConditionOperatorEnum.EQUALS.getOperator(),
  576 + String.valueOf(IotDeviceStateEnum.ONLINE.getState()));
  577 +
  578 + // 创建两个条件组
  579 + List<IotSceneRuleDO.TriggerCondition> group1 = new ArrayList<>();
  580 + group1.add(group1Cond1);
  581 + group1.add(group1Cond2);
  582 + List<IotSceneRuleDO.TriggerCondition> group2 = new ArrayList<>();
  583 + group2.add(group2Cond1);
  584 + group2.add(group2Cond2);
  585 + List<List<IotSceneRuleDO.TriggerCondition>> conditionGroups = new ArrayList<>();
  586 + conditionGroups.add(group1);
  587 + conditionGroups.add(group2);
  588 +
  589 + IotSceneRuleDO sceneRule = createBaseSceneRule();
  590 + IotSceneRuleDO.Trigger trigger = createTimerTrigger("0 0 12 * * ?", conditionGroups);
  591 + sceneRule.setTriggers(ListUtil.toList(trigger));
  592 +
  593 + // Mock:温度 25,湿度 60,设备在线
  594 + Map<String, IotDevicePropertyDO> properties = new HashMap<>();
  595 + IotDevicePropertyDO tempProperty = new IotDevicePropertyDO();
  596 + tempProperty.setValue(25);
  597 + properties.put("temperature", tempProperty);
  598 + IotDevicePropertyDO humidityProperty = new IotDevicePropertyDO();
  599 + humidityProperty.setValue(60);
  600 + properties.put("humidity", humidityProperty);
  601 + when(devicePropertyService.getLatestDeviceProperties(DEVICE_ID)).thenReturn(properties);
  602 + mockDeviceState(DEVICE_ID, IotDeviceStateEnum.ONLINE.getState());
  603 + when(sceneRuleMapper.selectById(SCENE_RULE_ID)).thenReturn(sceneRule);
  604 +
  605 + assertDoesNotThrow(() -> sceneRuleService.executeSceneRuleByTimer(SCENE_RULE_ID));
  606 +
  607 + verify(devicePropertyService, atLeastOnce()).getLatestDeviceProperties(DEVICE_ID);
  608 + }
  609 + }
  610 +
  611 +}