Commit a4be3bb84d0a24169f085a643edbd24eedb35aa5

Authored by YunaiV
1 parent 06749a18

【代码新增】IoT:增加 IotRuleSceneMessageHandler 处理规则场景,尝试基于 Spring El 表达式实现初步计算(部分场景) trigger 条件匹配

yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerConditionParameterOperatorEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.enums.rule;
  2 +
  3 +import cn.hutool.core.util.ArrayUtil;
  4 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  5 +import lombok.Getter;
  6 +import lombok.RequiredArgsConstructor;
  7 +
  8 +import java.util.Arrays;
  9 +
  10 +/**
  11 + * Iot 场景触发条件参数的操作符枚举
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@RequiredArgsConstructor
  16 +@Getter
  17 +public enum IotRuleSceneTriggerConditionParameterOperatorEnum implements ArrayValuable<String> {
  18 +
  19 + EQUALS("=", "%s == %s"),
  20 + NOT_EQUALS("!=", "%s != %s"),
  21 +
  22 + GREATER_THAN(">", "%s > %s"),
  23 + GREATER_THAN_OR_EQUALS(">=", "%s >= %s"),
  24 +
  25 + LESS_THAN("<", "%s < %s"),
  26 + LESS_THAN_OR_EQUALS("<=", "%s <= %s"),
  27 +
  28 + IN("in", "%s in { %s }"),
  29 + NOT_IN("not in", "%s not in { %s }"),
  30 +
  31 + BETWEEN("between", "(%s >= %s) && (%s <= %s)"),
  32 + NOT_BETWEEN("not between", "!(%s between %s and %s)"),
  33 +
  34 + LIKE("like", "%s like %s"), // 字符串匹配
  35 + NOT_NULL("not null", ""); // 非空
  36 +
  37 + private final String operator;
  38 + private final String springExpression;
  39 +
  40 + public static final String[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneTriggerConditionParameterOperatorEnum::getOperator).toArray(String[]::new);
  41 +
  42 + public static IotRuleSceneTriggerConditionParameterOperatorEnum operatorOf(String operator) {
  43 + return ArrayUtil.firstMatch(item -> item.getOperator().equals(operator), values());
  44 + }
  45 +
  46 + @Override
  47 + public String[] array() {
  48 + return ARRAYS;
  49 + }
  50 +
  51 +}
... ...
yudao-module-iot/yudao-module-iot-api/src/main/java/cn/iocoder/yudao/module/iot/enums/rule/IotRuleSceneTriggerTypeEnum.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.enums.rule;
  2 +
  3 +import cn.iocoder.yudao.framework.common.core.ArrayValuable;
  4 +import lombok.Getter;
  5 +import lombok.RequiredArgsConstructor;
  6 +
  7 +import java.util.Arrays;
  8 +
  9 +/**
  10 + * Iot 场景流转的触发类型枚举
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@RequiredArgsConstructor
  15 +@Getter
  16 +public enum IotRuleSceneTriggerTypeEnum implements ArrayValuable<Integer> {
  17 +
  18 + DEVICE(1), // 设备触发
  19 + TIMER(2); // 定时触发
  20 +
  21 + private final Integer type;
  22 +
  23 + public static final Integer[] ARRAYS = Arrays.stream(values()).map(IotRuleSceneTriggerTypeEnum::getType).toArray(Integer[]::new);
  24 +
  25 + @Override
  26 + public Integer[] array() {
  27 + return ARRAYS;
  28 + }
  29 +
  30 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/dataobject/rule/IotRuleSceneDO.java
... ... @@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
6 6 import cn.iocoder.yudao.module.iot.dal.dataobject.thingmodel.IotThingModelDO;
7 7 import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum;
8 8 import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum;
  9 +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerConditionParameterOperatorEnum;
9 10 import com.baomidou.mybatisplus.annotation.KeySequence;
10 11 import com.baomidou.mybatisplus.annotation.TableField;
11 12 import com.baomidou.mybatisplus.annotation.TableId;
... ... @@ -17,7 +18,7 @@ import java.util.List;
17 18 import java.util.Map;
18 19  
19 20 /**
20   - * IoT 场景联动 DO
  21 + * IoT 规则场景(场景联动) DO
21 22 *
22 23 * @author 芋道源码
23 24 */
... ... @@ -72,7 +73,7 @@ public class IotRuleSceneDO extends BaseDO {
72 73 /**
73 74 * 触发类型
74 75 *
75   - * TODO @芋艿:device、job
  76 + * 枚举 {@link cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum}
76 77 */
77 78 private Integer type;
78 79  
... ... @@ -147,12 +148,15 @@ public class IotRuleSceneDO extends BaseDO {
147 148 /**
148 149 * 操作符
149 150 *
150   - * TODO 芋艿:枚举
  151 + * 枚举 {@link IotRuleSceneTriggerConditionParameterOperatorEnum}
151 152 */
152 153 private String operator;
153 154  
154 155 /**
155   - * 值
  156 + * 比较值
  157 + *
  158 + * 如果有多个值,则使用 "," 分隔,类似 "1,2,3"。
  159 + * 例如说,{@link IotRuleSceneTriggerConditionParameterOperatorEnum#IN}、{@link IotRuleSceneTriggerConditionParameterOperatorEnum#BETWEEN}
156 160 */
157 161 private String value;
158 162  
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/dal/mysql/rule/IotRuleSceneMapper.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.dal.mysql.rule;
  2 +
  3 +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
  4 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO;
  5 +import org.apache.ibatis.annotations.Mapper;
  6 +
  7 +@Mapper
  8 +public interface IotRuleSceneMapper extends BaseMapperX<IotRuleSceneDO> {
  9 +
  10 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/mq/consumer/rule/IotRuleSceneMessageHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.mq.consumer.rule;
  2 +
  3 +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
  4 +import cn.iocoder.yudao.module.iot.service.rule.IotRuleSceneService;
  5 +import jakarta.annotation.Resource;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.context.event.EventListener;
  8 +import org.springframework.scheduling.annotation.Async;
  9 +import org.springframework.stereotype.Component;
  10 +
  11 +/**
  12 + * 针对 {@link IotDeviceMessage} 的消费者,处理规则场景
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@Component
  17 +@Slf4j
  18 +public class IotRuleSceneMessageHandler {
  19 +
  20 + @Resource
  21 + private IotRuleSceneService ruleSceneService;
  22 +
  23 + @EventListener
  24 + @Async
  25 + public void onMessage(IotDeviceMessage message) {
  26 + log.info("[onMessage][消息内容({})]", message);
  27 + ruleSceneService.executeRuleScene(message);
  28 + }
  29 +
  30 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneService.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule;
  2 +
  3 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO;
  4 +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
  5 +
  6 +import java.util.List;
  7 +
  8 +/**
  9 + * IoT 规则场景 Service 接口
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +public interface IotRuleSceneService {
  14 +
  15 + /**
  16 + * 【缓存】获得指定设备的场景列表
  17 + *
  18 + * @param productKey 产品 Key
  19 + * @param deviceName 设备名称
  20 + * @return 场景列表
  21 + */
  22 + List<IotRuleSceneDO> getRuleSceneListByProductKeyAndDeviceNameFromCache(String productKey, String deviceName);
  23 +
  24 + /**
  25 + * 执行规则场景
  26 + *
  27 + * @param message 消息
  28 + */
  29 + void executeRuleScene(IotDeviceMessage message);
  30 +
  31 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/IotRuleSceneServiceImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.collection.ListUtil;
  5 +import cn.hutool.core.text.CharPool;
  6 +import cn.hutool.core.util.ObjUtil;
  7 +import cn.hutool.core.util.StrUtil;
  8 +import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
  9 +import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
  10 +import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
  11 +import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotRuleSceneDO;
  12 +import cn.iocoder.yudao.module.iot.dal.mysql.rule.IotRuleSceneMapper;
  13 +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageIdentifierEnum;
  14 +import cn.iocoder.yudao.module.iot.enums.device.IotDeviceMessageTypeEnum;
  15 +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerConditionParameterOperatorEnum;
  16 +import cn.iocoder.yudao.module.iot.enums.rule.IotRuleSceneTriggerTypeEnum;
  17 +import cn.iocoder.yudao.module.iot.mq.message.IotDeviceMessage;
  18 +import jakarta.annotation.Resource;
  19 +import lombok.extern.slf4j.Slf4j;
  20 +import org.springframework.stereotype.Service;
  21 +import org.springframework.validation.annotation.Validated;
  22 +
  23 +import java.util.List;
  24 +import java.util.Map;
  25 +
  26 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
  27 +
  28 +/**
  29 + * IoT 规则场景 Service 实现类
  30 + *
  31 + * @author 芋道源码
  32 + */
  33 +@Service
  34 +@Validated
  35 +@Slf4j
  36 +public class IotRuleSceneServiceImpl implements IotRuleSceneService {
  37 +
  38 + @Resource
  39 + private IotRuleSceneMapper ruleSceneMapper;
  40 +
  41 + // TODO 芋艿,缓存待实现
  42 + @Override
  43 + @TenantIgnore // 忽略租户隔离:因为 IotRuleSceneMessageHandler 调用时,一般未传递租户,所以需要忽略
  44 + public List<IotRuleSceneDO> getRuleSceneListByProductKeyAndDeviceNameFromCache(String productKey, String deviceName) {
  45 + if (true) {
  46 + IotRuleSceneDO ruleScene01 = new IotRuleSceneDO();
  47 + ruleScene01.setTriggers(CollUtil.newArrayList());
  48 + IotRuleSceneDO.Trigger trigger01 = new IotRuleSceneDO.Trigger();
  49 + trigger01.setType(IotRuleSceneTriggerTypeEnum.DEVICE.getType());
  50 + trigger01.setConditions(CollUtil.newArrayList());
  51 + IotRuleSceneDO.TriggerCondition condition01 = new IotRuleSceneDO.TriggerCondition();
  52 + condition01.setType(IotDeviceMessageTypeEnum.PROPERTY.getType());
  53 + condition01.setIdentifier(IotDeviceMessageIdentifierEnum.PROPERTY_REPORT.getIdentifier());
  54 + condition01.setParameters(CollUtil.newArrayList());
  55 + IotRuleSceneDO.TriggerConditionParameter parameter011 = new IotRuleSceneDO.TriggerConditionParameter();
  56 + parameter011.setIdentifier("width");
  57 + parameter011.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.EQUALS.getOperator());
  58 + parameter011.setValue("1");
  59 + condition01.getParameters().add(parameter011);
  60 + IotRuleSceneDO.TriggerConditionParameter parameter012 = new IotRuleSceneDO.TriggerConditionParameter();
  61 + parameter012.setIdentifier("width");
  62 + parameter012.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_EQUALS.getOperator());
  63 + parameter012.setValue("2");
  64 + condition01.getParameters().add(parameter012);
  65 + IotRuleSceneDO.TriggerConditionParameter parameter013 = new IotRuleSceneDO.TriggerConditionParameter();
  66 + parameter013.setIdentifier("width");
  67 + parameter013.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN.getOperator());
  68 + parameter013.setValue("0");
  69 + condition01.getParameters().add(parameter013);
  70 + IotRuleSceneDO.TriggerConditionParameter parameter014 = new IotRuleSceneDO.TriggerConditionParameter();
  71 + parameter014.setIdentifier("width");
  72 + parameter014.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.GREATER_THAN_OR_EQUALS.getOperator());
  73 + parameter014.setValue("0");
  74 + condition01.getParameters().add(parameter014);
  75 + IotRuleSceneDO.TriggerConditionParameter parameter015 = new IotRuleSceneDO.TriggerConditionParameter();
  76 + parameter015.setIdentifier("width");
  77 + parameter015.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN.getOperator());
  78 + parameter015.setValue("2");
  79 + condition01.getParameters().add(parameter015);
  80 + IotRuleSceneDO.TriggerConditionParameter parameter016 = new IotRuleSceneDO.TriggerConditionParameter();
  81 + parameter016.setIdentifier("width");
  82 + parameter016.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.LESS_THAN_OR_EQUALS.getOperator());
  83 + parameter016.setValue("2");
  84 + condition01.getParameters().add(parameter016);
  85 + IotRuleSceneDO.TriggerConditionParameter parameter017 = new IotRuleSceneDO.TriggerConditionParameter();
  86 + parameter017.setIdentifier("width");
  87 + parameter017.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.IN.getOperator());
  88 + parameter017.setValue("1,2,3");
  89 + condition01.getParameters().add(parameter017);
  90 + IotRuleSceneDO.TriggerConditionParameter parameter018 = new IotRuleSceneDO.TriggerConditionParameter();
  91 + parameter018.setIdentifier("width");
  92 + parameter018.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_IN.getOperator());
  93 + parameter018.setValue("0,2,3");
  94 + condition01.getParameters().add(parameter018);
  95 + IotRuleSceneDO.TriggerConditionParameter parameter019 = new IotRuleSceneDO.TriggerConditionParameter();
  96 + parameter019.setIdentifier("width");
  97 + parameter019.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN.getOperator());
  98 + parameter019.setValue("1,3");
  99 + condition01.getParameters().add(parameter019);
  100 + IotRuleSceneDO.TriggerConditionParameter parameter020 = new IotRuleSceneDO.TriggerConditionParameter();
  101 + parameter020.setIdentifier("width");
  102 + parameter020.setOperator(IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN.getOperator());
  103 + parameter020.setValue("2,3");
  104 + condition01.getParameters().add(parameter020);
  105 + trigger01.getConditions().add(condition01);
  106 + ruleScene01.getTriggers().add(trigger01);
  107 +
  108 + return ListUtil.toList(ruleScene01);
  109 + }
  110 +
  111 + List<IotRuleSceneDO> list = ruleSceneMapper.selectList();
  112 + // TODO @芋艿:需要考虑开启状态
  113 + return filterList(list, ruleScene -> {
  114 + for (IotRuleSceneDO.Trigger trigger : ruleScene.getTriggers()) {
  115 + if (ObjUtil.notEqual(trigger.getProductKey(), productKey)) {
  116 + continue;
  117 + }
  118 + if (CollUtil.isEmpty(trigger.getDeviceNames()) // 无设备名称限制
  119 + || trigger.getDeviceNames().contains(deviceName)) { // 包含设备名称
  120 + return true;
  121 + }
  122 + }
  123 + return false;
  124 + });
  125 + }
  126 +
  127 + @Override
  128 + public void executeRuleScene(IotDeviceMessage message) {
  129 + // 1. 获得设备匹配的规则场景
  130 + List<IotRuleSceneDO> ruleScenes = getMatchedRuleSceneList(message);
  131 + if (CollUtil.isEmpty(ruleScenes)) {
  132 + return;
  133 + }
  134 + }
  135 +
  136 + /**
  137 + * 获得匹配的规则场景列表
  138 + *
  139 + * @param message 设备消息
  140 + * @return 规则场景列表
  141 + */
  142 + @SuppressWarnings("unchecked")
  143 + private List<IotRuleSceneDO> getMatchedRuleSceneList(IotDeviceMessage message) {
  144 + // 1. 匹配设备
  145 + // TODO @芋艿:可能需要 getSelf(); 缓存
  146 + List<IotRuleSceneDO> ruleScenes = getRuleSceneListByProductKeyAndDeviceNameFromCache(
  147 + message.getProductKey(), message.getDeviceName());
  148 + if (CollUtil.isEmpty(ruleScenes)) {
  149 + return ruleScenes;
  150 + }
  151 +
  152 + // 2. 匹配 trigger 触发器的条件
  153 + return filterList(ruleScenes, ruleScene -> {
  154 + for (IotRuleSceneDO.Trigger trigger : ruleScene.getTriggers()) {
  155 + // 非设备触发,不匹配
  156 + if (ObjUtil.notEqual(trigger.getType(), IotRuleSceneTriggerTypeEnum.DEVICE.getType())) {
  157 + return false;
  158 + }
  159 + // TODO 芋艿:产品、设备的匹配,要不要这里在做一次???貌似和 1. 部分重复了
  160 + // 条件为空,说明没有匹配的条件,因此不匹配
  161 + if (CollUtil.isEmpty(trigger.getConditions())) {
  162 + return false;
  163 + }
  164 + IotRuleSceneDO.TriggerCondition found = CollUtil.findOne(trigger.getConditions(), condition -> {
  165 + if (ObjUtil.notEqual(message.getType(), condition.getType())
  166 + || ObjUtil.notEqual(message.getIdentifier(), condition.getIdentifier())) {
  167 + return false;
  168 + }
  169 + // TODO @芋艿:设备上线,需要测试下。
  170 + for (IotRuleSceneDO.TriggerConditionParameter parameter : condition.getParameters()) {
  171 + // 计算是否匹配
  172 + IotRuleSceneTriggerConditionParameterOperatorEnum operator =
  173 + IotRuleSceneTriggerConditionParameterOperatorEnum.operatorOf(parameter.getOperator());
  174 + if (operator == null) {
  175 + log.error("[getMatchedRuleSceneList][规则场景编号({}) 的触发器({}) 存在错误的操作符({})]",
  176 + ruleScene.getId(), trigger, parameter.getOperator());
  177 + return false;
  178 + }
  179 + Object messageValue = ((Map<String, Object>) message.getData()).get(parameter.getIdentifier());
  180 + if (messageValue == null) {
  181 + return false;
  182 + }
  183 + String springExpression;
  184 + if (ObjectUtils.equalsAny(operator, IotRuleSceneTriggerConditionParameterOperatorEnum.BETWEEN,
  185 + IotRuleSceneTriggerConditionParameterOperatorEnum.NOT_BETWEEN)) {
  186 + String[] parameterValues = StrUtil.splitToArray(parameter.getValue(), CharPool.COMMA);
  187 + springExpression = String.format(operator.getSpringExpression(), messageValue, parameterValues[0],
  188 + messageValue, parameterValues[1]);
  189 + } else {
  190 + springExpression = String.format(operator.getSpringExpression(), messageValue, parameter.getValue());
  191 + }
  192 + // TODO @芋艿:【需优化】需要考虑 struct、时间等参数的比较
  193 + try {
  194 + System.out.println(SpringExpressionUtils.parseExpression(springExpression));
  195 + } catch (Exception e) {
  196 + log.error("[getMatchedRuleSceneList][消息({}) 规则场景编号({}) 的触发器({}) 的匹配表达式({}) 计算异常]",
  197 + message, ruleScene.getId(), trigger, springExpression, e);
  198 + }
  199 + }
  200 + return true;
  201 + });
  202 + if (found == null) {
  203 + return false;
  204 + }
  205 + log.info("[getMatchedRuleSceneList][消息({}) 匹配到规则场景编号({}) 的触发器({})]", message, ruleScene.getId(), trigger);
  206 + return true;
  207 + }
  208 + return false;
  209 + });
  210 + }
  211 +
  212 +}
... ...