Bläddra i källkod

增加规则引擎

zhouhao 5 år sedan
förälder
incheckning
e76c39ecd9
31 ändrade filer med 1837 tillägg och 106 borttagningar
  1. 2 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/node/MqttClientNode.java
  2. 2 0
      jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpClientNode.java
  3. 0 67
      jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpServerNode.java
  4. 0 39
      jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpServerNodeConfig.java
  5. 1 0
      jetlinks-components/pom.xml
  6. 66 0
      jetlinks-components/rule-engine-component/pom.xml
  7. 124 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineConfiguration.java
  8. 29 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/entity/ExecuteLogInfo.java
  9. 23 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/entity/RuleEngineExecuteEventInfo.java
  10. 21 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/event/handler/RuleEngineLoggerIndexProvider.java
  11. 44 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/event/handler/RuleLogHandler.java
  12. 155 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/DataMappingWorkerNode.java
  13. 94 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/ScriptWorkerNode.java
  14. 102 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/SqlExecutorWorkerNode.java
  15. 129 0
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/TimerWorkerNode.java
  16. 3 0
      jetlinks-components/timeseries-component/src/main/java/org/jetlinks/community/timeseries/query/AggregationQueryParam.java
  17. 1 0
      jetlinks-manager/pom.xml
  18. 27 0
      jetlinks-manager/rule-engine-manager/.gitignore
  19. 52 0
      jetlinks-manager/rule-engine-manager/pom.xml
  20. 73 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java
  21. 76 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleModelEntity.java
  22. 22 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/RuleInstanceState.java
  23. 29 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/DebugMessage.java
  24. 18 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/ExecuteRuleRequest.java
  25. 361 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleEngineDebugService.java
  26. 83 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleInstanceService.java
  27. 78 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleModelService.java
  28. 80 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleEngineDebugController.java
  29. 92 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleInstanceController.java
  30. 44 0
      jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleModelController.java
  31. 6 0
      jetlinks-standalone/pom.xml

+ 2 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/node/MqttClientNode.java

@@ -14,12 +14,14 @@ import org.jetlinks.rule.engine.api.executor.ExecutionContext;
 import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy;
 import org.jetlinks.rule.engine.executor.node.mqtt.*;
 import org.reactivestreams.Publisher;
+import org.springframework.stereotype.Component;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import java.util.function.Function;
 
 @AllArgsConstructor
+@Component
 public class MqttClientNode extends CommonExecutableRuleNodeFactoryStrategy<MqttClientConfiguration> {
 
     private NetworkManager networkManager;

+ 2 - 0
jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpClientNode.java

@@ -11,12 +11,14 @@ import org.jetlinks.rule.engine.api.RuleDataCodecs;
 import org.jetlinks.rule.engine.api.executor.ExecutionContext;
 import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy;
 import org.reactivestreams.Publisher;
+import org.springframework.stereotype.Component;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
 import java.util.function.Function;
 
 @AllArgsConstructor
+@Component
 public class TcpClientNode extends CommonExecutableRuleNodeFactoryStrategy<TcpClientNodeConfig> {
 
     private NetworkManager clientManager;

+ 0 - 67
jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpServerNode.java

@@ -1,67 +0,0 @@
-package org.jetlinks.community.network.tcp.node;//package org.jetlinks.community.network.tcp.node;
-//
-//import lombok.AllArgsConstructor;
-//import org.jetlinks.community.network.PubSubType;
-//import org.jetlinks.community.network.tcp.TcpMessage;
-//import org.jetlinks.community.network.tcp.server.TcpServer;
-//import org.jetlinks.community.network.tcp.server.TcpServerManager;
-//import org.jetlinks.rule.engine.api.RuleData;
-//import org.jetlinks.rule.engine.api.RuleDataCodecs;
-//import org.jetlinks.rule.engine.api.executor.ExecutionContext;
-//import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy;
-//import org.reactivestreams.Publisher;
-//import reactor.core.publisher.Flux;
-//import reactor.core.publisher.Mono;
-//
-//import java.util.function.Function;
-//
-//@AllArgsConstructor
-//public class TcpServerNode extends CommonExecutableRuleNodeFactoryStrategy<TcpServerNodeConfig> {
-//
-//    private TcpServerManager clientManager;
-//
-//    static {
-//        TcpMessageCodec.register();
-//    }
-//
-//    @Override
-//    public Function<RuleData, ? extends Publisher<?>> createExecutor(ExecutionContext context, TcpServerNodeConfig config) {
-//
-//        if (config.getType() != PubSubType.producer) {
-//            return Mono::just;
-//        }
-//        return data -> clientManager.getServer(config.getServerId())
-//                .flatMapMany(client -> RuleDataCodecs
-//                        .getCodec(TcpMessage.class)
-//                        .map(codec -> codec.decode(data, config.getSendPayloadType())
-//                                .cast(TcpMessage.class)
-//                                .switchIfEmpty(Mono.fromRunnable(() -> context.logger().warn("can not decode rule data to tcp server message:{}", data))))
-//                        .orElseGet(() -> Flux.just(new TcpMessage(config.getSendPayloadType().write(data.getData()))))
-//                        .flatMap(msg -> client.publish(msg))
-//                        .then(Mono.just(data)));
-//    }
-//
-//    @Override
-//    protected void onStarted(ExecutionContext context, TcpServerNodeConfig config) {
-//        super.onStarted(context, config);
-//        if (config.getType() == PubSubType.consumer) {
-//            context.onStop(clientManager
-//                    .getServer(config.getServerId())
-//                    .switchIfEmpty(Mono.fromRunnable(() -> context.logger().error("tcp server {} not found", config.getServerId())))
-//                    .flatMapMany(TcpServer::subscribe)
-//                    .doOnNext(msg -> context.logger().info("received tcp server message:{}", config.getSubPayloadType().read(msg.getPayload())))
-//                    .map(r -> RuleDataCodecs.getCodec(TcpMessage.class)
-//                            .map(codec -> codec.encode(r, config.getSubPayloadType()))
-//                            .orElse(r.getPayload()))
-//                    .onErrorContinue((err, obj) -> {
-//                        context.logger().error("consume tcp server message error", err);
-//                    })
-//                    .subscribe(msg -> context.getOutput().write(Mono.just(RuleData.create(msg))).subscribe())::dispose);
-//        }
-//    }
-//
-//    @Override
-//    public String getSupportType() {
-//        return "tcp-server";
-//    }
-//}

+ 0 - 39
jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/node/TcpServerNodeConfig.java

@@ -1,39 +0,0 @@
-package org.jetlinks.community.network.tcp.node;
-
-import lombok.Getter;
-import lombok.Setter;
-import org.jetlinks.community.network.PubSubType;
-import org.jetlinks.rule.engine.api.model.NodeType;
-import org.jetlinks.rule.engine.executor.PayloadType;
-import org.jetlinks.rule.engine.executor.node.RuleNodeConfig;
-import org.springframework.util.Assert;
-
-@Getter
-@Setter
-public class TcpServerNodeConfig implements RuleNodeConfig {
-
-    private String serverId;
-
-    private PubSubType type;
-
-    private PayloadType sendPayloadType;
-
-    private PayloadType subPayloadType;
-
-    @Override
-    public NodeType getNodeType() {
-        return NodeType.MAP;
-    }
-
-    @Override
-    public void setNodeType(NodeType nodeType) {
-
-    }
-
-    @Override
-    public void validate() {
-        Assert.hasText(serverId, "serverId can not be empty!");
-        Assert.notNull(type, "type can not be null!");
-
-    }
-}

+ 1 - 0
jetlinks-components/pom.xml

@@ -21,6 +21,7 @@
         <module>common-component</module>
         <module>notify-component</module>
         <module>logging-component</module>
+        <module>rule-engine-component</module>
     </modules>
 
     <artifactId>jetlinks-components</artifactId>

+ 66 - 0
jetlinks-components/rule-engine-component/pom.xml

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>jetlinks-components</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>rule-engine-component</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>rule-engine-support</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>common-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>elasticsearch-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>rule-engine-cluster</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>gateway-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-expands-script</artifactId>
+            <version>${hsweb.expands.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-easy-orm-rdb</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>tcp-component</artifactId>
+            <version>${project.version}</version>
+            <scope>compile</scope>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 124 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineConfiguration.java

@@ -0,0 +1,124 @@
+package org.jetlinks.community.rule.engine.configuration;
+
+import org.jetlinks.community.rule.engine.nodes.TimerWorkerNode;
+import org.jetlinks.rule.engine.api.ConditionEvaluator;
+import org.jetlinks.rule.engine.api.RuleEngine;
+import org.jetlinks.rule.engine.api.Slf4jLogger;
+import org.jetlinks.rule.engine.api.executor.ExecutableRuleNodeFactory;
+import org.jetlinks.rule.engine.cluster.logger.ClusterLogger;
+import org.jetlinks.rule.engine.condition.ConditionEvaluatorStrategy;
+import org.jetlinks.rule.engine.condition.DefaultConditionEvaluator;
+import org.jetlinks.rule.engine.condition.supports.DefaultScriptEvaluator;
+import org.jetlinks.rule.engine.condition.supports.ScriptConditionEvaluatorStrategy;
+import org.jetlinks.rule.engine.condition.supports.ScriptEvaluator;
+import org.jetlinks.rule.engine.executor.DefaultExecutableRuleNodeFactory;
+import org.jetlinks.rule.engine.executor.ExecutableRuleNodeFactoryStrategy;
+import org.jetlinks.rule.engine.executor.node.route.RouteEventNode;
+import org.jetlinks.rule.engine.model.DefaultRuleModelParser;
+import org.jetlinks.rule.engine.model.RuleModelParserStrategy;
+import org.jetlinks.rule.engine.model.antv.AntVG6RuleModelParserStrategy;
+import org.jetlinks.rule.engine.standalone.StandaloneRuleEngine;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.concurrent.ExecutorService;
+
+@Configuration
+public class RuleEngineConfiguration {
+
+    @Bean
+    public DefaultRuleModelParser defaultRuleModelParser() {
+        return new DefaultRuleModelParser();
+    }
+
+    @Bean
+    public DefaultConditionEvaluator defaultConditionEvaluator() {
+        return new DefaultConditionEvaluator();
+    }
+
+    @Bean
+    public DefaultExecutableRuleNodeFactory defaultExecutableRuleNodeFactory() {
+        return new DefaultExecutableRuleNodeFactory();
+    }
+
+    @Bean
+    public AntVG6RuleModelParserStrategy antVG6RuleModelParserStrategy() {
+        return new AntVG6RuleModelParserStrategy();
+    }
+
+    @Bean
+    public BeanPostProcessor autoRegisterStrategy(DefaultRuleModelParser defaultRuleModelParser,
+                                                  DefaultConditionEvaluator defaultConditionEvaluator,
+                                                  DefaultExecutableRuleNodeFactory ruleNodeFactory) {
+        return new BeanPostProcessor() {
+            @Override
+            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+
+                return bean;
+            }
+
+            @Override
+            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+                if (bean instanceof RuleModelParserStrategy) {
+                    defaultRuleModelParser.register(((RuleModelParserStrategy) bean));
+                }
+                if (bean instanceof ConditionEvaluatorStrategy) {
+                    defaultConditionEvaluator.register(((ConditionEvaluatorStrategy) bean));
+                }
+                if (bean instanceof ExecutableRuleNodeFactoryStrategy) {
+                    ruleNodeFactory.registerStrategy(((ExecutableRuleNodeFactoryStrategy) bean));
+                }
+                return bean;
+            }
+        };
+    }
+
+    @Bean
+    public ScriptEvaluator ruleEngineScriptEvaluator() {
+        return new DefaultScriptEvaluator();
+    }
+
+    @Bean
+    public ScriptConditionEvaluatorStrategy scriptConditionEvaluatorStrategy(ScriptEvaluator scriptEvaluator) {
+        return new ScriptConditionEvaluatorStrategy(scriptEvaluator);
+    }
+
+    @Bean
+    public RuleEngine ruleEngine(ExecutableRuleNodeFactory ruleNodeFactory,
+                                 ConditionEvaluator conditionEvaluator,
+                                 ApplicationEventPublisher eventPublisher,
+                                 ExecutorService executorService) {
+        StandaloneRuleEngine ruleEngine = new StandaloneRuleEngine();
+        ruleEngine.setNodeFactory(ruleNodeFactory);
+        ruleEngine.setExecutor(executorService);
+        ruleEngine.setEvaluator(conditionEvaluator);
+        ruleEngine.setEventListener(eventPublisher::publishEvent);
+        ruleEngine.setLoggerSupplier((ctxId, model) -> {
+            ClusterLogger logger = new ClusterLogger();
+            logger.setParent(new Slf4jLogger("rule.engine.logger.".concat(model.getId()).concat(".").concat(model.getName())));
+            logger.setLogInfoConsumer(eventPublisher::publishEvent);
+            logger.setNodeId(model.getId());
+            logger.setInstanceId(ctxId);
+            return logger;
+        });
+        return ruleEngine;
+    }
+
+    /* 规则引擎节点 */
+
+    @Bean //定时调度
+    public TimerWorkerNode timerWorkerNode() {
+        return new TimerWorkerNode();
+    }
+
+    @Bean
+    public RouteEventNode routeEventNode() {
+        return new RouteEventNode();
+    }
+
+
+
+}

+ 29 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/entity/ExecuteLogInfo.java

@@ -0,0 +1,29 @@
+package org.jetlinks.community.rule.engine.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+public class ExecuteLogInfo {
+
+    private String instanceId;
+
+    private String nodeId;
+
+    private String level;
+
+    private String message;
+
+    private long createTime = System.currentTimeMillis();
+
+    private long timestamp;
+
+    //private List<Object> args;
+
+    private String context;
+}

+ 23 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/entity/RuleEngineExecuteEventInfo.java

@@ -0,0 +1,23 @@
+package org.jetlinks.community.rule.engine.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+public class RuleEngineExecuteEventInfo {
+
+    private String event;
+
+    private long createTime = System.currentTimeMillis();
+
+    private String instanceId;
+
+    private String nodeId;
+
+    private String ruleData;
+}

+ 21 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/event/handler/RuleEngineLoggerIndexProvider.java

@@ -0,0 +1,21 @@
+package org.jetlinks.community.rule.engine.event.handler;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.jetlinks.community.elastic.search.index.ElasticIndex;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@AllArgsConstructor
+public enum RuleEngineLoggerIndexProvider implements ElasticIndex {
+
+    RULE_LOG("rule-log", "_doc"),
+    RULE_EVENT_LOG("rule-event-log", "_doc");
+
+    private String index;
+
+    private String type;
+}

+ 44 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/event/handler/RuleLogHandler.java

@@ -0,0 +1,44 @@
+package org.jetlinks.community.rule.engine.event.handler;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.community.elastic.search.service.ElasticSearchService;
+import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteEventInfo;
+import org.jetlinks.community.rule.engine.entity.ExecuteLogInfo;
+import org.jetlinks.rule.engine.api.events.NodeExecuteEvent;
+import org.jetlinks.rule.engine.api.events.RuleEvent;
+import org.jetlinks.rule.engine.cluster.logger.LogInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+@Component
+@Slf4j
+@Order(3)
+public class RuleLogHandler {
+
+    @Autowired
+    private ElasticSearchService elasticSearchService;
+
+
+    @EventListener
+    public void handleRuleLog(LogInfo event) {
+        ExecuteLogInfo logInfo = FastBeanCopier.copy(event, new ExecuteLogInfo());
+        elasticSearchService.commit(RuleEngineLoggerIndexProvider.RULE_LOG, Mono.just(logInfo))
+            .subscribe();
+    }
+
+    @EventListener
+    public void handleRuleExecuteEvent(NodeExecuteEvent event) {
+        //不记录BEFORE和RESULT事件
+        if (!RuleEvent.NODE_EXECUTE_BEFORE.equals(event.getEvent())
+            && !RuleEvent.NODE_EXECUTE_RESULT.equals(event.getEvent())) {
+            RuleEngineExecuteEventInfo eventInfo = FastBeanCopier.copy(event, new RuleEngineExecuteEventInfo());
+            elasticSearchService.commit(RuleEngineLoggerIndexProvider.RULE_LOG, Mono.just(eventInfo))
+                .subscribe();
+        }
+    }
+
+}

+ 155 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/DataMappingWorkerNode.java

@@ -0,0 +1,155 @@
+package org.jetlinks.community.rule.engine.nodes;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.hswebframework.web.bean.Converter;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.hswebframework.web.utils.ExpressionUtils;
+import org.jetlinks.rule.engine.api.RuleData;
+import org.jetlinks.rule.engine.api.executor.ExecutionContext;
+import org.jetlinks.rule.engine.api.model.NodeType;
+import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy;
+import org.jetlinks.rule.engine.executor.node.RuleNodeConfig;
+import org.reactivestreams.Publisher;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @author zhouhao
+ * @since 1.0.0
+ */
+@Component
+public class DataMappingWorkerNode extends CommonExecutableRuleNodeFactoryStrategy<DataMappingWorkerNode.Config> {
+
+    public static Converter converter = FastBeanCopier.DEFAULT_CONVERT;
+
+    @Override
+    public String getSupportType() {
+        return "data-mapping";
+    }
+
+    @Override
+    public Function<RuleData, Publisher<Object>> createExecutor(ExecutionContext context, Config config) {
+
+        return ruleData -> Mono.just(config.mapping(convertObject(ruleData.getData())));
+    }
+
+    @Getter
+    @Setter
+    public static class Config implements RuleNodeConfig {
+
+        private List<Mapping> mappings = new ArrayList<>();
+
+        private boolean keepSourceData = false;
+
+        private NodeType nodeType;
+
+        private Map<String, Object> toMap(Object source) {
+            return FastBeanCopier.copy(source, HashMap::new);
+        }
+
+        @SuppressWarnings("all")
+        private Object mapping(Object data) {
+            if (data instanceof Map) {
+                return doMapping(((Map) data));
+            }
+            if (data instanceof Collection) {
+                Collection<Object> source = ((Collection) data);
+                return source
+                        .stream()
+                        .map(this::toMap)
+                        .map(this::doMapping)
+                        .collect(Collectors.toList());
+            }
+            return data;
+        }
+
+        private Object doMapping(Map<String, Object> object) {
+            Map<String, Object> newData = new HashMap<>();
+            if (keepSourceData) {
+                newData.putAll(object);
+            }
+            for (Mapping mapping : mappings) {
+                Object data = mapping.getData(object);
+                if (data != null) {
+                    newData.put(mapping.target, data);
+                }
+            }
+            return newData;
+        }
+    }
+
+    @Getter
+    @Setter
+    public static class Mapping {
+        private String target;
+
+        private String source;
+
+        private String type;
+
+        private transient Class typeClass;
+
+        public Mapping() {
+
+        }
+
+        public Mapping(String target, String source) {
+            this.target = target;
+            this.source = source;
+        }
+
+        public Mapping(String target, String source, String type) {
+            this.target = target;
+            this.source = source;
+            this.type = type;
+        }
+
+        @SneakyThrows
+        public Class<?> getTypeClass() {
+            if (typeClass == null && type != null) {
+                String lowerType = type.toLowerCase();
+                switch (lowerType) {
+                    case "int":
+                    case "integer":
+                        return typeClass = Integer.class;
+                    case "string":
+                        return typeClass = String.class;
+                    case "double":
+                        return typeClass = Double.class;
+                    case "decimal":
+                        return typeClass = BigDecimal.class;
+                    case "boolean":
+                        return typeClass = Boolean.class;
+                    case "date":
+                        return typeClass = Date.class;
+                    default:
+                        return typeClass = Class.forName(type);
+                }
+            }
+            if (typeClass == Void.class) {
+                return null;
+            }
+            return typeClass;
+        }
+
+        @SneakyThrows
+        public Object getData(Map<String, Object> sourceData) {
+            Object data = sourceData.get(this.source);
+            if (data == null && this.source.contains("${")) {
+                data = ExpressionUtils.analytical(this.source, sourceData, "spel");
+            }
+            if (data == null) {
+                return null;
+            }
+            return getTypeClass() != null ? converter.convert(data, getTypeClass(), null) : data;
+        }
+
+    }
+}

+ 94 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/ScriptWorkerNode.java

@@ -0,0 +1,94 @@
+package org.jetlinks.community.rule.engine.nodes;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.hswebframework.expands.script.engine.DynamicScriptEngine;
+import org.hswebframework.expands.script.engine.DynamicScriptEngineFactory;
+import org.jetlinks.rule.engine.api.RuleData;
+import org.jetlinks.rule.engine.api.executor.ExecutionContext;
+import org.jetlinks.rule.engine.api.model.NodeType;
+import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy;
+import org.jetlinks.rule.engine.executor.node.RuleNodeConfig;
+import org.reactivestreams.Publisher;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@Component
+@Slf4j
+public class ScriptWorkerNode extends CommonExecutableRuleNodeFactoryStrategy<ScriptWorkerNode.Config> {
+
+    @Override
+    public String getSupportType() {
+        return "script";
+    }
+
+    @Override
+    @SneakyThrows
+    public Function<RuleData, Publisher<?>> createExecutor(ExecutionContext context, Config config) {
+
+        DynamicScriptEngine engine = DynamicScriptEngineFactory.getEngine(config.getLang());
+        if (engine == null) {
+            throw new UnsupportedOperationException("不支持的脚本语言:" + config.getLang());
+        }
+        if (StringUtils.isEmpty(config.getScript())) {
+            log.warn("script is empty");
+            return Mono::just;
+        }
+        String id = DigestUtils.md5Hex(config.getScript());
+        if (!engine.compiled(id)) {
+            engine.compile(id, config.getScript());
+        }
+
+        Handler handler = new Handler();
+        Map<String, Object> scriptContext = new HashMap<>();
+        scriptContext.put("context", context);
+        scriptContext.put("handler", handler);
+        engine.execute(id, scriptContext).getIfSuccess();
+
+        return ruleData -> Flux.defer(()->{
+            if (handler.onMessage != null) {
+                Object result = handler.onMessage.apply(ruleData);
+                if (result == null || result.getClass().getName().equals("jdk.nashorn.internal.runtime.Undefined")) {
+                    return Flux.empty();
+                }
+                if(result instanceof Publisher){
+                    return Flux.from(((Publisher) result));
+                }
+                if(result instanceof Map){
+                    result = new HashMap<>((Map<?, ?>) result);
+                }
+                return Flux.just(result);
+            }
+            return Flux.empty();
+        });
+    }
+
+    public static class Handler {
+        private Function<RuleData, Object> onMessage;
+
+        public void onMessage(Function<RuleData, Object> onMessage) {
+            this.onMessage = onMessage;
+        }
+    }
+
+    @Getter
+    @Setter
+    public static class Config implements RuleNodeConfig {
+
+        private String lang = "js";
+
+        private String script;
+
+        private NodeType nodeType;
+
+    }
+}

+ 102 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/SqlExecutorWorkerNode.java

@@ -0,0 +1,102 @@
+package org.jetlinks.community.rule.engine.nodes;
+
+import lombok.Getter;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.hswebframework.ezorm.rdb.executor.SqlRequests;
+import org.hswebframework.ezorm.rdb.executor.reactive.ReactiveSqlExecutor;
+import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers;
+import org.hswebframework.web.utils.ExpressionUtils;
+import org.jetlinks.rule.engine.api.RuleData;
+import org.jetlinks.rule.engine.api.executor.ExecutionContext;
+import org.jetlinks.rule.engine.api.model.NodeType;
+import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy;
+import org.jetlinks.rule.engine.executor.node.RuleNodeConfig;
+import org.reactivestreams.Publisher;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+@Component
+public class SqlExecutorWorkerNode extends CommonExecutableRuleNodeFactoryStrategy<SqlExecutorWorkerNode.Config> {
+
+    @Autowired
+    private ReactiveSqlExecutor sqlExecutor;
+
+    @Override
+    public String getSupportType() {
+        return "sql";
+    }
+
+    @Override
+    public Function<RuleData, Publisher<Object>> createExecutor(ExecutionContext context, Config config) {
+
+        if (config.isQuery()) {
+            return (data) -> Flux.defer(() -> {
+                String sql = config.getSql(data);
+                List<Flux<Map<String, Object>>> fluxes = new ArrayList<>();
+                data.acceptMap(map -> fluxes.add(sqlExecutor.select(Mono.just(SqlRequests.template(sql, map)), ResultWrappers.map())));
+                return Flux.concat(fluxes) ;
+            });
+        } else {
+            return data -> Mono.defer(() -> {
+                String sql = config.getSql(data);
+                List<Mono<Integer>> fluxes = new ArrayList<>();
+                data.acceptMap(map -> fluxes.add(sqlExecutor.update(Mono.just(SqlRequests.template(sql, map)))));
+
+                return Flux.concat(fluxes).reduce(Math::addExact);
+            });
+        }
+
+    }
+
+
+    @Getter
+    @Setter
+    public static class Config implements RuleNodeConfig {
+
+        private String dataSourceId;
+
+        private NodeType nodeType = NodeType.MAP;
+
+        private String sql;
+
+        private boolean stream;
+
+        private boolean transaction;
+
+        public boolean isQuery() {
+
+            return sql.trim().startsWith("SELECT") ||
+                    sql.trim().startsWith("select");
+        }
+
+        @SneakyThrows
+        public String getSql(RuleData data) {
+            if (!sql.contains("${")) {
+                return sql;
+            }
+            Map<String, Object> map = new HashMap<>();
+            map.put("data", data.getData());
+            map.put("ruleData", data);
+            map.put("attr", data.getAttributes());
+            return ExpressionUtils.analytical(sql, map, "spel");
+        }
+
+        public void switchDataSource() {
+
+        }
+
+        public void resetDataSource() {
+
+        }
+    }
+
+}

+ 129 - 0
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/nodes/TimerWorkerNode.java

@@ -0,0 +1,129 @@
+package org.jetlinks.community.rule.engine.nodes;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.rule.engine.api.RuleData;
+import org.jetlinks.rule.engine.api.executor.ExecutionContext;
+import org.jetlinks.rule.engine.api.model.NodeType;
+import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy;
+import org.jetlinks.rule.engine.executor.node.RuleNodeConfig;
+import org.reactivestreams.Publisher;
+import org.springframework.scheduling.support.CronSequenceGenerator;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+public class TimerWorkerNode extends CommonExecutableRuleNodeFactoryStrategy<TimerWorkerNode.Configuration> {
+
+    private Map<String, TimerWorkerNode.TimerJob> jobs = new ConcurrentHashMap<>();
+
+    @Override
+    public Function<RuleData, ? extends Publisher<?>> createExecutor(ExecutionContext context, Configuration config) {
+        return Mono::just;
+    }
+
+    @Override
+    protected void onStarted(ExecutionContext context, Configuration config) {
+        super.onStarted(context, config);
+        String id = context.getInstanceId() + ":" + context.getNodeId();
+
+        context.onStop(() -> {
+            TimerJob job = jobs.remove(id);
+            if (null != job) {
+                job.cancel();
+            }
+        });
+        TimerJob job = jobs.computeIfAbsent(id, _id -> new TimerJob(config, context));
+
+        job.doStart();
+    }
+
+    @Override
+    public String getSupportType() {
+        return "timer";
+    }
+
+    @AllArgsConstructor
+    private static class TimerJob {
+        private String id;
+        private TimerWorkerNode.Configuration configuration;
+        private ExecutionContext context;
+        private volatile boolean running;
+
+        TimerJob(TimerWorkerNode.Configuration configuration,
+                 ExecutionContext context) {
+            this.configuration = configuration;
+            this.context = context;
+            this.id = context.getInstanceId() + ":" + context.getNodeId();
+        }
+
+
+        void start() {
+            running = true;
+            doStart();
+        }
+
+        void doStart() {
+            if (!running) {
+                return;
+            }
+            running = true;
+            Mono.delay(Duration.ofMillis(configuration.nextMillis()))
+                .subscribe(t -> execute(this::start));
+        }
+
+        void execute(Runnable runnable) {
+            if (!running) {
+                return;
+            }
+            context.logger().debug("execute timer:{}", id);
+            context.getOutput()
+                .write(Mono.just(RuleData.create(System.currentTimeMillis())))
+                .doOnError(err -> context.logger().error("fire timer error", err))
+                .doFinally(s -> runnable.run())
+                .subscribe();
+        }
+
+        void cancel() {
+            running = false;
+        }
+    }
+
+
+    public static class Configuration implements RuleNodeConfig {
+        @Getter
+        @Setter
+        private String cron;
+
+        private volatile CronSequenceGenerator generator;
+
+        @Override
+        public NodeType getNodeType() {
+            return NodeType.PEEK;
+        }
+
+        @Override
+        public void setNodeType(NodeType nodeType) {
+
+        }
+
+        public void init() {
+            generator = new CronSequenceGenerator(cron);
+        }
+
+        @Override
+        public void validate() {
+            init();
+        }
+
+        public long nextMillis() {
+            return Math.max(100, generator.next(new Date()).getTime() - System.currentTimeMillis());
+        }
+
+    }
+}

+ 3 - 0
jetlinks-components/timeseries-component/src/main/java/org/jetlinks/community/timeseries/query/AggregationQueryParam.java

@@ -6,9 +6,12 @@ import org.hswebframework.ezorm.core.dsl.Query;
 import org.hswebframework.ezorm.core.param.QueryParam;
 
 import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.Consumer;
 import java.util.function.Function;
 

+ 1 - 0
jetlinks-manager/pom.xml

@@ -17,6 +17,7 @@
         <module>network-manager</module>
         <module>notify-manager</module>
         <module>logging-manager</module>
+        <module>rule-engine-manager</module>
     </modules>
 
 </project>

+ 27 - 0
jetlinks-manager/rule-engine-manager/.gitignore

@@ -0,0 +1,27 @@
+**/pom.xml.versionsBackup
+**/target/
+**/out/
+*.class
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+.idea/
+/nbproject
+*.ipr
+*.iws
+*.iml
+
+# Package Files #
+*.jar
+*.war
+*.ear
+*.log
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+**/transaction-logs/
+!/.mvn/wrapper/maven-wrapper.jar
+/data/
+*.db
+/static/
+/upload
+/ui/upload/
+docker/data

+ 52 - 0
jetlinks-manager/rule-engine-manager/pom.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.jetlinks.community</groupId>
+        <artifactId>jetlinks-manager</artifactId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <artifactId>rule-engine-manager</artifactId>
+
+    <properties>
+        <hsweb.framework.version>4.0.0-SNAPSHOT</hsweb.framework.version>
+    </properties>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-authorization-api</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-starter</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-easy-orm-rdb</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>rule-engine-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>elasticsearch-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+
+    </dependencies>
+
+</project>

+ 73 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java

@@ -0,0 +1,73 @@
+package org.jetlinks.community.rule.engine.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+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.jetlinks.community.rule.engine.enums.RuleInstanceState;
+import org.jetlinks.rule.engine.api.Rule;
+import org.jetlinks.rule.engine.api.model.RuleEngineModelParser;
+import org.jetlinks.rule.engine.api.model.RuleModel;
+
+import javax.persistence.Column;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Table;
+import java.sql.JDBCType;
+
+@Getter
+@Setter
+@Table(name = "rule_instance")
+public class RuleInstanceEntity extends GenericEntity<String> implements RecordCreationEntity {
+
+    @Override
+    @GeneratedValue(generator = "snow_flake")
+    public String getId() {
+        return super.getId();
+    }
+
+    @Column(name = "model_id", length = 32)
+    private String modelId;
+
+    @Column(name = "name")
+    private String name;
+
+    @Column(name = "description")
+    private String description;
+
+    @Column(name = "model_type")
+    private String modelType;
+
+    @Column(name = "model_meta")
+    @ColumnType(jdbcType = JDBCType.CLOB)
+    private String modelMeta;
+
+    @Column(name = "model_version", nullable = false)
+    private Integer modelVersion;
+
+    @Column(name = "create_time")
+    private Long createTime;
+
+    @Column(name = "creator_id")
+    private String creatorId;
+
+    @Column(name = "state")
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    private RuleInstanceState state;
+
+    @Column(name = "instance_detail_json")
+    @ColumnType(jdbcType = JDBCType.CLOB)
+    private String instanceDetailJson;
+
+
+    public Rule toRule(RuleEngineModelParser parser) {
+        RuleModel model = parser.parse(modelType, modelMeta);
+        Rule rule = new Rule();
+        rule.setModel(model);
+        rule.setVersion(modelVersion);
+        rule.setId(getId());
+        return rule;
+    }
+}

+ 76 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleModelEntity.java

@@ -0,0 +1,76 @@
+package org.jetlinks.community.rule.engine.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+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.jetlinks.community.rule.engine.enums.RuleInstanceState;
+
+import javax.persistence.Column;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Table;
+import java.sql.JDBCType;
+
+@Getter
+@Setter
+@Table(name = "rule_model")
+public class RuleModelEntity extends GenericEntity<String> implements RecordCreationEntity, RecordModifierEntity {
+
+    @Override
+    @GeneratedValue(generator = "snow_flake")
+    public String getId() {
+        return super.getId();
+    }
+
+    @Column(name = "name")
+    private String name;
+
+    @Column(name = "type")
+    private String type;
+
+    @Column(name = "description")
+    private String description;
+
+    @Column(name = "model_type",nullable = false)
+    private String modelType;
+
+    @Column(name = "model_meta")
+    @ColumnType(jdbcType = JDBCType.CLOB)
+    private String modelMeta;
+
+    @Column(name = "version",nullable = false)
+    private Integer version;
+
+    @Column(name = "creator_id")
+    private String creatorId;
+
+    @Column(name = "create_time")
+    private Long createTime;
+
+    @Column(name = "modifier_id")
+    private String modifierId;
+
+    @Column(name = "modify_time")
+    private Long modifyTime;
+
+
+    public RuleInstanceEntity toInstance() {
+        RuleInstanceEntity instanceEntity = new RuleInstanceEntity();
+        // rule-1:1
+        instanceEntity.setId(getId().concat("-").concat(String.valueOf(getVersion())));
+        instanceEntity.setState(RuleInstanceState.stopped);
+        instanceEntity.setModelId(getId());
+        instanceEntity.setCreateTimeNow();
+        instanceEntity.setDescription(getDescription());
+        instanceEntity.setName(getName());
+        instanceEntity.setModelVersion(getVersion());
+        instanceEntity.setModelMeta(getModelMeta());
+        instanceEntity.setModelType(getModelType());
+
+
+        return instanceEntity;
+
+    }
+}

+ 22 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/RuleInstanceState.java

@@ -0,0 +1,22 @@
+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;
+
+@Getter
+@AllArgsConstructor
+@Dict( "rule-instance-state")
+public enum RuleInstanceState implements EnumDict<String> {
+    disable("已禁用"),
+    started("已启动"),
+    stopped("已停止");
+    private final String text;
+
+    @Override
+    public String getValue() {
+        return name();
+    }
+
+}

+ 29 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/DebugMessage.java

@@ -0,0 +1,29 @@
+package org.jetlinks.community.rule.engine.service;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.util.Date;
+
+
+@Getter
+@Setter
+@AllArgsConstructor(staticName = "of")
+@NoArgsConstructor
+public class DebugMessage implements Serializable {
+
+    private String type;
+
+    private String contextId;
+
+    private Object message;
+
+    private Date timestamp;
+
+    public static DebugMessage of(String type, String contextId, Object message) {
+        return of(type, contextId, message, new Date());
+    }
+}

+ 18 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/ExecuteRuleRequest.java

@@ -0,0 +1,18 @@
+package org.jetlinks.community.rule.engine.service;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class ExecuteRuleRequest {
+    private String sessionId;
+
+    private String contextId;
+
+    private String startWith;
+
+    private String endWith;
+
+    private Object data;
+}

+ 361 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleEngineDebugService.java

@@ -0,0 +1,361 @@
+package org.jetlinks.community.rule.engine.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import lombok.Getter;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.utils.StringUtils;
+import org.hswebframework.web.exception.NotFoundException;
+import org.hswebframework.web.id.IDGenerator;
+import org.jetlinks.core.cluster.ClusterManager;
+import org.jetlinks.core.cluster.ClusterQueue;
+import org.jetlinks.core.cluster.ClusterTopic;
+import org.jetlinks.rule.engine.api.*;
+import org.jetlinks.rule.engine.api.executor.*;
+import org.jetlinks.rule.engine.api.model.Condition;
+import org.jetlinks.rule.engine.api.model.NodeType;
+import org.jetlinks.rule.engine.api.model.RuleEngineModelParser;
+import org.jetlinks.rule.engine.cluster.logger.ClusterLogger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.EmitterProcessor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+@Slf4j
+@Service
+public class RuleEngineDebugService {
+
+    @Autowired
+    private ExecutableRuleNodeFactory executableRuleNodeFactory;
+
+    @Autowired
+    private RuleEngineModelParser modelParser;
+
+    @Autowired
+    private ConditionEvaluator conditionEvaluator;
+
+    private Map<String, Session> sessionStore = new ConcurrentHashMap<>();
+
+
+    public Flux<DebugMessage> getDebugMessages(String sessionId) {
+        return getSession(sessionId)
+            .consumeOutPut();
+    }
+
+    private Session getSession(String id) {
+        return Optional.ofNullable(id)
+            .map(sessionStore::get)
+            .orElseThrow(() -> new NotFoundException("session不存在"));
+    }
+
+    public Mono<String> startSession() {
+        return Mono.fromSupplier(() -> {
+            String sessionId = IDGenerator.UUID.generate();
+            Session session = new Session(sessionId);
+            session.local = true;
+            sessionStore.put(sessionId, session);
+            return sessionId;
+        });
+    }
+
+
+    @SneakyThrows
+    public String startNode(String sessionId, RuleNodeConfiguration configuration) {
+        configuration.setNodeType(NodeType.MAP);
+        Session session = getSession(sessionId);
+
+        DebugExecutionContext context = session.createContext(configuration);
+
+        ExecutableRuleNode ruleNode = executableRuleNodeFactory.create(configuration);
+
+        ruleNode.start(context);
+
+        return context.id;
+    }
+
+    public void sendData(String sessionId, String contextId, RuleData ruleData) {
+        getSession(sessionId)
+            .getContext(contextId)
+            .execute(ruleData);
+    }
+
+
+    public Mono<Boolean> stopContext(String sessionId, String contextId) {
+
+        return Mono.fromRunnable(() -> getSession(sessionId).stopContext(contextId))
+            .thenReturn(true);
+    }
+
+    public Set<String> getAllContext(String sessionId) {
+        return getSession(sessionId)
+            .contexts
+            .keySet();
+    }
+
+    public Mono<Boolean> closeSession(String sessionId) {
+        return Mono.fromRunnable(() -> getSession(sessionId).close());
+    }
+
+    public Mono<Boolean> testCondition(String sessionId, Condition condition, Object data) {
+
+        Session session = getSession(sessionId);
+
+        try {
+            boolean success = conditionEvaluator.evaluate(condition, RuleData.create(data));
+            return session.writeMessage(DebugMessage.of("output", null, "测试条件:".concat(success ? "通过" : "未通过")));
+        } catch (Exception e) {
+            return session.writeMessage(DebugMessage.of("error", null, StringUtils.throwable2String(e)));
+        }
+    }
+
+    private class Session {
+        private String id;
+
+        private long lastOperationTime;
+
+        private Map<String, DebugExecutionContext> contexts = new ConcurrentHashMap<>();
+
+        private Map<String, RuleInstanceContext> instanceContext = new ConcurrentHashMap<>();
+
+        private Map<String, String> instanceContextMapping = new ConcurrentHashMap<>();
+
+        private EmitterProcessor<DebugMessage> messageQueue = EmitterProcessor.create(false);
+
+        @Getter
+        private boolean local = false;
+
+        private Session(String id) {
+            this.lastOperationTime = System.currentTimeMillis();
+            this.id = id;
+        }
+
+        private boolean isTimeout() {
+            return System.currentTimeMillis() - lastOperationTime > TimeUnit.MINUTES.toMillis(15);
+        }
+
+        private void checkContextTimeout() {
+            contexts.entrySet()
+                .stream()
+                .filter(e -> e.getValue().isTimeout())
+                .map(Map.Entry::getKey)
+                .map(contexts::remove)
+                .forEach(DebugExecutionContext::stop);
+        }
+
+        private void stopContext(String contextId) {
+            Optional.ofNullable(contexts.remove(contextId))
+                .ifPresent(ExecutionContext::stop);
+        }
+
+        private Logger createLogger(String contextId, String nodeId) {
+            ClusterLogger logger = new ClusterLogger();
+            logger.setParent(new Slf4jLogger("rule.engine.debug.".concat(nodeId)));
+            logger.setNodeId(nodeId);
+            logger.setInstanceId(contextId);
+            logger.setLogInfoConsumer(logInfo -> {
+
+                Map<String, Object> data = new HashMap<>();
+                data.put("level", logInfo.getLevel());
+                data.put("message", logInfo.getMessage());
+
+                writeMessage(DebugMessage.of("log", contextId, data))
+                    .subscribe();
+
+            });
+            return logger;
+        }
+
+        private DebugExecutionContext createContext(RuleNodeConfiguration configuration) {
+            lastOperationTime = System.currentTimeMillis();
+            String id = Optional.ofNullable(configuration.getId()).orElseGet(IDGenerator.MD5::generate);
+            DebugExecutionContext context = contexts.get(id);
+            if (context != null) {
+                context.stop();
+                contexts.remove(id);
+            }
+
+            context = new DebugExecutionContext(id, createLogger(id, configuration.getNodeId()), this);
+            context.local = true;
+            contexts.put(id, context);
+            return context;
+        }
+
+        private DebugExecutionContext getContext(String id) {
+            lastOperationTime = System.currentTimeMillis();
+
+            return contexts.computeIfAbsent(id, _id -> new DebugExecutionContext(id, new Slf4jLogger("rule.engine.debug.none"), this));
+        }
+
+        private void execute(RuleData ruleData) {
+            String instanceId = ruleData.getAttribute("instanceId").map(String::valueOf).orElse(null);
+
+            RuleInstanceContext context = instanceContext.get(instanceId);
+            if (context != null) {
+                doExecute(context, ruleData);
+            }
+
+        }
+
+        private void doExecute(RuleInstanceContext context, RuleData ruleData) {
+
+            context.execute(Mono.just(ruleData))
+                .doOnError((throwable) -> {
+                    writeMessage(DebugMessage.of("error", context.getId(), "执行规则失败:" + StringUtils.throwable2String(throwable)));
+                })
+                .subscribe(resp -> {
+                    writeMessage(DebugMessage.of("output", context.getId(), resp.getData()));
+                });
+
+        }
+
+        private void execute(ExecuteRuleRequest request) {
+
+            RuleInstanceContext context = instanceContext.get(request.getContextId());
+            if (context == null) {
+                return;
+            }
+            RuleData ruleData = RuleData.create(request.getData());
+            RuleDataHelper.markStartWith(ruleData, request.getStartWith());
+            RuleDataHelper.markSyncReturn(ruleData, request.getEndWith());
+            ruleData.setAttribute("debugSessionId", id);
+            ruleData.setAttribute("instanceId", request.getContextId());
+
+            doExecute(context, ruleData);
+        }
+
+        private Mono<Boolean> writeMessage(DebugMessage message) {
+            lastOperationTime = System.currentTimeMillis();
+            return Mono.fromRunnable(() -> messageQueue.onNext(message))
+                .thenReturn(true);
+        }
+
+
+        @SneakyThrows
+        public Flux<DebugMessage> consumeOutPut() {
+
+            return messageQueue
+                .map(Function.identity());
+        }
+
+        public void close() {
+            contexts.forEach((s, context) -> context.stop());
+            instanceContext.values().forEach(RuleInstanceContext::stop);
+            instanceContext.clear();
+            instanceContextMapping.clear();
+            messageQueue.onComplete();
+        }
+
+    }
+
+    private class DebugExecutionContext implements ExecutionContext {
+
+        private Session session;
+
+        private String id;
+
+        private EmitterProcessor<RuleData> inputQueue = EmitterProcessor.create(false);
+
+        private Logger logger;
+
+        private List<Runnable> stopListener = new CopyOnWriteArrayList<>();
+
+        private long lastOperationTime = System.currentTimeMillis();
+
+        @Getter
+        private boolean local = false;
+
+        public DebugExecutionContext(String id, Logger logger, Session session) {
+            this.session = session;
+            this.logger = logger;
+            this.id = id;
+        }
+
+        public boolean isTimeout() {
+            return System.currentTimeMillis() - lastOperationTime > TimeUnit.MINUTES.toMillis(15);
+        }
+
+        @Override
+        public String getInstanceId() {
+            return id;
+        }
+
+        @Override
+        public String getNodeId() {
+            return id;
+        }
+
+        @Override
+        public Logger logger() {
+            return logger;
+        }
+
+        public void execute(RuleData ruleData) {
+            lastOperationTime = System.currentTimeMillis();
+
+            ruleData.setAttribute("debug", true);
+            inputQueue.onNext(ruleData);
+        }
+
+        @Override
+        public Input getInput() {
+
+            return new Input() {
+                @Override
+                public Flux<RuleData> subscribe() {
+                    return inputQueue.map(Function.identity())
+                        .doOnNext(data -> {
+                            log.debug("handle input :{}", data);
+                        })
+                        .doFinally(s -> {
+                            log.debug("unsubscribe input:{}", id);
+                        });
+                }
+
+                @Override
+                public void close() {
+                    inputQueue.onComplete();
+                }
+            };
+        }
+
+        @Override
+        public Output getOutput() {
+            return (data) -> Flux.from(data)
+                .flatMap(d -> session.writeMessage(DebugMessage.of("output", id, JSON.toJSONString(d.getData(), SerializerFeature.PrettyFormat))))
+                .then(Mono.just(true));
+        }
+
+        @Override
+        public Mono<Void> fireEvent(String event, RuleData data) {
+            return Mono.empty();
+        }
+
+        @Override
+        public Mono<Void> onError(RuleData data, Throwable e) {
+            return session
+                .writeMessage(DebugMessage.of("error", id, StringUtils.throwable2String(e)))
+                .then();
+        }
+
+        @Override
+        public void stop() {
+            stopListener.forEach(Runnable::run);
+        }
+
+        @Override
+        public void onStop(Runnable runnable) {
+            stopListener.add(runnable);
+        }
+    }
+
+
+}

+ 83 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleInstanceService.java

@@ -0,0 +1,83 @@
+package org.jetlinks.community.rule.engine.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.hswebframework.web.api.crud.entity.PagerResult;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.jetlinks.community.rule.engine.entity.ExecuteLogInfo;
+import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteEventInfo;
+import org.jetlinks.community.rule.engine.event.handler.RuleEngineLoggerIndexProvider;
+import org.jetlinks.community.elastic.search.service.ElasticSearchService;
+import org.jetlinks.community.rule.engine.enums.RuleInstanceState;
+import org.jetlinks.community.rule.engine.entity.RuleInstanceEntity;
+import org.jetlinks.rule.engine.api.Rule;
+import org.jetlinks.rule.engine.api.RuleEngine;
+import org.jetlinks.rule.engine.api.RuleInstanceContext;
+import org.jetlinks.rule.engine.api.model.RuleEngineModelParser;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+@Service
+@Slf4j
+public class RuleInstanceService extends GenericReactiveCrudService<RuleInstanceEntity, String> implements CommandLineRunner {
+
+    @Autowired
+    private RuleEngine ruleEngine;
+
+    @Autowired
+    private RuleEngineModelParser modelParser;
+
+    @Autowired
+    private ElasticSearchService elasticSearchService;
+
+    public Mono<PagerResult<RuleEngineExecuteEventInfo>> queryExecuteEvent(QueryParam queryParam) {
+        return elasticSearchService.queryPager(RuleEngineLoggerIndexProvider.RULE_EVENT_LOG, queryParam, RuleEngineExecuteEventInfo.class);
+    }
+
+    public Mono<PagerResult<ExecuteLogInfo>> queryExecuteLog(QueryParam queryParam) {
+        return elasticSearchService.queryPager(RuleEngineLoggerIndexProvider.RULE_LOG, queryParam, ExecuteLogInfo.class);
+    }
+
+    public Mono<Void> stop(String id) {
+        return this.ruleEngine
+                .getInstance(id)
+                .flatMap(RuleInstanceContext::stop)
+                .switchIfEmpty(Mono.empty())
+                .then(createUpdate()
+                        .set(RuleInstanceEntity::getState, RuleInstanceState.stopped)
+                        .where(RuleInstanceEntity::getId,id)
+                        .execute())
+                .then();
+    }
+
+    public Mono<RuleInstanceContext> start(String id) {
+        return findById(Mono.just(id))
+                .flatMap(this::doStart);
+    }
+
+    private Mono<RuleInstanceContext> doStart(RuleInstanceEntity entity) {
+        return Mono.defer(() -> {
+            Rule rule = entity.toRule(modelParser);
+            return ruleEngine.startRule(rule)
+                    .flatMap(ctx -> createUpdate()
+                            .set(RuleInstanceEntity::getState, RuleInstanceState.started)
+                            .where(entity::getId)
+                            .execute()
+                            .thenReturn(ctx));
+        });
+    }
+
+    @Override
+    public void run(String... args) {
+        createQuery()
+                .where()
+                .is(RuleInstanceEntity::getState, RuleInstanceState.started)
+                .fetch()
+                .flatMap(this::doStart)
+                .subscribe(context -> {
+                    log.debug("start rule {}", context.getId());
+                });
+    }
+}

+ 78 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/RuleModelService.java

@@ -0,0 +1,78 @@
+package org.jetlinks.community.rule.engine.service;
+
+import org.hswebframework.ezorm.rdb.exception.DuplicateKeyException;
+import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult;
+import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.hswebframework.web.exception.BusinessException;
+import org.jetlinks.community.rule.engine.entity.RuleModelEntity;
+import org.jetlinks.rule.engine.api.model.RuleEngineModelParser;
+import org.reactivestreams.Publisher;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@Service
+public class RuleModelService extends GenericReactiveCrudService<RuleModelEntity, String> {
+
+    @Autowired
+    private RuleInstanceService instanceService;
+
+    @Autowired
+    private RuleEngineModelParser modelParser;
+
+
+    public Mono<Boolean> deploy(String id) {
+        return findById(Mono.just(id))
+                .map(RuleModelEntity::toInstance)
+                .doOnNext(instance -> modelParser.parse(instance.getModelType(), instance.getModelMeta()))
+                .flatMap(rule -> instanceService.save(Mono.just(rule)))
+                .thenReturn(true);
+    }
+
+    @Override
+    public Mono<Integer> updateById(String id, Mono<RuleModelEntity> entityPublisher) {
+        return entityPublisher
+                .flatMap(model -> {
+                    model.setId(id);
+                    model.setVersion(null);
+                    return createUpdate()
+                            .set(model)
+                            .set(RuleModelEntity::getVersion, NativeSql.of("version+1"))
+                            .where(model::getId)
+                            .execute();
+                });
+    }
+
+    @Override
+    public Mono<SaveResult> save(Publisher<RuleModelEntity> entityPublisher) {
+        return Flux.from(entityPublisher)
+                .flatMap(rule -> {
+                    rule.setVersion(null);
+                    return createUpdate()
+                            .set(rule)
+                            .set(RuleModelEntity::getVersion, NativeSql.of("version+1"))
+                            .where(rule::getId)
+                            .execute()
+                            .filter(i -> i > 0)
+                            .map(i -> SaveResult.of(0, i))
+                            .switchIfEmpty(Mono.defer(() -> insert(Mono.just(rule))
+                                    .map(i -> SaveResult.of(1, 0))));
+
+                }).reduce(SaveResult.of(0, 0), SaveResult::merge);
+    }
+
+
+    @Override
+    public Mono<Integer> insert(Publisher<RuleModelEntity> entityPublisher) {
+        return Flux.from(entityPublisher)
+                .map(rule -> {
+                    if (rule.getVersion() == null) {
+                        rule.setVersion(1);
+                    }
+                    return rule;
+                }).as(super::insert)
+                .onErrorMap(DuplicateKeyException.class, err -> new BusinessException("模型ID重复", err));
+    }
+}

+ 80 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleEngineDebugController.java

@@ -0,0 +1,80 @@
+package org.jetlinks.community.rule.engine.web;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.jetlinks.community.rule.engine.service.DebugMessage;
+import org.jetlinks.community.rule.engine.service.RuleEngineDebugService;
+import org.jetlinks.rule.engine.api.RuleData;
+import org.jetlinks.rule.engine.api.executor.RuleNodeConfiguration;
+import org.jetlinks.rule.engine.api.model.Condition;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/rule-engine/debug")
+@Authorize
+@Resource(id="rule-model",name = "规则引擎-模型")
+public class RuleEngineDebugController {
+
+    @Autowired
+    private RuleEngineDebugService ruleDebugService;
+
+    @PostMapping
+    public Mono<String> startSession() {
+        return ruleDebugService.startSession();
+    }
+
+    @PostMapping("/{sessionId}")
+    public Mono<String> startNode(@PathVariable String sessionId, @RequestBody RuleNodeConfiguration configuration) {
+        return Mono.just(ruleDebugService.startNode(sessionId, configuration));
+    }
+
+    @GetMapping(value = "/{sessionId}/logs", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
+    public Flux<DebugMessage> getSessionLogs(@PathVariable String sessionId) {
+        return ruleDebugService.getDebugMessages(sessionId);
+    }
+
+    @PostMapping("/{sessionId}/{contextId}")
+    public Mono<String> executeNode(@PathVariable String sessionId,
+                                    @PathVariable String contextId,
+                                    @RequestBody String data) {
+        Object object = data.trim();
+        if (data.startsWith("[") || data.startsWith("{")) {
+            object = JSON.parse(data);
+        }
+
+        RuleData ruleData = RuleData.create(object);
+
+        ruleDebugService.sendData(sessionId, contextId, ruleData);
+
+        return Mono.just(ruleData.getId());
+    }
+
+    @GetMapping("/{sessionId}/contexts")
+    public Flux<String> getSessionContexts(@PathVariable String sessionId) {
+        return Flux.fromIterable(ruleDebugService.getAllContext(sessionId));
+    }
+
+
+    @PostMapping("/{sessionId}/condition")
+    public Mono<Boolean> testCondition(@PathVariable String sessionId, @RequestBody JSONObject data) {
+        return ruleDebugService.testCondition(sessionId, data.getJSONObject("condition").toJavaObject(Condition.class), data.get("data"));
+    }
+
+    @DeleteMapping("/{sessionId}")
+    public Mono<Boolean> stopSession(@PathVariable String sessionId) {
+        return   ruleDebugService.closeSession(sessionId);
+    }
+
+    @DeleteMapping("/{sessionId}/{contextId}")
+    public Mono<Boolean> stopContext(@PathVariable String sessionId, @PathVariable String contextId) {
+
+        return  ruleDebugService.stopContext(sessionId, contextId);
+    }
+
+}

+ 92 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleInstanceController.java

@@ -0,0 +1,92 @@
+package org.jetlinks.community.rule.engine.web;
+
+import org.hswebframework.web.api.crud.entity.PagerResult;
+import org.hswebframework.web.api.crud.entity.QueryParamEntity;
+import org.hswebframework.web.authorization.annotation.QueryAction;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.hswebframework.web.authorization.annotation.ResourceAction;
+import org.hswebframework.web.crud.service.ReactiveCrudService;
+import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
+import org.hswebframework.web.exception.NotFoundException;
+import org.hswebframework.web.id.IDGenerator;
+import org.jetlinks.community.rule.engine.entity.ExecuteLogInfo;
+import org.jetlinks.community.rule.engine.entity.RuleEngineExecuteEventInfo;
+import org.jetlinks.community.rule.engine.entity.RuleInstanceEntity;
+import org.jetlinks.community.rule.engine.service.RuleInstanceService;
+import org.jetlinks.rule.engine.api.DefaultRuleData;
+import org.jetlinks.rule.engine.api.RuleDataHelper;
+import org.jetlinks.rule.engine.api.RuleEngine;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("rule-engine/instance")
+@Resource(id = "rule-instance", name = "规则引擎-实例")
+public class RuleInstanceController implements ReactiveServiceCrudController<RuleInstanceEntity, String> {
+
+    @Autowired
+    private RuleInstanceService instanceService;
+
+    @Autowired
+    private RuleEngine ruleEngine;
+
+    @PostMapping("/{id}/_start")
+    @ResourceAction(id = "start", name = "启动")
+    public Mono<Boolean> start(@PathVariable String id) {
+        return instanceService.start(id)
+            .thenReturn(true);
+    }
+
+    @PostMapping("/{id}/_stop")
+    @ResourceAction(id = "stop", name = "停止")
+    public Mono<Boolean> stop(@PathVariable String id) {
+        return instanceService.stop(id)
+            .thenReturn(true);
+    }
+
+    @GetMapping("/{id}/logs")
+    @QueryAction
+    public Mono<PagerResult<ExecuteLogInfo>> queryLog(@PathVariable String id,
+                                                      QueryParamEntity paramEntity) {
+        return paramEntity.toQuery()
+            .is("instanceId", id)
+            .execute(instanceService::queryExecuteLog);
+    }
+
+    @GetMapping("/{id}/events")
+    @QueryAction
+    public Mono<PagerResult<RuleEngineExecuteEventInfo>> queryEvents(@PathVariable String id,
+                                                                     QueryParamEntity paramEntity) {
+        return paramEntity.toQuery()
+            .is("instanceId", id)
+            .execute(instanceService::queryExecuteEvent);
+
+    }
+
+
+    @PostMapping("/{id}/_execute/{startWith}/{endWith}")
+    @ResourceAction(id = "execute", name = "执行")
+    public Flux<Object> execute(@PathVariable String id,
+                                @PathVariable String startWith,
+                                @PathVariable String endWith,
+                                @RequestBody Flux<DefaultRuleData> payload) {
+        return ruleEngine
+            .getInstance(id)
+            .switchIfEmpty(Mono.error(NotFoundException::new))
+            .flatMapMany(context -> context
+                .execute(payload
+                    .map(ruleData -> {
+                        ruleData.setId(IDGenerator.SNOW_FLAKE_STRING.generate());
+                        RuleDataHelper.markStartWith(ruleData, startWith);
+                        return RuleDataHelper.markSyncReturn(ruleData, endWith);
+                    })
+                ));
+    }
+
+    @Override
+    public ReactiveCrudService<RuleInstanceEntity, String> getService() {
+        return instanceService;
+    }
+}

+ 44 - 0
jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/RuleModelController.java

@@ -0,0 +1,44 @@
+package org.jetlinks.community.rule.engine.web;
+
+import org.hswebframework.web.authorization.annotation.QueryAction;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.hswebframework.web.authorization.annotation.ResourceAction;
+import org.hswebframework.web.crud.service.ReactiveCrudService;
+import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
+import org.jetlinks.community.rule.engine.entity.RuleModelEntity;
+import org.jetlinks.community.rule.engine.service.RuleModelService;
+import org.jetlinks.rule.engine.api.executor.ExecutableRuleNodeFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("rule-engine/model")
+@Resource(id = "rule-model", name = "规则引擎-模型")
+public class RuleModelController implements ReactiveServiceCrudController<RuleModelEntity, String> {
+
+    @Autowired
+    private RuleModelService ruleModelService;
+
+    @Autowired
+    private ExecutableRuleNodeFactory factory;
+
+    @Override
+    public ReactiveCrudService<RuleModelEntity, String> getService() {
+        return ruleModelService;
+    }
+
+    @PostMapping("/{id}/_deploy")
+    @ResourceAction(id = "deploy", name = "发布")
+    public Mono<Boolean> deploy(@PathVariable String id) {
+        return ruleModelService.deploy(id);
+    }
+
+    //获取全部支持的执行器
+    @GetMapping("/executors")
+    @QueryAction
+    public Flux<String> getAllSupportExecutors() {
+        return Flux.fromIterable(factory.getAllSupportExecutor());
+    }
+}

+ 6 - 0
jetlinks-standalone/pom.xml

@@ -135,6 +135,12 @@
             <version>${project.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>rule-engine-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>org.jetlinks</groupId>
             <artifactId>jetlinks-supports</artifactId>