Commit 4901912ece006720db02fc828bef4f0a810967e2
1 parent
e765099a
feat(iot):【场景联动】定时触发,增加条件组
Showing
5 changed files
with
1102 additions
and
162 deletions
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 | +} |