Browse Source

first commit

zhou-hao 5 years ago
commit
d854b56436
100 changed files with 5734 additions and 0 deletions
  1. 11 0
      .editorconfig
  2. 29 0
      .gitignore
  3. 47 0
      jetlinks-components/elasticsearch-component/pom.xml
  4. 22 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/ElasticRestClient.java
  5. 118 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/DefaultAggregationService.java
  6. 154 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/AggregationResponseHandle.java
  7. 42 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/Bucket.java
  8. 67 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/BucketAggregationsStructure.java
  9. 21 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/BucketResponse.java
  10. 38 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/DateHistogramInterval.java
  11. 19 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/Ranges.java
  12. 54 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/Sort.java
  13. 28 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/enums/AggregationType.java
  14. 182 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/enums/BucketType.java
  15. 132 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/enums/MetricsType.java
  16. 18 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/enums/OrderType.java
  17. 37 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/metrics/MetricsAggregationStructure.java
  18. 33 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/metrics/MetricsResponse.java
  19. 45 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/metrics/MetricsResponseSingleValue.java
  20. 35 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchConfiguration.java
  21. 51 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchProperties.java
  22. 41 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/enums/FieldDateFormat.java
  23. 48 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/enums/FieldType.java
  24. 69 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/enums/LinkTypeEnum.java
  25. 109 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/enums/TermTypeEnum.java
  26. 68 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/CreateIndex.java
  27. 128 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/DefaultIndexOperationService.java
  28. 37 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/ElasticIndex.java
  29. 64 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/mapping/IndexMappingMetadata.java
  30. 26 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/mapping/IndicesMappingCenter.java
  31. 71 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/mapping/MappingFactory.java
  32. 23 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/mapping/SingleMappingMetadata.java
  33. 40 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/setting/SettingFactory.java
  34. 35 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/AbstractQueryParamTranslateService.java
  35. 64 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/DefaultLinkTypeParser.java
  36. 52 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/DefaultQueryParamTranslateService.java
  37. 28 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/DefaultTermTypeParser.java
  38. 14 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/LinkTypeParser.java
  39. 14 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/QueryParamTranslateService.java
  40. 17 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/TermTypeParser.java
  41. 20 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/AggregationService.java
  42. 251 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/DefaultElasticSearchService.java
  43. 24 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/ElasticSearchService.java
  44. 18 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/IndexOperationService.java
  45. 47 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/translate/QueryParamTranslator.java
  46. 53 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/utils/DateTimeUtils.java
  47. 36 0
      jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/utils/TermCommonUtils.java
  48. 170 0
      jetlinks-components/elasticsearch-component/src/test/java/org/jetlinks/community/elastic/search/AggregationTest.java
  49. 47 0
      jetlinks-components/elasticsearch-component/src/test/java/org/jetlinks/community/elastic/search/ElasticSearchQueryParamTranslatorTest.java
  50. 61 0
      jetlinks-components/elasticsearch-component/src/test/java/org/jetlinks/community/elastic/search/IndexInitTest.java
  51. 3 0
      jetlinks-components/gateway-component/README.md
  52. 28 0
      jetlinks-components/gateway-component/pom.xml
  53. 62 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/ClientSession.java
  54. 61 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/ClientSessionManager.java
  55. 21 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DefaultTopicMessage.java
  56. 66 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGateway.java
  57. 15 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGatewayManager.java
  58. 12 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/EncodableMessage.java
  59. 33 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/JsonEncodedMessage.java
  60. 80 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessageConnection.java
  61. 57 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessageConnector.java
  62. 103 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessageGateway.java
  63. 12 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessageGatewayManager.java
  64. 24 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessagePublisher.java
  65. 56 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessageSubscriber.java
  66. 26 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/Subscription.java
  67. 39 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/TopicMessage.java
  68. 182 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/TopicPart.java
  69. 68 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNode.java
  70. 56 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNodeConfig.java
  71. 73 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/TopicMessageCodec.java
  72. 79 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DefaultDeviceGatewayManager.java
  73. 256 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DefaultMessageGateway.java
  74. 40 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DefaultMessageGatewayManager.java
  75. 22 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProperties.java
  76. 10 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayPropertiesManager.java
  77. 17 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProvider.java
  78. 57 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/LocalClientSession.java
  79. 58 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/LocalClientSessionManager.java
  80. 111 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/LocalMessageConnection.java
  81. 42 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/LocalMessageConnector.java
  82. 20 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/MessageConnectorProperties.java
  83. 14 0
      jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/MessageConnectorProvider.java
  84. 32 0
      jetlinks-components/io-component/pom.xml
  85. 48 0
      jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/excel/DefaultImportExportService.java
  86. 19 0
      jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/excel/ImportExportService.java
  87. 14 0
      jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/excel/RowResult.java
  88. 62 0
      jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/excel/easyexcel/ExcelReadDataListener.java
  89. 2 0
      jetlinks-components/jetlinks-components.iml
  90. 2 0
      jetlinks-components/network-component/mqtt-component/mqtt-component.iml
  91. 52 0
      jetlinks-components/network-component/mqtt-component/pom.xml
  92. 16 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClient.java
  93. 22 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClientProperties.java
  94. 93 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClientProvider.java
  95. 174 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/VertxMqttClient.java
  96. 199 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/MqttClientDeviceGateway.java
  97. 82 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/MqttClientDeviceGatewayProvider.java
  98. 194 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/MqttServerDeviceGateway.java
  99. 62 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/MqttServerDeviceGatewayProvider.java
  100. 0 0
      jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/session/MqttConnectionSession.java

+ 11 - 0
.editorconfig

@@ -0,0 +1,11 @@
+root = true
+
+[*]
+charset = utf-8
+
+[*.java]
+indent_style = space
+indent_size = tab
+tab_width = 4
+trim_trailing_whitespace = true
+insert_final_newline = false

+ 29 - 0
.gitignore

@@ -0,0 +1,29 @@
+**/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
+!ip2region.db
+!device-simulator.jar

+ 47 - 0
jetlinks-components/elasticsearch-component/pom.xml

@@ -0,0 +1,47 @@
+<?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>elasticsearch-component</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.projectreactor</groupId>
+            <artifactId>reactor-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-easy-orm-elasticsearch</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.elasticsearch.client</groupId>
+            <artifactId>elasticsearch-rest-high-level-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-commons-crud</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-easy-orm-rdb</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>jetlinks-core</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+        
+    </dependencies>
+
+</project>

+ 22 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/ElasticRestClient.java

@@ -0,0 +1,22 @@
+package org.jetlinks.community.elastic.search;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.apache.http.HttpHost;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestHighLevelClient;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@AllArgsConstructor
+@NoArgsConstructor
+@Getter
+public class ElasticRestClient {
+
+    private RestHighLevelClient queryClient;
+
+    private RestHighLevelClient writeClient;
+}

+ 118 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/DefaultAggregationService.java

@@ -0,0 +1,118 @@
+package org.jetlinks.community.elastic.search.aggreation;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.search.SearchRequest;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.jetlinks.community.elastic.search.ElasticRestClient;
+import org.jetlinks.community.elastic.search.aggreation.bucket.BucketAggregationsStructure;
+import org.jetlinks.community.elastic.search.aggreation.bucket.BucketResponse;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsAggregationStructure;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponse;
+import org.jetlinks.community.elastic.search.index.ElasticIndex;
+import org.jetlinks.community.elastic.search.parser.QueryParamTranslateService;
+import org.jetlinks.community.elastic.search.service.AggregationService;
+import org.jetlinks.community.elastic.search.service.IndexOperationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.MonoSink;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Service
+@Slf4j
+public class DefaultAggregationService implements AggregationService {
+
+    private final QueryParamTranslateService translateService;
+
+    private final ElasticRestClient restClient;
+
+    private final IndexOperationService indexOperationService;
+
+    @Autowired
+    public DefaultAggregationService(IndexOperationService indexOperationService,
+                                     ElasticRestClient restClient,
+                                     QueryParamTranslateService translateService) {
+        this.indexOperationService = indexOperationService;
+        this.restClient = restClient;
+        this.translateService = translateService;
+    }
+
+
+    @Override
+    public Mono<MetricsResponse> metricsAggregation(QueryParam queryParam,
+                                                    MetricsAggregationStructure structure,
+                                                    ElasticIndex provider) {
+        return searchSourceBuilderMono(queryParam, provider)
+                .doOnNext(builder -> builder.aggregation(
+                        structure.getType().aggregationBuilder(structure.getName(), structure.getField())))
+                .map(builder -> new SearchRequest(provider.getStandardIndex())
+                        .source(builder))
+                .flatMap(request -> Mono.<SearchResponse>create(monoSink ->
+                        restClient.getQueryClient().searchAsync(request, RequestOptions.DEFAULT, translatorActionListener(monoSink))))
+                .map(searchResponse -> structure.getType().getResponse(structure.getName(), searchResponse));
+    }
+
+    @Override
+    public Mono<BucketResponse> bucketAggregation(QueryParam queryParam, BucketAggregationsStructure structure, ElasticIndex provider) {
+        return searchSourceBuilderMono(queryParam, provider)
+                .doOnNext(builder ->
+                        builder.aggregation(structure.getType().aggregationBuilder(structure))
+                )
+                .map(builder -> new SearchRequest(provider.getStandardIndex())
+                        .source(builder))
+                .doOnNext(searchRequest ->
+                        log.debug("聚合查询index:{},参数:{}",
+                                provider.getStandardIndex(),
+                                JSON.toJSON(searchRequest.source().toString())))
+                .flatMap(request -> Mono.<SearchResponse>create(monoSink ->
+                        restClient.getQueryClient().searchAsync(request, RequestOptions.DEFAULT, translatorActionListener(monoSink))))
+                .map(response -> structure.getType().convert(response.getAggregations().get(structure.getName())))
+                .map(buckets -> BucketResponse.builder()
+                        .name(structure.getName())
+                        .buckets(buckets)
+                        .build()
+                )
+                ;
+
+    }
+
+    private Mono<SearchSourceBuilder> searchSourceBuilderMono(QueryParam queryParam, ElasticIndex provider) {
+        queryParam.setPaging(false);
+        return indexOperationService.getIndexMappingMetadata(provider.getStandardIndex())
+                .map(metadata -> translateService.translate(queryParam, metadata))
+                .doOnError(e -> log.error("解析queryParam错误, index:{}", provider.getStandardIndex(), e));
+        // return Mono.just(translateService.translate(queryParam, IndexMappingMetadata.getInstance(provider.getStandardIndex())));
+    }
+
+    private <T> ActionListener<T> translatorActionListener(MonoSink<T> sink) {
+        return new ActionListener<T>() {
+            @Override
+            public void onResponse(T response) {
+                sink.success(response);
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                if (e instanceof ElasticsearchException) {
+                    if (((ElasticsearchException) e).status().getStatus() == 404) {
+                        sink.success();
+                        return;
+                    } else if (((ElasticsearchException) e).status().getStatus() == 400) {
+                        sink.error(new ElasticsearchParseException("查询参数格式错误", e));
+                    }
+                }
+                sink.error(e);
+            }
+        };
+    }
+}

+ 154 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/AggregationResponseHandle.java

@@ -0,0 +1,154 @@
+package org.jetlinks.community.elastic.search.aggreation.bucket;
+
+import org.elasticsearch.search.aggregations.Aggregation;
+import org.elasticsearch.search.aggregations.bucket.histogram.Histogram;
+import org.elasticsearch.search.aggregations.bucket.range.Range;
+import org.elasticsearch.search.aggregations.bucket.terms.Terms;
+import org.elasticsearch.search.aggregations.metrics.avg.Avg;
+import org.elasticsearch.search.aggregations.metrics.max.Max;
+import org.elasticsearch.search.aggregations.metrics.min.Min;
+import org.elasticsearch.search.aggregations.metrics.stats.Stats;
+import org.elasticsearch.search.aggregations.metrics.sum.Sum;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponseSingleValue;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public class AggregationResponseHandle {
+
+
+    public static <A extends Aggregation> List<Bucket> terms(A a) {
+        Terms terms = (Terms) a;
+        return terms.getBuckets()
+                .stream()
+                .map(b -> {
+                    Bucket bucket = Bucket.builder()
+                            .key(b.getKeyAsString())
+                            .count(b.getDocCount())
+                            .build();
+                    b.getAggregations().asList()
+                            .forEach(subAggregation -> route(bucket, subAggregation));
+                    return bucket;
+                }).collect(Collectors.toList())
+                ;
+    }
+
+    public static <A extends Aggregation> List<Bucket> range(A a) {
+        Range range = (Range) a;
+        return range.getBuckets()
+                .stream()
+                .map(b -> {
+                    Bucket bucket = Bucket.builder()
+                            .key(b.getKeyAsString())
+                            .from(b.getFrom())
+                            .to(b.getTo())
+                            .fromAsString(b.getFromAsString())
+                            .toAsString(b.getToAsString())
+                            .count(b.getDocCount()).build();
+                    b.getAggregations().asList()
+                            .forEach(subAggregation -> {
+                                route(bucket, subAggregation);
+                            });
+                    return bucket;
+                }).collect(Collectors.toList())
+                ;
+    }
+
+    public static <A extends Aggregation> List<Bucket> dateHistogram(A a) {
+        Histogram histogram = (Histogram) a;
+        return bucketsHandle(histogram.getBuckets());
+    }
+
+    private static List<Bucket> bucketsHandle(List<? extends Histogram.Bucket> buckets) {
+        return buckets
+                .stream()
+                .map(b -> {
+                    Bucket bucket = Bucket.builder()
+                            .key(b.getKeyAsString())
+                            .count(b.getDocCount())
+                            .build();
+                    b.getAggregations().asList()
+                            .forEach(subAggregation -> route(bucket, subAggregation));
+                    return bucket;
+                }).collect(Collectors.toList())
+                ;
+    }
+
+    private static <A extends Aggregation> void route(Bucket bucket, A a) {
+        if (a instanceof Terms) {
+            bucket.setBuckets(terms(a));
+        } else if (a instanceof Range) {
+            bucket.setBuckets(range(a));
+        } else if (a instanceof Histogram) {
+            bucket.setBuckets(range(a));
+        } else if (a instanceof Avg) {
+            bucket.setAvg(avg(a));
+        } else if (a instanceof Min) {
+            bucket.setMin(min(a));
+        } else if (a instanceof Max) {
+            bucket.setMax(max(a));
+        } else if (a instanceof Sum) {
+            bucket.setSum(sum(a));
+        } else if (a instanceof Stats) {
+            stats(bucket, a);
+        } else {
+            throw new UnsupportedOperationException("不支持的聚合类型");
+        }
+    }
+
+    public static <A extends Aggregation> MetricsResponseSingleValue avg(A a) {
+        Avg avg = (Avg) a;
+        return MetricsResponseSingleValue.builder()
+                .value(avg.getValue())
+                .valueAsString(avg.getValueAsString())
+                .build();
+    }
+
+    public static <A extends Aggregation> MetricsResponseSingleValue max(A a) {
+        Max max = (Max) a;
+        return MetricsResponseSingleValue.builder()
+                .value(max.getValue())
+                .valueAsString(max.getValueAsString())
+                .build();
+    }
+
+    public static <A extends Aggregation> MetricsResponseSingleValue min(A a) {
+        Min min = (Min) a;
+        return MetricsResponseSingleValue.builder()
+                .value(min.getValue())
+                .valueAsString(min.getValueAsString())
+                .build();
+    }
+
+    public static <A extends Aggregation> MetricsResponseSingleValue sum(A a) {
+        Sum sum = (Sum) a;
+        return MetricsResponseSingleValue.builder()
+                .value(sum.getValue())
+                .valueAsString(sum.getValueAsString())
+                .build();
+    }
+
+    public static <A extends Aggregation> void stats(Bucket bucket, A a) {
+        Stats stats = (Stats) a;
+        bucket.setAvg(MetricsResponseSingleValue.builder()
+                .value(stats.getAvg())
+                .valueAsString(stats.getAvgAsString())
+                .build());
+        bucket.setMax(MetricsResponseSingleValue.builder()
+                .value(stats.getMax())
+                .valueAsString(stats.getMaxAsString())
+                .build());
+        bucket.setMin(MetricsResponseSingleValue.builder()
+                .value(stats.getMin())
+                .valueAsString(stats.getMinAsString())
+                .build());
+        bucket.setSum(MetricsResponseSingleValue.builder()
+                .value(stats.getSum())
+                .valueAsString(stats.getSumAsString())
+                .build());
+    }
+}

+ 42 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/Bucket.java

@@ -0,0 +1,42 @@
+package org.jetlinks.community.elastic.search.aggreation.bucket;
+
+import lombok.*;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponseSingleValue;
+
+import java.util.List;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class Bucket {
+
+    private String key;
+
+    private long count;
+
+    private String fromAsString;
+
+    private Object from;
+
+    private String toAsString;
+
+    private Object to;
+
+    private MetricsResponseSingleValue sum;
+
+    private MetricsResponseSingleValue valueCount;
+
+    private MetricsResponseSingleValue avg;
+
+    private MetricsResponseSingleValue min;
+
+    private MetricsResponseSingleValue max;
+
+    private List<Bucket> buckets;
+}

+ 67 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/BucketAggregationsStructure.java

@@ -0,0 +1,67 @@
+package org.jetlinks.community.elastic.search.aggreation.bucket;
+
+import lombok.*;
+import org.hswebframework.utils.StringUtils;
+import org.jetlinks.community.elastic.search.aggreation.enums.BucketType;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsAggregationStructure;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Setter
+@Getter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class BucketAggregationsStructure {
+
+    @NonNull
+    private String field;
+
+    private String name;
+
+    @NonNull
+    private BucketType type = BucketType.TERMS;
+
+    /**
+     * 指定返回分组数量
+     */
+    private Integer size;
+
+    private Sort sort;
+
+    private List<Ranges> ranges;
+
+    /**
+     * 时间格式
+     */
+    private String format;
+
+
+    /**
+     * 单位时间间隔
+     *
+     * @see DateHistogramInterval
+     */
+    private String interval;
+
+    /**
+     * 缺失值
+     */
+    private Object missingValue;
+
+    private List<MetricsAggregationStructure> subMetricsAggregation = new LinkedList<>();
+
+    private List<BucketAggregationsStructure> subBucketAggregation = new LinkedList<>();
+
+    public String getName() {
+        if (StringUtils.isNullOrEmpty(name)) {
+            name = type.name().concat("_").concat(field);
+        }
+        return name;
+    }
+}

+ 21 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/BucketResponse.java

@@ -0,0 +1,21 @@
+package org.jetlinks.community.elastic.search.aggreation.bucket;
+
+import lombok.*;
+
+import java.util.List;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class BucketResponse {
+
+    private String name;
+
+    private List<Bucket> buckets;
+}

+ 38 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/DateHistogramInterval.java

@@ -0,0 +1,38 @@
+
+package org.jetlinks.community.elastic.search.aggreation.bucket;
+
+/**
+ * The interval the date histogram is based on.
+ */
+public class DateHistogramInterval {
+
+    public static final String SECOND = "1s";
+    public static final String MINUTE = "1m";
+    public static final String HOUR = "1h";
+    public static final String DAY = "1d";
+    public static final String WEEK = "1w";
+    public static final String MONTH = "1M";
+    public static final String QUARTER = "1q";
+    public static final String YEAR = "1y";
+
+    public static String seconds(int sec) {
+        return sec + "s";
+    }
+
+    public static String minutes(int min) {
+        return min + "m";
+    }
+
+    public static String hours(int hours) {
+        return hours + "h";
+    }
+
+    public static String days(int days) {
+        return days + "d";
+    }
+
+    public static String weeks(int weeks) {
+        return weeks + "w";
+    }
+
+}

+ 19 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/Ranges.java

@@ -0,0 +1,19 @@
+package org.jetlinks.community.elastic.search.aggreation.bucket;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+public class Ranges {
+
+    private String key;
+
+    private Object form;
+
+    private Object to;
+}

+ 54 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/bucket/Sort.java

@@ -0,0 +1,54 @@
+package org.jetlinks.community.elastic.search.aggreation.bucket;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.community.elastic.search.aggreation.enums.OrderType;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+public class Sort {
+
+    private String order;
+
+    private OrderType type = OrderType.COUNT;
+
+    private Sort() {
+    }
+
+    private Sort(String order, OrderType type) {
+        this.type = type;
+        this.order = order;
+    }
+
+    private Sort(String order) {
+        this.order = order;
+    }
+
+    public String getOrder() {
+        if ("desc".equalsIgnoreCase(order)) {
+            return order;
+        } else {
+            return order = "asc";
+        }
+    }
+
+    public static Sort asc() {
+        return new Sort("asc");
+    }
+
+    public static Sort asc(OrderType type) {
+        return new Sort("asc", type);
+    }
+
+    public static Sort desc() {
+        return new Sort("desc");
+    }
+
+    public static Sort desc(OrderType type) {
+        return new Sort("desc", type);
+    }
+}

+ 28 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/enums/AggregationType.java

@@ -0,0 +1,28 @@
+package org.jetlinks.community.elastic.search.aggreation.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@AllArgsConstructor
+public enum AggregationType {
+    AVG("平均"),
+    MAX("最大"),
+    COUNT("计数"),
+    MIN("最小"),
+    SUM("总数"),
+    STATS("统计汇总"),
+    EXTENDED_STATS("扩展统计"),
+    CARDINALITY("基数"),//去重统计
+    VALUE_COUNT("非空值计数"),
+    TERMS("字段项"),
+    RANGE("范围"),
+    DATE_HISTOGRAM("直方图"),
+    DATE_RANGE("时间范围");
+
+    private String text;
+}

+ 182 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/enums/BucketType.java

@@ -0,0 +1,182 @@
+package org.jetlinks.community.elastic.search.aggreation.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import org.elasticsearch.search.aggregations.Aggregation;
+import org.elasticsearch.search.aggregations.AggregationBuilder;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.BucketOrder;
+import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramAggregationBuilder;
+import org.elasticsearch.search.aggregations.bucket.histogram.DateHistogramInterval;
+import org.elasticsearch.search.aggregations.bucket.range.DateRangeAggregationBuilder;
+import org.elasticsearch.search.aggregations.bucket.range.RangeAggregationBuilder;
+import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
+import org.jetlinks.community.elastic.search.aggreation.bucket.AggregationResponseHandle;
+import org.jetlinks.community.elastic.search.aggreation.bucket.Bucket;
+import org.jetlinks.community.elastic.search.aggreation.bucket.BucketAggregationsStructure;
+import org.jetlinks.community.elastic.search.aggreation.bucket.Sort;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsAggregationStructure;
+import org.joda.time.DateTimeZone;
+import org.springframework.util.StringUtils;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@AllArgsConstructor
+public enum BucketType {
+
+
+    TERMS("字段项") {
+        @Override
+        public AggregationBuilder aggregationBuilder(BucketAggregationsStructure structure) {
+            TermsAggregationBuilder builder = AggregationBuilders
+                    .terms(structure.getName())
+                    .field(structure.getField());
+            if (structure.getSize() != null) {
+                builder.size(structure.getSize());
+            }
+            Sort sort = structure.getSort();
+            if (sort != null) {
+                builder.order(mapping.get(OrderBuilder.of(sort.getOrder(), sort.getType())));
+            }
+            if (structure.getMissingValue() != null) {
+                builder.missing(structure.getMissingValue());
+            }
+            commonAggregationSetting(builder, structure);
+            return builder;
+        }
+
+        @Override
+        public <A extends Aggregation> List<Bucket> convert(A a) {
+            return AggregationResponseHandle.terms(a);
+        }
+    },
+    RANGE("范围") {
+        @Override
+        public AggregationBuilder aggregationBuilder(BucketAggregationsStructure structure) {
+            RangeAggregationBuilder builder = AggregationBuilders
+                    .range(structure.getName())
+                    .field(structure.getField());
+            if (structure.getMissingValue() != null) {
+                builder.missing(structure.getMissingValue());
+            }
+            structure.getRanges()
+                    .forEach(ranges -> {
+                        builder.addRange(ranges.getKey(), (Double) ranges.getForm(), (Double) ranges.getTo());
+                    });
+            commonAggregationSetting(builder, structure);
+            return builder;
+        }
+
+        @Override
+        public <A extends Aggregation> List<Bucket> convert(A a) {
+            return AggregationResponseHandle.range(a);
+        }
+    },
+    DATE_RANGE("时间范围") {
+        @Override
+        public AggregationBuilder aggregationBuilder(BucketAggregationsStructure structure) {
+            DateRangeAggregationBuilder builder = AggregationBuilders
+                    .dateRange(structure.getName())
+                    .field(structure.getField());
+            if (StringUtils.hasText(structure.getFormat())) {
+                builder.format(structure.getFormat());
+            }
+            structure.getRanges()
+                    .forEach(ranges -> {
+                        builder.addRange(ranges.getKey(), ranges.getForm().toString(), ranges.getTo().toString());
+                    });
+            if (structure.getMissingValue() != null) {
+                builder.missing(structure.getMissingValue());
+            }
+            builder.timeZone(DateTimeZone.getDefault());
+            commonAggregationSetting(builder, structure);
+            return builder;
+        }
+
+        @Override
+        public <A extends Aggregation> List<Bucket> convert(A a) {
+            return AggregationResponseHandle.range(a);
+        }
+    },
+    DATE_HISTOGRAM("时间范围") {
+        @Override
+        public AggregationBuilder aggregationBuilder(BucketAggregationsStructure structure) {
+            DateHistogramAggregationBuilder builder = AggregationBuilders
+                    .dateHistogram(structure.getName())
+                    .field(structure.getField());
+            if (StringUtils.hasText(structure.getFormat())) {
+                builder.format(structure.getFormat());
+            }
+            if (StringUtils.hasText(structure.getInterval())) {
+                builder.dateHistogramInterval(new DateHistogramInterval(structure.getInterval()));
+            }
+            if (structure.getMissingValue() != null) {
+                builder.missing(structure.getMissingValue());
+            }
+            builder.timeZone(DateTimeZone.getDefault());
+            commonAggregationSetting(builder, structure);
+            return builder;
+        }
+
+        @Override
+        public <A extends Aggregation> List<Bucket> convert(A a) {
+            return AggregationResponseHandle.dateHistogram(a);
+        }
+    };
+
+    private String text;
+
+    public abstract AggregationBuilder aggregationBuilder(BucketAggregationsStructure structure);
+
+    public abstract <A extends Aggregation> List<Bucket> convert(A a);
+
+    private static void commonAggregationSetting(AggregationBuilder builder, BucketAggregationsStructure structure) {
+        if (structure.getSubMetricsAggregation() != null && structure.getSubMetricsAggregation().size() > 0) {
+            addMetricsSubAggregation(builder, structure.getSubMetricsAggregation());
+        }
+        if (structure.getSubBucketAggregation() != null && structure.getSubBucketAggregation().size() > 0) {
+            addBucketSubAggregation(builder, structure.getSubBucketAggregation());
+        }
+    }
+
+    private static void addMetricsSubAggregation(AggregationBuilder builder, List<MetricsAggregationStructure> subMetricsAggregation) {
+        subMetricsAggregation
+                .forEach(subStructure -> {
+                    builder.subAggregation(subStructure.getType().aggregationBuilder(subStructure.getName(), subStructure.getField()));
+                });
+    }
+
+    private static void addBucketSubAggregation(AggregationBuilder builder, List<BucketAggregationsStructure> subBucketAggregation) {
+        subBucketAggregation
+                .forEach(subStructure -> {
+                    builder.subAggregation(subStructure.getType().aggregationBuilder(subStructure));
+                });
+    }
+
+    @Getter
+    @AllArgsConstructor(staticName = "of")
+    @EqualsAndHashCode
+    static class OrderBuilder {
+
+        private String order;
+
+        private OrderType orderType;
+    }
+
+    static Map<OrderBuilder, BucketOrder> mapping = new HashMap<>();
+
+    static {
+        mapping.put(OrderBuilder.of("asc", OrderType.COUNT), BucketOrder.count(true));
+        mapping.put(OrderBuilder.of("desc", OrderType.COUNT), BucketOrder.count(false));
+        mapping.put(OrderBuilder.of("asc", OrderType.KEY), BucketOrder.key(true));
+        mapping.put(OrderBuilder.of("desc", OrderType.KEY), BucketOrder.count(false));
+    }
+}

+ 132 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/enums/MetricsType.java

@@ -0,0 +1,132 @@
+package org.jetlinks.community.elastic.search.aggreation.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.search.aggregations.AggregationBuilder;
+import org.elasticsearch.search.aggregations.AggregationBuilders;
+import org.elasticsearch.search.aggregations.metrics.avg.Avg;
+import org.elasticsearch.search.aggregations.metrics.max.Max;
+import org.elasticsearch.search.aggregations.metrics.min.Min;
+import org.elasticsearch.search.aggregations.metrics.stats.Stats;
+import org.elasticsearch.search.aggregations.metrics.sum.Sum;
+import org.elasticsearch.search.aggregations.metrics.valuecount.ValueCount;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponse;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponseSingleValue;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@AllArgsConstructor
+public enum MetricsType {
+
+    AVG("平均") {
+        @Override
+        public AggregationBuilder aggregationBuilder(String name, String filed) {
+            return AggregationBuilders.avg(name).field(filed);
+        }
+
+        @Override
+        public MetricsResponse getResponse(String name, SearchResponse response) {
+            Avg avg = response.getAggregations().get(name);
+            return MetricsResponse.builder()
+                    .results(Collections.singletonMap(AVG,
+                            new MetricsResponseSingleValue(avg.getValue(), avg.getValueAsString())))
+                    .build();
+        }
+    },
+    MAX("最大") {
+        @Override
+        public AggregationBuilder aggregationBuilder(String name, String filed) {
+            return AggregationBuilders.max(name).field(filed);
+        }
+
+        @Override
+        public MetricsResponse getResponse(String name, SearchResponse response) {
+            Max max = response.getAggregations().get(name);
+            return MetricsResponse.builder()
+                    .results(Collections.singletonMap(MAX,
+                            new MetricsResponseSingleValue(max.getValue(), max.getValueAsString())))
+                    .build();
+        }
+    },
+    VALUE_COUNT("非空值计数") {
+        @Override
+        public AggregationBuilder aggregationBuilder(String name, String filed) {
+            return AggregationBuilders.count(name).field(filed);
+        }
+
+        @Override
+        public MetricsResponse getResponse(String name, SearchResponse response) {
+            ValueCount valueCount = response.getAggregations().get(name);
+            return MetricsResponse.builder()
+                    .results(Collections.singletonMap(VALUE_COUNT,
+                            new MetricsResponseSingleValue(valueCount.getValue(), valueCount.getValueAsString())))
+                    .build();
+        }
+    },
+    MIN("最小") {
+        @Override
+        public AggregationBuilder aggregationBuilder(String name, String filed) {
+            return AggregationBuilders.min(name).field(filed);
+        }
+
+        @Override
+        public MetricsResponse getResponse(String name, SearchResponse response) {
+            Min min = response.getAggregations().get(name);
+            return MetricsResponse.builder()
+                    .results(Collections.singletonMap(MIN,
+                            new MetricsResponseSingleValue(min.getValue(), min.getValueAsString())))
+                    .build();
+        }
+    },
+    SUM("总数") {
+        @Override
+        public AggregationBuilder aggregationBuilder(String name, String filed) {
+            return AggregationBuilders.sum(name).field(filed);
+        }
+
+        @Override
+        public MetricsResponse getResponse(String name, SearchResponse response) {
+            Sum sum = response.getAggregations().get(name);
+            return MetricsResponse.builder()
+                    .results(Collections.singletonMap(SUM,
+                            new MetricsResponseSingleValue(sum.getValue(), sum.getValueAsString())))
+                    .build();
+        }
+    },
+    STATS("统计汇总") {
+        @Override
+        public AggregationBuilder aggregationBuilder(String name, String filed) {
+            return AggregationBuilders.stats(name).field(filed);
+        }
+
+        @Override
+        public MetricsResponse getResponse(String name, SearchResponse response) {
+            Stats stats = response.getAggregations().get(name);
+            Map<MetricsType, MetricsResponseSingleValue> results = new HashMap<>();
+            results.put(AVG, new MetricsResponseSingleValue(stats.getAvg(), stats.getAvgAsString()));
+            results.put(MIN, new MetricsResponseSingleValue(stats.getMin(), stats.getMinAsString()));
+            results.put(MAX, new MetricsResponseSingleValue(stats.getMax(), stats.getMaxAsString()));
+            results.put(SUM, new MetricsResponseSingleValue(stats.getSum(), stats.getMaxAsString()));
+            results.put(VALUE_COUNT, new MetricsResponseSingleValue(stats.getCount(), String.valueOf(stats.getCount())));
+            return MetricsResponse.builder()
+                    .results(results)
+                    .build();
+        }
+    };
+
+    private String text;
+
+
+    public abstract AggregationBuilder aggregationBuilder(String name, String filed);
+
+    public abstract MetricsResponse getResponse(String name, SearchResponse response);
+
+}

+ 18 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/enums/OrderType.java

@@ -0,0 +1,18 @@
+package org.jetlinks.community.elastic.search.aggreation.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@AllArgsConstructor
+@Getter
+public enum OrderType {
+
+    COUNT("计数"),
+    KEY("分组值");
+
+    private String text;
+}

+ 37 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/metrics/MetricsAggregationStructure.java

@@ -0,0 +1,37 @@
+package org.jetlinks.community.elastic.search.aggreation.metrics;
+
+import lombok.*;
+import org.hswebframework.utils.StringUtils;
+import org.jetlinks.community.elastic.search.aggreation.enums.MetricsType;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Setter
+@Getter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class MetricsAggregationStructure {
+
+    @NonNull
+    private String field;
+
+    private String name;
+
+    @NonNull
+    private MetricsType type = MetricsType.VALUE_COUNT;
+
+    /**
+     * 缺失值
+     */
+    private Object missingValue;
+
+    public String getName() {
+        if (StringUtils.isNullOrEmpty(name)) {
+            name = type.name().concat("_").concat(field);
+        }
+        return name;
+    }
+}

+ 33 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/metrics/MetricsResponse.java

@@ -0,0 +1,33 @@
+package org.jetlinks.community.elastic.search.aggreation.metrics;
+
+import lombok.*;
+import org.jetlinks.community.elastic.search.aggreation.enums.MetricsType;
+
+import java.util.Map;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class MetricsResponse {
+
+    private Map<MetricsType, MetricsResponseSingleValue> results;
+
+    private MetricsResponseSingleValue singleResult;
+
+    public MetricsResponseSingleValue getSingleResult() {
+        if (singleResult == null) {
+            this.singleResult = results.entrySet()
+                    .stream()
+                    .findFirst()
+                    .map(Map.Entry::getValue)
+                    .orElse(MetricsResponseSingleValue.empty());
+        }
+        return singleResult;
+    }
+}

+ 45 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/aggreation/metrics/MetricsResponseSingleValue.java

@@ -0,0 +1,45 @@
+package org.jetlinks.community.elastic.search.aggreation.metrics;
+
+import lombok.*;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+
+@Setter
+@Getter
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class MetricsResponseSingleValue {
+
+    private double value;
+
+    private String valueAsString;
+
+    public static MetricsResponseSingleValue empty() {
+        return new MetricsResponseSingleValue();
+    }
+
+//    public double getValue() {
+//        if (isDoubleOrFloat(String.valueOf(value))) {
+//            BigDecimal b = BigDecimal.valueOf(value);
+//            return b.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
+//        } else {
+//            return 0;
+//        }
+//    }
+//
+//    public static boolean isDoubleOrFloat(String str) {
+//        if (str.length() > 15) {
+//            str = str.substring(0, 15);
+//        }
+//        Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
+//        return pattern.matcher(str).matches();
+//    }
+//
+//    public static void main(String[] args) {
+//        System.out.println(new BigDecimal("8.839927333333333E7"));
+//    }
+}

+ 35 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchConfiguration.java

@@ -0,0 +1,35 @@
+package org.jetlinks.community.elastic.search.configuration;
+
+import lombok.extern.slf4j.Slf4j;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.jetlinks.community.elastic.search.ElasticRestClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Configuration
+@Slf4j
+@EnableConfigurationProperties(ElasticSearchProperties.class)
+public class ElasticSearchConfiguration {
+
+    @Autowired
+    private ElasticSearchProperties properties;
+
+
+    @Bean
+    public ElasticRestClient elasticRestClient() {
+        RestHighLevelClient queryClient = new RestHighLevelClient(RestClient.builder(properties.createHosts())
+                .setRequestConfigCallback(properties::applyRequestConfigBuilder)
+                .setHttpClientConfigCallback(properties::applyHttpAsyncClientBuilder));
+        RestHighLevelClient writeClient = new RestHighLevelClient(RestClient.builder(properties.createHosts())
+                .setRequestConfigCallback(properties::applyRequestConfigBuilder)
+                .setHttpClientConfigCallback(properties::applyHttpAsyncClientBuilder));
+        return new ElasticRestClient(queryClient, writeClient);
+    }
+}

+ 51 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/configuration/ElasticSearchProperties.java

@@ -0,0 +1,51 @@
+package org.jetlinks.community.elastic.search.configuration;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.http.HttpHost;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+import java.util.List;
+
+@ConfigurationProperties(prefix = "elasticsearch.client")
+
+@Getter
+@Setter
+public class ElasticSearchProperties {
+
+    private String host = "localhost";
+    private int port = 9200;
+
+    private int connectionRequestTimeout = 5000;
+    private int connectTimeout = 2000;
+    private int socketTimeout = 2000;
+    private int maxConnTotal = 30;
+    private List<String> hosts;
+
+
+    public HttpHost[] createHosts() {
+        if (CollectionUtils.isEmpty(hosts)) {
+            return new HttpHost[]{new HttpHost(host, port, "http")};
+        }
+
+        return hosts.stream().map(HttpHost::create).toArray(HttpHost[]::new);
+    }
+
+    public RequestConfig.Builder applyRequestConfigBuilder(RequestConfig.Builder builder) {
+
+        builder.setConnectTimeout(connectTimeout);
+        builder.setConnectionRequestTimeout(connectionRequestTimeout);
+        builder.setSocketTimeout(socketTimeout);
+
+        return builder;
+    }
+
+    public HttpAsyncClientBuilder applyHttpAsyncClientBuilder(HttpAsyncClientBuilder builder) {
+        builder.setMaxConnTotal(maxConnTotal);
+
+        return builder;
+    }
+}

+ 41 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/enums/FieldDateFormat.java

@@ -0,0 +1,41 @@
+package org.jetlinks.community.elastic.search.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.hswebframework.web.dict.EnumDict;
+
+import java.util.List;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ * Values based on reference doc - https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html
+ **/
+@Getter
+@AllArgsConstructor
+public enum FieldDateFormat implements EnumDict<String> {
+
+    epoch_millis("epoch_millis", "毫秒"),
+    epoch_second("epoch_second", "秒"),
+    strict_date("strict_date", "yyyy-MM-dd"),
+    basic_date_time("basic_date_time", "yyyyMMdd'T'HHmmss.SSSZ"),
+    strict_date_time("strict_date_time", "yyyy-MM-dd'T'HH:mm:ss.SSSZZ"),
+    strict_date_hour_minute_second("strict_date_hour_minute_second", "yyyy-MM-dd'T'HH:mm:ss"),
+    strict_hour_minute_second("strict_hour_minute_second", "HH:mm:ss"),
+    simple_date("yyyy-MM-dd HH:mm:ss", "通用格式");
+
+    private String value;
+
+    private String text;
+
+    public static String getFormatStr(List<FieldDateFormat> dateFormats) {
+        StringBuffer format = new StringBuffer();
+        for (int i = 0; i < dateFormats.size(); i++) {
+            format.append(dateFormats.get(i).getValue());
+            if (i != dateFormats.size() - 1) {
+                format.append("||");
+            }
+        }
+        return format.toString();
+    }
+}

+ 48 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/enums/FieldType.java

@@ -0,0 +1,48 @@
+package org.jetlinks.community.elastic.search.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.hswebframework.web.dict.EnumDict;
+import org.hswebframework.web.exception.NotFoundException;
+import org.springframework.util.StringUtils;
+
+@AllArgsConstructor
+
+public enum FieldType implements EnumDict<String> {
+
+    TEXT("text"),
+    BYTE("byte"),
+    SHORT("short"),
+    INTEGER("integer"),
+    LONG("long"),
+    DATE("date"),
+    HALF_FLOAT("half_float"),
+    FLOAT("float"),
+    DOUBLE("double"),
+    BOOLEAN("boolean"),
+    OBJECT("object"),
+    AUTO("auto"),
+    NESTED("nested"),
+    IP("ip"),
+    ATTACHMENT("attachment"),
+    KEYWORD("keyword");
+
+    @Override
+    public String getText() {
+        return value;
+    }
+
+    @Getter
+    private String value;
+
+    public static FieldType of(Object value) {
+        if (!StringUtils.isEmpty(value)) {
+            for (FieldType fieldType : FieldType.values()) {
+                if (fieldType.getValue().equals(value)) {
+                    return fieldType;
+                }
+            }
+        }
+        throw new NotFoundException("未找到数据类型为:" + value + "的枚举");
+    }
+}

+ 69 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/enums/LinkTypeEnum.java

@@ -0,0 +1,69 @@
+package org.jetlinks.community.elastic.search.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.hswebframework.ezorm.core.param.Term;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Optional;
+
+/**
+ * @author Jia_RG
+ */
+@Getter
+@AllArgsConstructor
+public enum LinkTypeEnum {
+    and("and") {
+        @Override
+        public void process(BoolQueryBuilder query, Term term) {
+            if (term.getTerms().isEmpty()) {
+                query.must(TermTypeEnum.of(term.getTermType().trim()).map(e -> e.process(term)).orElse(QueryBuilders.boolQuery()));
+            } else {
+                // 嵌套查询新建一个包起来
+                BoolQueryBuilder nextQuery = QueryBuilders.boolQuery();
+                LinkedList<Term> terms = ((LinkedList<Term>) term.getTerms());
+                // 同一层级取最后一个的type
+                LinkTypeEnum.of(getLast(terms).getType().name()).ifPresent(e -> terms.forEach(t -> e.process(nextQuery, t)));
+                // 处理完后包括进去
+                query.must(nextQuery);
+            }
+        }
+    },
+    or("or") {
+        @Override
+        public void process(BoolQueryBuilder query, Term term) {
+            // 跟上面代码相似
+            if (term.getTerms().isEmpty()) {
+                query.should(TermTypeEnum.of(term.getTermType().trim()).map(e -> e.process(term)).orElse(QueryBuilders.boolQuery()));
+            } else {
+                BoolQueryBuilder nextQuery = QueryBuilders.boolQuery();
+                LinkedList<Term> terms = ((LinkedList<Term>) term.getTerms());
+                LinkTypeEnum.of(getLast(terms).getType().name()).ifPresent(e -> terms.forEach(t -> e.process(nextQuery, t)));
+                query.should(nextQuery);
+            }
+        }
+    };
+
+    private final String type;
+
+    public abstract void process(BoolQueryBuilder query, Term term);
+
+
+    public static Optional<LinkTypeEnum> of(String type) {
+        return Arrays.stream(values())
+                .filter(e -> e.getType().equalsIgnoreCase(type))
+                .findAny();
+    }
+
+    private static Term getLast(LinkedList<Term> terms) {
+        int index = terms.indexOf(terms.getLast());
+        while (index >= 0) {
+            if (terms.get(index).getTerms().isEmpty()) break;
+            index--;
+        }
+        return terms.get(index);
+    }
+}

+ 109 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/enums/TermTypeEnum.java

@@ -0,0 +1,109 @@
+package org.jetlinks.community.elastic.search.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.hswebframework.ezorm.core.param.Term;
+import org.jetlinks.community.elastic.search.utils.TermCommonUtils;
+import org.springframework.util.StringUtils;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Jia_RG
+ * @author bestfeng
+ */
+@Getter
+@AllArgsConstructor
+public enum TermTypeEnum {
+    eq("eq") {
+        @Override
+        public QueryBuilder process(Term term) {
+            return QueryBuilders.termQuery(term.getColumn().trim(), term.getValue());
+        }
+    },
+    not("not") {
+        @Override
+        public QueryBuilder process(Term term) {
+            return QueryBuilders.boolQuery().mustNot(QueryBuilders.termQuery(term.getColumn().trim(), term.getValue()));
+        }
+    },
+    btw("btw") {
+        @Override
+        public QueryBuilder process(Term term) {
+            Object between = null;
+            Object and = null;
+            List values = TermCommonUtils.convertToList(term.getValue());
+            if (values.size() > 0) {
+                between = values.get(0);
+            }
+            if (values.size() > 1) {
+                and = values.get(1);
+            }
+            return QueryBuilders.rangeQuery(term.getColumn().trim()).gte(between).lte(and);
+        }
+    },
+    gt("gt") {
+        @Override
+        public QueryBuilder process(Term term) {
+            return QueryBuilders.rangeQuery(term.getColumn().trim()).gt(term.getValue());
+        }
+    },
+    gte("gte") {
+        @Override
+        public QueryBuilder process(Term term) {
+            return QueryBuilders.rangeQuery(term.getColumn().trim()).gte(term.getValue());
+        }
+    },
+    lt("lt") {
+        @Override
+        public QueryBuilder process(Term term) {
+            return QueryBuilders.rangeQuery(term.getColumn().trim()).lt(term.getValue());
+        }
+    },
+    lte("lte") {
+        @Override
+        public QueryBuilder process(Term term) {
+            return QueryBuilders.rangeQuery(term.getColumn().trim()).lte(term.getValue());
+        }
+    },
+    in("in") {
+        @Override
+        public QueryBuilder process(Term term) {
+            return QueryBuilders.termsQuery(term.getColumn().trim(), TermCommonUtils.convertToList(term.getValue()));
+        }
+    },
+    like("like") {
+        @Override
+        public QueryBuilder process(Term term) {
+            //return QueryBuilders.matchPhraseQuery(term.getColumn().trim(), term.getValue());
+            return QueryBuilders.wildcardQuery(term.getColumn().trim(), likeQueryTermValueHandler(term.getValue()));
+        }
+    },
+    nlike("nlike") {
+        @Override
+        public QueryBuilder process(Term term) {
+            return QueryBuilders.boolQuery().mustNot(QueryBuilders.wildcardQuery(term.getColumn().trim(), likeQueryTermValueHandler(term.getValue())));
+        }
+    };
+
+    private final String type;
+
+    public abstract QueryBuilder process(Term term);
+
+    public static String likeQueryTermValueHandler(Object value) {
+        if (!StringUtils.isEmpty(value)) {
+            return value.toString().replace("%", "*");
+        }
+        return "**";
+    }
+
+    public static Optional<TermTypeEnum> of(String type) {
+        return Arrays.stream(values())
+                .filter(e -> e.getType().equalsIgnoreCase(type))
+                .findAny();
+    }
+}

+ 68 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/CreateIndex.java

@@ -0,0 +1,68 @@
+package org.jetlinks.community.elastic.search.index;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.elasticsearch.client.indices.CreateIndexRequest;
+import org.elasticsearch.common.settings.Settings;
+import org.jetlinks.community.elastic.search.index.mapping.MappingFactory;
+import org.jetlinks.community.elastic.search.index.setting.SettingFactory;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+
+public class CreateIndex {
+
+    @Getter
+    @Setter
+    private Map<String, Object> mapping;
+
+    @Getter
+    @Setter
+    private Settings.Builder settings;
+
+    private String index;
+
+    @Deprecated
+    private String type;
+
+    public CreateIndex addIndex(String index) {
+        this.index = index;
+        return this;
+    }
+
+    @Deprecated
+    public CreateIndex addType(String type) {
+        this.type = type;
+        return this;
+    }
+
+    public MappingFactory createMapping() {
+        return MappingFactory.createInstance(this);
+    }
+
+    public SettingFactory createSettings() {
+        return SettingFactory.createInstance(this);
+    }
+
+
+    public CreateIndexRequest createIndexRequest() {
+        CreateIndexRequest request = new CreateIndexRequest(index);
+        request.mapping(Collections.singletonMap("properties", getMapping()));
+        if (settings != null) {
+            request.settings(settings);
+        }
+        return request;
+    }
+
+    private CreateIndex() {
+    }
+
+    public static CreateIndex createInstance() {
+        return new CreateIndex();
+    }
+}

+ 128 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/DefaultIndexOperationService.java

@@ -0,0 +1,128 @@
+package org.jetlinks.community.elastic.search.index;
+
+import lombok.extern.slf4j.Slf4j;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.indices.*;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.community.elastic.search.ElasticRestClient;
+import org.jetlinks.community.elastic.search.enums.FieldType;
+import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
+import org.jetlinks.community.elastic.search.index.mapping.IndicesMappingCenter;
+import org.jetlinks.community.elastic.search.index.mapping.SingleMappingMetadata;
+import org.jetlinks.community.elastic.search.service.IndexOperationService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Mono;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Service
+@Slf4j
+public class DefaultIndexOperationService implements IndexOperationService {
+
+    private final ElasticRestClient restClient;
+
+    private final IndicesMappingCenter indicesMappingCenter;
+
+    @Autowired
+    public DefaultIndexOperationService(ElasticRestClient restClient, IndicesMappingCenter indicesMappingCenter) {
+        this.restClient = restClient;
+        this.indicesMappingCenter = indicesMappingCenter;
+    }
+
+
+    @Override
+    public Mono<Boolean> indexIsExists(String index) {
+        return Mono.create(sink -> {
+            try {
+                GetIndexRequest request = new GetIndexRequest(index);
+                sink.success(restClient.getQueryClient().indices().exists(request, RequestOptions.DEFAULT));
+            } catch (Exception e) {
+                log.error("查询es index 是否存在失败", e);
+                sink.error(e);
+            }
+        });
+    }
+
+    @Override
+    public Mono<Boolean> init(CreateIndexRequest request) {
+        return indexIsExists(request.index())
+                .filter(bool -> !bool)
+                .flatMap(b -> Mono.create(sink -> {
+                    restClient.getQueryClient().indices().createAsync(request, RequestOptions.DEFAULT, new ActionListener<CreateIndexResponse>() {
+                        @Override
+                        public void onResponse(CreateIndexResponse createIndexResponse) {
+                            sink.success(createIndexResponse.isAcknowledged());
+                        }
+
+                        @Override
+                        public void onFailure(Exception e) {
+                            sink.error(e);
+                        }
+                    });
+                }));
+
+    }
+
+
+    @Override
+    public Mono<IndexMappingMetadata> getIndexMappingMetadata(String index) {
+        return indicesMappingCenter.getIndexMappingMetaData(index)
+                .map(Mono::just).orElseGet(() ->
+                        getIndexMapping(index)
+                                .doOnNext(indicesMappingCenter::register));
+    }
+
+    private Mono<IndexMappingMetadata> getIndexMapping(String index) {
+        return indexIsExists(index)
+                .filter(Boolean::booleanValue)
+                .flatMap(bool -> Mono.create(sink -> {
+                    if (bool) {
+                        GetMappingsRequest mappingsRequest = new GetMappingsRequest();
+                        mappingsRequest.indices(index);
+                        restClient.getQueryClient().indices().getMappingAsync(mappingsRequest, RequestOptions.DEFAULT, new ActionListener<GetMappingsResponse>() {
+                            @Override
+                            public void onResponse(GetMappingsResponse getMappingsResponse) {
+                                //index存在时 getMappingsResponse.mappings().get(index).getSourceAsMap().get("properties") 不会为空
+                                sink.success(fieldMappingConvert(null, IndexMappingMetadata.getInstance(index), getMappingsResponse.mappings().get(index).getSourceAsMap().get("properties")));
+                            }
+
+                            @Override
+                            public void onFailure(Exception e) {
+                                sink.error(e);
+                            }
+                        });
+                    }
+                }));
+
+    }
+
+    private IndexMappingMetadata fieldMappingConvert(String baseKey, IndexMappingMetadata indexMappingMetaData, Object properties) {
+        FastBeanCopier.copy(properties, new HashMap<String, Object>())
+                .forEach((key, value) -> {
+                    if (StringUtils.hasText(baseKey)) {
+                        key = baseKey.concat(".").concat(key);
+                    }
+                    if (value instanceof Map) {
+                        Map tempValue = FastBeanCopier.copy(value, new HashMap<>());
+                        Object childProperties = tempValue.get("properties");
+                        if (childProperties != null) {
+                            fieldMappingConvert(key, indexMappingMetaData, childProperties);
+                            return;
+                        }
+                        indexMappingMetaData.setMetadata(SingleMappingMetadata.builder()
+                                .name(key)
+                                .type(FieldType.of(tempValue.get("type")))
+                                .build());
+                    }
+                });
+        return indexMappingMetaData;
+    }
+}

+ 37 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/ElasticIndex.java

@@ -0,0 +1,37 @@
+package org.jetlinks.community.elastic.search.index;
+
+import java.util.function.Supplier;
+
+/**
+ * @version 1.0
+ **/
+public interface ElasticIndex {
+
+    @Deprecated
+    String getIndex();
+
+    @Deprecated
+    String getType();
+
+    default String getStandardIndex(){
+        return getIndex().toLowerCase();
+    }
+
+    default String getStandardType(){
+        return getType().toLowerCase();
+    }
+
+    static ElasticIndex createDefaultIndex(Supplier<String> indexConsumer, Supplier<String> typeConsumer) {
+        return new ElasticIndex() {
+            @Override
+            public String getIndex() {
+                return indexConsumer.get();
+            }
+
+            @Override
+            public String getType() {
+                return typeConsumer.get();
+            }
+        };
+    }
+}

+ 64 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/mapping/IndexMappingMetadata.java

@@ -0,0 +1,64 @@
+package org.jetlinks.community.elastic.search.index.mapping;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.community.elastic.search.enums.FieldType;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+public class IndexMappingMetadata {
+
+    private String index;
+
+    private Map<String, SingleMappingMetadata> metadata = new HashMap<>();
+
+    public List<SingleMappingMetadata> getAllMetaData() {
+        return metadata.entrySet()
+                .stream()
+                .map(Map.Entry::getValue)
+                .collect(Collectors.toList());
+    }
+
+    public SingleMappingMetadata getMetaData(String fieldName) {
+        return metadata.get(fieldName);
+    }
+
+    public List<SingleMappingMetadata> getMetaDataByType(FieldType type) {
+        return getAllMetaData()
+                .stream()
+                .filter(singleMapping -> singleMapping.getType().equals(type))
+                .collect(Collectors.toList());
+    }
+
+    public Map<String, SingleMappingMetadata> getMetaDataByTypeToMap(FieldType type) {
+        return getMetaDataByType(type)
+                .stream()
+                .collect(Collectors.toMap(SingleMappingMetadata::getName, Function.identity()));
+    }
+
+    public void setMetadata(SingleMappingMetadata singleMapping) {
+        metadata.put(singleMapping.getName(), singleMapping);
+    }
+
+
+    private IndexMappingMetadata(String index) {
+        this.index = index;
+    }
+
+    private IndexMappingMetadata() {
+    }
+
+    public static IndexMappingMetadata getInstance(String index) {
+        return new IndexMappingMetadata(index);
+    }
+}

+ 26 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/mapping/IndicesMappingCenter.java

@@ -0,0 +1,26 @@
+package org.jetlinks.community.elastic.search.index.mapping;
+
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Component
+public class IndicesMappingCenter {
+
+    private Map<String, IndexMappingMetadata> indicesMapping = new ConcurrentHashMap<>();
+
+
+    public Optional<IndexMappingMetadata> getIndexMappingMetaData(String index) {
+        return Optional.ofNullable(indicesMapping.get(index));
+    }
+
+    public void register(IndexMappingMetadata mappingMetaData) {
+        indicesMapping.put(mappingMetaData.getIndex(), mappingMetaData);
+    }
+}

+ 71 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/mapping/MappingFactory.java

@@ -0,0 +1,71 @@
+package org.jetlinks.community.elastic.search.index.mapping;
+
+import org.hswebframework.web.exception.BusinessException;
+import org.jetlinks.community.elastic.search.enums.FieldType;
+import org.jetlinks.community.elastic.search.enums.FieldDateFormat;
+import org.jetlinks.community.elastic.search.index.CreateIndex;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public class MappingFactory {
+
+
+    private Map<String, Object> properties = new HashMap<>();
+
+    private Map<String, Object> filedMap = new HashMap<>();
+
+    private volatile boolean flag = true;
+
+    private CreateIndex index;
+
+    private MappingFactory(CreateIndex index) {
+        this.index = index;
+    }
+
+    public MappingFactory addFieldName(String fieldName) {
+        continuityOperateHandle(!flag);
+        flag = false;
+        filedMap = new HashMap<>();
+        properties.put(fieldName, filedMap);
+        return this;
+    }
+
+    public MappingFactory addFieldType(FieldType type) {
+        continuityOperateHandle(flag);
+        filedMap.put("type", type.getValue());
+        return this;
+    }
+
+    public MappingFactory addFieldDateFormat(FieldDateFormat... dateFormats) {
+        continuityOperateHandle(flag);
+        filedMap.put("format", FieldDateFormat.getFormatStr(Arrays.asList(dateFormats)));
+        return this;
+    }
+
+    public MappingFactory commit() {
+        flag = true;
+        return this;
+    }
+
+    public CreateIndex end() {
+        index.setMapping(properties);
+        return index;
+    }
+
+    public static MappingFactory createInstance(CreateIndex index) {
+        return new MappingFactory(index);
+    }
+
+    private void continuityOperateHandle(boolean inoperable) {
+        if (inoperable) {
+            throw new BusinessException("please exec commit() or addFiledName() later then operate");
+        }
+    }
+
+}

+ 23 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/mapping/SingleMappingMetadata.java

@@ -0,0 +1,23 @@
+package org.jetlinks.community.elastic.search.index.mapping;
+
+import lombok.*;
+import org.jetlinks.community.elastic.search.enums.FieldType;
+import org.jetlinks.community.elastic.search.enums.FieldDateFormat;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class SingleMappingMetadata {
+
+    private String name;
+
+    private FieldDateFormat format;
+
+    private FieldType type;
+}

+ 40 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/index/setting/SettingFactory.java

@@ -0,0 +1,40 @@
+package org.jetlinks.community.elastic.search.index.setting;
+
+import org.elasticsearch.common.settings.Settings;
+import org.jetlinks.community.elastic.search.index.CreateIndex;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public class SettingFactory {
+
+    private Settings.Builder settings = Settings.builder();
+
+    private CreateIndex index;
+
+    private SettingFactory(CreateIndex index) {
+        this.index = index;
+    }
+
+    public SettingFactory settingShards(Integer shards) {
+        settings.put("number_of_shards", shards);
+        return this;
+    }
+
+    public SettingFactory settingReplicas(Integer replicas) {
+        settings.put("number_of_replicas", replicas);
+        return this;
+    }
+
+    public CreateIndex end() {
+        index.setSettings(settings);
+        return index;
+    }
+
+    public static SettingFactory createInstance(CreateIndex index) {
+        return new SettingFactory(index);
+    }
+
+
+}

+ 35 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/AbstractQueryParamTranslateService.java

@@ -0,0 +1,35 @@
+package org.jetlinks.community.elastic.search.parser;
+
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.search.sort.SortOrder;
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public abstract class AbstractQueryParamTranslateService implements QueryParamTranslateService {
+
+    @Override
+    public SearchSourceBuilder translate(QueryParam queryParam, IndexMappingMetadata mappingMetaData) {
+        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
+        if (queryParam.isPaging()) {
+            sourceBuilder.from(queryParam.getPageIndex() * queryParam.getPageSize());
+            sourceBuilder.size(queryParam.getPageSize());
+        }
+        queryParam.getSorts()
+                .forEach(sort -> {
+                    if (!StringUtils.isEmpty(sort.getName())) {
+                        sourceBuilder.sort(sort.getName(), SortOrder.fromString(sort.getOrder()));
+                    }
+
+                });
+        return sourceBuilder.query(queryBuilder(queryParam, mappingMetaData));
+    }
+
+    protected abstract QueryBuilder queryBuilder(QueryParam queryParam, IndexMappingMetadata mappingMetaData);
+
+}

+ 64 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/DefaultLinkTypeParser.java

@@ -0,0 +1,64 @@
+package org.jetlinks.community.elastic.search.parser;
+
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.hswebframework.ezorm.core.param.Term;
+import org.springframework.stereotype.Component;
+
+import java.util.LinkedList;
+import java.util.function.Consumer;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Component
+public class DefaultLinkTypeParser implements LinkTypeParser {
+
+    private TermTypeParser parser = new DefaultTermTypeParser();
+
+    @Override
+    public BoolQueryBuilder process(Term term, Consumer<Term> consumer, BoolQueryBuilder queryBuilders) {
+        if ("or".equalsIgnoreCase(term.getType().name())) {
+            handleOr(queryBuilders, term, consumer);
+        } else if ("and".equalsIgnoreCase(term.getType().name())) {
+            handleAnd(queryBuilders, term, consumer);
+        } else {
+            throw new UnsupportedOperationException("不支持的查询连接类型,term.getType:" + term.getType().name());
+        }
+        return queryBuilders;
+    }
+
+    private void handleOr(BoolQueryBuilder queryBuilders, Term term, Consumer<Term> consumer) {
+        consumer.accept(term);
+        if (term.getTerms().isEmpty()) {
+            parser.process(() -> term, queryBuilders::should);
+        } else {
+            BoolQueryBuilder nextQuery = QueryBuilders.boolQuery();
+            LinkedList<Term> terms = ((LinkedList<Term>) term.getTerms());
+            terms.forEach(t -> process(t, consumer, nextQuery));
+            queryBuilders.should(nextQuery);
+        }
+    }
+
+    private void handleAnd(BoolQueryBuilder queryBuilders, Term term, Consumer<Term> consumer) {
+        consumer.accept(term);
+        if (term.getTerms().isEmpty()) {
+            parser.process(() -> term, queryBuilders::must);
+        } else {
+            BoolQueryBuilder nextQuery = QueryBuilders.boolQuery();
+            LinkedList<Term> terms = ((LinkedList<Term>) term.getTerms());
+            terms.forEach(t -> process(t, consumer, nextQuery));
+            queryBuilders.must(nextQuery);
+        }
+    }
+
+    private static Term getLast(LinkedList<Term> terms) {
+        int index = terms.indexOf(terms.getLast());
+        while (index >= 0) {
+            if (terms.get(index).getTerms().isEmpty()) break;
+            index--;
+        }
+        return terms.get(index);
+    }
+}

+ 52 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/DefaultQueryParamTranslateService.java

@@ -0,0 +1,52 @@
+package org.jetlinks.community.elastic.search.parser;
+
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.hswebframework.ezorm.core.param.Term;
+import org.jetlinks.community.elastic.search.enums.FieldType;
+import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
+import org.jetlinks.community.elastic.search.index.mapping.SingleMappingMetadata;
+import org.jetlinks.community.elastic.search.utils.DateTimeUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Component
+public class DefaultQueryParamTranslateService extends AbstractQueryParamTranslateService {
+
+
+    private final LinkTypeParser parser;
+
+    @Value("${jetlinks.system.formats:yyyy-MM-dd HH:mm:ss}")
+    private List<String> formats;
+
+    @Autowired
+    public DefaultQueryParamTranslateService(LinkTypeParser parser) {
+        this.parser = parser;
+    }
+
+    @Override
+    protected QueryBuilder  queryBuilder(QueryParam queryParam, IndexMappingMetadata mappingMetaData) {
+        BoolQueryBuilder queryBuilders = QueryBuilders.boolQuery();
+        queryParam.getTerms()
+                .forEach(term -> parser.process(term, t ->
+                        dateTypeHandle(t, mappingMetaData.getMetaData(t.getColumn())), queryBuilders));
+        return queryBuilders;
+    }
+
+
+    private void dateTypeHandle(Term term, SingleMappingMetadata singleMappingMetaData) {
+        if (singleMappingMetaData == null) return;
+        if (singleMappingMetaData.getType().equals(FieldType.DATE)) {
+            term.setValue(DateTimeUtils.formatDateToTimestamp(term.getValue(), formats));
+        }
+    }
+}

+ 28 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/DefaultTermTypeParser.java

@@ -0,0 +1,28 @@
+package org.jetlinks.community.elastic.search.parser;
+
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.hswebframework.ezorm.core.param.Term;
+import org.jetlinks.community.elastic.search.enums.TermTypeEnum;
+
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public class DefaultTermTypeParser implements TermTypeParser {
+
+
+    @Override
+    public void process(Supplier<Term> termSupplier, Function<QueryBuilder, BoolQueryBuilder> function) {
+        function.apply(queryBuilder(termSupplier.get()));
+    }
+
+
+    private QueryBuilder queryBuilder(Term term) {
+        return TermTypeEnum.of(term.getTermType().trim()).map(e -> e.process(term)).orElse(QueryBuilders.boolQuery());
+    }
+}

+ 14 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/LinkTypeParser.java

@@ -0,0 +1,14 @@
+package org.jetlinks.community.elastic.search.parser;
+
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.hswebframework.ezorm.core.param.Term;
+
+import java.util.function.Consumer;
+
+/**
+ * @version 1.0
+ **/
+public interface LinkTypeParser {
+
+    BoolQueryBuilder process(Term term, Consumer<Term> consumer, BoolQueryBuilder queryBuilders);
+}

+ 14 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/QueryParamTranslateService.java

@@ -0,0 +1,14 @@
+package org.jetlinks.community.elastic.search.parser;
+
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public interface QueryParamTranslateService {
+
+    SearchSourceBuilder translate(QueryParam queryParam, IndexMappingMetadata metaData);
+}

+ 17 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/parser/TermTypeParser.java

@@ -0,0 +1,17 @@
+package org.jetlinks.community.elastic.search.parser;
+
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.hswebframework.ezorm.core.param.Term;
+
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * @version 1.0
+ **/
+public interface TermTypeParser {
+
+    void process(Supplier<Term> termSupplier, Function<QueryBuilder, BoolQueryBuilder> function);
+
+}

+ 20 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/AggregationService.java

@@ -0,0 +1,20 @@
+package org.jetlinks.community.elastic.search.service;
+
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.jetlinks.community.elastic.search.aggreation.bucket.BucketAggregationsStructure;
+import org.jetlinks.community.elastic.search.aggreation.bucket.BucketResponse;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsAggregationStructure;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponse;
+import org.jetlinks.community.elastic.search.index.ElasticIndex;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public interface AggregationService {
+
+    Mono<MetricsResponse> metricsAggregation(QueryParam queryParam, MetricsAggregationStructure structure, ElasticIndex provider);
+
+    Mono<BucketResponse> bucketAggregation(QueryParam queryParam, BucketAggregationsStructure structure, ElasticIndex provider);
+}

+ 251 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/DefaultElasticSearchService.java

@@ -0,0 +1,251 @@
+package org.jetlinks.community.elastic.search.service;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.bulk.BulkRequest;
+import org.elasticsearch.action.bulk.BulkResponse;
+import org.elasticsearch.action.index.IndexRequest;
+import org.elasticsearch.action.search.SearchRequest;
+import org.elasticsearch.action.search.SearchResponse;
+import org.elasticsearch.client.RequestOptions;
+import org.elasticsearch.client.core.CountRequest;
+import org.elasticsearch.client.core.CountResponse;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.hswebframework.web.api.crud.entity.PagerResult;
+import org.jetlinks.community.elastic.search.ElasticRestClient;
+import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
+import org.jetlinks.core.utils.FluxUtils;
+import org.jetlinks.community.elastic.search.index.ElasticIndex;
+import org.jetlinks.community.elastic.search.parser.QueryParamTranslateService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.MonoSink;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Service
+@Slf4j
+public class DefaultElasticSearchService implements ElasticSearchService {
+
+    private final ElasticRestClient restClient;
+
+    private final IndexOperationService indexOperationService;
+
+    private final QueryParamTranslateService translateService;
+
+    FluxSink<Buffer> sink;
+
+    @Autowired
+    public DefaultElasticSearchService(ElasticRestClient restClient,
+                                       QueryParamTranslateService translateService,
+                                       IndexOperationService indexOperationService) {
+        this.restClient = restClient;
+        this.translateService = translateService;
+        this.indexOperationService = indexOperationService;
+    }
+
+
+    @Override
+    public <T> Mono<PagerResult<T>> queryPager(ElasticIndex index, QueryParam queryParam, Class<T> type) {
+        return query(searchRequestStructure(queryParam, index))
+            .map(response -> translatePageResult(type, queryParam, response))
+            .switchIfEmpty(Mono.just(PagerResult.empty()));
+    }
+
+    @Override
+    public <T> Flux<T> query(ElasticIndex index, QueryParam queryParam, Class<T> type) {
+        return query(searchRequestStructure(queryParam, index))
+            .flatMapIterable(response -> translate(type, response));
+    }
+
+    @Override
+    public Mono<Long> count(ElasticIndex index, QueryParam queryParam) {
+        return countQuery(countRequestStructure(queryParam, index))
+            .map(CountResponse::getCount)
+            .switchIfEmpty(Mono.just(0L));
+    }
+
+
+    @Override
+    public <T> Mono<Void> commit(ElasticIndex index, T payload) {
+        return Mono.fromRunnable(() -> {
+            sink.next(new Buffer(index, payload));
+        });
+    }
+
+    @Override
+    public <T> Mono<Void> commit(ElasticIndex index, Collection<T> payload) {
+        return Mono.fromRunnable(() -> {
+            for (T t : payload) {
+                sink.next(new Buffer(index, t));
+            }
+        });
+    }
+
+    @PreDestroy
+    public void shutdown() {
+        sink.complete();
+    }
+
+    @PostConstruct
+    public void init() {
+
+        FluxUtils.bufferRate(Flux.<Buffer>create(sink -> this.sink = sink),
+            1000, 2000, Duration.ofSeconds(3))
+            .flatMap(this::doSave)
+            .doOnNext((len) ->{
+                //System.out.println(len);
+                log.debug("保存ES数据成功:{}", len);
+            })
+            .onErrorContinue((err, obj) -> {
+                //打印到控制台以免递归调用ES导致崩溃
+                System.err.println(org.hswebframework.utils.StringUtils.throwable2String(err));
+            })
+            .subscribe();
+
+    }
+
+    @AllArgsConstructor
+    @Getter
+    static class Buffer {
+        ElasticIndex index;
+        Object payload;
+    }
+
+
+    protected Mono<Integer> doSave(Collection<Buffer> buffers) {
+        return Flux.fromIterable(buffers)
+            .collect(Collectors.groupingBy(Buffer::getIndex))
+            .map(Map::entrySet)
+            .flatMapIterable(Function.identity())
+            .map(entry -> {
+                ElasticIndex index = entry.getKey();
+                BulkRequest bulkRequest = new BulkRequest(index.getStandardIndex(), index.getStandardType());
+                for (Buffer buffer : entry.getValue()) {
+                    IndexRequest request = new IndexRequest();
+                    Object o = JSON.toJSON(buffer.getPayload());
+                    if (o instanceof Map) {
+                        request.source((Map) o);
+                    } else {
+                        request.source(o.toString(), XContentType.JSON);
+                    }
+                    bulkRequest.add(request);
+                }
+                entry.getValue().clear();
+                return bulkRequest;
+            })
+            .flatMap(bulkRequest ->
+                Mono.<Boolean>create(sink ->
+                    restClient.getWriteClient()
+                        .bulkAsync(bulkRequest, RequestOptions.DEFAULT, new ActionListener<BulkResponse>() {
+                            @Override
+                            public void onResponse(BulkResponse responses) {
+                                if (responses.hasFailures()) {
+                                    sink.error(new RuntimeException("批量存储es数据失败:" + responses.buildFailureMessage()));
+                                    return;
+                                }
+                                sink.success(!responses.hasFailures());
+                            }
+
+                            @Override
+                            public void onFailure(Exception e) {
+                                sink.error(e);
+                            }
+                        })))
+            .then(Mono.just(buffers.size()));
+    }
+
+    private <T> PagerResult<T> translatePageResult(Class<T> clazz, QueryParam param, SearchResponse response) {
+        long total = response.getHits().getTotalHits();
+        return PagerResult.of((int) total, translate(clazz, response), param);
+    }
+
+    private <T> List<T> translate(Class<T> clazz, SearchResponse response) {
+        return Arrays.stream(response.getHits().getHits())
+            .map(hit -> JSON.toJavaObject(new JSONObject(hit.getSourceAsMap()), clazz))
+            .collect(Collectors.toList());
+    }
+
+    private Mono<SearchResponse> query(Mono<SearchRequest> requestMono) {
+        return requestMono.flatMap((request) -> Mono.create(sink -> {
+                restClient.getQueryClient()
+                    .searchAsync(request, RequestOptions.DEFAULT, translatorActionListener(sink));
+            }
+        ));
+    }
+
+    private Mono<CountResponse> countQuery(Mono<CountRequest> requestMono) {
+        return requestMono.flatMap((request) -> Mono.create(sink -> {
+                restClient.getQueryClient()
+                    .countAsync(request, RequestOptions.DEFAULT, translatorActionListener(sink));
+            }
+        ));
+    }
+
+    private <T> ActionListener<T> translatorActionListener(MonoSink<T> sink) {
+        return new ActionListener<T>() {
+            @Override
+            public void onResponse(T response) {
+                sink.success(response);
+            }
+
+            @Override
+            public void onFailure(Exception e) {
+                if (e instanceof ElasticsearchException) {
+                    if (((ElasticsearchException) e).status().getStatus() == 404) {
+                        sink.success();
+                        return;
+                    } else if (((ElasticsearchException) e).status().getStatus() == 400) {
+                        sink.error(new ElasticsearchParseException("查询参数格式错误", e));
+                    }
+                }
+                sink.error(e);
+            }
+        };
+    }
+
+    private Mono<SearchRequest> searchRequestStructure(QueryParam queryParam, ElasticIndex provider) {
+        return indexOperationService.getIndexMappingMetadata(provider.getStandardIndex())
+            .switchIfEmpty(Mono.just(IndexMappingMetadata.getInstance(provider.getStandardIndex())))
+            .map(metadata -> {
+                SearchRequest request = new SearchRequest(provider.getStandardIndex())
+                    .source(translateService.translate(queryParam, metadata));
+                if (StringUtils.hasText(provider.getStandardType())) {
+                    request.types(provider.getStandardType());
+                }
+                return request;
+            })
+            .doOnNext(searchRequest -> log.debug("查询index:{},es查询参数:{}", provider.getStandardIndex(), searchRequest.source().toString()));
+    }
+
+    private Mono<CountRequest> countRequestStructure(QueryParam queryParam, ElasticIndex provider) {
+        queryParam.setPaging(false);
+        return indexOperationService.getIndexMappingMetadata(provider.getStandardIndex())
+            .map(metadata -> new CountRequest(provider.getStandardIndex())
+                .source(translateService.translate(queryParam, metadata)))
+            .doOnNext(searchRequest -> log.debug("查询index:{},es查询参数:{}", provider.getStandardIndex(), searchRequest.source().toString()));
+    }
+}

+ 24 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/ElasticSearchService.java

@@ -0,0 +1,24 @@
+package org.jetlinks.community.elastic.search.service;
+
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.hswebframework.web.api.crud.entity.PagerResult;
+import org.jetlinks.community.elastic.search.index.ElasticIndex;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+
+public interface ElasticSearchService {
+
+
+    <T> Mono<PagerResult<T>> queryPager(ElasticIndex index, QueryParam queryParam, Class<T> type);
+
+    <T> Flux<T> query(ElasticIndex index, QueryParam queryParam, Class<T> type);
+
+    Mono<Long> count(ElasticIndex index, QueryParam queryParam);
+
+    <T> Mono<Void> commit(ElasticIndex index, T payload);
+
+    <T> Mono<Void> commit(ElasticIndex index, Collection<T> payload);
+
+}

+ 18 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/service/IndexOperationService.java

@@ -0,0 +1,18 @@
+package org.jetlinks.community.elastic.search.service;
+
+import org.elasticsearch.client.indices.CreateIndexRequest;
+import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
+import reactor.core.publisher.Mono;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public interface IndexOperationService {
+
+    Mono<Boolean> indexIsExists(String index);
+
+    Mono<Boolean> init(CreateIndexRequest request);
+
+    Mono<IndexMappingMetadata> getIndexMappingMetadata(String index);
+}

+ 47 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/translate/QueryParamTranslator.java

@@ -0,0 +1,47 @@
+package org.jetlinks.community.elastic.search.translate;
+
+import lombok.extern.slf4j.Slf4j;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.elasticsearch.search.sort.SortOrder;
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.jetlinks.community.elastic.search.enums.LinkTypeEnum;
+import org.springframework.util.StringUtils;
+
+import java.util.Objects;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Slf4j
+public class QueryParamTranslator {
+
+    public static QueryBuilder translate(QueryParam queryParam) {
+
+        BoolQueryBuilder query = QueryBuilders.boolQuery();
+        Objects.requireNonNull(queryParam, "QueryParam must not null.")
+                .getTerms()
+                .forEach(term -> LinkTypeEnum.of(term.getType().name())
+                        .ifPresent(e -> e.process(query, term)));
+        return query;
+    }
+
+    public static SearchSourceBuilder transSourceBuilder(QueryParam queryParam) {
+        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
+        if (queryParam.isPaging()) {
+            sourceBuilder.from(queryParam.getPageIndex() * queryParam.getPageSize());
+            sourceBuilder.size(queryParam.getPageSize());
+        }
+        queryParam.getSorts()
+                .forEach(sort -> {
+                    if (!StringUtils.isEmpty(sort.getName())) {
+                        sourceBuilder.sort(sort.getName(), SortOrder.fromString(sort.getOrder()));
+                    }
+
+                });
+        return sourceBuilder.query(translate(queryParam));
+    }
+}

+ 53 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/utils/DateTimeUtils.java

@@ -0,0 +1,53 @@
+package org.jetlinks.community.elastic.search.utils;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Slf4j
+public class DateTimeUtils {
+
+    public static Object formatDateToTimestamp(Object date, List<String> formats) {
+        return TermCommonUtils.getStandardsTermValue(
+                formatDateArrayToTimestamp(TermCommonUtils.convertToList(date), formats)
+        );
+    }
+
+    private static Object formatDateStringToTimestamp(String dateString, List<String> formats) {
+        for (String format : formats) {
+            try {
+                return formatDateStringToTimestamp(dateString, format);
+            } catch (Exception e) {
+                log.debug("按格式:{}解析时间字符串:{}错误", format, dateString);
+            }
+        }
+        throw new UnsupportedOperationException("不支持的时间转换" + "formats:" +
+                JSON.toJSONString(formats) + "dateString:" + dateString);
+    }
+
+    private static long formatDateStringToTimestamp(String dateString, String format) {
+        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format);
+        LocalDateTime dateTime = LocalDateTime.parse(dateString, dateTimeFormatter);
+        return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+    }
+
+    private static List<Object> formatDateArrayToTimestamp(List<Object> values, List<String> formats) {
+        List<Object> result = new ArrayList<>();
+        for (Object value : values) {
+            if (value instanceof String) {
+                result.add(formatDateStringToTimestamp(value.toString(), formats));
+            } else {
+                result.add(value);
+            }
+        }
+        return result;
+    }
+}

+ 36 - 0
jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/utils/TermCommonUtils.java

@@ -0,0 +1,36 @@
+package org.jetlinks.community.elastic.search.utils;
+
+import java.util.*;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public class TermCommonUtils {
+
+    public static List<Object> convertToList(Object value) {
+        if (value == null) {
+            return Collections.emptyList();
+        }
+        if (value instanceof String) {
+            value = ((String) value).split(",");
+        }
+
+        if (value instanceof Object[]) {
+            value = Arrays.asList(((Object[]) value));
+        }
+
+        if (value instanceof Collection) {
+            return new ArrayList<Object>(((Collection) value));
+        }
+
+        return Arrays.asList(value);
+    }
+
+    public static Object getStandardsTermValue(List<Object> value) {
+        if (value.size() > 0 && value.size() < 2) {
+            return value.get(0);
+        }
+        return value;
+    }
+}

+ 170 - 0
jetlinks-components/elasticsearch-component/src/test/java/org/jetlinks/community/elastic/search/AggregationTest.java

@@ -0,0 +1,170 @@
+package org.jetlinks.community.elastic.search;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpHost;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.hswebframework.web.api.crud.entity.QueryParamEntity;
+import org.jetlinks.community.elastic.search.aggreation.bucket.Bucket;
+import org.jetlinks.community.elastic.search.aggreation.bucket.Sort;
+import org.jetlinks.community.elastic.search.aggreation.bucket.BucketAggregationsStructure;
+import org.jetlinks.community.elastic.search.aggreation.enums.BucketType;
+import org.jetlinks.community.elastic.search.aggreation.enums.MetricsType;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsAggregationStructure;
+import org.jetlinks.community.elastic.search.aggreation.metrics.MetricsResponseSingleValue;
+import org.jetlinks.community.elastic.search.index.DefaultIndexOperationService;
+import org.jetlinks.community.elastic.search.index.mapping.IndicesMappingCenter;
+import org.jetlinks.community.elastic.search.service.AggregationService;
+import org.jetlinks.community.elastic.search.aggreation.DefaultAggregationService;
+import org.jetlinks.community.elastic.search.index.ElasticIndex;
+import org.jetlinks.community.elastic.search.parser.DefaultLinkTypeParser;
+import org.jetlinks.community.elastic.search.parser.DefaultQueryParamTranslateService;
+import org.jetlinks.community.elastic.search.parser.QueryParamTranslateService;
+import org.jetlinks.community.elastic.search.service.IndexOperationService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import reactor.test.StepVerifier;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Slf4j
+public class AggregationTest {
+
+    private ElasticRestClient client;
+
+    private AggregationService aggregationService;
+
+    private QueryParamTranslateService translateService;
+
+    private IndexOperationService indexOperationService;
+
+
+    @BeforeEach
+    public void before() {
+        RestHighLevelClient restHighLevelClient = new RestHighLevelClient(
+                RestClient.builder(new HttpHost("localhost", 9200, "http")));
+        client = new ElasticRestClient(restHighLevelClient,restHighLevelClient);
+        translateService = new DefaultQueryParamTranslateService(new DefaultLinkTypeParser());
+        indexOperationService = new DefaultIndexOperationService(client, new IndicesMappingCenter());
+        aggregationService = new DefaultAggregationService(indexOperationService, client, translateService);
+    }
+
+    @Test
+    @SneakyThrows
+    public void minTest() {
+        MetricsAggregationStructure structure = new MetricsAggregationStructure();
+        structure.setField("lineNumber");
+        structure.setType(MetricsType.MIN);
+        aggregationService.metricsAggregation(
+                QueryParamEntity.of(), structure,
+                ElasticIndex.createDefaultIndex(() -> "system_log", () -> "doc"))
+                .doOnNext(metricsResponse -> {
+                    log.info("lineNumber 最小值结果:{}", metricsResponse.getSingleResult().getValueAsString());
+                })
+                .as(StepVerifier::create)
+                .expectNextCount(1)
+                .verifyComplete();
+    }
+
+    @Test
+    @SneakyThrows
+    public void termsTest() {
+        BucketAggregationsStructure structure = new BucketAggregationsStructure();
+        structure.setField("lineNumber");
+        structure.setType(BucketType.TERMS);
+        structure.setSort(Sort.asc());
+        structure.setSize(2);
+        aggregationService.bucketAggregation(
+                QueryParamEntity.of(), structure,
+                ElasticIndex.createDefaultIndex(() -> "system_log", () -> "doc"))
+                .doOnNext(bucketResponse -> {
+                    log.info("lineNumber terms聚合结果:{}", JSON.toJSONString(bucketResponse, SerializerFeature.PrettyFormat));
+                })
+                .as(StepVerifier::create)
+                .expectNextCount(1)
+                .verifyComplete();
+    }
+
+    @Test
+    @SneakyThrows
+    public void termsNestMetricsTest() {
+        BucketAggregationsStructure structureTime = new BucketAggregationsStructure();
+        structureTime.setField("name");
+        structureTime.setType(BucketType.TERMS);
+        structureTime.setSubMetricsAggregation(Collections.singletonList(MetricsAggregationStructure.builder()
+                .field("value")
+                .type(MetricsType.AVG)
+                .build()));
+        BucketAggregationsStructure structure = new BucketAggregationsStructure();
+        structure.setField("@timestamp");
+        structure.setType(BucketType.DATE_HISTOGRAM);
+        structure.setFormat("yyyy-MM-dd");
+        structure.setInterval("1d");
+        structure.setSort(Sort.asc());
+        structure.setSubBucketAggregation(Collections.singletonList(structureTime));
+        aggregationService.bucketAggregation(
+                QueryParamEntity.of("id.keyword", "Metaspace"), structure,
+                ElasticIndex.createDefaultIndex(() -> "metrics-2019-12", () -> ""))
+                .doOnNext(bucketResponse -> {
+
+                    bucketResponse.getBuckets()
+                            .forEach(bucket -> {
+                                Map<String, Double> map = bucket.getBuckets()
+                                        .stream()
+                                        .collect(Collectors.toMap(Bucket::getKey, b-> b.getAvg().getValue()));
+
+                                Double committed = map.get("jvm_memory_committed");
+                                Double used = map.get("jvm_memory_used");
+                                if (committed != null && used != null) {
+                                    double result = committed / (used + committed);
+                                    bucket.setAvg(MetricsResponseSingleValue.builder()
+                                            .value(result)
+                                            .valueAsString(String.valueOf(result))
+                                            .build());
+                                } else {
+                                    bucket.setAvg(MetricsResponseSingleValue.builder()
+                                            .value(0)
+                                            .valueAsString("0")
+                                            .build());
+                                    log.error("获取jvm内存使用率异常, jvm可用内存:{}, jvm已使用内存:{},key:{}", committed, used, bucket.getKey());
+                                }
+                            });
+                    log.info("lineNumber terms聚合结果:{}", JSON.toJSONString(bucketResponse, SerializerFeature.PrettyFormat));
+                })
+                .as(StepVerifier::create)
+                .expectNextCount(1)
+                .verifyComplete();
+    }
+
+    @Test
+    @SneakyThrows
+    public void termsNestBucketTest() {
+        BucketAggregationsStructure structure = new BucketAggregationsStructure();
+        structure.setField("lineNumber");
+        structure.setType(BucketType.TERMS);
+        structure.setSort(Sort.asc());
+        structure.setSubBucketAggregation(Arrays.asList(BucketAggregationsStructure.builder()
+                .field("createTime")
+                .type(BucketType.TERMS)
+                .build()));
+        aggregationService.bucketAggregation(
+                QueryParamEntity.of(), structure,
+                ElasticIndex.createDefaultIndex(() -> "system_log", () -> "doc"))
+                .doOnNext(bucketResponse -> {
+                    log.info("lineNumber terms聚合结果:{}", JSON.toJSONString(bucketResponse, SerializerFeature.PrettyFormat));
+                })
+                .as(StepVerifier::create)
+                .expectNextCount(1)
+                .verifyComplete();
+    }
+}

+ 47 - 0
jetlinks-components/elasticsearch-component/src/test/java/org/jetlinks/community/elastic/search/ElasticSearchQueryParamTranslatorTest.java

@@ -0,0 +1,47 @@
+package org.jetlinks.community.elastic.search;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import org.elasticsearch.search.builder.SearchSourceBuilder;
+import org.hswebframework.ezorm.core.dsl.Query;
+import org.hswebframework.ezorm.core.param.QueryParam;
+import org.jetlinks.community.elastic.search.index.mapping.IndexMappingMetadata;
+import org.jetlinks.community.elastic.search.parser.DefaultLinkTypeParser;
+import org.jetlinks.community.elastic.search.parser.DefaultQueryParamTranslateService;
+import org.jetlinks.community.elastic.search.parser.QueryParamTranslateService;
+import org.junit.Assert;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public class ElasticSearchQueryParamTranslatorTest {
+
+    @Test
+    public void test() {
+
+        Query query = Query.of(new QueryParam())
+                .where()
+                .is("methodName", "error")
+                .or()
+                .in("level", "ERROR", "DEBUG")
+                .orNest()
+                .or()
+                .is("threadId", "44")
+                .or()
+                .is("lineNumber", "319")
+                .between("aaa", "2019-12-11 22:00:10", "2019-12-11 23:00:10")
+                .end();
+
+        QueryParamTranslateService translateService = new DefaultQueryParamTranslateService(new DefaultLinkTypeParser());
+        SearchSourceBuilder searchSourceBuilder = translateService.translate(query.getParam(), IndexMappingMetadata.getInstance(""));
+
+        JSONObject jsonObject = JSON.parseObject(searchSourceBuilder.query().toString());
+
+        JSONObject boolJson = jsonObject.getJSONObject("bool");
+        Assert.assertEquals(boolJson.getJSONArray("must").size(), 1);
+        Assert.assertEquals(boolJson.getJSONArray("should").size(), 2);
+        System.out.println(searchSourceBuilder.query().toString());
+    }
+}

+ 61 - 0
jetlinks-components/elasticsearch-component/src/test/java/org/jetlinks/community/elastic/search/IndexInitTest.java

@@ -0,0 +1,61 @@
+package org.jetlinks.community.elastic.search;
+
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpHost;
+import org.elasticsearch.client.RestClient;
+import org.elasticsearch.client.RestHighLevelClient;
+import org.elasticsearch.client.indices.CreateIndexRequest;
+import org.jetlinks.community.elastic.search.enums.FieldDateFormat;
+import org.jetlinks.community.elastic.search.enums.FieldType;
+import org.jetlinks.community.elastic.search.index.CreateIndex;
+import org.jetlinks.community.elastic.search.index.DefaultIndexOperationService;
+import org.jetlinks.community.elastic.search.index.mapping.IndicesMappingCenter;
+import org.jetlinks.community.elastic.search.service.IndexOperationService;
+import org.junit.jupiter.api.Test;
+import reactor.test.StepVerifier;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.Date;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Slf4j
+public class IndexInitTest {
+    RestHighLevelClient client = new RestHighLevelClient(
+            RestClient.builder(new HttpHost("localhost", 9200, "http")));
+    IndexOperationService operationService =
+            new DefaultIndexOperationService(new ElasticRestClient(client,client),new IndicesMappingCenter());
+
+    @Test
+    @SneakyThrows
+    public void simpleTest() {
+
+        CreateIndexRequest request = CreateIndex.createInstance()
+                .addIndex("bestfeng")
+                .createMapping()
+                .addFieldName("date").addFieldType(FieldType.DATE).addFieldDateFormat(FieldDateFormat.epoch_millis, FieldDateFormat.simple_date).commit()
+                .addFieldName("name").addFieldType(FieldType.KEYWORD).commit()
+                .end()
+                .createSettings()
+                .settingReplicas(2)
+                .settingShards(6)
+                .end()
+                .createIndexRequest();
+        operationService.init(request)
+                .as(StepVerifier::create)
+                .expectNextMatches(bool -> bool)
+                .verifyComplete();
+    }
+
+    public static void main(String[] args) {
+        Date date2 = new Date(1575528676826L);
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        LocalDateTime localDateTime2 = LocalDateTime.ofInstant(date2.toInstant(), ZoneId.systemDefault());
+        System.out.println(localDateTime2.format(formatter));
+    }
+}

+ 3 - 0
jetlinks-components/gateway-component/README.md

@@ -0,0 +1,3 @@
+# 网关模块
+
+统一管理设备网关服务,消息网关.

+ 28 - 0
jetlinks-components/gateway-component/pom.xml

@@ -0,0 +1,28 @@
+<?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>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>gateway-component</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>jetlinks-core</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>network-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 62 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/ClientSession.java

@@ -0,0 +1,62 @@
+package org.jetlinks.community.gateway;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * 客户端连接会话,用于保存客户端订阅信息.
+ *
+ * @author zhouhao
+ * @version 1.0
+ * @since 1.0
+ */
+public interface ClientSession {
+
+    /**
+     * @return 会话ID
+     */
+    String getId();
+
+    /**
+     * @return 客户端ID
+     */
+    String getClientId();
+
+    /**
+     * @return 是否持久化
+     */
+    boolean isPersist();
+
+    /**
+     * 获取会话的全部订阅信息
+     *
+     * @return 订阅信息
+     */
+    Flux<Subscription> getSubscriptions();
+
+    /**
+     * 添加订阅信息
+     *
+     * @param subscription 订阅信息
+     * @return 添加结果
+     */
+    Mono<Void> addSubscription(Subscription subscription);
+
+    /**
+     * 移除订阅信息
+     *
+     * @param subscription 订阅信息
+     * @return 移除结果
+     */
+    Mono<Void> removeSubscription(Subscription subscription);
+
+    /**
+     * @return 会话是否存活
+     */
+    boolean isAlive();
+
+    /**
+     * 关闭会话
+     */
+    void close();
+}

+ 61 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/ClientSessionManager.java

@@ -0,0 +1,61 @@
+package org.jetlinks.community.gateway;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+
+/**
+ * 客户端会话管理器,用于管理客户端会话.
+ *
+ * @author zhouhao
+ * @version 1.0
+ * @since 1.0
+ */
+public interface ClientSessionManager {
+
+    /**
+     * 创建一个新会话
+     *
+     * @param messageGatewayId 消息网关ID
+     * @param connection       网关消息连接
+     * @return 创建结果
+     */
+    Mono<ClientSession> createSession(String messageGatewayId, MessageConnection connection);
+
+    /**
+     * 获取网关内全部会话
+     *
+     * @param messageGatewayId 网关
+     * @return 会话流
+     */
+    Flux<ClientSession> getSessions(String messageGatewayId);
+
+    /**
+     * 从指定的网关里获取指定的会话,如果会话不存在则返回{@link Mono#empty()}
+     *
+     * @param messageGatewayId 消息网关ID
+     * @param sessionId        会话ID
+     * @return 会话
+     */
+    Mono<ClientSession> getSession(String messageGatewayId, String sessionId);
+
+    /**
+     * 批量获取指定网关下的会话
+     *
+     * @param messageGatewayId 网关ID
+     * @param sessionIdList    会话ID
+     * @return 会话流
+     */
+    Flux<ClientSession> getSessions(String messageGatewayId, Collection<String> sessionIdList);
+
+    /**
+     * 关闭指定网关的会话
+     *
+     * @param messageGatewayId 消息网关ID
+     * @param sessionId        会话ID
+     * @return 关闭结果
+     */
+    Mono<Void> closeSession(String messageGatewayId, String sessionId);
+
+}

+ 21 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DefaultTopicMessage.java

@@ -0,0 +1,21 @@
+package org.jetlinks.community.gateway;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.core.message.codec.EncodedMessage;
+import org.jetlinks.core.message.codec.Transport;
+
+@Getter
+@Setter
+@AllArgsConstructor
+class DefaultTopicMessage implements TopicMessage {
+    private String topic;
+
+    private EncodedMessage message;
+
+    public DefaultTopicMessage(String topic,Object message){
+        this.topic=topic;
+
+    }
+}

+ 66 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGateway.java

@@ -0,0 +1,66 @@
+package org.jetlinks.community.gateway;
+
+import org.jetlinks.core.message.Message;
+import org.jetlinks.core.message.codec.Transport;
+import org.jetlinks.community.network.NetworkType;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+/**
+ * 设备网关,用于统一管理设备连接,状态以及消息收发
+ *
+ * @author zhouhao
+ * @version 1.0
+ * @since 1.0
+ */
+public interface DeviceGateway {
+
+    /**
+     * @return 网关ID
+     */
+    String getId();
+
+    /**
+     * @return 传输协议
+     * @see org.jetlinks.core.message.codec.DefaultTransport
+     */
+    Transport getTransport();
+
+    /**
+     * @return 网络类型
+     * @see org.jetlinks.community.network.DefaultNetworkType
+     */
+    NetworkType getNetworkType();
+
+    /**
+     * 订阅来自设备到消息,关闭网关时不会结束流.
+     *
+     * @return 设备消息流
+     */
+    Flux<Message> onMessage();
+
+    /**
+     * 启动网关
+     *
+     * @return 启动结果
+     */
+    Mono<Void> startup();
+
+    /**
+     * 暂停网关,暂停后停止处理设备消息.
+     *
+     * @return 暂停结果
+     */
+    Mono<Void> pause();
+
+    /**
+     * 关闭网关
+     *
+     * @return 关闭结果
+     */
+    Mono<Void> shutdown();
+
+   default boolean isAlive(){
+       return true;
+   }
+}

+ 15 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGatewayManager.java

@@ -0,0 +1,15 @@
+package org.jetlinks.community.gateway;
+
+import org.jetlinks.community.gateway.supports.DeviceGatewayProvider;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+public interface DeviceGatewayManager {
+
+    Mono<DeviceGateway> getGateway(String id);
+
+    Mono<Void> shutdown(String gatewayId);
+
+    List<DeviceGatewayProvider> getProviders();
+}

+ 12 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/EncodableMessage.java

@@ -0,0 +1,12 @@
+package org.jetlinks.community.gateway;
+
+import org.jetlinks.core.message.codec.EncodedMessage;
+
+public interface EncodableMessage extends EncodedMessage {
+
+    Object getNativePayload();
+
+    static EncodableMessage of(Object object) {
+        return new JsonEncodedMessage(object);
+    }
+}

+ 33 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/JsonEncodedMessage.java

@@ -0,0 +1,33 @@
+package org.jetlinks.community.gateway;
+
+import io.netty.buffer.ByteBuf;
+import lombok.Getter;
+import org.jetlinks.core.message.codec.EncodedMessage;
+import org.jetlinks.rule.engine.executor.PayloadType;
+
+import javax.annotation.Nonnull;
+import java.util.Objects;
+
+public class JsonEncodedMessage implements EncodableMessage {
+
+    private volatile ByteBuf payload;
+
+    @Getter
+    private Object nativePayload;
+
+    public JsonEncodedMessage(Object nativePayload) {
+        Objects.requireNonNull(nativePayload);
+        this.nativePayload = nativePayload;
+    }
+
+    @Nonnull
+    @Override
+    public ByteBuf getPayload() {
+        if (payload == null) {
+            payload = PayloadType.JSON.write(nativePayload);
+        }
+        return payload;
+    }
+
+
+}

+ 80 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessageConnection.java

@@ -0,0 +1,80 @@
+package org.jetlinks.community.gateway;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * 消息连接,在网络组件中,一个客户端则可能认为是一个消息连接.
+ * <p>
+ * 如果实现了{@link MessagePublisher}接口,则认为可以从此连接中订阅消息,进行网关转发.
+ * <p>
+ * 如果实现了{@link MessageSubscriber}接口,则认为连接可以进行消息订阅,并接收来自网关的消息.
+ *
+ * @see MessagePublisher
+ * @see MessageSubscriber
+ */
+public interface MessageConnection {
+
+    /**
+     * @return 连接唯一标识
+     */
+    String getId();
+
+    /**
+     * 添加断开连接监听器,当网络断开时,执行{@link Runnable#run()},支持多个监听器
+     *
+     * @param disconnectListener 监听器
+     */
+    void onDisconnect(Runnable disconnectListener);
+
+    /**
+     * 主动断开连接
+     */
+    void disconnect();
+
+    /**
+     * 当前连接是否存活
+     *
+     * @return 是否存活
+     */
+    boolean isAlive();
+
+    /**
+     * 判断是否为推送器,如果是则可以使用{@link this#asPublisher()}转为推送器,然后进行消息订阅等处理.
+     *
+     * @return 是否为推送器
+     * @see MessagePublisher
+     * @see this#asPublisher()
+     */
+    default boolean isPublisher() {
+        return this instanceof MessagePublisher;
+    }
+
+    /**
+     * 同{@link this#isPublisher()}
+     *
+     * @return 是否为订阅器
+     */
+    default boolean isSubscriber() {
+        return this instanceof MessageSubscriber;
+    }
+
+    /**
+     * 尝试转为消息推送器,如果不可行则返回{@link Mono#empty()}
+     *
+     * @return MessagePublisher
+     * @see MessagePublisher
+     */
+    default Mono<MessagePublisher> asPublisher() {
+        return isPublisher() ? Mono.just(this).cast(MessagePublisher.class) : Mono.empty();
+    }
+
+    /**
+     * 尝试转为消息订阅,如果不可行则返回{@link Mono#empty()}
+     *
+     * @return MessageSubscriber
+     * @see MessageSubscriber
+     */
+    default Mono<MessageSubscriber> asSubscriber() {
+        return isSubscriber() ? Mono.just(this).cast(MessageSubscriber.class) : Mono.empty();
+    }
+}

+ 57 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessageConnector.java

@@ -0,0 +1,57 @@
+package org.jetlinks.community.gateway;
+
+import reactor.core.publisher.Flux;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/**
+ * 网关消息连接器,用于连接自定义到消息到网关.
+ * <p>
+ * 如果连接器没有任何连接订阅者,将拒绝连接{@link MessageConnection#disconnect()}.
+ * <p>
+ * ⚠️注意: 连接器不会保存任何连接信息
+ *
+ * @author zhouhao
+ * @see MessageConnection
+ */
+public interface MessageConnector {
+
+    /**
+     * @return 连接器唯一标识
+     */
+    @Nonnull
+    String getId();
+
+    /**
+     * @return 名称
+     */
+    @Nullable
+    default String getName() {
+        return getId();
+    }
+
+    /**
+     * @return 说明
+     */
+    @Nullable
+    default String getDescription() {
+        return null;
+    }
+
+    /**
+     * 订阅连接器中到网络连接,此订阅只会获取到最新的连接.
+     * <p>
+     * 如果订阅了多次,每个订阅都会收到每一个连接.
+     * <p>
+     * ⚠️: 如果发生错误不想停止订阅,请处理好{@link Flux#onErrorContinue(BiConsumer)}或者{@link Flux#onErrorResume(Function)}
+     *
+     * @return 网络连接流.
+     * @see MessageConnection
+     */
+    @Nonnull
+    Flux<MessageConnection> onConnection();
+
+}

+ 103 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessageGateway.java

@@ -0,0 +1,103 @@
+package org.jetlinks.community.gateway;
+
+import reactor.core.publisher.Flux;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 基于topic对消息网关,用于对各种消息进行路由和转发.
+ * <p>
+ * 如: 使用 CoAP 发送 /group/1/user/1 消息到网关,其他任何订阅了此topic的客户端将都收到此消息.
+ *
+ * @author zhouhao
+ * @see 1.0
+ * @see MessageConnector
+ */
+public interface MessageGateway {
+
+    /**
+     * @return 网关ID
+     */
+    String getId();
+
+    String getName();
+
+    /**
+     * 向网关推送消息,消息将根据{@link TopicMessage#getTopic()}发送到对应的订阅者.
+     *
+     * @param message      消息
+     * @param shareCluster 是否广播到集群其他节点
+     * @return 成功推送的会话流
+     */
+    Flux<ClientSession> publish(TopicMessage message, boolean shareCluster);
+
+    /**
+     * 向网关推送消息.
+     *
+     * @param topic        话题
+     * @param payload      消息内容
+     * @param shareCluster 是否广播到集群其他节点
+     * @return 成功推送的会话流
+     * @see this#publish(TopicMessage, boolean)
+     */
+    default Flux<ClientSession> publish(String topic, Object payload, boolean shareCluster) {
+        return publish(TopicMessage.of(topic, payload), shareCluster);
+    }
+
+    default Flux<ClientSession> publish(String topic, Object payload) {
+        return publish(topic, payload, false);
+    }
+
+    default Flux<ClientSession> publish(TopicMessage message) {
+        return publish(message, false);
+    }
+
+    /**
+     * 订阅当前网关收到的消息.
+     *
+     * @param subscription 订阅信息
+     * @param shareCluster 是否共享集群中其他节点到消息
+     * @return 信息流
+     */
+    Flux<TopicMessage> subscribe(Collection<Subscription> subscription, boolean shareCluster);
+
+    /**
+     * 订阅当前网关收到的消息.
+     * 如果存在集群,不会收到来自集群其他节点的消息.
+     *
+     * @param topics 话题数组,可同时订阅多个话题
+     * @return 消息
+     */
+    default Flux<TopicMessage> subscribe(String... topics) {
+        return subscribe(Stream.of(topics).map(Subscription::new).collect(Collectors.toList()), false);
+    }
+
+    /**
+     * 注册一个消息连接器,用于进行真实的消息收发
+     *
+     * @param connector 连接器
+     */
+    void registerMessageConnector(MessageConnector connector);
+
+    /**
+     * 根据连接器ID删除一个连接器
+     *
+     * @param connectorId 连接器ID {@link MessageConnector#getId()}
+     * @return 被删除的连接器, 连接器不存在则返回<code>null</code>
+     * @see MessageConnector
+     */
+    MessageConnector removeConnector(String connectorId);
+
+    /**
+     * 启动网关
+     */
+    void startup();
+
+    /**
+     * 停止网关
+     */
+    void shutdown();
+
+}

+ 12 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessageGatewayManager.java

@@ -0,0 +1,12 @@
+package org.jetlinks.community.gateway;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface MessageGatewayManager {
+
+    Mono<MessageGateway> getGateway(String id);
+
+    Flux<MessageGateway> getAllGateway();
+
+}

+ 24 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessagePublisher.java

@@ -0,0 +1,24 @@
+package org.jetlinks.community.gateway;
+
+import reactor.core.publisher.Flux;
+
+import javax.annotation.Nonnull;
+
+/**
+ * 消息推送器,可通过{@link this#onMessage()}来订阅推送器中到消息
+ *
+ * @see MessageConnection
+ * @since 1.0
+ */
+public interface MessagePublisher {
+
+    /**
+     * 订阅连接中的消息
+     *
+     * @return 消息流
+     */
+    @Nonnull
+    Flux<TopicMessage> onMessage();
+
+
+}

+ 56 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/MessageSubscriber.java

@@ -0,0 +1,56 @@
+package org.jetlinks.community.gateway;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+/**
+ * 消息订阅器,订阅器用于订阅来自其他连接器的消息.
+ *
+ * @author zhouhao
+ * @see MessageConnection
+ * @see MessagePublisher
+ * @since 1.0
+ */
+public interface MessageSubscriber {
+
+    /**
+     * 推送消息到订阅器,处理消息应该是无阻塞的,并且快速失败.
+     *
+     * @param message 消息
+     * @return 处理结果
+     */
+    @Nonnull
+    Mono<Void> publish(@Nonnull TopicMessage message);
+
+    /**
+     * 监听订阅请求,网关收到订阅请求后才会将对应的广播消息推送{@link this#publish(TopicMessage)}到该订阅器.
+     *
+     * @return 订阅流
+     */
+    @Nonnull
+    Flux<Subscription> onSubscribe();
+
+    /**
+     * 监听取消订阅,取消订阅后,将不会再收到该话题的消息
+     *
+     * @return 取消订阅流
+     */
+    @Nonnull
+    Flux<Subscription> onUnSubscribe();
+
+    /**
+     * 是否共享集群中的消息
+     * <p>
+     * 如果为<code>true</code>,
+     * 则当集群当其他节点收到消息时,也会调用{@link this#publish(TopicMessage)}.
+     * ⚠️: 如果同一个订阅者在集群中多个节点进行相同的订阅,则会收到相同的消息. 在一些场景下(比如业务系统消息队列)不建议设置为true.
+     * <p>
+     * 如果为<code>false</code>
+     * 则只会收到当前服务器的消息
+     *
+     * @return 是否共享集群中的消息
+     */
+    boolean isShareCluster();
+}

+ 26 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/Subscription.java

@@ -0,0 +1,26 @@
+package org.jetlinks.community.gateway;
+
+import lombok.*;
+
+/**
+ * 订阅信息.支持通配符**(匹配多层目录)和*(匹配单层目录).
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+@Getter
+@Setter
+@EqualsAndHashCode(of = "topic")
+public class Subscription {
+
+    private String topic;
+
+    public Subscription(String topic) {
+        //适配mqtt topic通配符
+        if (topic.contains("#") || topic.contains("+")) {
+            topic = topic.replace("#", "**").replace("+", "*");
+        }
+        this.topic = topic;
+    }
+
+}

+ 39 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/TopicMessage.java

@@ -0,0 +1,39 @@
+package org.jetlinks.community.gateway;
+
+import org.jetlinks.core.message.codec.EncodedMessage;
+
+import javax.annotation.Nonnull;
+
+public interface TopicMessage {
+
+    /**
+     * 主题: 格式为: /group/1/user/1, 支持通配符: **(多层路径),*(单层路径)
+     *
+     * <pre>
+     *     /group/** , /group/下的全部topic.包括子目录
+     *     /group/1/* , /group/1/下的topic. 不包括子目录
+     * </pre>
+     *
+     * @return topic
+     */
+    @Nonnull
+    String getTopic();
+
+    /**
+     * @return 已编码的消息
+     * @see org.jetlinks.core.message.codec.MqttMessage
+     */
+    @Nonnull
+    EncodedMessage getMessage();
+
+    static TopicMessage of(String topic, EncodedMessage message) {
+        return new DefaultTopicMessage(topic, message);
+    }
+
+    static TopicMessage of(String topic, Object payload) {
+        if (payload instanceof EncodedMessage) {
+            return of(topic, ((EncodedMessage) payload));
+        }
+        return of(topic, EncodableMessage.of(payload));
+    }
+}

+ 182 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/TopicPart.java

@@ -0,0 +1,182 @@
+package org.jetlinks.community.gateway;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.util.AntPathMatcher;
+import org.springframework.util.StringUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.function.BiFunction;
+
+@Getter
+@Setter
+@EqualsAndHashCode(of = "part")
+public class TopicPart {
+
+    private TopicPart parent;
+
+    private String part;
+
+    private volatile String topic;
+
+    private int depth;
+
+    private ConcurrentMap<String, TopicPart> child = new ConcurrentHashMap<>();
+
+    private Set<String> sessionId = new CopyOnWriteArraySet<>();
+
+    private static final AntPathMatcher matcher = new AntPathMatcher();
+
+    public TopicPart(TopicPart parent, String part) {
+
+        if (StringUtils.isEmpty(part) || part.equals("/")) {
+            this.part = "";
+        } else {
+            if (part.contains("/")) {
+                this.ofTopic(part);
+            } else {
+                this.part = part;
+            }
+        }
+        this.parent = parent;
+        if (null != parent) {
+            this.depth = parent.depth + 1;
+        }
+    }
+
+    public String getTopic() {
+        if (topic == null) {
+            TopicPart parent = getParent();
+            StringBuilder builder = new StringBuilder();
+            if (parent != null) {
+                String parentTopic = parent.getTopic();
+                builder.append(parentTopic).append(parentTopic.equals("/") ? "" : "/");
+            } else {
+                builder.append("/");
+            }
+            return topic = builder.append(part).toString();
+        }
+        return topic;
+    }
+
+
+    public TopicPart subscribe(String topic) {
+        return getOrDefault(topic, TopicPart::new);
+    }
+
+    public void addSessionId(String... sessionId) {
+        this.sessionId.addAll(Arrays.asList(sessionId));
+    }
+
+    public void removeSession(String... sessionId) {
+        this.sessionId.removeAll(Arrays.asList(sessionId));
+    }
+
+    private void ofTopic(String topic) {
+        String[] parts = topic.split("[/]", 2);
+        this.part = parts[0];
+        if (parts.length > 1) {
+            TopicPart part = new TopicPart(this, parts[1]);
+            this.child.put(part.part, part);
+        }
+    }
+
+    private TopicPart getOrDefault(String topic, BiFunction<TopicPart, String, TopicPart> mapping) {
+        if (topic.startsWith("/")) {
+            topic = topic.substring(1);
+        }
+        String[] parts = topic.split("[/]");
+        TopicPart part = child.computeIfAbsent(parts[0], _topic -> mapping.apply(this, _topic));
+        for (int i = 1; i < parts.length && part != null; i++) {
+            TopicPart parent = part;
+            part = part.child.computeIfAbsent(parts[i], _topic -> mapping.apply(parent, _topic));
+        }
+        return part;
+    }
+
+    public Mono<TopicPart> get(String topic) {
+        return Mono.justOrEmpty(getOrDefault(topic, ((topicPart, s) -> null)));
+    }
+
+    public Flux<TopicPart> find(String topic) {
+        return find(topic, this);
+    }
+
+    @Override
+    public String toString() {
+        return "topic: " + getTopic() + ", sessions: " + sessionId.size();
+    }
+
+
+    public static Flux<TopicPart> find(String topic,
+                                       TopicPart topicPart) {
+        return Flux.create(sink -> {
+            ArrayDeque<TopicPart> cache = new ArrayDeque<>();
+            cache.add(topicPart);
+            String[] topicParts = topic.split("[/]");
+            String nextPart = null;
+            while (!cache.isEmpty() && !sink.isCancelled()) {
+                TopicPart part = cache.poll();
+                if (part == null) {
+                    break;
+                }
+                if (part.part.equals("**")
+//                    || part.part.equals("*")
+                    || matcher.match(part.getTopic(), topic)
+                    || (matcher.match(topic, part.getTopic()))) {
+                    sink.next(part);
+                }
+
+                //订阅了如 /device/**/event/*
+                if (part.part.equals("**")) {
+                    TopicPart tmp = null;
+                    for (int i = part.depth; i < topicParts.length; i++) {
+                        tmp = part.child.get(topicParts[i]);
+                        if (tmp != null) {
+                            cache.add(tmp);
+                        }
+                    }
+                    if (null != tmp) {
+                        continue;
+                    }
+                }
+                if ("**".equals(nextPart) || "*".equals(nextPart)) {
+                    cache.addAll(part.child.values());
+                    continue;
+                }
+                TopicPart next = part.child.get("**");
+                if (next != null) {
+                    cache.add(next);
+                }
+                next = part.child.get("*");
+                if (next != null) {
+                    cache.add(next);
+                }
+
+                if (part.depth + 1 >= topicParts.length) {
+                    continue;
+                }
+                nextPart = topicParts[part.depth + 1];
+                if (nextPart.equals("*") || nextPart.equals("**")) {
+                    cache.addAll(part.child.values());
+                    continue;
+                }
+                next = part.child.get(nextPart);
+                if (next != null) {
+                    cache.add(next);
+                }
+
+            }
+            sink.complete();
+        });
+
+    }
+
+
+}

+ 68 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNode.java

@@ -0,0 +1,68 @@
+package org.jetlinks.community.gateway.rule;
+
+import org.hswebframework.web.exception.NotFoundException;
+import org.jetlinks.community.gateway.MessageGatewayManager;
+import org.jetlinks.community.network.PubSubType;
+import org.jetlinks.rule.engine.api.RuleData;
+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.Mono;
+
+import java.util.function.Function;
+
+@Component
+public class MessageGatewayRuleNode extends CommonExecutableRuleNodeFactoryStrategy<MessageGatewayRuleNodeConfig> {
+
+    private final MessageGatewayManager gatewayManager;
+
+    static {
+        TopicMessageCodec.register();
+    }
+
+    public MessageGatewayRuleNode(MessageGatewayManager gatewayManager) {
+        this.gatewayManager = gatewayManager;
+    }
+
+    @Override
+    public Function<RuleData, ? extends Publisher<?>> createExecutor(ExecutionContext context, MessageGatewayRuleNodeConfig config) {
+        if (config.getType() == PubSubType.consumer) {
+            return Mono::just;
+        }
+        return ruleData -> gatewayManager
+            .getGateway(config.getGatewayId())
+            .switchIfEmpty(Mono.error(() -> new NotFoundException("消息网关[{" + config.getGatewayId() + "}]不存在")))
+            .flatMap(gateway -> config.convert(ruleData)
+                .flatMap(msg -> gateway.publish(msg, config.isShareCluster()))
+                .then())
+            .thenReturn(ruleData);
+    }
+
+    @Override
+    protected void onStarted(ExecutionContext context, MessageGatewayRuleNodeConfig config) {
+        super.onStarted(context, config);
+        if (config.getType() == PubSubType.producer) {
+            return;
+        }
+        //订阅网关中的消息
+        context.onStop(gatewayManager
+            .getGateway(config.getGatewayId())
+            .switchIfEmpty(Mono.fromRunnable(() -> context.logger().error("消息网关[{" + config.getGatewayId() + "}]不存在")))
+            .flatMapMany(gateway -> gateway.subscribe(config.createTopics()))
+            .map(config::convert)
+            .flatMap(data -> context.getOutput().write(Mono.just(RuleData.create(data))))
+            .onErrorContinue((err, obj) -> {
+                context.logger().error(err.getMessage(), err);
+            })
+            .subscribe()::dispose);
+
+    }
+
+    @Override
+    public String getSupportType() {
+        return "message-gateway";
+    }
+
+
+}

+ 56 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/MessageGatewayRuleNodeConfig.java

@@ -0,0 +1,56 @@
+package org.jetlinks.community.gateway.rule;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.community.gateway.TopicMessage;
+import org.jetlinks.community.network.PubSubType;
+import org.jetlinks.rule.engine.api.RuleData;
+import org.jetlinks.rule.engine.api.model.NodeType;
+import org.jetlinks.rule.engine.executor.node.RuleNodeConfig;
+import org.springframework.util.Assert;
+import reactor.core.publisher.Flux;
+
+@Getter
+@Setter
+public class MessageGatewayRuleNodeConfig implements RuleNodeConfig {
+
+    private String gatewayId;
+
+    private PubSubType type;
+
+    private String topics;
+
+    private boolean shareCluster;
+
+    public Flux<TopicMessage> convert(RuleData data) {
+        return TopicMessageCodec.getInstance()
+            .decode(data, TopicMessageCodec.feature(createTopics()));
+    }
+
+    public Object convert(TopicMessage message) {
+        return TopicMessageCodec.getInstance().encode(message);
+    }
+
+    public String[] createTopics() {
+        return topics.split("[,;\n]");
+    }
+
+    @Override
+    public void validate() {
+        Assert.hasText(gatewayId, "gatewayId can not be empty");
+        Assert.hasText(topics, "topics can not be empty");
+        Assert.notNull(type, "type can not be null");
+
+    }
+
+    @Override
+    public NodeType getNodeType() {
+        return NodeType.MAP;
+    }
+
+
+    @Override
+    public void setNodeType(NodeType nodeType) {
+
+    }
+}

+ 73 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/rule/TopicMessageCodec.java

@@ -0,0 +1,73 @@
+package org.jetlinks.community.gateway.rule;
+
+import io.netty.buffer.ByteBuf;
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.community.gateway.TopicMessage;
+import org.jetlinks.rule.engine.api.RuleData;
+import org.jetlinks.rule.engine.api.RuleDataCodec;
+import org.jetlinks.rule.engine.api.RuleDataCodecs;
+import org.jetlinks.rule.engine.executor.PayloadType;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class TopicMessageCodec implements RuleDataCodec<TopicMessage> {
+
+    private static final TopicMessageCodec INSTANCE = new TopicMessageCodec();
+
+    static {
+        RuleDataCodecs.register(TopicMessage.class, INSTANCE);
+    }
+
+    public static void register() {
+    }
+
+
+    public static TopicMessageCodec getInstance() {
+        return INSTANCE;
+    }
+
+    @Override
+    public Map<String, Object> encode(TopicMessage data, Feature... features) {
+
+        ByteBuf payload = data.getMessage().getPayload();
+        PayloadType payloadType = PayloadType.valueOf(data.getMessage().getPayloadType().name());
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("topic", data.getTopic());
+        map.put("message", payloadType.read(payload));
+        return map;
+    }
+
+    @Override
+    public Flux<TopicMessage> decode(RuleData data, Feature... features) {
+
+        return Mono.fromSupplier(() -> Feature.find(TopicFeature.class, features)
+            .map(TopicFeature::getTopics)
+            .orElseThrow(() -> new UnsupportedOperationException("topics not found")))
+            .flatMapMany(Flux::just)
+            .flatMap(topic -> data
+                .dataToMap()
+                .map(map -> TopicMessage.of(topic, map)));
+    }
+
+
+    public static TopicFeature feature(String... topics) {
+        return new TopicFeature(topics);
+    }
+
+    @Getter
+    @Setter
+    public static class TopicFeature implements RuleDataCodec.Feature {
+
+        private String[] topics;
+
+        public TopicFeature(String... topics) {
+            this.topics = topics;
+        }
+    }
+
+}

+ 79 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DefaultDeviceGatewayManager.java

@@ -0,0 +1,79 @@
+package org.jetlinks.community.gateway.supports;
+
+import org.jetlinks.community.gateway.DeviceGateway;
+import org.jetlinks.community.gateway.DeviceGatewayManager;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class DefaultDeviceGatewayManager implements DeviceGatewayManager, BeanPostProcessor {
+
+    private final DeviceGatewayPropertiesManager propertiesManager;
+
+    private Map<String, DeviceGatewayProvider> providers = new ConcurrentHashMap<>();
+
+    private Map<String, DeviceGateway> store = new ConcurrentHashMap<>();
+
+    public DefaultDeviceGatewayManager(DeviceGatewayPropertiesManager propertiesManager) {
+        this.propertiesManager = propertiesManager;
+    }
+
+    private Mono<DeviceGateway> doGetGateway(String id) {
+        if (store.containsKey(id)) {
+            return Mono.just(store.get(id));
+        }
+        return propertiesManager
+            .getProperties(id)
+            .switchIfEmpty(Mono.error(new UnsupportedOperationException("网关配置[" + id + "]不存在")))
+            .flatMap(properties -> Mono
+                .justOrEmpty(providers.get(properties.getProvider()))
+                .switchIfEmpty(Mono.error(new UnsupportedOperationException("不支持的网络服务[" + properties.getProvider() + "]")))
+                .flatMap(provider -> provider
+                    .createDeviceGateway(properties)
+                    .flatMap(gateway -> {
+                        if (store.containsKey(id)) {
+                            return gateway
+                                .shutdown()
+                                .thenReturn(store.get(id));
+                        }
+                        store.put(id, gateway);
+                        return Mono.justOrEmpty(gateway);
+                    })));
+    }
+
+    @Override
+    public Mono<Void> shutdown(String gatewayId) {
+        return Mono.justOrEmpty(store.remove(gatewayId))
+            .flatMap(DeviceGateway::shutdown);
+    }
+
+    @Override
+    public Mono<DeviceGateway> getGateway(String id) {
+        return Mono
+            .justOrEmpty(store.get(id))
+            .switchIfEmpty(doGetGateway(id));
+    }
+
+    @Override
+    public List<DeviceGatewayProvider> getProviders() {
+        return new ArrayList<>(providers.values());
+    }
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof DeviceGatewayProvider) {
+            DeviceGatewayProvider provider = ((DeviceGatewayProvider) bean);
+            providers.put(provider.getId(), provider);
+        }
+        return bean;
+    }
+
+
+}

+ 256 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DefaultMessageGateway.java

@@ -0,0 +1,256 @@
+package org.jetlinks.community.gateway.supports;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.id.IDGenerator;
+import org.jetlinks.community.gateway.*;
+import reactor.core.Disposable;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Predicate;
+
+@Slf4j
+public class DefaultMessageGateway implements MessageGateway {
+
+    @Getter
+    private final String id;
+    @Getter
+    private String name;
+
+    private TopicPart root = new TopicPart(null, "/");
+
+    private Map<String, ConnectionSession> sessions = new ConcurrentHashMap<>();
+
+    private ClientSessionManager sessionManager;
+
+    private Map<String, Connector> connectors = new ConcurrentHashMap<>();
+
+    private AtomicBoolean started = new AtomicBoolean();
+
+    private LocalMessageConnector localGatewayConnector;
+
+    public DefaultMessageGateway(String id, ClientSessionManager sessionManager) {
+        this(id, id, sessionManager);
+    }
+
+    public DefaultMessageGateway(String id, String name, ClientSessionManager sessionManager) {
+        this.id = id;
+        this.name = name;
+        this.sessionManager = sessionManager;
+        this.localGatewayConnector = new LocalMessageConnector();
+        this.registerMessageConnector(localGatewayConnector);
+    }
+
+    @Override
+    public Flux<ClientSession> publish(TopicMessage message, boolean shareCluster) {
+        return publishLocal(message, session -> true);
+    }
+
+    @Override
+    public Flux<TopicMessage> subscribe(Collection<Subscription> subscriptions, boolean shareCluster) {
+        return Flux.defer(() -> {
+            LocalMessageConnection networkConnection = localGatewayConnector.addConnection("local:" + IDGenerator.SNOW_FLAKE_STRING.generate(), shareCluster);
+            return networkConnection
+                .onLocalMessage()
+                .doOnSubscribe(sub -> subscriptions.forEach(networkConnection::addSubscription))
+                .doFinally((s) -> networkConnection.disconnect());
+        });
+    }
+
+    @Override
+    public void registerMessageConnector(MessageConnector connector) {
+        if (null != removeConnector(connector.getId())) {
+            log.warn("connector exists , shutdown it !");
+        }
+
+        Connector _connector = new Connector(connector);
+        connectors.put(connector.getId(), _connector);
+
+        if (started.get()) {
+            _connector.startup();
+        }
+    }
+
+    @Override
+    public MessageConnector removeConnector(String connectorId) {
+
+        Connector connector = connectors.remove(connectorId);
+        if (connector != null) {
+            connector.shutdown();
+            return connector.connector;
+        }
+        return null;
+    }
+
+    private Flux<ClientSession> publishLocal(TopicMessage message,
+                                             Predicate<ConnectionSession> filter) {
+        return Flux.defer(() -> root.find(message.getTopic())
+            .flatMapIterable(TopicPart::getSessionId)
+            .flatMap(id -> Mono.justOrEmpty(sessions.get(id)))
+            .filter(connectionSession -> connectionSession.isAlive() && filter.test(connectionSession))
+            .flatMap(session ->
+                session.connection
+                    .asSubscriber()
+                    .flatMap(subscriber -> subscriber.publish(message))
+                    .doOnSuccess(nil -> {
+                        log.debug("publish message [{}] to session:[{}] complete", message.getTopic(), session.getId());
+                    })
+                    .onErrorContinue((err, se) -> {
+                        log.error("publish message [{}] to session:[{}] error", message.getTopic(), session.getId(), err);
+                    })
+                    .thenReturn(session))
+            .map(ConnectionSession::getSession))
+            ;
+    }
+
+    @Override
+    public void startup() {
+        Flux.interval(Duration.ofSeconds(10))
+            .subscribe();
+        if (!started.getAndSet(true)) {
+            for (Connector value : connectors.values()) {
+                if (value.disposable == null) {
+                    value.startup();
+                }
+            }
+        }
+    }
+
+    @Override
+    public void shutdown() {
+        started.set(false);
+        for (Connector value : connectors.values()) {
+            value.shutdown();
+        }
+    }
+
+    private Mono<Void> dispatch(ConnectionSession from, TopicMessage message) {
+        //转发到其他topic
+        return publishLocal(message, session -> true).then();
+    }
+
+    @Getter
+    class ConnectionSession {
+        String id;
+        Connector connector;
+        ClientSession session;
+
+        MessageConnection connection;
+
+        Disposable disposable;
+
+        boolean onlyConsumeLocal;
+
+        boolean isAlive() {
+            return connection.isAlive();
+        }
+
+        void init() {
+            disposable = connection
+                .asSubscriber()
+                .subscribe(subscriber -> {
+                    subscriber
+                        .onSubscribe()
+                        .takeWhile(r -> isAlive())
+                        .flatMap(subscription -> {
+                            if (log.isDebugEnabled()) {
+                                log.debug("session:[{}] subscribe:[{}]", session.getId(), subscription.getTopic());
+                            }
+                            root.subscribe(subscription.getTopic())
+                                .addSessionId(getId());
+                            return session.addSubscription(subscription)
+                                .thenReturn(subscription);
+                        }).subscribe();
+                    subscriber.onUnSubscribe()
+                        .takeWhile(r -> isAlive())
+                        .flatMap(subscription ->
+                            root.get(subscription.getTopic())
+                                .doOnNext(part -> {
+                                    if (log.isDebugEnabled()) {
+                                        log.debug("session:[{}] unsubscribe:[{}]", session.getId(), part.getTopic());
+                                    }
+                                    part.removeSession(getId());
+                                })
+                                .then(session.removeSubscription(subscription)))
+                        .subscribe();
+                });
+            //加载会话已有的订阅信息
+            session.getSubscriptions()
+                .map(Subscription::getTopic)
+                .flatMap(topic -> root.find(topic))
+                .subscribe(part -> part.addSessionId(getId()));
+        }
+
+        void close() {
+            if (disposable != null && !disposable.isDisposed()) {
+                disposable.dispose();
+            }
+            sessions.remove(getId());
+            //取消订阅
+            session.getSubscriptions()
+                .map(Subscription::getTopic)
+                .flatMap(topic -> root.get(topic))
+                .doOnNext(part -> part.removeSession(getId()))
+                .then(sessionManager.closeSession(DefaultMessageGateway.this.getId(), getId()))
+                .doFinally(s -> log.debug("session [{}] closed", getId()))
+                .subscribe();
+        }
+
+    }
+
+    class Connector {
+        private MessageConnector connector;
+
+        private Disposable disposable;
+
+        public Connector(MessageConnector connector) {
+            this.connector = connector;
+        }
+
+        private void shutdown() {
+            if (disposable != null && !disposable.isDisposed()) {
+                disposable.dispose();
+                disposable = null;
+            }
+        }
+
+        public void startup() {
+            shutdown();
+            disposable = connector
+                .onConnection()
+                .flatMap(connection -> sessionManager
+                    .createSession(getId(), connection)
+                    .map(session -> {
+                        ConnectionSession connectionSession = new ConnectionSession();
+                        connectionSession.connection = connection;
+                        connectionSession.onlyConsumeLocal = connector instanceof LocalMessageConnector;
+                        connectionSession.session = session;
+                        connectionSession.connector = this;
+                        connectionSession.id = session.getId();
+                        return connectionSession;
+                    }))
+                .filter(ConnectionSession::isAlive)
+                .doOnNext(session -> {
+                    sessions.put(session.getId(), session);
+                    session.init();
+                    session.connection.onDisconnect(session::close);
+                    session.getConnection()
+                        .asPublisher()
+                        .flatMapMany(MessagePublisher::onMessage)
+                        .takeWhile(r -> disposable != null)
+                        .flatMap(msg -> dispatch(session, msg))
+                        .onErrorContinue((err, obj) -> {
+                            log.error(err.getMessage(), err);
+                        })
+                        .subscribe();
+                })
+                .subscribe();
+        }
+    }
+}

+ 40 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DefaultMessageGatewayManager.java

@@ -0,0 +1,40 @@
+package org.jetlinks.community.gateway.supports;
+
+import org.jetlinks.community.gateway.MessageGateway;
+import org.jetlinks.community.gateway.MessageGatewayManager;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class DefaultMessageGatewayManager implements MessageGatewayManager, BeanPostProcessor {
+
+    private Map<String, MessageGateway> cache = new ConcurrentHashMap<>();
+
+    @Override
+    public Mono<MessageGateway> getGateway(String id) {
+        return Mono.justOrEmpty(cache.get(id));
+    }
+
+    @Override
+    public Flux<MessageGateway> getAllGateway() {
+        return Flux.fromIterable(cache.values());
+    }
+
+    public void register(MessageGateway gateway) {
+        cache.put(gateway.getId(), gateway);
+    }
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof MessageGateway) {
+            register(((MessageGateway) bean));
+        }
+        return bean;
+    }
+}

+ 22 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProperties.java

@@ -0,0 +1,22 @@
+package org.jetlinks.community.gateway.supports;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Getter
+@Setter
+public class DeviceGatewayProperties {
+
+    private String id;
+
+    private String provider;
+
+    private String networkId;
+
+    private Map<String,Object> configuration=new HashMap<>();
+
+
+}

+ 10 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayPropertiesManager.java

@@ -0,0 +1,10 @@
+package org.jetlinks.community.gateway.supports;
+
+import reactor.core.publisher.Mono;
+
+public interface DeviceGatewayPropertiesManager {
+
+    Mono<DeviceGatewayProperties> getProperties(String id);
+
+
+}

+ 17 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProvider.java

@@ -0,0 +1,17 @@
+package org.jetlinks.community.gateway.supports;
+
+import org.jetlinks.community.gateway.DeviceGateway;
+import org.jetlinks.community.network.NetworkType;
+import reactor.core.publisher.Mono;
+
+public interface DeviceGatewayProvider {
+
+    String getId();
+
+    String getName();
+
+    NetworkType getNetworkType();
+
+    Mono<DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties);
+
+}

+ 57 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/LocalClientSession.java

@@ -0,0 +1,57 @@
+package org.jetlinks.community.gateway.supports;
+
+import lombok.Getter;
+import org.jetlinks.community.gateway.ClientSession;
+import org.jetlinks.community.gateway.Subscription;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+class LocalClientSession implements ClientSession {
+
+    @Getter
+    private String id;
+
+    @Getter
+    private String clientId;
+
+    private Map<String, Subscription> subscriptions = new ConcurrentHashMap<>();
+
+
+    public LocalClientSession(String id) {
+        this.id = id;
+        this.clientId = id;
+    }
+
+    @Override
+    public boolean isPersist() {
+        return false;
+    }
+
+    @Override
+    public Flux<Subscription> getSubscriptions() {
+        return Flux.fromIterable(subscriptions.values());
+    }
+
+    @Override
+    public Mono<Void> addSubscription(Subscription subscription) {
+        return Mono.fromRunnable(() -> subscriptions.put(subscription.getTopic(), subscription));
+    }
+
+    @Override
+    public Mono<Void> removeSubscription(Subscription subscription) {
+        return Mono.fromRunnable(() -> subscriptions.remove(subscription.getTopic()));
+    }
+
+    @Override
+    public boolean isAlive() {
+        return true;
+    }
+
+    @Override
+    public void close() {
+
+    }
+}

+ 58 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/LocalClientSessionManager.java

@@ -0,0 +1,58 @@
+package org.jetlinks.community.gateway.supports;
+
+import org.jetlinks.community.gateway.ClientSession;
+import org.jetlinks.community.gateway.ClientSessionManager;
+import org.jetlinks.community.gateway.MessageConnection;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class LocalClientSessionManager implements ClientSessionManager {
+
+    private Map<String, Map<String, LocalClientSession>> sessionStore = new ConcurrentHashMap<>();
+
+    protected Map<String, LocalClientSession> getGateWaySessionStore(String gateway) {
+        return sessionStore.computeIfAbsent(gateway, __ -> new ConcurrentHashMap<>());
+    }
+
+    @Override
+    public Mono<ClientSession> createSession(String messageGatewayId, MessageConnection connection) {
+        return Mono.fromSupplier(() -> {
+            LocalClientSession session = new LocalClientSession(connection.getId());
+
+            getGateWaySessionStore(messageGatewayId).put(connection.getId(), session);
+            return session;
+        });
+    }
+
+    @Override
+    public Flux<ClientSession> getSessions(String messageGatewayId) {
+        return Flux.fromIterable(getGateWaySessionStore(messageGatewayId).values());
+    }
+
+    @Override
+    public Mono<ClientSession> getSession(String messageGatewayId, String id) {
+        return Mono.justOrEmpty(getGateWaySessionStore(messageGatewayId).get(id));
+    }
+
+    @Override
+    public Flux<ClientSession> getSessions(String gateway, Collection<String> id) {
+        Map<String, LocalClientSession> store = getGateWaySessionStore(gateway);
+
+        return Flux.fromIterable(id)
+                .flatMap(_id -> Mono.justOrEmpty(store.get(_id)));
+    }
+
+    @Override
+    public Mono<Void> closeSession(String gateway, String id) {
+
+        return Mono.fromRunnable(() -> {
+            Optional.ofNullable(getGateWaySessionStore(gateway).remove(id))
+                    .ifPresent(LocalClientSession::close);
+        });
+    }
+}

+ 111 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/LocalMessageConnection.java

@@ -0,0 +1,111 @@
+package org.jetlinks.community.gateway.supports;
+
+import lombok.Getter;
+import org.jetlinks.community.gateway.*;
+import reactor.core.publisher.EmitterProcessor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+
+class LocalMessageConnection implements
+    MessageConnection,
+    MessageSubscriber,
+    MessagePublisher {
+
+    private final List<Runnable> listener = new CopyOnWriteArrayList<>();
+
+    @Getter
+    private String id;
+
+    private boolean shareCluster;
+
+    private AtomicBoolean disconnected = new AtomicBoolean(false);
+
+    private EmitterProcessor<TopicMessage> processor = EmitterProcessor.create(false);
+
+    private FluxSink<TopicMessage> sink = processor.sink();
+
+    private EmitterProcessor<Subscription> subscriptionProcessor = EmitterProcessor.create(false);
+    private EmitterProcessor<Subscription> unsubscriptionProcessor = EmitterProcessor.create(false);
+
+    public LocalMessageConnection(String id, boolean shareCluster) {
+        this.id = id;
+        this.shareCluster = shareCluster;
+    }
+
+    public void addSubscription(Subscription subscription) {
+        subscriptionProcessor.onNext(subscription);
+    }
+
+    public void removeSubscription(Subscription subscription) {
+        unsubscriptionProcessor.onNext(subscription);
+    }
+
+    @Override
+    public void onDisconnect(Runnable disconnectListener) {
+        if (disconnected.get()) {
+            disconnectListener.run();
+            return;
+        }
+        listener.add(disconnectListener);
+    }
+
+    @Override
+    public void disconnect() {
+        listener.forEach(Runnable::run);
+        listener.clear();
+        disconnected.set(true);
+        processor.onComplete();
+        subscriptionProcessor.onComplete();
+        unsubscriptionProcessor.onComplete();
+    }
+
+    @Override
+    public boolean isAlive() {
+        return !disconnected.get();
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Void> publish(@Nonnull TopicMessage message) {
+        return Mono.fromRunnable(() -> {
+            if (processor.hasDownstreams()) {
+                sink.next(message);
+            }
+        });
+    }
+
+    public Flux<TopicMessage> onLocalMessage() {
+        return processor.map(Function.identity());
+    }
+
+    @Nonnull
+    @Override
+    public Flux<TopicMessage> onMessage() {
+
+        return Flux.empty();
+    }
+
+    @Nonnull
+    @Override
+    public Flux<Subscription> onSubscribe() {
+        return subscriptionProcessor.map(Function.identity());
+    }
+
+    @Nonnull
+    @Override
+    public Flux<Subscription> onUnSubscribe() {
+        return unsubscriptionProcessor.map(Function.identity());
+    }
+
+    @Override
+    public boolean isShareCluster() {
+        return shareCluster;
+    }
+}

+ 42 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/LocalMessageConnector.java

@@ -0,0 +1,42 @@
+package org.jetlinks.community.gateway.supports;
+
+import org.jetlinks.community.gateway.MessageConnection;
+import org.jetlinks.community.gateway.MessageConnector;
+import reactor.core.publisher.EmitterProcessor;
+import reactor.core.publisher.Flux;
+
+import javax.annotation.Nonnull;
+import java.util.function.Function;
+
+class LocalMessageConnector implements MessageConnector {
+
+
+    public LocalMessageConnector() {
+
+    }
+
+    @Nonnull
+    @Override
+    public String getId() {
+        return "local";
+    }
+
+    @Override
+    public String getName() {
+        return "本地连接器";
+    }
+
+    EmitterProcessor<MessageConnection> processor = EmitterProcessor.create(false);
+
+    public LocalMessageConnection addConnection(String id, boolean shareCluster) {
+        LocalMessageConnection connection = new LocalMessageConnection(id,shareCluster);
+        processor.onNext(connection);
+        return connection;
+    }
+
+    @Nonnull
+    @Override
+    public Flux<MessageConnection> onConnection() {
+        return processor.map(Function.identity());
+    }
+}

+ 20 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/MessageConnectorProperties.java

@@ -0,0 +1,20 @@
+package org.jetlinks.community.gateway.supports;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Getter
+@Setter
+public class MessageConnectorProperties {
+
+    private String id;
+
+    private String provider;
+
+    private Map<String,Object> configuration=new HashMap<>();
+
+
+}

+ 14 - 0
jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/MessageConnectorProvider.java

@@ -0,0 +1,14 @@
+package org.jetlinks.community.gateway.supports;
+
+import org.jetlinks.community.gateway.MessageConnector;
+import reactor.core.publisher.Mono;
+
+public interface MessageConnectorProvider {
+
+    String getId();
+
+    String getName();
+
+    Mono<MessageConnector> createMessageConnector(MessageConnectorProperties properties);
+
+}

+ 32 - 0
jetlinks-components/io-component/pom.xml

@@ -0,0 +1,32 @@
+<?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>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>io-component</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>2.1.2</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webflux</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-core</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 48 - 0
jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/excel/DefaultImportExportService.java

@@ -0,0 +1,48 @@
+package org.jetlinks.community.io.excel;
+
+import org.jetlinks.community.io.excel.easyexcel.ExcelReadDataListener;
+import org.springframework.core.io.Resource;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Component
+public class DefaultImportExportService implements ImportExportService {
+
+
+    public <T> Flux<RowResult<T>> doImport(Class<T> clazz, String fileUrl) {
+        return getInputStream(fileUrl)
+                .flatMapMany(inputStream -> ExcelReadDataListener.of(inputStream, clazz));
+    }
+
+    @Override
+    public <T> Flux<RowResult<T>> doImport(Class<T> clazz, InputStream stream) {
+        return ExcelReadDataListener.of(stream, clazz);
+    }
+
+    public Mono<InputStream> getInputStream(String fileUrl) {
+
+        return Mono.defer(()->{
+            if (fileUrl.startsWith("http")) {
+               return WebClient.create().get()
+                    .uri(fileUrl)
+                    .accept(MediaType.APPLICATION_OCTET_STREAM)
+                    .exchange()
+                    .flatMap(clientResponse -> clientResponse.bodyToMono(Resource.class))
+                    .flatMap(resource -> Mono.fromCallable(resource::getInputStream));
+            } else {
+                return Mono.fromCallable(()->new FileInputStream(fileUrl));
+            }
+        });
+
+    }
+}

+ 19 - 0
jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/excel/ImportExportService.java

@@ -0,0 +1,19 @@
+package org.jetlinks.community.io.excel;
+
+
+import reactor.core.publisher.Flux;
+
+import java.io.InputStream;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+public interface ImportExportService {
+
+    <T> Flux<RowResult<T>> doImport(Class<T> clazz, String fileUrl);
+
+
+    <T> Flux<RowResult<T>> doImport(Class<T> clazz, InputStream stream);
+
+}

+ 14 - 0
jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/excel/RowResult.java

@@ -0,0 +1,14 @@
+package org.jetlinks.community.io.excel;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class RowResult<T> {
+
+    private int rowIndex;
+
+    private T result;
+
+}

+ 62 - 0
jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/excel/easyexcel/ExcelReadDataListener.java

@@ -0,0 +1,62 @@
+package org.jetlinks.community.io.excel.easyexcel;
+
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.context.AnalysisContext;
+import com.alibaba.excel.event.AnalysisEventListener;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.io.excel.RowResult;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+
+import java.io.InputStream;
+
+/**
+ * 不能用spring管理,每次调用都需要new
+ *
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Slf4j
+public class ExcelReadDataListener<T> extends AnalysisEventListener<T> {
+
+
+    private FluxSink<RowResult<T>> sink;
+
+    public ExcelReadDataListener(FluxSink<RowResult<T>> sink) {
+        this.sink = sink;
+    }
+
+
+    public static <T> Flux<RowResult<T>> of(InputStream fileInputStream, Class<T> clazz) {
+        return Flux.create(sink -> {
+            EasyExcel.read(fileInputStream, clazz, new ExcelReadDataListener<>(sink)).sheet().doRead();
+        });
+    }
+
+    @Override
+    public void onException(Exception exception, AnalysisContext context) {
+        sink.error(exception);
+    }
+
+    /**
+     * 这个每一条数据解析都会来调用
+     */
+    @Override
+    public void invoke(T data, AnalysisContext analysisContext) {
+        RowResult<T> result=new RowResult<>();
+        result.setResult(data);
+        result.setRowIndex(analysisContext.readRowHolder().getRowIndex());
+
+        sink.next(result);
+    }
+
+    @Override
+    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
+        sink.complete();
+    }
+
+    @Override
+    public boolean hasNext(AnalysisContext context) {
+        return !sink.isCancelled();
+    }
+}

+ 2 - 0
jetlinks-components/jetlinks-components.iml

@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4" />

+ 2 - 0
jetlinks-components/network-component/mqtt-component/mqtt-component.iml

@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4" />

+ 52 - 0
jetlinks-components/network-component/mqtt-component/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">
+    <parent>
+        <artifactId>network-component</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>mqtt-component</artifactId>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>rule-engine-support</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.vertx</groupId>
+            <artifactId>vertx-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>io.vertx</groupId>
+            <artifactId>vertx-mqtt</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>jetlinks-core</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>network-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>gateway-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+    </dependencies>
+</project>

+ 16 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClient.java

@@ -0,0 +1,16 @@
+package org.jetlinks.community.network.mqtt.client;
+
+import org.jetlinks.core.message.codec.MqttMessage;
+import org.jetlinks.community.network.Network;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+public interface MqttClient extends Network {
+
+    Flux<MqttMessage> subscribe(List<String> topics);
+
+    Mono<Void> publish(MqttMessage message);
+
+}

+ 22 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClientProperties.java

@@ -0,0 +1,22 @@
+package org.jetlinks.community.network.mqtt.client;
+
+import io.vertx.mqtt.MqttClientOptions;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class MqttClientProperties {
+    private String id;
+    private String clientId;
+    private String host;
+    private int port;
+
+    private String username;
+    private String password;
+
+    private String certId;
+    private MqttClientOptions options;
+    private boolean ssl;
+
+}

+ 93 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/MqttClientProvider.java

@@ -0,0 +1,93 @@
+package org.jetlinks.community.network.mqtt.client;
+
+import io.vertx.core.Vertx;
+import io.vertx.mqtt.MqttClient;
+import io.vertx.mqtt.MqttClientOptions;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.community.network.*;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.community.network.security.CertificateManager;
+import org.jetlinks.community.network.security.VertxKeyCertTrustOptions;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+@Component
+@Slf4j
+public class MqttClientProvider implements NetworkProvider<MqttClientProperties> {
+
+    private final Vertx vertx;
+
+    private final CertificateManager certificateManager;
+
+    public MqttClientProvider(CertificateManager certificateManager, Vertx vertx) {
+        this.vertx = vertx;
+        this.certificateManager = certificateManager;
+    }
+
+    @Nonnull
+    @Override
+    public NetworkType getType() {
+        return DefaultNetworkType.MQTT_CLIENT;
+    }
+
+    @Nonnull
+    @Override
+    public VertxMqttClient createNetwork(@Nonnull MqttClientProperties properties) {
+        VertxMqttClient mqttClient = new VertxMqttClient(properties.getId());
+        initMqttClient(mqttClient, properties);
+        return mqttClient;
+    }
+
+    @Override
+    public void reload(@Nonnull Network network, @Nonnull MqttClientProperties properties) {
+        VertxMqttClient mqttClient = ((VertxMqttClient) network);
+        mqttClient.shutdown();
+
+        initMqttClient(mqttClient, properties);
+    }
+
+    public void initMqttClient(VertxMqttClient mqttClient, MqttClientProperties properties) {
+        MqttClient client = MqttClient.create(vertx, properties.getOptions());
+        client.connect(properties.getPort(), properties.getHost(), result -> {
+            if (!result.succeeded()) {
+                log.warn("connect mqtt [{}] error", properties.getId(), result.cause());
+            } else {
+                mqttClient.setClient(client);
+            }
+        });
+    }
+
+    @Nullable
+    @Override
+    public ConfigMetadata getConfigMetadata() {
+        // TODO: 2019/12/19
+        return null;
+    }
+
+    @Nonnull
+    @Override
+    public Mono<MqttClientProperties> createConfig(@Nonnull NetworkProperties properties) {
+        return Mono.defer(() -> {
+            MqttClientProperties config = FastBeanCopier.copy(properties.getConfigurations(), new MqttClientProperties());
+            config.setId(properties.getId());
+            if (config.getOptions() == null) {
+                config.setOptions(new MqttClientOptions());
+                config.getOptions().setPassword(config.getPassword());
+                config.getOptions().setUsername(config.getUsername());
+            }
+            if (config.isSsl()) {
+                config.getOptions().setSsl(true);
+                return certificateManager.getCertificate(config.getCertId())
+                    .map(VertxKeyCertTrustOptions::new)
+                    .doOnNext(config.getOptions()::setKeyCertOptions)
+                    .doOnNext(config.getOptions()::setTrustOptions)
+                    .thenReturn(config);
+            }
+            return Mono.just(config);
+        });
+    }
+}

+ 174 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/VertxMqttClient.java

@@ -0,0 +1,174 @@
+package org.jetlinks.community.network.mqtt.client;
+
+import io.netty.handler.codec.mqtt.MqttQoS;
+import io.vertx.core.buffer.Buffer;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.core.message.codec.MqttMessage;
+import org.jetlinks.core.message.codec.SimpleMqttMessage;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.supports.utils.MqttTopicUtils;
+import reactor.core.publisher.*;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class VertxMqttClient implements MqttClient {
+
+    @Getter
+    private io.vertx.mqtt.MqttClient client;
+
+    private FluxProcessor<MqttMessage, MqttMessage> messageProcessor;
+
+    private FluxSink<MqttMessage> sink;
+
+    private Map<String, AtomicInteger> topicsSubscribeCounter = new ConcurrentHashMap<>();
+
+    private boolean neverSubscribe = true;
+
+    private String id;
+
+    @Getter
+    private AtomicInteger reloadCounter = new AtomicInteger();
+
+
+    public VertxMqttClient(String id) {
+        this.id = id;
+        this.messageProcessor = EmitterProcessor.create(false);
+        sink = this.messageProcessor.sink();
+    }
+
+    public void setClient(io.vertx.mqtt.MqttClient client) {
+        this.client = client;
+        if (isAlive()) {
+            reloadCounter.set(0);
+            client.publishHandler(msg -> {
+                //从未订阅,可能消息是还没来得及
+                //或者已经有了下游消费者
+                if (neverSubscribe || messageProcessor.hasDownstreams()) {
+                    sink.next(SimpleMqttMessage
+                        .builder()
+                        .topic(msg.topicName())
+                        .clientId(client.clientId())
+                        .qosLevel(msg.qosLevel().value())
+                        .retain(msg.isRetain())
+                        .dup(msg.isDup())
+                        .payload(msg.payload().getByteBuf())
+                        .messageId(msg.messageId())
+                        .build());
+                }
+            });
+            if (!topicsSubscribeCounter.isEmpty()) {
+                Map<String, Integer> reSubscribe = topicsSubscribeCounter
+                    .entrySet()
+                    .stream()
+                    .filter(e -> e.getValue().get() > 0)
+                    .map(Map.Entry::getKey)
+                    .collect(Collectors.toMap(Function.identity(), (r) -> 0));
+                if (!reSubscribe.isEmpty()) {
+                    log.info("re subscribe [{}] topic {}", client.clientId(), reSubscribe.keySet());
+                    client.subscribe(reSubscribe);
+                }
+            }
+        }
+
+    }
+
+    private AtomicInteger getTopicCounter(String topic) {
+        return topicsSubscribeCounter.computeIfAbsent(topic, (ignore) -> new AtomicInteger());
+    }
+
+
+    @Override
+    public Flux<MqttMessage> subscribe(List<String> topics) {
+        neverSubscribe = false;
+        AtomicBoolean canceled = new AtomicBoolean();
+        return Flux.defer(() -> {
+            Map<String, Integer> subscribeTopic = topics.stream()
+                .filter(r -> getTopicCounter(r).getAndIncrement() == 0)
+                .collect(Collectors.toMap(Function.identity(), (r) -> 0));
+            if (isAlive()) {
+                if (!subscribeTopic.isEmpty()) {
+                    log.info("subscribe mqtt [{}] topic : {}", client.clientId(), subscribeTopic);
+                    client.subscribe(subscribeTopic);
+                }
+            }
+            return messageProcessor
+                .filter(msg -> topics
+                    .stream()
+                    .anyMatch(topic -> MqttTopicUtils.match(topic, msg.getTopic())));
+        }).doOnCancel(() -> {
+            if (!canceled.getAndSet(true)) {
+                for (String topic : topics) {
+                    if (getTopicCounter(topic).decrementAndGet() <= 0 && isAlive()) {
+                        log.info("unsubscribe mqtt [{}] topic : {}", client.clientId(), topic);
+                        client.unsubscribe(topic);
+                    }
+                }
+            }
+        });
+    }
+
+    @Override
+    public Mono<Void> publish(MqttMessage message) {
+        return Mono.create((sink) -> {
+            if (!isAlive()) {
+                sink.error(new IOException("mqtt client not alive"));
+                return;
+            }
+            Buffer buffer = Buffer.buffer(message.getPayload());
+            client.publish(message.getTopic(),
+                buffer,
+                MqttQoS.valueOf(message.getQosLevel()),
+                message.isDup(),
+                message.isRetain(),
+                result -> {
+                    if (result.succeeded()) {
+                        log.info("publish mqtt [{}] message success: {}", client.clientId(), message);
+                        sink.success();
+                    } else {
+                        log.info("publish mqtt [{}] message error : {}", client.clientId(), message, result.cause());
+                        sink.error(result.cause());
+                    }
+                });
+        });
+    }
+
+    @Override
+    public String getId() {
+        return id;
+    }
+
+    @Override
+    public NetworkType getType() {
+        return DefaultNetworkType.MQTT_CLIENT;
+    }
+
+    @Override
+    public void shutdown() {
+        if (isAlive()) {
+            client.disconnect();
+            client = null;
+        }
+
+    }
+
+    @Override
+    public boolean isAlive() {
+        return client != null && client.isConnected();
+    }
+
+    @Override
+    public boolean isAutoReload() {
+        return true;
+    }
+
+}

+ 199 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/MqttClientDeviceGateway.java

@@ -0,0 +1,199 @@
+package org.jetlinks.community.network.mqtt.gateway.device;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.core.ProtocolSupport;
+import org.jetlinks.core.ProtocolSupports;
+import org.jetlinks.core.device.*;
+import org.jetlinks.core.message.DeviceMessage;
+import org.jetlinks.core.message.DeviceOfflineMessage;
+import org.jetlinks.core.message.DeviceOnlineMessage;
+import org.jetlinks.core.message.Message;
+import org.jetlinks.core.message.codec.*;
+import org.jetlinks.core.server.MessageHandler;
+import org.jetlinks.community.gateway.DeviceGateway;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.community.network.mqtt.client.MqttClient;
+import org.jetlinks.supports.server.DecodedClientMessageHandler;
+import reactor.core.Disposable;
+import reactor.core.publisher.EmitterProcessor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+
+@Slf4j
+public class MqttClientDeviceGateway implements DeviceGateway {
+
+    @Getter
+    private String id;
+
+    private MqttClient mqttClient;
+
+    private DeviceRegistry registry;
+
+    private List<String> topics;
+
+    private String protocol;
+
+    private ProtocolSupports protocolSupport;
+
+    private DecodedClientMessageHandler clientMessageHandler;
+
+    private MessageHandler messageHandler;
+
+    private EmitterProcessor<Message> messageProcessor = EmitterProcessor.create(false);
+
+    private FluxSink<Message> sink = messageProcessor.sink();
+
+    private AtomicBoolean started = new AtomicBoolean();
+
+    private List<Disposable> disposable = new CopyOnWriteArrayList<>();
+
+    public MqttClientDeviceGateway(String id,
+                                   MqttClient mqttClient,
+                                   DeviceRegistry registry,
+                                   ProtocolSupports protocolSupport,
+                                   String protocol,
+                                   DecodedClientMessageHandler clientMessageHandler,
+                                   MessageHandler messageHandler,
+                                   List<String> topics) {
+        this.id = Objects.requireNonNull(id, "id");
+        this.mqttClient = Objects.requireNonNull(mqttClient, "mqttClient");
+        this.registry = Objects.requireNonNull(registry, "registry");
+        this.protocolSupport = Objects.requireNonNull(protocolSupport, "protocolSupport");
+        this.protocol = Objects.requireNonNull(protocol, "protocol");
+        this.clientMessageHandler = Objects.requireNonNull(clientMessageHandler, "clientMessageHandler");
+        this.messageHandler = Objects.requireNonNull(messageHandler, "messageHandler");
+        this.topics = Objects.requireNonNull(topics, "topics");
+    }
+
+
+
+    protected Mono<ProtocolSupport> getProtocol() {
+        return protocolSupport.getProtocol(protocol);
+    }
+
+    private void doStart() {
+        if (started.getAndSet(true) || !disposable.isEmpty()) {
+            return;
+        }
+
+        messageHandler
+            .handleGetDeviceState(getId(), idPublisher ->
+                Flux.from(idPublisher)
+                    .map(id -> new DeviceStateInfo(id, DeviceState.online)));
+
+        disposable.add(messageHandler
+            .handleSendToDeviceMessage(getId())
+            .filter((msg) -> started.get())
+            .flatMap(msg -> {
+                if (msg instanceof DeviceMessage) {
+                    DeviceMessage deviceMessage = ((DeviceMessage) msg);
+                    return registry.getDevice(deviceMessage.getDeviceId())
+                        .flatMapMany(device -> device.getProtocol()
+                            .flatMapMany(protocol ->
+                                protocol.getMessageCodec(getTransport())
+                                    .flatMapMany(codec -> codec.encode(new MessageEncodeContext() {
+                                        @Override
+                                        public Message getMessage() {
+                                            return deviceMessage;
+                                        }
+
+                                        @Override
+                                        public DeviceOperator getDevice() {
+                                            return device;
+                                        }
+                                    }))))
+                        .flatMap(message -> mqttClient.publish(((MqttMessage) message)));
+                }
+                return Mono.empty();
+            })
+            .onErrorContinue((err, res) -> log.error("处理MQTT消息失败", err))
+            .subscribe());
+
+        disposable.add(mqttClient
+            .subscribe(topics)
+            .filter((msg) -> started.get())
+            .flatMap(mqttMessage -> getProtocol()
+                .flatMap(codec -> codec.getMessageCodec(getTransport()))
+                .flatMapMany(codec -> codec.decode(new MessageDecodeContext() {
+                    @Override
+                    public EncodedMessage getMessage() {
+                        return mqttMessage;
+                    }
+
+                    @Override
+                    public DeviceOperator getDevice() {
+                        throw new UnsupportedOperationException();
+                    }
+                }))
+                .cast(DeviceMessage.class)
+                .flatMap(msg -> {
+                    if (messageProcessor.hasDownstreams()) {
+                        sink.next(msg);
+                    }
+                    return registry
+                        .getDevice(msg.getDeviceId())
+                        .flatMap(device -> {
+                            Mono<Void> handle = clientMessageHandler.handleMessage(device, msg).then();
+                            if (msg instanceof DeviceOfflineMessage) {
+                                handle = handle.then(device.offline().then());
+                            }
+                            if (msg instanceof DeviceOnlineMessage) {
+                                handle = handle.then(device.online(getId(), getId()).then());
+                            }
+                            return handle;
+                        });
+                }))
+            .onErrorContinue((err, res) -> log.error("处理MQTT消息失败", err))
+            .subscribe());
+    }
+
+    @Override
+    public Transport getTransport() {
+        return DefaultTransport.MQTT;
+    }
+
+    @Override
+    public NetworkType getNetworkType() {
+        return DefaultNetworkType.MQTT_CLIENT;
+    }
+
+    @Override
+    public Flux<Message> onMessage() {
+        return messageProcessor.map(Function.identity());
+    }
+
+    @Override
+    public Mono<Void> pause() {
+        return Mono.fromRunnable(() -> started.set(false));
+    }
+
+    @Override
+    public Mono<Void> startup() {
+        return Mono.fromRunnable(this::doStart);
+    }
+
+    @Override
+    public Mono<Void> shutdown() {
+        return Mono.fromRunnable(() -> {
+            started.set(false);
+
+            disposable.forEach(Disposable::dispose);
+
+            disposable.clear();
+        });
+    }
+
+    @Override
+    public boolean isAlive() {
+        return started.get();
+    }
+}

+ 82 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/MqttClientDeviceGatewayProvider.java

@@ -0,0 +1,82 @@
+package org.jetlinks.community.network.mqtt.gateway.device;
+
+import org.jetlinks.core.ProtocolSupports;
+import org.jetlinks.core.device.DeviceRegistry;
+import org.jetlinks.core.server.MessageHandler;
+import org.jetlinks.community.gateway.DeviceGateway;
+import org.jetlinks.community.gateway.supports.DeviceGatewayProperties;
+import org.jetlinks.community.gateway.supports.DeviceGatewayProvider;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkManager;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.community.network.mqtt.client.MqttClient;
+import org.jetlinks.supports.server.DecodedClientMessageHandler;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+@Component
+public class MqttClientDeviceGatewayProvider implements DeviceGatewayProvider {
+
+    private final NetworkManager networkManager;
+
+    private final DeviceRegistry registry;
+
+    private final MessageHandler messageHandler;
+
+    private final DecodedClientMessageHandler clientMessageHandler;
+
+    private final ProtocolSupports protocolSupports;
+
+    public MqttClientDeviceGatewayProvider(NetworkManager networkManager,
+                                           DeviceRegistry registry,
+                                           MessageHandler messageHandler,
+                                           DecodedClientMessageHandler clientMessageHandler,
+                                           ProtocolSupports protocolSupports) {
+        this.networkManager = networkManager;
+        this.registry = registry;
+        this.messageHandler = messageHandler;
+        this.clientMessageHandler = clientMessageHandler;
+        this.protocolSupports = protocolSupports;
+    }
+
+    @Override
+    public String getId() {
+        return "mqtt-client-gateway";
+    }
+
+    @Override
+    public String getName() {
+        return "MQTT客户端设备网关";
+    }
+
+    @Override
+    public NetworkType getNetworkType() {
+        return DefaultNetworkType.MQTT_CLIENT;
+    }
+
+    @Override
+    public Mono<DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties) {
+        return networkManager
+            .<MqttClient>getNetwork(getNetworkType(), properties.getNetworkId())
+            .map(mqttClient -> {
+
+                String protocol = (String) properties.getConfiguration().get("protocol");
+                String topics = (String) properties.getConfiguration().get("topics");
+                Objects.requireNonNull(topics, "topics");
+
+                return new MqttClientDeviceGateway(properties.getId(),
+                    mqttClient,
+                    registry,
+                    protocolSupports,
+                    protocol,
+                    clientMessageHandler,
+                    messageHandler,
+                    Arrays.asList(topics.split("[,;\n]"))
+                );
+
+            });
+    }
+}

+ 194 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/MqttServerDeviceGateway.java

@@ -0,0 +1,194 @@
+package org.jetlinks.community.network.mqtt.gateway.device;
+
+import io.netty.handler.codec.mqtt.MqttConnectReturnCode;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.core.device.AuthenticationResponse;
+import org.jetlinks.core.device.DeviceOperator;
+import org.jetlinks.core.device.DeviceRegistry;
+import org.jetlinks.core.device.MqttAuthenticationRequest;
+import org.jetlinks.core.message.Message;
+import org.jetlinks.core.message.codec.DefaultTransport;
+import org.jetlinks.core.message.codec.EncodedMessage;
+import org.jetlinks.core.message.codec.FromDeviceMessageContext;
+import org.jetlinks.core.message.codec.Transport;
+import org.jetlinks.core.server.session.DeviceSession;
+import org.jetlinks.core.server.session.DeviceSessionManager;
+import org.jetlinks.community.gateway.DeviceGateway;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.community.network.mqtt.gateway.device.session.MqttConnectionSession;
+import org.jetlinks.community.network.mqtt.server.MqttConnection;
+import org.jetlinks.community.network.mqtt.server.MqttServer;
+import org.jetlinks.supports.server.DecodedClientMessageHandler;
+import org.springframework.util.StringUtils;
+import reactor.core.Disposable;
+import reactor.core.publisher.EmitterProcessor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.core.publisher.Mono;
+import reactor.util.function.Tuples;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Function;
+
+@Slf4j
+class MqttServerDeviceGateway implements DeviceGateway {
+
+    @Getter
+    private String id;
+
+    private DeviceRegistry registry;
+
+    private DeviceSessionManager sessionManager;
+
+    private MqttServer mqttServer;
+
+    private DecodedClientMessageHandler messageHandler;
+
+    public MqttServerDeviceGateway(String id,
+                                   DeviceRegistry registry,
+                                   DeviceSessionManager sessionManager,
+                                   MqttServer mqttServer,
+                                   DecodedClientMessageHandler messageHandler) {
+        this.id = id;
+        this.registry = registry;
+        this.sessionManager = sessionManager;
+        this.mqttServer = mqttServer;
+        this.messageHandler = messageHandler;
+    }
+
+    private EmitterProcessor<Message> messageProcessor = EmitterProcessor.create(false);
+
+    private FluxSink<Message> sink = messageProcessor.sink();
+
+    private AtomicBoolean started = new AtomicBoolean();
+
+    private Disposable disposable;
+
+    private void doStart() {
+        if (started.getAndSet(true) || disposable != null) {
+            return;
+        }
+        disposable = mqttServer
+            .handleConnection()
+            .filter(conn -> {
+                if (!started.get()) {
+                    conn.reject(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE);
+                }
+                return started.get();
+            })
+            .flatMap(con -> Mono.justOrEmpty(con.getAuth())
+                //没有认证信息,则拒绝连接.
+                .switchIfEmpty(Mono.fromRunnable(() -> con.reject(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED)))
+                .flatMap(auth ->
+                    registry.getDevice(con.getClientId())
+                        .flatMap(device -> device
+                            .authenticate(new MqttAuthenticationRequest(con.getClientId(), auth.getUsername(), auth.getPassword(), getTransport()))
+                            .switchIfEmpty(Mono.fromRunnable(() -> con.reject(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD)))
+                            .flatMap(resp -> {
+                                String deviceId = StringUtils.isEmpty(resp.getDeviceId()) ? device.getDeviceId() : resp.getDeviceId();
+                                //认证返回了新的设备ID,则使用新的设备
+                                if (!deviceId.equals(device.getDeviceId())) {
+                                    return registry
+                                        .getDevice(deviceId)
+                                        .map(operator -> Tuples.of(operator, resp, con));
+                                }
+                                return Mono.just(Tuples.of(device, resp, con));
+                            })
+                        ))
+                //设备注册信息不存在,拒绝连接
+                .switchIfEmpty(Mono.fromRunnable(() -> con.reject(MqttConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED)))
+                .onErrorContinue((err, res) -> {
+                    con.reject(MqttConnectReturnCode.CONNECTION_REFUSED_SERVER_UNAVAILABLE);
+                    log.error("MQTT连接认证[{}]失败", con.getClientId(), err);
+                }))
+            .flatMap(tuple3 -> {
+                DeviceOperator device = tuple3.getT1();
+                AuthenticationResponse resp = tuple3.getT2();
+                MqttConnection con = tuple3.getT3();
+                String deviceId = device.getDeviceId();
+                if (resp.isSuccess()) {
+                    DeviceSession session = new MqttConnectionSession(deviceId, device, getTransport(), con);
+                    sessionManager.register(session);
+                    con.onClose(conn -> sessionManager.unregister(deviceId));
+                    return Mono.just(Tuples.of(con.accept(), device, session));
+                } else {
+                    log.warn("MQTT客户端认证[{}]失败:{}", deviceId, resp.getMessage());
+                }
+                return Mono.empty();
+            })
+            .onErrorContinue((err, res) -> log.error("处理MQTT连接失败", err))
+            .subscribe(tp -> tp.getT1()
+                .handleMessage()
+                .filter(pb -> started.get())
+                .takeWhile(pub -> disposable != null)
+                .flatMap(publishing -> tp.getT2()
+                    .getProtocol()
+                    .flatMap(protocol -> protocol.getMessageCodec(getTransport()))
+                    .flatMapMany(codec -> codec.decode(new FromDeviceMessageContext() {
+                        @Override
+                        public DeviceSession getSession() {
+                            return tp.getT3();
+                        }
+
+                        @Override
+                        public EncodedMessage getMessage() {
+                            return publishing.getMessage();
+                        }
+                    }))
+                    .flatMap(msg -> {
+                        if (messageProcessor.hasDownstreams()) {
+                            sink.next(msg);
+                        }
+                        return messageHandler.handleMessage(tp.getT2(), msg);
+                    })
+                    .onErrorContinue((err, res) -> log.error("处理MQTT连接[{}]消息失败:{}", tp.getT2().getDeviceId(), publishing.getMessage(), err)))
+                .subscribe()
+            );
+
+    }
+
+    @Override
+    public Transport getTransport() {
+        return DefaultTransport.MQTT;
+    }
+
+    @Override
+    public NetworkType getNetworkType() {
+        return DefaultNetworkType.MQTT_SERVER;
+    }
+
+    @Override
+    public Flux<Message> onMessage() {
+        return messageProcessor
+            .map(Function.identity());
+    }
+
+    @Override
+    public Mono<Void> pause() {
+        return Mono.fromRunnable(() -> started.set(false));
+    }
+
+    @Override
+    public Mono<Void> startup() {
+        return Mono.fromRunnable(this::doStart);
+    }
+
+    @Override
+    public Mono<Void> shutdown() {
+        return Mono.fromRunnable(() -> {
+            started.set(false);
+            if (disposable != null && !disposable.isDisposed()) {
+                disposable.dispose();
+            }
+            disposable = null;
+        });
+    }
+
+    @Override
+    public boolean isAlive() {
+        return started.get();
+    }
+
+}

+ 62 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/MqttServerDeviceGatewayProvider.java

@@ -0,0 +1,62 @@
+package org.jetlinks.community.network.mqtt.gateway.device;
+
+import org.jetlinks.core.device.DeviceRegistry;
+import org.jetlinks.core.server.session.DeviceSessionManager;
+import org.jetlinks.community.gateway.DeviceGateway;
+import org.jetlinks.community.gateway.supports.DeviceGatewayProperties;
+import org.jetlinks.community.gateway.supports.DeviceGatewayProvider;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkManager;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.community.network.mqtt.server.MqttServer;
+import org.jetlinks.supports.server.DecodedClientMessageHandler;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+@Component
+public class MqttServerDeviceGatewayProvider implements DeviceGatewayProvider {
+
+    private final NetworkManager networkManager;
+
+    private final DeviceRegistry registry;
+
+    private final DeviceSessionManager sessionManager;
+
+    private final DecodedClientMessageHandler messageHandler;
+
+    public MqttServerDeviceGatewayProvider(NetworkManager networkManager,
+                                           DeviceRegistry registry,
+                                           DeviceSessionManager sessionManager,
+                                           DecodedClientMessageHandler messageHandler) {
+        this.networkManager = networkManager;
+        this.registry = registry;
+        this.sessionManager = sessionManager;
+        this.messageHandler = messageHandler;
+    }
+
+    @Override
+    public String getId() {
+        return "mqtt-server-gateway";
+    }
+
+    @Override
+    public String getName() {
+        return "MQTT服务设备网关";
+    }
+
+    @Override
+    public NetworkType getNetworkType() {
+        return DefaultNetworkType.MQTT_SERVER;
+    }
+
+    @Override
+    public Mono<DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties) {
+        return networkManager
+            .<MqttServer>getNetwork(getNetworkType(), properties.getNetworkId())
+            .map(mqttServer -> {
+                MqttServerDeviceGateway gateway = new MqttServerDeviceGateway(properties.getId(), registry, sessionManager, mqttServer, messageHandler);
+
+                return gateway;
+            });
+    }
+}

+ 0 - 0
jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/session/MqttConnectionSession.java


Some files were not shown because too many files changed in this diff