feat(rule-engine): 告警场景2.2相关功能更新 (#552)
This commit is contained in:
parent
f946c97ce5
commit
f754740646
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package org.jetlinks.community.dictionary;
|
||||
|
||||
public interface DictionaryConstants {
|
||||
|
||||
/**
|
||||
* 系统分类标识
|
||||
*/
|
||||
String CLASSIFIED_SYSTEM = "system";
|
||||
}
|
||||
|
|
@ -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();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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/*";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,10 @@
|
|||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.hswebframework.web</groupId>
|
||||
<artifactId>hsweb-system-dictionary</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
|
|
|||
|
|
@ -21,5 +21,10 @@ public interface AlarmConstants {
|
|||
String sourceType = "sourceType";
|
||||
String sourceId = "sourceId";
|
||||
String sourceName = "sourceName";
|
||||
|
||||
//告警条件
|
||||
String alarmFilterTermSpecs = "_filterTermSpecs";
|
||||
|
||||
String alarmConfigSource = "alarmCenter";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import org.springframework.context.annotation.Configuration;
|
|||
@AutoConfiguration
|
||||
public class RuleEngineManagerConfiguration {
|
||||
|
||||
|
||||
@Bean
|
||||
public SceneTaskExecutorProvider sceneTaskExecutorProvider(EventBus eventBus,
|
||||
ObjectProvider<SceneFilter> filters,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("人工");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<>();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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("设备触发");
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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", "告警配置名称"))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -91,7 +91,6 @@
|
|||
<dependency>
|
||||
<groupId>org.hswebframework.web</groupId>
|
||||
<artifactId>hsweb-system-dictionary</artifactId>
|
||||
<version>${hsweb.framework.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
|
|
|||
7
pom.xml
7
pom.xml
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue