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 | 23 | import cn.iocoder.yudao.module.iot.service.rule.scene.action.IotSceneRuleAction; |
| 24 | 24 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherManager; |
| 25 | 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 | 27 | import jakarta.annotation.Resource; |
| 27 | 28 | import lombok.extern.slf4j.Slf4j; |
| 28 | 29 | import org.springframework.cache.annotation.CacheEvict; |
| ... | ... | @@ -62,6 +63,8 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 62 | 63 | private List<IotSceneRuleAction> sceneRuleActions; |
| 63 | 64 | @Resource |
| 64 | 65 | private IotSceneRuleTimerHandler timerHandler; |
| 66 | + @Resource | |
| 67 | + private IotTimerConditionEvaluator timerConditionEvaluator; | |
| 65 | 68 | |
| 66 | 69 | @Override |
| 67 | 70 | @CacheEvict(value = RedisKeyConstants.SCENE_RULE_LIST, allEntries = true) |
| ... | ... | @@ -222,19 +225,99 @@ public class IotSceneRuleServiceImpl implements IotSceneRuleService { |
| 222 | 225 | return; |
| 223 | 226 | } |
| 224 | 227 | // 1.2 判断是否有定时触发器,避免脏数据 |
| 225 | - IotSceneRuleDO.Trigger config = CollUtil.findOne(scene.getTriggers(), | |
| 228 | + IotSceneRuleDO.Trigger timerTrigger = CollUtil.findOne(scene.getTriggers(), | |
| 226 | 229 | trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType())); |
| 227 | - if (config == null) { | |
| 230 | + if (timerTrigger == null) { | |
| 228 | 231 | log.error("[executeSceneRuleByTimer][规则场景({}) 不存在定时触发器]", scene); |
| 229 | 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 | 244 | TenantUtils.execute(scene.getTenantId(), |
| 234 | 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 | 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 | 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 | 3 | import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; |
| 7 | 4 | import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO; |
| 8 | 5 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionOperatorEnum; |
| 9 | 6 | import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleConditionTypeEnum; |
| 7 | +import cn.iocoder.yudao.module.iot.service.rule.scene.IotSceneRuleTimeHelper; | |
| 10 | 8 | import cn.iocoder.yudao.module.iot.service.rule.scene.matcher.IotSceneRuleMatcherHelper; |
| 11 | 9 | import lombok.extern.slf4j.Slf4j; |
| 12 | 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 | 18 | @Slf4j |
| 26 | 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 | 21 | @Override |
| 39 | 22 | public IotSceneRuleConditionTypeEnum getSupportedConditionType() { |
| 40 | 23 | return IotSceneRuleConditionTypeEnum.CURRENT_TIME; |
| ... | ... | @@ -62,13 +45,13 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc |
| 62 | 45 | return false; |
| 63 | 46 | } |
| 64 | 47 | |
| 65 | - if (!isTimeOperator(operatorEnum)) { | |
| 48 | + if (IotSceneRuleTimeHelper.isTimeOperator(operatorEnum)) { | |
| 66 | 49 | IotSceneRuleMatcherHelper.logConditionMatchFailure(message, condition, "不支持的时间操作符: " + operator); |
| 67 | 50 | return false; |
| 68 | 51 | } |
| 69 | 52 | |
| 70 | 53 | // 2.1 执行时间匹配 |
| 71 | - boolean matched = executeTimeMatching(operatorEnum, condition.getParam()); | |
| 54 | + boolean matched = IotSceneRuleTimeHelper.executeTimeMatching(operatorEnum, condition.getParam()); | |
| 72 | 55 | |
| 73 | 56 | // 2.2 记录匹配结果 |
| 74 | 57 | if (matched) { |
| ... | ... | @@ -80,145 +63,6 @@ public class IotCurrentTimeConditionMatcher implements IotSceneRuleConditionMatc |
| 80 | 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 | 66 | @Override |
| 223 | 67 | public int getPriority() { |
| 224 | 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 | +} | ... | ... |