feat(rule-engine): 告警场景2.2相关功能更新 (#552)

This commit is contained in:
liusq 2024-08-21 10:58:03 +08:00 committed by GitHub
parent f946c97ce5
commit f754740646
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
79 changed files with 3728 additions and 689 deletions

View File

@ -56,6 +56,17 @@
<artifactId>h2-mvstore</artifactId>
</dependency>
<dependency>
<groupId>org.jetlinks.sdk</groupId>
<artifactId>jetlinks-sdk-api</artifactId>
<version>${jetlinks.sdk.version}</version>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-system-dictionary</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>

View File

@ -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<String, Object> data;
/**
* 告警触发条件
*/
private TermSpec termSpec;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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> T convert(Class<T> 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

View File

@ -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<String, Map<String, DictionaryItemEntity>> itemStore = new ConcurrentHashMap<>();
@Nonnull
@Override
public List<EnumDict<?>> getItems(@Nonnull String dictId) {
Map<String, DictionaryItemEntity> itemEntityMap = itemStore.get(dictId);
if (MapUtils.isEmpty(itemEntityMap)) {
return Collections.emptyList();
}
return new ArrayList<>(itemEntityMap.values());
}
@Nonnull
@Override
public Optional<EnumDict<?>> getItem(@Nonnull String dictId, @Nonnull String itemId) {
Map<String, DictionaryItemEntity> itemEntityMap = itemStore.get(dictId);
if (itemEntityMap == null) {
return Optional.empty();
}
return Optional.ofNullable(itemEntityMap.get(itemId));
}
public void registerItems(List<DictionaryItemEntity> items) {
items.forEach(this::registerItem);
}
public void removeItems(List<DictionaryItemEntity> 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();
}
}

View File

@ -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<EnumDict<?>> 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<EnumDict<?>> 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<EnumDict<?>> 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<EnumDict<?>> getItem(@Nonnull String dictId,
@Nonnull String itemId) {
return HOLDER == null ? Optional.empty() : HOLDER.getItem(dictId, itemId);
}
public static long toMask(Collection<EnumDict<?>> items) {
long value = 0L;
for (EnumDict<?> t1 : items) {
value |= t1.getMask();
}
return value;
}
}

View File

@ -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;
/**
* 定义字段是一个数据字典,和枚举的使用方式类似.
* <p>
* 区别是数据的值通过{@link Dictionaries}进行获取.
*
* <pre>{@code
* public class MyEntity{
*
* //数据库存储的是枚举的值
* @Column(length=32)
* @Dictionary("my_status")
* @ColumnType(javaType=String.class)
* private EnumDict<String> status;
*
* @Column
* @Dictionary("my_types")
* //使用long来存储数据,表示使用字段的序号来进行mask运算进行存储.
* @ColumnType(javaType=Long.class,jdbcType=JDBCType.BIGINT)
* private EnumDict<String>[] types;
* }
* }</pre>
* <b>️注意</b>
* <ul>
* <li>
* 字段类型只支持{@code EnumDict<String>},{@code EnumDict<String>[]},{@code List<EnumDict<String>>}
* </li>
* <li>
* 多选时建议使用位掩码来存储: {@code @ColumnType(javaType=Long.class,jdbcType=JDBCType.BIGINT) },便于查询.
* </li>
* <li>使用位掩码存储字典值时,基于{@link EnumDict#ordinal()}进行计算,因此字段选项数量不能超过64个,修改字典时,请注意序号值变化</li>
* <li>模块需要引入依赖:<pre>{@code
* <dependency>
* <groupId>org.hswebframework.web</groupId>
* <artifactId>hsweb-system-dictionary</artifactId>
* </dependency>
* }</pre></li>
* </ul>
*
*
* @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();
}

View File

@ -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<Annotation> 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) {
}
}

View File

@ -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<DictionaryInitInfo> initInfo,
DefaultDictionaryService defaultDictionaryService,
DefaultDictionaryItemService itemService) {
return new DictionaryInitManager(initInfo, defaultDictionaryService, itemService);
}
}
}

View File

@ -0,0 +1,9 @@
package org.jetlinks.community.dictionary;
public interface DictionaryConstants {
/**
* 系统分类标识
*/
String CLASSIFIED_SYSTEM = "system";
}

View File

@ -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<DictionaryEntity> 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<DictionaryEntity> 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<DictionaryEntity> 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<DictionaryEntity> 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();
})
);
}
}

View File

@ -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<DictionaryEntity> getDict();
default Flux<DictionaryEntity> getDictAsync() {
if (CollectionUtils.isEmpty(getDict())) {
return Flux.empty();
}
return Flux.fromIterable(getDict());
}
}

View File

@ -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<DictionaryEntity> inits = new ArrayList<>();
public final ObjectProvider<DictionaryInitInfo> initInfo;
private final DefaultDictionaryService defaultDictionaryService;
private final DefaultDictionaryItemService itemService;
public DictionaryInitManager(ObjectProvider<DictionaryInitInfo> 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<DictionaryItemEntity> 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<DictionaryItemEntity> generateItems(List<DictionaryEntity> dictionaryList) {
List<DictionaryItemEntity> 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;
}
}

View File

@ -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<EnumDict<?>> {
@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;
}
}

View File

@ -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<EnumDict<?>> getItems(@Nonnull String dictId);
/**
* 获取字段选项
*
* @param dictId 字典ID
* @param itemId 选项ID
* @return 选项值
*/
@Nonnull
Optional<EnumDict<?>> getItem(@Nonnull String dictId,
@Nonnull String itemId);
}

View File

@ -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<Object, Object> {
public static final String TYPE = "Enum";
private final boolean multiple;
private final String dictId;
private final JDBCType jdbcType;
private boolean array = false;
private Function<EnumDict<?>,Object> fieldValueConverter = EnumDict::getWriteJSONObject;;
public EnumFieldType withArray(boolean array) {
this.array = array;
return this;
}
public EnumFieldType withFieldValueConverter(Function<EnumDict<?>,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)
.<Object>map(EnumDict::getValue)
.orElseGet(() -> {
if (value instanceof EnumDict) {
return ((EnumDict<?>) value).getValue();
}
return value;
});
}
@Override
public Object decode(Object data) {
if (multiple) {
List<Object> 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<JDBCType> 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;
}
}

View File

@ -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();
}

View File

@ -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<FieldTypeProvider> supports = Provider.create(FieldTypeProvider.class);
String getProvider();
default String getProviderName() {
return getProvider();
}
default int getDefaultLength() {
return 255;
}
Set<JDBCType> getSupportJdbcTypes();
FieldType create(FieldTypeSpec configuration);
static FieldType createType(FieldTypeSpec spec) {
return supports
.getNow(spec.getName())
.create(spec);
}
}

View File

@ -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<String,Object> configuration;
@Override
public Map<String, Object> values() {
return configuration;
}
}

View File

@ -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<Boolean> 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));
}
}

View File

@ -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 <T> 类型
* @author zhouhao
* @since 2.2
*/
public interface Provider<T> {
/**
* 根据类型创建Provider
*
* @param type 类型
* @param <T> 类型
* @return Provider
*/
static <T> Provider<T> create(Class<? super T> type) {
return new SimpleProvider<>(type.getSimpleName());
}
/**
* 注册一个提供商,可通过调用返回值{@link Disposable#dispose()}来注销.
*
* @param id ID
* @param provider 提供商实例
* @return Disposable
*/
Disposable register(String id, T provider);
/**
* 获取所有的注册的提供商
*
* @return 提供商
*/
List<T> getAll();
/**
* 注册提供商,如果已经存在则忽略注册,并返回旧的提供商实例
*
* @param id ID
* @param provider 提供商实例
* @return 旧的提供商实例
*/
T registerIfAbsent(String id, T provider);
/**
* 注册提供商,如果已经存在则忽略注册,并返回旧的提供商实例,否则返回注册后的提供商实例
*
* @param id ID
* @param providerBuilder 提供商构造器
* @return 如果已经存在则忽略注册, 并返回旧的提供商实例, 否则返回注册后的提供商实例
*/
T registerIfAbsent(String id, Function<String, T> 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<T> hook);
/**
* 根据ID获取提供商
*
* @param id ID
* @return Optional
*/
Optional<T> get(String id);
/**
* 根据ID获取提供商,如果不存在将抛出{@link UnsupportedOperationException}
*
* @param id ID
* @return 提供商
*/
T getNow(String id);
interface Hook<T> {
/**
* 当注册时调用
*
* @param provider 提供商实例
*/
void onRegister(T provider);
/**
* 当注销时调用
*
* @param provider 提供商实例
*/
void onUnRegister(T provider);
}
}

View File

@ -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<T> implements Provider<T> {
private final Map<String, T> providers = new ConcurrentHashMap<>();
private final List<Hook<T>> 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<T> 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<String, T> 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<T> hook) {
hooks.add(hook);
return () -> hooks.remove(hook);
}
public Optional<T> 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<Hook<T>, T> executor) {
if (hooks.isEmpty()) {
return;
}
for (Hook<T> hook : hooks) {
executor.accept(hook, provider);
}
}
}

View File

@ -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;
/**
* 支持国际化参数嵌套的国际化编码类适用于实现国际化表达式的参数也需要国际化的场景
*
* <pre>{@code
*
* 国际化表达式携带的参数也需要国际化示例
* 示例参数:
*
* {
* "code": "message.scene.term.full_name",
* "args": [
* {
* "defaultMessage": "温度"
* },
* {
* "code": "message.property.recent"
* }
* ]
* }
* 在国际化配置文件中的配置示例:
* message.scene.term.full_name=属性:{0}/{1}
* message.property.recent=当前值
*
* 国际化完成后的结果为属性:温度/当前值
* }</pre>
*
* @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<I18nSpec> 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<I18nSpec> of(Object... args) {
List<I18nSpec> codes = new ArrayList<>();
for (Object arg : args) {
I18nSpec i18NSpec = new I18nSpec();
i18NSpec.setDefaultMessage(arg);
codes.add(i18NSpec);
}
return codes;
}
protected List<Object> parseI18nCodeParams() {
if (CollectionUtils.isEmpty(args)) {
return new ArrayList<>();
}
List<Object> 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);
}
}

View File

@ -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<TermSpec> children;
private List<String> options;
private boolean expectIsExpr;
@Schema(description = "是否为物模型变量")
private boolean metadata;
@Override
public JSONObject toJson() {
@SuppressWarnings("all")
Map<String, Object> 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<String> parseTermSpecActualDesc(TermSpec termSpec) {
Set<String> 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<TermSpec> of(List<Term> terms) {
return of(terms, (term, spec) -> {
});
}
public static List<TermSpec> of(List<Term> terms, BiConsumer<Term, TermSpec> customizer) {
if (terms == null) {
return null;
}
return terms
.stream()
.map(spec -> of(spec, customizer))
.collect(Collectors.toList());
}
public static TermSpec of(Term term, BiConsumer<Term, TermSpec> 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<String, Object> 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<String, Object> 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<TermSpec> terms) {
return toString(new StringBuilder(), terms).toString();
}
public static StringBuilder toString(StringBuilder builder, List<TermSpec> 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<TermSpec> 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<TermSpec> 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();
}
}

View File

@ -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
* <pre>{@code
* /rel/{objectType}/{objectId}/{relation}/{topic}
*
* : /rel/用户/user1/manager/{topic}
* }</pre>
*
* @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/*";

View File

@ -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;
}

View File

@ -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.
*
* <p>
* select * from ( sql )
* group by
* _window('1s') --时间窗口
@ -78,6 +78,7 @@ public class ShakeLimit implements Serializable {
* @param totalConsumer 总数接收器
* @param <T> 数据类型
* @return 新流
* @deprecated {@link ShakeLimitProvider#shakeLimit(String, Flux, ShakeLimit)}
*/
public <T> Flux<T> transfer(Flux<T> source,
BiFunction<Duration, Flux<T>, Flux<Flux<T>>> 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);
}
}

View File

@ -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<ShakeLimitProvider> supports = Provider.create(ShakeLimitProvider.class);
/**
* @return 提供商唯一标识
*/
String provider();
/**
* 对指定分组数据源进行防抖,并输出满足条件的数据.
*
* @param sourceKey 数据源唯一标识
* @param grouped 分组数据源
* @param limit 防抖条件
* @param <T> 数据类型
* @return 防抖结果
*/
<T> Flux<ShakeLimitResult<T>> shakeLimit(
String sourceKey,
Flux<GroupedFlux<String, T>> grouped,
ShakeLimit limit);
}

View File

@ -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<T> {
private String groupKey;
private long times;
private T element;
}

View File

@ -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 <T> Flux<T> wrapSource(String sourceKey, Flux<T> source) {
return source;
}
@Override
public <T> Flux<ShakeLimitResult<T>> shakeLimit(String sourceKey,
Flux<GroupedFlux<String, T>> 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 <T> Mono<ShakeLimitResult<T>> handleWindow(String key,
String groupKey,
Duration duration,
Flux<T> 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();
}
}

View File

@ -44,6 +44,10 @@
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-system-dictionary</artifactId>
</dependency>
</dependencies>

View File

@ -21,5 +21,10 @@ public interface AlarmConstants {
String sourceType = "sourceType";
String sourceId = "sourceId";
String sourceName = "sourceName";
//告警条件
String alarmFilterTermSpecs = "_filterTermSpecs";
String alarmConfigSource = "alarmCenter";
}
}

View File

@ -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

View File

@ -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<AlarmResult> triggerAlarm(AlarmInfo alarmInfo);
/**
* 解除告警
* @see AlarmTaskExecutorProvider
*
* @return 告警解除结果
*/
Mono<RelieveResult> relieveAlarm(RelieveInfo relieveInfo);
}

View File

@ -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());

View File

@ -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<AlarmHandleHistoryEntity, String> handleHistoryRepository;
private final EventBus eventBus;
private final ConfigStorageManager storageManager;
private final ApplicationEventPublisher eventPublisher;
@Override
public Mono<AlarmResult> 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<Void> 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<RelieveResult> 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<Void> publishAlarmRecord(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) {
String topic = Topics.alarm(historyInfo.getTargetType(), historyInfo.getTargetId(), historyInfo.getAlarmConfigId());
return doPublishAlarmHistoryInfo(topic, historyInfo, alarmInfo);
}
public Mono<Void> doPublishAlarmHistoryInfo(String topic, AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) {
return Mono.just(topic)
.flatMap(assetTopic -> eventBus.publish(assetTopic, historyInfo))
.then();
}
private Mono<Void> publishEvent(AlarmHistoryInfo historyInfo) {
return Mono.fromRunnable(() -> eventPublisher.publishEvent(historyInfo));
}
private Mono<AlarmResult> saveAlarmCache(AlarmResult result,
AlarmRecordEntity record) {
return this
.updateRecordCache(record.getId(), cache -> cache.with(result))
.thenReturn(result);
}
public Mono<Void> publishAlarmRelieve(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) {
String topic = Topics.alarmRelieve(historyInfo.getTargetType(), historyInfo.getTargetId(), historyInfo.getAlarmConfigId());
return this.doPublishAlarmHistoryInfo(topic, historyInfo, alarmInfo);
}
private Mono<Void> 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<DefaultAlarmRuleHandler.RecordCache> getRecordCache(String recordId) {
return storageManager
.getStorage("alarm-records")
.flatMap(store -> store
.getConfig(recordId)
.map(val -> val.as(DefaultAlarmRuleHandler.RecordCache.class)));
}
private Mono<DefaultAlarmRuleHandler.RecordCache> updateRecordCache(String recordId, Function<DefaultAlarmRuleHandler.RecordCache, DefaultAlarmRuleHandler.RecordCache> 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;
}
}

View File

@ -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<Tuple2<String, Integer>, Set<String>> 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<Result> 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<Result> 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<AlarmInfo> 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<String> alarmId = getBoundAlarmId(context.getInstanceId(), branchIndex);
Set<String> 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<String, Object> 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<AlarmInfo> parseAlarm(ExecutionContext context, ConfigStorage alarm, Map<String, Object> 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<AlarmInfo> 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<Void> 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<AlarmInfo> 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<Void> 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<Void> publishAlarmRecord(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) {
String topic = Topics.alarm(historyInfo.getTargetType(), historyInfo.getTargetId(), historyInfo.getAlarmConfigId());
return eventBus
.publish(topic, historyInfo)
.then();
}
private Mono<AlarmInfo> saveAlarmCache(AlarmInfo result,
AlarmRecordEntity record) {
return this
.updateRecordCache(record.getId(), cache -> cache.with(result))
.thenReturn(result);
// return this
// .getAlarmStorage(result.getAlarmConfigId())
// .flatMap(store -> {
// Map<String, Object> 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<AlarmInfo> 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<AlarmConfigEntity> 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<Map<String, Object>> bindings;
public static TermSpec parseAlarmTrigger(ExecutionContext context, Map<String, Object> contextMap) {
TermSpec termSpec = new TermSpec();
Map<String, Object> 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<RecordCache> getRecordCache(String recordId) {
return storageManager
.getStorage("alarm-records")
.flatMap(store -> store
.getConfig(recordId)
.map(val -> val.as(RecordCache.class)));
}
private Mono<RecordCache> updateRecordCache(String recordId, Function<RecordCache, RecordCache> 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;

View File

@ -14,7 +14,6 @@ import org.springframework.context.annotation.Configuration;
@AutoConfiguration
public class RuleEngineManagerConfiguration {
@Bean
public SceneTaskExecutorProvider sceneTaskExecutorProvider(EventBus eventBus,
ObjectProvider<SceneFilter> filters,

View File

@ -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");
}

View File

@ -86,6 +86,13 @@ public class AlarmConfigEntity extends GenericEntity<String> 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<String> implements RecordCr
@Schema(description = "更新时间")
private Long modifyTime;
@Column(length = 64)
@Schema(description = "修改人名称")
private String modifierName;
public Map<String, Object> toConfigMap() {
Map<String, Object> configs = new HashMap<>();

View File

@ -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<String> implements RecordCreationEntity {
@ -30,11 +37,11 @@ public class AlarmHandleHistoryEntity extends GenericEntity<String> 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<String> handleType;
@Column(length = 256, nullable = false, updatable = false)
@Schema(description = "说明")
@ -63,12 +70,21 @@ public class AlarmHandleHistoryEntity extends GenericEntity<String> 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;

View File

@ -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<Map<String, Object>> 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;
}

View File

@ -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<String> {
@ -57,6 +63,19 @@ public class AlarmRecordEntity extends GenericEntity<String> {
@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<String> {
@DefaultValue("normal")
private AlarmRecordState state;
@Column(length = 32)
@Dictionary(AlarmHandleTypeDictInit.DICT_ID)
@Schema(description = "告警处理类型")
@ColumnType(javaType = String.class)
private EnumDict<String> handleType;
/**
* 标识告警触发的配置来自什么业务功能
*/
@Column
@Schema(description = "告警配置源")
private String alarmConfigSource;
@Column
@Schema(description = "说明")
private String description;

View File

@ -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<String> {
@Column(nullable = false, updatable = false, length = 64)
@NotBlank
@Schema(description = "告警配置ID")
@Schema(description = "告警ID")
private String alarmId;
@Column(nullable = false, updatable = false, length = 64)

View File

@ -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<String> implements RecordCreationEntity {
public class RuleInstanceEntity extends GenericEntity<String> implements RecordCreationEntity, RecordModifierEntity {
@Override
@GeneratedValue(generator = "snow_flake")
@ -76,6 +77,25 @@ public class RuleInstanceEntity extends GenericEntity<String> 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)

View File

@ -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<String> 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<String> 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<String> 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<String> implements RecordCreation
@Schema(description = "说明")
private String description;
public RuleInstance toRule() {
public Mono<RuleInstance> 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<String> 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");
}
}
}

View File

@ -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<String> {
public enum AlarmHandleType implements EnumDict<String> {
system("系统"),
user("人工");

View File

@ -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<String> {
public enum RuleInstanceState implements I18nEnumDict<String> {
started("正常"),
disable("禁用");
private final String text;

View File

@ -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<E extends SceneTriggerProvider.TriggerConfig>
implements SceneTriggerProvider<E> {
private String shakeLimitProvider = SimpleShakeLimitProvider.PROVIDER;
protected String getShakeLimitKey(Map<String, Object> data) {
return String.valueOf(data.get(SOURCE_ID_KEY));
}
@Override
public final Flux<ShakeLimitResult<Map<String, Object>>> shakeLimit(String key,
Flux<Map<String, Object>> source,
ShakeLimit limit) {
return ShakeLimitProvider
.supports
.get(shakeLimitProvider)
.orElse(SimpleShakeLimitProvider.GLOBAL)
.shakeLimit(key,
source.groupBy(this::getShakeLimitKey, Integer.MAX_VALUE),
limit);
}
}

View File

@ -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<TermColumn> 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)
.<List<PropertyMetadata>>map(event -> Collections
.singletonList(
of("data",
event.getName(),
event.getType())
))
.orElse(Collections.emptyList()),
(property, column) -> column.setChildren(createTermColumn("event", property, false))));
.<List<PropertyMetadata>>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)
.<List<PropertyMetadata>>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))
.<List<PropertyMetadata>>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<String, TermColumn> 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<TermColumn> 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);
}

View File

@ -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<String> parseActionTerms() {
return SceneProviders
.getActionProviderNow(executor)
.parseColumns(actionConfig());
}
public List<String> createContextColumns() {
List<String> 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<Variable> children) {
public static Variable createVariable(Integer branchIndex, Integer group, int actionIndex, List<Variable> 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<TermSpec> 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,

View File

@ -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<C> {
String getProvider();
C newConfig();
@ -14,5 +18,12 @@ public interface SceneActionProvider<C> {
Flux<Variable> createVariable(C config);
void applyRuleNode(C config,RuleNodeModel model);
void applyRuleNode(C config, RuleNodeModel model);
default void applyFilterSpec(RuleNodeModel node, List<TermSpec> specs) {
node.addConfiguration(
AlarmConstants.ConfigKey.alarmFilterTermSpecs,
SerializeUtils.convertToSafelySerializable(specs)
);
}
}

View File

@ -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<Term> when;
@ -36,7 +31,6 @@ public class SceneConditionAction implements Serializable {
@Schema(description = "分支ID")
private Integer branchId;
//仅用于设置到reactQl sql的column中
public List<Term> createContextTerm() {
List<Term> contextTerm = new ArrayList<>();

View File

@ -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<Map<String, Object>, Mono<Boolean>> 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<Term> 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<Term> getTermList() {
List<Term> 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<Map<String, Object>, Mono<Boolean>> createDefaultFilter(List<Term> 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<Object> args = Arrays.asList(request.getParameters());
String sqlString = request.toNativeSql();
return new Function<Map<String, Object>, Mono<Boolean>>() {
@Override
public Mono<Boolean> apply(Map<String, Object> 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<Map<String, Object>> createGrouping() {
//todo 其他分组方式实现
return flux -> flux
.groupBy(map -> map.getOrDefault("deviceId", "null"), Integer.MAX_VALUE);
}
private Flux<Variable> createSceneVariables(List<TermColumn> 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<Map<String, Object>, Mono<Boolean>> filter = createDefaultFilter(branch.getWhen());
Function<Map<String, Object>, Mono<Boolean>> filter = TraceHolder
.traceBlocking("/scene/create-branch-filter",
span -> {
Function<Map<String, Object>, Mono<Boolean>>
f = createDefaultFilter(branch.getWhen());
span.setAttribute("filter", f.toString());
span.setAttribute("branch", _branchIndex);
return f;
});
//满足条件后的输出操作
List<Function<Map<String, Object>, Mono<Void>>> outs = new ArrayList<>();
List<SceneActions> groups = branch.getThen();
int thenIndex = 0;
if (CollectionUtils.isNotEmpty(groups)) {
List<Function<Map<String, Object>, Mono<Void>>> 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<Map<String, Object>> sinks = Sinks
.many()
.unicast()
.onBackpressureBuffer(Queues.<Map<String, Object>>unboundedMultiproducer().get());
//分组方式,比如设备触发时,应该按设备分组,每个设备都走独立的防抖策略
ShakeLimitGrouping<Map<String, Object>> grouping = createGrouping();
Function<Map<String, Object>, Mono<Void>> 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<Map<String, Object>> sinks = Sinks
.many()
.unicast()
.onBackpressureBuffer(Queues.<Map<String, Object>>unboundedMultiproducer().get());
//动作输出
Flux<Function<Map<String, Object>, Mono<Void>>> _outs = Flux.fromIterable(new ArrayList<>(actionOuts));
Function<Map<String, Object>, Mono<Void>> 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<Variable> createDefaultVariable() {
return trigger != null
? trigger.createDefaultVariable()
: Collections.emptyList();
}
public RuleModel toModel() {
public SceneRule where(String expression) {
setTerms(TermExpressionParser.parse(expression));
return this;
}
public Mono<RuleModel> 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<Mono<?>> async = new ArrayList<>();
Mono<List<TermColumn>> columns = trigger
.parseTermColumns()
.collectList()
.cache();
Mono<Map<String, Variable>> 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<List<TermSpec>> branchTermSpec = null;
for (SceneConditionAction branch : branches) {
int branchId = branch.getBranchId() == null ? branchIndex : branch.getBranchId();
branchIndex++;
List<SceneActions> group = branch.getThen();
Mono<List<TermSpec>> 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<List<TermSpec>> 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<Map<String, Variable>> 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<Term> termList = preAction.getTerms();
//合并上一个节点输出的变量
groupTerm = Mono.zip(
groupTerm.<List<TermSpec>>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<String, Variable> 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<Term> getTermList() {
List<Term> 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;
}
}

View File

@ -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<Map<String, Object>> 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<Map<String, Object>> 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<Void> handleOutput(RuleData data) {
return data
.dataToMap()
@ -252,6 +250,14 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider {
}
protected SceneData buildSceneData(Map<String, Object> map) {
SceneData sceneData = new SceneData();
sceneData.setId(IDGenerator.RANDOM.generate());
sceneData.setRule(rule);
sceneData.setOutput(map);
return sceneData;
}
private Mono<Void> handleOutput(Map<String, Object> data) {
return handleOutput(context.newRuleData(data));
}
@ -273,13 +279,5 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider {
}
return handleOutput(ruleData);
}
protected SceneData buildSceneData(Map<String, Object> map) {
SceneData sceneData = new SceneData();
sceneData.setId(IDGenerator.RANDOM.generate());
sceneData.setRule(rule);
sceneData.setOutput(map);
return sceneData;
}
}
}

View File

@ -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> E 配置类型
* @author zhouhao
* @since 2.1
*/
public interface SceneTriggerProvider<E extends SceneTriggerProvider.TriggerConfig> {
/**
* @return 提供商唯一标识
*/
String getProvider();
/**
* @return 名称
*/
String getName();
/**
* 创建配置
*
* @return 配置
*/
E newConfig();
SqlRequest createSql(E config,List<Term> 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<Term> terms, boolean hasFilter);
SqlFragments createFilter(E config,List<Term> terms);
/**
* 创建过滤条件.不含where前缀.
*
* @param config 配置
* @param terms 条件
* @return 条件SQL语句
*/
SqlFragments createFilter(E config, List<Term> 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<List<TermSpec>> createFilterSpec(E config, List<Term> terms, BiConsumer<Term, TermSpec> customizer) {
return Mono.justOrEmpty(TermSpec.of(terms, customizer));
}
/**
* 创建默认变量信息
*
* @param config 配置
* @return 变量信息
*/
List<Variable> createDefaultVariable(E config);
/**
* 应用规则节点配置
*
* @param config 触发器配置
* @param model 规则模型
* @param sceneNode 场景触发节点模型
*/
void applyRuleNode(E config, RuleModel model, RuleNodeModel sceneNode);
/**
* 解析配置信息为支持的条件列,用于展示当前触发方式支持的触发条件.
*
* @param config 配置
* @return 条件列信息
*/
Flux<TermColumn> parseTermColumns(E config);
/**
* 配置信息
*/
default Flux<ShakeLimitResult<Map<String, Object>>> shakeLimit(String key,
Flux<Map<String, Object>> 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<String,Object> config){
FastBeanCopier.copy(config,this);
default void with(Map<String, Object> config) {
FastBeanCopier.copy(config, this);
}
}
}

View File

@ -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<Variable> parseVariable(List<Term> terms,
List<TermColumn> columns) {
//平铺条件
Map<String, Term> termCache = expandTerm(terms);
Map<String, List<Term>> termCache = expandTerm(terms);
//解析变量
List<Variable> variables = new ArrayList<>(termCache.size());
@ -60,19 +79,27 @@ public class SceneUtils {
return variables;
}
public static Map<String, Term> expandTerm(List<Term> terms) {
Map<String, Term> termCache = new LinkedHashMap<>();
public static Map<String, List<Term>> expandTerm(List<Term> terms) {
Map<String, List<Term>> termCache = new LinkedHashMap<>();
expandTerm(terms, termCache);
return termCache;
}
private static void expandTerm(List<Term> terms, Map<String, Term> container) {
private static void expandTerm(List<Term> terms, Map<String, List<Term>> container) {
if (terms == null) {
return;
}
for (Term term : terms) {
if (StringUtils.hasText(term.getColumn())) {
container.put(term.getColumn(), term);
List<Term> termList = container.get(term.getColumn());
if (termList == null){
List<Term> 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<Variable> columnToVariable(String prefixName,
TermColumn column,
Function<String, Term> termSupplier) {
Function<String, List<Term>> termSupplier) {
List<Variable> 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<Term> termList = termSupplier.apply(column.getColumn());
variables.add(Variable.of(column.getVariable("_"), variableName)
.with(column)
);
if (term != null) {
List<TermValue> 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<TermValue> 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<String> getSupportTriggers() {
return Flux
.fromIterable(SceneProviders.triggerProviders())
.map(SceneTriggerProvider::getProvider);
}
public static Flux<String> getSupportActions() {
return Flux
.fromIterable(SceneProviders.actionProviders())
.map(SceneActionProvider::getProvider);
}
public static Flux<TermColumn> parseTermColumns(SceneRule ruleMono) {
Trigger trigger = ruleMono.getTrigger();
if (trigger != null) {
return trigger.parseTermColumns();
}
return Flux.empty();
}
public static Flux<Variable> parseVariables(Mono<SceneRule> ruleMono, Integer branch, Integer branchGroup, Integer action) {
Mono<SceneRule> 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<SelectorInfo> 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<TermValue> values = TermValue.of(term);
if (values.isEmpty()) {
return term;
}
Function<TermValue, Object> 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");
}
}

View File

@ -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<String, Object> configuration;
public String getTypeName(){
return provider().getName();
}
/**
* 重构查询条件,替换为实际将要输出的变量.
*
* @param terms 条件
* @return 重构后的条件
* @see DeviceTrigger#refactorTermValue(String, Term)
* @see SceneTriggerProvider#refactorTerm(String, Term)
*/
public List<Term> refactorTerm(String tableName, List<Term> terms) {
if (CollectionUtils.isEmpty(terms)) {
@ -65,7 +72,7 @@ public class Trigger implements Serializable {
List<Term> 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<Term> 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<List<TermSpec>> createFilterSpec(List<Term> terms, BiConsumer<Term,TermSpec> customizer){
return provider().createFilterSpec(triggerConfig(), terms,customizer);
}
public Flux<TermColumn> 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<SceneTriggerProvider.TriggerConfig> provider() {
SceneTriggerProvider<SceneTriggerProvider.TriggerConfig> provider() {
return SceneProviders.getTriggerProviderNow(type);
}
@ -117,7 +131,9 @@ public class Trigger implements Serializable {
}
public List<Variable> 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 {

View File

@ -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<String> {
manual("手动触发"),
collector("采集器触发"),
timer("定时触发"),
device("设备触发");

View File

@ -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());
}

View File

@ -22,6 +22,12 @@ public class AlarmAction extends AlarmTaskExecutorProvider.Config {
List<Variable> 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", "告警配置名称"))

View File

@ -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<Term> terms, boolean hasWhere) {
Map<String, Term> termsMap = SceneUtils.expandTerm(terms);
Map<String, List<Term>> termsMap = SceneUtils.expandTerm(terms);
List<Term> termList = new ArrayList<>();
for (List<Term> 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<String> selectColumns = Sets.newLinkedHashSetWithExpectedSize(10 + termsMap.size());
Set<String> 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<DeviceTrigger> {
@ -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<TermValue> values = TermValue.of(term);
if (values.size() == 0) {
return term;
}
Function<TermValue, Object> 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;
}

View File

@ -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<DeviceTrigger> {
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "rule.scene.trigger.device")
public class DeviceTriggerProvider extends AbstractSceneTriggerProvider<DeviceTrigger> {
public static final String PROVIDER = "device";
@ -39,8 +41,8 @@ public class DeviceTriggerProvider implements SceneTriggerProvider<DeviceTrigger
}
@Override
public SqlRequest createSql(DeviceTrigger config, List<Term> terms, boolean hasWhere) {
return config.createSql(terms, hasWhere);
public SqlRequest createSql(DeviceTrigger config, List<Term> terms, boolean hasFilter) {
return config.createSql(terms, hasFilter);
}
@Override

View File

@ -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<ManualTrigger> {
@ConfigurationProperties(prefix = "rule.scene.trigger.manual")
public class ManualTriggerProvider extends AbstractSceneTriggerProvider<ManualTrigger> {
public static final String PROVIDER = "manual";
@ -40,7 +42,7 @@ public class ManualTriggerProvider implements SceneTriggerProvider<ManualTrigger
}
@Override
public SqlRequest createSql(ManualTrigger config, List<Term> terms, boolean hasWhere) {
public SqlRequest createSql(ManualTrigger config, List<Term> terms, boolean hasFilter) {
return EmptySqlRequest.INSTANCE;
}

View File

@ -32,7 +32,7 @@ public class TimerTriggerProvider implements SceneTriggerProvider<TimerTrigger>
@Override
public String getName() {
return "手动触发";
return "定时触发";
}
@Override
@ -41,7 +41,7 @@ public class TimerTriggerProvider implements SceneTriggerProvider<TimerTrigger>
}
@Override
public SqlRequest createSql(TimerTrigger config, List<Term> terms, boolean hasWhere) {
public SqlRequest createSql(TimerTrigger config, List<Term> terms, boolean hasFilter) {
return EmptySqlRequest.INSTANCE;
}

View File

@ -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<String> 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<String, TermColumn> columnGetter) {
public static List<TermColumn> refactorTermsInfo(String perText, List<TermColumn> terms) {
Map<String, TermColumn> 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<String, TermColumn> 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<String, TermColumn> columnGetter) {
public void doRefactorDescription(String perText, Function<String, TermColumn> 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<String, Object> 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);
}
}

View File

@ -43,7 +43,7 @@ public class AlarmConfigService extends GenericReactiveCrudService<AlarmConfigEn
*/
public Mono<Void> handleAlarm(AlarmHandleInfo info){
return alarmRecordService
.changeRecordState(info.getState(), info.getAlarmRecordId())
.changeRecordState(info.getType(), info.getState(), info.getAlarmRecordId())
.flatMap(total-> {
if (total > 0){
return handleHistoryRepository

View File

@ -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<DictionaryEntity> getDict() {
DictionaryEntity entity = new DictionaryEntity();
entity.setId(DICT_ID);
entity.setName("告警处理类型");
entity.setClassified(DictionaryConstants.CLASSIFIED_SYSTEM);
entity.setStatus((byte) 1);
List<DictionaryItemEntity> 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);
}
}

View File

@ -13,14 +13,18 @@ public class AlarmRecordService extends GenericReactiveCrudService<AlarmRecordEn
/**
* 修改告警记录状态
* @param type 告警处理类型
* @param state 修改后的告警记录状态
* @param id 告警记录ID
* @return
*/
public Mono<Integer> changeRecordState(AlarmRecordState state, String id) {
public Mono<Integer> 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();

View File

@ -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<String, Object> createData(AlarmHistoryInfo info) {
return FastBeanCopier.copy(info, new HashMap<>(16));
Map<String, Object> 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));
}

View File

@ -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<RuleInstance> 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<RuleInstance> 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)
)
;
}
}

View File

@ -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<SceneEntity, String> implements CommandLineRunner {
public class SceneService extends GenericReactiveCrudService<SceneEntity, String> {
private final RuleEngine ruleEngine;
@ -161,7 +158,10 @@ public class SceneService extends GenericReactiveCrudService<SceneEntity, String
return ruleEngine.shutdown(scene.getId());
} else if (scene.getState() == RuleInstanceState.started) {
scene.validate();
return ruleEngine.startRule(scene.getId(), scene.toRule().getModel());
return scene
.toRule()
.flatMap(instance -> ruleEngine.startRule(scene.getId(), instance.getModel()).then())
;
}
return Mono.empty();
})
@ -178,19 +178,4 @@ public class SceneService extends GenericReactiveCrudService<SceneEntity, String
);
}
@Override
public void run(String... args) {
createQuery()
.where()
.is(SceneEntity::getState, RuleInstanceState.started)
.fetch()
.flatMap(e -> Mono
.defer(() -> ruleEngine.startRule(e.getId(), e.toRule().getModel()).then())
.onErrorResume(err -> {
log.warn("启动场景[{}]失败", e.getName(), err);
return Mono.empty();
}))
.subscribe();
}
}

View File

@ -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<Al
.execute());
}
@PostMapping("/{alarmId}/{ruleId}/_delete")
@DeleteAction
@Operation(summary = "删除指定分支的告警规则绑定")
public Mono<Integer> deleteAlarmBindByBranchId(@PathVariable @Parameter(description = "告警配置ID") String alarmId,
@PathVariable @Parameter(description = "场景联动ID") String ruleId,
@RequestBody @Parameter(description = "分支ID") Mono<List<Integer>> 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<Integer> deleteAlarmBindById(@RequestBody @Parameter(description = "绑定信息") Mono<List<AlarmRuleBindEntity>> payload) {
return payload
.flatMapIterable(Function.identity())
.map(AlarmRuleBindEntity::getId)
.as(service::deleteById);
}
}

View File

@ -37,8 +37,6 @@ import java.util.function.Function;
@Resource(id = "rule-scene", name = "场景管理")
public class SceneController implements ReactiveServiceQueryController<SceneEntity, String> {
private final DeviceRegistry deviceRegistry;
@Getter
private final SceneService service;

View File

@ -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;
}
}

View File

@ -91,7 +91,6 @@
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-system-dictionary</artifactId>
<version>${hsweb.framework.version}</version>
</dependency>
<dependency>

View File

@ -32,6 +32,8 @@
<reactor.ql.version>1.0.16</reactor.ql.version>
<!-- https://github.com/jetlinks/jetlinks-plugin -->
<jetlinks.plugin.version>1.0.1</jetlinks.plugin.version>
<!-- https://github.com/jetlinks/jetlinks-sdk -->
<jetlinks.sdk.version>1.0.0</jetlinks.sdk.version>
<!-- 第三方依赖版本 -->
<r2dbc.version>Borca-SR2</r2dbc.version>
@ -479,6 +481,11 @@
<version>${jetlinks.version}</version>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-system-dictionary</artifactId>
<version>${hsweb.framework.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>