From f7547406469514ef38d1292ab36850a7dc92410a Mon Sep 17 00:00:00 2001 From: liusq <106655480+liu4410@users.noreply.github.com> Date: Wed, 21 Aug 2024 10:58:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(rule-engine):=20=E5=91=8A=E8=AD=A6?= =?UTF-8?q?=E5=9C=BA=E6=99=AF2.2=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=20(#552)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jetlinks-components/common-component/pom.xml | 11 + .../command/rule/data/AlarmInfo.java | 68 ++++ .../command/rule/data/AlarmResult.java | 36 ++ .../command/rule/data/RelieveInfo.java | 29 ++ .../command/rule/data/RelieveResult.java | 32 ++ .../configuration/CommonConfiguration.java | 29 +- .../dictionary/DatabaseDictionaryManager.java | 88 +++++ .../community/dictionary/Dictionaries.java | 94 +++++ .../community/dictionary/Dictionary.java | 71 ++++ .../DictionaryColumnCustomizer.java | 57 +++ .../dictionary/DictionaryConfiguration.java | 48 +++ .../dictionary/DictionaryConstants.java | 9 + .../dictionary/DictionaryEventHandler.java | 95 +++++ .../dictionary/DictionaryInitInfo.java | 23 ++ .../dictionary/DictionaryInitManager.java | 81 +++++ .../DictionaryJsonDeserializer.java | 40 +++ .../dictionary/DictionaryManager.java | 41 +++ .../community/form/type/EnumFieldType.java | 204 +++++++++++ .../community/form/type/FieldType.java | 28 ++ .../form/type/FieldTypeProvider.java | 31 ++ .../community/form/type/FieldTypeSpec.java | 26 ++ .../reactorql/term/TermTypeSupport.java | 77 +++++ .../org/jetlinks/community/spi/Provider.java | 117 +++++++ .../community/spi/SimpleProvider.java | 98 ++++++ .../jetlinks/community/terms/I18nSpec.java | 131 +++++++ .../jetlinks/community/terms/TermSpec.java | 286 +++++++++++++++ .../org/jetlinks/community/topic/Topics.java | 68 +++- .../community/utils/ReactorUtils.java | 7 +- .../rule/engine/commons/ShakeLimit.java | 26 +- .../engine/commons/ShakeLimitProvider.java | 37 ++ .../rule/engine/commons/ShakeLimitResult.java | 16 + .../impl/SimpleShakeLimitProvider.java | 87 +++++ jetlinks-manager/rule-engine-manager/pom.xml | 4 + .../rule/engine/alarm/AlarmConstants.java | 5 + .../rule/engine/alarm/AlarmHandleInfo.java | 6 +- .../rule/engine/alarm/AlarmHandler.java | 30 ++ .../rule/engine/alarm/AlarmRuleHandler.java | 8 +- .../engine/alarm/DefaultAlarmHandler.java | 277 +++++++++++++++ .../engine/alarm/DefaultAlarmRuleHandler.java | 313 +++++------------ .../RuleEngineManagerConfiguration.java | 1 - .../rule/engine/entity/AlarmConfigDetail.java | 10 +- .../rule/engine/entity/AlarmConfigEntity.java | 11 + .../entity/AlarmHandleHistoryEntity.java | 26 +- .../rule/engine/entity/AlarmHistoryInfo.java | 25 +- .../rule/engine/entity/AlarmRecordEntity.java | 43 ++- .../engine/entity/AlarmRuleBindEntity.java | 6 +- .../engine/entity/RuleInstanceEntity.java | 22 +- .../rule/engine/entity/SceneEntity.java | 48 ++- .../rule/engine/enums/AlarmHandleType.java | 7 +- .../rule/engine/enums/RuleInstanceState.java | 4 +- .../scene/AbstractSceneTriggerProvider.java | 39 +++ .../rule/engine/scene/DeviceOperation.java | 123 ++++--- .../rule/engine/scene/SceneAction.java | 44 ++- .../engine/scene/SceneActionProvider.java | 13 +- .../engine/scene/SceneConditionAction.java | 6 - .../rule/engine/scene/SceneRule.java | 325 +++++++++++++----- .../scene/SceneTaskExecutorProvider.java | 48 ++- .../engine/scene/SceneTriggerProvider.java | 109 +++++- .../rule/engine/scene/SceneUtils.java | 194 +++++++++-- .../community/rule/engine/scene/Trigger.java | 37 +- .../rule/engine/scene/TriggerType.java | 5 + .../community/rule/engine/scene/Variable.java | 25 +- .../scene/internal/actions/AlarmAction.java | 6 + .../internal/triggers/DeviceTrigger.java | 117 +------ .../triggers/DeviceTriggerProvider.java | 14 +- .../triggers/ManualTriggerProvider.java | 8 +- .../triggers/TimerTriggerProvider.java | 4 +- .../rule/engine/scene/term/TermColumn.java | 118 ++++++- .../engine/service/AlarmConfigService.java | 2 +- .../service/AlarmHandleTypeDictInit.java | 53 +++ .../engine/service/AlarmRecordService.java | 6 +- .../ElasticSearchAlarmHistoryService.java | 12 +- .../service/LocalRuleInstanceRepository.java | 74 ++++ .../rule/engine/service/SceneService.java | 25 +- .../engine/web/AlarmRuleBindController.java | 25 ++ .../rule/engine/web/SceneController.java | 2 - .../engine/web/response/SelectorInfo.java | 38 ++ jetlinks-standalone/pom.xml | 1 - pom.xml | 7 + 79 files changed, 3728 insertions(+), 689 deletions(-) create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmInfo.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmResult.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveInfo.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveResult.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DatabaseDictionaryManager.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/Dictionaries.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/Dictionary.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryColumnCustomizer.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryConfiguration.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryConstants.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryEventHandler.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryInitInfo.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryInitManager.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryJsonDeserializer.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryManager.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/EnumFieldType.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldType.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldTypeProvider.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldTypeSpec.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/spi/Provider.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/spi/SimpleProvider.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/I18nSpec.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/TermSpec.java create mode 100644 jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitProvider.java create mode 100644 jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitResult.java create mode 100644 jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/impl/SimpleShakeLimitProvider.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandler.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmHandler.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/AbstractSceneTriggerProvider.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleTypeDictInit.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/LocalRuleInstanceRepository.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SelectorInfo.java diff --git a/jetlinks-components/common-component/pom.xml b/jetlinks-components/common-component/pom.xml index 60726206..5dc91ef8 100644 --- a/jetlinks-components/common-component/pom.xml +++ b/jetlinks-components/common-component/pom.xml @@ -56,6 +56,17 @@ h2-mvstore + + org.jetlinks.sdk + jetlinks-sdk-api + ${jetlinks.sdk.version} + + + + org.hswebframework.web + hsweb-system-dictionary + + com.github.ben-manes.caffeine caffeine diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmInfo.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmInfo.java new file mode 100644 index 00000000..6ea0ee2b --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmInfo.java @@ -0,0 +1,68 @@ +package org.jetlinks.community.command.rule.data; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.jetlinks.community.terms.TermSpec; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * 触发告警参数 + */ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class AlarmInfo implements Serializable { + private static final long serialVersionUID = -2316376361116648370L; + + @Schema(description = "告警配置ID") + private String alarmConfigId; + + @Schema(description = "告警名称") + private String alarmName; + + @Schema(description = "告警说明") + private String description; + + @Schema(description = "告警级别") + private int level; + + @Schema(description = "告警目标类型") + private String targetType; + + @Schema(description = "告警目标ID") + private String targetId; + + @Schema(description = "告警目标名称") + private String targetName; + + @Schema(description = "告警来源类型") + private String sourceType; + + @Schema(description = "告警来源ID") + private String sourceId; + + @Schema(description = "告警来源名称") + private String sourceName; + + /** + * 标识告警触发的配置来自什么业务功能 + */ + @Schema(description = "告警配置源") + private String alarmConfigSource; + + @Schema(description = "告警数据") + private Map data; + + /** + * 告警触发条件 + */ + private TermSpec termSpec; +} \ No newline at end of file diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmResult.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmResult.java new file mode 100644 index 00000000..4a955ae4 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmResult.java @@ -0,0 +1,36 @@ +package org.jetlinks.community.command.rule.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.io.Serializable; + +/** + * 告警结果 + */ +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class AlarmResult implements Serializable { + + private static final long serialVersionUID = -1752497262936740164L; + + @Schema(description = "告警ID") + private String recordId; + + @Schema(description = "是否重复告警") + private boolean alarming; + + @Schema(description = "当前首次触发") + private boolean firstAlarm; + + @Schema(description = "上一次告警时间") + private long lastAlarmTime; + + @Schema(description = "首次告警或者解除告警后的再一次告警时间") + private long alarmTime; +} \ No newline at end of file diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveInfo.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveInfo.java new file mode 100644 index 00000000..41580d99 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveInfo.java @@ -0,0 +1,29 @@ +package org.jetlinks.community.command.rule.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 解除告警参数 + */ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class RelieveInfo extends AlarmInfo{ + + @Schema(description = "解除原因") + private String relieveReason; + + @Schema(description = "解除说明") + private String describe; + + /** + * 告警解除类型,人工(user)、系统(system)等 + */ + @Schema(description = "告警解除类型") + private String alarmRelieveType; +} \ No newline at end of file diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveResult.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveResult.java new file mode 100644 index 00000000..644c20dd --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveResult.java @@ -0,0 +1,32 @@ +package org.jetlinks.community.command.rule.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +/** + * 解除警告结果 + */ +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class RelieveResult extends AlarmResult{ + + @Schema(description = "告警级别") + private int level; + + @Schema(description = "告警原因描述") + private String actualDesc; + + @Schema(description = "解除原因") + private String relieveReason; + + @Schema(description = "解除时间") + private long relieveTime; + + @Schema(description = "解除说明") + private String describe; +} \ No newline at end of file diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java index adf69afe..4fc77fbe 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java @@ -3,11 +3,15 @@ package org.jetlinks.community.configuration; import com.alibaba.fastjson.JSON; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import lombok.Generated; import org.apache.commons.beanutils.BeanUtilsBean; import org.apache.commons.beanutils.Converter; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.api.crud.entity.EntityFactory; +import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.cache.ReactiveCacheManager; +import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.defaults.DefaultItemDefine; import org.jetlinks.community.Interval; import org.jetlinks.community.JvmErrorException; import org.jetlinks.community.config.ConfigManager; @@ -15,6 +19,7 @@ import org.jetlinks.community.config.ConfigScopeCustomizer; import org.jetlinks.community.config.ConfigScopeProperties; import org.jetlinks.community.config.SimpleConfigManager; import org.jetlinks.community.config.entity.ConfigEntity; +import org.jetlinks.community.dictionary.DictionaryJsonDeserializer; import org.jetlinks.community.reference.DataReferenceManager; import org.jetlinks.community.reference.DataReferenceProvider; import org.jetlinks.community.reference.DefaultDataReferenceManager; @@ -26,6 +31,7 @@ import org.jetlinks.community.resource.initialize.PermissionResourceProvider; import org.jetlinks.community.service.DefaultUserBindService; import org.jetlinks.community.utils.TimeUtils; import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.rpc.RpcManager; import org.jetlinks.reactor.ql.feature.Feature; import org.jetlinks.reactor.ql.supports.DefaultReactorQLMetadata; @@ -47,6 +53,10 @@ import reactor.core.publisher.Hooks; import javax.annotation.Nonnull; import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Date; @@ -123,6 +133,22 @@ public class CommonConfiguration { } }, Long.class); + BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() { + @Override + @Generated + public T convert(Class type, Object value) { + + if (value instanceof String) { + return (T) DefaultItemDefine.builder() + .value(String.valueOf(value)) + .build(); + } + + return (T) FastBeanCopier.copy(value, new DefaultItemDefine()); + + } + }, EnumDict.class); + //捕获jvm错误,防止Flux被挂起 Hooks.onOperatorError((err, val) -> { if (Exceptions.isJvmFatal(err)) { @@ -155,6 +181,7 @@ public class CommonConfiguration { public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){ return builder->{ builder.deserializerByType(Date.class,new SmartDateDeserializer()); + builder.deserializerByType(EnumDict.class, new DictionaryJsonDeserializer()); }; } @@ -196,7 +223,7 @@ public class CommonConfiguration { return referenceManager; } - @AutoConfiguration + @Configuration @ConditionalOnClass(ReactiveRedisOperations.class) static class DefaultUserBindServiceConfiguration { @Bean diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DatabaseDictionaryManager.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DatabaseDictionaryManager.java new file mode 100644 index 00000000..afec7249 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DatabaseDictionaryManager.java @@ -0,0 +1,88 @@ +package org.jetlinks.community.dictionary; + +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.MapUtils; +import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dictionary.entity.DictionaryItemEntity; +import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService; +import org.springframework.boot.CommandLineRunner; + +import javax.annotation.Nonnull; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @author bestfeng + */ +@AllArgsConstructor +public class DatabaseDictionaryManager implements DictionaryManager, CommandLineRunner{ + + private final DefaultDictionaryItemService dictionaryItemService; + + private final Map> itemStore = new ConcurrentHashMap<>(); + + @Nonnull + @Override + public List> getItems(@Nonnull String dictId) { + Map itemEntityMap = itemStore.get(dictId); + if (MapUtils.isEmpty(itemEntityMap)) { + return Collections.emptyList(); + } + return new ArrayList<>(itemEntityMap.values()); + } + + @Nonnull + @Override + public Optional> getItem(@Nonnull String dictId, @Nonnull String itemId) { + Map itemEntityMap = itemStore.get(dictId); + if (itemEntityMap == null) { + return Optional.empty(); + } + return Optional.ofNullable(itemEntityMap.get(itemId)); + } + + + public void registerItems(List items) { + items.forEach(this::registerItem); + } + + + public void removeItems(List items) { + items.forEach(this::removeItem); + } + + + public void removeItem(DictionaryItemEntity item) { + if (item == null || item.getDictId() == null || item.getId() == null) { + return; + } + itemStore.compute(item.getDictId(), (k, v) -> { + if (v != null) { + v.remove(item.getId()); + if (!v.isEmpty()) { + return v; + } + } + return null; + }); + } + + + public void registerItem(DictionaryItemEntity item) { + if (item == null || item.getDictId() == null) { + return; + } + itemStore + .computeIfAbsent(item.getDictId(), k -> new ConcurrentHashMap<>()) + .put(item.getId(), item); + } + + @Override + public void run(String... args) throws Exception { + dictionaryItemService + .createQuery() + .fetch() + .doOnNext(this::registerItem) + .subscribe(); + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/Dictionaries.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/Dictionaries.java new file mode 100644 index 00000000..02001ab7 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/Dictionaries.java @@ -0,0 +1,94 @@ +package org.jetlinks.community.dictionary; + +import org.hswebframework.web.dict.EnumDict; + +import javax.annotation.Nonnull; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 动态数据字典工具类 + * + * @author zhouhao + * @since 2.1 + */ +public class Dictionaries { + + static DictionaryManager HOLDER = null; + + static void setup(DictionaryManager manager) { + HOLDER = manager; + } + + /** + * 获取字典的所有选型 + * + * @param dictId 字典ID + * @return 字典值 + */ + @Nonnull + public static List> getItems(@Nonnull String dictId) { + return HOLDER == null ? Collections.emptyList() : HOLDER.getItems(dictId); + } + + /** + * 根据掩码获取枚举选项,通常用于多选时获取选项. + * + * @param dictId 枚举ID + * @param mask 掩码 + * @return 选项 + * @see Dictionaries#toMask(Collection) + */ + @Nonnull + public static List> getItems(@Nonnull String dictId, long mask) { + if (HOLDER == null) { + return Collections.emptyList(); + } + return HOLDER + .getItems(dictId) + .stream() + .filter(item -> item.in(mask)) + .collect(Collectors.toList()); + } + + /** + * 查找枚举选项 + * + * @param dictId 枚举ID + * @param value 选项值 + * @return 选项 + */ + public static Optional> findItem(@Nonnull String dictId, Object value) { + return getItems(dictId) + .stream() + .filter(item -> item.eq(value)) + .findFirst(); + } + + /** + * 获取字段选型 + * + * @param dictId 字典ID + * @param itemId 选项ID + * @return 选项值 + */ + @Nonnull + public static Optional> getItem(@Nonnull String dictId, + @Nonnull String itemId) { + return HOLDER == null ? Optional.empty() : HOLDER.getItem(dictId, itemId); + } + + + public static long toMask(Collection> items) { + long value = 0L; + for (EnumDict t1 : items) { + value |= t1.getMask(); + } + return value; + } + + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/Dictionary.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/Dictionary.java new file mode 100644 index 00000000..99edee94 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/Dictionary.java @@ -0,0 +1,71 @@ +package org.jetlinks.community.dictionary; + +import org.hswebframework.ezorm.rdb.mapping.annotation.Codec; +import org.hswebframework.web.dict.EnumDict; + +import java.lang.annotation.*; +import java.util.Collection; + +/** + * 定义字段是一个数据字典,和枚举的使用方式类似. + *

+ * 区别是数据的值通过{@link Dictionaries}进行获取. + * + *

{@code
+ *   public class MyEntity{
+ *
+ *      //数据库存储的是枚举的值
+ *       @Column(length=32)
+ *       @Dictionary("my_status")
+ *       @ColumnType(javaType=String.class)
+ *       private EnumDict status;
+ *
+ *       @Column
+ *       @Dictionary("my_types")
+ *       //使用long来存储数据,表示使用字段的序号来进行mask运算进行存储.
+ *       @ColumnType(javaType=Long.class,jdbcType=JDBCType.BIGINT)
+ *       private EnumDict[] types;
+ *   }
+ * }
+ * ⚠️注意 + *
    + *
  • + * 字段类型只支持{@code EnumDict},{@code EnumDict[]},{@code List>} + *
  • + *
  • + * 多选时建议使用位掩码来存储: {@code @ColumnType(javaType=Long.class,jdbcType=JDBCType.BIGINT) },便于查询. + *
  • + *
  • 使用位掩码存储字典值时,基于{@link EnumDict#ordinal()}进行计算,因此字段选项数量不能超过64个,修改字典时,请注意序号值变化。
  • + *
  • 模块需要引入依赖:
    {@code
    + *   
    + *      org.hswebframework.web
    + *      hsweb-system-dictionary
    + *   
    + *     }
  • + *
+ * + * + * @author zhouhao + * @see EnumDict#getValue() + * @see EnumDict#getMask() + * @see Dictionaries + * @see Dictionaries#toMask(Collection) + * @see DictionaryManager + * @since 2.2 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Codec +public @interface Dictionary { + + /** + * 数据字典ID + * + * @return 数据字典ID + * @see Dictionaries#getItem(String, String) + */ + String value(); + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryColumnCustomizer.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryColumnCustomizer.java new file mode 100644 index 00000000..c8df829b --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryColumnCustomizer.java @@ -0,0 +1,57 @@ +package org.jetlinks.community.dictionary; + +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.EnumFragmentBuilder; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.EnumInFragmentBuilder; +import org.hswebframework.web.crud.configuration.TableMetadataCustomizer; +import org.jetlinks.community.form.type.EnumFieldType; + +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.sql.JDBCType; +import java.util.List; +import java.util.Set; + +public class DictionaryColumnCustomizer implements TableMetadataCustomizer { + @Override + public void customColumn(Class entityType, + PropertyDescriptor descriptor, + Field field, + Set annotations, + RDBColumnMetadata column) { + Dictionary dictionary = annotations + .stream() + .filter(Dictionary.class::isInstance) + .findFirst() + .map(Dictionary.class::cast) + .orElse(null); + if (dictionary != null) { + Class type = field.getType(); + + JDBCType jdbcType = (JDBCType) column.getType().getSqlType(); + EnumFieldType codec = new EnumFieldType( + type.isArray() || List.class.isAssignableFrom(type), + dictionary.value(), + jdbcType) + .withArray(type.isArray()) + .withFieldValueConverter(e -> e); + + column.setValueCodec(codec); + if (codec.isToMask()) { + column.addFeature(EnumFragmentBuilder.eq); + column.addFeature(EnumFragmentBuilder.not); + + column.addFeature(EnumInFragmentBuilder.of(column.getDialect())); + column.addFeature(EnumInFragmentBuilder.ofNot(column.getDialect())); + + } + } + } + + @Override + public void customTable(Class entityType, RDBTableMetadata table) { + + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryConfiguration.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryConfiguration.java new file mode 100644 index 00000000..8a17ea37 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryConfiguration.java @@ -0,0 +1,48 @@ +package org.jetlinks.community.dictionary; + +import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService; +import org.hswebframework.web.dictionary.service.DefaultDictionaryService; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DictionaryConfiguration { + + + @Configuration + @ConditionalOnClass(DefaultDictionaryItemService.class) + //@ConditionalOnBean(DefaultDictionaryItemService.class) + public static class DictionaryManagerConfiguration { + + + @Bean + public DictionaryEventHandler dictionaryEventHandler(DefaultDictionaryItemService service) { + return new DictionaryEventHandler(service); + } + + @Bean + public DatabaseDictionaryManager defaultDictionaryManager(DefaultDictionaryItemService service) { + DatabaseDictionaryManager dictionaryManager = new DatabaseDictionaryManager(service); + Dictionaries.setup(dictionaryManager); + return dictionaryManager; + } + + @Bean + public DictionaryColumnCustomizer dictionaryColumnCustomizer() { + return new DictionaryColumnCustomizer(); + } + + + @Bean + @ConfigurationProperties(prefix = "jetlinks.dict") + public DictionaryInitManager dictionaryInitManager(ObjectProvider initInfo, + DefaultDictionaryService defaultDictionaryService, + DefaultDictionaryItemService itemService) { + return new DictionaryInitManager(initInfo, defaultDictionaryService, itemService); + } + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryConstants.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryConstants.java new file mode 100644 index 00000000..f4294f8b --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryConstants.java @@ -0,0 +1,9 @@ +package org.jetlinks.community.dictionary; + +public interface DictionaryConstants { + + /** + * 系统分类标识 + */ + String CLASSIFIED_SYSTEM = "system"; +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryEventHandler.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryEventHandler.java new file mode 100644 index 00000000..5a3670a3 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryEventHandler.java @@ -0,0 +1,95 @@ +package org.jetlinks.community.dictionary; + +import lombok.AllArgsConstructor; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.hswebframework.web.crud.events.*; +import org.hswebframework.web.dictionary.entity.DictionaryEntity; +import org.hswebframework.web.dictionary.entity.DictionaryItemEntity; +import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService; +import org.hswebframework.web.exception.BusinessException; +import org.springframework.context.event.EventListener; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author bestfeng + */ +@AllArgsConstructor +public class DictionaryEventHandler implements EntityEventListenerCustomizer { + + private final DefaultDictionaryItemService itemService; + + @EventListener + public void handleDictionaryCreated(EntityCreatedEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .flatMap(dictionary -> { + if (!CollectionUtils.isEmpty(dictionary.getItems())) { + return Flux + .fromIterable(dictionary.getItems()) + .doOnNext(item -> item.setDictId(dictionary.getId())) + .as(itemService::save); + } + return Mono.empty(); + }) + ); + + } + + @EventListener + public void handleDictionarySaved(EntitySavedEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .flatMap(dictionary -> { + if (!CollectionUtils.isEmpty(dictionary.getItems())) { + return Flux + .fromIterable(dictionary.getItems()) + .doOnNext(item -> item.setDictId(dictionary.getId())) + .as(itemService::save); + } + return Mono.empty(); + }) + ); + } + + @EventListener + public void handleDictionaryDeleted(EntityDeletedEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .map(DictionaryEntity::getId) + .collectList() + .flatMap(dictionary -> itemService + .createDelete() + .where() + .in(DictionaryItemEntity::getDictId, dictionary) + .execute() + .then()) + ); + } + + @Override + public void customize(EntityEventListenerConfigure configure) { + configure.enable(DictionaryItemEntity.class); + configure.enable(DictionaryEntity.class); + } + + /** + * 监听字典删除前事件,阻止删除分类标识为系统的字典 + * @param event 字典删除前事件 + */ + @EventListener + public void handleDictionaryBeforeDelete(EntityBeforeDeleteEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .any(dictionary -> + StringUtils.equals(dictionary.getClassified(), DictionaryConstants.CLASSIFIED_SYSTEM)) + .flatMap(any -> { + if (any) { + return Mono.error(() -> new BusinessException("error.system_dictionary_can_not_delete")); + } + return Mono.empty(); + }) + ); + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryInitInfo.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryInitInfo.java new file mode 100644 index 00000000..4384cb09 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryInitInfo.java @@ -0,0 +1,23 @@ +package org.jetlinks.community.dictionary; + +import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.web.dictionary.entity.DictionaryEntity; +import reactor.core.publisher.Flux; + +import java.util.Collection; + +/** + * @author gyl + * @since 2.2 + */ +public interface DictionaryInitInfo { + Collection getDict(); + + default Flux getDictAsync() { + if (CollectionUtils.isEmpty(getDict())) { + return Flux.empty(); + } + return Flux.fromIterable(getDict()); + } + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryInitManager.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryInitManager.java new file mode 100644 index 00000000..5e32b14d --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryInitManager.java @@ -0,0 +1,81 @@ +package org.jetlinks.community.dictionary; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections.CollectionUtils; +import org.hswebframework.web.crud.events.EntityEventHelper; +import org.hswebframework.web.dictionary.entity.DictionaryEntity; +import org.hswebframework.web.dictionary.entity.DictionaryItemEntity; +import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService; +import org.hswebframework.web.dictionary.service.DefaultDictionaryService; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.CommandLineRunner; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author gyl + * @since 2.2 + */ +@Slf4j +public class DictionaryInitManager implements CommandLineRunner { + + @Getter + @Setter + private List inits = new ArrayList<>(); + + public final ObjectProvider initInfo; + + private final DefaultDictionaryService defaultDictionaryService; + + private final DefaultDictionaryItemService itemService; + + public DictionaryInitManager(ObjectProvider initInfo, DefaultDictionaryService defaultDictionaryService, DefaultDictionaryItemService itemService) { + this.initInfo = initInfo; + this.defaultDictionaryService = defaultDictionaryService; + this.itemService = itemService; + } + + @Override + public void run(String... args) { + Flux + .merge( + Flux.fromIterable(inits), + Flux + .fromIterable(initInfo) + .flatMap(DictionaryInitInfo::getDictAsync) + ) + .buffer(200) + .filter(CollectionUtils::isNotEmpty) + .flatMap(collection -> { + List items = generateItems(collection); + return defaultDictionaryService + .save(collection) + .mergeWith(itemService.save(items)); + }) + .as(EntityEventHelper::setDoNotFireEvent) + .subscribe(ignore -> { + }, + err -> log.error("init dict error", err)); + + } + + + public List generateItems(List dictionaryList) { + List items = new ArrayList<>(); + for (DictionaryEntity dictionary : dictionaryList) { + if (!CollectionUtils.isEmpty(dictionary.getItems())) { + for (DictionaryItemEntity item : dictionary.getItems()) { + item.setDictId(dictionary.getId()); + items.add(item); + } + } + } + return items; + } + + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryJsonDeserializer.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryJsonDeserializer.java new file mode 100644 index 00000000..46e3b018 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryJsonDeserializer.java @@ -0,0 +1,40 @@ +package org.jetlinks.community.dictionary; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.defaults.DefaultItemDefine; + +import java.io.IOException; +import java.util.Map; + +public class DictionaryJsonDeserializer extends JsonDeserializer> { + @Override + public EnumDict deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JacksonException { + if (jsonParser.hasToken(JsonToken.VALUE_NUMBER_INT)) { + DefaultItemDefine defaultItemDefine = new DefaultItemDefine(); + defaultItemDefine.setOrdinal(jsonParser.getIntValue()); + return defaultItemDefine; + } + if (jsonParser.hasToken(JsonToken.VALUE_STRING)) { + String str = jsonParser.getText().trim(); + if (!str.isEmpty()) { + DefaultItemDefine defaultItemDefine = new DefaultItemDefine(); + defaultItemDefine.setValue(str); + return defaultItemDefine; + } + } + + if (jsonParser.hasToken(JsonToken.START_OBJECT)) { + Map map = ctxt.readValue(jsonParser, Map.class); + if (map != null) { + return FastBeanCopier.copy(map, new DefaultItemDefine()); + } + } + return null; + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryManager.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryManager.java new file mode 100644 index 00000000..6e91c990 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/dictionary/DictionaryManager.java @@ -0,0 +1,41 @@ +package org.jetlinks.community.dictionary; + +import org.hswebframework.web.dict.EnumDict; + +import javax.annotation.Nonnull; +import java.util.List; +import java.util.Optional; + +/** + * 数据字典管理器,用于获取数据字典的枚举值 + * + * @author zhouhao + * @since 2.1 + */ +public interface DictionaryManager { + + /** + * 获取字典的所有选项 + * + * @param dictId 字典ID + * @return 字典值 + */ + @Nonnull + List> getItems(@Nonnull String dictId); + + /** + * 获取字段选项 + * + * @param dictId 字典ID + * @param itemId 选项ID + * @return 选项值 + */ + @Nonnull + Optional> getItem(@Nonnull String dictId, + @Nonnull String itemId); + + + + + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/EnumFieldType.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/EnumFieldType.java new file mode 100644 index 00000000..84ffc1b4 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/EnumFieldType.java @@ -0,0 +1,204 @@ +package org.jetlinks.community.form.type; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.hswebframework.ezorm.core.ValueCodec; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.dict.EnumDict; +import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.types.EnumType; +import org.jetlinks.community.dictionary.Dictionaries; +import org.jetlinks.community.utils.ConverterUtils; +import org.jetlinks.reactor.ql.utils.CastUtils; + +import java.sql.JDBCType; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @since 2.1 + */ +@Getter +@RequiredArgsConstructor +public class EnumFieldType implements FieldType, ValueCodec { + public static final String TYPE = "Enum"; + + private final boolean multiple; + + private final String dictId; + + private final JDBCType jdbcType; + + private boolean array = false; + + private Function,Object> fieldValueConverter = EnumDict::getWriteJSONObject;; + + public EnumFieldType withArray(boolean array) { + this.array = array; + return this; + } + + public EnumFieldType withFieldValueConverter(Function,Object> converter) { + this.fieldValueConverter = converter; + return this; + } + + + @Override + public final String getId() { + return TYPE; + } + + public boolean isToMask() { + return multiple && jdbcType == JDBCType.BIGINT; + } + + @Override + public Class getJavaType() { + return isToMask() ? Long.class : String.class; + } + + @Override + public JDBCType getJdbcType() { + return jdbcType; + } + + @Override + public int getLength() { + return 64; + } + + @Override + public DataType getDataType() { + EnumType enumType = new EnumType(); + for (EnumDict item : Dictionaries.getItems(dictId)) { + enumType.addElement(EnumType.Element.of(String.valueOf(item.getValue()), item.getText())); + } + enumType.setMulti(multiple); + return enumType; + } + + @Override + public ValueCodec getCodec() { + return this; + } + + @Override + public Object encode(Object value) { + //转为位掩码 + if (isToMask()) { + return Dictionaries.toMask( + ConverterUtils + .convertToList(value, val -> Dictionaries.findItem(dictId, val).orElse(null)) + .stream() + .filter(Objects::nonNull) + .collect(Collectors.toSet())); + } + //多选,使用逗号分隔 + if (multiple) { + return ConverterUtils + .convertToList(value, val -> Dictionaries.findItem(dictId, val).orElse(null)) + .stream() + .filter(Objects::nonNull) + .map(e -> String.valueOf(e.getValue())) + .collect(Collectors.joining(",")); + } + return Dictionaries + .findItem(dictId, value) + .map(EnumDict::getValue) + .orElseGet(() -> { + if (value instanceof EnumDict) { + return ((EnumDict) value).getValue(); + } + return value; + }); + } + + @Override + public Object decode(Object data) { + if (multiple) { + List list; + if (isToMask()) { + list = + Dictionaries + .getItems(dictId, CastUtils.castNumber(data).longValue()) + .stream() + .map(fieldValueConverter) + .collect(Collectors.toList()); + } else { + list = ConverterUtils + .convertToList(data, val -> Dictionaries.findItem(dictId, val).orElse(null)) + .stream() + .filter(Objects::nonNull) + .map(fieldValueConverter) + .collect(Collectors.toList()); + } + if (isArray()) { + return list.toArray(new EnumDict[0]); + } + return list; + } + if(isToMask()){ + return Dictionaries + .getItems(dictId, CastUtils.castNumber(data).longValue()) + .stream() + .map(fieldValueConverter) + .findFirst() + .orElse(null); + } + return Dictionaries + .findItem(dictId, data) + .map(fieldValueConverter) + .orElse(fieldValueConverter.apply(EnumDict.create(String.valueOf(data)))); + } + + public static class Provider implements FieldTypeProvider { + + @Override + public String getProvider() { + return EnumFieldType.TYPE; + } + + @Override + public String getProviderName() { + return "枚举"; + } + + @Override + public Set getSupportJdbcTypes() { + return new HashSet<>(Arrays.asList(JDBCType.VARCHAR, JDBCType.BIGINT)); + } + + @Override + public int getDefaultLength() { + return 64; + } + + @Override + public FieldType create(FieldTypeSpec configuration) { + DictionarySpec spec = Optional + .ofNullable(configuration.getConfiguration()) + .map(configSpec -> FastBeanCopier.copy(configSpec, new DictionarySpec())) + .orElse(new DictionarySpec()); + return new EnumFieldType( + spec.isMultiple(), + Optional + .ofNullable(spec.getDictionaryId()) + .orElseThrow(() -> new IllegalArgumentException("dictionaryId can not be null")), + configuration.getJdbcType() == null ? JDBCType.VARCHAR : configuration.getJdbcType() + ); + } + } + + //数据字典 + @Getter + @Setter + public static class DictionarySpec { + //字典ID + private String dictionaryId; + //多选 + private boolean multiple; + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldType.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldType.java new file mode 100644 index 00000000..a31338f5 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldType.java @@ -0,0 +1,28 @@ +package org.jetlinks.community.form.type; + +import org.hswebframework.ezorm.core.ValueCodec; +import org.jetlinks.core.metadata.DataType; + +import java.sql.JDBCType; + + +public interface FieldType { + + String getId(); + + Class getJavaType(); + + JDBCType getJdbcType(); + + ValueCodec getCodec(); + + default int getLength() { + return 255; + } + + default int getScale() { + return 2; + } + + DataType getDataType(); +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldTypeProvider.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldTypeProvider.java new file mode 100644 index 00000000..23116a88 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldTypeProvider.java @@ -0,0 +1,31 @@ +package org.jetlinks.community.form.type; + +import org.jetlinks.community.spi.Provider; + +import java.sql.JDBCType; +import java.util.Set; + +public interface FieldTypeProvider { + + Provider supports = Provider.create(FieldTypeProvider.class); + + String getProvider(); + + default String getProviderName() { + return getProvider(); + } + + default int getDefaultLength() { + return 255; + } + + Set getSupportJdbcTypes(); + + FieldType create(FieldTypeSpec configuration); + + static FieldType createType(FieldTypeSpec spec) { + return supports + .getNow(spec.getName()) + .create(spec); + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldTypeSpec.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldTypeSpec.java new file mode 100644 index 00000000..146d9da1 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/form/type/FieldTypeSpec.java @@ -0,0 +1,26 @@ +package org.jetlinks.community.form.type; + +import lombok.Getter; +import lombok.Setter; +import org.jetlinks.community.ValueObject; + +import java.sql.JDBCType; +import java.util.Map; + +@Getter +@Setter +public class FieldTypeSpec implements ValueObject { + private String name; + + private int length; + private int scale; + + private JDBCType jdbcType; + + private Map configuration; + + @Override + public Map values() { + return configuration; + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypeSupport.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypeSupport.java index c17f5e2f..4b901110 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypeSupport.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypeSupport.java @@ -1,20 +1,97 @@ package org.jetlinks.community.reactorql.term; +import lombok.SneakyThrows; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; +import org.jetlinks.community.utils.ReactorUtils; import org.jetlinks.core.metadata.DataType; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.concurrent.TimeUnit; public interface TermTypeSupport { + /** + * @return 条件标识 + */ String getType(); + /** + * @return 条件名称 + */ String getName(); + /** + * 判断是否支持特定的数据类型 + * + * @param type 数据类型 + * @return 是否支持 + */ boolean isSupported(DataType type); + /** + * 创建SQL片段 + * + * @param column 列名 + * @param value 值 + * @param term 条件 + * @return SQL片段 + */ SqlFragments createSql(String column, Object value, Term term); + /** + * 判断是否已经过时,过时的条件应当不可选择. + * + * @return 是否已经过时 + */ + default boolean isDeprecated() { + return false; + } + + /** + * 转为条件类型 + * + * @return 条件类型 + */ default TermType type() { return TermType.of(getType(), getName()); } + + default String createDesc(String property, Object expect, Object actual) { + return String.format("%s%s(%s)", property, getName(), expect); + } + + /** + * 阻塞方式判断能否满足期望值 + * + * @param expect 期望值 + * @param actual 实际值 + * @return 是否满足 + */ + @SneakyThrows + default boolean matchBlocking(Object expect, Object actual) { + return this + .match(expect, actual) + .toFuture() + .get(1, TimeUnit.SECONDS); + } + + /** + * 判断能否满足期望值 + * + * @param expect 期望值 + * @param actual 实际值 + * @return 是否满足 + */ + default Mono match(Object expect, Object actual) { + Term term = new Term(); + term.setTermType(getType()); + term.setColumn("_mock"); + term.setValue(expect); + + return ReactorUtils + .createFilter(Collections.singletonList(term)) + .apply(Collections.singletonMap("_mock", actual)); + } } diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/spi/Provider.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/spi/Provider.java new file mode 100644 index 00000000..31645d96 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/spi/Provider.java @@ -0,0 +1,117 @@ +package org.jetlinks.community.spi; + +import reactor.core.Disposable; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +/** + * 通用提供商管理接口,用于提供通用的提供商支持 + * + * @param 类型 + * @author zhouhao + * @since 2.2 + */ +public interface Provider { + /** + * 根据类型创建Provider + * + * @param type 类型 + * @param 类型 + * @return Provider + */ + static Provider create(Class type) { + return new SimpleProvider<>(type.getSimpleName()); + } + + /** + * 注册一个提供商,可通过调用返回值{@link Disposable#dispose()}来注销. + * + * @param id ID + * @param provider 提供商实例 + * @return Disposable + */ + Disposable register(String id, T provider); + + /** + * 获取所有的注册的提供商 + * + * @return 提供商 + */ + List getAll(); + + /** + * 注册提供商,如果已经存在则忽略注册,并返回旧的提供商实例 + * + * @param id ID + * @param provider 提供商实例 + * @return 旧的提供商实例 + */ + T registerIfAbsent(String id, T provider); + + /** + * 注册提供商,如果已经存在则忽略注册,并返回旧的提供商实例,否则返回注册后的提供商实例 + * + * @param id ID + * @param providerBuilder 提供商构造器 + * @return 如果已经存在则忽略注册, 并返回旧的提供商实例, 否则返回注册后的提供商实例 + */ + T registerIfAbsent(String id, Function providerBuilder); + + /** + * 根据ID注销提供商 + * + * @param id ID + */ + void unregister(String id); + + /** + * 根据ID和具体的实例注销提供商 + * + * @param id ID + * @param provider 提供商实例 + */ + void unregister(String id, T provider); + + /** + * 注册监听钩子,当提供商注册和注销时调用对应方法进行业务逻辑处理. + * 可通过调用返回值{@link Disposable#dispose()}来注销钩子 + * + * @param hook Hook + * @return Disposable + */ + Disposable addHook(Hook hook); + + /** + * 根据ID获取提供商 + * + * @param id ID + * @return Optional + */ + Optional get(String id); + + /** + * 根据ID获取提供商,如果不存在将抛出{@link UnsupportedOperationException} + * + * @param id ID + * @return 提供商 + */ + T getNow(String id); + + interface Hook { + /** + * 当注册时调用 + * + * @param provider 提供商实例 + */ + void onRegister(T provider); + + /** + * 当注销时调用 + * + * @param provider 提供商实例 + */ + void onUnRegister(T provider); + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/spi/SimpleProvider.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/spi/SimpleProvider.java new file mode 100644 index 00000000..c72d13ef --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/spi/SimpleProvider.java @@ -0,0 +1,98 @@ +package org.jetlinks.community.spi; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.hswebframework.ezorm.core.CastUtil; +import org.hswebframework.web.exception.BusinessException; +import reactor.core.Disposable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; +import java.util.function.Function; + +@AllArgsConstructor(access = AccessLevel.PACKAGE) +public class SimpleProvider implements Provider { + + private final Map providers = new ConcurrentHashMap<>(); + + private final List> hooks = new CopyOnWriteArrayList<>(); + + private final String name; + + + public Disposable register(String id, T provider) { + providers.put(id, provider); + executeHook(provider, Hook::onRegister); + return () -> unregister(id, provider); + } + + public List getAll() { + return new ArrayList<>(providers.values()); + } + + public T registerIfAbsent(String id, T provider) { + T old = providers.putIfAbsent(id, provider); + if (old == null || old != provider) { + executeHook(provider, Hook::onRegister); + } + return old; + } + + @Override + public T registerIfAbsent(String id, Function providerBuilder) { + Object[] absent = new Object[1]; + T old = providers + .computeIfAbsent(id, _id -> { + T provider = providerBuilder.apply(_id); + absent[0] = provider; + return provider; + }); + + if (absent[0] != null) { + executeHook(CastUtil.cast(absent[0]), Hook::onRegister); + } + return old; + } + + public void unregister(String id, T provider) { + if (providers.remove(id, provider)) { + executeHook(provider, Hook::onUnRegister); + } + } + + public void unregister(String id) { + T provider = providers.remove(id); + if (provider != null) { + executeHook(provider, Hook::onUnRegister); + } + } + + public Disposable addHook(Hook hook) { + hooks.add(hook); + return () -> hooks.remove(hook); + } + + public Optional get(String id) { + return Optional.ofNullable(providers.get(id)); + } + + public T getNow(String id) { + return get(id) + .orElseThrow(() -> new BusinessException.NoStackTrace("Unsupported " + name + ":" + id)); + } + + protected void executeHook(T provider, BiConsumer, T> executor) { + if (hooks.isEmpty()) { + return; + } + for (Hook hook : hooks) { + executor.accept(hook, provider); + } + } + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/I18nSpec.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/I18nSpec.java new file mode 100644 index 00000000..7db3b51e --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/I18nSpec.java @@ -0,0 +1,131 @@ +package org.jetlinks.community.terms; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.reactor.ql.utils.CastUtils; +import org.springframework.util.CollectionUtils; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 支持国际化参数嵌套的国际化编码类。适用于实现国际化表达式的参数也需要国际化的场景。 + * + *
{@code
+ *
+ * 国际化表达式携带的参数也需要国际化示例
+ * 示例参数:
+ *
+ * {
+ *     "code": "message.scene.term.full_name",
+ *     "args": [
+ *         {
+ *             "defaultMessage": "温度"
+ *         },
+ *         {
+ *             "code": "message.property.recent"
+ *         }
+ *     ]
+ * }
+ * 在国际化配置文件中的配置示例:
+ * message.scene.term.full_name=属性:{0}/{1}
+ * message.property.recent=当前值
+ *
+ * 国际化完成后的结果为:属性:温度/当前值
+ * }
+ * + * @author bestfeng + */ +@Getter +@Setter +@EqualsAndHashCode +public class I18nSpec implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 国际化编码为空时,表示当前对象不需要国际化。直接返回defaultMessage值 + */ + @Schema(description = "国际化编码") + private String code; + + @Schema(description = "默认值") + private Object defaultMessage; + + @Schema(description = "支持国际化的嵌套参数") + private List args; + + public static I18nSpec of(String code, String defaultMessage, Object... args) { + I18nSpec i18NSpec = new I18nSpec(); + i18NSpec.setCode(code); + i18NSpec.setDefaultMessage(defaultMessage); + i18NSpec.setArgs(of(args)); + return i18NSpec; + } + + public I18nSpec withArgs(String code, String defaultMessage, Object... args) { + if (CollectionUtils.isEmpty(this.args)) { + this.setArgs(new ArrayList<>()); + } + this.getArgs().add(I18nSpec.of(code, defaultMessage, args)); + return this; + } + + public I18nSpec withArgs(I18nSpec i18NSpec) { + if (CollectionUtils.isEmpty(this.args)) { + this.setArgs(new ArrayList<>()); + } + this.getArgs().add(i18NSpec); + return this; + } + + public String resolveI18nMessage() { + if (CollectionUtils.isEmpty(args)) { + return CastUtils.castString(resolveI18nMessage(code, defaultMessage)); + } + return CastUtils.castString(resolveI18nMessage(code, defaultMessage, parseI18nCodeParams().toArray())); + } + + + public I18nSpec copy() { + return FastBeanCopier.copy(this, new I18nSpec()); + } + + + private static List of(Object... args) { + List codes = new ArrayList<>(); + for (Object arg : args) { + I18nSpec i18NSpec = new I18nSpec(); + i18NSpec.setDefaultMessage(arg); + codes.add(i18NSpec); + } + return codes; + } + + protected List parseI18nCodeParams() { + if (CollectionUtils.isEmpty(args)) { + return new ArrayList<>(); + } + List params = new ArrayList<>(); + for (I18nSpec code : args) { + params.add(code.resolveI18nMessage()); + } + return params; + } + + private static Object resolveI18nMessage(String code, Object name, Object... args) { + if (StringUtils.isEmpty(code)){ + return name; + } + if (name == null){ + return LocaleUtils.resolveMessage(code, args); + } + return LocaleUtils.resolveMessage(code, CastUtils.castString(name), args); + } +} \ No newline at end of file diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/TermSpec.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/TermSpec.java new file mode 100644 index 00000000..a4f9d961 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/TermSpec.java @@ -0,0 +1,286 @@ +package org.jetlinks.community.terms; + +import com.alibaba.fastjson.JSONObject; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.core.param.TermType; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.reactorql.term.TermTypeSupport; +import org.jetlinks.community.reactorql.term.TermTypes; +import org.jetlinks.core.metadata.Jsonable; +import org.jetlinks.core.utils.SerializeUtils; +import org.jetlinks.reactor.ql.supports.DefaultPropertyFeature; +import org.springframework.util.StringUtils; + +import java.io.Serializable; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +@Getter +@Setter +@EqualsAndHashCode +public class TermSpec implements Jsonable, Serializable { + + private static final long serialVersionUID = 1L; + + @Schema(description = "列名") + private String column; + + @Schema(description = "列显码") + private I18nSpec displayCode; + + @Schema(description = "列显示名") + private String displayName; + + @Schema(description = "条件类型") + private String termType; + + @Schema(description = "关联类型") + private Term.Type type; + + @Schema(description = "期望值") + private Object expected; + + @Schema(description = "实际值") + private Object actual; + + @Schema(description = "当前条件是否匹配") + private Boolean matched; + + @Schema(description = "说明") + private String description; + + @Schema(description = "嵌套条件") + private List children; + + private List options; + + private boolean expectIsExpr; + + @Schema(description = "是否为物模型变量") + private boolean metadata; + + @Override + public JSONObject toJson() { + @SuppressWarnings("all") + Map map = (Map) SerializeUtils.convertToSafelySerializable(Jsonable.super.toJson()); + return new JSONObject(map); + } + + public String getDisplayName() { + if (displayCode != null) { + return displayCode.resolveI18nMessage(); + } + return displayName; + } + + public String getActualDesc() { + return String.join(";", parseTermSpecActualDesc(this)); + } + + public String getTriggerDesc() { + return toString(); + } + + //解析TermSpec实际值说明; 如:温度=35;湿度=28 + public static Set parseTermSpecActualDesc(TermSpec termSpec) { + Set actualDesc = new HashSet<>(); + + //兼容直接通过termSpec.setActual(Value)设置了实际值,导致matched一直为false的情况 + if (termSpec.getMatched() == null && StringUtils.hasText(termSpec.getColumn())){ + TermTypes + .lookupSupport(termSpec.getTermType()) + .ifPresent(support -> termSpec.matched = support.matchBlocking(termSpec.getExpected(),termSpec.getActual())); + } + if (termSpec.matched != null && termSpec.matched) { + actualDesc.add(termSpec.getDisplayName() + " = " + termSpec.getActual()); + } + if (CollectionUtils.isNotEmpty(termSpec.children)) { + for (TermSpec child : termSpec.children) { + actualDesc.addAll(parseTermSpecActualDesc(child)); + } + } + return actualDesc; + } + + public static List of(List terms) { + return of(terms, (term, spec) -> { + }); + } + + public static List of(List terms, BiConsumer customizer) { + if (terms == null) { + return null; + } + return terms + .stream() + .map(spec -> of(spec, customizer)) + .collect(Collectors.toList()); + } + + public static TermSpec of(Term term, BiConsumer customizer) { + TermSpec spec = new TermSpec(); + spec.setType(term.getType()); + spec.setColumn(term.getColumn()); + spec.setTermType(term.getTermType()); + spec.setExpected(term.getValue()); + spec.setDisplayName(term.getColumn()); + spec.setOptions(term.getOptions()); + if (term.getValue() instanceof NativeSql) { + spec.setExpectIsExpr(true); + //fixme 参数支持? + spec.setExpected(((NativeSql) term.getValue()).getSql()); + } + customizer.accept(term, spec); + spec.children = of(term.getTerms(), customizer); + return spec; + } + + public String getTermType() { + return termType == null ? TermType.eq : termType; + } + + public Term.Type getType() { + return type == null ? Term.Type.and : type; + } + + private void apply0(Map context) { + if (this.column != null) { + this.actual = DefaultPropertyFeature + .GLOBAL + .getProperty(this.column, context) + .orElse(null); + if (expectIsExpr) { + this.expected = DefaultPropertyFeature + .GLOBAL + .getProperty(String.valueOf(expected), context) + .orElse(null); + expectIsExpr = false; + } + TermTypes + .lookupSupport(getTermType()) + .ifPresent(support -> this.matched = support.matchBlocking(expected, actual)); + + } + if (this.children != null) { + for (TermSpec child : this.children) { + child.apply0(context); + } + } + } + + public TermSpec apply(Map context) { + TermSpec copy = copy(); + copy.apply0(context); + return copy; + } + + public TermSpec copy() { + TermSpec spec = FastBeanCopier.copy(this, new TermSpec(), "children"); + if (this.children != null) { + spec.children = this.children.stream().map(TermSpec::copy).collect(Collectors.toList()); + } + return spec; + } + + public static String toString(List terms) { + return toString(new StringBuilder(), terms).toString(); + } + + public static StringBuilder toString(StringBuilder builder, List terms) { + int lastLength = builder.length(); + + for (TermSpec child : terms) { + + if (lastLength != builder.length()) { + builder + .append(' ') + .append(LocaleUtils.resolveMessage("message.term-type-" + child.getType(), child + .getType() + .name())) + .append(' '); + lastLength = builder.length(); + } + child.toString(builder); + + } + return builder; + } + + public void toString(StringBuilder builder) { + boolean hasSpec = false; + if (column != null) { + TermTypeSupport support = TermTypes + .lookupSupport(getTermType()) + .orElse(null); + if (support != null) { + hasSpec = true; + builder.append(support.createDesc(getDisplayName(), expected, actual)); + } + } + List children = compressChildren(); + + if (CollectionUtils.isNotEmpty(children)) { + + if (hasSpec) { + builder + .append(' ') + .append(LocaleUtils.resolveMessage("message.term-type-" + getType(), getType().name())) + .append(' '); + } else { + //对整个嵌套结果取反? + if (Objects.equals(getTermType(), TermType.not)) { + builder.append('!'); + } + } + builder.append("( "); + toString(builder, children); + builder.append(" )"); + } + + } + + + public void setActual(Object actual) { + this.actual = actual; + this.setMatched(null); + } + + public void setExpected(Object expected) { + this.expected = expected; + this.setMatched(null); + } + + public void setTermType(String termType) { + this.termType = termType; + this.setMatched(null); + } + + //压缩子节点,避免过多嵌套. + protected List compressChildren() { + if (CollectionUtils.isEmpty(children)) { + return children; + } + if (children.size() == 1) { + TermSpec child = children.get(0); + if (child.column == null && Objects.equals(child.getTermType(), TermType.eq)) { + return child.compressChildren(); + } + } + return children; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + toString(sb); + return sb.toString(); + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/topic/Topics.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/topic/Topics.java index e43c73b7..10f4d3b8 100755 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/topic/Topics.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/topic/Topics.java @@ -3,14 +3,65 @@ package org.jetlinks.community.topic; import lombok.Generated; import org.jetlinks.core.utils.StringBuilderUtils; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - public interface Topics { + @Deprecated + static String org(String orgId, String topic) { + if (!topic.startsWith("/")) { + topic = "/" + topic; + } + return String.join("", "/org/", orgId, topic); + } + + static String creator(String creatorId, String topic) { + return StringBuilderUtils.buildString(creatorId, topic, Topics::creator); + } + + static void creator(String creatorId, String topic, StringBuilder builder) { + builder.append("/user/").append(creatorId); + if (topic.charAt(0) != '/') { + builder.append('/'); + } + builder.append(topic); + } + + static void binding(String type, String id, String topic, StringBuilder builder) { + builder.append('/') + .append(type) + .append('/') + .append(id); + if (topic.charAt(0) != '/') { + builder.append('/'); + } + builder.append(topic); + } + + /** + * 根据关系构造topic + *
{@code
+     * /rel/{objectType}/{objectId}/{relation}/{topic}
+     *
+     * 如: /rel/用户/user1/manager/{topic}
+     * }
+ * + * @param objectType 对象类型 + * @param objectId 对象ID + * @param relation 关系标识 + * @param topic topic后缀 + * @param builder StringBuilder + */ + static void relation(String objectType, String objectId, String relation, String topic, StringBuilder builder) { + builder.append("/rel/") + .append(objectType) + .append('/') + .append(objectId) + .append('/') + .append(relation); + if (topic.charAt(0) != '/') { + builder.append('/'); + } + builder.append(topic); + } String allDeviceRegisterEvent = "/_sys/registry-device/*/register"; String allDeviceUnRegisterEvent = "/_sys/registry-device/*/unregister"; @@ -64,6 +115,11 @@ public interface Topics { return String.join("", "/alarm/", targetType, "/", targetId, "/", alarmId, "/record"); } + static String alarmRelieve(String targetType, String targetId, String alarmId) { + // /alarm/{targetType}/{targetId}/{alarmId}/relieve + return String.join("", "/alarm/", targetType, "/", targetId, "/", alarmId, "/relieve"); + } + interface Authentications { String allUserAuthenticationChanged = "/_sys/user-dimension-changed/*"; diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/ReactorUtils.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/ReactorUtils.java index 3d1aae29..1bf3261e 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/ReactorUtils.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/ReactorUtils.java @@ -58,6 +58,7 @@ public class ReactorUtils { return FluxUtils.distinct(keySelector, duration); } + /** * 尝试执行 {@link Disposable#dispose()} * @@ -198,14 +199,14 @@ public class ReactorUtils { termType = FixedTermTypeSupport.gte.name(); break; case "<": - termType = FixedTermTypeSupport.lt.getName(); + termType = FixedTermTypeSupport.lt.name(); break; case "<=": - termType = FixedTermTypeSupport.lte.getName(); + termType = FixedTermTypeSupport.lte.name(); break; case "!=": case "<>": - termType = FixedTermTypeSupport.neq.getName(); + termType = FixedTermTypeSupport.neq.name(); break; } diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimit.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimit.java index 35241b3e..b8dc5a3d 100644 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimit.java +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimit.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.Setter; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import reactor.util.function.Tuples; import javax.annotation.Nonnull; @@ -41,9 +42,8 @@ public class ShakeLimit implements Serializable { private boolean alarmFirst; /** - * * 利用窗口函数,将ReactorQL语句包装为支持抖动限制的SQL. - * + *

* select * from ( sql ) * group by * _window('1s') --时间窗口 @@ -78,6 +78,7 @@ public class ShakeLimit implements Serializable { * @param totalConsumer 总数接收器 * @param 数据类型 * @return 新流 + * @deprecated {@link ShakeLimitProvider#shakeLimit(String, Flux, ShakeLimit)} */ public Flux transfer(Flux source, BiFunction, Flux>> windowFunction, @@ -88,18 +89,27 @@ public class ShakeLimit implements Serializable { int thresholdNumber = getThreshold(); Duration windowTime = Duration.ofSeconds(getTime()); - return source - .as(flux -> windowFunction.apply(windowTime, flux)) + return windowFunction + .apply(windowTime, source) //处理每一组数据 .flatMap(group -> group //给数据打上索引,索引号就是告警次数 .index((index, data) -> Tuples.of(index + 1, data)) + .switchOnFirst((e, flux) -> { + if (e.hasValue()) { + @SuppressWarnings("all") + T ele = e.get().getT2(); + return flux.map(tp2 -> Tuples.of(tp2.getT1(), tp2.getT2(), ele)); + } + return flux.then(Mono.empty()); + }) //超过阈值告警时 .filter(tp -> tp.getT1() >= thresholdNumber) .as(flux -> isAlarmFirst() ? flux.take(1) : flux.takeLast(1))//取第一个或者最后一个 - .map(tp2 -> { - totalConsumer.accept(tp2.getT2(), tp2.getT1()); - return tp2.getT2(); - })); + .map(tp3 -> { + T next = isAlarmFirst() ? tp3.getT3() : tp3.getT2(); + totalConsumer.accept(next, tp3.getT1()); + return next; + }), Integer.MAX_VALUE); } } \ No newline at end of file diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitProvider.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitProvider.java new file mode 100644 index 00000000..86215b9e --- /dev/null +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitProvider.java @@ -0,0 +1,37 @@ +package org.jetlinks.community.rule.engine.commons; + +import org.jetlinks.community.spi.Provider; +import reactor.core.publisher.Flux; +import reactor.core.publisher.GroupedFlux; + +/** + * 防抖提供商 + * + * @author zhouhao + * @since 2.2 + */ +public interface ShakeLimitProvider { + + Provider supports = Provider.create(ShakeLimitProvider.class); + + /** + * @return 提供商唯一标识 + */ + String provider(); + + /** + * 对指定分组数据源进行防抖,并输出满足条件的数据. + * + * @param sourceKey 数据源唯一标识 + * @param grouped 分组数据源 + * @param limit 防抖条件 + * @param 数据类型 + * @return 防抖结果 + */ + Flux> shakeLimit( + String sourceKey, + Flux> grouped, + ShakeLimit limit); + + +} diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitResult.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitResult.java new file mode 100644 index 00000000..24b4c60c --- /dev/null +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitResult.java @@ -0,0 +1,16 @@ +package org.jetlinks.community.rule.engine.commons; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +@Getter +@AllArgsConstructor +@ToString +public class ShakeLimitResult { + + private String groupKey; + private long times; + private T element; + +} diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/impl/SimpleShakeLimitProvider.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/impl/SimpleShakeLimitProvider.java new file mode 100644 index 00000000..f270bad4 --- /dev/null +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/impl/SimpleShakeLimitProvider.java @@ -0,0 +1,87 @@ +package org.jetlinks.community.rule.engine.commons.impl; + +import lombok.extern.slf4j.Slf4j; +import org.jetlinks.community.rule.engine.commons.ShakeLimit; +import org.jetlinks.community.rule.engine.commons.ShakeLimitProvider; +import org.jetlinks.community.rule.engine.commons.ShakeLimitResult; +import reactor.core.publisher.Flux; +import reactor.core.publisher.GroupedFlux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuples; + +import java.time.Duration; + +@Slf4j +public class SimpleShakeLimitProvider implements ShakeLimitProvider { + + public static final ShakeLimitProvider GLOBAL = new SimpleShakeLimitProvider(); + + public static final String PROVIDER = "simple"; + + @Override + public String provider() { + return PROVIDER; + } + + protected Flux wrapSource(String sourceKey, Flux source) { + return source; + } + + @Override + public Flux> shakeLimit(String sourceKey, + Flux> grouped, + ShakeLimit limit) { + int thresholdNumber = limit.getThreshold(); + boolean isAlarmFirst = limit.isAlarmFirst(); + Duration windowSpan = Duration.ofSeconds(limit.getTime()); + return grouped + .flatMap(group -> { + String groupKey = group.key(); + String key = sourceKey + ":" + groupKey; + return Flux + .defer(() -> this + //使用timeout,当2倍窗口时间没有收到数据时,则结束分组.释放内存. + .wrapSource(key, group.timeout(windowSpan.plus(windowSpan), Mono.empty()))) + //按时间窗口分组 + .window(windowSpan) + .flatMap(source -> this + .handleWindow(key, + groupKey, + windowSpan, + source, + thresholdNumber, + isAlarmFirst)) + .onErrorResume(err -> { + log.warn("shake limit [{}] error", key, err); + return Mono.empty(); + }); + }, Integer.MAX_VALUE); + } + + protected Mono> handleWindow(String key, + String groupKey, + Duration duration, + Flux source, + long thresholdNumber, + boolean isAlarmFirst) { + //给数据打上索引,索引号就是告警次数 + return source + .index((index, data) -> Tuples.of(index + 1, data)) + .switchOnFirst((e, flux) -> { + if (e.hasValue()) { + @SuppressWarnings("all") + T ele = e.get().getT2(); + return flux.map(tp2 -> Tuples.of(tp2.getT1(), tp2.getT2(), ele)); + } + return flux.then(Mono.empty()); + }) + //超过阈值告警时 + .filter(tp -> tp.getT1() >= thresholdNumber) + .as(flux -> isAlarmFirst ? flux.take(1) : flux.takeLast(1))//取第一个或者最后一个 + .map(tp3 -> { + T next = isAlarmFirst ? tp3.getT3() : tp3.getT2(); + return new ShakeLimitResult<>(groupKey, tp3.getT1(), next); + }) + .singleOrEmpty(); + } +} diff --git a/jetlinks-manager/rule-engine-manager/pom.xml b/jetlinks-manager/rule-engine-manager/pom.xml index 3dc5b7bd..8ab8c789 100644 --- a/jetlinks-manager/rule-engine-manager/pom.xml +++ b/jetlinks-manager/rule-engine-manager/pom.xml @@ -44,6 +44,10 @@ ${project.version} + + org.hswebframework.web + hsweb-system-dictionary + diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmConstants.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmConstants.java index f34e3c71..ef10009f 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmConstants.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmConstants.java @@ -21,5 +21,10 @@ public interface AlarmConstants { String sourceType = "sourceType"; String sourceId = "sourceId"; String sourceName = "sourceName"; + + //告警条件 + String alarmFilterTermSpecs = "_filterTermSpecs"; + + String alarmConfigSource = "alarmCenter"; } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandleInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandleInfo.java index 5450c354..09120e91 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandleInfo.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandleInfo.java @@ -7,6 +7,7 @@ import org.jetlinks.community.rule.engine.enums.AlarmHandleType; import org.jetlinks.community.rule.engine.enums.AlarmRecordState; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; /** * @author bestfeng @@ -24,7 +25,7 @@ public class AlarmHandleInfo { private String alarmConfigId; @Schema(description = "告警时间") - @NotBlank + @NotNull private Long alarmTime; @Schema(description = "处理说明") @@ -33,9 +34,8 @@ public class AlarmHandleInfo { @Schema(description = "处理时间") private Long handleTime; - @NotBlank @Schema(description = "处理类型") - private AlarmHandleType type; + private String type; @Schema(description = "处理后的状态") @NotBlank diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandler.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandler.java new file mode 100644 index 00000000..2259c696 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandler.java @@ -0,0 +1,30 @@ +package org.jetlinks.community.rule.engine.alarm; + +import org.jetlinks.community.command.rule.data.AlarmInfo; +import org.jetlinks.community.command.rule.data.AlarmResult; +import org.jetlinks.community.command.rule.data.RelieveInfo; +import org.jetlinks.community.command.rule.data.RelieveResult; +import reactor.core.publisher.Mono; + + +/** + * 告警处理支持 + */ +public interface AlarmHandler { + + /** + * 触发告警 + * @see AlarmTaskExecutorProvider + * + * @return 告警触发结果 + */ + Mono triggerAlarm(AlarmInfo alarmInfo); + + /** + * 解除告警 + * @see AlarmTaskExecutorProvider + * + * @return 告警解除结果 + */ + Mono relieveAlarm(RelieveInfo relieveInfo); +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmRuleHandler.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmRuleHandler.java index 5ac0bb3c..ba945d0b 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmRuleHandler.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmRuleHandler.java @@ -18,8 +18,8 @@ import java.util.Map; * 告警规则数据处理器,当场景规则中配置的告警动作被执行时,将调用此处理器的相关方法. * * @author zhouhao - * @since 2.0 * @see AlarmTaskExecutorProvider + * @since 2.0 */ public interface AlarmRuleHandler { @@ -91,6 +91,12 @@ public interface AlarmRuleHandler { @Schema(description = "告警来源名称") private String sourceName; + /** + * 标识告警触发的配置来自什么业务功能 + */ + @Schema(description = "告警配置源") + private String alarmConfigSource = ConfigKey.alarmConfigSource; + public Result copyWith(AlarmTargetInfo targetInfo) { Result result = FastBeanCopier.copy(this, new Result()); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmHandler.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmHandler.java new file mode 100644 index 00000000..b1c4c9da --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmHandler.java @@ -0,0 +1,277 @@ +package org.jetlinks.community.rule.engine.alarm; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.i18n.LocaleUtils; +import org.hswebframework.web.id.IDGenerator; +import org.jetlinks.core.config.ConfigStorageManager; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.utils.Reactors; +import org.jetlinks.community.command.rule.data.AlarmInfo; +import org.jetlinks.community.command.rule.data.AlarmResult; +import org.jetlinks.community.command.rule.data.RelieveInfo; +import org.jetlinks.community.command.rule.data.RelieveResult; +import org.jetlinks.community.rule.engine.entity.AlarmHandleHistoryEntity; +import org.jetlinks.community.rule.engine.entity.AlarmHistoryInfo; +import org.jetlinks.community.rule.engine.entity.AlarmRecordEntity; +import org.jetlinks.community.rule.engine.enums.AlarmRecordState; +import org.jetlinks.community.rule.engine.service.AlarmHistoryService; +import org.jetlinks.community.rule.engine.service.AlarmRecordService; +import org.jetlinks.community.topic.Topics; +import org.jetlinks.community.utils.ObjectMappers; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import java.util.function.Function; + +@Slf4j +@AllArgsConstructor +@Component +public class DefaultAlarmHandler implements AlarmHandler { + + private final AlarmRecordService alarmRecordService; + + private final AlarmHistoryService historyService; + + private final ReactiveRepository handleHistoryRepository; + + private final EventBus eventBus; + + private final ConfigStorageManager storageManager; + + private final ApplicationEventPublisher eventPublisher; + + @Override + public Mono triggerAlarm(AlarmInfo alarmInfo) { + return getRecordCache(createRecordId(alarmInfo)) + .map(this::ofRecordCache) + .defaultIfEmpty(new AlarmResult()) + .flatMap(result -> { + AlarmRecordEntity record = ofRecord(result, alarmInfo); + //更新告警状态. + return alarmRecordService + .createUpdate() + .set(record) + .where(AlarmRecordEntity::getId, record.getId()) + .and(AlarmRecordEntity::getState, AlarmRecordState.warning) + .execute() + //更新数据库报错,依然尝试触发告警! + .onErrorResume(err -> { + log.error("trigger alarm error", err); + return Reactors.ALWAYS_ZERO; + }) + .flatMap(total -> { + AlarmHistoryInfo historyInfo = createHistory(record, alarmInfo); + + //更新结果返回0 说明是新产生的告警数据 + if (total == 0) { + result.setFirstAlarm(true); + result.setAlarming(false); + result.setAlarmTime(historyInfo.getAlarmTime()); + record.setAlarmTime(historyInfo.getAlarmTime()); + record.setHandleTime(null); + record.setHandleType(null); + + return this + .saveAlarmRecord(record) + .then(historyService.save(historyInfo)) + .then(publishAlarmRecord(historyInfo, alarmInfo)) + .then(publishEvent(historyInfo)) + .then(saveAlarmCache(result, record)); + } + result.setFirstAlarm(false); + result.setAlarming(true); + return historyService + .save(historyInfo) + .then(publishEvent(historyInfo)) + .then(saveAlarmCache(result, record)); + }); + }); + + } + + private Mono saveAlarmRecord(AlarmRecordEntity record){ + return alarmRecordService + .createUpdate() + .set(record) + .setNull(AlarmRecordEntity::getHandleTime) + .setNull(AlarmRecordEntity::getHandleType) + .where(AlarmRecordEntity::getId, record.getId()) + .execute() + .flatMap(update -> { + // 如果是首次告警需要手动保存 + if (update == 0) { + return alarmRecordService.save(record).then(); + } + return Mono.empty(); + }); + } + + @Override + public Mono relieveAlarm(RelieveInfo relieveInfo) { + return getRecordCache(createRecordId(relieveInfo)) + .map(this::ofRecordCache) + .defaultIfEmpty(new AlarmResult()) + .flatMap(result -> { + AlarmRecordEntity record = this.ofRecord(result, relieveInfo); + return Mono + .zip(alarmRecordService.changeRecordState( + relieveInfo.getAlarmRelieveType() + , AlarmRecordState.normal, + record.getId()), + this.updateRecordCache(record.getId(), DefaultAlarmRuleHandler.RecordCache::withNormal), + (total, ignore) -> total) + .flatMap(total -> { + //如果有数据被更新说明是正在告警中 + if (total > 0) { + result.setAlarming(true); + AlarmHistoryInfo historyInfo = this.createHistory(record, relieveInfo); + RelieveResult relieveResult = FastBeanCopier.copy(record, new RelieveResult()); + relieveResult.setRelieveTime(System.currentTimeMillis()); + relieveResult.setActualDesc(historyInfo.getActualDesc()); + relieveResult.setRelieveReason(relieveInfo.getRelieveReason()); + relieveResult.setDescribe(relieveInfo.getDescription()); + return saveAlarmHandleHistory(relieveInfo, record) + .then(publishAlarmRelieve(historyInfo, relieveInfo)) + .thenReturn(relieveResult); + } + return Mono.empty(); + }); + }); + } + + public AlarmRecordEntity ofRecord(AlarmResult result, AlarmInfo alarmData) { + AlarmRecordEntity entity = new AlarmRecordEntity(); + entity.setAlarmConfigId(alarmData.getAlarmConfigId()); + entity.setAlarmTime(result.getAlarmTime()); + entity.setState(AlarmRecordState.warning); + entity.setLevel(alarmData.getLevel()); + entity.setTargetType(alarmData.getTargetType()); + entity.setTargetName(alarmData.getTargetName()); + entity.setTargetId(alarmData.getTargetId()); + + entity.setSourceType(alarmData.getSourceType()); + entity.setSourceName(alarmData.getSourceName()); + entity.setSourceId(alarmData.getSourceId()); + + entity.setAlarmName(alarmData.getAlarmName()); + entity.setDescription(alarmData.getDescription()); + entity.setAlarmConfigSource(alarmData.getAlarmConfigSource()); + if (alarmData.getTermSpec() != null) { + entity.setTermSpec(alarmData.getTermSpec()); + entity.setTriggerDesc(alarmData.getTermSpec().getTriggerDesc()); + entity.setActualDesc(alarmData.getTermSpec().getActualDesc()); + } + entity.generateId(); + return entity; + } + + public AlarmHistoryInfo createHistory(AlarmRecordEntity record, AlarmInfo alarmInfo) { + AlarmHistoryInfo info = new AlarmHistoryInfo(); + info.setId(IDGenerator.RANDOM.generate()); + info.setAlarmConfigId(record.getAlarmConfigId()); + info.setAlarmConfigName(record.getAlarmName()); + info.setDescription(record.getDescription()); + info.setAlarmRecordId(record.getId()); + info.setLevel(record.getLevel()); + info.setAlarmTime(System.currentTimeMillis()); + info.setTriggerDesc(record.getTriggerDesc()); + info.setAlarmConfigSource(alarmInfo.getAlarmConfigSource()); + info.setActualDesc(record.getActualDesc()); + + info.setTargetName(record.getTargetName()); + info.setTargetId(record.getTargetId()); + info.setTargetType(record.getTargetType()); + + info.setSourceType(record.getSourceType()); + info.setSourceName(record.getSourceName()); + info.setSourceId(record.getSourceId()); + + info.setAlarmInfo(ObjectMappers.toJsonString(alarmInfo.getData())); + return info; + } + + public Mono publishAlarmRecord(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { + String topic = Topics.alarm(historyInfo.getTargetType(), historyInfo.getTargetId(), historyInfo.getAlarmConfigId()); + + return doPublishAlarmHistoryInfo(topic, historyInfo, alarmInfo); + } + + public Mono doPublishAlarmHistoryInfo(String topic, AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { + return Mono.just(topic) + .flatMap(assetTopic -> eventBus.publish(assetTopic, historyInfo)) + .then(); + } + + private Mono publishEvent(AlarmHistoryInfo historyInfo) { + return Mono.fromRunnable(() -> eventPublisher.publishEvent(historyInfo)); + } + + private Mono saveAlarmCache(AlarmResult result, + AlarmRecordEntity record) { + return this + .updateRecordCache(record.getId(), cache -> cache.with(result)) + .thenReturn(result); + } + + public Mono publishAlarmRelieve(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { + String topic = Topics.alarmRelieve(historyInfo.getTargetType(), historyInfo.getTargetId(), historyInfo.getAlarmConfigId()); + + return this.doPublishAlarmHistoryInfo(topic, historyInfo, alarmInfo); + } + + private Mono saveAlarmHandleHistory(RelieveInfo relieveInfo, AlarmRecordEntity record) { + AlarmHandleInfo alarmHandleInfo = new AlarmHandleInfo(); + alarmHandleInfo.setHandleTime(System.currentTimeMillis()); + alarmHandleInfo.setAlarmRecordId(record.getId()); + alarmHandleInfo.setAlarmConfigId(record.getAlarmConfigId()); + alarmHandleInfo.setAlarmTime(record.getAlarmTime()); + alarmHandleInfo.setState(AlarmRecordState.normal); + alarmHandleInfo.setType(relieveInfo.getAlarmRelieveType()); + alarmHandleInfo.setDescribe(getLocaleDescribe()); + // TODO: 2022/12/22 批量缓冲保存 + return handleHistoryRepository + .save(AlarmHandleHistoryEntity.of(alarmHandleInfo)) + .then(); + } + + private String getLocaleDescribe() { + return LocaleUtils.resolveMessage("message.scene_triggered_relieve_alarm", "场景触发解除告警"); + } + + + private String createRecordId(AlarmInfo alarmInfo) { + return AlarmRecordEntity.generateId(alarmInfo.getTargetId(), alarmInfo.getTargetType(), alarmInfo.getAlarmConfigId()); + } + + private Mono getRecordCache(String recordId) { + return storageManager + .getStorage("alarm-records") + .flatMap(store -> store + .getConfig(recordId) + .map(val -> val.as(DefaultAlarmRuleHandler.RecordCache.class))); + } + + private Mono updateRecordCache(String recordId, Function handler) { + return storageManager + .getStorage("alarm-records") + .flatMap(store -> store + .getConfig(recordId) + .map(val -> val.as(DefaultAlarmRuleHandler.RecordCache.class)) + .switchIfEmpty(Mono.fromSupplier(DefaultAlarmRuleHandler.RecordCache::new)) + .mapNotNull(handler) + .flatMap(cache -> store.setConfig(recordId, cache) + .thenReturn(cache))); + } + + + private AlarmResult ofRecordCache(DefaultAlarmRuleHandler.RecordCache cache) { + AlarmResult result = new AlarmResult(); + result.setAlarmTime(cache.alarmTime); + result.setLastAlarmTime(cache.lastAlarmTime); + result.setAlarming(cache.isAlarming()); + return result; + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmRuleHandler.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmRuleHandler.java index 232d2c88..b9f744c0 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmRuleHandler.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmRuleHandler.java @@ -11,32 +11,30 @@ import org.hswebframework.web.crud.events.EntityCreatedEvent; import org.hswebframework.web.crud.events.EntityDeletedEvent; import org.hswebframework.web.crud.events.EntityModifyEvent; import org.hswebframework.web.crud.events.EntitySavedEvent; -import org.hswebframework.web.i18n.LocaleUtils; -import org.hswebframework.web.id.IDGenerator; -import org.jetlinks.community.gateway.annotation.Subscribe; -import org.jetlinks.community.rule.engine.RuleEngineConstants; -import org.jetlinks.community.rule.engine.entity.*; -import org.jetlinks.community.rule.engine.enums.AlarmHandleType; -import org.jetlinks.community.rule.engine.enums.AlarmRecordState; -import org.jetlinks.community.rule.engine.enums.AlarmState; -import org.jetlinks.community.rule.engine.scene.SceneRule; -import org.jetlinks.community.rule.engine.service.AlarmConfigService; -import org.jetlinks.community.rule.engine.service.AlarmHistoryService; -import org.jetlinks.community.rule.engine.service.AlarmRecordService; -import org.jetlinks.community.topic.Topics; -import org.jetlinks.community.utils.ObjectMappers; import org.jetlinks.core.config.ConfigStorage; import org.jetlinks.core.config.ConfigStorageManager; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.event.Subscription; import org.jetlinks.core.utils.CompositeSet; -import org.jetlinks.core.utils.Reactors; +import org.jetlinks.community.command.rule.data.AlarmResult; +import org.jetlinks.community.command.rule.data.RelieveInfo; +import org.jetlinks.community.gateway.annotation.Subscribe; +import org.jetlinks.community.rule.engine.RuleEngineConstants; +import org.jetlinks.community.rule.engine.entity.AlarmConfigEntity; +import org.jetlinks.community.rule.engine.entity.AlarmHandleHistoryEntity; +import org.jetlinks.community.rule.engine.entity.AlarmRecordEntity; +import org.jetlinks.community.rule.engine.entity.AlarmRuleBindEntity; +import org.jetlinks.community.rule.engine.enums.AlarmState; +import org.jetlinks.community.rule.engine.scene.SceneRule; +import org.jetlinks.community.rule.engine.service.AlarmConfigService; +import org.jetlinks.community.rule.engine.service.AlarmRecordService; +import org.jetlinks.community.terms.TermSpec; +import org.jetlinks.community.utils.ConverterUtils; import org.jetlinks.reactor.ql.utils.CastUtils; import org.jetlinks.rule.engine.api.RuleData; import org.jetlinks.rule.engine.api.RuleDataHelper; import org.jetlinks.rule.engine.api.task.ExecutionContext; import org.springframework.boot.CommandLineRunner; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; @@ -50,7 +48,6 @@ import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; @Slf4j @AllArgsConstructor @@ -71,9 +68,7 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun private final Map, Set> ruleAlarmBinds = new ConcurrentHashMap<>(); private final AlarmRecordService alarmRecordService; - private final AlarmHistoryService historyService; private final ConfigStorageManager storageManager; - private final ApplicationEventPublisher eventPublisher; private final EventBus eventBus; @@ -83,35 +78,67 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun public final AlarmConfigService alarmConfigService; + public final AlarmHandler alarmHandler; + @Override public Flux triggered(ExecutionContext context, RuleData data) { return this .parseAlarmInfo(context, data) - .flatMap(this::triggerAlarm); + .flatMap(alarmInfo -> alarmHandler + .triggerAlarm(FastBeanCopier + .copy(alarmInfo, new org.jetlinks.community.command.rule.data.AlarmInfo())) + .map(info -> { + Result result = FastBeanCopier.copy(alarmInfo, new Result()); + FastBeanCopier.copy(info, result); + return result; + })); } @Override public Flux relieved(ExecutionContext context, RuleData data) { return this .parseAlarmInfo(context, data) - .flatMap(this::relieveAlarm); + .flatMap(alarmInfo -> { + // 已经被解除不重复更新 + if (alarmInfo.isCached() && !alarmInfo.isAlarming()) { + return Mono.empty(); + } + return alarmHandler + .relieveAlarm(FastBeanCopier.copy(alarmInfo, new RelieveInfo())) + .map(info -> { + Result result = FastBeanCopier.copy(alarmInfo, new Result()); + FastBeanCopier.copy(info, result); + return result; + }); + }); } private Flux parseAlarmInfo(ExecutionContext context, RuleData data) { if (ruleAlarmBinds.isEmpty()) { return Flux.empty(); } - //节点所在的条件分支索引 - int branchIndex = context + + //节点所在的执行动作索引 + int actionIndex = context .getJob() - .getConfiguration(SceneRule.ACTION_KEY_BRANCH_ID) + .getConfiguration(SceneRule.ACTION_KEY_ACTION_ID) .map(idx -> CastUtils.castNumber(idx).intValue()) .orElse(AlarmRuleBindEntity.ANY_BRANCH_INDEX); - Set alarmId = getBoundAlarmId(context.getInstanceId(), branchIndex); + Set alarmId = getBoundAlarmId(context.getInstanceId(), actionIndex); if (CollectionUtils.isEmpty(alarmId)) { - return Flux.empty(); + //节点所在的条件分支索引 + int branchIndex = context + .getJob() + .getConfiguration(SceneRule.ACTION_KEY_BRANCH_ID) + .map(idx -> CastUtils.castNumber(idx).intValue()) + .orElse(AlarmRuleBindEntity.ANY_BRANCH_INDEX); + + alarmId = getBoundAlarmId(context.getInstanceId(), branchIndex); + if (CollectionUtils.isEmpty(alarmId)) { + return Flux.empty(); + } } Map contextMap = RuleDataHelper.toContextMap(data); @@ -145,25 +172,6 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun } } - private AlarmRecordEntity ofRecord(Result result) { - AlarmRecordEntity entity = new AlarmRecordEntity(); - entity.setAlarmConfigId(result.getAlarmConfigId()); - entity.setState(AlarmRecordState.warning); - entity.setAlarmTime(System.currentTimeMillis()); - entity.setLevel(result.getLevel()); - entity.setTargetType(result.getTargetType()); - entity.setTargetName(result.getTargetName()); - entity.setTargetId(result.getTargetId()); - - entity.setSourceType(result.getSourceType()); - entity.setSourceName(result.getSourceName()); - entity.setSourceId(result.getSourceId()); - - entity.setAlarmName(result.getAlarmName()); - entity.setDescription(result.getDescription()); - entity.generateId(); - return entity; - } private Flux parseAlarm(ExecutionContext context, ConfigStorage alarm, Map contextMap) { return this @@ -182,152 +190,14 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun contextMap); result.setData(alarmData); - + result.setTermSpec(AlarmInfo.parseAlarmTrigger(context, contextMap)); return AlarmTarget .of(result.getTargetType()) .convert(alarmData) .map(result::copyWith); - }) - .flatMap(info -> this - .getRecordCache(info.createRecordId()) - .map(info::with) - .defaultIfEmpty(info)); - } - - private Mono relieveAlarm(AlarmInfo result) { - // 已经被解除不重复更新 - if (result.isCached() && !result.isAlarming()) { - return Mono.empty(); - } - - AlarmRecordEntity record = ofRecord(result); - return Mono - .zip(alarmRecordService.changeRecordState(AlarmRecordState.normal, record.getId()), - updateRecordCache(record.getId(), RecordCache::withNormal), - (total, ignore) -> total) - .flatMap(total -> { - //如果有数据被更新说明是正在告警中 - if (total > 0) { - result.setAlarming(true); - return saveAlarmHandleHistory(record); - } - return Mono.empty(); - }) - .thenReturn(result); - } - - private Mono saveAlarmHandleHistory(AlarmRecordEntity record) { - AlarmHandleInfo alarmHandleInfo = new AlarmHandleInfo(); - alarmHandleInfo.setHandleTime(System.currentTimeMillis()); - alarmHandleInfo.setAlarmRecordId(record.getId()); - alarmHandleInfo.setAlarmConfigId(record.getAlarmConfigId()); - alarmHandleInfo.setAlarmTime(record.getAlarmTime()); - alarmHandleInfo.setState(AlarmRecordState.normal); - alarmHandleInfo.setType(AlarmHandleType.system); - alarmHandleInfo.setDescribe(LocaleUtils.resolveMessage("message.scene_triggered_relieve_alarm", "场景触发解除告警")); - // TODO: 2022/12/22 批量缓冲保存 - return handleHistoryRepository - .save(AlarmHandleHistoryEntity.of(alarmHandleInfo)) - .then(); - } - - - private Mono triggerAlarm(AlarmInfo result) { - AlarmRecordEntity record = ofRecord(result); - - //更新告警状态. - return alarmRecordService - .createUpdate() - .set(record) - .where(AlarmRecordEntity::getId, record.getId()) - .and(AlarmRecordEntity::getState, AlarmRecordState.warning) - .execute() - //更新数据库报错,依然尝试触发告警! - .onErrorResume(err -> { - log.error("trigger alarm error", err); - return Reactors.ALWAYS_ZERO; - }) - .flatMap(total -> { - AlarmHistoryInfo historyInfo = createHistory(record, result); - result.setAlarmTime(record.getAlarmTime()); - - //更新结果返回0 说明是新产生的告警数据 - if (total == 0) { - result.setFirstAlarm(true); - result.setAlarming(false); - - return alarmRecordService - .save(record) - .then(historyService.save(historyInfo)) - .then(publishAlarmRecord(historyInfo, result)) - .then(publishEvent(historyInfo)) - .then(saveAlarmCache(result, record)); - } - result.setFirstAlarm(false); - result.setAlarming(true); - - return historyService - .save(historyInfo) - .then(publishEvent(historyInfo)) - .then(saveAlarmCache(result, record)); }); } - private Mono publishEvent(AlarmHistoryInfo historyInfo) { - return Mono.fromRunnable(() -> eventPublisher.publishEvent(historyInfo)); - } - - private AlarmHistoryInfo createHistory(AlarmRecordEntity record, AlarmInfo alarmInfo) { - AlarmHistoryInfo info = new AlarmHistoryInfo(); - info.setId(IDGenerator.RANDOM.generate()); - info.setAlarmConfigId(record.getAlarmConfigId()); - info.setAlarmConfigName(record.getAlarmName()); - info.setDescription(record.getDescription()); - info.setAlarmRecordId(record.getId()); - info.setLevel(record.getLevel()); - info.setAlarmTime(record.getAlarmTime()); - - info.setTargetName(record.getTargetName()); - info.setTargetId(record.getTargetId()); - info.setTargetType(record.getTargetType()); - - info.setSourceType(record.getSourceType()); - info.setSourceName(record.getSourceName()); - info.setSourceId(record.getSourceId()); - - - info.setAlarmInfo(ObjectMappers.toJsonString(alarmInfo.getData().getOutput())); - return info; - } - - public Mono publishAlarmRecord(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { - String topic = Topics.alarm(historyInfo.getTargetType(), historyInfo.getTargetId(), historyInfo.getAlarmConfigId()); - return eventBus - .publish(topic, historyInfo) - .then(); - } - - private Mono saveAlarmCache(AlarmInfo result, - AlarmRecordEntity record) { - - return this - .updateRecordCache(record.getId(), cache -> cache.with(result)) - .thenReturn(result); - -// return this -// .getAlarmStorage(result.getAlarmConfigId()) -// .flatMap(store -> { -// Map configs = new HashMap<>(); -// -// configs.put(AlarmConstants.ConfigKey.lastAlarmTime, record.getAlarmTime()); -// if (!result.isAlarming()) { -// configs.put(AlarmConstants.ConfigKey.alarmTime, record.getAlarmTime()); -// } -// return store.setConfigs(configs); -// }) -// .thenReturn(result); - } - private Mono getAlarmInfo(ConfigStorage alarm) { return alarm .getConfigs(configInfoKey) @@ -338,7 +208,6 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun .equals(AlarmState.disabled.name())) { return null; } - AlarmInfo result = FastBeanCopier.copy(values.getAllValues(), new AlarmInfo()); if (result.getAlarmConfigId() == null || @@ -389,7 +258,15 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun public void handleConfigEvent(EntityDeletedEvent event) { event.async( Flux.fromIterable(event.getEntity()) - .flatMap(e -> eventBus.publish(TOPIC_ALARM_CONFIG_DELETE, e)) + .flatMap(e -> eventBus + .publish(TOPIC_ALARM_CONFIG_DELETE, e) + .then( + // 同步删除告警记录 + alarmRecordService + .createDelete() + .where(AlarmRecordEntity::getAlarmConfigId, e.getId()) + .execute()) + .then()) ); } @@ -498,8 +375,28 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun private AlarmData data; + /** + * 告警触发条件 + */ + private TermSpec termSpec; + private boolean cached; + private List> bindings; + + public static TermSpec parseAlarmTrigger(ExecutionContext context, Map contextMap) { + TermSpec termSpec = new TermSpec(); + Map configuration = context.getJob().getConfiguration(); + Object termSpecs = configuration.get(AlarmConstants.ConfigKey.alarmFilterTermSpecs); + if (termSpecs != null) { + termSpec.setChildren(ConverterUtils.convertToList(termSpecs,o -> FastBeanCopier.copy(o, TermSpec.class))); + return termSpec.apply(contextMap); + + } + return termSpec; + } + + @Override public AlarmInfo copyWith(AlarmTargetInfo targetInfo) { AlarmInfo result = FastBeanCopier.copy(this, new AlarmInfo()); @@ -513,49 +410,17 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun return result; } - - public AlarmInfo with(RecordCache cache) { - this.setAlarmTime(cache.alarmTime); - this.setLastAlarmTime(cache.lastAlarmTime); - this.setAlarming(cache.isAlarming()); - this.cached = true; - return this; - } - - public String createRecordId() { - return AlarmRecordEntity.generateId(getTargetId(), getTargetType(), getAlarmConfigId()); - } } - private Mono getRecordCache(String recordId) { - return storageManager - .getStorage("alarm-records") - .flatMap(store -> store - .getConfig(recordId) - .map(val -> val.as(RecordCache.class))); - } - - private Mono updateRecordCache(String recordId, Function handler) { - return storageManager - .getStorage("alarm-records") - .flatMap(store -> store - .getConfig(recordId) - .map(val -> val.as(RecordCache.class)) - .switchIfEmpty(Mono.fromSupplier(RecordCache::new)) - .mapNotNull(handler) - .flatMap(cache -> store.setConfig(recordId, cache) - .thenReturn(cache))); - } - public static class RecordCache implements Externalizable { static final byte stateNormal = 0x01; static final byte stateAlarming = 0x02; byte state; - long alarmTime; - long lastAlarmTime; + public long alarmTime; + public long lastAlarmTime; public boolean isAlarming() { @@ -572,13 +437,13 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun return this; } - public RecordCache with(Result record) { + public RecordCache with(AlarmResult result) { - this.lastAlarmTime = this.alarmTime == 0 ? record.getAlarmTime() : this.alarmTime; + this.lastAlarmTime = this.alarmTime == 0 ? result.getAlarmTime() : this.alarmTime; - this.alarmTime = record.getAlarmTime(); + this.alarmTime = result.getAlarmTime(); - if (record.isAlarming() || record.isFirstAlarm()) { + if (result.isAlarming() || result.isFirstAlarm()) { this.state = stateAlarming; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineManagerConfiguration.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineManagerConfiguration.java index 402e2a30..532c8110 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineManagerConfiguration.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineManagerConfiguration.java @@ -14,7 +14,6 @@ import org.springframework.context.annotation.Configuration; @AutoConfiguration public class RuleEngineManagerConfiguration { - @Bean public SceneTaskExecutorProvider sceneTaskExecutorProvider(EventBus eventBus, ObjectProvider filters, diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigDetail.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigDetail.java index 54965279..2a2ee176 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigDetail.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigDetail.java @@ -6,7 +6,6 @@ import lombok.Setter; import org.hswebframework.web.bean.FastBeanCopier; import org.jetlinks.community.rule.engine.enums.AlarmState; import org.jetlinks.community.rule.engine.enums.RuleInstanceState; -import org.jetlinks.community.rule.engine.scene.TriggerType; import org.jetlinks.community.rule.engine.scene.internal.triggers.ManualTriggerProvider; import javax.persistence.Column; @@ -59,12 +58,21 @@ public class AlarmConfigDetail { ) private Long createTime; + @Schema( + description = "创建者名称(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorName; + @Schema(description = "更新者ID", accessMode = Schema.AccessMode.READ_ONLY) private String modifierId; @Schema(description = "更新时间") private Long modifyTime; + @Schema(description = "修改人名称") + private String modifierName; + public static AlarmConfigDetail of(AlarmConfigEntity entity) { return FastBeanCopier.copy(entity, new AlarmConfigDetail(), "sceneTriggerType"); } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigEntity.java index 10269369..162aff97 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigEntity.java @@ -86,6 +86,13 @@ public class AlarmConfigEntity extends GenericEntity implements RecordCr ) private Long createTime; + @Column(name = "creator_name", updatable = false) + @Schema( + description = "创建者名称(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorName; + @Column(length = 64) @Schema(description = "更新者ID", accessMode = Schema.AccessMode.READ_ONLY) private String modifierId; @@ -95,6 +102,10 @@ public class AlarmConfigEntity extends GenericEntity implements RecordCr @Schema(description = "更新时间") private Long modifyTime; + @Column(length = 64) + @Schema(description = "修改人名称") + private String modifierName; + public Map toConfigMap() { Map configs = new HashMap<>(); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHandleHistoryEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHandleHistoryEntity.java index 61495ec4..6ecceeb6 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHandleHistoryEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHandleHistoryEntity.java @@ -10,15 +10,22 @@ import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; import org.hswebframework.web.crud.generator.Generators; +import org.hswebframework.web.dict.EnumDict; +import org.jetlinks.community.dictionary.Dictionary; import org.jetlinks.community.rule.engine.alarm.AlarmHandleInfo; import org.jetlinks.community.rule.engine.enums.AlarmHandleType; +import org.jetlinks.community.rule.engine.service.AlarmHandleTypeDictInit; +import org.springframework.util.StringUtils; import javax.persistence.Column; +import javax.persistence.Index; import javax.persistence.Table; @Getter @Setter -@Table(name = "alarm_handle_history") +@Table(name = "alarm_handle_history", indexes = { + @Index(name = "idx_ahh_alarm_record_id", columnList = "alarmRecordId") +}) @Comment("告警处理记录") public class AlarmHandleHistoryEntity extends GenericEntity implements RecordCreationEntity { @@ -30,11 +37,11 @@ public class AlarmHandleHistoryEntity extends GenericEntity implements R @Schema(description = "告警记录Id") private String alarmRecordId; - @Column(length = 64, nullable = false, updatable = false) + @Column(length = 32) + @Dictionary(AlarmHandleTypeDictInit.DICT_ID) @Schema(description = "告警处理类型") - @EnumCodec @ColumnType(javaType = String.class) - private AlarmHandleType handleType; + private EnumDict handleType; @Column(length = 256, nullable = false, updatable = false) @Schema(description = "说明") @@ -63,12 +70,21 @@ public class AlarmHandleHistoryEntity extends GenericEntity implements R ) private Long createTime; + @Column(name = "creator_name", updatable = false) + @Schema( + description = "创建者名称(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorName; + + @SuppressWarnings("all") public static AlarmHandleHistoryEntity of(AlarmHandleInfo handleInfo) { AlarmHandleHistoryEntity entity = new AlarmHandleHistoryEntity(); entity.setAlarmId(handleInfo.getAlarmConfigId()); entity.setAlarmRecordId(handleInfo.getAlarmRecordId()); entity.setAlarmTime(handleInfo.getAlarmTime()); - entity.setHandleType(handleInfo.getType()); + String type = handleInfo.getType(); + entity.setHandleType(StringUtils.hasText(type) ? EnumDict.create(type) : AlarmHandleType.system); entity.setDescription(handleInfo.getDescribe()); entity.setHandleTime(handleInfo.getHandleTime() == null ? System.currentTimeMillis() : handleInfo.getHandleTime()); return entity; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHistoryInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHistoryInfo.java index b39f88ef..97a22083 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHistoryInfo.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHistoryInfo.java @@ -6,6 +6,7 @@ import lombok.Getter; import lombok.Setter; import org.jetlinks.community.rule.engine.alarm.AlarmTargetInfo; import org.jetlinks.community.rule.engine.scene.SceneData; +import org.jetlinks.community.terms.TermSpec; import java.io.Serializable; import java.util.*; @@ -57,9 +58,28 @@ public class AlarmHistoryInfo implements Serializable { @Schema(description = "告警信息") private String alarmInfo; - @Schema(description = "绑定信息") - private List> bindings; + @Schema(description = "创建者ID") + private String creatorId; + @Schema(description = "触发条件") + private TermSpec termSpec; + + @Schema(description = "告警配置源") + private String alarmConfigSource; + + @Schema(description = "触发条件描述") + private String triggerDesc; + + @Schema(description = "告警原因描述") + private String actualDesc; + + public void withTermSpec(TermSpec termSpec){ + if (termSpec != null) { + this.setTermSpec(termSpec); + this.setTriggerDesc(termSpec.getTriggerDesc()); + this.setActualDesc(termSpec.getActualDesc()); + } + } @Deprecated public static AlarmHistoryInfo of(String alarmRecordId, @@ -84,7 +104,6 @@ public class AlarmHistoryInfo implements Serializable { info.setAlarmInfo(JSON.toJSONString(data.getOutput())); info.setDescription(alarmConfig.getDescription()); - info.setBindings(convertBindings(targetInfo, data, alarmConfig)); return info; } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRecordEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRecordEntity.java index e9b9cb2f..30b65fe2 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRecordEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRecordEntity.java @@ -3,20 +3,26 @@ package org.jetlinks.community.rule.engine.entity; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; -import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; -import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; -import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; -import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec; +import org.hswebframework.ezorm.rdb.mapping.annotation.*; import org.hswebframework.web.api.crud.entity.GenericEntity; +import org.hswebframework.web.dict.EnumDict; import org.hswebframework.web.utils.DigestUtils; +import org.jetlinks.community.dictionary.Dictionary; import org.jetlinks.community.rule.engine.enums.AlarmRecordState; +import org.jetlinks.community.rule.engine.service.AlarmHandleTypeDictInit; +import org.jetlinks.community.terms.TermSpec; import javax.persistence.Column; +import javax.persistence.Index; import javax.persistence.Table; +import java.sql.JDBCType; @Getter @Setter -@Table(name = "alarm_record") +@Table(name = "alarm_record",indexes = { + @Index(name = "idx_alarm_rec_t_type",columnList = "targetType"), + @Index(name = "idx_alarm_rec_t_key",columnList = "targetKey") +}) @Comment("告警记录") public class AlarmRecordEntity extends GenericEntity { @@ -57,6 +63,19 @@ public class AlarmRecordEntity extends GenericEntity { @Schema(description = "告警源名称") private String sourceName; + @Column + @ColumnType(jdbcType = JDBCType.LONGVARCHAR) + @JsonCodec + @Schema(description = "触发条件") + private TermSpec termSpec; + + @Column(length = 1024) + @Schema(description = "触发条件描述") + private String triggerDesc; + + @Column(length = 1024) + @Schema(description = "告警原因描述") + private String actualDesc; @Column @Schema(description = "最近一次告警时间") @@ -77,6 +96,20 @@ public class AlarmRecordEntity extends GenericEntity { @DefaultValue("normal") private AlarmRecordState state; + + @Column(length = 32) + @Dictionary(AlarmHandleTypeDictInit.DICT_ID) + @Schema(description = "告警处理类型") + @ColumnType(javaType = String.class) + private EnumDict handleType; + + /** + * 标识告警触发的配置来自什么业务功能 + */ + @Column + @Schema(description = "告警配置源") + private String alarmConfigSource; + @Column @Schema(description = "说明") private String description; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRuleBindEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRuleBindEntity.java index 316734eb..24c16fb2 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRuleBindEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRuleBindEntity.java @@ -15,8 +15,8 @@ import javax.persistence.Table; import javax.validation.constraints.NotBlank; @Table(name = "s_alarm_rule_bind", indexes = { - @Index(name = "idx_alarm_rule_aid", columnList = "alarmId"), - @Index(name = "idx_alarm_rule_rid", columnList = "ruleId"), + @Index(name = "idx_alarm_rule_aid", columnList = "alarmId"), + @Index(name = "idx_alarm_rule_rid", columnList = "ruleId"), }) @Getter @Setter @@ -28,7 +28,7 @@ public class AlarmRuleBindEntity extends GenericEntity { @Column(nullable = false, updatable = false, length = 64) @NotBlank - @Schema(description = "告警配置ID") + @Schema(description = "告警ID") private String alarmId; @Column(nullable = false, updatable = false, length = 64) diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java index 7ae195bc..0e0ca459 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java @@ -10,6 +10,7 @@ import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.api.crud.entity.RecordModifierEntity; import org.hswebframework.web.crud.annotation.EnableEntityEvent; import org.hswebframework.web.crud.generator.Generators; import org.hswebframework.web.validator.CreateGroup; @@ -31,7 +32,7 @@ import java.sql.JDBCType; @Table(name = "rule_instance") @Comment("规则实例表") @EnableEntityEvent -public class RuleInstanceEntity extends GenericEntity implements RecordCreationEntity { +public class RuleInstanceEntity extends GenericEntity implements RecordCreationEntity, RecordModifierEntity { @Override @GeneratedValue(generator = "snow_flake") @@ -76,6 +77,25 @@ public class RuleInstanceEntity extends GenericEntity implements RecordC @Schema(description = "创建者ID") private String creatorId; + @Column(length = 64) + @Schema( + description = "修改人ID" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String modifierId; + + @Column + @DefaultValue(generator = Generators.CURRENT_TIME) + @Schema( + description = "修改时间" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private Long modifyTime; + + @Column(length = 64) + @Schema(description = "修改人名称") + private String modifierName; + @Column(name = "state", length = 16) @EnumCodec @ColumnType(javaType = String.class) diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/SceneEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/SceneEntity.java index eb7a55e5..10d71bc9 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/SceneEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/SceneEntity.java @@ -18,9 +18,12 @@ import org.hswebframework.web.exception.BusinessException; import org.jetlinks.community.rule.engine.RuleEngineConstants; import org.jetlinks.community.rule.engine.enums.RuleInstanceState; import org.jetlinks.community.rule.engine.enums.SceneFeature; -import org.jetlinks.community.rule.engine.scene.*; -import org.jetlinks.rule.engine.api.model.RuleModel; +import org.jetlinks.community.rule.engine.scene.SceneAction; +import org.jetlinks.community.rule.engine.scene.SceneConditionAction; +import org.jetlinks.community.rule.engine.scene.SceneRule; +import org.jetlinks.community.rule.engine.scene.Trigger; import org.jetlinks.rule.engine.cluster.RuleInstance; +import reactor.core.publisher.Mono; import javax.persistence.Column; import javax.persistence.Table; @@ -43,8 +46,8 @@ public class SceneEntity extends GenericEntity implements RecordCreation @Schema(description = "触发器类型") @Column(length = 32, nullable = false, updatable = false) -// @EnumCodec -// @ColumnType(javaType = String.class) + // @EnumCodec + // @ColumnType(javaType = String.class) @NotNull private String triggerType; @@ -87,6 +90,13 @@ public class SceneEntity extends GenericEntity implements RecordCreation @DefaultValue(generator = Generators.CURRENT_TIME) private Long createTime; + @Column(name = "creator_name", updatable = false) + @Schema( + description = "创建者名称(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorName; + @Column(length = 64) @Schema(description = "修改人") private String modifierId; @@ -96,6 +106,10 @@ public class SceneEntity extends GenericEntity implements RecordCreation @DefaultValue(generator = Generators.CURRENT_TIME) private Long modifyTime; + @Column(length = 64) + @Schema(description = "修改人名称") + private String modifierName; + @Column @Schema(description = "启动时间") private Long startTime; @@ -125,16 +139,20 @@ public class SceneEntity extends GenericEntity implements RecordCreation @Schema(description = "说明") private String description; - public RuleInstance toRule() { + public Mono toRule() { SceneRule rule = copyTo(new SceneRule()); - RuleInstance instance = new RuleInstance(); - instance.setId(getId()); - RuleModel model = rule.toModel(); - model.addConfiguration(RuleEngineConstants.ruleCreatorIdKey, modifierId); - model.addConfiguration(RuleEngineConstants.ruleName, getName()); - instance.setModel(model); - return instance; + return rule + .toModel() + .map(model -> { + RuleInstance instance = new RuleInstance(); + instance.setId(getId()); + model.addConfiguration(RuleEngineConstants.ruleCreatorIdKey, modifierId); + model.addConfiguration(RuleEngineConstants.ruleName, getName()); + instance.setModel(model); + return instance; + }); + } public SceneEntity with(SceneRule rule) { @@ -146,8 +164,8 @@ public class SceneEntity extends GenericEntity implements RecordCreation public void validate() { getTrigger().validate(); - if (CollectionUtils.isEmpty(getActions()) && CollectionUtils.isEmpty(getBranches())){ - throw new BusinessException("error.scene_action_rule_cannot_be_null"); - } + if (CollectionUtils.isEmpty(getActions()) && CollectionUtils.isEmpty(getBranches())) { + throw new BusinessException("error.scene_action_rule_cannot_be_null"); + } } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleType.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleType.java index 3a2186aa..6dd6e2f6 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleType.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleType.java @@ -2,11 +2,14 @@ package org.jetlinks.community.rule.engine.enums; import lombok.AllArgsConstructor; import lombok.Getter; -import org.hswebframework.web.dict.I18nEnumDict; +import org.hswebframework.web.dict.EnumDict; +/** + * @author bestfeng + */ @AllArgsConstructor @Getter -public enum AlarmHandleType implements I18nEnumDict { +public enum AlarmHandleType implements EnumDict { system("系统"), user("人工"); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/RuleInstanceState.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/RuleInstanceState.java index 34b462da..6cd60fa8 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/RuleInstanceState.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/RuleInstanceState.java @@ -3,12 +3,12 @@ package org.jetlinks.community.rule.engine.enums; import lombok.AllArgsConstructor; import lombok.Getter; import org.hswebframework.web.dict.Dict; -import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.I18nEnumDict; @Getter @AllArgsConstructor @Dict( "rule-instance-state") -public enum RuleInstanceState implements EnumDict { +public enum RuleInstanceState implements I18nEnumDict { started("正常"), disable("禁用"); private final String text; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/AbstractSceneTriggerProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/AbstractSceneTriggerProvider.java new file mode 100644 index 00000000..c0f57a85 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/AbstractSceneTriggerProvider.java @@ -0,0 +1,39 @@ +package org.jetlinks.community.rule.engine.scene; + +import lombok.Getter; +import lombok.Setter; +import org.jetlinks.community.rule.engine.commons.ShakeLimit; +import org.jetlinks.community.rule.engine.commons.ShakeLimitProvider; +import org.jetlinks.community.rule.engine.commons.ShakeLimitResult; +import org.jetlinks.community.rule.engine.commons.impl.SimpleShakeLimitProvider; +import reactor.core.publisher.Flux; + +import java.util.Map; + +import static org.jetlinks.community.rule.engine.scene.SceneRule.SOURCE_ID_KEY; + +@Setter +@Getter +public abstract class AbstractSceneTriggerProvider + implements SceneTriggerProvider { + + + private String shakeLimitProvider = SimpleShakeLimitProvider.PROVIDER; + + protected String getShakeLimitKey(Map data) { + return String.valueOf(data.get(SOURCE_ID_KEY)); + } + + @Override + public final Flux>> shakeLimit(String key, + Flux> source, + ShakeLimit limit) { + return ShakeLimitProvider + .supports + .get(shakeLimitProvider) + .orElse(SimpleShakeLimitProvider.GLOBAL) + .shakeLimit(key, + source.groupBy(this::getShakeLimitKey, Integer.MAX_VALUE), + limit); + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceOperation.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceOperation.java index ec6b8849..ef3c8499 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceOperation.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceOperation.java @@ -9,20 +9,20 @@ import org.jetlinks.core.message.function.FunctionInvokeMessage; import org.jetlinks.core.message.function.FunctionParameter; import org.jetlinks.core.message.property.ReadPropertyMessage; import org.jetlinks.core.message.property.WritePropertyMessage; +import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.types.BooleanType; import org.jetlinks.core.metadata.types.DateTimeType; import org.jetlinks.core.metadata.types.ObjectType; +import org.jetlinks.core.metadata.types.UnknownType; import org.jetlinks.core.things.ThingMetadata; import org.jetlinks.community.TimerSpec; import org.jetlinks.community.rule.engine.scene.term.TermColumn; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; import javax.validation.constraints.NotNull; import java.util.*; import java.util.function.BiConsumer; -import java.util.function.Function; import java.util.stream.Collectors; import static org.jetlinks.core.metadata.SimplePropertyMetadata.of; @@ -104,33 +104,34 @@ public class DeviceOperation { List terms = new ArrayList<>(32); //服务器时间 // _now terms.add(TermColumn.of("_now", - resolveI18n("message.scene_term_column_now", "服务器时间"), - DateTimeType.GLOBAL, - resolveI18n("message.scene_term_column_now_desc", "收到设备数据时,服务器的时间."))); + "message.scene_term_column_now", + "服务器时间", + DateTimeType.GLOBAL, + "收到设备数据时,服务器的时间.")); //数据上报时间 // timestamp terms.add(TermColumn.of("timestamp", - resolveI18n("message.scene_term_column_timestamp", "数据上报时间"), - DateTimeType.GLOBAL, - resolveI18n("message.scene_term_column_timestamp_desc", "设备上报的数据中指定的时间."))); + "message.scene_term_column_timestamp", + "数据上报时间", + DateTimeType.GLOBAL, + "设备上报的数据中指定的时间.")); //下发指令操作可以判断结果 if (operator == Operator.readProperty || operator == Operator.writeProperty || operator == Operator.invokeFunction) { terms.add(TermColumn.of("success", - resolveI18n("message.scene_term_column_event_success", "场景触发是否成功"), - BooleanType.GLOBAL)); + "message.scene_term_column_event_success", + "场景触发是否成功", + BooleanType.GLOBAL)); } //属性相关 if (operator == Operator.readProperty || operator == Operator.reportProperty - || operator == Operator.readPropertyReply || operator == Operator.writeProperty) { terms.addAll( this.createTerm( metadata.getProperties(), - (property, column) -> column.setChildren(createTermColumn("properties", property, true, PropertyValueType - .values())), + (property, column) -> column.setChildren(createTermColumn("properties", property, true, PropertyValueType.values())), LocaleUtils.resolveMessage("message.device_metadata_property", "属性")) ); } else { @@ -138,7 +139,12 @@ public class DeviceOperation { terms.addAll( this.createTerm( metadata.getProperties(), - (property, column) -> column.setChildren(createTermColumn("properties", property, true, PropertyValueType.last)), + (property, column) -> column.setChildren( + createTermColumn( + "properties", + property, + true, + PropertyValueType.last, PropertyValueType.lastTime)), LocaleUtils.resolveMessage("message.device_metadata_property", "属性"))); } @@ -147,53 +153,40 @@ public class DeviceOperation { terms.addAll( this.createTerm( metadata.getEvent(eventId) - .>map(event -> Collections - .singletonList( - of("data", - event.getName(), - event.getType()) - )) - .orElse(Collections.emptyList()), - (property, column) -> column.setChildren(createTermColumn("event", property, false)))); + .>map(event -> Collections + .singletonList( + of("data", + event.getName(), + event.getType()) + )) + .orElse(Collections.emptyList()), + (property, column) -> column.setChildren(createTermColumn("event", property, false)), + LocaleUtils.resolveMessage("message.device_metadata_event", "事件"))); } //调用功能 if (operator == Operator.invokeFunction) { terms.addAll( this.createTerm( metadata.getFunction(functionId) - .>map(meta -> Collections.singletonList( - of("output", - meta.getName(), - meta.getOutput())) - ) - .orElse(Collections.emptyList()), - (property, column) -> column.setChildren(createTermColumn("function", property, false)))); + //过滤掉异步功能和无返回值功能的参数输出 + .filter(fun -> !fun.isAsync() && !(fun.getOutput() instanceof UnknownType)) + .>map(meta -> Collections.singletonList( + of("output", + meta.getName(), + meta.getOutput())) + ) + .orElse(Collections.emptyList()), + (property, column) -> column.setChildren(createTermColumn("function", property, false)), + LocaleUtils.resolveMessage("message.device_metadata_function", "功能调用"))); } - Map allColumn = terms - .stream() - .collect(Collectors.toMap(TermColumn::getColumn, Function.identity(), (a, b) -> a)); - for (TermColumn term : terms) { - term.refactorDescription(allColumn::get); - term.refactorFullName(null); - } - return terms; + return TermColumn.refactorTermsInfo("properties", terms); } private String resolveI18n(String key, String name) { return LocaleUtils.resolveMessage(key, name); } - private String appendColumn(String... columns) { - StringJoiner joiner = new StringJoiner("."); - for (String column : columns) { - if (StringUtils.hasText(column)) { - joiner.add(column); - } - } - return joiner.toString(); - } - private List createTermColumn(String prefix, PropertyMetadata property, boolean last, PropertyValueType... valueTypes) { //对象类型嵌套 if (property.getValueType() instanceof ObjectType) { @@ -216,16 +209,22 @@ public class DeviceOperation { } else { if (!last) { return Collections.singletonList( - TermColumn.of(appendColumn(prefix, property.getId()), - property.getName(), property.getValueType()) - .withMetrics(property) - .withMetadataTrue() + TermColumn.of(SceneUtils.appendColumn(prefix, property.getId()), + property.getName(), property.getValueType()) + .withMetrics(property) + .withMetadataTrue() ); } return Arrays .stream(valueTypes) .map(type -> TermColumn - .of(appendColumn(prefix, property.getId(), type.name()), type.getName(), property.getValueType()) + .of(SceneUtils + .appendColumn(prefix, + property.getId(), + type.name()), + type.getKey(), + null, + type.dataType == null ? property.getValueType() : type.dataType) .withMetrics(property) .withMetadataTrue() ) @@ -253,24 +252,21 @@ public class DeviceOperation { case online: case offline: case reportProperty: - case readPropertyReply: return; case reportEvent: Assert.hasText(eventId, "error.scene_rule_trigger_device_operation_event_id_cannot_be_null"); return; case readProperty: Assert.notEmpty(readProperties, - "error.scene_rule_trigger_device_operation_read_property_cannot_be_empty"); + "error.scene_rule_trigger_device_operation_read_property_cannot_be_empty"); return; case writeProperty: Assert.notEmpty(writeProperties, - "error.scene_rule_trigger_device_operation_write_property_cannot_be_empty"); + "error.scene_rule_trigger_device_operation_write_property_cannot_be_empty"); return; case invokeFunction: Assert.hasText(functionId, - "error.scene_rule_trigger_device_operation_function_id_cannot_be_null"); - Assert.notEmpty(functionParameters, - "error.scene_rule_trigger_device_operation_function_parameter_cannot_be_empty"); + "error.scene_rule_trigger_device_operation_function_id_cannot_be_null"); } } @@ -283,8 +279,6 @@ public class DeviceOperation { reportProperty, //读取属性 readProperty, - //读取属性回复 - readPropertyReply, //修改属性 writeProperty, //调用功能 @@ -295,13 +289,16 @@ public class DeviceOperation { @AllArgsConstructor @Getter public enum PropertyValueType { - current("message.property_value_type_current"), - recent("message.property_value_type_recent"), - last("message.property_value_type_last"), + current("message.property_value_type_current", null), + recent("message.property_value_type_recent", null), + last("message.property_value_type_last", null), + lastTime("message.property_value_type_last_time", DateTimeType.GLOBAL), ; private final String key; + private final DataType dataType; + public String getName() { return LocaleUtils.resolveMessage(key); } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneAction.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneAction.java index 15be6175..1fc29c95 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneAction.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneAction.java @@ -8,28 +8,27 @@ import org.apache.commons.collections4.MapUtils; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.i18n.LocaleUtils; -import org.jetlinks.community.rule.engine.scene.internal.actions.*; import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.metadata.PropertyMetadata; -import org.jetlinks.core.metadata.types.*; +import org.jetlinks.core.metadata.types.ObjectType; import org.jetlinks.community.reactorql.term.TermTypes; +import org.jetlinks.community.rule.engine.scene.internal.actions.*; +import org.jetlinks.community.terms.TermSpec; import org.jetlinks.community.utils.ConverterUtils; import org.jetlinks.rule.engine.api.model.RuleNodeModel; import reactor.core.publisher.Flux; import javax.validation.constraints.NotNull; import java.io.Serializable; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.function.Consumer; import static org.hswebframework.web.i18n.LocaleUtils.resolveMessage; import static org.jetlinks.community.rule.engine.scene.SceneRule.createBranchActionId; -/** - * @see org.jetlinks.community.rule.engine.executor.TimerTaskExecutorProvider - * @see org.jetlinks.community.rule.engine.executor.DelayTaskExecutorProvider - * @see org.jetlinks.community.rule.engine.executor.DeviceMessageSendTaskExecutorProvider - */ @Getter @Setter public class SceneAction implements Serializable { @@ -80,15 +79,16 @@ public class SceneAction implements Serializable { /** * 尝试从动作的变量中提取出需要动态获取的列信息 + * * @return 条件 */ private List parseActionTerms() { + return SceneProviders .getActionProviderNow(executor) .parseColumns(actionConfig()); } - public List createContextColumns() { List termList = new ArrayList<>(); termList.addAll(parseColumnFromOptions(options)); @@ -96,6 +96,7 @@ public class SceneAction implements Serializable { return termList; } + public Object actionConfig() { switch (executor) { case Executor.device: @@ -126,7 +127,7 @@ public class SceneAction implements Serializable { } - private static Variable createVariable(Integer branchIndex, Integer group, int actionIndex, List children) { + public static Variable createVariable(Integer branchIndex, Integer group, int actionIndex, List children) { String varId = "action_" + actionIndex; @@ -180,14 +181,19 @@ public class SceneAction implements Serializable { SceneProviders .getActionProviderNow(executor) .applyRuleNode(actionConfig(), node); + } - + public void applyFilterSpec(RuleNodeModel node, List specs) { + SceneProviders + .getActionProviderNow(executor) + .applyFilterSpec(node, specs); + } public static Variable toVariable(String prefix, - PropertyMetadata metadata, - String i18nKey, - String msgPattern) { + PropertyMetadata metadata, + String i18nKey, + String msgPattern) { return toVariable(prefix.concat(".").concat(metadata.getId()), metadata.getName(), metadata.getValueType(), @@ -197,11 +203,11 @@ public class SceneAction implements Serializable { } public static Variable toVariable(String id, - String metadataName, - DataType dataType, - String i18nKey, - String msgPattern, - String parentName) { + String metadataName, + DataType dataType, + String i18nKey, + String msgPattern, + String parentName) { String fullName = parentName == null ? metadataName : parentName + "." + metadataName; Variable variable = Variable.of(id, LocaleUtils.resolveMessage(i18nKey, diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActionProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActionProvider.java index ef34dd87..12d434df 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActionProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActionProvider.java @@ -1,11 +1,15 @@ package org.jetlinks.community.rule.engine.scene; +import org.jetlinks.core.utils.SerializeUtils; +import org.jetlinks.community.rule.engine.alarm.AlarmConstants; +import org.jetlinks.community.terms.TermSpec; import org.jetlinks.rule.engine.api.model.RuleNodeModel; import reactor.core.publisher.Flux; import java.util.List; public interface SceneActionProvider { + String getProvider(); C newConfig(); @@ -14,5 +18,12 @@ public interface SceneActionProvider { Flux createVariable(C config); - void applyRuleNode(C config,RuleNodeModel model); + void applyRuleNode(C config, RuleNodeModel model); + + default void applyFilterSpec(RuleNodeModel node, List specs) { + node.addConfiguration( + AlarmConstants.ConfigKey.alarmFilterTermSpecs, + SerializeUtils.convertToSafelySerializable(specs) + ); + } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneConditionAction.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneConditionAction.java index 32daaad1..c32ad470 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneConditionAction.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneConditionAction.java @@ -16,11 +16,6 @@ import java.util.stream.Collectors; @Setter public class SceneConditionAction implements Serializable { - /** - * @see org.jetlinks.community.rule.engine.scene.term.TermColumn - * @see org.jetlinks.community.reactorql.term.TermType - * @see org.jetlinks.community.rule.engine.scene.value.TermValue - */ @Schema(description = "条件") private List when; @@ -36,7 +31,6 @@ public class SceneConditionAction implements Serializable { @Schema(description = "分支ID") private Integer branchId; - //仅用于设置到reactQl sql的column中 public List createContextTerm() { List contextTerm = new ArrayList<>(); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneRule.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneRule.java index 54119aeb..3242b28e 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneRule.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneRule.java @@ -3,24 +3,29 @@ package org.jetlinks.community.rule.engine.scene; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.core.param.TermType; import org.hswebframework.ezorm.rdb.executor.EmptySqlRequest; import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.hswebframework.web.api.crud.entity.TermExpressionParser; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.i18n.LocaleUtils; +import org.hswebframework.web.utils.DigestUtils; import org.hswebframework.web.validator.ValidatorUtils; +import org.jetlinks.core.trace.MonoTracer; +import org.jetlinks.core.trace.TraceHolder; +import org.jetlinks.core.utils.NamedFunction; +import org.jetlinks.core.utils.Reactors; import org.jetlinks.community.rule.engine.commons.ShakeLimit; import org.jetlinks.community.rule.engine.commons.TermsConditionEvaluator; import org.jetlinks.community.rule.engine.enums.SceneFeature; import org.jetlinks.community.rule.engine.scene.internal.triggers.ManualTriggerProvider; import org.jetlinks.community.rule.engine.scene.term.TermColumn; -import org.jetlinks.community.rule.engine.scene.term.limit.ShakeLimitGrouping; -import org.jetlinks.core.trace.MonoTracer; -import org.jetlinks.core.trace.TraceHolder; -import org.jetlinks.core.utils.Reactors; +import org.jetlinks.community.terms.TermSpec; import org.jetlinks.reactor.ql.DefaultReactorQLContext; import org.jetlinks.reactor.ql.ReactorQL; import org.jetlinks.reactor.ql.ReactorQLContext; @@ -46,11 +51,15 @@ import java.util.function.Function; @Getter @Setter +@Slf4j public class SceneRule implements Serializable { + static final Function, Mono> FILTER_TRUE = + NamedFunction.of("true", ignore -> Reactors.ALWAYS_TRUE); public static final String ACTION_KEY_BRANCH_ID = "_branchId"; public static final String ACTION_KEY_BRANCH_INDEX = "_branchIndex"; public static final String ACTION_KEY_GROUP_INDEX = "_groupIndex"; + public static final String ACTION_KEY_ACTION_ID = "_actionId"; public static final String ACTION_KEY_ACTION_INDEX = "_actionIndex"; public static final String CONTEXT_KEY_SCENE_OUTPUT = "scene"; @@ -60,11 +69,11 @@ public class SceneRule implements Serializable { public static final String SOURCE_NAME_KEY = "sourceName"; - @Schema(description = "告警ID") + @Schema(description = "场景ID") @NotBlank(message = "error.scene_rule_id_cannot_be_blank") private String id; - @Schema(description = "告警名称") + @Schema(description = "场景名称") @NotBlank(message = "error.scene_rule_name_cannot_be_blank") private String name; @@ -72,11 +81,6 @@ public class SceneRule implements Serializable { @NotNull(message = "error.scene_rule_trigger_cannot_be_null") private Trigger trigger; - /** - * @see org.jetlinks.community.rule.engine.scene.term.TermColumn - * @see org.jetlinks.community.reactorql.term.TermType - * @see org.jetlinks.community.rule.engine.scene.value.TermValue - */ @Schema(description = "触发条件") private List terms; @@ -100,29 +104,23 @@ public class SceneRule implements Serializable { public SqlRequest createSql(boolean hasWhere) { if (trigger != null) { - return trigger.createSql(getTermList(), hasWhere); + return TraceHolder + .traceBlocking("/scene/create-sql", span -> { + SqlRequest request = trigger.createSql(getTermList(), hasWhere); + if (!(request instanceof EmptySqlRequest)) { + span.setAttribute("sql", request.toNativeSql()); + } + return request; + }); } return EmptySqlRequest.INSTANCE; } - private List getTermList() { - List terms = new ArrayList<>(); - if (CollectionUtils.isNotEmpty(this.terms)) { - terms.addAll(this.terms); - } - if (CollectionUtils.isNotEmpty(this.branches)) { - for (SceneConditionAction branch : branches) { - terms.addAll(branch.createContextTerm()); - } - } - return terms; - } - public Function, Mono> createDefaultFilter(List terms) { if (trigger != null) { return createDefaultFilter(trigger.createFilter(terms)); } - return ignore -> Reactors.ALWAYS_TRUE; + return FILTER_TRUE; } public static String DEFAULT_FILTER_TABLE = "t"; @@ -137,30 +135,17 @@ public class SceneRule implements Serializable { .build(); List args = Arrays.asList(request.getParameters()); String sqlString = request.toNativeSql(); - return new Function, Mono>() { - @Override - public Mono apply(Map map) { + return NamedFunction + .of(sqlString, map -> { ReactorQLContext context = new DefaultReactorQLContext((t) -> Flux.just(map), args); return ql .start(context) .hasElements(); - } - - @Override - public String toString() { - return sqlString; - } - }; + }); } - - return ignore -> Reactors.ALWAYS_TRUE; + return FILTER_TRUE; } - public ShakeLimitGrouping> createGrouping() { - //todo 其他分组方式实现 - return flux -> flux - .groupBy(map -> map.getOrDefault("deviceId", "null"), Integer.MAX_VALUE); - } private Flux createSceneVariables(List columns) { return LocaleUtils @@ -225,7 +210,7 @@ public class SceneRule implements Serializable { .doOnNext(Variable::refactorPrefix); } - public static String createBranchActionId(int branchIndex, int groupId, int actionIndex) { + static String createBranchActionId(int branchIndex, int groupId, int actionIndex) { return "branch_" + branchIndex + "_group_" + groupId + "_action_" + actionIndex; } @@ -245,14 +230,23 @@ public class SceneRule implements Serializable { for (SceneConditionAction branch : branches) { int _branchIndex = ++branchIndex; //执行条件 - Function, Mono> filter = createDefaultFilter(branch.getWhen()); + Function, Mono> filter = TraceHolder + .traceBlocking("/scene/create-branch-filter", + span -> { + Function, Mono> + f = createDefaultFilter(branch.getWhen()); + span.setAttribute("filter", f.toString()); + span.setAttribute("branch", _branchIndex); + return f; + }); //满足条件后的输出操作 List, Mono>> outs = new ArrayList<>(); List groups = branch.getThen(); int thenIndex = 0; if (CollectionUtils.isNotEmpty(groups)) { - + List, Mono>> actionOuts = new ArrayList<>(); + //执行动作 for (SceneActions then : groups) { thenIndex++; @@ -278,39 +272,45 @@ public class SceneRule implements Serializable { .flatMap(nodeId -> output.apply(_branchIndex, nodeId, data)) .then(); } - //防抖 - ShakeLimit shakeLimit = branch.getShakeLimit(); - if (shakeLimit != null && shakeLimit.isEnabled()) { - - Sinks.Many> sinks = Sinks - .many() - .unicast() - .onBackpressureBuffer(Queues.>unboundedMultiproducer().get()); - - //分组方式,比如设备触发时,应该按设备分组,每个设备都走独立的防抖策略 - ShakeLimitGrouping> grouping = createGrouping(); - - Function, Mono> handler = out; - - disposable.add( - shakeLimit - .transfer(sinks.asFlux(), - (duration, stream) -> - grouping - .group(stream)//先按自定义分组再按时间窗口进行分组 - .flatMap(group -> group.window(duration), Integer.MAX_VALUE), - (map, total) -> map.put("_total", total)) - .flatMap(handler) - .subscribe() - ); - //输出到sink进行防抖控制 - out = data -> { - sinks.emitNext(data, Reactors.emitFailureHandler()); - return Mono.empty(); - }; - } - outs.add(out); + actionOuts.add(out); } + + //防抖 + ShakeLimit shakeLimit = branch.getShakeLimit(); + if (shakeLimit != null && shakeLimit.isEnabled()) { + Sinks.Many> sinks = Sinks + .many() + .unicast() + .onBackpressureBuffer(Queues.>unboundedMultiproducer().get()); + + //动作输出 + Flux, Mono>> _outs = Flux.fromIterable(new ArrayList<>(actionOuts)); + Function, Mono> handler = + map -> _outs.flatMap(call -> call.apply(map)).then(); + + //防抖 + disposable.add( + trigger + .provider() + .shakeLimit(DigestUtils.md5Hex(id + ":" + _branchIndex), + sinks.asFlux(), + shakeLimit) + .flatMap(res -> { + res.getElement().put("_total", res.getTimes()); + return handler.apply(res.getElement()); + }) + .subscribe() + ); + //满足输出给防抖策略 + actionOuts.clear(); + actionOuts.add(data -> { + sinks.emitNext(data, Reactors.emitFailureHandler()); + return Mono.empty(); + }); + } + + //动作输出 + outs.addAll(actionOuts); } @@ -340,7 +340,7 @@ public class SceneRule implements Serializable { .apply(data) .flatMap(match -> { //无论如何都尝试执行当前分支 - if(executeAnyway){ + if (executeAnyway) { return handler.apply(data); } //上一个分支满足了则返回,不执行此分支逻辑 @@ -374,18 +374,19 @@ public class SceneRule implements Serializable { return disposable; } - public SceneRule where(String expression) { - setTerms(TermExpressionParser.parse(expression)); - return this; - } - public List createDefaultVariable() { return trigger != null ? trigger.createDefaultVariable() : Collections.emptyList(); } - public RuleModel toModel() { + public SceneRule where(String expression) { + setTerms(TermExpressionParser.parse(expression)); + return this; + } + + + public Mono toModel() { validate(); RuleModel model = new RuleModel(); model.setId(id); @@ -405,6 +406,7 @@ public class SceneRule implements Serializable { //触发器 trigger.applyModel(model, sceneNode); model.getNodes().add(sceneNode); + if (CollectionUtils.isNotEmpty(actions)) { int index = 1; @@ -433,7 +435,8 @@ public class SceneRule implements Serializable { RuleLink link = model.link(preNode, actionNode); //设置上一个节点到此节点的输出条件 if (CollectionUtils.isNotEmpty(preAction.getTerms())) { - link.setCondition(TermsConditionEvaluator.createCondition(trigger.refactorTerm("this", preAction.getTerms()))); + link.setCondition(TermsConditionEvaluator.createCondition(trigger.refactorTerm("this", preAction + .getTerms()))); } preNode = actionNode; } @@ -444,13 +447,53 @@ public class SceneRule implements Serializable { } } + List> async = new ArrayList<>(); + + Mono> columns = trigger + .parseTermColumns() + .collectList() + .cache(); + + Mono> columnMapping = + columns + .flatMapMany(this::createSceneVariables) + .expand(var -> var.getChildren() == null ? Flux.empty() : Flux.fromIterable(var.getChildren())) + .collectMap(Variable::getColumn, Function.identity()) + .cache(); + + //使用分支条件时 if (CollectionUtils.isNotEmpty(branches)) { int branchIndex = 0; + Mono> branchTermSpec = null; for (SceneConditionAction branch : branches) { + int branchId = branch.getBranchId() == null ? branchIndex : branch.getBranchId(); branchIndex++; List group = branch.getThen(); + Mono> lastTerm = branchTermSpec; + //场景输出的条件描述 + branchTermSpec = columnMapping + .flatMap(mapping -> trigger + .createFilterSpec( + branch.getWhen(), + (term, spec) -> applyTermSpec(term, spec, mapping))) + .flatMap(list -> { + TermSpec spec = new TermSpec(); + //最后一个分支? else + if (branch.getWhen().isEmpty() + && lastTerm != null + && !branch.isExecuteAnyway()) { + spec.setTermType(TermType.not); + return lastTerm + .doOnNext(spec::setChildren) + .thenReturn(Collections.singletonList(spec)); + } + spec.setChildren(list); + return Mono.just(Collections.singletonList(spec)); + }) + .cache(); + if (CollectionUtils.isNotEmpty(group)) { int groupIndex = 0; @@ -460,13 +503,33 @@ public class SceneRule implements Serializable { int actionIndex = 1; RuleNodeModel preNode = null; SceneAction preAction = null; + Mono> groupTerm = branchTermSpec; + for (SceneAction action : actions.getActions()) { + + int finalBranchIndex = branchIndex - 1, + finalGroupIndex = groupIndex - 1, + finalActionIndex = actionIndex - 1; + + int actionId = action.getActionId() == null ? actionIndex : action.getActionId(); + + //变量信息 + Mono> groupVar = columns + .flatMapMany(termColumns -> this + .createVariables(termColumns, finalBranchIndex, finalGroupIndex, finalActionIndex)) + .expand(var -> var.getChildren() == null + ? Flux.empty() + : Flux.fromIterable(var.getChildren())) + .collectMap(Variable::getColumn, Function.identity()); + RuleNodeModel actionNode = new RuleNodeModel(); actionNode.setId(createBranchActionId(branchIndex, groupIndex, actionIndex)); actionNode.setName("条件" + branchIndex + "_分组" + groupIndex + "_动作" + actionIndex); action.applyNode(actionNode); //串行 + actionNode.addConfiguration(ACTION_KEY_BRANCH_ID, branchId); + actionNode.addConfiguration(ACTION_KEY_ACTION_ID, actionId); if (!actions.isParallel()) { //串行的时候 标记记录每一个动作的数据到header中,用于进行条件判断或者数据引用 actionNode.addConfiguration(RuleData.RECORD_DATA_TO_HEADER, true); @@ -480,36 +543,114 @@ public class SceneRule implements Serializable { RuleLink link = model.link(preNode, actionNode); //设置上一个节点到此节点的输出条件 if (CollectionUtils.isNotEmpty(preAction.getTerms())) { - link.setCondition(TermsConditionEvaluator.createCondition(trigger.refactorTerm("this", preAction.getTerms()))); + List termList = preAction.getTerms(); + //合并上一个节点输出的变量 + groupTerm = Mono.zip( + groupTerm.>map(ArrayList::new), + groupVar, + (parent, mapping) -> { + TermSpec childSpec = new TermSpec(); + childSpec + .setChildren( + TermSpec.of( + termList, + (term, spec) -> applyTermSpec(term, spec, mapping)) + ); + parent.add(childSpec); + return parent; + }); + + link.setCondition(TermsConditionEvaluator.createCondition( + trigger.refactorTerm("this", termList))); } + } else if (Objects.equals(trigger.getType(), ManualTriggerProvider.PROVIDER)) { model.link(sceneNode, actionNode); } - - preNode = actionNode; } else { if (Objects.equals(trigger.getType(), ManualTriggerProvider.PROVIDER)) { model.link(sceneNode, actionNode); } } + groupTerm = groupTerm + .doOnNext(arr -> { + log.debug( + "scene[{}] action[{}] term spec: {}", + getId(), + actionNode.getId(), + arr); + action.applyFilterSpec(actionNode, arr); + }) + .as(MonoTracer.create( + "/scene/branch_filter_spec", + (span, next) -> { + span.setAttribute("spec", TermSpec.toString(next)); + span.setAttribute("actionId", actionNode.getId()); + })) + .cache(); + + preNode = actionNode; + model.getNodes().add(actionNode); preAction = action; actionIndex++; } + async.add(groupTerm); } } + } - - } } - return model; + return Flux + .concat(async) + .then(Mono.just(model)); } public void validate() { ValidatorUtils.tryValidate(this); } + + private void applyTermSpec(Term term, TermSpec spec, Map mapping) { + Variable var = mapping.get(term.getColumn()); + Term newTerm = trigger.refactorTerm("this", term.clone()); + + Object newValue = newTerm.getValue(); + //期望值是另外一个变量 + if (newValue instanceof NativeSql) { + spec.setExpectIsExpr(true); + String sql = ((NativeSql) newValue).getSql(); + //sql语法格式特殊如: this['temp_current'] + //只需要里面的temp_current作为变量表达式 + if (sql.contains("['") && sql.endsWith("']")) { + spec.setExpected(sql.substring(sql.indexOf("['") + 2, sql.length() - 2)); + } + } else { + spec.setExpected(newTerm.getValue()); + } + + if (var != null) { + spec.setColumn(var.getId()); + spec.setMetadata(var.isMetadata()); + spec.setDisplayCode(var.getFullNameCode()); + } + + } + + private List getTermList() { + List terms = new ArrayList<>(); + if (CollectionUtils.isNotEmpty(this.terms)) { + terms.addAll(this.terms); + } + if (CollectionUtils.isNotEmpty(this.branches)) { + for (SceneConditionAction branch : branches) { + terms.addAll(branch.createContextTerm()); + } + } + return terms; + } + } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTaskExecutorProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTaskExecutorProvider.java index af3e2473..e94fdcd7 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTaskExecutorProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTaskExecutorProvider.java @@ -11,8 +11,6 @@ import org.jetlinks.core.event.Subscription; import org.jetlinks.core.trace.TraceHolder; import org.jetlinks.core.utils.FluxUtils; import org.jetlinks.community.PropertyConstants; -import org.jetlinks.community.rule.engine.RuleEngineConstants; -import org.jetlinks.community.rule.engine.scene.term.limit.ShakeLimitGrouping; import org.jetlinks.reactor.ql.ReactorQL; import org.jetlinks.reactor.ql.ReactorQLContext; import org.jetlinks.reactor.ql.ReactorQLRecord; @@ -54,13 +52,13 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { class SceneTaskExecutor extends AbstractTaskExecutor { + private SceneRule rule; + private String ruleId; private String ruleName; private boolean useBranch; - private SceneRule rule; - public SceneTaskExecutor(ExecutionContext context) { super(context); load(); @@ -140,6 +138,7 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { ruleId = rule.getId(); ruleName = rule.getName(); useBranch = CollectionUtils.isNotEmpty(rule.getBranches()); + SqlRequest request = rule.createSql(!useBranch); Flux> source; @@ -150,13 +149,13 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { .accept() .flatMap(RuleData::dataToMap); } else { - if (log.isDebugEnabled()) { - log.debug("init scene [{}:{}], sql:{}", ruleId, ruleName, request.toNativeSql()); + if (log.isInfoEnabled()) { + log.info("init scene [{}:{}], sql:{}", ruleId, ruleName, request.toNativeSql()); } ReactorQLContext qlContext = createReactorQLContext(); - //request = {PrepareSqlRequest@25664} "select * from (\n\tselect\n\tnow() "_now",\n\tthis.timestamp "timestamp",\n\tthis.deviceId "deviceId",\n\tthis.headers.deviceName "deviceName",\n\tthis.headers.productId "productId",\n\tthis.headers.productName "productName",\n\t'device' "sourceType",\n\tthis.deviceId "sourceId",\n\tthis.deviceName "sourceName",\n\tthis.headers._uid "_uid",\n\tthis.headers.bindings "_bindings",\n\tthis.headers.traceparent "traceparent",\n\tthis.properties "properties",\n\tcoalesce(this['properties.te'],device.property.recent(deviceId,'te',timestamp)) "te_recent",\n\tproperty.metric('device',deviceId,'te','t') te_metric_t\t\nfrom "/device/1684380948267950080/11/message/property/report"\n) t \n"…视图sql参数 + //sql参数 for (Object parameter : request.getParameters()) { qlContext.bind(parameter); } @@ -187,21 +186,21 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { .write(nodeId, ruleData)) .onErrorResume(err -> context.onError(err, ruleData)); }); + }); } //防抖 Trigger.GroupShakeLimit shakeLimit = rule.getTrigger().getShakeLimit(); if (shakeLimit != null && shakeLimit.isEnabled()) { - - ShakeLimitGrouping> grouping = shakeLimit.createGrouping(); - - source = shakeLimit.transfer( - source, - (time, flux) -> grouping - .group(flux) - .flatMap(group -> group.window(time), Integer.MAX_VALUE), - (map, total) -> map.put("_total", total)); + source = rule + .getTrigger() + .provider() + .shakeLimit(ruleId, source, shakeLimit) + .map(result -> { + result.getElement().put("_total", result.getTimes()); + return result.getElement(); + }); } return source @@ -227,7 +226,6 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { }); } - private Mono handleOutput(RuleData data) { return data .dataToMap() @@ -252,6 +250,14 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { } + protected SceneData buildSceneData(Map map) { + SceneData sceneData = new SceneData(); + sceneData.setId(IDGenerator.RANDOM.generate()); + sceneData.setRule(rule); + sceneData.setOutput(map); + return sceneData; + } + private Mono handleOutput(Map data) { return handleOutput(context.newRuleData(data)); } @@ -273,13 +279,5 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { } return handleOutput(ruleData); } - - protected SceneData buildSceneData(Map map) { - SceneData sceneData = new SceneData(); - sceneData.setId(IDGenerator.RANDOM.generate()); - sceneData.setRule(rule); - sceneData.setOutput(map); - return sceneData; - } } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTriggerProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTriggerProvider.java index 3eeaab43..6c8fe1a4 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTriggerProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTriggerProvider.java @@ -4,38 +4,137 @@ import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.community.rule.engine.commons.ShakeLimit; +import org.jetlinks.community.rule.engine.commons.ShakeLimitResult; +import org.jetlinks.community.rule.engine.commons.impl.SimpleShakeLimitProvider; import org.jetlinks.community.rule.engine.scene.term.TermColumn; +import org.jetlinks.community.terms.TermSpec; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.util.List; import java.util.Map; +import java.util.function.BiConsumer; +import static org.jetlinks.community.rule.engine.scene.SceneRule.SOURCE_ID_KEY; + +/** + * 场景触发支持提供商,用于提供对场景触发条件的支持. + * + * @param E 配置类型 + * @author zhouhao + * @since 2.1 + */ public interface SceneTriggerProvider { + /** + * @return 提供商唯一标识 + */ String getProvider(); + /** + * @return 名称 + */ String getName(); + /** + * 创建配置 + * + * @return 配置 + */ E newConfig(); - SqlRequest createSql(E config,List terms, boolean hasWhere); + /** + * 根据配置以及条件创建SQL,该sql执行基于{@link org.jetlinks.reactor.ql.ReactorQL}. + * + * @param config 配置 + * @param terms 条件 + * @param hasFilter 是否包含过滤条件 + * @return SQL + * @see org.jetlinks.reactor.ql.ReactorQL + */ + SqlRequest createSql(E config, List terms, boolean hasFilter); - SqlFragments createFilter(E config,List terms); + /** + * 创建过滤条件.不含where前缀. + * + * @param config 配置 + * @param terms 条件 + * @return 条件SQL语句 + */ + SqlFragments createFilter(E config, List terms); + /** + * 重构条件为匹配场景输出变量的条件 + * + * @param mainTableName 主表名 + * @param term 条件 + * @return 重构后的条件 + */ + default Term refactorTerm(String mainTableName, + Term term) { + return SceneUtils.refactorTerm(mainTableName, term); + } + + /** + * 创建对用户友好的条件描述信息. + * + * @param config 配置信息 + * @param terms 条件 + * @return 描述信息 + * @since 2.2 + */ + default Mono> createFilterSpec(E config, List terms, BiConsumer customizer) { + return Mono.justOrEmpty(TermSpec.of(terms, customizer)); + } + + /** + * 创建默认变量信息 + * + * @param config 配置 + * @return 变量信息 + */ List createDefaultVariable(E config); + /** + * 应用规则节点配置 + * + * @param config 触发器配置 + * @param model 规则模型 + * @param sceneNode 场景触发节点模型 + */ void applyRuleNode(E config, RuleModel model, RuleNodeModel sceneNode); + /** + * 解析配置信息为支持的条件列,用于展示当前触发方式支持的触发条件. + * + * @param config 配置 + * @return 条件列信息 + */ Flux parseTermColumns(E config); + /** + * 配置信息 + */ + default Flux>> shakeLimit(String key, + Flux> source, + ShakeLimit limit) { + return SimpleShakeLimitProvider + .GLOBAL + .shakeLimit(key, + source.groupBy( + data -> String.valueOf(data.getOrDefault(SOURCE_ID_KEY, "null")), + Integer.MAX_VALUE), + limit); + } - interface TriggerConfig{ + interface TriggerConfig { void validate(); - default void with(Map config){ - FastBeanCopier.copy(config,this); + default void with(Map config) { + FastBeanCopier.copy(config, this); } } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneUtils.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneUtils.java index 73aa012a..1894285e 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneUtils.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneUtils.java @@ -2,17 +2,25 @@ package org.jetlinks.community.rule.engine.scene; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; import org.jetlinks.community.PropertyMetric; +import org.jetlinks.community.reactorql.term.TermType; +import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProviders; import org.jetlinks.community.rule.engine.scene.term.TermColumn; import org.jetlinks.community.rule.engine.scene.value.TermValue; +import org.jetlinks.community.rule.engine.web.response.SelectorInfo; import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.util.*; import java.util.function.Function; +import java.util.stream.Collectors; public class SceneUtils { - public static String createColumnAlias(String prefix,String column, boolean wrapColumn) { + + public static String createColumnAlias(String prefix, String column, boolean wrapColumn) { if (!column.contains(".")) { return wrapColumn ? wrapColumnName(column) : column; } @@ -39,6 +47,17 @@ public class SceneUtils { return "\"" + (column.replace("\"", "\\\"")) + "\""; } + public static String appendColumn(String... columns) { + StringJoiner joiner = new StringJoiner("."); + for (String column : columns) { + if (StringUtils.hasText(column)) { + joiner.add(column); + } + } + return joiner.toString(); + } + + /** * 根据条件和可选的条件列解析出将要输出的变量信息 * @@ -49,7 +68,7 @@ public class SceneUtils { public static List parseVariable(List terms, List columns) { //平铺条件 - Map termCache = expandTerm(terms); + Map> termCache = expandTerm(terms); //解析变量 List variables = new ArrayList<>(termCache.size()); @@ -60,19 +79,27 @@ public class SceneUtils { return variables; } - public static Map expandTerm(List terms) { - Map termCache = new LinkedHashMap<>(); + public static Map> expandTerm(List terms) { + Map> termCache = new LinkedHashMap<>(); expandTerm(terms, termCache); return termCache; } - private static void expandTerm(List terms, Map container) { + private static void expandTerm(List terms, Map> container) { if (terms == null) { return; } for (Term term : terms) { if (StringUtils.hasText(term.getColumn())) { - container.put(term.getColumn(), term); + List termList = container.get(term.getColumn()); + if (termList == null){ + List list = new ArrayList<>(); + list.add(term); + container.put(term.getColumn(),list); + } else { + termList.add(term); + container.put(term.getColumn(), termList); + } } if (term.getTerms() != null) { expandTerm(term.getTerms(), container); @@ -82,29 +109,33 @@ public class SceneUtils { private static List columnToVariable(String prefixName, TermColumn column, - Function termSupplier) { + Function> termSupplier) { List variables = new ArrayList<>(1); String variableName = column.getName(); //prefixName == null ? column.getName() : prefixName + "/" + column.getName(); if (CollectionUtils.isEmpty(column.getChildren())) { - Term term = termSupplier.apply(column.getColumn()); + List termList = termSupplier.apply(column.getColumn()); variables.add(Variable.of(column.getVariable("_"), variableName) .with(column) ); - if (term != null) { - List termValues = TermValue.of(term); - String property = column.getPropertyOrNull(); - for (TermValue termValue : termValues) { - PropertyMetric metric = column.getMetricOrNull(termValue.getMetric()); - if (property != null && metric != null && termValue.getSource() == TermValue.Source.metric) { - // temp_metric - variables.add(Variable.of( - property + "_metric_" + termValue.getMetric(), - (prefixName == null ? column.getName() : prefixName) + "_指标_" + metric.getName()) - .withTermType(column.getTermTypes()) - .withColumn(column.getColumn()) - .withMetadata(column.isMetadata()) - ); + if (termList != null && !termList.isEmpty()) { + for (Term term : termList) { + List termValues = TermValue.of(term); + String property = column.getPropertyOrNull(); + for (TermValue termValue : termValues) { + PropertyMetric metric = column.getMetricOrNull(termValue.getMetric()); + if (property != null && metric != null && termValue.getSource() == TermValue.Source.metric) { + // temp_metric + variables.add(Variable.of( + property + "_metric_" + termValue.getMetric(), + (prefixName == null ? column.getName() : prefixName) + "_指标_" + metric.getName()) + .withTermType(column.getTermTypes()) + .withColumn(column.getColumn()) + .withCode(column.getCode()) + .withFullNameCode(column.getFullNameCode().copy()) + .withMetadata(column.isMetadata()) + ); + } } } } @@ -122,4 +153,123 @@ public class SceneUtils { } return variables; } + + public static Flux getSupportTriggers() { + return Flux + .fromIterable(SceneProviders.triggerProviders()) + .map(SceneTriggerProvider::getProvider); + } + + public static Flux getSupportActions() { + return Flux + .fromIterable(SceneProviders.actionProviders()) + .map(SceneActionProvider::getProvider); + } + + public static Flux parseTermColumns(SceneRule ruleMono) { + Trigger trigger = ruleMono.getTrigger(); + if (trigger != null) { + return trigger.parseTermColumns(); + } + return Flux.empty(); + } + + public static Flux parseVariables(Mono ruleMono, Integer branch, Integer branchGroup, Integer action) { + Mono cache = ruleMono.cache(); + return Mono + .zip( + cache.flatMapMany(SceneUtils::parseTermColumns).collectList(), + cache, + (columns, rule) -> rule + .createVariables(columns, + branch, + branchGroup, + action)) + .flatMapMany(Function.identity()); + } + + public static Flux getDeviceSelectors() { + return Flux + .fromIterable(DeviceSelectorProviders.allProvider()) + //场景联动的设备动作必须选择一个产品,不再列出产品 + .filter(provider -> !"product".equals(provider.getProvider())) + .map(SelectorInfo::of); + } + + public static Term refactorTerm(String tableName, Term term) { + if (term.getColumn() == null) { + return term; + } + String[] arr = term.getColumn().split("[.]"); + + List values = TermValue.of(term); + if (values.isEmpty()) { + return term; + } + + Function parser = value -> { + //上游变量 + if (value.getSource() == TermValue.Source.variable + || value.getSource() == TermValue.Source.upper) { + term.getOptions().add(TermType.OPTIONS_NATIVE_SQL); + return tableName + "['" + value.getValue() + "']"; + } + //指标 + else if (value.getSource() == TermValue.Source.metric) { + term.getOptions().add(TermType.OPTIONS_NATIVE_SQL); + return tableName + "['" + arr[1] + "_metric_" + value.getMetric() + "']"; + } + //手动设置值 + else { + return value.getValue(); + } + }; + Object val; + if (values.size() == 1) { + val = parser.apply(values.get(0)); + } else { + val = values + .stream() + .map(parser) + .collect(Collectors.toList()); + } + + String column; + // properties.xxx.last的场景 + if (arr.length > 3 && arr[0].equals("properties")) { + column = tableName + "['" + createColumnAlias("properties", term.getColumn(), false) + + "." + String.join(".", Arrays.copyOfRange(arr, 2, arr.length - 1)) + "']"; + } else if (!isDirectTerm(arr[0])) { + column = tableName + "['" + createColumnAlias("properties", term.getColumn(), false) + "']"; + } else { + column = term.getColumn(); + } + + if (term.getOptions().contains(TermType.OPTIONS_NATIVE_SQL) && !(val instanceof NativeSql)) { + val = NativeSql.of(String.valueOf(val)); + } + + term.setColumn(column); + + term.setValue(val); + + return term; + } + + + private static boolean isDirectTerm(String column) { + //直接term,构建Condition输出条件时使用 + return isBranchTerm(column) || isSceneTerm(column); + } + + private static boolean isBranchTerm(String column) { + return column.startsWith("branch_") && + column.contains("_group_") + && column.contains("_action_"); + } + + private static boolean isSceneTerm(String column) { + return column.startsWith("scene"); + } + } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Trigger.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Trigger.java index 0930e074..87153552 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Trigger.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Trigger.java @@ -9,21 +9,25 @@ import org.hswebframework.ezorm.rdb.executor.EmptySqlRequest; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.ezorm.rdb.operator.builder.fragments.EmptySqlFragments; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; -import org.hswebframework.web.bean.FastBeanCopier; -import org.jetlinks.community.TimerSpec; import org.jetlinks.community.rule.engine.commons.ShakeLimit; import org.jetlinks.community.rule.engine.scene.internal.triggers.*; import org.jetlinks.community.rule.engine.scene.term.TermColumn; import org.jetlinks.community.rule.engine.scene.term.limit.ShakeLimitGrouping; +import org.jetlinks.community.terms.TermSpec; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import javax.validation.constraints.NotNull; import java.io.Serializable; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; @Getter @Setter @@ -34,6 +38,10 @@ public class Trigger implements Serializable { @NotNull(message = "error.scene_rule_trigger_cannot_be_null") private String type; + /** + * @deprecated {@link SceneConditionAction#getShakeLimit()} + */ + @Deprecated @Schema(description = "防抖配置") private GroupShakeLimit shakeLimit; @@ -43,20 +51,19 @@ public class Trigger implements Serializable { @Schema(description = "[type]为[timer]时不能为空") private TimerTrigger timer; - @Schema(description = "[type]不为[device,timer,collector]时不能为控") + @Schema(description = "[type]不为[device,timer,collector]时不能为空") private Map configuration; public String getTypeName(){ return provider().getName(); } - /** * 重构查询条件,替换为实际将要输出的变量. * * @param terms 条件 * @return 重构后的条件 - * @see DeviceTrigger#refactorTermValue(String, Term) + * @see SceneTriggerProvider#refactorTerm(String, Term) */ public List refactorTerm(String tableName, List terms) { if (CollectionUtils.isEmpty(terms)) { @@ -65,7 +72,7 @@ public class Trigger implements Serializable { List target = new ArrayList<>(terms.size()); for (Term term : terms) { Term copy = term.clone(); - target.add(DeviceTrigger.refactorTermValue(tableName, copy)); + target.add(provider().refactorTerm(tableName, copy)); if (org.apache.commons.collections4.CollectionUtils.isNotEmpty(copy.getTerms())) { copy.setTerms(refactorTerm(tableName, copy.getTerms())); } @@ -73,6 +80,10 @@ public class Trigger implements Serializable { return target; } + public Term refactorTerm(String tableName, Term term) { + return provider().refactorTerm(tableName, term); + } + public SqlRequest createSql(List terms, boolean hasWhere) { SceneTriggerProvider.TriggerConfig config = triggerConfig(); @@ -85,13 +96,16 @@ public class Trigger implements Serializable { return config == null ? EmptySqlFragments.INSTANCE : provider().createFilter(config, terms); } + public Mono> createFilterSpec(List terms, BiConsumer customizer){ + return provider().createFilterSpec(triggerConfig(), terms,customizer); + } + public Flux parseTermColumns() { SceneTriggerProvider.TriggerConfig config = triggerConfig(); return config == null ? Flux.empty() : provider().parseTermColumns(config); } - public SceneTriggerProvider.TriggerConfig triggerConfig() { switch (type) { case DeviceTriggerProvider.PROVIDER: @@ -107,7 +121,7 @@ public class Trigger implements Serializable { } } - private SceneTriggerProvider provider() { + SceneTriggerProvider provider() { return SceneProviders.getTriggerProviderNow(type); } @@ -117,7 +131,9 @@ public class Trigger implements Serializable { } public List createDefaultVariable() { - return provider().createDefaultVariable(triggerConfig()); + SceneTriggerProvider.TriggerConfig config = triggerConfig(); + + return config == null ? Collections.emptyList() : provider().createDefaultVariable(config); } public static Trigger device(DeviceTrigger device) { @@ -133,7 +149,6 @@ public class Trigger implements Serializable { return trigger; } - @Getter @Setter public static class GroupShakeLimit extends ShakeLimit { diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/TriggerType.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/TriggerType.java index 571e764d..39b0f144 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/TriggerType.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/TriggerType.java @@ -4,11 +4,16 @@ import lombok.AllArgsConstructor; import lombok.Getter; import org.hswebframework.web.dict.EnumDict; +/** + * @deprecated 请使用 {@link SceneTriggerProvider}替代 + * @see SceneProviders + */ @Getter @AllArgsConstructor @Deprecated public enum TriggerType implements EnumDict { manual("手动触发"), + collector("采集器触发"), timer("定时触发"), device("设备触发"); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Variable.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Variable.java index 61e7f82a..627ecafe 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Variable.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Variable.java @@ -10,6 +10,7 @@ import org.jetlinks.core.metadata.types.StringType; import org.jetlinks.community.reactorql.term.TermType; import org.jetlinks.community.reactorql.term.TermTypes; import org.jetlinks.community.rule.engine.scene.term.TermColumn; +import org.jetlinks.community.terms.I18nSpec; import org.springframework.util.StringUtils; import java.util.HashMap; @@ -26,9 +27,15 @@ public class Variable { @Schema(description = "变量名") private String name; + @Schema(description = "变量编码") + private String code; + @Schema(description = "变量全名") private String fullName; + @Schema(description = "全名显码") + private I18nSpec fullNameCode; + @Schema(description = "列") private String column; @@ -88,7 +95,7 @@ public class Variable { public Variable withType(DataType type) { withType(type.getId()) - .withTermType(TermTypes.lookup(type)); + .withTermType(TermTypes.lookup(type)); return this; } @@ -102,6 +109,16 @@ public class Variable { return this; } + public Variable withFullNameCode(I18nSpec fullNameCode) { + this.fullNameCode = fullNameCode; + return this; + } + + public Variable withCode(String code) { + this.code = code; + return this; + } + public String getColumn() { if (StringUtils.hasText(column)) { return column; @@ -111,12 +128,15 @@ public class Variable { public Variable with(TermColumn column) { this.name = column.getName(); + this.code = column.getCode(); this.column = column.getColumn(); this.metadata = column.isMetadata(); this.description = column.getDescription(); this.fullName = column.getFullName(); + this.fullNameCode = column.getFullNameCode() == null ? null : column.getFullNameCode().copy(); this.type = column.getDataType(); this.termTypes = column.getTermTypes(); + withOptions(column.safeOptions()); return this; } @@ -132,8 +152,7 @@ public class Variable { child.setId(main.id + "." + child.getId()); } - if (StringUtils.hasText(child.getFullName()) - && StringUtils.hasText(main.getFullName())) { + if (StringUtils.hasText(child.getFullName()) && StringUtils.hasText(main.getFullName())) { child.setFullName(main.getFullName() + "/" + child.getFullName()); } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmAction.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmAction.java index 0e071981..a970a463 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmAction.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmAction.java @@ -22,6 +22,12 @@ public class AlarmAction extends AlarmTaskExecutorProvider.Config { List variables = new ArrayList<>(); + variables.add( + Variable.of(AlarmConstants.ConfigKey.alarmConfigId, + LocaleUtils.resolveMessage("message.alarm_config_id", "告警配置ID")) + .withType(StringType.GLOBAL) + ); + variables.add( Variable.of(AlarmConstants.ConfigKey.alarmName, LocaleUtils.resolveMessage("message.alarm_config_name", "告警配置名称")) diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTrigger.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTrigger.java index dd99ecec..d8fd7108 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTrigger.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTrigger.java @@ -9,31 +9,23 @@ import org.hswebframework.ezorm.rdb.executor.PrepareSqlRequest; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.ezorm.rdb.operator.builder.fragments.AbstractTermsFragmentBuilder; import org.hswebframework.ezorm.rdb.operator.builder.fragments.EmptySqlFragments; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.validator.ValidatorUtils; -import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProviders; -import org.jetlinks.community.rule.engine.scene.*; -import org.jetlinks.core.device.DeviceRegistry; -import org.jetlinks.core.metadata.DeviceMetadata; import org.jetlinks.core.metadata.types.StringType; import org.jetlinks.core.things.ThingMetadata; import org.jetlinks.core.things.ThingsRegistry; -import org.jetlinks.core.utils.Reactors; import org.jetlinks.community.TimerSpec; -import org.jetlinks.community.reactorql.term.TermType; import org.jetlinks.community.reactorql.term.TermTypeSupport; import org.jetlinks.community.reactorql.term.TermTypes; import org.jetlinks.community.rule.engine.executor.DeviceMessageSendTaskExecutorProvider; +import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProviders; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorSpec; import org.jetlinks.community.rule.engine.executor.device.SelectorValue; +import org.jetlinks.community.rule.engine.scene.*; import org.jetlinks.community.rule.engine.scene.term.TermColumn; import org.jetlinks.community.rule.engine.scene.value.TermValue; -import org.jetlinks.reactor.ql.DefaultReactorQLContext; -import org.jetlinks.reactor.ql.ReactorQL; -import org.jetlinks.reactor.ql.ReactorQLContext; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; import org.springframework.util.Assert; @@ -71,7 +63,11 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro public SqlRequest createSql(List terms, boolean hasWhere) { - Map termsMap = SceneUtils.expandTerm(terms); + Map> termsMap = SceneUtils.expandTerm(terms); + List termList = new ArrayList<>(); + for (List values : termsMap.values()) { + termList.addAll(values); + } // select * from ( // select // this.deviceId deviceId, @@ -84,7 +80,7 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro // coalesce(this.properties.temp,device.property.recent(deviceId,'temp')) temp_recent, // property.metric('device',deviceId,'temp','max') temp_metric_max // ) t where t.temp_current > t.temp_metric_max and t._now between ? and ? - Set selectColumns = Sets.newLinkedHashSetWithExpectedSize(10 + termsMap.size()); + Set selectColumns = Sets.newLinkedHashSetWithExpectedSize(10 + termList.size()); selectColumns.add("now() \"_now\""); selectColumns.add("this.timestamp \"timestamp\""); selectColumns.add("this.deviceId \"deviceId\""); @@ -107,7 +103,6 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro case writeProperty: selectColumns.add("this.success \"success\""); case reportProperty: - case readPropertyReply: selectColumns.add("this.properties \"properties\""); break; case reportEvent: @@ -118,7 +113,7 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro selectColumns.add("this['output'] \"output\""); break; } - for (Term value : termsMap.values()) { + for (Term value : termList) { String column = value.getColumn(); if (StringUtils.hasText(value.getColumn())) { String selectColumn = createSelectColumn(column); @@ -186,9 +181,6 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro case reportProperty: topic = "/device/" + productId + "/%s/message/property/report"; break; - case readPropertyReply: - topic = "/device/" + productId + "/%s/message/property/read/reply"; - break; case reportEvent: topic = "/device/" + productId + "/%s/message/event/" + operation.getEventId(); break; @@ -231,7 +223,7 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro return "\"" + topic + "\""; } - public static final TermBuilder termBuilder = new TermBuilder(); + static final TermBuilder termBuilder = new TermBuilder(); static class TermBuilder extends AbstractTermsFragmentBuilder { @@ -252,95 +244,13 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro Term copy = refactorTermValue(DEFAULT_FILTER_TABLE, term.clone()); - return support.createSql(copy.getColumn(), copy.getValue(), term); + return support.createSql(copy.getColumn(), copy.getValue(), copy); } } - static String createTermColumn(String tableName, String column) { - String[] arr = column.split("[.]"); - - // properties.xxx.last的场景 - if (arr.length > 3 && arr[0].equals("properties")) { - column = tableName + "['" + createColumnAlias(column, false) + "." + String.join(".", Arrays.copyOfRange(arr, 2, arr.length - 1)) + "']"; - } else { - column = tableName + "['" + createColumnAlias(column, false) + "']"; - } - return column; - } - public static Term refactorTermValue(String tableName, Term term) { - if (term.getColumn() == null) { - return term; - } - String[] arr = term.getColumn().split("[.]"); - - List values = TermValue.of(term); - if (values.size() == 0) { - return term; - } - - Function parser = value -> { - //上游变量 - if (value.getSource() == TermValue.Source.variable - || value.getSource() == TermValue.Source.upper) { - term.getOptions().add(TermType.OPTIONS_NATIVE_SQL); - return tableName + "['" + value.getValue() + "']"; - } - //指标 - else if (value.getSource() == TermValue.Source.metric) { - term.getOptions().add(TermType.OPTIONS_NATIVE_SQL); - return tableName + "['" + arr[1] + "_metric_" + value.getMetric() + "']"; - } - //手动设置值 - else { - return value.getValue(); - } - }; - Object val; - if (values.size() == 1) { - val = parser.apply(values.get(0)); - } else { - val = values - .stream() - .map(parser) - .collect(Collectors.toList()); - } - - String column; - // properties.xxx.last的场景 - if (arr.length > 3 && arr[0].equals("properties")) { - column = tableName + "['" + createColumnAlias(term.getColumn(), false) + "." + String.join(".", Arrays.copyOfRange(arr, 2, arr.length - 1)) + "']"; - } else if (!isDirectTerm(arr[0])) { - column = tableName + "['" + createColumnAlias(term.getColumn(), false) + "']"; - } else { - column = term.getColumn(); - } - - if (term.getOptions().contains(TermType.OPTIONS_NATIVE_SQL)) { - val = NativeSql.of(String.valueOf(val)); - } - - term.setColumn(column); - - term.setValue(val); - - return term; - } - - private static boolean isDirectTerm(String column) { - //直接term,构建Condition输出条件时使用 - return isBranchTerm(column) || isSceneTerm(column); - } - - private static boolean isBranchTerm(String column) { - return column.startsWith("branch_") && - column.contains("_group_") - && column.contains("_action_"); - } - - private static boolean isSceneTerm(String column) { - return column.startsWith("scene"); + return SceneUtils.refactorTerm(tableName, term); } static String parseProperty(String column) { @@ -369,6 +279,8 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro return "coalesce(this['properties." + property + "']" + ",device.property.recent(deviceId,'" + property + "',timestamp - 1))"; case last: return "device.property.recent(deviceId,'" + property + "',timestamp - 1)"; + case lastTime: + return "device.property_time.recent(deviceId,'" + property + "',timestamp - 1)"; } } catch (IllegalArgumentException ignore) { @@ -433,7 +345,6 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro case offline: case reportEvent: case reportProperty: - case readPropertyReply: return; } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTriggerProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTriggerProvider.java index 7c19542d..10c24a02 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTriggerProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTriggerProvider.java @@ -1,23 +1,25 @@ package org.jetlinks.community.rule.engine.scene.internal.triggers; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.jetlinks.core.things.ThingsRegistry; -import org.jetlinks.community.rule.engine.scene.SceneTriggerProvider; +import org.jetlinks.community.rule.engine.scene.AbstractSceneTriggerProvider; import org.jetlinks.community.rule.engine.scene.Variable; import org.jetlinks.community.rule.engine.scene.term.TermColumn; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import java.util.List; @Component -@AllArgsConstructor -public class DeviceTriggerProvider implements SceneTriggerProvider { +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "rule.scene.trigger.device") +public class DeviceTriggerProvider extends AbstractSceneTriggerProvider { public static final String PROVIDER = "device"; @@ -39,8 +41,8 @@ public class DeviceTriggerProvider implements SceneTriggerProvider terms, boolean hasWhere) { - return config.createSql(terms, hasWhere); + public SqlRequest createSql(DeviceTrigger config, List terms, boolean hasFilter) { + return config.createSql(terms, hasFilter); } @Override diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/ManualTriggerProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/ManualTriggerProvider.java index 78807348..69d76dde 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/ManualTriggerProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/ManualTriggerProvider.java @@ -8,11 +8,12 @@ import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.core.metadata.types.DateTimeType; import org.jetlinks.community.reactorql.term.TermTypes; -import org.jetlinks.community.rule.engine.scene.SceneTriggerProvider; +import org.jetlinks.community.rule.engine.scene.AbstractSceneTriggerProvider; import org.jetlinks.community.rule.engine.scene.Variable; import org.jetlinks.community.rule.engine.scene.term.TermColumn; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; @@ -20,7 +21,8 @@ import java.util.Collections; import java.util.List; @Component -public class ManualTriggerProvider implements SceneTriggerProvider { +@ConfigurationProperties(prefix = "rule.scene.trigger.manual") +public class ManualTriggerProvider extends AbstractSceneTriggerProvider { public static final String PROVIDER = "manual"; @@ -40,7 +42,7 @@ public class ManualTriggerProvider implements SceneTriggerProvider terms, boolean hasWhere) { + public SqlRequest createSql(ManualTrigger config, List terms, boolean hasFilter) { return EmptySqlRequest.INSTANCE; } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/TimerTriggerProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/TimerTriggerProvider.java index 21f3ddf6..209d4097 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/TimerTriggerProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/TimerTriggerProvider.java @@ -32,7 +32,7 @@ public class TimerTriggerProvider implements SceneTriggerProvider @Override public String getName() { - return "手动触发"; + return "定时触发"; } @Override @@ -41,7 +41,7 @@ public class TimerTriggerProvider implements SceneTriggerProvider } @Override - public SqlRequest createSql(TimerTrigger config, List terms, boolean hasWhere) { + public SqlRequest createSql(TimerTrigger config, List terms, boolean hasFilter) { return EmptySqlRequest.INSTANCE; } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/term/TermColumn.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/term/TermColumn.java index 2659f3a2..0956ff3a 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/term/TermColumn.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/term/TermColumn.java @@ -7,13 +7,17 @@ import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.metadata.types.BooleanType; import org.jetlinks.core.metadata.types.EnumType; import org.jetlinks.community.PropertyMetadataConstants; import org.jetlinks.community.PropertyMetric; import org.jetlinks.community.rule.engine.scene.DeviceOperation; +import org.jetlinks.community.terms.I18nSpec; import org.springframework.util.StringUtils; + import org.jetlinks.community.reactorql.term.TermType; import org.jetlinks.community.reactorql.term.TermTypes; @@ -29,12 +33,21 @@ public class TermColumn { @Schema(description = "条件列") private String column; + @Schema(description = "名称编码") + private String code; + @Schema(description = "名称") private String name; @Schema(description = "全名") private String fullName; + @Schema(description = "全名显码") + private I18nSpec fullNameCode; + + @Schema(description = "描述编码") + private String codeDesc; + @Schema(description = "说明") private String description; @@ -44,6 +57,9 @@ public class TermColumn { @Schema(description = "是否为物模型列") private boolean metadata; + @Schema(description = "物模型各层级变量名称集合。如:['事件名称','事件属性']") + private List metadataHierarchyNames; + /** * @see Term#getTermType() */ @@ -72,10 +88,10 @@ public class TermColumn { if (CollectionUtils.isNotEmpty(children)) { copy.setChildren( - children.stream() - .filter(child -> childrenPredicate.test(child.getColumn())) - .map(child -> child.copyColumn(childrenPredicate)) - .collect(Collectors.toList()) + children.stream() + .filter(child -> childrenPredicate.test(child.getColumn())) + .map(child -> child.copyColumn(childrenPredicate)) + .collect(Collectors.toList()) ); } @@ -163,6 +179,25 @@ public class TermColumn { return of(column, name, type, null); } + public static TermColumn of(String column, String code, String defaultName, DataType type) { + TermColumn termColumn = of(column, defaultName, type, null); + termColumn.setCode(code); + return termColumn; + } + + + public static TermColumn of(String column, + String code, + String defaultName, + DataType type, + String defaultDescription) { + TermColumn termColumn = of(column, defaultName, type, resolveI18n(getDescriptionByCode(code), defaultDescription)); + termColumn.setCode(code); + termColumn.setCodeDesc(getDescriptionByCode(code)); + return termColumn; + } + + public static TermColumn of(String column, String name, DataType type, String description) { TermColumn termColumn = new TermColumn(); termColumn.setColumn(column); @@ -178,8 +213,12 @@ public class TermColumn { options.add(PropertyMetric.of(element.getValue(), element.getText(), null)); } termColumn.setOptions(options); + termColumn.withOther("elements", elements); } } + if (type instanceof BooleanType) { + termColumn.withOther("bool", type); + } return termColumn; } @@ -187,39 +226,51 @@ public class TermColumn { return new TermColumn().with(metadata); } - public void refactorDescription(Function columnGetter) { + public static List refactorTermsInfo(String perText, List terms) { + Map allColumn = terms + .stream() + .collect(Collectors.toMap(TermColumn::getColumn, Function.identity(), (a, b) -> a)); + for (TermColumn term : terms) { + + term.refactorDescription(perText, allColumn::get); + term.refactorFullName(null); + } + return terms; + } + + public void refactorDescription(String perText, Function columnGetter) { if (!StringUtils.hasText(description)) { - doRefactorDescription(columnGetter); + doRefactorDescription(perText, columnGetter); } if (children != null) { for (TermColumn child : children) { - child.refactorDescription(columnGetter); + child.refactorDescription(perText, columnGetter); } } } - public void doRefactorDescription(Function columnGetter) { + public void doRefactorDescription(String perText, Function columnGetter) { if (CollectionUtils.isNotEmpty(children)) { return; } - //属性 - if (column.startsWith("properties")) { + //属性或采集器数据 + if (column.startsWith(perText)) { String[] arr = column.split("[.]"); if (arr.length > 3) { TermColumn column = columnGetter.apply(arr[1]); //类型,report,recent,latest String type = arr[arr.length - 1]; setDescription( - DeviceOperation.PropertyValueType - .valueOf(type) - .getNestDescription(column.name) + DeviceOperation.PropertyValueType + .valueOf(type) + .getNestDescription(column.name) ); } else { String type = arr[arr.length - 1]; setDescription( - DeviceOperation.PropertyValueType - .valueOf(type) - .getDescription() + DeviceOperation.PropertyValueType + .valueOf(type) + .getDescription() ); } } @@ -227,9 +278,26 @@ public class TermColumn { public void refactorFullName(String parentName) { if (StringUtils.hasText(parentName)) { - this.fullName = parentName + "/" + name; + //code不为空,表示当前termColumn需要国际化 + if (StringUtils.hasText(code)) { + this.fullNameCode = I18nSpec + .of("message.scene_term_column_full_name", null, parentName) + .withArgs(code, name); + } else { + this.fullNameCode = I18nSpec + .of("message.scene_term_column_full_name", null, parentName, name); + } + this.fullName = fullNameCode.resolveI18nMessage(); } else { this.fullName = name; + if (CollectionUtils.isEmpty(children)) { + if (StringUtils.hasText(code)) { + this.fullNameCode = I18nSpec.of(code, name); + this.fullName = fullNameCode.resolveI18nMessage(); + } else { + this.fullName = name; + } + } } if (CollectionUtils.isNotEmpty(children)) { for (TermColumn child : children) { @@ -238,6 +306,11 @@ public class TermColumn { } } + public TermColumn withOther(String key, Object value) { + safeOptions().put(key, value); + return this; + } + public TermColumn withOthers(Map options) { if (options != null) { safeOptions().putAll(options); @@ -249,4 +322,15 @@ public class TermColumn { return others == null ? others = new HashMap<>() : others; } + + private static String getDescriptionByCode(String code) { + return code + "_desc"; + } + + private static String resolveI18n(String key, String name) { + if (StringUtils.hasText(name)) { + return LocaleUtils.resolveMessage(key, name); + } + return LocaleUtils.resolveMessage(key); + } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmConfigService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmConfigService.java index d8eefafc..1b723835 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmConfigService.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmConfigService.java @@ -43,7 +43,7 @@ public class AlarmConfigService extends GenericReactiveCrudService handleAlarm(AlarmHandleInfo info){ return alarmRecordService - .changeRecordState(info.getState(), info.getAlarmRecordId()) + .changeRecordState(info.getType(), info.getState(), info.getAlarmRecordId()) .flatMap(total-> { if (total > 0){ return handleHistoryRepository diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleTypeDictInit.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleTypeDictInit.java new file mode 100644 index 00000000..03beb8d7 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleTypeDictInit.java @@ -0,0 +1,53 @@ +package org.jetlinks.community.rule.engine.service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.codec.digest.DigestUtils; +import org.hswebframework.web.dictionary.entity.DictionaryEntity; +import org.hswebframework.web.dictionary.entity.DictionaryItemEntity; +import org.jetlinks.community.dictionary.DictionaryConstants; +import org.jetlinks.community.dictionary.DictionaryInitInfo; +import org.jetlinks.community.rule.engine.enums.AlarmHandleType; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +@AllArgsConstructor +@Slf4j +@Component +public class AlarmHandleTypeDictInit implements DictionaryInitInfo { + + public static final String DICT_ID = "alarm_handle_type"; + + @Override + public Collection getDict() { + + DictionaryEntity entity = new DictionaryEntity(); + entity.setId(DICT_ID); + entity.setName("告警处理类型"); + entity.setClassified(DictionaryConstants.CLASSIFIED_SYSTEM); + entity.setStatus((byte) 1); + + List items = new ArrayList<>(); + + int index = 1; + for (AlarmHandleType type : AlarmHandleType.values()) { + DictionaryItemEntity item = new DictionaryItemEntity(); + item.setId(DigestUtils.md5Hex(DICT_ID + type.getValue())); + item.setValue(type.getValue()); + item.setText(type.getText()); + item.setDictId(DICT_ID); + item.setStatus((byte) 1); + item.setOrdinal(index++); + items.add(item); + } + + entity.setItems(items); + + return Collections.singletonList(entity); + + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmRecordService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmRecordService.java index 58043868..dfadab06 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmRecordService.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmRecordService.java @@ -13,14 +13,18 @@ public class AlarmRecordService extends GenericReactiveCrudService changeRecordState(AlarmRecordState state, String id) { + public Mono changeRecordState(String type, + AlarmRecordState state, + String id) { return createUpdate() .set(AlarmRecordEntity::getState, state) .set(AlarmRecordEntity::getHandleTime, System.currentTimeMillis()) + .set(AlarmRecordEntity::getHandleType, type) .where(AlarmRecordEntity::getId, id) .not(AlarmRecordEntity::getState, state) .execute(); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/ElasticSearchAlarmHistoryService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/ElasticSearchAlarmHistoryService.java index eaae1a7b..1d7173e3 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/ElasticSearchAlarmHistoryService.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/ElasticSearchAlarmHistoryService.java @@ -1,5 +1,6 @@ package org.jetlinks.community.rule.engine.service; +import com.alibaba.fastjson.JSONObject; import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.core.param.QueryParam; @@ -52,8 +53,11 @@ public class ElasticSearchAlarmHistoryService implements AlarmHistoryService { } private Map createData(AlarmHistoryInfo info) { - return FastBeanCopier.copy(info, new HashMap<>(16)); - + Map data = FastBeanCopier.copy(info, new HashMap<>(16), "termSpec"); + if (info.getTermSpec() != null) { + data.put("termSpec", JSONObject.toJSONString(info.getTermSpec())); + } + return data; } public void init() { @@ -76,6 +80,10 @@ public class ElasticSearchAlarmHistoryService implements AlarmHistoryService { .addProperty("alarmInfo", StringType.GLOBAL) .addProperty("creatorId", StringType.GLOBAL) + .addProperty("termSpec", StringType.GLOBAL) + .addProperty("triggerDesc", StringType.GLOBAL) + .addProperty("actualDesc", StringType.GLOBAL) + .addProperty("alarmConfigSource", StringType.GLOBAL) .addProperty("bindings", new ArrayType().elementType(StringType.GLOBAL)) ).block(Duration.ofSeconds(10)); } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/LocalRuleInstanceRepository.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/LocalRuleInstanceRepository.java new file mode 100644 index 00000000..838e372e --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/LocalRuleInstanceRepository.java @@ -0,0 +1,74 @@ +package org.jetlinks.community.rule.engine.service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jetlinks.community.rule.engine.entity.RuleInstanceEntity; +import org.jetlinks.community.rule.engine.entity.SceneEntity; +import org.jetlinks.community.rule.engine.enums.RuleInstanceState; +import org.jetlinks.rule.engine.api.model.RuleEngineModelParser; +import org.jetlinks.rule.engine.cluster.RuleInstance; +import org.jetlinks.rule.engine.cluster.RuleInstanceRepository; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; + +@Component +@AllArgsConstructor +@Slf4j +public class LocalRuleInstanceRepository implements RuleInstanceRepository { + private final RuleInstanceService instanceService; + + private final SceneService sceneService; + + private final RuleEngineModelParser parser; + + @Nonnull + @Override + public Flux findAll() { + return Flux + .merge( + instanceService + .createQuery() + .where(RuleInstanceEntity::getState, RuleInstanceState.started) + .fetch() + .flatMap(en -> Mono + .fromCallable(() -> en.toRuleInstance(parser)) + .onErrorResume(err -> { + log.warn("convert rule instance [{}] error", en.getId(), err); + return Mono.empty(); + })), + sceneService + .createQuery() + .where(SceneEntity::getState, RuleInstanceState.started) + .fetch() + .flatMap(en -> Mono + .defer(en::toRule) + .onErrorResume(err -> { + log.warn("convert scene rule [{}] error", en.getId(), err); + return Mono.empty(); + })) + ); + } + + @Nonnull + @Override + public Flux findById(String id) { + return Flux.merge( + instanceService + .createQuery() + .where(RuleInstanceEntity::getId, id) + .and(RuleInstanceEntity::getState, RuleInstanceState.started) + .fetch() + .map(en -> en.toRuleInstance(parser)), + sceneService + .createQuery() + .where(SceneEntity::getId, id) + .and(SceneEntity::getState, RuleInstanceState.started) + .fetch() + .flatMap(SceneEntity::toRule) + ) + ; + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/SceneService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/SceneService.java index 1903f037..7ea4e560 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/SceneService.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/SceneService.java @@ -1,7 +1,6 @@ package org.jetlinks.community.rule.engine.service; import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.crud.events.EntityCreatedEvent; import org.hswebframework.web.crud.events.EntityDeletedEvent; import org.hswebframework.web.crud.events.EntityModifyEvent; @@ -14,7 +13,6 @@ import org.jetlinks.community.rule.engine.scene.SceneRule; import org.jetlinks.community.rule.engine.web.request.SceneExecuteRequest; import org.jetlinks.rule.engine.api.RuleData; import org.jetlinks.rule.engine.api.RuleEngine; -import org.springframework.boot.CommandLineRunner; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,8 +28,7 @@ import java.util.Map; @Service @AllArgsConstructor -@Slf4j -public class SceneService extends GenericReactiveCrudService implements CommandLineRunner { +public class SceneService extends GenericReactiveCrudService { private final RuleEngine ruleEngine; @@ -161,7 +158,10 @@ public class SceneService extends GenericReactiveCrudService ruleEngine.startRule(scene.getId(), instance.getModel()).then()) + ; } return Mono.empty(); }) @@ -178,19 +178,4 @@ public class SceneService extends GenericReactiveCrudService Mono - .defer(() -> ruleEngine.startRule(e.getId(), e.toRule().getModel()).then()) - .onErrorResume(err -> { - log.warn("启动场景[{}]失败", e.getName(), err); - return Mono.empty(); - })) - .subscribe(); - } - } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmRuleBindController.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmRuleBindController.java index 4570cdd5..5b666b61 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmRuleBindController.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmRuleBindController.java @@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; import java.util.List; +import java.util.function.Function; /** * 告警规则绑定. @@ -49,4 +50,28 @@ public class AlarmRuleBindController implements ReactiveServiceCrudController deleteAlarmBindByBranchId(@PathVariable @Parameter(description = "告警配置ID") String alarmId, + @PathVariable @Parameter(description = "场景联动ID") String ruleId, + @RequestBody @Parameter(description = "分支ID") Mono> branchIndex) { + return branchIndex + .flatMap(idList -> service + .createDelete() + .where(AlarmRuleBindEntity::getAlarmId, alarmId) + .and(AlarmRuleBindEntity::getRuleId, ruleId) + .in(AlarmRuleBindEntity::getBranchIndex, idList) + .execute()); + } + + @PostMapping("/_delete") + @DeleteAction + @Operation(summary = "批量删除多个规则或告警的绑定") + public Mono deleteAlarmBindById(@RequestBody @Parameter(description = "绑定信息") Mono> payload) { + return payload + .flatMapIterable(Function.identity()) + .map(AlarmRuleBindEntity::getId) + .as(service::deleteById); + } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneController.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneController.java index cdf6d017..5c3000e9 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneController.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneController.java @@ -37,8 +37,6 @@ import java.util.function.Function; @Resource(id = "rule-scene", name = "场景管理") public class SceneController implements ReactiveServiceQueryController { - private final DeviceRegistry deviceRegistry; - @Getter private final SceneService service; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SelectorInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SelectorInfo.java new file mode 100644 index 00000000..be1d03e9 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SelectorInfo.java @@ -0,0 +1,38 @@ +package org.jetlinks.community.rule.engine.web.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProvider; + +import java.io.Serializable; + +/** + * @author liusq + * @date 2024/4/17 + */ +@Getter +@Setter +public class SelectorInfo implements Serializable { + @Schema(description = "ID") + private String id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "说明") + private String description; + + public static SelectorInfo of(DeviceSelectorProvider provider) { + SelectorInfo info = new SelectorInfo(); + info.setId(provider.getProvider()); + + info.setName(LocaleUtils + .resolveMessage("message.device_selector_" + provider.getProvider(), provider.getName())); + + info.setDescription(LocaleUtils + .resolveMessage("message.device_selector_" + provider.getProvider() + "_desc", provider.getName())); + return info; + } +} diff --git a/jetlinks-standalone/pom.xml b/jetlinks-standalone/pom.xml index 4bfdf287..8bc3f952 100644 --- a/jetlinks-standalone/pom.xml +++ b/jetlinks-standalone/pom.xml @@ -91,7 +91,6 @@ org.hswebframework.web hsweb-system-dictionary - ${hsweb.framework.version} diff --git a/pom.xml b/pom.xml index 56f28013..fe0af1a7 100644 --- a/pom.xml +++ b/pom.xml @@ -32,6 +32,8 @@ 1.0.16 1.0.1 + + 1.0.0 Borca-SR2 @@ -479,6 +481,11 @@ ${jetlinks.version} + + org.hswebframework.web + hsweb-system-dictionary + ${hsweb.framework.version} + com.google.guava