Compare commits

..

2 Commits
2.11 ... 1.4

Author SHA1 Message Date
zhouhao a0ae73e60e 修复规则功能调用参数表达式错误 2021-02-09 14:42:10 +08:00
zhouhao ebd74dc485 修复设备名称错误 2020-09-05 15:47:50 +08:00
1742 changed files with 20085 additions and 134440 deletions

View File

@ -3,318 +3,10 @@ root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = tab
indent_style = space
insert_final_newline = false
max_line_length = 120
tab_width = 4
trim_trailing_whitespace = true
ij_continuation_indent_size = 4
ij_formatter_off_tag = @formatter:off
ij_formatter_on_tag = @formatter:on
ij_formatter_tags_enabled = true
ij_smart_tabs = false
ij_visual_guides =
ij_wrap_on_typing = true
[*.java]
ij_wrap_on_typing = false
ij_java_align_consecutive_assignments = false
ij_java_align_consecutive_variable_declarations = false
ij_java_align_group_field_declarations = false
ij_java_align_multiline_annotation_parameters = false
ij_java_align_multiline_array_initializer_expression = false
ij_java_align_multiline_assignment = false
ij_java_align_multiline_binary_operation = false
ij_java_align_multiline_chained_methods = true
ij_java_align_multiline_deconstruction_list_components = true
ij_java_align_multiline_extends_list = false
ij_java_align_multiline_for = true
ij_java_align_multiline_method_parentheses = false
ij_java_align_multiline_parameters = true
ij_java_align_multiline_parameters_in_calls = true
ij_java_align_multiline_parenthesized_expression = false
ij_java_align_multiline_records = true
ij_java_align_multiline_resources = true
ij_java_align_multiline_ternary_operation = false
ij_java_align_multiline_text_blocks = false
ij_java_align_multiline_throws_list = false
ij_java_align_subsequent_simple_methods = false
ij_java_align_throws_keyword = false
ij_java_align_types_in_multi_catch = true
ij_java_annotation_parameter_wrap = off
ij_java_array_initializer_new_line_after_left_brace = false
ij_java_array_initializer_right_brace_on_new_line = false
ij_java_array_initializer_wrap = off
ij_java_assert_statement_colon_on_next_line = false
ij_java_assert_statement_wrap = off
ij_java_assignment_wrap = off
ij_java_binary_operation_sign_on_next_line = false
ij_java_binary_operation_wrap = off
ij_java_blank_lines_after_anonymous_class_header = 0
ij_java_blank_lines_after_class_header = 0
ij_java_blank_lines_after_imports = 1
ij_java_blank_lines_after_package = 1
ij_java_blank_lines_around_class = 1
ij_java_blank_lines_around_field = 0
ij_java_blank_lines_around_field_in_interface = 0
ij_java_blank_lines_around_initializer = 1
ij_java_blank_lines_around_method = 1
ij_java_blank_lines_around_method_in_interface = 1
ij_java_blank_lines_before_class_end = 0
ij_java_blank_lines_before_imports = 1
ij_java_blank_lines_before_method_body = 0
ij_java_blank_lines_before_package = 0
ij_java_block_brace_style = end_of_line
ij_java_block_comment_add_space = false
ij_java_block_comment_at_first_column = true
ij_java_builder_methods =
ij_java_call_parameters_new_line_after_left_paren = false
ij_java_call_parameters_right_paren_on_new_line = false
ij_java_call_parameters_wrap = off
ij_java_case_statement_on_separate_line = true
ij_java_catch_on_new_line = false
ij_java_class_annotation_wrap = split_into_lines
ij_java_class_brace_style = end_of_line
ij_java_class_count_to_use_import_on_demand = 5
ij_java_class_names_in_javadoc = 1
ij_java_deconstruction_list_wrap = normal
ij_java_do_not_indent_top_level_class_members = false
ij_java_do_not_wrap_after_single_annotation = false
ij_java_do_not_wrap_after_single_annotation_in_parameter = false
ij_java_do_while_brace_force = never
ij_java_doc_add_blank_line_after_description = true
ij_java_doc_add_blank_line_after_param_comments = false
ij_java_doc_add_blank_line_after_return = false
ij_java_doc_add_p_tag_on_empty_lines = true
ij_java_doc_align_exception_comments = true
ij_java_doc_align_param_comments = true
ij_java_doc_do_not_wrap_if_one_line = false
ij_java_doc_enable_formatting = true
ij_java_doc_enable_leading_asterisks = true
ij_java_doc_indent_on_continuation = false
ij_java_doc_keep_empty_lines = true
ij_java_doc_keep_empty_parameter_tag = true
ij_java_doc_keep_empty_return_tag = true
ij_java_doc_keep_empty_throws_tag = true
ij_java_doc_keep_invalid_tags = true
ij_java_doc_param_description_on_new_line = false
ij_java_doc_preserve_line_breaks = false
ij_java_doc_use_throws_not_exception_tag = true
ij_java_else_on_new_line = false
ij_java_entity_dd_prefix =
ij_java_entity_dd_suffix = EJB
ij_java_entity_eb_prefix =
ij_java_entity_eb_suffix = Bean
ij_java_entity_hi_prefix =
ij_java_entity_hi_suffix = Home
ij_java_entity_lhi_prefix = Local
ij_java_entity_lhi_suffix = Home
ij_java_entity_li_prefix = Local
ij_java_entity_li_suffix =
ij_java_entity_pk_class = java.lang.String
ij_java_entity_ri_prefix =
ij_java_entity_ri_suffix =
ij_java_entity_vo_prefix =
ij_java_entity_vo_suffix = VO
ij_java_enum_constants_wrap = off
ij_java_extends_keyword_wrap = off
ij_java_extends_list_wrap = off
ij_java_field_annotation_wrap = split_into_lines
ij_java_field_name_prefix =
ij_java_field_name_suffix =
ij_java_filter_class_prefix =
ij_java_filter_class_suffix =
ij_java_filter_dd_prefix =
ij_java_filter_dd_suffix =
ij_java_finally_on_new_line = false
ij_java_for_brace_force = never
ij_java_for_statement_new_line_after_left_paren = false
ij_java_for_statement_right_paren_on_new_line = false
ij_java_for_statement_wrap = off
ij_java_generate_final_locals = false
ij_java_generate_final_parameters = false
ij_java_if_brace_force = never
ij_java_imports_layout = *, |, javax.**, java.**, |, $*
ij_java_indent_case_from_switch = true
ij_java_insert_inner_class_imports = false
ij_java_insert_override_annotation = true
ij_java_keep_blank_lines_before_right_brace = 2
ij_java_keep_blank_lines_between_package_declaration_and_header = 2
ij_java_keep_blank_lines_in_code = 2
ij_java_keep_blank_lines_in_declarations = 2
ij_java_keep_builder_methods_indents = false
ij_java_keep_control_statement_in_one_line = true
ij_java_keep_first_column_comment = true
ij_java_keep_indents_on_empty_lines = false
ij_java_keep_line_breaks = true
ij_java_keep_multiple_expressions_in_one_line = false
ij_java_keep_simple_blocks_in_one_line = false
ij_java_keep_simple_classes_in_one_line = false
ij_java_keep_simple_lambdas_in_one_line = false
ij_java_keep_simple_methods_in_one_line = false
ij_java_label_indent_absolute = false
ij_java_label_indent_size = 0
ij_java_lambda_brace_style = end_of_line
ij_java_layout_static_imports_separately = true
ij_java_line_comment_add_space = false
ij_java_line_comment_add_space_on_reformat = false
ij_java_line_comment_at_first_column = true
ij_java_listener_class_prefix =
ij_java_listener_class_suffix =
ij_java_local_variable_name_prefix =
ij_java_local_variable_name_suffix =
ij_java_message_dd_prefix =
ij_java_message_dd_suffix = EJB
ij_java_message_eb_prefix =
ij_java_message_eb_suffix = Bean
ij_java_method_annotation_wrap = split_into_lines
ij_java_method_brace_style = end_of_line
ij_java_method_call_chain_wrap = on_every_item
ij_java_method_parameters_new_line_after_left_paren = false
ij_java_method_parameters_right_paren_on_new_line = false
ij_java_method_parameters_wrap = off
ij_java_modifier_list_wrap = false
ij_java_multi_catch_types_wrap = normal
ij_java_names_count_to_use_import_on_demand = 3
ij_java_new_line_after_lparen_in_annotation = false
ij_java_new_line_after_lparen_in_deconstruction_pattern = true
ij_java_new_line_after_lparen_in_record_header = false
ij_java_packages_to_use_import_on_demand = java.awt.*, javax.swing.*
ij_java_parameter_annotation_wrap = off
ij_java_parameter_name_prefix =
ij_java_parameter_name_suffix =
ij_java_parentheses_expression_new_line_after_left_paren = false
ij_java_parentheses_expression_right_paren_on_new_line = false
ij_java_place_assignment_sign_on_next_line = false
ij_java_prefer_longer_names = true
ij_java_prefer_parameters_wrap = false
ij_java_record_components_wrap = normal
ij_java_repeat_synchronized = true
ij_java_replace_instanceof_and_cast = false
ij_java_replace_null_check = true
ij_java_replace_sum_lambda_with_method_ref = true
ij_java_resource_list_new_line_after_left_paren = false
ij_java_resource_list_right_paren_on_new_line = false
ij_java_resource_list_wrap = off
ij_java_rparen_on_new_line_in_annotation = false
ij_java_rparen_on_new_line_in_deconstruction_pattern = true
ij_java_rparen_on_new_line_in_record_header = false
ij_java_servlet_class_prefix =
ij_java_servlet_class_suffix =
ij_java_servlet_dd_prefix =
ij_java_servlet_dd_suffix =
ij_java_session_dd_prefix =
ij_java_session_dd_suffix = EJB
ij_java_session_eb_prefix =
ij_java_session_eb_suffix = Bean
ij_java_session_hi_prefix =
ij_java_session_hi_suffix = Home
ij_java_session_lhi_prefix = Local
ij_java_session_lhi_suffix = Home
ij_java_session_li_prefix = Local
ij_java_session_li_suffix =
ij_java_session_ri_prefix =
ij_java_session_ri_suffix =
ij_java_session_si_prefix =
ij_java_session_si_suffix = Service
ij_java_space_after_closing_angle_bracket_in_type_argument = false
ij_java_space_after_colon = true
ij_java_space_after_comma = true
ij_java_space_after_comma_in_type_arguments = true
ij_java_space_after_for_semicolon = true
ij_java_space_after_quest = true
ij_java_space_after_type_cast = true
ij_java_space_before_annotation_array_initializer_left_brace = false
ij_java_space_before_annotation_parameter_list = false
ij_java_space_before_array_initializer_left_brace = false
ij_java_space_before_catch_keyword = true
ij_java_space_before_catch_left_brace = true
ij_java_space_before_catch_parentheses = true
ij_java_space_before_class_left_brace = true
ij_java_space_before_colon = true
ij_java_space_before_colon_in_foreach = true
ij_java_space_before_comma = false
ij_java_space_before_deconstruction_list = false
ij_java_space_before_do_left_brace = true
ij_java_space_before_else_keyword = true
ij_java_space_before_else_left_brace = true
ij_java_space_before_finally_keyword = true
ij_java_space_before_finally_left_brace = true
ij_java_space_before_for_left_brace = true
ij_java_space_before_for_parentheses = true
ij_java_space_before_for_semicolon = false
ij_java_space_before_if_left_brace = true
ij_java_space_before_if_parentheses = true
ij_java_space_before_method_call_parentheses = false
ij_java_space_before_method_left_brace = true
ij_java_space_before_method_parentheses = false
ij_java_space_before_opening_angle_bracket_in_type_parameter = false
ij_java_space_before_quest = true
ij_java_space_before_switch_left_brace = true
ij_java_space_before_switch_parentheses = true
ij_java_space_before_synchronized_left_brace = true
ij_java_space_before_synchronized_parentheses = true
ij_java_space_before_try_left_brace = true
ij_java_space_before_try_parentheses = true
ij_java_space_before_type_parameter_list = false
ij_java_space_before_while_keyword = true
ij_java_space_before_while_left_brace = true
ij_java_space_before_while_parentheses = true
ij_java_space_inside_one_line_enum_braces = false
ij_java_space_within_empty_array_initializer_braces = false
ij_java_space_within_empty_method_call_parentheses = false
ij_java_space_within_empty_method_parentheses = false
ij_java_spaces_around_additive_operators = true
ij_java_spaces_around_annotation_eq = true
ij_java_spaces_around_assignment_operators = true
ij_java_spaces_around_bitwise_operators = true
ij_java_spaces_around_equality_operators = true
ij_java_spaces_around_lambda_arrow = true
ij_java_spaces_around_logical_operators = true
ij_java_spaces_around_method_ref_dbl_colon = false
ij_java_spaces_around_multiplicative_operators = true
ij_java_spaces_around_relational_operators = true
ij_java_spaces_around_shift_operators = true
ij_java_spaces_around_type_bounds_in_type_parameters = true
ij_java_spaces_around_unary_operator = false
ij_java_spaces_within_angle_brackets = false
ij_java_spaces_within_annotation_parentheses = false
ij_java_spaces_within_array_initializer_braces = false
ij_java_spaces_within_braces = false
ij_java_spaces_within_brackets = false
ij_java_spaces_within_cast_parentheses = false
ij_java_spaces_within_catch_parentheses = false
ij_java_spaces_within_deconstruction_list = false
ij_java_spaces_within_for_parentheses = false
ij_java_spaces_within_if_parentheses = false
ij_java_spaces_within_method_call_parentheses = false
ij_java_spaces_within_method_parentheses = false
ij_java_spaces_within_parentheses = false
ij_java_spaces_within_record_header = false
ij_java_spaces_within_switch_parentheses = false
ij_java_spaces_within_synchronized_parentheses = false
ij_java_spaces_within_try_parentheses = false
ij_java_spaces_within_while_parentheses = false
ij_java_special_else_if_treatment = true
ij_java_static_field_name_prefix =
ij_java_static_field_name_suffix =
ij_java_subclass_name_prefix =
ij_java_subclass_name_suffix = Impl
ij_java_ternary_operation_signs_on_next_line = false
ij_java_ternary_operation_wrap = off
ij_java_test_name_prefix =
ij_java_test_name_suffix = Test
ij_java_throws_keyword_wrap = off
ij_java_throws_list_wrap = off
ij_java_use_external_annotations = false
ij_java_use_fq_class_names = false
ij_java_use_relative_indents = false
ij_java_use_single_class_imports = true
ij_java_variable_annotation_wrap = off
ij_java_visibility = public
ij_java_while_brace_force = never
ij_java_while_on_new_line = false
ij_java_wrap_comments = false
ij_java_wrap_first_method_in_call_chain = true
ij_java_wrap_long_lines = false
indent_style = space
indent_size = tab
tab_width = 4
trim_trailing_whitespace = true
insert_final_newline = false

View File

@ -1,27 +1,26 @@
name: Auto Deploy Docker
on:
push:
branches: [ "master","2.0","2.1","2.2" ]
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Cache Maven Repository
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.m2
key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }}
key: jetlinks-community-maven-repository
- name: Build with Maven
run: ./mvnw clean install -Dmaven.build.timestamp="$(date "+%Y-%m-%d %H:%M:%S")" -Dmaven.test.skip=true -Pbuild && cd jetlinks-standalone && docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-community:$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) .
run: mvn clean install -Dmaven.test.skip=true -Pbuild && cd jetlinks-standalone && docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) .
- name: Login Docker Repo
run: echo "${{ secrets.ALIYUN_DOCKER_REPO_PWD }}" | docker login registry.cn-shenzhen.aliyuncs.com -u ${{ secrets.ALIYUN_DOCKER_REPO_USERNAME }} --password-stdin
- name: Push Docker
run: docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-community:$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
run: docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)

View File

@ -1,45 +0,0 @@
name: Auto Deploy Docker
on:
push:
branches: [ "2.10","2.11" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Cache Maven Repository
uses: actions/cache@v3
with:
path: ~/.m2
key: ${{ runner.os }}-${{ hashFiles('**/pom.xml') }}
- name: Build with Maven
run: ./mvnw clean install -Dmaven.build.timestamp="$(date "+%Y-%m-%d %H:%M:%S")" -Dmaven.test.skip=true -Pbuild
- name: Get Maven project version
id: maven-version
run: |
VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Project version: $VERSION"
- name: Log in to Aliyun ACR
uses: docker/login-action@v3
with:
registry: registry.cn-shenzhen.aliyuncs.com
username: ${{ secrets.ALIYUN_DOCKER_REPO_USERNAME }}
password: ${{ secrets.ALIYUN_DOCKER_REPO_PWD }}
- name: build and docker image
uses: docker/build-push-action@v6
with:
context: ./jetlinks-standalone
file: ./jetlinks-standalone/Dockerfile
platforms: linux/amd64,linux/arm64/v8
push: true
tags: |
registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-community:${{ steps.maven-version.outputs.version }}

View File

@ -1,27 +0,0 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Java CI with Maven
on:
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Cache Maven Repository
uses: actions/cache@v4.2.3
with:
path: ~/.m2
key: jetlinks-community-maven-repository
- name: Build with Maven
run: ./mvnw package -Dmaven.test.skip=true -Pbuild

View File

@ -1,27 +0,0 @@
# This workflow will build a Java project with Maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Pull Request 2.10 with java17
on:
pull_request:
branches: [ "2.10","2.11" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
- name: Cache Maven Repository
uses: actions/cache@v4.2.3
with:
path: ~/.m2
key: jetlinks-community-maven-repository
- name: Build with Maven
run: ./mvnw package -Dmaven.test.skip=true -Pbuild

8
.gitignore vendored
View File

@ -20,14 +20,10 @@ hs_err_pid*
**/transaction-logs/
!/.mvn/wrapper/maven-wrapper.jar
*.db
/data/
data/
/static/
/upload
/ui/upload/
docker/data
!device-simulator.jar
!demo-protocol-1.0.jar
application-local.yml
dev/
.DS_Store
.java-version
!demo-protocol-1.0.jar

View File

@ -1 +1 @@
distributionUrl=https://archive.apache.org/dist/maven/maven-3/3.9.3/binaries/apache-maven-3.9.3-bin.zip
distributionUrl=http://mirrors.hust.edu.cn/apache/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.zip

View File

@ -1,11 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="JetLinksApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot">
<option name="ACTIVE_PROFILES" value="default,dev,local" />
<module name="jetlinks-standalone" />
<option name="SPRING_BOOT_MAIN_CLASS" value="org.jetlinks.community.standalone.JetLinksApplication" />
<option name="VM_PARAMETERS" value="--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.util.concurrent=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.scripting/javax.script=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED -XX:+EnableDynamicAgentLoading" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

142
README.md
View File

@ -1,66 +1,45 @@
# JetLinks 物联网基础平台
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/jetlinks/jetlinks-community/maven.yml?branch=master)
![Version](https://img.shields.io/badge/version-2.11-brightgreen)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/e8d527d692c24633aba4f869c1c5d6ad)](https://app.codacy.com/gh/jetlinks/jetlinks-community?utm_source=github.com&utm_medium=referral&utm_content=jetlinks/jetlinks-community&utm_campaign=Badge_Grade_Settings)
[![OSCS Status](https://www.oscs1024.com/platform/badge/jetlinks/jetlinks-community.svg?size=small)](https://www.oscs1024.com/project/jetlinks/jetlinks-community?ref=badge_small)
[![star](https://img.shields.io/github/stars/jetlinks/jetlinks-community?style=social)](https://github.com/jetlinks/jetlinks-community)
[![star](https://gitee.com/jetlinks/jetlinks-community/badge/star.svg?theme=gvp)](https://gitee.com/jetlinks/jetlinks-community/stargazers)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/jetlinks/jetlinks-community/Auto%20Deploy%20Docker?label=docker)
![Version](https://img.shields.io/badge/Version-1.4--RELEASE-brightgreen)
![QQ群2021514](https://img.shields.io/badge/QQ群-2021514-brightgreen)
![jetlinks](https://visitor-badge.glitch.me/badge?page_id=jetlinks)
[![QQ⑥群572077464](https://img.shields.io/badge/QQ⑥群-572077464-brightgreen)](https://qm.qq.com/q/kLT3trlXuE)
[![QQ⑤群554591908](https://img.shields.io/badge/QQ⑤群-554591908-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=jiirLiyFUecy_gsankzVQ-cl6SrZCnv9&&jump_from=webapi)
[![QQ④群780133058](https://img.shields.io/badge/QQ④群-780133058-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=Gj47w9kg7TlV5ceD5Bqew_M_O0PIjh_l&jump_from=webapi)
[![QQ③群647954464](https://img.shields.io/badge/QQ③群-647954464-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=K5m27CkhDn3B_Owr-g6rfiTBC5DKEY59&jump_from=webapi)
[![QQ②群324606263](https://img.shields.io/badge/QQ②群-324606263-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=IMas2cH-TNsYxUcY8lRbsXqPnA2sGHYQ&jump_from=webapi)
[![QQ①群2021514](https://img.shields.io/badge/QQ①群-2021514-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=LGf0OPQqvLGdJIZST3VTcypdVWhdfAOG&jump_from=webapi)
JetLinks 2.1x 基于Java 17,Spring Boot 3.x,WebFlux,Netty,Vert.x,Reactor等开发,
JetLinks 基于Java8,Spring Boot 2.x,WebFlux,Netty,Vert.x,Reactor等开发,
是一个开箱即用,可二次开发的企业级物联网基础平台。平台实现了物联网相关的众多基础功能,
能帮助你快速建立物联网相关业务系统。
## 核心特性
#### 开放源代码
支持统一物模型管理,多种设备,多种厂家,统一管理。
全部源代码开放,可自由拓展功能,不再受制于人.前后端分离,接口全开放
统一设备连接管理,多协议适配(TCP,MQTT,UDP,CoAP,HTTP等),屏蔽网络编程复杂性,灵活接入不同厂家不同协议的设备
#### 部署简单
灵活的规则引擎,设备告警,消息通知,数据转发.可基于SQL进行复杂的数据处理逻辑.
最小化运行仅需要`java 17`,`redis`,`timescaledb`即可,无需部署大量中间件。
地理位置:统一管理地理位置信息,支持区域搜索.
#### 统一设备接入,海量设备管理
TCP/UDP/MQTT/HTTP、TLS/DTLS、不同厂商、不同设备、不同报文、统一接入统一管理。
#### 规则引擎
灵活的规则模型配置,支持多种规则模型以及自定义规则模型. 设备告警,场景联动,均由统一的规则引擎管理。
#### 数据权限控制
灵活的非侵入数据权限控制。可实现菜单、按钮、数据三维维度的数据权限控制。可控制单条数据的操作权限。
官方QQ群: `2021514`
## 技术栈
1. [Spring Boot 3.4.x](https://spring.io/projects/spring-boot)
1. [Spring Boot 2.3.x](https://spring.io/projects/spring-boot)
2. [Spring WebFlux](https://spring.io/) 响应式Web支持
3. [R2DBC](https://r2dbc.io/) 响应式关系型数据库驱动
4. [Project Reactor](https://projectreactor.io/) 响应式编程框架
5. [Netty](https://netty.io/),[Vert.x](https://vertx.io/) 高性能网络编程框架
6. [hsweb framework 4](https://github.com/hs-web) 业务功能基础框架
7. [ElasticSearch](https://www.elastic.co/cn/products/enterprise-search) 全文检索,日志,时序数据存储 (可选)
8. [TDengine](https://www.taosdata.com/) 设备时序数据存储(可选)
9. [Redis](https://redis.io/) 缓存数据
10. [TimescaleDB](https://www.timescale.com/) 时序数据存储(可选)
11. [PostgreSQL](https://www.postgresql.org) 业务功能数据管理
4. [Netty](https://netty.io/) ,[Vert.x](https://vertx.io/) 高性能网络编程框架
5. [ElasticSearch](https://www.elastic.co/cn/products/enterprise-search) 全文检索,日志,时序数据存储
6. [PostgreSQL](https://www.postgresql.org) 业务功能数据管理
7. [hsweb framework 4](https://github.com/hs-web) 业务功能基础框架
## 架构
![platform](./platform.png)
![platform](./platform.svg)
## 设备接入流程
![device-flow](./device-flow.png)
![flow](./flow.svg)
## 模块
@ -68,61 +47,50 @@ TCP/UDP/MQTT/HTTP、TLS/DTLS、不同厂商、不同设备、不同报文、统
--jetlinks-community
------|----docker
------|------|----dev-env # 启动开发环境
------|------|----run-all # 启动全部,通过http://localhost:8848 访问系统.
------|------|----run-all # 启动全部,通过http://localhost:9000 访问系统.
------|----jetlinks-components # 公共组件模块
------|-------|----common-component # 通用组件.
------|-------|----configuration-component # 通用配置.
------|-------|----dashboard-component # 仪表盘.
------|-------|----datasource-component # 数据源.
------|-------|----elasticsearch-component # elasticsearch集成.
------|-------|----gateway-component # 网关组件,消息网关,设备接入.
------|-------|----io-component # IO 组件,Excel导入导出等.
------|-------|----logging-component # 日志组件
------|-------|----network-component # 网络组件,MQTT,TCP,CoAP,UDP等
------|-------|----notify-component # 通知组件,短信,右键等通知
------|-------|----protocol-component # 协议组件
------|-------|----relation-component # 关系组件
------|-------|----rule-engine-component # 规则引擎
------|-------|----script-component # 脚本组件
------|-------|----timeseries-component # 时序数据组件
------|-------|----tdengine-component # TDengine集成
------|-------|----things-component # 物组件
------|----jetlinks-manager # 业务管理模块
------|-------|----authentication-manager # 用户,权限管理
------|-------|----device-manager # 设备管理
------|-------|----logging-manager # 日志管理
------|-------|----network-manager # 网络组件管理
------|-------|----notify-manager # 通知管理
------|-------|----visualization-manager # 数据可视化管理
------|-------|----rule-engine-manager # 规则引擎管理
------|----jetlinks-standalone # 服务启动模块
------|----simulator # 设备模拟器
```
## 服务支持
我们提供了各种服务方式帮助您深入了解物联网平台和代码,通过产品文档、技术交流群、付费教学等方式,你将获得如下服务:
| 服务项 | 服务内容 | 服务收费 | 服务方式 |
|----------|-----------------------------------------------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 基础问题答疑 | 问题答疑 | 免费 | 技术交流群支持 [![QQ⑤群554591908](https://img.shields.io/badge/QQ⑤群-554591908-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=jiirLiyFUecy_gsankzVQ-cl6SrZCnv9&&jump_from=webapi) [![QQ④群780133058](https://img.shields.io/badge/QQ④群-780133058-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=Gj47w9kg7TlV5ceD5Bqew_M_O0PIjh_l&jump_from=webapi) [![QQ③群647954464](https://img.shields.io/badge/QQ③群-647954464-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=K5m27CkhDn3B_Owr-g6rfiTBC5DKEY59&jump_from=webapi) [![QQ②群324606263](https://img.shields.io/badge/QQ②群-324606263-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=IMas2cH-TNsYxUcY8lRbsXqPnA2sGHYQ&jump_from=webapi) [![QQ①群2021514](https://img.shields.io/badge/QQ①群-2021514-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=LGf0OPQqvLGdJIZST3VTcypdVWhdfAOG&jump_from=webapi) |
| 系统部署 | 系统部署 | 免费 | 文档自助。[源码部署](https://hanta.yuque.com/px7kg1/yfac2l/vvoa3u2ztymtp4oh) [Docker部署](https://hanta.yuque.com/px7kg1/yfac2l/mzq23z4iey5ev1a5) |
| 产品使用 | 教学产品各功能使用 | 免费 | 文档自助。[产品文档](https://hanta.yuque.com/px7kg1/yfac2l) |
| 二次开发 | 教学平台源码开发过程、工具使用等; | 免费 | 文档自助。[开发文档](https://hanta.yuque.com/px7kg1/dev) |
| 系统部署 | 在客户指定的网络和硬件环境中完成社区版服务部署;提供**模拟**设备接入到平台中,并能完成正常设备上线、数据上下行 | 199元 | 线上部署支持 |
| 技术支持 | 提供各类部署、功能使用中遇到的问题答疑 | 100元 | 半小时内 线上远程支持 |
| 设备接入协议开发 | 根据提供的设备型号,编写并提供接入平台协议包的源码。 | 3000+元 | 定制化开发 |
| 其他服务 | 企业版源码购买;定制化开发;定制化时长、功能服务等 | 面议 | 面议 |
### **付费**服务支持或商务合作请联系
![qrCode.jpg](./qrCode.png)
## 文档
[产品文档](https://hanta.yuque.com/px7kg1/yfac2l)
[快速开始](https://hanta.yuque.com/px7kg1/yfac2l/raspyc4p1asfuxks)
[开发文档](https://hanta.yuque.com/px7kg1/nn1gdr)
[快速开始](http://doc.jetlinks.cn/basics-guide/quick-start.html)
[开发文档](http://doc.jetlinks.cn/dev-guide/start.html)
[常见问题](http://doc.jetlinks.cn/common-problems/network-components.html)
[![Stargazers over time](https://starchart.cc/jetlinks/jetlinks-community.svg?variant=adaptive)](https://starchart.cc/jetlinks/jetlinks-community)
## 许可版本
| 功能 | 社区版 | 专业版 | 企业版 |
| ---- | ---- | ---- | ----- |
| 开放源代码 | ✅ | ✅ | ✅ |
| 设备管理,设备接入| ✅ | ✅ | ✅ |
| 多消息协议支持| ✅ | ✅ | ✅ |
| 规则引擎-设备告警 | ✅ | ✅ | ✅ |
| 规则引擎-数据转发 | ✅ | ✅ | ✅ |
| 系统监控,数据统计 | ✅ | ✅ | ✅ |
| 邮件消息通知 | ✅ | ✅ | ✅ |
| 微信企业消息 | ✅ | ✅ | ✅ |
| 钉钉消息通知 | ✅ | ✅ | ✅ |
| MQTT(TLS) | ✅ | ✅ | ✅ |
| TCP(TLS) | ✅ | ✅ | ✅ |
| CoAP(DTLS) | ⭕ | ✅ | ✅ |
| Http,WebSocket(TLS) | ⭕ | ✅ | ✅ |
| 数据转发:MQTT,HTTP,Kafka... | ⭕ | ✅ | ✅ |
| Geo地理位置支持 | ⭕ | ✅ | ✅ |
| 规则引擎-可视化设计器 | ⭕ | ✅ | ✅ |
| OpenAPI | ⭕ | ✅ | ✅ |
| 多租户(建设中) | ⭕ | ✅ | ✅ |
| 集群支持 | ⭕ | ✅ | ✅ |
| QQ群技术支持 | ⭕ | ✅ | ✅ |
| 一对一技术支持 | ⭕ | ⭕ | ✅ |
| 微服务架构 | ⭕ | ⭕ | ✅ |
| 统一认证 | ⭕ | ⭕ | ✅ |
| 选配业务模块(建设中) | ⭕ | ⭕ | ✅ |
| 定制开发 | ⭕ | ⭕ | ✅ |
| 商业限制 | 无 | 单个项目 | 无 |
| 定价 | 免费 | 联系我们 | 联系我们 |
⚠️:所有版本均不可发布为与JetLinks同类的产品进行二次销售.

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
dockerImage=registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-community:$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
./mvnw clean package -Dmaven.test.skip=true -Dmaven.build.timestamp="$(date "+%Y-%m-%d %H:%M:%S")"
dockerImage=registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
./mvnw clean install -Dmaven.test.skip=true
if [ $? -ne 0 ];then
echo "构建失败!"
else

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

View File

@ -1,24 +1,50 @@
version: '2'
services:
redis:
image: redis:6
container_name: jetlinks-ce-redis
ports:
- "6379:6379"
volumes:
- "./data/redis:/data"
command: redis-server --appendonly yes
environment:
- TZ=Asia/Shanghai
postgres:
image: timescale/timescaledb:latest-pg16
container_name: jetlinks-postgres-16
ports:
- "5432:5432"
volumes:
- "./data/postgres:/var/lib/postgresql/data"
environment:
POSTGRES_PASSWORD: jetlinks
POSTGRES_DB: jetlinks
POSTGRES_HOST_AUTH_METHOD: trust
TZ: Asia/Shanghai
redis:
image: redis:5.0.4
container_name: jetlinks-ce-redis
ports:
- "6379:6379"
volumes:
- "redis-volume:/data"
command: redis-server --appendonly yes
environment:
- TZ=Asia/Shanghai
elasticsearch:
image: elasticsearch:6.8.11
container_name: jetlinks-ce-elasticsearch
environment:
ES_JAVA_OPTS: -Djava.net.preferIPv4Stack=true -Xms1g -Xmx1g
transport.host: 0.0.0.0
discovery.type: single-node
bootstrap.memory_lock: "true"
discovery.zen.minimum_master_nodes: 1
discovery.zen.ping.unicast.hosts: elasticsearch
ports:
- "9200:9200"
- "9300:9300"
kibana:
image: kibana:6.8.11
container_name: jetlinks-ce-kibana
environment:
ELASTICSEARCH_URL: http://elasticsearch:9200
links:
- elasticsearch:elasticsearch
ports:
- "5601:5601"
depends_on:
- elasticsearch
postgres:
image: postgres:11-alpine
container_name: jetlinks-ce-postgres
ports:
- "5432:5432"
volumes:
- "postgres-volume:/var/lib/postgresql/data"
environment:
POSTGRES_PASSWORD: jetlinks
POSTGRES_DB: jetlinks
TZ: Asia/Shanghai
volumes:
postgres-volume:
redis-volume:

View File

@ -1 +0,0 @@
data

View File

@ -0,0 +1,51 @@
version: '2'
services:
redis:
image: redis:5.0.4
container_name: jetlinks-ce-redis
# ports:
# - "6379:6379"
volumes:
- "redis-volume:/data"
command: redis-server --appendonly yes
environment:
- TZ=Asia/Shanghai
ui:
image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-antd:1.4.0
container_name: jetlinks-ce-ui
ports:
- 9000:80
environment:
- "API_BASE_PATH=http://jetlinks:8848/" #API根路径
volumes:
- "jetlinks-upload-volume:/usr/share/nginx/html/upload"
links:
- jetlinks:jetlinks
jetlinks:
image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:1.4.0
container_name: jetlinks-ce
ports:
- 8848:8848 # API端口
- 1883:1883 # MQTT端口
- 8000:8000 # 预留
- 8001:8001 # 预留
- 8002:8002 # 预留
- 9000:9000 # elasticsearch
- 6379:6379 # redis
volumes:
- "jetlinks-upload-volume:/static/upload" # 持久化上传的文件
- "jetlinks-data-volume:/data"
environment:
# - "JAVA_OPTS=-Xms4g -Xmx18g -XX:+UseG1GC"
- "spring.profiles.active=dev,embedded" #使用dev和embedded环境.
- "hsweb.file.upload.static-location=http://127.0.0.1:8848/upload" #上传的静态文件访问根地址,为ui的地址.
- "logging.level.io.r2dbc=warn"
- "spring.redis.host=redis"
- "logging.level.org.springframework.data=warn"
- "logging.level.org.springframework=warn"
- "logging.level.org.jetlinks=warn"
- "logging.level.org.hswebframework=warn"
- "logging.level.org.springframework.data.r2dbc.connectionfactory=warn"
volumes:
jetlinks-upload-volume:
jetlinks-data-volume:

View File

@ -1,92 +1,99 @@
version: '3'
version: '2'
services:
redis:
image: redis:6
container_name: jetlinks-ce-redis
# ports:
# - "6379:6379" # 仅供jetlinks-ce访问
volumes:
- "./data/redis:/data"
command: redis-server --appendonly yes --requirepass "JetLinks@redis"
environment:
- TZ=Asia/Shanghai
healthcheck:
test: [ "CMD", "redis-cli", "-h", "localhost", "-p", "6379", "-a", "JetLinks@redis", "ping" ]
interval: 10s
timeout: 5s
retries: 3
postgres:
image: timescale/timescaledb:latest-pg16
container_name: jetlinks-ce-postgres
# ports:
# - "5432:5432" # 仅供jetlinks-ce访问
volumes:
- "./data/postgres:/var/lib/postgresql/data"
environment:
POSTGRES_PASSWORD: JetLinks@postgres
POSTGRES_DB: jetlinks
POSTGRES_HOST_AUTH_METHOD: trust
TZ: Asia/Shanghai
healthcheck:
test: [ "CMD", "pg_isready", "-U", "postgres" ]
interval: 10s
timeout: 5s
retries: 5
jetlinks:
image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-community:2.11.0-SNAPSHOT
container_name: jetlinks-ce
ports:
- "8848:8848" # 平台访问端口
- "1883-1890:1883-1890" # 预留
- "8800-8810:8800-8810" # 预留
- "5060-5061:5060-5061" # 预留
volumes:
- "./data/jetlinks:/application/data"
environment:
- "JAVA_OPTS=-Duser.language=zh"
- "TZ=Asia/Shanghai"
- "EXTERNAL_HOST=local-host.cn" # 对外提供访问的域名或者ip地址
- "EXTERNAL_PORT=8848" # 对外提供访问的端口,修改了端口映射同时也需要修改这里.
- "ADMIN_USER_PASSWORD=JetLinks.C0mmVn1ty" # admin用户的初始密码
- "spring.r2dbc.url=r2dbc:postgresql://postgres:5432/jetlinks" #数据库连接地址
- "spring.r2dbc.username=postgres"
- "spring.r2dbc.password=JetLinks@postgres"
- "spring.data.redis.host=redis"
- "spring.data.redis.port=6379"
- "file.manager.storage-base-path=/application/data/files"
- "spring.data.redis.password=JetLinks@redis"
- "logging.level.io.r2dbc=warn"
- "logging.level.org.springframework.data=warn"
- "logging.level.org.springframework=warn"
- "logging.level.org.jetlinks=warn"
- "logging.level.org.hswebframework=warn"
- "logging.level.org.springframework.data.r2dbc.connectionfactory=warn"
- "network.resources[0]=0.0.0.0:8800-8810/tcp"
- "network.resources[1]=0.0.0.0:1883-1890"
- "hsweb.cors.enable=true"
- "hsweb.cors.configs[0].path=/**"
- "hsweb.cors.configs[0].allowed-credentials=true"
- "hsweb.cors.configs[0].allowed-headers=*"
- "hsweb.cors.configs[0].allowed-origins=*"
- "hsweb.cors.configs[0].allowed-methods[0]=GET"
- "hsweb.cors.configs[0].allowed-methods[1]=POST"
- "hsweb.cors.configs[0].allowed-methods[2]=PUT"
- "hsweb.cors.configs[0].allowed-methods[3]=PATCH"
- "hsweb.cors.configs[0].allowed-methods[4]=DELETE"
- "hsweb.cors.configs[0].allowed-methods[5]=OPTIONS"
links:
- redis:redis
- postgres:postgres
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# ui: # 如果通过访问内嵌的前端存在问题,可以使用这个镜像
# image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.11.0
# ports:
# - "9000:9000"
# environment:
# API_BASE_PATH: "http://jetlinks:8848/"
# TZ: Asia/Shanghai
redis:
image: redis:5.0.4
container_name: jetlinks-ce-redis
# ports:
# - "6379:6379"
volumes:
- "redis-volume:/data"
command: redis-server --appendonly yes
environment:
- TZ=Asia/Shanghai
elasticsearch:
image: elasticsearch:6.8.11
container_name: jetlinks-ce-elasticsearch
environment:
ES_JAVA_OPTS: -Djava.net.preferIPv4Stack=true -Xms1g -Xmx1g
transport.host: 0.0.0.0
discovery.type: single-node
bootstrap.memory_lock: "true"
discovery.zen.minimum_master_nodes: 1
discovery.zen.ping.unicast.hosts: elasticsearch
volumes:
- elasticsearch-volume:/usr/share/elasticsearch/data
# ports:
# - "9200:9200"
# - "9300:9300"
kibana:
image: kibana:6.8.11
container_name: jetlinks-ce-kibana
environment:
ELASTICSEARCH_URL: http://elasticsearch:9200
links:
- elasticsearch:elasticsearch
ports:
- "5602:5601"
depends_on:
- elasticsearch
postgres:
image: postgres:11-alpine
container_name: jetlinks-ce-postgres
volumes:
- "postgres-volume:/var/lib/postgresql/data"
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: jetlinks
POSTGRES_DB: jetlinks
TZ: Asia/Shanghai
ui:
image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-antd:1.4.0
container_name: jetlinks-ce-ui
ports:
- 9000:80
environment:
- "API_BASE_PATH=http://jetlinks:8848/" #API根路径
volumes:
- "jetlinks-volume:/usr/share/nginx/html/upload"
links:
- jetlinks:jetlinks
jetlinks:
image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:1.4.0
container_name: jetlinks-ce
ports:
- 8848:8848 # API端口
- 1883-1890:1883-1890 # 预留
- 8000-8010:8000-8010 # 预留
volumes:
- "jetlinks-volume:/application/static/upload" # 持久化上传的文件
environment:
# - "JAVA_OPTS=-Xms4g -Xmx18g -XX:+UseG1GC"
- "TZ=Asia/Shanghai"
- "hsweb.file.upload.static-location=http://127.0.0.1:8848/upload" #上传的静态文件访问根地址,为ui的地址.
- "spring.r2dbc.url=r2dbc:postgresql://postgres:5432/jetlinks" #数据库连接地址
- "spring.r2dbc.username=postgres"
- "spring.r2dbc.password=jetlinks"
- "elasticsearch.client.host=elasticsearch"
- "elasticsearch.client.post=9200"
- "spring.redis.host=redis"
- "spring.redis.port=6379"
- "logging.level.io.r2dbc=warn"
- "logging.level.org.springframework.data=warn"
- "logging.level.org.springframework=warn"
- "logging.level.org.jetlinks=warn"
- "logging.level.org.hswebframework=warn"
- "logging.level.org.springframework.data.r2dbc.connectionfactory=warn"
links:
- redis:redis
- postgres:postgres
- elasticsearch:elasticsearch
depends_on:
- postgres
- redis
- elasticsearch
volumes:
postgres-volume:
redis-volume:
elasticsearch-volume:
jetlinks-volume:

6
docker/ui/Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM nginx
ADD nginx.conf /etc/nginx/conf.d/default.conf
CMD ["sh","docker-entrypoint.sh"]
#ADD oauth2 /usr/share/nginx/html/oauth2

View File

@ -0,0 +1,6 @@
FROM nginx
ADD nginx.conf /etc/nginx/conf.d/default.conf
ADD docker-entrypoint.sh /docker-entrypoint.sh
CMD ["sh","/docker-entrypoint.sh"]
#ADD oauth2 /usr/share/nginx/html/oauth2

View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
API_BASE_PATH=$API_BASE_PATH;
if [ -z "$API_BASE_PATH" ]; then
API_BASE_PATH="http://jetlinks:8844/";
fi
apiUrl="proxy_pass $API_BASE_PATH;"
sed -i '18c '"$apiUrl"'' /etc/nginx/conf.d/default.conf
nginx -g "daemon off;"

View File

@ -0,0 +1,27 @@
server {
listen 80;
# gzip config
gzip on;
gzip_min_length 1k;
gzip_comp_level 9;
gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml;
gzip_vary on;
gzip_disable "MSIE [1-6]\.";
root /usr/share/nginx/html;
include /etc/nginx/mime.types;
location / {
try_files $uri $uri/ /index.html;
}
location ^~/api/ {
proxy_pass http://jetlinks:8844/;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host:$server_port;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
client_max_body_size 50m;
}
}

486
flow.svg Normal file
View File

@ -0,0 +1,486 @@
<svg id="SvgjsSvg1492" width="792" height="653.5" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs">
<defs id="SvgjsDefs1493">
<marker id="SvgjsMarker1502" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1503" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1510" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1511" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1518" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1519" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1526" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1527" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1536" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1537" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1546" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1547" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1554" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1555" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1568" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1569" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1576" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1577" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1606" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1607" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1614" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1615" d="M0,2 L14,6 L0,11 L0,2" fill="#000000" stroke="#000000" stroke-width="1">
</path>
</marker>
<marker id="SvgjsMarker1700" markerWidth="16" markerHeight="12" refX="16" refY="6" viewBox="0 0 16 12" orient="auto" markerUnits="userSpaceOnUse">
<path id="SvgjsPath1701" d="M0,2 L14,6 L0,11 L0,2" fill="#323232" stroke="#323232" stroke-width="1">
</path>
</marker>
</defs>
<g id="SvgjsG1494" transform="translate(240,33.75)">
<path id="SvgjsPath1495" d="M 15 0L 148 0C 168 0 168 45 148 45L 15 45C -5 45 -5 0 15 0Z" stroke="#335f94" stroke-width="3" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1496">
<text id="SvgjsText1497" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="103" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="12.55" transform="rotate(0)">
<tspan id="SvgjsTspan1498" dy="16" x="81.5">
<tspan id="SvgjsTspan1499" style="text-decoration:;">
设备是否有IP能力
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1500">
<path id="SvgjsPath1501" d="M240 56.25L160 56.25L160 106.25" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1502)">
</path>
<rect id="SvgjsRect1504" width="13" height="16" x="168.5" y="48.25" fill="#ffffff">
</rect>
<text id="SvgjsText1505" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="13" fill="#0000ff" font-weight="700" align="top" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="46.3" transform="rotate(0)">
<tspan id="SvgjsTspan1506" dy="16" x="175">
<tspan id="SvgjsTspan1507" style="text-decoration:;">
</tspan>
</tspan>
</text>
</g>
<g id="SvgjsG1508">
<path id="SvgjsPath1509" d="M403 56.25L457.875 56.25L457.875 56.25L512.75 56.25" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1510)">
</path>
<rect id="SvgjsRect1512" width="13" height="16" x="451.375" y="48.25" fill="#ffffff">
</rect>
<text id="SvgjsText1513" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="13" fill="#ff0000" font-weight="700" align="top" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="46.3" transform="rotate(0)">
<tspan id="SvgjsTspan1514" dy="16" x="457.875">
<tspan id="SvgjsTspan1515" style="text-decoration:;">
</tspan>
</tspan>
</text>
</g>
<g id="SvgjsG1516">
<path id="SvgjsPath1517" d="M148 306.25L209 306.25L209 306.25L270 306.25" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1518)">
</path>
<rect id="SvgjsRect1520" width="13" height="16" x="202.5" y="298.25" fill="#ffffff">
</rect>
<text id="SvgjsText1521" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="13" fill="#ff0000" font-weight="700" align="top" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="296.3" transform="rotate(0)">
<tspan id="SvgjsTspan1522" dy="16" x="209">
<tspan id="SvgjsTspan1523" style="text-decoration:;">
</tspan>
</tspan>
</text>
</g>
<g id="SvgjsG1524">
<path id="SvgjsPath1525" d="M331 321.25L331 343L331 343L331 364.75" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1526)">
</path>
</g>
<g id="SvgjsG1528" transform="translate(270,364.75)">
<path id="SvgjsPath1529" d="M 0 4Q 0 0 4 0L 118 0Q 122 0 122 4L 122 26Q 122 30 118 30L 4 30Q 0 30 0 26Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#ffffff">
</path>
<g id="SvgjsG1530">
<text id="SvgjsText1531" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="65" fill="#323232" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="5.05" transform="rotate(0)">
<tspan id="SvgjsTspan1532" dy="16" x="61">
<tspan id="SvgjsTspan1533" style="text-decoration:;">
上传到平台
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1534">
<path id="SvgjsPath1535" d="M331 394.75L331 418L331 418L331 441.25" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1536)">
</path>
</g>
<g id="SvgjsG1538" transform="translate(533,285.75)">
<path id="SvgjsPath1539" d="M 0 20.5L 54.5 0L 109 20.5L 54.5 41Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#ffffff">
</path>
<g id="SvgjsG1540">
<text id="SvgjsText1541" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="52" fill="#323232" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="10.755" transform="rotate(0)">
<tspan id="SvgjsTspan1542" dy="16" x="54.5">
<tspan id="SvgjsTspan1543" style="text-decoration:;">
接入方式
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1544">
<path id="SvgjsPath1545" d="M533 306.25L462.5 306.25L462.5 306.25L392 306.25" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1546)">
</path>
<rect id="SvgjsRect1548" width="91" height="16" x="417" y="298.25" fill="#ffffff">
</rect>
<text id="SvgjsText1549" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="91" fill="#323232" font-weight="700" align="top" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="296.3" transform="rotate(0)">
<tspan id="SvgjsTspan1550" dy="16" x="462.5">
<tspan id="SvgjsTspan1551" style="text-decoration:;">
第三方应用推送
</tspan>
</tspan>
</text>
</g>
<g id="SvgjsG1552">
<path id="SvgjsPath1553" d="M587.5 326.75L587.5 384.125L587.5 384.125L587.5 441.5" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1554)">
</path>
<rect id="SvgjsRect1556" width="104" height="16" x="535.5" y="376.125" fill="#ffffff">
</rect>
<text id="SvgjsText1557" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="104" fill="#323232" font-weight="700" align="top" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="374.175" transform="rotate(0)">
<tspan id="SvgjsTspan1558" dy="16" x="587.5">
<tspan id="SvgjsTspan1559" style="text-decoration:;">
第三方云平台接入
</tspan>
</tspan>
</text>
</g>
<g id="SvgjsG1560" transform="translate(512.75,25)">
<path id="SvgjsPath1561" d="M 0 31.25L 70.5 0L 141 31.25L 70.5 62.5Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#ffffff">
</path>
<g id="SvgjsG1562">
<text id="SvgjsText1563" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="91" fill="#323232" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="21.6125" transform="rotate(0)">
<tspan id="SvgjsTspan1564" dy="16" x="70.5">
<tspan id="SvgjsTspan1565" style="text-decoration:;">
是否已接入网关
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1566">
<path id="SvgjsPath1567" d="M583.25 87.5L583.25 146.25L220 146.25" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1568)">
</path>
<rect id="SvgjsRect1570" width="13" height="16" x="424.5" y="138.25" fill="#ffffff">
</rect>
<text id="SvgjsText1571" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="13" fill="#3333ff" font-weight="700" align="top" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="136.3" transform="rotate(0)">
<tspan id="SvgjsTspan1572" dy="16" x="431">
<tspan id="SvgjsTspan1573" style="text-decoration:;">
</tspan>
</tspan>
</text>
</g>
<g id="SvgjsG1574">
<path id="SvgjsPath1575" d="M653.75 56.25L708 56.25L708 148.75" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1576)">
</path>
<rect id="SvgjsRect1578" width="13" height="16" x="701.5" y="67.375" fill="#ffffff">
</rect>
<text id="SvgjsText1579" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="13" fill="#ff0000" font-weight="700" align="top" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="65.425" transform="rotate(0)">
<tspan id="SvgjsTspan1580" dy="16" x="708">
<tspan id="SvgjsTspan1581" style="text-decoration:;">
</tspan>
</tspan>
</text>
</g>
<g id="SvgjsG1582" transform="translate(649,148.75)">
<path id="SvgjsPath1583" d="M 0 4Q 0 0 4 0L 114 0Q 118 0 118 4L 118 39Q 118 43 114 43L 4 43Q 0 43 0 39Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#ffffff">
</path>
<g id="SvgjsG1584">
<text id="SvgjsText1585" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="52" fill="#323232" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="11.55" transform="rotate(0)">
<tspan id="SvgjsTspan1586" dy="16" x="59">
<tspan id="SvgjsTspan1587" style="text-decoration:;">
联系我们
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1588" transform="translate(25,266.25)">
<path id="SvgjsPath1589" d="M 0 40L 61.5 0L 123 40L 61.5 80Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#ffffff">
</path>
<g id="SvgjsG1590">
<text id="SvgjsText1591" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="91" fill="#323232" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="14.45" transform="rotate(0)">
<tspan id="SvgjsTspan1592" dy="16" x="61.5">
<tspan id="SvgjsTspan1593" style="text-decoration:;">
是否使用
</tspan>
</tspan>
<tspan id="SvgjsTspan1594" dy="16" x="61.5">
<tspan id="SvgjsTspan1595" style="text-decoration:;">
JetLinks官方协
</tspan>
</tspan>
<tspan id="SvgjsTspan1596" dy="16" x="61.5">
<tspan id="SvgjsTspan1597" style="text-decoration:;">
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1598" transform="translate(100,106.25)">
<path id="SvgjsPath1599" d="M 0 40L 60 0L 120 40L 60 80Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#ffffff">
</path>
<g id="SvgjsG1600">
<text id="SvgjsText1601" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="78" fill="#323232" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="30.45" transform="rotate(0)">
<tspan id="SvgjsTspan1602" dy="16" x="60">
<tspan id="SvgjsTspan1603" style="text-decoration:;">
是否直连平台
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1604">
<path id="SvgjsPath1605" d="M100 146.25L86.5 146.25L86.5 266.25" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1606)">
</path>
<rect id="SvgjsRect1608" width="13" height="16" x="80" y="191.5" fill="#ffffff">
</rect>
<text id="SvgjsText1609" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="13" fill="#3333ff" font-weight="700" align="top" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="189.55" transform="rotate(0)">
<tspan id="SvgjsTspan1610" dy="16" x="86.5">
<tspan id="SvgjsTspan1611" style="text-decoration:;">
</tspan>
</tspan>
</text>
</g>
<g id="SvgjsG1612">
<path id="SvgjsPath1613" d="M160 186.25L160 236L587.5 236L587.5 285.75" stroke="#000000" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1614)">
</path>
<rect id="SvgjsRect1616" width="13" height="16" x="367.25" y="228" fill="#ffffff">
</rect>
<text id="SvgjsText1617" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="13" fill="#ff0000" font-weight="700" align="top" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="226.05" transform="rotate(0)">
<tspan id="SvgjsTspan1618" dy="16" x="373.75">
<tspan id="SvgjsTspan1619" style="text-decoration:;">
</tspan>
</tspan>
</text>
</g>
<g id="SvgjsG1620" transform="translate(489,441.5)">
<path id="SvgjsPath1621" d="M 0 0L 197 0L 197 187L 0 187Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#ffffff">
</path>
<g id="SvgjsG1622">
<text id="SvgjsText1623" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="0" fill="#323232" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="83.55" transform="rotate(0)">
</text>
</g>
</g>
<g id="SvgjsG1624" transform="translate(512.75,449.875)">
<path id="SvgjsPath1625" d="M 0 4Q 0 0 4 0L 149 0Q 153 0 153 4L 153 20.25Q 153 24.25 149 24.25L 4 24.25Q 0 24.25 0 20.25Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1626">
<text id="SvgjsText1627" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="46" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="2.175" transform="rotate(0)">
<tspan id="SvgjsTspan1628" dy="16" x="76.5">
<tspan id="SvgjsTspan1629" style="text-decoration:;">
NB-IoT
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1630" transform="translate(512.75,519.875)">
<path id="SvgjsPath1631" d="M 0 4Q 0 0 4 0L 149 0Q 153 0 153 4L 153 22.875Q 153 26.875 149 26.875L 4 26.875Q 0 26.875 0 22.875Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1632">
<text id="SvgjsText1633" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="65" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="3.4875" transform="rotate(0)">
<tspan id="SvgjsTspan1634" dy="16" x="76.5">
<tspan id="SvgjsTspan1635" style="text-decoration:;">
阿里云平台
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1636" transform="translate(512.75,556.125)">
<path id="SvgjsPath1637" d="M 0 4Q 0 0 4 0L 149 0Q 153 0 153 4L 153 24Q 153 28 149 28L 4 28Q 0 28 0 24Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1638">
<text id="SvgjsText1639" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="78" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="4.05" transform="rotate(0)">
<tspan id="SvgjsTspan1640" dy="16" x="76.5">
<tspan id="SvgjsTspan1641" style="text-decoration:;">
海康视频平台
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1642" transform="translate(512.75,592.5)">
<path id="SvgjsPath1643" d="M 0 4Q 0 0 4 0L 149 0Q 153 0 153 4L 153 20.25Q 153 24.25 149 24.25L 4 24.25Q 0 24.25 0 20.25Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1644">
<text id="SvgjsText1645" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="39" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="2.175" transform="rotate(0)">
<tspan id="SvgjsTspan1646" dy="16" x="76.5">
<tspan id="SvgjsTspan1647" style="text-decoration:;">
自定义
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1648" transform="translate(235,441.25)">
<path id="SvgjsPath1649" d="M 0 0L 192 0L 192 187.5L 0 187.5Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#ffffff">
</path>
<g id="SvgjsG1650">
<text id="SvgjsText1651" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="0" fill="#323232" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="83.8" transform="rotate(0)">
</text>
</g>
</g>
<g id="SvgjsG1652" transform="translate(247.5,469.75)">
<path id="SvgjsPath1653" d="M 0 4Q 0 0 4 0L 154.5 0Q 158.5 0 158.5 4L 158.5 26Q 158.5 30 154.5 30L 4 30Q 0 30 0 26Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1654">
<text id="SvgjsText1655" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="121" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="5.05" transform="rotate(0)">
<tspan id="SvgjsTspan1656" dy="16" x="79.5">
<tspan id="SvgjsTspan1657" style="text-decoration:;">
MQTT Server,Client
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1658" transform="translate(254.25,434.75)">
<path id="SvgjsPath1659" d="M 0 0L 160 0L 160 40L 0 40Z" stroke="none" fill="none">
</path>
<g id="SvgjsG1660">
<text id="SvgjsText1661" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="52" fill="#323232" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="10.05" transform="rotate(0)">
<tspan id="SvgjsTspan1662" dy="16" x="80">
<tspan id="SvgjsTspan1663" style="text-decoration:;">
接入平台
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1664" transform="translate(247.5,507)">
<path id="SvgjsPath1665" d="M 0 4Q 0 0 4 0L 154.5 0Q 158.5 0 158.5 4L 158.5 25.75Q 158.5 29.75 154.5 29.75L 4 29.75Q 0 29.75 0 25.75Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1666">
<text id="SvgjsText1667" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="109" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="4.925" transform="rotate(0)">
<tspan id="SvgjsTspan1668" dy="16" x="79.5">
<tspan id="SvgjsTspan1669" style="text-decoration:;">
TCP Server,Client
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1670" transform="translate(247.5,546.75)">
<path id="SvgjsPath1671" d="M 0 4Q 0 0 4 0L 69.5 0Q 73.5 0 73.5 4L 73.5 34Q 73.5 38 69.5 38L 4 38Q 0 38 0 34Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1672">
<text id="SvgjsText1673" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="40" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="1.05" transform="rotate(0)">
<tspan id="SvgjsTspan1674" dy="16" x="37">
<tspan id="SvgjsTspan1675" style="text-decoration:;">
HTTP
</tspan>
</tspan>
<tspan id="SvgjsTspan1676" dy="16" x="37">
<tspan id="SvgjsTspan1677" style="text-decoration:;">
Server
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1678" transform="translate(335.5,546.75)">
<path id="SvgjsPath1679" d="M 0 4Q 0 0 4 0L 66.5 0Q 70.5 0 70.5 4L 70.5 34Q 70.5 38 66.5 38L 4 38Q 0 38 0 34Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1680">
<text id="SvgjsText1681" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="40" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="1.05" transform="rotate(0)">
<tspan id="SvgjsTspan1682" dy="16" x="35.5">
<tspan id="SvgjsTspan1683" style="text-decoration:;">
CoAP
</tspan>
</tspan>
<tspan id="SvgjsTspan1684" dy="16" x="35.5">
<tspan id="SvgjsTspan1685" style="text-decoration:;">
Server
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1686" transform="translate(247.5,592.5)">
<path id="SvgjsPath1687" d="M 0 4Q 0 0 4 0L 154.5 0Q 158.5 0 158.5 4L 158.5 23Q 158.5 27 154.5 27L 4 27Q 0 27 0 23Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1688">
<text id="SvgjsText1689" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="27" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="3.55" transform="rotate(0)">
<tspan id="SvgjsTspan1690" dy="16" x="79.5">
<tspan id="SvgjsTspan1691" style="text-decoration:;">
UDP
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1692" transform="translate(270,291.25)">
<path id="SvgjsPath1693" d="M 0 4Q 0 0 4 0L 118 0Q 122 0 122 4L 122 26Q 122 30 118 30L 4 30Q 0 30 0 26Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#ffffff">
</path>
<g id="SvgjsG1694">
<text id="SvgjsText1695" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="91" fill="#323232" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="5.05" transform="rotate(0)">
<tspan id="SvgjsTspan1696" dy="16" x="61">
<tspan id="SvgjsTspan1697" style="text-decoration:;">
开发报文解析包
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1698">
<path id="SvgjsPath1699" d="M86.5 346.25L86.5 535L235 535" stroke="#323232" stroke-width="1" fill="none" marker-end="url(#SvgjsMarker1700)">
</path>
<rect id="SvgjsRect1702" width="13" height="16" x="80" y="506.875" fill="#ffffff">
</rect>
<text id="SvgjsText1703" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="13" fill="#3333ff" font-weight="700" align="top" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="504.925" transform="rotate(0)">
<tspan id="SvgjsTspan1704" dy="16" x="86.5">
<tspan id="SvgjsTspan1705" style="text-decoration:;">
</tspan>
</tspan>
</text>
</g>
<g id="SvgjsG1706" transform="translate(512.75,482.875)">
<path id="SvgjsPath1707" d="M 0 4Q 0 0 4 0L 70 0Q 74 0 74 4L 74 22.5Q 74 26.5 70 26.5L 4 26.5Q 0 26.5 0 22.5Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1708">
<text id="SvgjsText1709" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="26" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="3.3" transform="rotate(0)">
<tspan id="SvgjsTspan1710" dy="16" x="37">
<tspan id="SvgjsTspan1711" style="text-decoration:;">
移动
</tspan>
</tspan>
</text>
</g>
</g>
<g id="SvgjsG1712" transform="translate(591.75,482.875)">
<path id="SvgjsPath1713" d="M 0 4Q 0 0 4 0L 70 0Q 74 0 74 4L 74 22.5Q 74 26.5 70 26.5L 4 26.5Q 0 26.5 0 22.5Z" stroke="#335f94" stroke-width="2" fill-opacity="1" fill="#3581bb">
</path>
<g id="SvgjsG1714">
<text id="SvgjsText1715" font-family="微软雅黑" text-anchor="middle" font-size="13px" width="26" fill="#ffffff" font-weight="700" align="middle" anchor="middle" family="微软雅黑" size="13px" weight="700" font-style="" y="3.3" transform="rotate(0)">
<tspan id="SvgjsTspan1716" dy="16" x="37">
<tspan id="SvgjsTspan1717" style="text-decoration:;">
电信
</tspan>
</tspan>
</text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

View File

@ -5,8 +5,7 @@
<parent>
<artifactId>jetlinks-components</artifactId>
<groupId>org.jetlinks.community</groupId>
<version>2.11.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
<version>1.4.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -19,11 +18,6 @@
<version>${jetlinks.version}</version>
</dependency>
<dependency>
<groupId>org.jetlinks</groupId>
<artifactId>jetlinks-supports</artifactId>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-authorization-api</artifactId>
@ -40,69 +34,5 @@
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</dependency>
<dependency>
<groupId>org.jetlinks</groupId>
<artifactId>reactor-ql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
</dependency>
<dependency>
<groupId>de.ruedigermoeller</groupId>
<artifactId>fst</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2-mvstore</artifactId>
</dependency>
<dependency>
<groupId>org.jetlinks.sdk</groupId>
<artifactId>jetlinks-sdk-api</artifactId>
<version>${jetlinks.sdk.version}</version>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-system-dictionary</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
</exclusion>
<exclusion>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.glassfish.expressly</groupId>
<artifactId>expressly</artifactId>
</dependency>
</dependencies>
</project>
</project>

View File

@ -1,29 +1,8 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import org.jetlinks.core.config.ConfigKey;
import org.jetlinks.core.metadata.MergeOption;
import java.util.Map;
/**
* 数据验证配置常量类
*
* @author zhouhao
* @see ConfigKey
*/
public interface ConfigMetadataConstants {
@ -36,10 +15,5 @@ public interface ConfigMetadataConstants {
ConfigKey<Boolean> allowInput = ConfigKey.of("allowInput", "允许输入", Boolean.TYPE);
ConfigKey<Boolean> required = ConfigKey.of("required", "是否必填", Boolean.TYPE);
ConfigKey<String> format = ConfigKey.of("format", "格式", String.class);
ConfigKey<String> defaultValue = ConfigKey.of("defaultValue", "默认值", String.class);
ConfigKey<Boolean> indexEnabled = ConfigKey.of("indexEnabled", "开启索引", Boolean.TYPE);
}

View File

@ -1,31 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class DynamicOperationType implements OperationType {
private String id;
private String name;
}

View File

@ -1,225 +1,66 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import com.alibaba.fastjson.annotation.JSONType;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.*;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import reactor.core.publisher.Flux;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.function.BiFunction;
import java.util.function.Function;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@JsonDeserialize(using = Interval.IntervalJSONDeserializer.class)
@JSONType(deserializer = Interval.IntervalJSONDeserializer.class)
public class Interval {
public static final String year = "y";
public static final String quarter = "q";
public static final String month = "M";
public static final String weeks = "w";
public static final String days = "d";
public static final String hours = "h";
public static final String minutes = "m";
public static final String seconds = "s";
public static final String millis = "S";
public static String year = "y";
public static String quarter = "q";
public static String month = "M";
public static String weeks = "w";
public static String days = "d";
public static String hours = "h";
public static String minutes = "m";
public static String seconds = "s";
private BigDecimal number;
private String expression;
public Interval(String expr) {
char[] chars = expr.toCharArray();
int numIndex = 0;
for (char c : expr.toCharArray()) {
if (c == '-' || c == '.' || (c >= '0' && c <= '9')) {
numIndex++;
} else {
BigDecimal val = new BigDecimal(chars, 0, numIndex);
this.expression = expr.substring(numIndex);
this.number = val;
}
}
if (this.expression == null) {
throw new IllegalArgumentException("can not parse interval expression:" + expr);
}
}
@Override
public String toString() {
return (number) + expression;
}
@Generated
public static Interval ofSeconds(int seconds) {
return of(seconds, Interval.seconds);
}
@Generated
public static Interval ofDays(int days) {
return of(days, Interval.days);
}
@Generated
public static Interval ofWeeks(int weeks) {
return of(weeks, Interval.weeks);
}
@Generated
public static Interval ofHours(int hours) {
return of(hours, Interval.hours);
}
@Generated
public static Interval ofMonth(int month) {
return of(month, Interval.month);
}
@Generated
public static Interval ofMinutes(int month) {
return of(month, Interval.minutes);
}
@Generated
public static Interval of(int month, String expression) {
return new Interval(new BigDecimal(month), expression);
}
public static Interval of(String expr) {
return new Interval(expr);
}
public String getDefaultFormat() {
switch (getExpression()) {
case year:
return "yyyy";
case quarter:
case month:
return "yyyy-MM";
case days:
return "yyyy-MM-dd";
case hours:
return "MM-dd HH";
case minutes:
return "MM-dd HH:mm";
case seconds:
return "HH:mm:ss";
default:
return "yyyy-MM-dd HH:mm:ss";
}
}
public IntervalUnit getUnit() {
switch (expression) {
case year:
return IntervalUnit.YEARS;
case quarter:
return IntervalUnit.QUARTER;
case month:
return IntervalUnit.MONTHS;
case weeks:
return IntervalUnit.WEEKS;
case days:
return IntervalUnit.DAYS;
case hours:
return IntervalUnit.HOURS;
case minutes:
return IntervalUnit.MINUTES;
case seconds:
return IntervalUnit.SECONDS;
case millis:
return IntervalUnit.MILLIS;
}
throw new UnsupportedOperationException("unsupported interval express:" + expression);
}
public static class IntervalJSONDeserializer extends JsonDeserializer<Interval> {
@Override
@SneakyThrows
public Interval deserialize(JsonParser jp, DeserializationContext ctxt) {
JsonNode node = jp.getCodec().readTree(jp);
String currentName = jp.currentName();
Object currentValue = jp.getCurrentValue();
if (currentName == null || currentValue == null) {
return null;
char[] number = new char[32];
int numIndex = 0;
for (char c : expr.toCharArray()) {
if (c == '-' || c == '.' || (c >= '0' && c <= '9')) {
number[numIndex++] = c;
continue;
}
return of(node.textValue());
BigDecimal val = new BigDecimal(number, 0, numIndex);
return new Interval(val, expr.substring(numIndex));
}
}
public static class IntervalJSONSerializer extends JsonSerializer<Interval> {
@Override
public void serialize(Interval value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.toString());
}
}
public long toMillis() {
return getUnit().toMillis(number.intValue());
}
/**
* 对指定的时间戳按周期取整
*
* @param timestamp 时间戳
* @return 取整后的值
*/
public long round(long timestamp) {
return getUnit().truncatedTo(timestamp, number.intValue());
}
/**
* 按当前周期对指定的时间范围进行迭代,每次迭代一个周期的时间戳
*
* @param from 时间从
* @param to 时间止
* @return 迭代器
*/
public Iterable<Long> iterate(long from, long to) {
return getUnit().iterate(from, to, number.intValue());
}
public <T> Flux<T> generate(long from, long to, Function<Long, T> converter) {
return Flux
.fromIterable(iterate(from, to))
.map(converter);
}
public <T> Flux<T> generateWithFormat(long from,
long to,
String pattern,
BiFunction<Long, String, T> converter) {
DateTimeFormatter formatter = DateTimeFormat.forPattern(pattern);
return generate(from, to, t -> converter.apply(t, new DateTime(t).toString(formatter)));
throw new IllegalArgumentException("can not parse interval expression:" + expr);
}
}

View File

@ -1,174 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
/**
* 时间间隔单位可用于计算时间范围的间隔周期
*
* @author zhouhao
* @since 1.12
*/
@AllArgsConstructor
public enum IntervalUnit {
MILLIS(1, ChronoUnit.MILLIS),
SECONDS(1, ChronoUnit.SECONDS),
MINUTES(1, ChronoUnit.MINUTES),
HOURS(1, ChronoUnit.HOURS),
DAYS(1, ChronoUnit.DAYS),
WEEKS(1, ChronoUnit.WEEKS) {
@Override
protected LocalDateTime doTruncateTo(LocalDateTime time) {
return time.truncatedTo(ChronoUnit.DAYS)
.minusDays(time.getDayOfWeek().getValue() - 1);
}
},
MONTHS(1, ChronoUnit.MONTHS) {
@Override
protected LocalDateTime doTruncateTo(LocalDateTime time) {
return time.truncatedTo(ChronoUnit.DAYS)
.withDayOfMonth(1);
}
},
//季度
QUARTER(3, ChronoUnit.MONTHS) {
@Override
protected LocalDateTime doTruncateTo(LocalDateTime time) {
return time
.withMonth(time.getMonth().firstMonthOfQuarter().getValue())
.truncatedTo(ChronoUnit.DAYS)
.withDayOfMonth(1);
}
},
YEARS(1, ChronoUnit.YEARS) {
@Override
protected LocalDateTime doTruncateTo(LocalDateTime time) {
return time.truncatedTo(ChronoUnit.DAYS)
.withDayOfYear(1);
}
},
//不分区,永远返回0
FOREVER(1, ChronoUnit.FOREVER) {
@Override
public long truncatedTo(long timestamp) {
return 0;
}
@Override
public Iterable<Long> iterate(long from, long to, int duration) {
return () -> new Iterator<Long>() {
private boolean nexted;
@Override
public boolean hasNext() {
return !nexted;
}
@Override
public Long next() {
nexted = true;
return 0L;
}
};
}
};
@Getter
private final int durationOfUnit;
@Getter
private final ChronoUnit unit;
protected LocalDateTime doTruncateTo(LocalDateTime time) {
return time.truncatedTo(unit);
}
protected LocalDateTime next(LocalDateTime time, int duration) {
return time.plus((long) durationOfUnit * duration, unit);
}
protected long next(long timestamp) {
return next(timestamp, 1);
}
protected long next(long timestamp, int duration) {
return this
.next(LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC), duration)
.toInstant(ZoneOffset.UTC)
.toEpochMilli();
}
public long truncatedTo(long timestamp) {
return this
.doTruncateTo(LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC))
.toInstant(ZoneOffset.UTC)
.toEpochMilli();
}
public final long truncatedTo(long timestamp, int duration) {
long ts = truncatedTo(timestamp);
//指定了多个周期
if (Math.abs(duration) > 1) {
ts = next(ts, duration);
}
return ts;
}
public long toMillis(int duration) {
return duration * durationOfUnit * unit.getDuration().toMillis();
}
public final Iterable<Long> iterate(long from, long to) {
return iterate(from, to, 1);
}
/**
* 迭代时间区间的每一个周期时间
*
* @param from 时间从
* @param to 时间止
* @param duration 间隔数量,比如 2天为一个间隔
* @return 每个间隔的时间戳迭代器
*/
public Iterable<Long> iterate(long from, long to, int duration) {
return () -> new Iterator<Long>() {
long _from = truncatedTo(Math.min(from, to));
final long _to = truncatedTo(Math.max(from, to));
@Override
public boolean hasNext() {
return _from <= _to;
}
@Override
public Long next() {
long that = truncatedTo(_from, duration);
_from = IntervalUnit.this.next(_from, duration);
return that;
}
};
}
}

View File

@ -1,27 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import org.hswebframework.web.exception.I18nSupportException;
public class JvmErrorException extends I18nSupportException {
public JvmErrorException(Throwable cause) {
super("error.jvm_error",cause);
}
}

View File

@ -1,68 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetlinks.core.utils.SerializeUtils;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* 描述,用于对某些操作的通用描述.
*
* @author zhouhao
* @since 2.0
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class Operation implements Externalizable {
/**
* 操作来源
*/
private OperationSource source;
/**
* 操作类型比如: transparent-codec等
*/
private OperationType type;
@Override
public String toString() {
return type.getId() + "(" + type.getName() + "):[" + source.getId() + "]";
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
source.writeExternal(out);
SerializeUtils.writeObject(type.getId(), out);
SerializeUtils.writeObject(type.getName(), out);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
source = new OperationSource();
source.readExternal(in);
type = new DynamicOperationType((String) SerializeUtils.readObject(in), (String) SerializeUtils.readObject(in));
}
}

View File

@ -1,80 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetlinks.core.utils.SerializeUtils;
import reactor.util.context.Context;
import reactor.util.context.ContextView;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Optional;
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
@Getter
@Setter
public class OperationSource implements Externalizable {
private static final long serialVersionUID = 1L;
/**
* ID,type对应操作的唯一标识
*/
private String id;
/**
* 操作源名称
*/
private String name;
/**
* 操作目标,通常为ID对应的详情数据
*/
private Object data;
public static OperationSource of(String id, Object data) {
return of(id, id, data);
}
public static Context ofContext(String id, String name, Object data) {
return Context.of(OperationSource.class, of(id, name, data));
}
public static Optional<OperationSource> fromContext(ContextView ctx) {
return ctx.getOrEmpty(OperationSource.class);
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(id);
SerializeUtils.writeObject(name, out);
SerializeUtils.writeObject(data, out);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
id = in.readUTF();
name = (String) SerializeUtils.readObject(in);
data = SerializeUtils.readObject(in);
}
}

View File

@ -1,26 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
public interface OperationType {
String getId();
String getName();
static OperationType of(String id,String name){
return new DynamicOperationType(id,name);
}
}

View File

@ -1,180 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import lombok.Generated;
import org.hswebframework.web.id.IDGenerator;
import org.jetlinks.core.config.ConfigKey;
import org.jetlinks.core.message.HeaderKey;
import org.springframework.core.ResolvableType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
/**
* @author wangzheng
* @since 1.0
*/
@Generated
public interface PropertyConstants {
//机构ID
Key<String> orgId = Key.of("orgId");
//设备名称
Key<String> deviceName = Key.of("deviceName");
//产品名称
Key<String> productName = Key.of("productName");
//产品ID
Key<String> productId = Key.of("productId");
/**
* 关系信息.值格式:
* <pre>{@code
* [{"type":"user","id":"userId","rel":"manager"}]
* }</pre>
*/
Key<List<Map<String, Object>>> relations = Key.of("relations");
//分组ID
Key<List<String>> groupId = Key.of("groupId");
//是否记录task记录
Key<Boolean> useTask = Key.of("useTask", false);
//taskId
Key<String> taskId = Key.of("taskId");
//最大重试次数
Key<Long> maxRetryTimes = Key.of("maxRetryTimes", () -> Long.getLong("device.message.task.retryTimes", 1), Long.class);
//当前重试次数
Key<Long> retryTimes = Key.of("retryTimes", () -> 0L, Long.class);
//服务ID
Key<String> serverId = Key.of("serverId");
//全局唯一ID
Key<String> uid = Key.of("_uid", IDGenerator.RANDOM::generate);
//设备接入网关ID
Key<String> accessId = Key.of("accessId");
/**
* 设备接入方式
*
* @see org.jetlinks.community.gateway.supports.DeviceGatewayProvider#getId
*/
Key<String> accessProvider = Key.of("accessProvider");
//设备创建者
Key<String> creatorId = Key.of("creatorId");
@SuppressWarnings("all")
static <T> Optional<T> getFromMap(ConfigKey<T> key, Map<String, Object> map) {
return Optional.ofNullable((T) map.get(key.getKey()));
}
@SuppressWarnings("all")
static <T> T getFromMapOrElse(ConfigKey<T> key, Map<String, Object> map, Supplier<T> defaultIfEmpty) {
Object value = map.get(key.getKey());
if (value == null) {
return defaultIfEmpty.get();
}
return (T) value;
}
@Generated
interface Key<V> extends ConfigKey<V>, HeaderKey<V> {
@Override
default Type getValueType() {
return ConfigKey.super.getValueType();
}
@Override
default Class<V> getType() {
return ConfigKey.super.getType();
}
static <T> Key<T> of(String key) {
return new Key<T>() {
@Override
public String getKey() {
return key;
}
@Override
public T getDefaultValue() {
return null;
}
};
}
static <T> Key<T> of(String key, T defaultValue) {
return new Key<T>() {
@Override
public String getKey() {
return key;
}
@Override
public T getDefaultValue() {
return defaultValue;
}
};
}
static <T> Key<T> of(String key, Supplier<T> defaultValue) {
return new Key<T>() {
@Override
public String getKey() {
return key;
}
@Override
public T getDefaultValue() {
return defaultValue == null ? null : defaultValue.get();
}
};
}
static <T> Key<T> of(String key, Supplier<T> defaultValue, Type type) {
return new Key<T>() {
@Override
public Type getValueType() {
return type;
}
@Override
public String getKey() {
return key;
}
@Override
public T getDefaultValue() {
return defaultValue == null ? null : defaultValue.get();
}
};
}
}
}

View File

@ -1,153 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import org.jetlinks.community.utils.ConverterUtils;
import org.jetlinks.core.config.ConfigKey;
import org.jetlinks.core.message.DeviceMessage;
import org.jetlinks.core.message.HeaderKey;
import org.jetlinks.core.metadata.PropertyMetadata;
import org.jetlinks.reactor.ql.utils.CastUtils;
import java.util.*;
public interface PropertyMetadataConstants {
/**
* 属性来源
*/
interface Source {
//数据来源
String id = "source";
HeaderKey<String> headerKey = HeaderKey.of(id, null);
//手动写值
String manual = "manual";
//规则,虚拟属性
String rule = "rule";
static boolean isManual(DeviceMessage message) {
return message
.getHeader(Source.headerKey)
.map(Source.manual::equals)
.orElse(false);
}
static void setManual(DeviceMessage message) {
message.addHeader(headerKey, manual);
}
/**
* 判断属性是否手动赋值
*
* @param metadata 属性物模型
* @return 是否手动赋值
*/
static boolean isManual(PropertyMetadata metadata) {
return metadata.getExpand(id)
.map(manual::equals)
.orElse(false);
}
}
/**
* 属性读写模式
*/
interface AccessMode {
String id = "type";
//
String read = "read";
//
String write = "write";
//上报
String report = "report";
static boolean isRead(PropertyMetadata property) {
return property
.getExpand(id)
.map(val -> val.toString().contains(read))
.orElse(false);
}
static boolean isWrite(PropertyMetadata property) {
return property
.getExpand(id)
.map(val -> val.toString().contains(write))
.orElse(false);
}
static boolean isReport(PropertyMetadata property) {
return property
.getExpand(id)
.map(val -> val.toString().contains(report))
.orElse(false);
}
}
interface Metrics {
String id = "metrics";
static Map<String, Object> metricsToExpands(List<PropertyMetric> metrics) {
return Collections.singletonMap(id, metrics);
}
static List<PropertyMetric> getMetrics(PropertyMetadata metadata) {
return metadata
.getExpand(id)
.map(obj -> ConverterUtils.convertToList(obj, PropertyMetric::of))
.orElseGet(Collections::emptyList);
}
static Optional<PropertyMetric> getMetric(PropertyMetadata metadata, String metric) {
return metadata
.getExpand(id)
.map(obj -> {
for (PropertyMetric propertyMetric : ConverterUtils.convertToList(obj, PropertyMetric::of)) {
if (Objects.equals(metric, propertyMetric.getId())) {
return propertyMetric;
}
}
return null;
});
}
}
interface Group {
ConfigKey<String> id = ConfigKey.of("groupId", "分组ID", String.class);
ConfigKey<String> name = ConfigKey.of("groupName", "分组名称", String.class);
static String getId(PropertyMetadata metadata) {
return metadata
.getExpand(id)
.orElse("");
}
static String getName(PropertyMetadata metadata) {
return metadata
.getExpand(name)
.orElse("");
}
}
}

View File

@ -1,79 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.community.utils.ConverterUtils;
import org.springframework.util.StringUtils;
import jakarta.validation.constraints.NotBlank;
import java.util.Map;
import java.util.function.Function;
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PropertyMetric {
@Schema(description = "指标ID")
@NotBlank
private String id;
@Schema(description = "名称")
@NotBlank
private String name;
@Schema(description = "值,范围值使用逗号分隔")
private Object value;
@Schema(description = "是否为范围值")
private boolean range;
@Schema(description = "其他拓展配置")
private Map<String, Object> expands;
public Object castValue() {
if (value == null) {
return null;
}
if (range) {
return ConverterUtils.tryConvertToList(value, Function.identity());
}
return value;
}
public PropertyMetric merge(PropertyMetric another) {
if (!StringUtils.hasText(this.name)) {
this.setValue(another.value);
}
return this;
}
public static PropertyMetric of(String id, String name, Object value) {
PropertyMetric metric = new PropertyMetric();
metric.setId(id);
metric.setName(name);
metric.setValue(value);
return metric;
}
public static PropertyMetric of(Object mapMetric) {
return FastBeanCopier.copy(mapMetric, new PropertyMetric());
}
}

View File

@ -1,25 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import java.time.ZonedDateTime;
import java.util.Iterator;
public interface TimerIterable {
Iterator<ZonedDateTime> iterator(ZonedDateTime baseTime);
}

View File

@ -1,876 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import com.cronutils.builder.CronBuilder;
import com.cronutils.model.Cron;
import com.cronutils.model.definition.CronConstraintsFactory;
import com.cronutils.model.definition.CronDefinition;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.model.field.expression.FieldExpression;
import com.cronutils.model.field.expression.FieldExpressionFactory;
import com.cronutils.model.time.ExecutionTime;
import com.cronutils.parser.CronParser;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.ezorm.core.param.Term;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.i18n.LocaleUtils;
import org.jetlinks.community.terms.I18nSpec;
import org.reactivestreams.Subscription;
import org.springframework.util.Assert;
import reactor.core.CoreSubscriber;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import javax.annotation.Nonnull;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@Getter
@Setter
public class TimerSpec implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "触发方式")
@NotNull
private Trigger trigger;
@Schema(description = "使用日程标签进行触发")
private Set<String> scheduleTags;
//Cron表达式
@Schema(description = "触发方式为[cron]时不能为空")
private String cron;
@Schema(description = "执行的时间.为空则表示每天,触发方式为[week]则为1-7,触发方式为[month]时则为1-31")
private Set<Integer> when;
@Schema(description = "执行模式,一次还是周期执行")
private ExecuteMod mod;
@Schema(description = "执行模式为[period]时不能为空")
private Period period;
@Schema(description = "执行模式为[period]时不能与period同时为空")
private List<Period> periods;
@Schema(description = "执行模式为[once]时不能为空")
private Once once;
@Schema(description = "组合触发配置列表")
private Multi multi;
public static TimerSpec cron(String cron) {
TimerSpec spec = new TimerSpec();
spec.cron = cron;
spec.trigger = Trigger.cron;
return spec;
}
public List<Period> periods() {
List<Period> list = new ArrayList<>(1);
if (periods != null) {
list.addAll(periods);
}
if (period != null) {
list.add(period);
}
return list;
}
public Predicate<LocalDateTime> createRangeFilter() {
if (CollectionUtils.isEmpty(when)) {
return ignore -> true;
}
if (trigger == Trigger.week) {
return date -> when.contains(date.getDayOfWeek().getValue());
} else if (trigger == Trigger.month) {
return date -> when.contains(date.getDayOfMonth());
}
return ignore -> true;
}
public Predicate<LocalDateTime> createTimeFilter() {
Predicate<LocalDateTime> range = createRangeFilter();
if (mod == ExecuteMod.period) {
Predicate<LocalDateTime> predicate = null;
//可能多个周期
for (Period period : periods()) {
//周期执行指定了to,表示只在时间范围段内执行
LocalTime to = period.toLocalTime();
LocalTime from = period.fromLocalTime();
Predicate<LocalDateTime> _predicate = time -> !time.toLocalTime().isBefore(from);
if (to != null) {
_predicate = _predicate.and(time -> !time.toLocalTime().isAfter(to));
}
if (predicate == null) {
predicate = _predicate;
} else {
predicate = _predicate.or(predicate);
}
}
if (predicate == null) {
return range;
}
return predicate.and(range);
}
if (mod == ExecuteMod.once) {
LocalTime onceTime = once.localTime();
Predicate<LocalDateTime> predicate
= time -> compareOnceTime(time.toLocalTime(), onceTime) == 0;
return predicate.and(range);
}
return range;
}
public int compareOnceTime(LocalTime time1, LocalTime time2) {
int cmp = Integer.compare(time1.getHour(), time2.getHour());
if (cmp == 0) {
cmp = Integer.compare(time1.getMinute(), time2.getMinute());
if (cmp == 0) {
cmp = Integer.compare(time1.getSecond(), time2.getSecond());
//不比较纳秒
}
}
return cmp;
}
public String toCronExpression() {
return toCron().asString();
}
private static CronDefinition quartz() {
return CronDefinitionBuilder
.defineCron()
.withSeconds()
.withValidRange(0, 59)
.and()
.withMinutes()
.withValidRange(0, 59)
.and()
.withHours()
.withValidRange(0, 23)
.and()
.withDayOfMonth()
.withValidRange(1, 31)
.supportsL()
.supportsW()
.supportsLW()
.supportsQuestionMark()
.and()
.withMonth()
.withValidRange(1, 12)
.and()
.withDayOfWeek()
.withValidRange(1, 7)
.withMondayDoWValue(1)
.supportsHash()
.supportsL()
.supportsQuestionMark()
.and()
.withYear()
.withValidRange(1970, 2099)
.withStrictRange()
.optional()
.and()
.withCronValidation(CronConstraintsFactory.ensureEitherDayOfWeekOrDayOfMonth())
.instance();
}
public Cron toCron() {
CronDefinition definition = quartz();
if (trigger == Trigger.cron || trigger == null) {
Assert.hasText(cron, "error.scene_rule_timer_cron_cannot_be_empty");
return new CronParser(definition).parse(cron).validate();
}
CronBuilder builder = CronBuilder.cron(definition);
builder.withYear(FieldExpression.always());
builder.withMonth(FieldExpression.always());
FieldExpression range;
if (CollectionUtils.isNotEmpty(when)) {
FieldExpression expr = null;
for (Integer integer : when) {
if (expr == null) {
expr = FieldExpressionFactory.on(integer);
} else {
expr = expr.and(FieldExpressionFactory.on(integer));
}
}
range = expr;
} else {
range = FieldExpressionFactory.questionMark();
}
if (trigger == Trigger.week) {
builder.withDoM(FieldExpressionFactory.questionMark())
.withDoW(range);
} else if (trigger == Trigger.month) {
builder.withDoM(range)
.withDoW(FieldExpressionFactory.questionMark());
}
//执行一次
if (mod == ExecuteMod.once) {
LocalTime time = once.localTime();
builder.withHour(FieldExpressionFactory.on(time.getHour()));
builder.withMinute(FieldExpressionFactory.on(time.getMinute()));
builder.withSecond(FieldExpressionFactory.on(time.getSecond()));
}
//周期执行
if (mod == ExecuteMod.period) {
LocalTime time = period.fromLocalTime();
PeriodUnit unit = period.unit;
if (unit == PeriodUnit.hours) {
builder.withHour(FieldExpressionFactory.every(FieldExpressionFactory.on(time.getHour()), period.every))
.withMinute(FieldExpressionFactory.on(time.getMinute()))
.withSecond(FieldExpressionFactory.on(time.getSecond()));
} else if (unit == PeriodUnit.minutes) {
builder
.withHour(FieldExpressionFactory.always())
.withMinute(FieldExpressionFactory.every(FieldExpressionFactory.on(time.getMinute()), period.every))
.withSecond(FieldExpressionFactory.on(time.getSecond()));
} else if (unit == PeriodUnit.seconds) {
builder
.withHour(FieldExpressionFactory.always())
.withMinute(FieldExpressionFactory.always())
.withSecond(FieldExpressionFactory.every(FieldExpressionFactory.on(time.getSecond()), period.every));
}
}
return builder.instance().validate();
}
public void validate() {
if (trigger == null) {
Assert.hasText(cron, "error.scene_rule_timer_cron_cannot_be_empty");
}
if (trigger == Trigger.cron) {
try {
toCronExpression();
} catch (Throwable e) {
ValidationException exception = new ValidationException("cron", "error.cron_format_error", cron);
exception.addSuppressed(e);
throw exception;
}
} else if (trigger == Trigger.multi) {
List<TimerSpec> multiSpec = multi.getSpec();
if (CollectionUtils.isNotEmpty(multiSpec)) {
for (TimerSpec spec : multiSpec) {
spec.validate();
}
}
} else {
nextDurationBuilder().apply(ZonedDateTime.now());
}
}
@Getter
@Setter
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public static class Once implements Serializable {
private static final long serialVersionUID = 1L;
//时间点
@Schema(description = "时间点.格式:[hh:mm],或者[hh:mm:ss]")
@NotBlank
private String time;
public LocalTime localTime() {
return parsTime(time);
}
}
@Getter
@Setter
public static class Period implements Serializable {
private static final long serialVersionUID = 1L;
//周期执行的时间区间
@Schema(description = "执行时间范围从.格式:[hh:mm],或者[hh:mm:ss]")
private String from;
@Schema(description = "执行时间范围止.格式:[hh:mm],或者[hh:mm:ss]")
private String to;
@Schema(description = "周期值,如:每[every][unit]执行一次")
private int every;
@Schema(description = "周期执行单位")
private PeriodUnit unit;
public LocalTime fromLocalTime() {
return parsTime(from);
}
public LocalTime toLocalTime() {
return parsTime(to);
}
}
@Getter
@Setter
public static class Multi implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "组合触发配置列表")
private List<TimerSpec> spec;
@Schema(description = "多个触发的关系。and、or")
private Term.Type type = Term.Type.or;
@Schema(description = "组合触发时,只触发一次的最小时间间隔")
private int timeSpanSecond = 2;
}
private static LocalTime parsTime(String time) {
return LocalTime.parse(time);
}
/**
* 创建一个下一次执行时间间隔构造器,通过构造器来获取基准时间间隔
* <pre>{@code
*
* Function<ZonedDateTime, Duration> builder = nextDurationBuilder();
*
* Duration duration = builder.apply(ZonedDateTime.now());
*
* }</pre>
*
* @return 构造器
*/
public Function<ZonedDateTime, Duration> nextDurationBuilder() {
return nextDurationBuilder(ZonedDateTime.now());
}
public Function<ZonedDateTime, Duration> nextDurationBuilder(ZonedDateTime baseTime) {
Iterator<ZonedDateTime> it = iterable().iterator(baseTime);
return (time) -> {
Duration duration;
do {
duration = Duration.between(time, time = it.next());
}
while (duration.toMillis() < 0);
return duration;
};
}
/**
* 创建一个时间构造器,通过构造器来获取下一次时间
* <pre>{@code
*
* Function<ZonedDateTime, ZonedDateTime> builder = nextTimeBuilder();
*
* ZonedDateTime nextTime = builder.apply(ZonedDateTime.now());
*
* }</pre>
*
* @return 构造器
*/
public Function<ZonedDateTime, ZonedDateTime> nextTimeBuilder() {
TimerIterable it = iterable();
return time -> it.iterator(time).next();
}
static int MAX_IT_TIMES = 10000;
private TimerIterable cronIterable() {
Cron cron = this.toCron();
ExecutionTime executionTime = ExecutionTime.forCron(cron);
Predicate<LocalDateTime> filter = createTimeFilter();
return baseTime -> new Iterator<ZonedDateTime>() {
ZonedDateTime current = baseTime;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public ZonedDateTime next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
ZonedDateTime dateTime = current;
int i = 0;
do {
dateTime = executionTime
.nextExecution(dateTime)
.orElse(null);
if (dateTime == null) {
i++;
continue;
}
if (filter.test(dateTime.toLocalDateTime())) {
break;
}
} while (i < MAX_IT_TIMES);
return current = dateTime;
}
};
}
private TimerIterable periodIterable() {
List<Period> periods = periods();
Assert.notEmpty(periods, "period or periods can not be null");
Predicate<LocalDateTime> filter = createTimeFilter();
Duration _Duration = null;
LocalTime earliestFrom = LocalTime.MAX;
LocalTime latestTo = LocalTime.MIN;
//使用最小的执行周期进行判断?
for (Period period : periods) {
Duration duration = Duration.of(period.every, period.unit.temporal);
LocalTime from = period.fromLocalTime();
LocalTime to = period.toLocalTime();
// 更新最小的duration
if (_Duration == null || duration.compareTo(_Duration) < 0) {
_Duration = duration;
}
// 更新最早的起始时间
if (from != null && from.isBefore(earliestFrom)) {
earliestFrom = from;
}
// 更新最晚的结束时间
if (to != null && to.isAfter(latestTo)) {
latestTo = to;
}
}
Duration duration = _Duration;
LocalTime firstFrom = earliestFrom.equals(LocalTime.MAX) ? LocalTime.MIDNIGHT : earliestFrom;
LocalTime endTo = latestTo.equals(LocalTime.MIN) ? null : latestTo;
return baseTime -> new Iterator<ZonedDateTime>() {
ZonedDateTime current = baseTime;
@Override
public boolean hasNext() {
return true;
}
@Override
public ZonedDateTime next() {
ZonedDateTime dateTime = current;
int max = MAX_IT_TIMES;
do {
dateTime = dateTime.plus(duration);
// 检查时间是否在 firstFrom endTo 之间
LocalTime time = dateTime.toLocalTime();
if (time.isBefore(firstFrom) || time.isAfter(endTo)) {
// 获取第二天的 firstFrom
ZonedDateTime nextDayFromTime = dateTime.toLocalDate().plusDays(1).atTime(firstFrom).atZone(dateTime.getZone());
// 计算当前时间到 nextDayFromTime 的差值
Duration timeDifference = Duration.between(dateTime, nextDayFromTime);
// 计算可以整除的 duration 数量
long n = timeDifference.toMillis() / duration.toMillis();
// 跳转到下一个 n * duration 的时间点
dateTime = dateTime.plus(n * duration.toMillis(), ChronoUnit.MILLIS);
}
if (filter.test(dateTime.toLocalDateTime())) {
break;
}
max--;
} while (max > 0);
return current = dateTime;
}
};
}
private TimerIterable onceIterable() {
Assert.notNull(once, "once can not be null");
Predicate<LocalDateTime> filter = createTimeFilter();
LocalTime onceTime = once.localTime();
return baseTime -> new Iterator<ZonedDateTime>() {
ZonedDateTime current = baseTime;
@Override
public boolean hasNext() {
return true;
}
@Override
public ZonedDateTime next() {
ZonedDateTime dateTime = current;
int max = MAX_IT_TIMES;
if (!dateTime.toLocalTime().equals(onceTime)) {
dateTime = onceTime.atDate(dateTime.toLocalDate()).atZone(dateTime.getZone());
}
do {
if (filter.test(dateTime.toLocalDateTime()) && current.compareTo(dateTime) <= 0) {
current = dateTime.plusDays(1);
break;
}
dateTime = dateTime.plusDays(1);
max--;
} while (max > 0);
return dateTime;
}
};
}
private TimerIterable multiSpecIterable() {
List<TimerSpec> multiSpec = multi.getSpec();
Assert.notEmpty(multiSpec, "multiSpec can not be empty");
return baseTime -> new Iterator<ZonedDateTime>() {
final List<ZonedDateTime> timeList = new ArrayList<>(multiSpec.size());
final List<Iterator<ZonedDateTime>> iterators = multiSpec
.stream()
.map(spec -> spec.iterable().iterator(baseTime))
.collect(Collectors.toList());
@Override
public boolean hasNext() {
switch (multi.getType()) {
case and:
return iterators.stream().allMatch(Iterator::hasNext);
case or:
return iterators.stream().anyMatch(Iterator::hasNext);
default:
return false;
}
}
@Override
public ZonedDateTime next() {
switch (multi.getType()) {
case and:
return handleNextAnd();
case or:
return handleNextOr();
default:
return baseTime;
}
}
private ZonedDateTime handleNextAnd() {
ZonedDateTime dateTime = null;
int max = MAX_IT_TIMES;
int match = 0;
do {
for (Iterator<ZonedDateTime> iterator : iterators) {
ZonedDateTime next = iterator.next();
if (dateTime == null) {
dateTime = next;
}
// 若生成的时间比当前选中的时间早则继续生成
while (next.isBefore(dateTime)) {
next = iterator.next();
}
if (next.isEqual(dateTime)) {
// 所有定时器的next时间一致时返回时间
if (++match == iterators.size()) {
return dateTime;
}
} else {
dateTime = next;
}
}
max--;
} while (
max > 0
);
return dateTime;
}
private ZonedDateTime handleNextOr() {
ZonedDateTime earliest = null;
// 每个定时器生成next
fillTimeList();
// 获取最早的一个时间
for (ZonedDateTime zonedDateTime : timeList) {
if (earliest == null || earliest.isAfter(zonedDateTime) || earliest.isEqual(zonedDateTime)) {
earliest = zonedDateTime;
}
}
// 清空被选中的最早时间
for (int i = 0; i < timeList.size(); i++) {
if (timeList.get(i).isEqual(earliest)) {
timeList.set(i, null);
}
}
return earliest;
}
/**
* 遍历所有定时器若有next为空的则生成新的
*/
private void fillTimeList() {
for (int i = 0; i < iterators.size(); i++) {
if (timeList.size() <= i) {
timeList.add(iterators.get(i).next());
} else if (timeList.get(i) == null) {
timeList.set(i, iterators.get(i).next());
}
}
}
};
}
public TimerIterable iterable() {
if (trigger == Trigger.multi) {
return multiSpecIterable();
}
if ((trigger == Trigger.cron || trigger == null) && cron != null) {
return cronIterable();
}
return mod == ExecuteMod.period ? periodIterable() : onceIterable();
}
public List<ZonedDateTime> getNextExecuteTimes(ZonedDateTime from, long times) {
List<ZonedDateTime> timeList = new ArrayList<>((int) times);
Iterator<ZonedDateTime> it = iterable().iterator(from);
for (long i = 0; i < times; i++) {
timeList.add(it.next());
}
return timeList;
}
public Flux<Long> flux() {
return flux(Schedulers.parallel());
}
public Flux<Long> flux(Scheduler scheduler) {
return new TimerFlux(nextDurationBuilder(), scheduler);
}
@Override
public String toString() {
if (getTrigger() == null) {
return null;
}
switch (getTrigger()) {
case week: {
return weekDesc();
}
case month: {
return monthDesc();
}
case cron: {
return getCron();
}
}
return null;
}
private String weekDesc() {
I18nSpec spec = new I18nSpec();
spec.setCode("message.timer_spec_desc");
List<I18nSpec> args = new ArrayList<>();
if (when == null || when.isEmpty()) {
args.add(I18nSpec.of("message.timer_spec_desc_everyday", "每天"));
} else {
String week = when
.stream()
.map(weekNum -> LocaleUtils.resolveMessage("message.timer_spec_desc_week_" + weekNum))
.collect(Collectors.joining(LocaleUtils.resolveMessage("message.timer_spec_desc_seperator")));
args.add(I18nSpec.of(
"message.timer_spec_desc_everyweek",
"每周" + week,
week));
}
args.add(timerModDesc());
spec.setArgs(args);
return spec.resolveI18nMessage();
}
private String monthDesc() {
I18nSpec spec = new I18nSpec();
spec.setCode("message.timer_spec_desc");
List<I18nSpec> args = new ArrayList<>();
if (when == null || when.isEmpty()) {
args.add(I18nSpec.of("message.timer_spec_desc_everyday", "每天"));
} else {
String month = when
.stream()
.map(monthNum -> {
switch (monthNum) {
case 1:
return LocaleUtils.resolveMessage("message.timer_spec_desc_month_1", monthNum);
case 2:
return LocaleUtils.resolveMessage("message.timer_spec_desc_month_2", monthNum);
case 3:
return LocaleUtils.resolveMessage("message.timer_spec_desc_month_3", monthNum);
default:
return LocaleUtils.resolveMessage("message.timer_spec_desc_month", monthNum);
}
})
.collect(Collectors.joining(LocaleUtils.resolveMessage("message.timer_spec_desc_seperator")));
args.add(I18nSpec.of(
"message.timer_spec_desc_everymonth",
"每月" + month,
month));
}
args.add(timerModDesc());
spec.setArgs(args);
return spec.resolveI18nMessage();
}
private I18nSpec timerModDesc() {
switch (getMod()) {
case period: {
if (getPeriod() == null) {
break;
}
return I18nSpec.of(
"message.timer_spec_desc_period",
getPeriod().getFrom() + "-" + getPeriod().getTo() +
"" + getPeriod().getEvery() + getPeriod().getUnit().name(),
I18nSpec.of("message.timer_spec_desc_period_duration",
getPeriod().getFrom() + "-" + getPeriod().getTo(),
getPeriod().getFrom(),
getPeriod().getTo()),
getPeriod().getEvery(),
I18nSpec.of("message.timer_spec_desc_period_" + getPeriod().getUnit().name(),
getPeriod().getUnit().name())
);
}
case once: {
// [time]执行1次
if (getOnce() == null) {
break;
}
return I18nSpec.of("message.timer_spec_desc_period_once", getOnce().getTime(), getOnce().getTime());
}
}
return I18nSpec.of("", "");
}
@AllArgsConstructor
static class TimerFlux extends Flux<Long> {
final Function<ZonedDateTime, Duration> spec;
final Scheduler scheduler;
@Override
public void subscribe(@Nonnull CoreSubscriber<? super Long> coreSubscriber) {
TimerSubscriber subscriber = new TimerSubscriber(spec, scheduler, coreSubscriber);
coreSubscriber.onSubscribe(subscriber);
}
}
static class TimerSubscriber implements Subscription {
final Function<ZonedDateTime, Duration> spec;
final CoreSubscriber<? super Long> subscriber;
final Scheduler scheduler;
long count;
Disposable scheduling;
public TimerSubscriber(Function<ZonedDateTime, Duration> spec,
Scheduler scheduler,
CoreSubscriber<? super Long> subscriber) {
this.scheduler = scheduler;
this.spec = spec;
this.subscriber = subscriber;
}
@Override
public void request(long l) {
trySchedule();
}
@Override
public void cancel() {
if (scheduling != null) {
scheduling.dispose();
}
}
public void onNext() {
if (canSchedule()) {
subscriber.onNext(count++);
}
trySchedule();
}
void trySchedule() {
if (scheduling != null) {
scheduling.dispose();
}
ZonedDateTime now = ZonedDateTime.ofInstant(Instant.ofEpochMilli(scheduler.now(TimeUnit.MILLISECONDS)), ZoneId.systemDefault());
Duration delay = spec.apply(now);
scheduling = scheduler
.schedule(
this::onNext,
delay.toMillis(),
TimeUnit.MILLISECONDS
);
}
protected boolean canSchedule() {
return true;
}
}
public enum Trigger {
//按周
week,
//按月
month,
//cron表达式
cron,
// 多个触发组合
multi
}
public enum ExecuteMod {
period,
once
}
@AllArgsConstructor
public enum PeriodUnit {
seconds(ChronoUnit.SECONDS),
minutes(ChronoUnit.MINUTES),
hours(ChronoUnit.HOURS);
private final TemporalUnit temporal;
}
}

View File

@ -1,23 +1,7 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.community.utils.TimeUtils;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.springframework.util.StringUtils;
import java.time.Duration;
@ -27,11 +11,11 @@ import java.util.Optional;
public interface ValueObject {
Map<String, Object> values();
Map<String, Object> getAll();
default Optional<Object> get(String name) {
return Optional.ofNullable(values())
.map(map -> map.get(name));
return Optional.ofNullable(getAll())
.map(map -> map.get(name));
}
default Optional<Integer> getInt(String name) {
@ -60,8 +44,9 @@ public interface ValueObject {
.map(Interval::of);
}
default Interval getInterval(String name, Interval defaultValue) {
return getInterval(name)
default Interval getInterval(String name,Interval defaultValue) {
return getString(name)
.map(Interval::of)
.orElse(defaultValue);
}
@ -71,14 +56,9 @@ public interface ValueObject {
}
default Optional<Date> getDate(String name) {
return this
.get(name)
.map(d -> {
if (d instanceof Date) {
return (Date) d;
}
return TimeUtils.parseDate(String.valueOf(d));
});
return get(name)
.map(String::valueOf)
.map(TimeUtils::parseDate);
}
default Date getDate(String name, Date defaultValue) {
@ -103,8 +83,7 @@ public interface ValueObject {
}
default Optional<Boolean> getBoolean(String name) {
return get(name)
.map(CastUtils::castBoolean);
return get(name, Boolean.class);
}
default boolean getBoolean(String name, boolean defaultValue) {
@ -116,12 +95,7 @@ public interface ValueObject {
.map(obj -> FastBeanCopier.DEFAULT_CONVERT.convert(obj, type, FastBeanCopier.EMPTY_CLASS_ARRAY));
}
@SuppressWarnings("unchecked")
static ValueObject of(Map<String, ?> mapVal) {
return () -> (Map<String, Object>) mapVal;
}
default <T> T as(Class<T> type) {
return FastBeanCopier.copy(values(), type);
static ValueObject of(Map<String, Object> mapVal) {
return () -> mapVal;
}
}

View File

@ -1,18 +1,3 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community;
import lombok.Getter;
@ -23,6 +8,6 @@ public class Version {
private final String edition = "community";
private final String version = "2.11.0-SNAPSHOT";
private final String version = "1.4.0-SNAPSHOT";
}

View File

@ -1,79 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.annotation.command;
import org.jetlinks.core.annotation.command.CommandHandler;
import org.springframework.stereotype.Indexed;
import java.lang.annotation.*;
/**
* 标记一个类为命令服务支持端点,用于对外提供命令支持
* <pre>{@code
*
* @CommandService("myService")
* public class MyCommandService{
*
* }
*
* }</pre>
*
* @author zhouhao
* @since 1.2.3
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Indexed
public @interface CommandService {
/**
* 服务标识
*
* @return 服务标识
*/
String id();
/**
* 服务名称
*
* @return 服务名称
*/
String name();
/**
* 服务描述
*
* @return 服务描述
*/
String[] description() default {};
/**
* 是否根据注解扫描注册服务
*
* @return 是否注册服务
*/
boolean autoRegistered() default true;
/**
* 命令定义,用于声明支持的命令
*
* @return 命令定义
*/
CommandHandler[] commands() default {};
}

View File

@ -1,81 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.authorize;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.DefaultDimensionType;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
@Getter
@Setter
public class AuthenticationSpec implements Serializable {
private static final long serialVersionUID = 3512105446265694264L;
private RoleSpec role;
private List<PermissionSpec> permissions;
@Getter
@Setter
public static class RoleSpec {
private List<String> idList;
}
@Getter
@Setter
public static class PermissionSpec implements Serializable {
private static final long serialVersionUID = 7188197046015343251L;
private String id;
private List<String> actions;
}
public boolean isGranted(Authentication auth) {
return createFilter().test(auth);
}
public Predicate<Authentication> createFilter() {
RoleSpec role = this.role;
List<PermissionSpec> permissions = this.permissions;
List<Predicate<Authentication>> all = new ArrayList<>();
if (null != role && role.getIdList() != null) {
all.add(auth -> auth.hasDimension(DefaultDimensionType.role.getId(), role.getIdList()));
}
if (null != permissions) {
for (PermissionSpec permission : permissions) {
all.add(auth -> auth.hasPermission(permission.getId(), permission.getActions()));
}
}
Predicate<Authentication> temp = null;
for (Predicate<Authentication> predicate : all) {
if (temp == null) {
temp = predicate;
} else {
temp = temp.and(predicate);
}
}
return temp == null ? auth -> true : temp;
}
}

View File

@ -1,448 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.authorize;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Collections2;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import lombok.AllArgsConstructor;
import lombok.Setter;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.hswebframework.web.authorization.*;
import org.hswebframework.web.authorization.simple.*;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.core.metadata.Jsonable;
import org.jetlinks.core.utils.RecyclerUtils;
import org.jetlinks.core.utils.SerializeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
@Setter
public class FastSerializableAuthentication extends SimpleAuthentication
implements Externalizable, Jsonable {
private static final Logger log = LoggerFactory.getLogger(FastSerializableAuthentication.class);
static {
SerializeUtils.registerSerializer(
0x90,
FastSerializableAuthentication.class,
(ignore) -> new FastSerializableAuthentication());
}
public static void load() {
}
@SuppressWarnings("all")
public static Authentication of(Object jsonOrObject, boolean share) {
if (jsonOrObject == null) {
return null;
}
//json
if (jsonOrObject instanceof String) {
return of((String) jsonOrObject, share);
}
// map
if (jsonOrObject instanceof Map) {
FastSerializableAuthentication fast = new FastSerializableAuthentication();
fast.shared = share;
fast.fromJson(new JSONObject((Map) jsonOrObject));
return fast;
}
//auth
if (jsonOrObject instanceof Authentication) {
return of(((Authentication) jsonOrObject));
}
throw new UnsupportedOperationException();
}
public static Authentication of(String json, boolean share) {
if (StringUtils.isEmpty(json)) {
return null;
}
FastSerializableAuthentication fast = new FastSerializableAuthentication();
fast.shared = share;
fast.fromJson(JSON.parseObject(json));
return fast;
}
public static Authentication of(Authentication auth) {
return of(auth, false);
}
public static Authentication of(Authentication auth, boolean simplify) {
if (auth instanceof FastSerializableAuthentication) {
((FastSerializableAuthentication) auth).simplify = simplify;
return auth;
}
FastSerializableAuthentication fast = new FastSerializableAuthentication();
fast.setUser(auth.getUser());
fast.setSimplify(simplify);
fast.getPermissions().addAll(auth.getPermissions());
fast.getDimensions().addAll(auth.getDimensions());
fast.getAttributes().putAll(auth.getAttributes());
return fast;
}
@Override
protected FastSerializableAuthentication newInstance() {
return new FastSerializableAuthentication();
}
/**
* 是否简化,为true时,不序列化权限名称
*
* @see Permission#getName()
*/
private boolean simplify = false;
private transient boolean shared;
public void makeShared() {
shared = true;
List<Dimension> dimensions = getDimensions()
.stream()
.map(RecyclerUtils::intern)
.collect(Collectors.toList());
setDimensions(dimensions);
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
String userId = null;
try {
out.writeByte(0x01);
//是否简化模式
out.writeBoolean(simplify);
//user
User user = getUser();
out.writeUTF(user.getId() == null ? "" : (userId = user.getId()));
SerializeUtils.writeNullableUTF(user.getName(), out);
out.writeUTF(user.getUsername() == null ? "" : user.getUsername());
SerializeUtils.writeNullableUTF(user.getUserType(), out);
SerializeUtils.writeKeyValue(user.getOptions(), out);
//permission
{
List<Permission> permissions = getPermissions();
if (permissions == null) {
permissions = Collections.emptyList();
}
out.writeInt(permissions.size());
for (Permission permission : permissions) {
write(permission, out);
}
}
//dimension
{
List<Dimension> dimensions = getDimensions();
if (dimensions == null) {
dimensions = Collections.emptyList();
}
out.writeInt(dimensions.size());
for (Dimension permission : dimensions) {
write(permission, out);
}
}
SerializeUtils.writeKeyValue(getAttributes(), out);
} catch (Throwable e) {
log.warn("write FastSerializableAuthentication [{}] error", userId, e);
throw e;
}
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
byte version = in.readByte();
simplify = in.readBoolean();
//user
SimpleUser user = new SimpleUser();
user.setId(in.readUTF());
user.setName(SerializeUtils.readNullableUTF(in));
user.setUsername(in.readUTF());
user.setUserType(SerializeUtils.readNullableUTF(in));
user.setOptions(SerializeUtils.readMap(in, Maps::newHashMapWithExpectedSize));
setUser0(user);
//permission
{
int size = in.readInt();
List<Permission> permissions = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
Permission permission = readPermission(in);
permissions.add(permission);
}
setPermissions(permissions);
}
//dimension
{
int size = in.readInt();
Set<Dimension> dimensions = new HashSet<>(size);
for (int i = 0; i < size; i++) {
Dimension dimension = readDimension(in);
dimensions.add(dimension);
}
setDimensions(dimensions);
}
setAttributes(SerializeUtils.readMap(in, Maps::newHashMapWithExpectedSize));
}
@SneakyThrows
private Dimension readDimension(ObjectInput in) {
SimpleDimension dimension = new SimpleDimension();
dimension.setId(in.readUTF());
dimension.setName(in.readUTF());
dimension.setOptions(SerializeUtils.readMap(
in,
k -> RecyclerUtils.intern(String.valueOf(k)),
Function.identity(),
Maps::newHashMapWithExpectedSize));
boolean known = in.readBoolean();
if (known) {
KnownDimension knownDimension = KnownDimension.ALL[in.readByte()];
dimension.setType(knownDimension.type);
} else {
SimpleDimensionType type = new SimpleDimensionType();
type.setId(in.readUTF());
type.setName(in.readUTF());
dimension.setType(RecyclerUtils.intern(type));
}
return dimension;
}
@SneakyThrows
private void write(Dimension dimension, ObjectOutput out) {
out.writeUTF(dimension.getId());
out.writeUTF(dimension.getName() == null ? "" : dimension.getName());
SerializeUtils.writeKeyValue(dimension.getOptions(), out);
KnownDimension knownDimension = KnownDimension.MAPPING.get(dimension.getType().getId());
out.writeBoolean(knownDimension != null);
if (knownDimension != null) {
out.writeByte(knownDimension.ordinal());
} else {
out.writeUTF(dimension.getType().getId());
out.writeUTF(dimension.getType().getName());
}
}
@SneakyThrows
private Permission readPermission(ObjectInput in) {
SimplePermission permission = new SimplePermission();
permission.setId(in.readUTF());
if (!simplify) {
permission.setName(in.readUTF());
} else {
permission.setName(permission.getId());
}
permission.setOptions(SerializeUtils.readMap(in, Maps::newHashMapWithExpectedSize));
int actionSize = in.readUnsignedShort();
Set<String> actions = Sets.newHashSetWithExpectedSize(actionSize);
for (int i = 0; i < actionSize; i++) {
if (in.readBoolean()) {
actions.add(KnownAction.ALL[in.readByte()].action);
} else {
actions.add(in.readUTF());
}
}
permission.setActions(actions);
return permission;
}
@SneakyThrows
private void write(Permission permission, ObjectOutput out) {
out.writeUTF(permission.getId());
if (!simplify) {
out.writeUTF(permission.getName() == null ? "" : permission.getName());
}
SerializeUtils.writeKeyValue(permission.getOptions(), out);
Set<String> actions = permission.getActions();
out.writeShort(actions.size());
for (String action : actions) {
KnownAction knownAction = KnownAction.ACTION_MAP.get(action);
out.writeBoolean(knownAction != null);
if (null != knownAction) {
out.writeByte(knownAction.ordinal());
} else {
out.writeUTF(action);
}
}
}
@AllArgsConstructor
enum KnownDimension {
user(DefaultDimensionType.user),
role(DefaultDimensionType.role),
org(OrgDimensionType.org),
parentOrg(OrgDimensionType.parentOrg);
private final DimensionType type;
static final KnownDimension[] ALL = values();
static final Map<Object, KnownDimension> MAPPING = new HashMap<>();
static {
for (KnownDimension value : ALL) {
MAPPING.put(value, value);
MAPPING.put(value.ordinal(), value);
MAPPING.put(value.name(), value);
}
}
}
enum KnownAction {
query,
get,
update,
save,
delete,
export,
_import(Permission.ACTION_IMPORT),
enable,
disable;
static final KnownAction[] ALL = values();
static final Map<Object, KnownAction> ACTION_MAP = new HashMap<>();
static {
for (KnownAction value : ALL) {
ACTION_MAP.put(value, value);
ACTION_MAP.put(value.ordinal(), value);
ACTION_MAP.put(value.action, value);
}
}
private final String action;
KnownAction() {
this.action = name();
}
KnownAction(String action) {
this.action = action;
}
}
@Override
public JSONObject toJson() {
JSONObject obj = new JSONObject();
obj.put("user", SerializeUtils.convertToSafelySerializable(getUser()));
obj.put("permissions", SerializeUtils.convertToSafelySerializable(getPermissions()));
//忽略user
obj.put("dimensions", SerializeUtils.convertToSafelySerializable(
Collections2.filter(getDimensions(), i -> !(i instanceof User))
));
obj.put("attributes", new HashMap<>(getAttributes()));
return obj;
}
@Override
public void fromJson(JSONObject json) {
JSONObject user = json.getJSONObject("user");
if (user != null) {
setUser(user.toJavaObject(SimpleUser.class));
}
JSONArray permissions = json.getJSONArray("permissions");
if (permissions != null) {
for (int i = 0, size = permissions.size(); i < size; i++) {
JSONObject permission = permissions.getJSONObject(i);
//不再支持
permission.remove("dataAccesses");
Object actions = permission.remove("actions");
SimplePermission perm = permission.toJavaObject(SimplePermission.class);
if (actions instanceof Collection) {
@SuppressWarnings("all")
Collection<Object> _actions = (Collection<Object>) actions;
Set<String> acts = Sets.newHashSetWithExpectedSize(_actions.size());
for (Object action : _actions) {
KnownAction act = KnownAction.ACTION_MAP.get(action);
if (act == null) {
acts.add(String.valueOf(action));
} else {
acts.add(act.action);
}
}
perm.setActions(acts);
}
getPermissions().add(shared ? RecyclerUtils.intern(perm) : perm);
}
}
JSONArray dimensions = json.getJSONArray("dimensions");
if (dimensions != null) {
for (int i = 0, size = dimensions.size(); i < size; i++) {
JSONObject dimension = dimensions.getJSONObject(i);
Object type = dimension.remove("type");
if (type == null) {
continue;
}
SimpleDimension simpleDimension = dimension.toJavaObject(SimpleDimension.class);
if (type instanceof DimensionType) {
simpleDimension.setType((DimensionType) type);
} else {
KnownDimension knownDimension = KnownDimension.MAPPING.get(type);
if (knownDimension != null) {
simpleDimension.setType(knownDimension.type);
} else {
SimpleDimensionType dimensionType;
if (type instanceof String) {
dimensionType = SimpleDimensionType.of(String.valueOf(type));
} else {
dimensionType = FastBeanCopier.copy(type, new SimpleDimensionType());
}
if (StringUtils.isNoneEmpty(dimensionType.getId())) {
simpleDimension.setType(shared ? RecyclerUtils.intern(dimensionType) : dimensionType);
}
}
}
getDimensions().add(shared ? RecyclerUtils.intern(simpleDimension) : simpleDimension);
}
}
JSONObject attr = json.getJSONObject("attributes");
if (attr != null) {
getAttributes().putAll(Maps.transformValues(attr, Serializable.class::cast));
}
}
}

View File

@ -1,37 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.authorize;
import lombok.AllArgsConstructor;
import lombok.Generated;
import lombok.Getter;
import org.hswebframework.web.authorization.DimensionType;
/**
* @author wangzheng
* @since 1.0
*/
@AllArgsConstructor
@Getter
@Generated
public enum OrgDimensionType implements DimensionType {
org("org","组织"),
parentOrg("parentOrg","上级组织");
private final String id;
private final String name;
}

View File

@ -1,96 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
import org.jetlinks.community.Operation;
import org.jetlinks.community.OperationSource;
import org.jetlinks.community.OperationType;
import org.jetlinks.community.event.SystemEventHolder;
import org.jetlinks.community.utils.TimeUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicLongFieldUpdater;
public abstract class AbstractBufferEviction implements BufferEviction {
public static final OperationType OPERATION_TYPE = OperationType.of("buffer-eviction", "缓冲区数据丢弃");
private static final AtomicLongFieldUpdater<AbstractBufferEviction>
LAST_EVENT_TIME = AtomicLongFieldUpdater.newUpdater(AbstractBufferEviction.class, "lastEventTime");
private static final AtomicIntegerFieldUpdater<AbstractBufferEviction>
LAST_TIMES = AtomicIntegerFieldUpdater.newUpdater(AbstractBufferEviction.class, "lastTimes");
//最大事件推送频率
//可通过java -Djetlinks.buffer.eviction.event.max-interval=10m修改配置
private static final long MAX_EVENT_INTERVAL =
TimeUtils.parse(System.getProperty("jetlinks.buffer.eviction.event.max-interval", "10m")).toMillis();
private volatile long lastEventTime;
private volatile int lastTimes;
abstract boolean doEviction(EvictionContext context);
@Override
public boolean tryEviction(EvictionContext context) {
if (doEviction(context)) {
sendEvent(context);
return true;
}
return false;
}
private String operationCode() {
return getClass().getSimpleName();
}
private void sendEvent(EvictionContext context) {
long now = System.currentTimeMillis();
long time = LAST_EVENT_TIME.get(this);
//记录事件推送周期内总共触发了多少次
LAST_TIMES.incrementAndGet(this);
//超过间隔事件则推送事件,防止推送太多错误事件
if (now - time > MAX_EVENT_INTERVAL) {
LAST_EVENT_TIME.set(this, now);
Map<String, Object> info = new HashMap<>();
//缓冲区数量
info.put("bufferSize", context.size(EvictionContext.BufferType.buffer));
//死数据数量
info.put("deadSize", context.size(EvictionContext.BufferType.dead));
//总计触发次数
info.put("times", LAST_TIMES.getAndSet(this, 0));
//应用自定义的数据,比如磁盘剩余空间等信息
applyEventData(info);
//推送系统事件
SystemEventHolder.warn(
Operation.of(
OperationSource.of(context.getName(), "eviction"),
OPERATION_TYPE),
operationCode(),
info
);
}
}
protected void applyEventData(Map<String, Object> data) {
}
}

View File

@ -1,148 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
import org.springframework.util.unit.DataSize;
import java.io.File;
/**
* 缓存淘汰策略
*
* @author zhouhao
* @since 2.0
*/
public interface BufferEviction {
BufferEviction NONE = new BufferEviction() {
@Override
public boolean tryEviction(EvictionContext context) {
return false;
}
@Override
public String toString() {
return "None";
}
};
/**
* 根据磁盘使用率来进行淘汰,磁盘使用率超过阈值时则淘汰旧数据
*
* @param path 文件路径
* @param threshold 使用率阈值 范围为0-1 .: 0.8 表示磁盘使用率超过80%则丢弃数据
* @return 淘汰策略
*/
static BufferEviction disk(String path, float threshold) {
return new DiskUsageEviction(new File(path), threshold);
}
/**
* 根据磁盘可用空间来进行淘汰,磁盘剩余空间低于阈值时则淘汰旧数据
*
* @param path 文件路径
* @param minUsableDataSize 磁盘最小可用空间阈值,当磁盘可用空间低于此值时则则淘汰旧数据
* @return 淘汰策略
*/
static BufferEviction disk(String path, DataSize minUsableDataSize) {
return new DiskFreeEviction(new File(path), minUsableDataSize.toBytes());
}
/**
* 根据缓冲区数量来淘汰数据,当数量超过指定阈值后则淘汰旧数据
*
* @param bufferLimit 数量阈值
* @return 淘汰策略
*/
static BufferEviction limit(long bufferLimit) {
return limit(bufferLimit, bufferLimit);
}
/**
* 根据缓冲区数量来淘汰数据,当数量超过指定阈值后则淘汰旧数据
*
* @param bufferLimit 缓冲数量阈值
* @param deadLimit 死数据数量阈值
* @return 淘汰策略
*/
static BufferEviction limit(long bufferLimit, long deadLimit) {
return new SizeLimitEviction(bufferLimit, deadLimit);
}
/**
* 根据缓冲区数量来淘汰死数据
*
* @param deadLimit 死数据数量阈值
* @return 淘汰策略
*/
static BufferEviction deadLimit(long deadLimit) {
return new SizeLimitEviction(-1, deadLimit);
}
/**
* 尝试执行淘汰
*
* @param context 上下文
* @return 是否有数据被淘汰
*/
boolean tryEviction(EvictionContext context);
/**
* 组合另外一个淘汰策略,2个策略同时执行.
*
* @param after 后续策略
* @return 淘汰策略
*/
default BufferEviction and(BufferEviction after) {
BufferEviction self = this;
return new BufferEviction() {
@Override
public boolean tryEviction(EvictionContext context) {
return self.tryEviction(context) & after.tryEviction(context);
}
@Override
public String toString() {
return self + " and " + after;
}
};
}
/**
* 组合另外一个淘汰策略,当前策略淘汰了数据才执行另外一个策略
*
* @param after 后续策略
* @return 淘汰策略
*/
default BufferEviction then(BufferEviction after) {
BufferEviction self = this;
return new BufferEviction() {
@Override
public boolean tryEviction(EvictionContext context) {
if (self.tryEviction(context)) {
after.tryEviction(context);
return true;
}
return false;
}
@Override
public String toString() {
return self + " then " + after;
}
};
}
}

View File

@ -1,70 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
import lombok.Getter;
import lombok.Setter;
import org.springframework.util.unit.DataSize;
@Getter
@Setter
public class BufferEvictionSpec {
public static final BufferEviction DEFAULT = new BufferEvictionSpec().build();
//最大队列数量,超过则淘汰最旧的数据
private int maxSize = -1;
//最大死信数量,超过则淘汰dead数据
private int maxDeadSize = Integer.getInteger("jetlinks.buffer.dead.limit", 100_0000);
//根据磁盘空间淘汰数据
private DataSize diskFree = DataSize.parse(System.getProperty("jetlinks.buffer.disk.free.threshold", "4GB"));
//磁盘最大使用率
private float diskThreshold;
//判断磁盘空间大小的目录
private String diskPath = System.getProperty("jetlinks.buffer.disk.free.path", "./");
public BufferEviction build() {
BufferEviction
eviction = null,
size = BufferEviction.limit(maxSize, maxDeadSize),
disk = null;
if (diskThreshold > 0) {
disk = BufferEviction.disk(diskPath, diskThreshold);
} else if (diskFree != null) {
disk = BufferEviction.disk(diskPath, diskFree);
}
if (disk != null) {
eviction = disk;
}
if (eviction == null) {
eviction = size;
} else {
eviction = eviction.then(size);
}
return eviction;
}
}

View File

@ -1,53 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
import lombok.Getter;
import lombok.Setter;
import java.time.Duration;
@Getter
@Setter
public class BufferProperties {
//缓冲文件存储目录
private String filePath;
//缓冲区大小,超过此大小将执行 handler 处理逻辑
private int size = 1000;
//缓冲超时时间
private Duration timeout = Duration.ofSeconds(1);
//并行度,表示支持并行写入的最大线程数.
private int parallelism = Math.max(1, Runtime.getRuntime().availableProcessors());
//最大重试次数,超过此次数的数据将会放入死队列.
private long maxRetryTimes = 64;
//文件操作的最大并行度,默认为1,不建议设置超过4.
private int fileConcurrency = 1;
//消费策略 默认先进先出
private ConsumeStrategy strategy = ConsumeStrategy.FIFO;
//淘汰策略
private BufferEvictionSpec eviction = new BufferEvictionSpec();
public boolean isExceededRetryCount(int count) {
return maxRetryTimes > 0 && count >= maxRetryTimes;
}
}

View File

@ -1,220 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.jetlinks.community.utils.ErrorUtils;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.transaction.CannotCreateTransactionException;
import java.io.IOException;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
@Getter
@AllArgsConstructor
public class BufferSettings {
private static final Predicate<Throwable> DEFAULT_RETRY_WHEN_ERROR =
e -> ErrorUtils.hasException(e, IOException.class,
IllegalStateException.class,
RejectedExecutionException.class,
TimeoutException.class,
DataAccessResourceFailureException.class,
CannotCreateTransactionException.class,
QueryTimeoutException.class);
public static Predicate<Throwable> defaultRetryWhenError() {
return DEFAULT_RETRY_WHEN_ERROR;
}
public static BufferEviction defaultEviction(){
return BufferEvictionSpec.DEFAULT;
}
private final String filePath;
private final String fileName;
//缓存淘汰策略
private final BufferEviction eviction;
private final Predicate<Throwable> retryWhenError;
//缓冲区大小,超过此大小将执行 handler 处理逻辑
private final int bufferSize;
//缓冲超时时间
private final Duration bufferTimeout;
//并行度,表示支持并行写入的最大线程数.
private final int parallelism;
//最大重试次数,超过此次数的数据将会放入死队列.
private final long maxRetryTimes;
private final int fileConcurrency;
private final ConsumeStrategy strategy;
public static BufferSettings create(String filePath, String fileName) {
return new BufferSettings(
filePath,
fileName,
defaultEviction(),
//默认重试逻辑
defaultRetryWhenError(),
1000,
Duration.ofSeconds(1),
Math.max(1, Runtime.getRuntime().availableProcessors() / 2),
5,
1,
ConsumeStrategy.FIFO);
}
public static BufferSettings create(BufferProperties properties) {
return create("buffer.queue", properties);
}
public static BufferSettings create(String fileName, BufferProperties properties) {
return create(properties.getFilePath(), fileName).properties(properties);
}
public BufferSettings eviction(BufferEviction eviction) {
return new BufferSettings(filePath,
fileName,
eviction,
retryWhenError,
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings bufferSize(int bufferSize) {
return new BufferSettings(filePath,
fileName,
eviction,
retryWhenError,
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings bufferTimeout(Duration bufferTimeout) {
return new BufferSettings(filePath,
fileName,
eviction,
retryWhenError,
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings parallelism(int parallelism) {
return new BufferSettings(filePath,
fileName,
eviction,
retryWhenError,
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings maxRetry(int maxRetryTimes) {
return new BufferSettings(filePath,
fileName,
eviction,
retryWhenError,
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings retryWhenError(Predicate<Throwable> retryWhenError) {
return new BufferSettings(filePath,
fileName,
eviction,
Objects.requireNonNull(retryWhenError),
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings fileConcurrency(int fileConcurrency) {
return new BufferSettings(filePath,
fileName,
eviction,
retryWhenError,
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings strategy(ConsumeStrategy strategy) {
return new BufferSettings(filePath,
fileName,
eviction,
retryWhenError,
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings properties(BufferProperties properties) {
return new BufferSettings(filePath,
fileName,
properties.getEviction().build(),
Objects.requireNonNull(retryWhenError),
properties.getSize(),
properties.getTimeout(),
properties.getParallelism(),
properties.getMaxRetryTimes(),
properties.getFileConcurrency(),
properties.getStrategy()
);
}
}

View File

@ -1,46 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
/**
* 已缓冲的数据
*
* @param <T> 数据类型
* @author zhouhao
* @since 2.2
*/
public interface Buffered<T> {
/**
* @return 数据
*/
T getData();
/**
* @return 当前重试次数
*/
int getRetryTimes();
/**
* 标记是否重试此数据
*/
void retry(boolean retry);
/**
* 标记此数据为死信
*/
void dead();
}

View File

@ -1,25 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
public enum ConsumeStrategy {
// 先进先出
FIFO,
// 后进先出
LIFO
}

View File

@ -1,74 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
import org.jetlinks.community.utils.FormatUtils;
import org.springframework.util.unit.DataSize;
import java.io.File;
import java.util.Map;
class DiskFreeEviction extends AbstractBufferEviction {
private final File path;
private final long minUsableBytes;
public DiskFreeEviction(File path, long minUsableBytes) {
this.path = path;
this.minUsableBytes = minUsableBytes;
}
private volatile long usableSpace = -1;
private volatile long lastUpdateTime;
@Override
public boolean doEviction(EvictionContext context) {
tryUpdate();
if (freeOutOfThreshold()) {
context.removeOldest(EvictionContext.BufferType.buffer);
return true;
}
return false;
}
protected boolean freeOutOfThreshold() {
return usableSpace != -1 && usableSpace <= minUsableBytes;
}
private void tryUpdate() {
long now = System.currentTimeMillis();
//1秒更新一次
if (now - lastUpdateTime <= 1000) {
return;
}
usableSpace = path.getUsableSpace();
lastUpdateTime = now;
}
@Override
protected void applyEventData(Map<String, Object> data) {
data.put("usableSpace", DataSize.ofBytes(usableSpace).toMegabytes());
data.put("minUsableBytes", DataSize.ofBytes(minUsableBytes).toMegabytes());
}
@Override
public String toString() {
return "DiskFree(path=" + path
+ ",space=" + FormatUtils.formatDataSize(usableSpace) + "/" + FormatUtils.formatDataSize(minUsableBytes) + ")";
}
}

View File

@ -1,74 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
import java.io.File;
import java.util.Map;
class DiskUsageEviction extends AbstractBufferEviction {
private final File path;
private final float threshold;
public DiskUsageEviction(File path, float threshold) {
this.path = path;
this.threshold = threshold;
}
private volatile float usage;
private volatile long lastUpdateTime;
@Override
public boolean doEviction(EvictionContext context) {
tryUpdate();
if (freeOutOfThreshold()) {
context.removeOldest(EvictionContext.BufferType.buffer);
return true;
}
return false;
}
protected boolean freeOutOfThreshold() {
return usage >= threshold;
}
private void tryUpdate() {
long now = System.currentTimeMillis();
//1秒更新一次
if (now - lastUpdateTime <= 1000) {
return;
}
long total = path.getTotalSpace();
long usable = path.getUsableSpace();
usage = (float) ((total - usable) / (double) total);
lastUpdateTime = now;
}
@Override
protected void applyEventData(Map<String, Object> data) {
data.put("usage", String.format("%.2f%%", usage * 100));
}
@Override
public String toString() {
return "DiskUsage(path=" + path
+ ", threshold=" + String.format("%.2f%%", threshold * 100)
+ ", usage=" + String.format("%.2f%%", usage * 100) + ")";
}
}

View File

@ -1,59 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
/**
* 缓冲淘汰上下文
*
* @author zhouhao
* @since 2.0
*/
public interface EvictionContext {
/**
* 获取指定类型的数据量
*
* @param type 类型
* @return 数据量
*/
long size(BufferType type);
/**
* 删除最新的数据
*
* @param type 类型
*/
void removeLatest(BufferType type);
/**
* 删除最旧的数据
*
* @param type 类型
*/
void removeOldest(BufferType type);
/**
* @return 缓冲区名称, 用于区分多个不同的缓冲区
*/
String getName();
enum BufferType {
//缓冲区
buffer,
//死数据
dead
}
}

View File

@ -1,22 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
public interface MemoryUsage {
int usage();
}

View File

@ -1,52 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.buffer;
import lombok.AllArgsConstructor;
import java.util.Map;
@AllArgsConstructor
class SizeLimitEviction extends AbstractBufferEviction {
private final long bufferLimit;
private final long deadLimit;
@Override
public boolean doEviction(EvictionContext context) {
boolean anyEviction = false;
if (bufferLimit > 0 && context.size(EvictionContext.BufferType.buffer) >= bufferLimit) {
context.removeOldest(EvictionContext.BufferType.buffer);
anyEviction = true;
}
if (deadLimit > 0 && context.size(EvictionContext.BufferType.dead) >= deadLimit) {
context.removeOldest(EvictionContext.BufferType.dead);
anyEviction = true;
}
return anyEviction;
}
@Override
protected void applyEventData(Map<String, Object> data) {
data.put("bufferLimit", bufferLimit);
data.put("deadLimit", deadLimit);
}
@Override
public String toString() {
return "SizeLimit(buffer=" + bufferLimit + ", dead=" + deadLimit + ")";
}
}

View File

@ -1,30 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.codec;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.OutputStream;
public interface ObjectSerializer {
ObjectInput createInput(InputStream stream);
ObjectOutput createOutput(OutputStream stream);
}

View File

@ -1,159 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.codec;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.Unpooled;
import io.netty.util.concurrent.FastThreadLocal;
import lombok.SneakyThrows;
import org.jetlinks.core.utils.SerializeUtils;
import org.nustaq.serialization.FSTConfiguration;
import java.io.*;
import java.util.Base64;
import java.util.function.Supplier;
public class Serializers {
private static final ObjectSerializer JDK = new ObjectSerializer() {
@Override
@SneakyThrows
public ObjectInput createInput(InputStream stream) {
return new ObjectInputStream(stream);
}
@Override
@SneakyThrows
public ObjectOutput createOutput(OutputStream stream) {
return new ObjectOutputStream(stream);
}
};
private static final ObjectSerializer FST = new ObjectSerializer() {
final FastThreadLocal<FSTConfiguration> conf =
new FastThreadLocal<FSTConfiguration>() {
@Override
protected FSTConfiguration initialValue() {
FSTConfiguration configuration = FSTConfiguration.createDefaultConfiguration();
configuration.setForceSerializable(true);
configuration.setClassLoader(FST.getClass().getClassLoader());
return configuration;
}
};
@Override
@SneakyThrows
public ObjectInput createInput(InputStream stream) {
return conf.get().getObjectInput(stream);
}
@Override
@SneakyThrows
public ObjectOutput createOutput(OutputStream stream) {
return conf.get().getObjectOutput(stream);
}
};
private static final ObjectSerializer DEFAULT;
static {
DEFAULT = System.getProperty("jetlinks.object.serializer.type", "fst").equals("fst") ? FST : JDK;
}
public static ObjectSerializer jdk() {
return JDK;
}
public static ObjectSerializer fst() {
return FST;
}
public static ObjectSerializer getDefault() {
return DEFAULT;
}
public static String serializeToBase64(Object source) {
return Base64.getEncoder().encodeToString(serialize(source));
}
public static Object deserializeFromBase64(String base64) {
return deserialize(Base64.getDecoder().decode(base64));
}
@SuppressWarnings("all")
private static final FastThreadLocal<ByteArrayOutputStream>
SHARED_STREAM = new FastThreadLocal<ByteArrayOutputStream>() {
@Override
protected ByteArrayOutputStream initialValue() {
return new ByteArrayOutputStream();
}
};
@SneakyThrows
public static ByteBuf serializeExternal(Externalizable source) {
ByteArrayOutputStream outputStream = SHARED_STREAM.get();
if (outputStream.size() != 0) {
outputStream = new ByteArrayOutputStream();
}
try (ObjectOutput output = getDefault().createOutput(outputStream)) {
source.writeExternal(output);
output.flush();
return Unpooled.wrappedBuffer(outputStream.toByteArray());
} finally {
outputStream.reset();
}
}
@SneakyThrows
public static <T extends Externalizable> T deserializeExternal(ByteBuf buffer, Supplier<T> instance) {
try (ObjectInput input = getDefault().createInput(new ByteBufInputStream(buffer, true))) {
T data = instance.get();
data.readExternal(input);
return data;
}
}
@SneakyThrows
public static byte[] serialize(Object source) {
ByteArrayOutputStream outputStream = SHARED_STREAM.get();
if (outputStream.size() != 0) {
outputStream = new ByteArrayOutputStream();
}
try (ObjectOutput output = getDefault().createOutput(outputStream)) {
SerializeUtils.writeObject(source, output);
output.flush();
return outputStream.toByteArray();
} finally {
outputStream.reset();
}
}
@SneakyThrows
public static Object deserialize(byte[] data) {
ByteArrayInputStream stream = new ByteArrayInputStream(data);
try (ObjectInput input = getDefault().createInput(stream)) {
return SerializeUtils.readObject(input);
}
}
}

View File

@ -1,117 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.core.command.CommandSupport;
import org.jetlinks.core.utils.SerializeUtils;
import org.jetlinks.community.spi.Provider;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Map;
/**
* 命令支持提供者,用于针对多个基于命令模式的可选模块依赖时的解耦.
* <p>
*
* @author zhouhao
* @see org.jetlinks.sdk.server.SdkServices
* @see InternalSdkServices
* @since 2.1
*/
public interface CommandSupportManagerProvider {
/**
* 所有支持的提供商
*/
Provider<CommandSupportManagerProvider> supports = Provider.create(CommandSupportManagerProvider.class);
/**
* 命令服务提供商标识
*
* @return 唯一标识
*/
String getProvider();
/**
* 获取命令支持,不同的命令管理支持多种命令支持,可能通过id进行区分,具体规则由对应服务实
*
* @param id 命令ID标识
* @param options 拓展配置
* @return CommandSupport
* @see CommandSupportManagerProviders#getCommandSupport(String, Map)
*/
Mono<? extends CommandSupport> getCommandSupport(String id, Map<String, Object> options);
/**
* 获取所有支持的信息
*
* @return id
*/
default Flux<CommandSupportInfo> getSupportInfo() {
return Flux.empty();
}
@Getter
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
@Setter
class CommandSupportInfo implements Externalizable {
private String id;
private String name;
private String description;
public CommandSupportInfo copy() {
return FastBeanCopier.copy(this, new CommandSupportInfo());
}
/**
* @param serviceId serviceId
* @return this
* @see CommandSupportManagerProviders#getCommandSupport(String)
*/
public CommandSupportInfo appendService(String serviceId) {
if (this.id == null) {
this.id = serviceId;
} else {
this.id = serviceId + ":" + id;
}
return this;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
SerializeUtils.writeNullableUTF(id, out);
SerializeUtils.writeNullableUTF(name, out);
SerializeUtils.writeNullableUTF(description, out);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
id = SerializeUtils.readNullableUTF(in);
name = SerializeUtils.readNullableUTF(in);
description = SerializeUtils.readNullableUTF(in);
}
}
}

View File

@ -1,136 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command;
import org.jetlinks.core.command.CommandSupport;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
/**
* 命令支持管理提供商工具类,用于提供对{@link CommandSupportManagerProvider}相关通用操作.
*
* @author zhouhao
* @see CommandSupportManagerProvider
* @see CommandSupportManagerProviders#getCommandSupport(String, Map)
* @since 2.2
*/
public class CommandSupportManagerProviders {
/**
* 根据服务ID获取CommandSupport.
* <pre>{@code
*
* CommandSupportManagerProviders
* .getCommandSupport("deviceService:device",Collections.emptyMap())
*
* }</pre>
*
* @param serviceId serviceId 服务名
* @return CommandSupport
* @see InternalSdkServices
* @see org.jetlinks.sdk.server.SdkServices
*/
public static Mono<CommandSupport> getCommandSupport(String serviceId) {
return getCommandSupport(serviceId, Collections.emptyMap());
}
/**
* 根据服务ID和支持ID获取CommandSupport.
*
* @param serviceId 服务ID
* @param supportId 支持ID
* @return CommandSupport
*/
public static Mono<CommandSupport> getCommandSupport(String serviceId, String supportId) {
return getProviderNow(serviceId)
.getCommandSupport(supportId, Collections.emptyMap())
.cast(CommandSupport.class);
}
/**
* 根据服务ID获取CommandSupport.
* <pre>{@code
*
* CommandSupportManagerProviders
* .getCommandSupport("deviceService:device",Collections.emptyMap())
*
* }</pre>
*
* @param serviceId serviceId 服务名
* @param options options
* @return CommandSupport
* @see InternalSdkServices
* @see org.jetlinks.sdk.server.SdkServices
*/
public static Mono<CommandSupport> getCommandSupport(String serviceId,
Map<String, Object> options) {
//fast path
CommandSupportManagerProvider provider = CommandSupportManagerProvider
.supports
.get(serviceId)
.orElse(null);
if (provider != null) {
return provider
.getCommandSupport(serviceId, options)
.cast(CommandSupport.class);
}
String supportId = serviceId;
// deviceService:product
if (serviceId.contains(":")) {
String[] arr = serviceId.split(":", 2);
serviceId = arr[0];
supportId = arr[1];
}
String finalServiceId = serviceId;
String finalSupportId = supportId;
return Mono.defer(() -> getProviderNow(finalServiceId).getCommandSupport(finalSupportId, options));
}
/**
* 注册命令支持
*
* @param provider {@link CommandSupportManagerProvider#getProvider()}
*/
public static void register(CommandSupportManagerProvider provider) {
CommandSupportManagerProvider.supports.register(provider.getProvider(), provider);
}
/**
* 获取命令支持
*
* @param provider {@link CommandSupportManagerProvider#getProvider()}
* @return Optional
*/
public static Optional<CommandSupportManagerProvider> getProvider(String provider) {
return CommandSupportManagerProvider.supports.get(provider);
}
/**
* 获取命令支持,如果不存在则抛出异常{@link UnsupportedOperationException}
*
* @param provider provider {@link CommandSupportManagerProvider#getProvider()}
* @return CommandSupportManagerProvider
*/
public static CommandSupportManagerProvider getProviderNow(String provider) {
return CommandSupportManagerProvider.supports.getNow(provider);
}
}

View File

@ -1,52 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command;
import lombok.AllArgsConstructor;
import org.jetlinks.core.command.CommandSupport;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
@AllArgsConstructor
public class CompositeCommandSupportManagerProvider implements CommandSupportManagerProvider {
private final List<CommandSupportManagerProvider> providers;
@Override
public String getProvider() {
return providers.get(0).getProvider();
}
@Override
public Mono<? extends CommandSupport> getCommandSupport(String id, Map<String, Object> options) {
return Flux
.fromIterable(providers)
.flatMap(provider -> provider.getCommandSupport(id, options))
.take(1)
.singleOrEmpty();
}
@Override
public Flux<CommandSupportInfo> getSupportInfo() {
return Flux
.fromIterable(providers)
.flatMap(CommandSupportManagerProvider::getSupportInfo)
.distinct(CommandSupportInfo::getId);
}
}

View File

@ -1,322 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command;
import lombok.SneakyThrows;
import org.hswebframework.web.api.crud.entity.EntityFactoryHolder;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.Permission;
import org.hswebframework.web.authorization.exception.AccessDenyException;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.crud.service.ReactiveCrudService;
import org.jetlinks.core.command.AbstractCommandSupport;
import org.jetlinks.core.command.Command;
import org.jetlinks.core.metadata.FunctionMetadata;
import org.jetlinks.core.metadata.SimplePropertyMetadata;
import org.jetlinks.core.metadata.types.ArrayType;
import org.jetlinks.core.metadata.types.IntType;
import org.jetlinks.core.metadata.types.ObjectType;
import org.jetlinks.core.metadata.types.StringType;
import org.jetlinks.core.utils.Reactors;
import org.jetlinks.sdk.server.commons.cmd.*;
import org.jetlinks.supports.official.DeviceMetadataParser;
import org.springframework.core.ResolvableType;
import reactor.bool.BooleanUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 通用增删改查命令支持,基于{@link ReactiveCrudService}来实现增删改查相关命令
*
* @param <T> 实体类型
* @author zhouhao
* @see QueryByIdCommand
* @see QueryPagerCommand
* @see QueryListCommand
* @see CountCommand
* @see SaveCommand
* @see AddCommand
* @see UpdateCommand
* @see DeleteCommand
* @see DeleteByIdCommand
* @since 2.2
*/
public class CrudCommandSupport<T> extends AbstractCommandSupport {
final ReactiveCrudService<T, String> service;
final ResolvableType _entityType;
public CrudCommandSupport(ReactiveCrudService<T, String> service) {
this(service, ResolvableType
.forClass(ReactiveCrudService.class, service.getClass())
.getGeneric(0));
}
public CrudCommandSupport(ReactiveCrudService<T, String> service, ResolvableType _entityType) {
this.service = service;
this._entityType = _entityType;
registerQueries();
registerSaves();
registerDelete();
}
@Override
public Flux<FunctionMetadata> getCommandMetadata() {
return super
.getCommandMetadata()
.filterWhen(func -> commandIsSupported(func.getId()));
}
@Override
public Mono<Boolean> commandIsSupported(String commandId) {
return BooleanUtils.and(
super.commandIsSupported(commandId),
hasPermission(getPermissionId(), getAction(commandId))
);
}
@Override
public Mono<FunctionMetadata> getCommandMetadata(String commandId) {
return super
.getCommandMetadata(commandId)
.filterWhen(func -> commandIsSupported(func.getId()));
}
@Override
public Mono<FunctionMetadata> getCommandMetadata(Command<?> command) {
return super
.getCommandMetadata(command)
.filterWhen(func -> commandIsSupported(func.getId()));
}
@Override
public Mono<FunctionMetadata> getCommandMetadata(@Nonnull String commandId,
@Nullable Map<String, Object> parameters) {
return super
.getCommandMetadata(commandId, parameters)
.filterWhen(func -> commandIsSupported(func.getId()));
}
@SneakyThrows
@SuppressWarnings("all")
private T newInstance0() {
return (T) _entityType.toClass().getConstructor().newInstance();
}
protected T newInstance() {
@SuppressWarnings("all")
Class<T> clazz = (Class<T>) _entityType.toClass();
return EntityFactoryHolder
.newInstance(clazz,
this::newInstance0);
}
protected ResolvableType getResolvableType() {
return _entityType;
}
protected ObjectType createEntityType() {
return (ObjectType) DeviceMetadataParser.withType(_entityType);
}
protected String getPermissionId() {
return null;
}
protected Mono<Void> assetPermission(String action) {
return assetPermission(getPermissionId(), action);
}
protected Mono<Boolean> hasPermission(String permissionId, String action) {
if (permissionId == null) {
return Reactors.ALWAYS_TRUE;
}
return Authentication
.currentReactive()
.map(auth -> auth.hasPermission(permissionId, action))
.defaultIfEmpty(true);
}
protected Mono<Void> assetPermission(String permissionId, String action) {
if (permissionId == null) {
return Mono.empty();
}
return Authentication
.currentReactive()
.flatMap(
auth -> auth.hasPermission(permissionId, action)
? Mono.empty()
: Mono.error(new AccessDenyException.NoStackTrace(permissionId, Collections.singleton(action))));
}
protected String getAction(String commandId) {
if (commandId.startsWith("Delete")) {
return Permission.ACTION_DELETE;
}
if (commandId.startsWith("Update") ||
commandId.startsWith("Save") ||
commandId.startsWith("Add") ||
commandId.startsWith("Disable") ||
commandId.startsWith("Enable")) {
return Permission.ACTION_SAVE;
}
return Permission.ACTION_QUERY;
}
protected void registerQueries() {
//根据id查询
registerHandler(
QueryByIdCommand
.<T>createHandler(
metadata -> {
metadata
.setInputs(Collections.singletonList(
SimplePropertyMetadata
.of("id", "id", new ArrayType()
.elementType(StringType.GLOBAL))
));
metadata.setOutput(createEntityType());
},
cmd -> assetPermission(Permission.ACTION_QUERY)
.then(service.findById(cmd.getId())),
_entityType)
);
//分页查询
registerHandler(
QueryPagerCommand
.<T>createHandler(
metadata -> metadata.setOutput(
QueryPagerCommand
.createOutputType(createEntityType().getProperties())),
cmd -> assetPermission(Permission.ACTION_QUERY)
.then(service.queryPager(cmd.asQueryParam())),
_entityType)
);
//查询列表
registerHandler(
QueryListCommand
.<T>createHandler(
metadata -> metadata.setOutput(createEntityType()),
cmd -> assetPermission(Permission.ACTION_QUERY)
.thenMany(service.query(cmd.asQueryParam())),
_entityType)
);
//查询数量
registerHandler(
CountCommand
.createHandler(
metadata -> metadata.setOutput(new ObjectType()
.addProperty("total", "总数", IntType.GLOBAL)),
cmd -> assetPermission(Permission.ACTION_QUERY)
.then(service.count(cmd.asQueryParam())))
);
//todo 聚合查询?
}
protected void registerSaves() {
//批量保存
registerHandler(
SaveCommand.createHandler(
metadata -> {
metadata
.setInputs(Collections.singletonList(
SimplePropertyMetadata.of("data", "数据列表", new ArrayType().elementType(createEntityType()))
));
metadata.setOutput(createEntityType());
},
cmd -> {
List<T> list = cmd.dataList((data) -> FastBeanCopier.copy(data, newInstance()));
return assetPermission(Permission.ACTION_SAVE)
.then(service.save(list))
.thenMany(Flux.fromIterable(list));
},
_entityType)
);
//新增
registerHandler(
AddCommand
.<T>createHandler(
metadata -> {
metadata
.setInputs(Collections.singletonList(
SimplePropertyMetadata.of("data", "数据列表", new ArrayType().elementType(createEntityType()))
));
metadata.setOutput(createEntityType());
},
cmd -> Flux
.fromIterable(cmd.dataList((data) -> FastBeanCopier.copy(data, newInstance())))
.as(flux -> assetPermission(Permission.ACTION_SAVE)
.then(service.insert(flux))
.thenMany(flux)))
);
//修改
registerHandler(
UpdateCommand
.<T>createHandler(
metadata -> {
metadata.setInputs(
Arrays.asList(
SimplePropertyMetadata.of("data", "数据", createEntityType()),
QueryCommand.getTermsMetadata()
));
metadata.setOutput(IntType.GLOBAL);
},
cmd -> this
.assetPermission(Permission.ACTION_SAVE)
.then(cmd
.applyUpdate(service.createUpdate(), map -> FastBeanCopier.copy(map, newInstance()))
.execute()))
);
}
protected void registerDelete() {
//删除
registerHandler(
DeleteCommand.createHandler(
metadata -> {
metadata.setInputs(Collections.singletonList(
SimplePropertyMetadata.of("terms", "删除条件", QueryCommand.getTermsDataType())));
metadata.setOutput(IntType.GLOBAL);
},
cmd -> this
.assetPermission(Permission.ACTION_DELETE)
.then(cmd.applyDelete(service.createDelete()).execute()))
);
//根据id移除
registerHandler(
DeleteByIdCommand
.<Mono<Void>>createHandler(
metadata -> {
},
cmd -> this
.assetPermission(Permission.ACTION_DELETE)
.then(service.deleteById(cmd.getId()).then()))
);
}
}

View File

@ -1,58 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command;
/**
* 平台内部的一些服务定义
*
* @author zhouhao
* @see org.jetlinks.sdk.server.SdkServices
* @since 2.2
*/
public interface InternalSdkServices {
/**
* 网络组件服务
*/
String networkService = "networkService";
/**
* 设备接入网关服务
*/
String deviceGatewayService = "deviceGatewayService";
/**
* 采集器服务
*/
String collectorService = "collectorService";
/**
* 规则服务
*/
String ruleService = "ruleService";
/**
* 插件服务
*/
String pluginService = "pluginService";
/**
* 基础服务
*/
String commonService = "commonService";
}

View File

@ -1,90 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.hswebframework.web.i18n.LocaleUtils;
import org.jetlinks.core.command.AbstractCommandSupport;
import org.jetlinks.core.command.CommandSupport;
import org.jetlinks.community.annotation.command.CommandService;
import org.springframework.core.annotation.AnnotationUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
/**
* 通用静态命令管理.
*
* @author zhangji 2024/2/2
* @since 2.2.0
*/
@AllArgsConstructor
public class StaticCommandSupportManagerProvider extends AbstractCommandSupport implements CommandSupportManagerProvider {
@Getter
public String provider;
private final Map<String, CommandSupport> commandSupports = new HashMap<>();
public void register(String id, CommandSupport commandSupport) {
commandSupports.put(id, commandSupport);
}
@Override
public final Mono<? extends CommandSupport> getCommandSupport(String id, Map<String, Object> options) {
CommandSupport cmd = commandSupports.get(id);
if (cmd == null) {
return getUndefined(id, options);
}
return Mono.just(cmd);
}
protected Mono<? extends CommandSupport> getUndefined(String id, Map<String, Object> options) {
return Mono.just(this);
}
@Override
public Flux<CommandSupportInfo> getSupportInfo() {
Flux<CommandSupportInfo> another = Flux
.fromIterable(commandSupports.entrySet())
.map(entry -> createCommandSupport(entry.getKey(), entry.getValue().getClass()));
if (!this.handlers.isEmpty()) {
return Flux.concat(another, Flux.just(createCommandSupport(null, this.getClass())));
}
return another;
}
protected final CommandSupportInfo createCommandSupport(String id, Class<?> clazz) {
String name = id;
String description = null;
Schema schema = AnnotationUtils.findAnnotation(clazz, Schema.class);
if (null != schema) {
name = schema.title();
description = schema.description();
}
CommandService service = AnnotationUtils.findAnnotation(clazz, CommandService.class);
if (null != service) {
name = LocaleUtils.resolveMessage(service.name(), service.name());
description = String.join("", service.description());
}
return CommandSupportInfo.of(id, name, description);
}
}

View File

@ -1,25 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.crud;
import org.hswebframework.web.crud.service.ReactiveCrudService;
public interface CrudCommandHandler<T, PK>
extends QueryCommandHandler<T, PK>, SaveCommandHandler<T, PK>, DeleteCommandHandler<T, PK> {
@Override
ReactiveCrudService<T, PK> getService();
}

View File

@ -1,48 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.crud;
import com.google.common.collect.Collections2;
import org.hswebframework.web.authorization.annotation.SaveAction;
import org.hswebframework.web.crud.service.ReactiveCrudService;
import org.jetlinks.core.annotation.command.CommandHandler;
import org.jetlinks.sdk.server.commons.cmd.DeleteByIdCommand;
import org.jetlinks.sdk.server.commons.cmd.DeleteCommand;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface DeleteCommandHandler<T, PK> {
ReactiveCrudService<T, PK> getService();
@CommandHandler
@SaveAction
default Mono<Integer> deleteById(DeleteByIdCommand<Integer> command) {
return getService()
.deleteById(Flux.fromIterable(
Collections2
.transform(command.getIdList(),
v -> (PK) v)));
}
@CommandHandler
@SaveAction
default Mono<Integer> delete(DeleteCommand command) {
return command
.applyDelete(getService().createDelete())
.execute();
}
}

View File

@ -1,61 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.crud;
import com.google.common.collect.Collections2;
import org.hswebframework.web.api.crud.entity.PagerResult;
import org.hswebframework.web.authorization.annotation.QueryAction;
import org.hswebframework.web.crud.service.ReactiveCrudService;
import org.jetlinks.core.annotation.command.CommandHandler;
import org.jetlinks.sdk.server.commons.cmd.CountCommand;
import org.jetlinks.sdk.server.commons.cmd.QueryByIdCommand;
import org.jetlinks.sdk.server.commons.cmd.QueryListCommand;
import org.jetlinks.sdk.server.commons.cmd.QueryPagerCommand;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface QueryCommandHandler<T, PK> {
ReactiveCrudService<T, PK> getService();
@CommandHandler
@QueryAction
default Flux<T> queryById(QueryByIdCommand<T> command) {
return getService()
.findById(
Collections2.transform(command.getIdList(),
v -> (PK) v)
);
}
@CommandHandler
@QueryAction
default Flux<T> queryList(QueryListCommand<T> command) {
return getService().query(command.asQueryParam());
}
@CommandHandler
@QueryAction
default Mono<PagerResult<T>> queryPager(QueryPagerCommand<T> command) {
return getService().queryPager(command.asQueryParam());
}
@CommandHandler
@QueryAction
default Mono<Integer> count(CountCommand command) {
return getService().count(command.asQueryParam());
}
}

View File

@ -1,64 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.crud;
import org.hswebframework.web.authorization.annotation.SaveAction;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.crud.service.ReactiveCrudService;
import org.jetlinks.core.annotation.command.CommandHandler;
import org.jetlinks.sdk.server.commons.cmd.AddCommand;
import org.jetlinks.sdk.server.commons.cmd.SaveCommand;
import org.jetlinks.sdk.server.commons.cmd.UpdateCommand;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
public interface SaveCommandHandler<T, PK> {
ReactiveCrudService<T, PK> getService();
default T convertData(Object data) {
return FastBeanCopier.copy(data, getService().getRepository().newInstanceNow());
}
@CommandHandler
@SaveAction
default Flux<T> save(SaveCommand<T> command) {
List<T> data = command.dataList(this::convertData);
return getService()
.save(data)
.thenMany(Flux.fromIterable(data));
}
@CommandHandler
@SaveAction
default Flux<T> add(AddCommand<T> command) {
List<T> data = command.dataList(this::convertData);
return getService()
.save(data)
.thenMany(Flux.fromIterable(data));
}
@CommandHandler
@SaveAction
default Mono<Integer> update(UpdateCommand<T> command) {
return command
.applyUpdate(getService().createUpdate(),
this::convertData)
.execute();
}
}

View File

@ -1,98 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.register;
import lombok.extern.slf4j.Slf4j;
import org.jetlinks.community.command.CommandSupportManagerProvider;
import org.jetlinks.community.command.CommandSupportManagerProviders;
import org.jetlinks.community.command.CompositeCommandSupportManagerProvider;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.ClassUtils;
import org.jetlinks.community.annotation.command.CommandService;
import javax.annotation.Nonnull;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
public class CommandServiceEndpointRegister implements ApplicationContextAware, SmartInitializingSingleton {
private ApplicationContext context;
@Override
public void setApplicationContext(@Nonnull ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
@Override
public void afterSingletonsInstantiated() {
Map<String, Object> beans = context.getBeansWithAnnotation(CommandService.class);
//静态Provider
Map<String, List<CommandSupportManagerProvider>> statics = context
.getBeanProvider(CommandSupportManagerProvider.class)
.stream()
.collect(Collectors.groupingBy(CommandSupportManagerProvider::getProvider));
Map<String, SpringBeanCommandSupportProvider> providers = new HashMap<>();
for (Object value : beans.values()) {
CommandService endpoint =
AnnotatedElementUtils.findMergedAnnotation(ClassUtils.getUserClass(value), CommandService.class);
if (endpoint == null || !endpoint.autoRegistered()) {
continue;
}
String id = endpoint.id();
String support = id;
if (id.contains(":")) {
support = id.substring(id.indexOf(":") + 1);
id = id.substring(0, id.indexOf(":"));
}
SpringBeanCommandSupportProvider provider = providers
.computeIfAbsent(id, SpringBeanCommandSupportProvider::new);
log.debug("register command support:{} -> {}", endpoint.id(), value);
provider.register(support, endpoint, value);
}
for (SpringBeanCommandSupportProvider value : providers.values()) {
if (value.isEmpty()) {
continue;
}
//合并静态Provider
List<CommandSupportManagerProvider> provider = statics.remove(value.getProvider());
if (provider != null) {
provider.forEach(value::register);
}
CommandSupportManagerProviders.register(value);
}
for (List<CommandSupportManagerProvider> value : statics.values()) {
if (value.size() == 1) {
CommandSupportManagerProviders.register(value.get(0));
} else {
CommandSupportManagerProviders.register(new CompositeCommandSupportManagerProvider(value));
}
}
}
}

View File

@ -1,153 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.register;
import com.google.common.collect.Lists;
import lombok.Getter;
import org.hswebframework.web.i18n.LocaleUtils;
import org.jetlinks.core.command.CommandSupport;
import org.jetlinks.core.command.CompositeCommandSupport;
import org.jetlinks.community.annotation.command.CommandService;
import org.jetlinks.community.command.CommandSupportManagerProvider;
import org.jetlinks.supports.command.JavaBeanCommandSupport;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.*;
class SpringBeanCommandSupportProvider implements CommandSupportManagerProvider {
private final String provider;
private final Map<String, CompositeSpringBeanCommandSupport> commandSupports = new HashMap<>();
private final List<CommandSupportManagerProvider> statics = new ArrayList<>();
public SpringBeanCommandSupportProvider(String provider) {
this.provider = provider;
}
void register(CommandSupportManagerProvider provider) {
statics.add(provider);
}
void register(String support, CommandService annotation, Object bean) {
Objects.requireNonNull(annotation, "endpoint");
Objects.requireNonNull(bean, "bean");
SpringBeanCommandSupport commandSupport = new SpringBeanCommandSupport(annotation, bean);
if (commandSupport.isEmpty()) {
return;
}
//相同support合并成一个
commandSupports
.computeIfAbsent(support, id -> new CompositeSpringBeanCommandSupport(id,provider))
.register(commandSupport);
}
boolean isEmpty() {
return commandSupports.isEmpty();
}
@Override
public String getProvider() {
return provider;
}
@Override
public Mono<? extends CommandSupport> getCommandSupport(String id, Map<String, Object> options) {
CommandSupport support = commandSupports.get(StringUtils.hasText(id) ? id : provider);
if (support != null) {
return Mono.just(support);
}
if (statics.isEmpty()) {
return Mono.empty();
}
return Flux
.fromIterable(statics)
.flatMap(provider -> provider.getCommandSupport(id, options))
.take(1)
.singleOrEmpty();
}
@Override
public Flux<CommandSupportInfo> getSupportInfo() {
if (statics.isEmpty()) {
return Flux
.fromIterable(commandSupports.values())
.flatMapIterable(CompositeSpringBeanCommandSupport::getInfo);
}
return Flux
.concat(
Flux
.fromIterable(commandSupports.values())
.flatMapIterable(CompositeSpringBeanCommandSupport::getInfo),
Flux.fromIterable(statics)
.flatMap(CommandSupportManagerProvider::getSupportInfo)
)
.distinct(info -> {
String id = info.getId();
return String.valueOf(id);
});
}
static class CompositeSpringBeanCommandSupport extends CompositeCommandSupport {
private final String id;
private final String provider;
public CompositeSpringBeanCommandSupport(String id,String provider) {
super();
this.id = id;
this.provider = provider;
}
public List<CommandSupportInfo> getInfo() {
return Lists
.transform(
getSupports(),
support -> {
SpringBeanCommandSupport commandSupport = support.unwrap(SpringBeanCommandSupport.class);
//兼容为null
String _id = id.equals(provider) ? null : id;
return CommandSupportInfo.of(
_id,
LocaleUtils.resolveMessage(commandSupport.annotation.name(), commandSupport.annotation.name()),
String.join("", commandSupport.annotation.description())
);
});
}
@Override
public String toString() {
return getSupports().toString();
}
}
@Getter
static class SpringBeanCommandSupport extends JavaBeanCommandSupport {
private final CommandService annotation;
boolean isEmpty() {
return handlers.isEmpty();
}
public SpringBeanCommandSupport(CommandService annotation, Object target) {
super(target);
this.annotation = annotation;
}
}
}

View File

@ -1,50 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.rule;
import io.swagger.v3.oas.annotations.media.Schema;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.core.command.AbstractCommand;
import org.jetlinks.core.command.CommandMetadataResolver;
import org.jetlinks.core.command.CommandUtils;
import org.jetlinks.core.metadata.FunctionMetadata;
import org.jetlinks.core.metadata.SimpleFunctionMetadata;
import org.jetlinks.community.command.rule.data.RelieveInfo;
import org.jetlinks.community.command.rule.data.RelieveResult;
import org.springframework.core.ResolvableType;
import reactor.core.publisher.Mono;
@Schema(title = "解除告警命令")
public class RelievedAlarmCommand extends AbstractCommand<Mono<RelieveResult>,RelievedAlarmCommand> {
@Schema(description = "解除告警传参信息")
public RelieveInfo getRelieveInfo() {
return FastBeanCopier.copy(readable(), new RelieveInfo());
}
public RelievedAlarmCommand setRelieveInfo(RelieveInfo relieveInfo) {
return with(FastBeanCopier.copy(relieveInfo, writable()));
}
public static FunctionMetadata metadata() {
SimpleFunctionMetadata metadata = new SimpleFunctionMetadata();
metadata.setId(CommandUtils.getCommandIdByType(RelievedAlarmCommand.class));
metadata.setName("解除告警命令");
metadata.setInputs(CommandMetadataResolver.resolveInputs(ResolvableType.forClass(RelieveInfo.class)));
metadata.setOutput(CommandMetadataResolver.resolveOutput(ResolvableType.forClass(RelievedAlarmCommand.class)));
return metadata;
}
}

View File

@ -1,49 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.rule;
public interface RuleCommandServices {
/**
* 场景
*/
String sceneService = "sceneService";
/**
* 告警配置
*/
String alarmConfigService = "alarmConfigService";
/**
* 告警记录
*/
String alarmRecordService = "alarmRecordService";
/**
* 告警历史
*/
String alarmHistoryService = "alarmHistoryService";
/**
* 告警规则绑定
*/
String alarmRuleBindService = "alarmRuleBindService";
/**
* 告警相关
*/
String alarm = "alarm";
}

View File

@ -1,53 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.rule;
import io.swagger.v3.oas.annotations.media.Schema;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.core.command.AbstractCommand;
import org.jetlinks.core.command.CommandMetadataResolver;
import org.jetlinks.core.command.CommandUtils;
import org.jetlinks.core.metadata.FunctionMetadata;
import org.jetlinks.core.metadata.SimpleFunctionMetadata;
import org.jetlinks.community.command.rule.data.AlarmInfo;
import org.jetlinks.community.command.rule.data.AlarmResult;
import org.springframework.core.ResolvableType;
import reactor.core.publisher.Mono;
@Schema(title = "触发告警命令")
public class TriggerAlarmCommand extends AbstractCommand<Mono<AlarmResult>,TriggerAlarmCommand> {
private static final long serialVersionUID = 7056867872399432831L;
@Schema(description = "告警传参信息")
public AlarmInfo getAlarmInfo() {
return FastBeanCopier.copy(readable(), new AlarmInfo());
}
public TriggerAlarmCommand setAlarmInfo(AlarmInfo alarmInfo) {
return with(FastBeanCopier.copy(alarmInfo, writable()));
}
public static FunctionMetadata metadata() {
SimpleFunctionMetadata metadata = new SimpleFunctionMetadata();
metadata.setId(CommandUtils.getCommandIdByType(TriggerAlarmCommand.class));
metadata.setName("触发告警命令");
metadata.setInputs(CommandMetadataResolver.resolveInputs(ResolvableType.forClass(AlarmInfo.class)));
metadata.setOutput(CommandMetadataResolver.resolveOutput(ResolvableType.forClass(TriggerAlarmCommand.class)));
return metadata;
}
}

View File

@ -1,89 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetlinks.community.terms.TermSpec;
import java.io.Serializable;
import java.util.Map;
/**
* 触发告警参数
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class AlarmInfo implements Serializable {
private static final long serialVersionUID = -2316376361116648370L;
@Schema(description = "告警配置ID")
private String alarmConfigId;
@Schema(description = "告警名称")
private String alarmName;
@Schema(description = "告警说明")
private String description;
@Schema(description = "告警级别")
private int level;
@Schema(description = "告警目标类型")
private String targetType;
@Schema(description = "告警目标ID")
private String targetId;
@Schema(description = "告警目标名称")
private String targetName;
@Schema(description = "告警来源类型")
private String sourceType;
@Schema(description = "告警来源ID")
private String sourceId;
@Schema(description = "告警来源的创建人ID")
private String sourceCreatorId;
@Schema(description = "告警来源名称")
private String sourceName;
/**
* 标识告警触发的配置来自什么业务功能
*/
@Schema(description = "告警配置源")
private String alarmConfigSource;
@Schema(description = "告警数据")
private Map<String, Object> data;
/**
* 告警触发条件
*/
private TermSpec termSpec;
@Schema(description = "告警时间")
private Long alarmTime;
}

View File

@ -1,51 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
/**
* 告警结果
*/
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class AlarmResult implements Serializable {
private static final long serialVersionUID = -1752497262936740164L;
@Schema(description = "告警ID")
private String recordId;
@Schema(description = "是否重复告警")
private boolean alarming;
@Schema(description = "当前首次触发")
private boolean firstAlarm;
@Schema(description = "上一次告警时间")
private long lastAlarmTime;
@Schema(description = "首次告警或者解除告警后的再一次告警时间")
private long alarmTime;
}

View File

@ -1,47 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 解除告警参数
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RelieveInfo extends AlarmInfo{
@Schema(description = "解除原因")
private String relieveReason;
@Schema(description = "解除时间")
private Long relieveTime;
@Schema(description = "解除说明")
private String describe;
/**
* 告警解除类型人工user系统system
*/
@Schema(description = "告警解除类型")
private String alarmRelieveType;
}

View File

@ -1,47 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 解除警告结果
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RelieveResult extends AlarmResult{
@Schema(description = "告警级别")
private int level;
@Schema(description = "告警原因描述")
private String actualDesc;
@Schema(description = "解除原因")
private String relieveReason;
@Schema(description = "解除时间")
private long relieveTime;
@Schema(description = "解除说明")
private String describe;
}

View File

@ -1,71 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.config;
import org.jetlinks.community.ValueObject;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
/**
* 配置管理器,统一管理系统相关配置信息
*
* @author zhouhao
* @since 2.0
*/
public interface ConfigManager {
/**
* 获取全部已经定义的配置作用域
*
* @return 配置作用域
*/
Flux<ConfigScope> getScopes();
/**
* 获取根据作用域ID获取已经定义的配置作用域
*
* @return 配置作用域
*/
Mono<ConfigScope> getScope(String scope);
/**
* 获取指定作用域下的属性定义信息
*
* @param scope 配置作用域
* @return 属性定义信息
*/
Flux<ConfigPropertyDef> getPropertyDef(String scope);
/**
* 获取作用于下的全部配置
*
* @param scope 配置作用域
* @return 配置信息
*/
Mono<ValueObject> getProperties(String scope);
/**
* 设置作用域下的配置
*
* @param scope 作用域
* @param values 配置信息
* @return void
*/
Mono<Void> setProperties(String scope, Map<String, Object> values);
}

View File

@ -1,43 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.config;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import org.jetlinks.core.metadata.types.StringType;
@Getter
@Setter
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
@EqualsAndHashCode(of = "key")
public class ConfigPropertyDef {
@Schema(description = "配置key")
private String key;
@Schema(description = "配置名称")
private String name;
@Schema(description = "是否只读")
private boolean readonly;
@Schema(description = "配置类型")
private String type = StringType.ID;
@Schema(description = "默认值")
private String defaultValue;
}

View File

@ -1,38 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.config;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
@Getter
@Setter
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
@EqualsAndHashCode(of = "id")
public class ConfigScope {
@Schema(description = "ID")
private String id;
@Schema(description = "名称")
private String name;
@Schema(description = "是否公开访问(不需要登录)")
private boolean publicAccess;
}

View File

@ -1,33 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.config;
/**
* 实现此接口,自定义配置域以及配置定义
*
* @author zhouhao
* @since 2.0
*/
public interface ConfigScopeCustomizer {
/**
* 执行自定义,通过manager来添加自定义作用域
*
* @param manager manager
*/
void custom(ConfigScopeManager manager);
}

View File

@ -1,24 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.config;
import java.util.List;
public interface ConfigScopeManager {
void addScope(ConfigScope scope, List<ConfigPropertyDef> properties);
}

View File

@ -1,46 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.config;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.bean.FastBeanCopier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
@ConfigurationProperties(prefix = "system.config")
public class ConfigScopeProperties implements ConfigScopeCustomizer{
@Getter
@Setter
private List<Scope> scopes = new ArrayList<>();
@Override
public void custom(ConfigScopeManager manager) {
for (Scope scope : scopes) {
manager.addScope(FastBeanCopier.copy(scope,new ConfigScope()), scope.properties);
}
}
@Getter
@Setter
public static class Scope extends ConfigScope {
private List<ConfigPropertyDef> properties = new ArrayList<>();
}
}

View File

@ -1,116 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.config;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.web.cache.ReactiveCache;
import org.hswebframework.web.cache.ReactiveCacheManager;
import org.jetlinks.community.ValueObject;
import org.jetlinks.community.config.entity.ConfigEntity;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@AllArgsConstructor
public class SimpleConfigManager implements ConfigManager, ConfigScopeManager {
private final Map<ConfigScope, Set<ConfigPropertyDef>> scopes = new ConcurrentHashMap<>();
private final ReactiveRepository<ConfigEntity, String> repository;
private final ReactiveCache<Map<String, Object>> cache;
public SimpleConfigManager(ReactiveRepository<ConfigEntity, String> repository, ReactiveCacheManager cacheManager) {
this.repository = repository;
this.cache = cacheManager.getCache("system-config");
}
@Override
public void addScope(ConfigScope scope,
List<ConfigPropertyDef> properties) {
scopes.computeIfAbsent(scope, ignore -> new LinkedHashSet<>())
.addAll(properties);
}
@Override
public Flux<ConfigScope> getScopes() {
return Flux.fromIterable(scopes.keySet());
}
@Override
public Mono<ConfigScope> getScope(String scope) {
return this
.getScopes()
.filter(configScope -> Objects.equals(configScope.getId(), scope))
.take(1)
.singleOrEmpty();
}
@Override
public Flux<ConfigPropertyDef> getPropertyDef(String scope) {
return Flux.fromIterable(scopes.getOrDefault(
ConfigScope.of(scope, scope, false),
Collections.emptySet()));
}
@Override
public Mono<ValueObject> getProperties(String scope) {
return Mono
.zip(
//默认值
getPropertyDef(scope)
.filter(def -> null != def.getDefaultValue())
.collectMap(ConfigPropertyDef::getKey, ConfigPropertyDef::getDefaultValue),
//数据库配置的值
cache
.getMono(scope, () -> getPropertiesNow(scope)),
(defaults, values) -> {
Map<String, Object> properties = new HashMap<>(values);
defaults.forEach(properties::putIfAbsent);
return properties;
}
)
.map(ValueObject::of);
}
private Mono<Map<String, Object>> getPropertiesNow(String scope) {
return repository
.createQuery()
.where(ConfigEntity::getScope, scope)
.fetch()
.filter(val -> MapUtils.isNotEmpty(val.getProperties()))
.reduce(new LinkedHashMap<>(), (l, r) -> {
l.putAll(r.getProperties());
return l;
});
}
@Override
public Mono<Void> setProperties(String scope, Map<String, Object> values) {
ConfigEntity entity = new ConfigEntity();
entity.setProperties(values);
entity.setScope(scope);
entity.getId();
return repository
.save(entity)
.then(cache.evict(scope));
}
}

View File

@ -1,63 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.config.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
import org.hswebframework.web.api.crud.entity.GenericEntity;
import org.hswebframework.web.crud.annotation.EnableEntityEvent;
import org.hswebframework.web.utils.DigestUtils;
import org.springframework.util.StringUtils;
import javax.persistence.Column;
import javax.persistence.Index;
import javax.persistence.Table;
import java.sql.JDBCType;
import java.util.Map;
@Table(name = "s_config", indexes = {
@Index(name = "idx_conf_scope", columnList = "scope")
})
@Getter
@Setter
@EnableEntityEvent
public class ConfigEntity extends GenericEntity<String> {
@Column(length = 64, nullable = false, updatable = false)
@Schema(description = "作用域")
private String scope;
@Column(nullable = false)
@Schema
@JsonCodec
@ColumnType(jdbcType = JDBCType.LONGVARCHAR)
private Map<String,Object> properties;
@Override
public String getId() {
if (!StringUtils.hasText(super.getId())) {
setId(generateId(scope));
}
return super.getId();
}
public static String generateId(String scope) {
return DigestUtils.md5Hex(String.join("|", scope));
}
}

View File

@ -1,110 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.config.verification;
import io.swagger.v3.oas.annotations.Operation;
import org.hswebframework.web.crud.events.EntitySavedEvent;
import org.hswebframework.web.exception.BusinessException;
import org.jetlinks.community.config.entity.ConfigEntity;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeoutException;
/**
* @author bestfeng
*/
@RestController
public class ConfigVerificationService {
private final WebClient webClient;
private static final String PATH_VERIFICATION_URI = "/system/config/base-path/verification";
public ConfigVerificationService() {
this.webClient = WebClient
.builder()
.build();
}
@GetMapping(value = PATH_VERIFICATION_URI)
@Operation(description = "basePath配置验证接口")
public Mono<String> basePathValidate() {
return Mono.just("auth:"+PATH_VERIFICATION_URI);
}
@EventListener
public void handleConfigSavedEvent(EntitySavedEvent<ConfigEntity> event){
//base-path校验
event.async(
Flux.fromIterable(event.getEntity())
.filter(config -> Objects.equals(config.getScope(), "paths"))
.flatMap(config-> doBasePathValidate(config.getProperties().get("base-path")))
);
}
public Mono<Void> doBasePathValidate(Object basePath) {
if (basePath == null) {
return Mono.empty();
}
URI uri = URI.create(CastUtils.castString(CastUtils.castString(basePath).concat(PATH_VERIFICATION_URI)));
if (Objects.equals(uri.getHost(), "127.0.0.1")){
return Mono.error(new BusinessException("error.base_path_host_error", 500, "127.0.0.1"));
}
if (Objects.equals(uri.getHost(), "localhost")){
return Mono.error(new BusinessException("error.base_path_host_error", 500, "localhost"));
}
return webClient
.get()
.uri(uri)
.exchangeToMono(cr -> {
if (cr.statusCode().is2xxSuccessful()) {
return cr.bodyToMono(String.class)
.filter(r-> r.contains("auth:"+PATH_VERIFICATION_URI))
.switchIfEmpty(Mono.error(()-> new BusinessException("error.base_path_error")));
}
return Mono.defer(() -> Mono.error(new BusinessException("error.base_path_error")));
})
.timeout(Duration.ofSeconds(3), Mono.error(TimeoutException::new))
.onErrorResume(err -> {
while (err != null) {
if (err instanceof TimeoutException) {
return Mono.error(() -> new BusinessException("error.base_path_validate_request_timeout"));
} else if (err instanceof UnknownHostException) {
return Mono.error(() -> new BusinessException("error.base_path_DNS_resolution_failed"));
}
err = err.getCause();
}
return Mono.error(() -> new BusinessException("error.base_path_error"));
})
.then();
}
}

View File

@ -1,146 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.config.web;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.annotation.Authorize;
import org.hswebframework.web.authorization.annotation.QueryAction;
import org.hswebframework.web.authorization.annotation.Resource;
import org.hswebframework.web.authorization.annotation.SaveAction;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.community.ValueObject;
import org.jetlinks.community.config.ConfigManager;
import org.jetlinks.community.config.ConfigPropertyDef;
import org.jetlinks.community.config.ConfigScope;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/system/config")
@Resource(id = "system_config", name = "系统配置管理")
@AllArgsConstructor
@Tag(name = "系统配置管理")
public class SystemConfigManagerController {
private final ConfigManager configManager;
@GetMapping("/scopes")
@QueryAction
@Operation(summary = "获取配置作用域")
public Flux<ConfigScope> getConfigScopes() {
return configManager.getScopes();
}
@GetMapping("/{scope}")
@Authorize(ignore = true)
@Operation(summary = "获取作用域下的全部配置信息")
public Mono<Map<String, Object>> getConfigs(@PathVariable String scope) {
return Authentication
.currentReactive()
.hasElement()
.flatMap(hasAuth -> configManager
.getScope(scope)
//公共访问配置或者用户已登录
.map(conf -> conf.isPublicAccess() || hasAuth)
//没有定义配置,则用户登录即可访问
.defaultIfEmpty(hasAuth)
.filter(Boolean::booleanValue)
.flatMap(ignore -> configManager.getProperties(scope))
.map(ValueObject::values))
.defaultIfEmpty(Collections.emptyMap());
}
@GetMapping("/{scope}/_detail")
@QueryAction
@Operation(summary = "获取作用域下的配置信息")
public Flux<ConfigPropertyValue> getConfigDetail(@PathVariable String scope) {
return configManager
.getProperties(scope)
.flatMapMany(values -> configManager
.getPropertyDef(scope)
.map(def -> ConfigPropertyValue.of(def, values.get(def.getKey()).orElse(null))));
}
@PostMapping("/scopes")
@QueryAction
@Operation(summary = "获取作用域下的配置详情")
public Flux<Scope> getConfigDetail(@RequestBody Mono<List<String>> scopeMono) {
return scopeMono
.flatMapMany(scopes -> Flux
.fromIterable(scopes)
.flatMap(scope -> getConfigs(scope)
.map(properties -> new Scope(scope, properties))));
}
@PostMapping("/{scope}")
@SaveAction
@Operation(summary = "保存配置")
public Mono<Void> saveConfig(@PathVariable String scope,
@RequestBody Mono<Map<String, Object>> properties) {
return properties.flatMap(props -> configManager.setProperties(scope, props));
}
@PostMapping("/scope/_save")
@SaveAction
@Operation(summary = "批量保存配置")
@Transactional
public Mono<Void> saveConfig(@RequestBody Flux<Scope> scope) {
return scope
.concatMap(scopeConfig -> configManager.setProperties(scopeConfig.getScope(), scopeConfig.getProperties()))
.then();
}
@Getter
@Setter
public static class ConfigPropertyValue extends ConfigPropertyDef {
private Object value;
public static ConfigPropertyValue of(ConfigPropertyDef def, Object value) {
ConfigPropertyValue val = FastBeanCopier.copy(def, new ConfigPropertyValue());
val.setValue(value);
return val;
}
}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public static class Scope {
private String scope;
private Map<String, Object> properties;
}
}

View File

@ -1,95 +1,23 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.configuration;
import com.alibaba.fastjson.JSON;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Generated;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.Converter;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.cache.ReactiveCacheManager;
import org.hswebframework.web.dict.EnumDict;
import org.hswebframework.web.dict.defaults.DefaultItemDefine;
import org.jetlinks.community.Interval;
import org.jetlinks.community.JvmErrorException;
import org.jetlinks.community.command.CommandSupportManagerProvider;
import org.jetlinks.community.command.CommandSupportManagerProviders;
import org.jetlinks.community.command.register.CommandServiceEndpointRegister;
import org.jetlinks.community.config.ConfigManager;
import org.jetlinks.community.config.ConfigScopeCustomizer;
import org.jetlinks.community.config.ConfigScopeProperties;
import org.jetlinks.community.config.SimpleConfigManager;
import org.jetlinks.community.config.entity.ConfigEntity;
import org.jetlinks.community.dictionary.DictionaryJsonDeserializer;
import org.jetlinks.community.form.type.FieldTypeProvider;
import org.jetlinks.community.reactorql.aggregation.InternalAggregationSupports;
import org.jetlinks.community.reactorql.function.InternalFunctionSupport;
import org.jetlinks.community.reactorql.term.TermTypeSupport;
import org.jetlinks.community.reactorql.term.TermTypes;
import org.jetlinks.community.reference.DataReferenceManager;
import org.jetlinks.community.reference.DataReferenceProvider;
import org.jetlinks.community.reference.DefaultDataReferenceManager;
import org.jetlinks.community.resource.DefaultResourceManager;
import org.jetlinks.community.resource.ResourceManager;
import org.jetlinks.community.resource.ResourceProvider;
import org.jetlinks.community.resource.TypeScriptDeclareResourceProvider;
import org.jetlinks.community.resource.initialize.PermissionResourceProvider;
import org.jetlinks.community.service.DefaultUserBindService;
import org.jetlinks.community.utils.TimeUtils;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.metadata.types.DataTypes;
import org.jetlinks.reactor.ql.feature.Feature;
import org.jetlinks.reactor.ql.supports.DefaultReactorQLMetadata;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.jetlinks.supports.official.JetLinksDataTypeCodecs;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.util.unit.DataSize;
import reactor.core.Exceptions;
import reactor.core.publisher.Hooks;
import javax.annotation.Nonnull;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
@AutoConfiguration
@Configuration
@SuppressWarnings("all")
@EnableConfigurationProperties({ConfigScopeProperties.class})
public class CommonConfiguration {
static {
InternalAggregationSupports.register();
InternalFunctionSupport.register();
BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() {
@Override
public <T> T convert(Class<T> aClass, Object o) {
@ -116,7 +44,7 @@ public class CommonConfiguration {
BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() {
@Override
public <T> T convert(Class<T> type, Object value) {
return (T)DataSize.parse(String.valueOf(value));
return (T) DataSize.parse(String.valueOf(value));
}
}, DataSize.class);
@ -134,154 +62,6 @@ public class CommonConfiguration {
}
}, Interval.class);
BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() {
@Override
public <T> T convert(Class<T> type, Object value) {
return (T) TimeUtils.parseUnit(String.valueOf(value));
}
}, ChronoUnit.class);
BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() {
@Override
public <T> T convert(Class<T> type, Object value) {
return (T)((Long)CastUtils.castNumber(value).longValue());
}
}, long.class);
BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() {
@Override
public <T> T convert(Class<T> type, Object value) {
return (T)((Long) CastUtils.castNumber(value).longValue());
}
}, Long.class);
BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() {
@Override
@Generated
public <T> T convert(Class<T> type, Object value) {
if (value instanceof String) {
return (T) DefaultItemDefine.builder()
.value(String.valueOf(value))
.build();
}
return (T) FastBeanCopier.copy(value, new DefaultItemDefine());
}
}, EnumDict.class);
BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() {
@Override
@Generated
public <T> T convert(Class<T> type, Object value) {
if (value instanceof Map) {
Map<String, Object> map = ((Map) value);
String typeId = (String) map.get("type");
if (StringUtils.isEmpty(typeId)) {
return null;
}
return (T) JetLinksDataTypeCodecs.decode(DataTypes.lookup(typeId).get(), map);
}
return null;
}
}, DataType.class);
//捕获jvm错误,防止Flux被挂起
Hooks.onOperatorError((err, val) -> {
if (Exceptions.isJvmFatal(err)) {
return new JvmErrorException(err);
}
return err;
});
Hooks.onNextError((err, val) -> {
if (Exceptions.isJvmFatal(err)) {
return new JvmErrorException(err);
}
return err;
});
}
@Bean
public ApplicationContextAware staticBeanRegister() {
return ctx -> {
ctx.getBeanProvider(Feature.class)
.forEach(DefaultReactorQLMetadata::addGlobal);
ctx.getBeanProvider(CommandSupportManagerProvider.class)
.forEach(CommandSupportManagerProviders::register);
ctx.getBeanProvider(TermTypeSupport.class)
.forEach(TermTypes::register);
ctx.getBeanProvider(FieldTypeProvider.class)
.forEach(provider -> FieldTypeProvider.supports.register(provider.getProvider(), provider));
};
}
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){
return builder->{
builder.deserializerByType(DataType.class, new DataTypeJSONDeserializer());
builder.deserializerByType(Date.class,new SmartDateDeserializer());
builder.deserializerByType(EnumDict.class, new DictionaryJsonDeserializer());
};
}
@Bean
public ConfigManager configManager(ObjectProvider<ConfigScopeCustomizer> configScopeCustomizers,
ReactiveRepository<ConfigEntity, String> repository,
ReactiveCacheManager cacheManager) {
SimpleConfigManager configManager = new SimpleConfigManager(repository,cacheManager);
for (ConfigScopeCustomizer customizer : configScopeCustomizers) {
customizer.custom(configManager);
}
return configManager;
}
@Bean
public PermissionResourceProvider permissionResourceProvider(){
return new PermissionResourceProvider();
}
@Bean
public TypeScriptDeclareResourceProvider typeScriptDeclareResourceProvider() {
return new TypeScriptDeclareResourceProvider();
}
@Bean
public ResourceManager resourceManager(ObjectProvider<ResourceProvider> providers) {
DefaultResourceManager manager = new DefaultResourceManager();
providers.forEach(manager::addProvider);
return manager;
}
@Bean
public DataReferenceManager dataReferenceManager(ObjectProvider<DataReferenceProvider> provider) {
DefaultDataReferenceManager referenceManager = new DefaultDataReferenceManager();
provider.forEach(referenceManager::addStrategy);
return referenceManager;
}
@Bean
public CommandServiceEndpointRegister commandServiceEndpointRegister() {
return new CommandServiceEndpointRegister();
}
@Configuration
@ConditionalOnClass(ReactiveRedisOperations.class)
static class DefaultUserBindServiceConfiguration {
@Bean
public DefaultUserBindService defaultUserBindService(ReactiveRedisOperations<Object, Object> redis) {
return new DefaultUserBindService(redis);
}
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.configuration;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.jetlinks.core.metadata.DataType;
import java.io.IOException;
import java.util.Map;
/**
*
* @author zhangji 2025/1/23
* @since 2.3
*/
public class DataTypeJSONDeserializer extends JsonDeserializer<DataType> {
@Override
public DataType deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Map<String,Object> map= ctxt.readValue(parser, Map.class);
return (DataType) BeanUtilsBean.getInstance().getConvertUtils().convert(map, DataType.class);
}
}

View File

@ -1,49 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.configuration;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import lombok.SneakyThrows;
import org.jetlinks.community.utils.TimeUtils;
import java.util.Date;
/**
* 时间反序列化配置
*
* @author zhouhao
*/
public class SmartDateDeserializer extends JsonDeserializer<Date> {
@Override
@SneakyThrows
public Date deserialize(JsonParser p, DeserializationContext ctxt) {
if (p.hasToken(JsonToken.VALUE_STRING)) {
String str = p.getText().trim();
if (str.length() == 0) {
return (Date) getEmptyValue(ctxt);
}
return TimeUtils.parseDate(str);
}
if (p.hasToken(JsonToken.VALUE_NUMBER_INT)) {
long ts = p.getLongValue();
return new Date(ts);
}
return null;
}
}

View File

@ -1,39 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.configuration;
import org.jetlinks.community.resource.ui.UiMenuResourceProvider;
import org.jetlinks.community.resource.ui.UiResourceProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(prefix = "jetlinks.ui", name = "enabled", havingValue = "true", matchIfMissing = true)
public class UiResourceConfiguration {
@Bean
public UiResourceProvider uiResourceProvider() {
return new UiResourceProvider();
}
@Bean
public UiMenuResourceProvider uiMenuResourceProvider() {
return new UiMenuResourceProvider();
}
}

View File

@ -1,103 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.hswebframework.web.dict.EnumDict;
import org.hswebframework.web.dictionary.entity.DictionaryItemEntity;
import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService;
import org.springframework.boot.CommandLineRunner;
import javax.annotation.Nonnull;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author bestfeng
*/
@AllArgsConstructor
public class DatabaseDictionaryManager implements DictionaryManager, CommandLineRunner{
private final DefaultDictionaryItemService dictionaryItemService;
private final Map<String, Map<String, DictionaryItemEntity>> itemStore = new ConcurrentHashMap<>();
@Nonnull
@Override
public List<EnumDict<?>> getItems(@Nonnull String dictId) {
Map<String, DictionaryItemEntity> itemEntityMap = itemStore.get(dictId);
if (MapUtils.isEmpty(itemEntityMap)) {
return Collections.emptyList();
}
return new ArrayList<>(itemEntityMap.values());
}
@Nonnull
@Override
public Optional<EnumDict<?>> getItem(@Nonnull String dictId, @Nonnull String itemId) {
Map<String, DictionaryItemEntity> itemEntityMap = itemStore.get(dictId);
if (itemEntityMap == null) {
return Optional.empty();
}
return Optional.ofNullable(itemEntityMap.get(itemId));
}
public void registerItems(List<DictionaryItemEntity> items) {
items.forEach(this::registerItem);
}
public void removeItems(List<DictionaryItemEntity> items) {
items.forEach(this::removeItem);
}
public void removeItem(DictionaryItemEntity item) {
if (item == null || item.getDictId() == null || item.getId() == null) {
return;
}
itemStore.compute(item.getDictId(), (k, v) -> {
if (v != null) {
v.remove(item.getId());
if (!v.isEmpty()) {
return v;
}
}
return null;
});
}
public void registerItem(DictionaryItemEntity item) {
if (item == null || item.getDictId() == null) {
return;
}
itemStore
.computeIfAbsent(item.getDictId(), k -> new ConcurrentHashMap<>())
.put(item.getId(), item);
}
@Override
public void run(String... args) throws Exception {
dictionaryItemService
.createQuery()
.fetch()
.doOnNext(this::registerItem)
.subscribe();
}
}

View File

@ -1,109 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
import org.hswebframework.web.dict.EnumDict;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 动态数据字典工具类
*
* @author zhouhao
* @since 2.1
*/
public class Dictionaries {
static DictionaryManager HOLDER = null;
static void setup(DictionaryManager manager) {
HOLDER = manager;
}
/**
* 获取字典的所有选型
*
* @param dictId 字典ID
* @return 字典值
*/
@Nonnull
public static List<EnumDict<?>> getItems(@Nonnull String dictId) {
return HOLDER == null ? Collections.emptyList() : HOLDER.getItems(dictId);
}
/**
* 根据掩码获取枚举选项,通常用于多选时获取选项.
*
* @param dictId 枚举ID
* @param mask 掩码
* @return 选项
* @see Dictionaries#toMask(Collection)
*/
@Nonnull
public static List<EnumDict<?>> getItems(@Nonnull String dictId, long mask) {
if (HOLDER == null) {
return Collections.emptyList();
}
return HOLDER
.getItems(dictId)
.stream()
.filter(item -> item.in(mask))
.collect(Collectors.toList());
}
/**
* 查找枚举选项
*
* @param dictId 枚举ID
* @param value 选项值
* @return 选项
*/
public static Optional<EnumDict<?>> findItem(@Nonnull String dictId, Object value) {
return getItems(dictId)
.stream()
.filter(item -> item.eq(value))
.findFirst();
}
/**
* 获取字段选型
*
* @param dictId 字典ID
* @param itemId 选项ID
* @return 选项值
*/
@Nonnull
public static Optional<EnumDict<?>> getItem(@Nonnull String dictId,
@Nonnull String itemId) {
return HOLDER == null ? Optional.empty() : HOLDER.getItem(dictId, itemId);
}
public static long toMask(Collection<EnumDict<?>> items) {
long value = 0L;
for (EnumDict<?> t1 : items) {
value |= t1.getMask();
}
return value;
}
}

View File

@ -1,86 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
import org.hswebframework.ezorm.rdb.mapping.annotation.Codec;
import org.hswebframework.web.dict.EnumDict;
import java.lang.annotation.*;
import java.util.Collection;
/**
* 定义字段是一个数据字典,和枚举的使用方式类似.
* <p>
* 区别是数据的值通过{@link Dictionaries}进行获取.
*
* <pre>{@code
* public class MyEntity{
*
* //数据库存储的是枚举的值
* @Column(length=32)
* @Dictionary("my_status")
* @ColumnType(javaType=String.class)
* private EnumDict<String> status;
*
* @Column
* @Dictionary("my_types")
* //使用long来存储数据,表示使用字段的序号来进行mask运算进行存储.
* @ColumnType(javaType=Long.class,jdbcType=JDBCType.BIGINT)
* private EnumDict<String>[] types;
* }
* }</pre>
* <b>️注意</b>
* <ul>
* <li>
* 字段类型只支持{@code EnumDict<String>},{@code EnumDict<String>[]},{@code List<EnumDict<String>>}
* </li>
* <li>
* 多选时建议使用位掩码来存储: {@code @ColumnType(javaType=Long.class,jdbcType=JDBCType.BIGINT) },便于查询.
* </li>
* <li>使用位掩码存储字典值时,基于{@link EnumDict#ordinal()}进行计算,因此字段选项数量不能超过64个,修改字典时,请注意序号值变化</li>
* <li>模块需要引入依赖:<pre>{@code
* <dependency>
* <groupId>org.hswebframework.web</groupId>
* <artifactId>hsweb-system-dictionary</artifactId>
* </dependency>
* }</pre></li>
* </ul>
*
*
* @author zhouhao
* @see EnumDict#getValue()
* @see EnumDict#getMask()
* @see Dictionaries
* @see Dictionaries#toMask(Collection)
* @see DictionaryManager
* @since 2.2
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Codec
public @interface Dictionary {
/**
* 数据字典ID
*
* @return 数据字典ID
* @see Dictionaries#getItem(String, String)
*/
String value();
}

View File

@ -1,72 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.EnumFragmentBuilder;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.EnumInFragmentBuilder;
import org.hswebframework.web.crud.configuration.TableMetadataCustomizer;
import org.jetlinks.community.form.type.EnumFieldType;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.sql.JDBCType;
import java.util.List;
import java.util.Set;
public class DictionaryColumnCustomizer implements TableMetadataCustomizer {
@Override
public void customColumn(Class<?> entityType,
PropertyDescriptor descriptor,
Field field,
Set<Annotation> annotations,
RDBColumnMetadata column) {
Dictionary dictionary = annotations
.stream()
.filter(Dictionary.class::isInstance)
.findFirst()
.map(Dictionary.class::cast)
.orElse(null);
if (dictionary != null) {
Class<?> type = field.getType();
JDBCType jdbcType = (JDBCType) column.getType().getSqlType();
EnumFieldType codec = new EnumFieldType(
type.isArray() || List.class.isAssignableFrom(type),
dictionary.value(),
jdbcType)
.withArray(type.isArray())
.withFieldValueConverter(e -> e);
column.setValueCodec(codec);
if (codec.isToMask()) {
column.addFeature(EnumFragmentBuilder.eq);
column.addFeature(EnumFragmentBuilder.not);
column.addFeature(EnumInFragmentBuilder.of(column.getDialect()));
column.addFeature(EnumInFragmentBuilder.ofNot(column.getDialect()));
}
}
}
@Override
public void customTable(Class<?> entityType, RDBTableMetadata table) {
}
}

View File

@ -1,74 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
import org.hswebframework.web.crud.events.EntityEventListenerCustomizer;
import org.hswebframework.web.dictionary.entity.DictionaryEntity;
import org.hswebframework.web.dictionary.entity.DictionaryItemEntity;
import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService;
import org.hswebframework.web.dictionary.service.DefaultDictionaryService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@AutoConfiguration
public class DictionaryConfiguration {
@AutoConfiguration
@ConditionalOnClass(DefaultDictionaryItemService.class)
//@ConditionalOnBean(DefaultDictionaryItemService.class)
public static class DictionaryManagerConfiguration {
@Bean
public EntityEventListenerCustomizer dictionaryEntityEventListenerCustomizer() {
return configure -> {
configure.enable(DictionaryItemEntity.class);
configure.enable(DictionaryEntity.class);
};
}
@Bean
public DictionaryEventHandler dictionaryEventHandler(DefaultDictionaryItemService service) {
return new DictionaryEventHandler(service);
}
@Bean
public DatabaseDictionaryManager defaultDictionaryManager(DefaultDictionaryItemService service) {
DatabaseDictionaryManager dictionaryManager = new DatabaseDictionaryManager(service);
Dictionaries.setup(dictionaryManager);
return dictionaryManager;
}
@Bean
public DictionaryColumnCustomizer dictionaryColumnCustomizer() {
return new DictionaryColumnCustomizer();
}
@Bean
@ConfigurationProperties(prefix = "jetlinks.dict")
public DictionaryInitManager dictionaryInitManager(ObjectProvider<DictionaryInitInfo> initInfo,
DefaultDictionaryService defaultDictionaryService,
DefaultDictionaryItemService itemService) {
return new DictionaryInitManager(initInfo, defaultDictionaryService, itemService);
}
}
}

View File

@ -1,24 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
public interface DictionaryConstants {
/**
* 系统分类标识
*/
String CLASSIFIED_SYSTEM = "system";
}

View File

@ -1,110 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
import lombok.AllArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.hswebframework.web.crud.events.*;
import org.hswebframework.web.dictionary.entity.DictionaryEntity;
import org.hswebframework.web.dictionary.entity.DictionaryItemEntity;
import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService;
import org.hswebframework.web.dictionary.service.DefaultDictionaryService;
import org.hswebframework.web.exception.BusinessException;
import org.springframework.context.event.EventListener;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
/**
* @author bestfeng
*/
@AllArgsConstructor
public class DictionaryEventHandler {
private final DefaultDictionaryItemService itemService;
@EventListener
public void handleDictionaryCreated(EntityCreatedEvent<DictionaryEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.flatMap(dictionary -> {
if (!CollectionUtils.isEmpty(dictionary.getItems())) {
return Flux
.fromIterable(dictionary.getItems())
.doOnNext(item -> item.setDictId(dictionary.getId()))
.as(itemService::save);
}
return Mono.empty();
})
);
}
@EventListener
public void handleDictionarySaved(EntitySavedEvent<DictionaryEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.flatMap(dictionary -> {
if (!CollectionUtils.isEmpty(dictionary.getItems())) {
return Flux
.fromIterable(dictionary.getItems())
.doOnNext(item -> item.setDictId(dictionary.getId()))
.as(itemService::save);
}
return Mono.empty();
})
);
}
@EventListener
public void handleDictionaryDeleted(EntityDeletedEvent<DictionaryEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.map(DictionaryEntity::getId)
.collectList()
.flatMap(dictionary -> itemService
.createDelete()
.where()
.in(DictionaryItemEntity::getDictId, dictionary)
.execute()
.then())
);
}
/**
* 监听字典删除前事件阻止删除分类标识为系统的字典
*
* @param event 字典删除前事件
*/
@EventListener
public void handleDictionaryBeforeDelete(EntityBeforeDeleteEvent<DictionaryEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.any(dictionary ->
StringUtils.equals(dictionary.getClassified(), DictionaryConstants.CLASSIFIED_SYSTEM))
.flatMap(any -> {
if (any) {
return Mono.error(() -> new BusinessException("error.system_dictionary_can_not_delete"));
}
return Mono.empty();
})
);
}
}

View File

@ -1,38 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.web.dictionary.entity.DictionaryEntity;
import reactor.core.publisher.Flux;
import java.util.Collection;
/**
* @author gyl
* @since 2.2
*/
public interface DictionaryInitInfo {
Collection<DictionaryEntity> getDict();
default Flux<DictionaryEntity> getDictAsync() {
if (CollectionUtils.isEmpty(getDict())) {
return Flux.empty();
}
return Flux.fromIterable(getDict());
}
}

View File

@ -1,96 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.hswebframework.web.crud.events.EntityEventHelper;
import org.hswebframework.web.dictionary.entity.DictionaryEntity;
import org.hswebframework.web.dictionary.entity.DictionaryItemEntity;
import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService;
import org.hswebframework.web.dictionary.service.DefaultDictionaryService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.CommandLineRunner;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/**
* @author gyl
* @since 2.2
*/
@Slf4j
public class DictionaryInitManager implements CommandLineRunner {
@Getter
@Setter
private List<DictionaryEntity> inits = new ArrayList<>();
public final ObjectProvider<DictionaryInitInfo> initInfo;
private final DefaultDictionaryService defaultDictionaryService;
private final DefaultDictionaryItemService itemService;
public DictionaryInitManager(ObjectProvider<DictionaryInitInfo> initInfo, DefaultDictionaryService defaultDictionaryService, DefaultDictionaryItemService itemService) {
this.initInfo = initInfo;
this.defaultDictionaryService = defaultDictionaryService;
this.itemService = itemService;
}
@Override
public void run(String... args) {
Flux
.merge(
Flux.fromIterable(inits),
Flux
.fromIterable(initInfo)
.flatMap(DictionaryInitInfo::getDictAsync)
)
.buffer(200)
.filter(CollectionUtils::isNotEmpty)
.flatMap(collection -> {
List<DictionaryItemEntity> items = generateItems(collection);
return defaultDictionaryService
.save(collection)
.mergeWith(itemService.save(items));
})
.as(EntityEventHelper::setDoNotFireEvent)
.subscribe(ignore -> {
},
err -> log.error("init dict error", err));
}
public List<DictionaryItemEntity> generateItems(List<DictionaryEntity> dictionaryList) {
List<DictionaryItemEntity> items = new ArrayList<>();
for (DictionaryEntity dictionary : dictionaryList) {
if (!CollectionUtils.isEmpty(dictionary.getItems())) {
for (DictionaryItemEntity item : dictionary.getItems()) {
item.setDictId(dictionary.getId());
items.add(item);
}
}
}
return items;
}
}

View File

@ -1,55 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.dict.EnumDict;
import org.hswebframework.web.dict.defaults.DefaultItemDefine;
import java.io.IOException;
import java.util.Map;
public class DictionaryJsonDeserializer extends JsonDeserializer<EnumDict<?>> {
@Override
public EnumDict<?> deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JacksonException {
if (jsonParser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
DefaultItemDefine defaultItemDefine = new DefaultItemDefine();
defaultItemDefine.setOrdinal(jsonParser.getIntValue());
return defaultItemDefine;
}
if (jsonParser.hasToken(JsonToken.VALUE_STRING)) {
String str = jsonParser.getText().trim();
if (!str.isEmpty()) {
DefaultItemDefine defaultItemDefine = new DefaultItemDefine();
defaultItemDefine.setValue(str);
return defaultItemDefine;
}
}
if (jsonParser.hasToken(JsonToken.START_OBJECT)) {
Map<?, ?> map = ctxt.readValue(jsonParser, Map.class);
if (map != null) {
return FastBeanCopier.copy(map, new DefaultItemDefine());
}
}
return null;
}
}

View File

@ -1,56 +0,0 @@
/*
* Copyright 2025 JetLinks https://www.jetlinks.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetlinks.community.dictionary;
import org.hswebframework.web.dict.EnumDict;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Optional;
/**
* 数据字典管理器,用于获取数据字典的枚举值
*
* @author zhouhao
* @since 2.1
*/
public interface DictionaryManager {
/**
* 获取字典的所有选项
*
* @param dictId 字典ID
* @return 字典值
*/
@Nonnull
List<EnumDict<?>> getItems(@Nonnull String dictId);
/**
* 获取字段选项
*
* @param dictId 字典ID
* @param itemId 选项ID
* @return 选项值
*/
@Nonnull
Optional<EnumDict<?>> getItem(@Nonnull String dictId,
@Nonnull String itemId);
}

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