From 910bb6ca3c06eeae4d97b665ca61991722013bb0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 2 Feb 2025 22:08:34 +0800 Subject: [PATCH] =?UTF-8?q?=E3=80=90=E4=BB=A3=E7=A0=81=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E3=80=91IoT=EF=BC=9A=E5=AE=8C=E5=96=84=20IotRuleSceneServiceIm?= =?UTF-8?q?pl=20=E7=9A=84=E8=A7=84=E5=88=99=E5=8C=B9=E9=85=8D=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=EF=BC=8CisTriggerConditionParameterMatched=20?= =?UTF-8?q?=E5=87=BD=E6=95=B0=E6=9C=89=E7=82=B9=E9=95=BF=EF=BC=8C=3D=20=3D?= =?UTF-8?q?=20=E6=8D=89=E6=91=B8=E5=92=8B=E4=BC=98=E5=8C=96=E4=B8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/util/number/NumberUtils.java | 14 ++ .../util/spring/SpringExpressionUtils.java | 14 ++ ...TriggerConditionParameterOperatorEnum.java | 37 +++-- .../service/rule/IotRuleSceneServiceImpl.java | 138 +++++++++++++----- 4 files changed, 153 insertions(+), 50 deletions(-) diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java index c928e2fcfd..c2787f46f1 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/number/NumberUtils.java @@ -1,9 +1,11 @@ package cn.iocoder.yudao.framework.common.util.number; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.StrUtil; import java.math.BigDecimal; +import java.util.List; /** * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 @@ -20,6 +22,18 @@ public class NumberUtils { return StrUtil.isNotEmpty(str) ? Integer.valueOf(str) : null; } + public static boolean isAllNumber(List values) { + if (CollUtil.isEmpty(values)) { + return false; + } + for (String value : values) { + if (!NumberUtil.isNumber(value)) { + return false; + } + } + return true; + } + /** * 通过经纬度获取地球上两点之间的距离 * diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java index 069e89db3d..abb1cece85 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringExpressionUtils.java @@ -97,12 +97,26 @@ public class SpringExpressionUtils { * @return 执行界面 */ public static Object parseExpression(String expressionString) { + return parseExpression(expressionString, null); + } + + /** + * 从 Bean 工厂,解析 EL 表达式的结果 + * + * @param expressionString EL 表达式 + * @param variables 变量 + * @return 执行界面 + */ + public static Object parseExpression(String expressionString, Map variables) { if (StrUtil.isBlank(expressionString)) { return null; } Expression expression = EXPRESSION_PARSER.parseExpression(expressionString); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new BeanFactoryResolver(SpringUtil.getApplicationContext())); + if (MapUtil.isNotEmpty(variables)) { + context.setVariables(variables); + } return expression.getValue(context); } diff --git a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java index f1a78cf31b..1aac8c2371 100644 --- a/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java +++ b/yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java @@ -16,29 +16,42 @@ import java.util.Arrays; @Getter public enum IotRuleSceneTriggerConditionParameterOperatorEnum implements ArrayValuable { - EQUALS("=", "%s == %s"), - NOT_EQUALS("!=", "%s != %s"), + EQUALS("=", "#source == #value"), + NOT_EQUALS("!=", "!(#source == #value)"), - GREATER_THAN(">", "%s > %s"), - GREATER_THAN_OR_EQUALS(">=", "%s >= %s"), + GREATER_THAN(">", "#source > #value"), + GREATER_THAN_OR_EQUALS(">=", "#source >= #value"), - LESS_THAN("<", "%s < %s"), - LESS_THAN_OR_EQUALS("<=", "%s <= %s"), + LESS_THAN("<", "#source < #value"), + LESS_THAN_OR_EQUALS("<=", "#source <= #value"), - IN("in", "%s in { %s }"), - NOT_IN("not in", "%s not in { %s }"), + IN("in", "#values.contains(#source)"), + NOT_IN("not in", "!(#values.contains(#source))"), - BETWEEN("between", "(%s >= %s) && (%s <= %s)"), - NOT_BETWEEN("not between", "!(%s between %s and %s)"), + BETWEEN("between", "(#source >= #values.get(0)) && (#source <= #values.get(1))"), + NOT_BETWEEN("not between", "(#source < #values.get(0)) || (#source > #values.get(1))"), - LIKE("like", "%s like %s"), // 字符串匹配 - NOT_NULL("not null", ""); // 非空 + LIKE("like", "#source.contains(#value)"), // 字符串匹配 + NOT_NULL("not null", "#source != null && #source.length() > 0"); // 非空 private final String operator; private final String springExpression; public static final String[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneTriggerConditionParameterOperatorEnum::getOperator).toArray(String[]::new); + /** + * Spring 表达式 - 原始值 + */ + public static final String SPRING_EXPRESSION_SOURCE = "source"; + /** + * Spring 表达式 - 目标值 + */ + public static final String SPRING_EXPRESSION_VALUE = "value"; + /** + * Spring 表达式 - 目标值数组 + */ + public static final String SPRING_EXPRESSION_VALUE_List = "values"; + public static IotRuleSceneTriggerConditionParameterOperatorEnum operatorOf(String operator) { return ArrayUtil.firstMatch(item -> item.getOperator().equals(operator), values()); } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java index 0d8d0cdddf..90f893ef20 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java @@ -2,9 +2,12 @@ package cn.iocoder.yudao.module.iot.service.rule; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.text.CharPool; +import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; @@ -20,9 +23,11 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; +import java.util.HashMap; import java.util.List; import java.util.Map; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList; /** @@ -48,10 +53,16 @@ public class IotRuleSceneServiceImpl implements IotRuleSceneService { IotRuleSceneDO.Trigger trigger01 = new IotRuleSceneDO.Trigger(); trigger01.setType(IotRuleSceneTriggerTypeEnum.DEVICE.getType()); trigger01.setConditions(CollUtil.newArrayList()); + // 属性 IotRuleSceneDO.TriggerCondition condition01 = new IotRuleSceneDO.TriggerCondition(); condition01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType()); condition01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier()); condition01.setParameters(CollUtil.newArrayList()); +// IotRuleSceneDO.TriggerConditionParameter parameter010 = new IotRuleSceneDO.TriggerConditionParameter(); +// parameter010.setIdentifier("width"); +// parameter010.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); +// parameter010.setValue("abc"); +// condition01.getParameters().add(parameter010); IotRuleSceneDO.TriggerConditionParameter parameter011 = new IotRuleSceneDO.TriggerConditionParameter(); parameter011.setIdentifier("width"); parameter011.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); @@ -103,8 +114,23 @@ public class IotRuleSceneServiceImpl implements IotRuleSceneService { parameter020.setValue("2,3"); condition01.getParameters().add(parameter020); trigger01.getConditions().add(condition01); + // 状态 + IotRuleSceneDO.TriggerCondition condition02 = new IotRuleSceneDO.TriggerCondition(); + condition02.setType(IotDeviceMessageTypeEnum.STATE.getType()); + condition02.setIdentifier(IotDeviceMessageIdentifierEnum.STATE_ONLINE.getIdentifier()); + condition02.setParameters(CollUtil.newArrayList()); + trigger01.getConditions().add(condition02); + // TODO 芋艿:事件 + IotRuleSceneDO.TriggerCondition condition03 = new IotRuleSceneDO.TriggerCondition(); + condition03.setType(IotDeviceMessageTypeEnum.EVENT.getType()); + condition03.setIdentifier("xxx"); + condition03.setParameters(CollUtil.newArrayList()); + IotRuleSceneDO.TriggerConditionParameter parameter030 = new IotRuleSceneDO.TriggerConditionParameter(); + parameter030.setIdentifier("width"); + parameter030.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator()); + parameter030.setValue("1"); + trigger01.getConditions().add(condition03); ruleScene01.getTriggers().add(trigger01); - return ListUtil.toList(ruleScene01); } @@ -139,7 +165,6 @@ public class IotRuleSceneServiceImpl implements IotRuleSceneService { * @param message 设备消息 * @return 规则场景列表 */ - @SuppressWarnings("unchecked") private List getMatchedRuleSceneList(IotDeviceMessage message) { // 1. 匹配设备 // TODO @芋艿:可能需要 getSelf(); 缓存 @@ -152,54 +177,27 @@ public class IotRuleSceneServiceImpl implements IotRuleSceneService { // 2. 匹配 trigger 触发器的条件 return filterList(ruleScenes, ruleScene -> { for (IotRuleSceneDO.Trigger trigger : ruleScene.getTriggers()) { - // 非设备触发,不匹配 + // 2.1 非设备触发,不匹配 if (ObjUtil.notEqual(trigger.getType(), IotRuleSceneTriggerTypeEnum.DEVICE.getType())) { return false; } // TODO 芋艿:产品、设备的匹配,要不要这里在做一次???貌似和 1. 部分重复了 - // 条件为空,说明没有匹配的条件,因此不匹配 + // 2.2 条件为空,说明没有匹配的条件,因此不匹配 if (CollUtil.isEmpty(trigger.getConditions())) { return false; } - IotRuleSceneDO.TriggerCondition found = CollUtil.findOne(trigger.getConditions(), condition -> { + // 2.3 多个条件,只需要满足一个即可 + IotRuleSceneDO.TriggerCondition matchedCondition = CollUtil.findOne(trigger.getConditions(), condition -> { if (ObjUtil.notEqual(message.getType(), condition.getType()) || ObjUtil.notEqual(message.getIdentifier(), condition.getIdentifier())) { return false; } - // TODO @芋艿:设备上线,需要测试下。 - for (IotRuleSceneDO.TriggerConditionParameter parameter : condition.getParameters()) { - // 计算是否匹配 - IotRuleSceneTriggerConditionParameterOperatorEnum operator = - IotRuleSceneTriggerConditionParameterOperatorEnum.operatorOf(parameter.getOperator()); - if (operator == null) { - log.error("[getMatchedRuleSceneList][规则场景编号({}) 的触发器({}) 存在错误的操作符({})]", - ruleScene.getId(), trigger, parameter.getOperator()); - return false; - } - Object messageValue = ((Map) message.getData()).get(parameter.getIdentifier()); - if (messageValue == null) { - return false; - } - String springExpression; - if (ObjectUtils.equalsAny(operator, IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN, - IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN)) { - String[] parameterValues = StrUtil.splitToArray(parameter.getValue(), CharPool.COMMA); - springExpression = String.format(operator.getSpringExpression(), messageValue, parameterValues[0], - messageValue, parameterValues[1]); - } else { - springExpression = String.format(operator.getSpringExpression(), messageValue, parameter.getValue()); - } - // TODO @芋艿:【需优化】需要考虑 struct、时间等参数的比较 - try { - System.out.println(SpringExpressionUtils.parseExpression(springExpression)); - } catch (Exception e) { - log.error("[getMatchedRuleSceneList][消息({}) 规则场景编号({}) 的触发器({}) 的匹配表达式({}) 计算异常]", - message, ruleScene.getId(), trigger, springExpression, e); - } - } - return true; + // 多个条件参数,必须全部满足。所以,下面的逻辑就是找到一个不满足的条件参数 + IotRuleSceneDO.TriggerConditionParameter notMatchedParameter = CollUtil.findOne(condition.getParameters(), + parameter -> !isTriggerConditionParameterMatched(message, parameter, ruleScene, trigger)); + return notMatchedParameter == null; }); - if (found == null) { + if (matchedCondition == null) { return false; } log.info("[getMatchedRuleSceneList][消息({}) 匹配到规则场景编号({}) 的触发器({})]", message, ruleScene.getId(), trigger); @@ -209,4 +207,68 @@ public class IotRuleSceneServiceImpl implements IotRuleSceneService { }); } + // TODO @芋艿:【可优化】可以考虑增加下单测,边界太多了。 + /** + * 判断触发器的条件参数是否匹配 + * + * @param message 设备消息 + * @param parameter 触发器条件参数 + * @param ruleScene 规则场景(用于日志,无其它作用) + * @param trigger 触发器(用于日志,无其它作用) + * @return 是否匹配 + */ + @SuppressWarnings({"unchecked", "DataFlowIssue"}) + private boolean isTriggerConditionParameterMatched(IotDeviceMessage message, IotRuleSceneDO.TriggerConditionParameter parameter, + IotRuleSceneDO ruleScene, IotRuleSceneDO.Trigger trigger) { + // 计算是否匹配 + IotRuleSceneTriggerConditionParameterOperatorEnum operator = + IotRuleSceneTriggerConditionParameterOperatorEnum.operatorOf(parameter.getOperator()); + if (operator == null) { + log.error("[isTriggerConditionParameterMatched][规则场景编号({}) 的触发器({}) 存在错误的操作符({})]", + ruleScene.getId(), trigger, parameter.getOperator()); + return false; + } + // TODO @芋艿:目前只支持方便转换成 string 的类型,如果是 struct、list 这种,需要考虑下 + String messageValue = MapUtil.getStr((Map) message.getData(), parameter.getIdentifier()); + if (messageValue == null) { + return false; + } + Map springExpressionVariables = new HashMap<>(); + try { + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_SOURCE, messageValue); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE, parameter.getValue()); + if (ObjectUtils.equalsAny(operator, IotRuleSceneTriggerConditionParameterOperatorEnum.IN, + IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN)) { + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE_List, + StrUtil.split(parameter.getValue(), CharPool.COMMA)); + } else if (ObjectUtils.equalsAny(operator, IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN, + IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN)) { + List parameterValues = StrUtil.splitTrim(parameter.getValue(), CharPool.COMMA); + if (NumberUtil.isNumber(messageValue) && NumberUtils.isAllNumber(parameterValues)) { // 特殊:解决数字的比较 + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_SOURCE, + NumberUtil.parseDouble(messageValue)); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE_List, + convertList(parameterValues, NumberUtil::parseDouble)); + } else { + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE_List, parameterValues); + } + } else if (ObjectUtils.equalsAny(operator, IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN, + IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS, + IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN, + IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS)) { + if (NumberUtil.isNumber(messageValue) && NumberUtil.isNumber(parameter.getValue())) { // 特殊:解决数字的比较 + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_SOURCE, + NumberUtil.parseDouble(messageValue)); + springExpressionVariables.put(IotRuleSceneTriggerConditionParameterOperatorEnum.SPRING_EXPRESSION_VALUE, + NumberUtil.parseDouble(parameter.getValue())); + } + } + return (Boolean) SpringExpressionUtils.parseExpression(operator.getSpringExpression(), springExpressionVariables); + } catch (Exception e) { + log.error("[isTriggerConditionParameterMatched][消息({}) 规则场景编号({}) 的触发器({}) 的匹配表达式({}/{}) 计算异常]", + message, ruleScene.getId(), trigger, operator, springExpressionVariables, e); + return false; + } + } + }