Compare commits

...

632 Commits
1.13 ... 2.11

Author SHA1 Message Date
dependabot[bot] 6abc815cdf
build(deps): bump org.apache.logging.log4j:log4j-core (#730)
Bumps org.apache.logging.log4j:log4j-core from 2.25.1 to 2.25.3.

---
updated-dependencies:
- dependency-name: org.apache.logging.log4j:log4j-core
  dependency-version: 2.25.3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-21 18:43:31 +08:00
zhouhao c7a226bd4b feat: 适配新的网络组件查询接口 2025-12-17 09:33:51 +08:00
zhouhao d6eb169f32 fix: 修复设备日志时间错误 2025-12-09 17:37:23 +08:00
zhouhao f2b99b501d refactor: 优化内嵌UI 2025-12-01 09:58:00 +08:00
zhouhao 5b32a0f813 fix: NPE 2025-11-25 15:17:53 +08:00
zhouhao 51a600af70 build(CI): 构建多平台镜像 2025-11-19 19:00:13 +08:00
zhouhao 2ef1a7840a build(CI): 构建多平台镜像 2025-11-19 17:55:26 +08:00
zhouhao dbcf9c8f3a Merge remote-tracking branch 'origin/2.11' into 2.11 2025-11-19 16:38:31 +08:00
zhouhao fff6038c5c refactor(基础模块): 优化es索引以及查询逻辑 2025-11-19 16:38:17 +08:00
PengyuDeng 7c46400585
perf(menu):Replace Guava Cache with Caffeine in DefaultMenuService (#722) 2025-11-18 15:09:06 +08:00
zhouhao b952f34719 refactor(基础模块): 优化条件比对 2025-11-13 15:22:54 +08:00
zhouhao 58e91a5097 refactor(基础模块): 优化设备会话持久化逻辑 2025-11-12 09:05:55 +08:00
PengyuDeng deff207dd8
feat(timescaledb): 支持配置TimescaleDB函数所在的schema (#710)
* feat(timescaledb): 支持配置TimescaleDB函数所在的schema

- 在application.yml和application-default.yml中新增TIMESCALEDB_FUNCTION_SCHEMA配置项
- 修改TimescaleDBCreateTableSqlBuilder类,移除构造函数中的schema参数
- 更新创建超表和保留策略的SQL语句,使用functionSchema替代原有schema
- 在TimescaleDBTimeSeriesManager中注入TimescaleDBProperties并设置FunctionSchema特性
- 调整时间分组列的创建逻辑,支持传入functionSchema参数
- 引入FunctionSchema类管理TimescaleDB相关的函数位置信息

* 把配置放进RDBDatabaseMetadata里

* 优化
2025-11-05 18:38:52 +08:00
zhouhao 4d7d40696d refactor: 使用单独线程处理设备消息 2025-11-05 16:47:32 +08:00
PengyuDeng 00f9391534
refactor(common): 调整属性读写模式常量定义 (#712) 2025-11-05 16:23:04 +08:00
zhouhao 70e42b11fc refactor: 优化网关设备接口权限控制 2025-11-03 14:49:50 +08:00
PengyuDeng bfb61547a3
feat(io-component): 添加布尔值转换器支持 (#707)
* feat(io-component): 添加布尔值转换器支持
- 新增 BooleanConverter 类处理布尔值的读写转换

* 优化

* Update ExcelUtils.java

* 优化
2025-10-31 19:31:39 +08:00
PengyuDeng ccd7f8b806
feat(rule-engine): 添加字段分组名称支持 (#700)
* feat(rule-engine): 添加字段分组名称支持

- 在TermColumn中新增groupName字段
- 更新with方法以支持从PropertyMetadata获取分组名称

* refactor(rule-engine):优化场景条件列配置

- 使用 ConfigKey 动态处理分组名称配置

* 优化

* 优化
2025-10-30 12:02:34 +08:00
zhouhao fbc756f8b7 build: 2.11.0-SNAPSHOT 2025-10-28 14:37:06 +08:00
zhouhao c03c318b85 doc: 优化说明 2025-10-27 13:42:56 +08:00
zhouhao 87d75c22a6 Merge branch '2.10' into 2.11 2025-10-27 11:52:35 +08:00
zhouhao 60ecc9f65d refactor: 升级依赖 2025-10-27 11:45:17 +08:00
zhouhao 2fde2a25e3 build: 2.11 2025-10-27 11:44:40 +08:00
zhouhao 23ce017844 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-10-27 11:38:59 +08:00
zhouhao 0515c78a88 refactor: 升级依赖版本 2025-10-27 11:38:25 +08:00
zhouhao 26e555f881 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-10-26 19:11:55 +08:00
zhouhao 5dca15e76a refactor: 优化条件匹配逻辑 2025-10-26 16:00:31 +08:00
zhouhao bc758c2885 refactor: 启动失败 退出进程 2025-10-23 15:19:29 +08:00
zhouhao 0429233c31 refactor: 优化透传解析api 2025-10-23 09:19:47 +08:00
zhouhao e3aa51a3ab Merge remote-tracking branch 'origin/2.10' into 2.10 2025-10-20 09:52:48 +08:00
zhouhao a3edb86a49 refactor: 优化 2025-10-20 09:52:37 +08:00
zhouhao 6a0fff01b6 refactor: 优化延迟节点 2025-10-18 16:43:13 +08:00
zhouhao fc7d09ce8b refactor: 优化 2025-10-15 16:56:08 +08:00
zhouhao 89968c8e87 refactor: 优化条件匹配性能 2025-10-15 15:17:29 +08:00
zhouhao 069b7dace7 refactor: 优化配置 2025-10-15 14:40:25 +08:00
zhouhao 5879d8bbe5 refactor: 优化告警解除逻辑 2025-10-15 14:05:09 +08:00
zhouhao 67789af77d refactor: 优化 2025-10-15 09:33:46 +08:00
zhouhao fd196fbaf8 refactor: 优化心跳超时时间设置 2025-10-13 16:33:20 +08:00
dependabot[bot] 7c1617337c
build(deps): bump io.grpc:grpc-netty-shaded from 1.65.0 to 1.75.0 (#696)
Bumps [io.grpc:grpc-netty-shaded](https://github.com/grpc/grpc-java) from 1.65.0 to 1.75.0.
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.65.0...v1.75.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty-shaded
  dependency-version: 1.75.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-07 14:33:18 +08:00
zhouhao d99dac0041 refactor: 优化tcp报文打印 2025-09-27 17:03:14 +08:00
zhouhao 472e1dc89c refactor: 优化权限初始化逻辑 2025-09-27 15:03:41 +08:00
zhouhao 3811266945 refactor: 优化菜单初始化逻辑 2025-09-27 12:14:26 +08:00
zhouhao 8e829817c5 refactor: 优化用户权限初始化逻辑 2025-09-27 10:00:58 +08:00
squlenhat c28de1741c
fix: 修复阿里云语音通知Tts方式不能同时发送给多个关联用户的问题 (#683) 2025-09-16 14:31:32 +08:00
squlenhat fd3455f173
fix: 修复邮件通知类型中username未赋值导致的setFrom逻辑错误 (#681) 2025-09-12 15:56:35 +08:00
zhouhao 766584426d refactor: 优化 2025-09-12 15:06:29 +08:00
zhouhao 79b2257a8b refactor: 升级依赖版本 2025-09-11 16:38:28 +08:00
zhouhao cbe852e290 refactor: 优化 2025-09-11 09:43:39 +08:00
zhouhao a7f5b03e6c Merge remote-tracking branch 'origin/2.10' into 2.10 2025-09-11 09:31:13 +08:00
zhouhao dcdba73994 refactor: 优化 2025-09-11 09:31:02 +08:00
wthee 960b940b5c
fix: 修复 TimescaleDB 方法调用问题 (#678) 2025-09-05 14:12:01 +08:00
zhouhao 20f589999a build(maven): 升级依赖版本 2025-08-06 16:07:22 +08:00
zhouhao 886d918739 refactor: 优化 2025-08-06 10:39:42 +08:00
zhouhao b6510fe9c0 refactor: 优化 2025-08-04 15:11:26 +08:00
zhouhao 8e72147362 refactor: 优化lock 2025-08-01 16:38:21 +08:00
zhouhao b9cf1b3db5 fix: 修复设备统计没有产品字段问题 2025-07-30 15:44:28 +08:00
zhouhao 773f9f4bb1 fix: 修复设备统计没有产品字段问题 2025-07-30 15:36:28 +08:00
zhouhao 488989d9a0 refactor: 优化excel导入导出 2025-07-24 10:26:43 +08:00
zhouhao 1ee2c16a48 refactor: 优化数据库兼容性 2025-07-18 13:43:38 +08:00
zhouhao c711b0e627 refactor: 优化日志配置 2025-07-11 18:34:29 +08:00
zhouhao 0878f81361 refactor: 增加时序模块错误报告 2025-07-11 10:10:38 +08:00
zhouhao c53e4522c1 fix: 修复配置错误 2025-07-10 14:10:08 +08:00
zhouhao 12ec7dfcb3 refactor: 优化redis序列化配置 2025-07-10 10:21:39 +08:00
zhouhao 4494701946 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-07-10 09:48:05 +08:00
zhouhao 2621efa6ce refactor: 优化jdbc 2025-07-10 09:47:53 +08:00
zhouhao 6392775e9a refactor: 优化redis配置 2025-07-09 21:26:40 +08:00
zhouhao 2c891e5d20 refactor: 优化 2025-07-09 21:04:32 +08:00
zhouhao 6e3c92dc48 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-07-09 20:19:11 +08:00
zhouhao 24899e1b1f build: 升级依赖版本 2025-07-09 20:18:55 +08:00
zhouhao 916736c1ec refactor: 优化并行度 2025-07-09 15:37:07 +08:00
zhouhao 705929a713 refactor: 优化配置 2025-07-08 09:25:19 +08:00
zhouhao 5497beff58 refactor: 优化子设备会话 2025-07-07 19:08:50 +08:00
zhouhao 99015efd8e Merge branch '2.10' of github.com:jetlinks/jetlinks-community into 2.10 2025-07-07 09:35:06 +08:00
zhouhao 8af4d7eecc build: 升级依赖版本 2025-07-05 21:21:33 +08:00
zhouhao bfa8c8ea03 fix: 修复无法单独使用timescaledb问题 2025-07-05 21:03:32 +08:00
PengyuDeng e5a68aab20
fix(基础模块): 修改国际化支持文件目录 (#653) 2025-07-04 10:07:53 +08:00
zhouhao edce20eeca doc: 优化文档 2025-07-03 18:33:06 +08:00
zhouhao 1ffc0e6070 fix(网关管理): 修复插件网关禁用仍调用createPlugin()方法的问题 2025-07-03 18:32:53 +08:00
zhouhao 2c52227714 refactor: 优化状态同步消息订阅 2025-07-01 16:04:02 +08:00
zhouhao b529059e69 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-06-30 19:02:41 +08:00
zhouhao 7b2ea0913f refactor: 优化插件设备指令下发 2025-06-30 19:01:24 +08:00
zhouhao 9a07c28499 build: 优化插件设备指令下发 2025-06-30 18:58:52 +08:00
dependabot[bot] 863466a518
build(deps): bump commons-beanutils:commons-beanutils (#652)
Bumps commons-beanutils:commons-beanutils from 1.10.1 to 1.11.0.

---
updated-dependencies:
- dependency-name: commons-beanutils:commons-beanutils
  dependency-version: 1.11.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-30 18:39:18 +08:00
zhouhao 248e15ece1 build: 优化依赖 2025-06-30 17:18:21 +08:00
zhouhao c22152ede9 build: 优化依赖 2025-06-30 16:48:32 +08:00
zhouhao 0b1b233fa2 build: 优化依赖 2025-06-30 13:53:24 +08:00
zhouhao a7887857c9 fix: 修复表结构字段名称错误 2025-06-27 10:27:34 +08:00
PengyuDeng cac929d71e
refactor: i18n support for DefaultNetworkType enum (#651) 2025-06-26 19:46:18 +08:00
zhouhao ee13e07799 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-06-26 14:08:03 +08:00
zhouhao b543c55b4a build: 优化profile 2025-06-26 14:07:45 +08:00
zeje 38622f04ee
build: 整理plugin,解决install失败问题 (#649) 2025-06-25 16:16:17 +08:00
zhouhao 084c55bd9a Merge remote-tracking branch 'origin/2.10' into 2.10 2025-06-24 20:38:24 +08:00
zhouhao dc6b1b118b build: 移除无用依赖 2025-06-24 20:38:12 +08:00
zhouhao bed9337550 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-06-24 11:03:08 +08:00
zhouhao 9f15c5333b doc: add license 2025-06-24 11:02:53 +08:00
zhouhao 317fc42c6b fix: 修复告警记录统计错误 2025-06-24 11:02:22 +08:00
PengyuDeng a8b211ad21
fix: 优化timescaledb多列存储建表语句 (#647)
* fix: 优化timescaledb多列存储建表语句

* Update TimescaleDBColumnModeDDLOperations.java

* Update TimescaleDBColumnModeDDLOperations.java
2025-06-23 09:26:35 +08:00
zhouhao 555d6ec5d7 refactor: 升级依赖 2025-06-20 19:10:46 +08:00
zhouhao 821a9ec736 refactor: 优化mqtt连接处理 2025-06-19 09:16:42 +08:00
zhouhao 780b14187b refactor: 优化插件ID映射获取逻辑 2025-06-17 11:25:09 +08:00
zhouhao ff336cdae0 refactor: 优化tdengine配置 2025-06-17 10:34:18 +08:00
zhouhao f4af008ed1 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-06-17 09:20:44 +08:00
zhouhao dae843fe49 build: 升级spring依赖版本 2025-06-17 09:19:43 +08:00
zhouhao d6628c52f5 refactor: 取消解析变量时的校验 2025-06-12 20:32:44 +08:00
zhouhao 7fe35252f3 refactor: 优化兼容性 2025-06-12 14:04:06 +08:00
zhouhao da121cefee refactor: 移除jakarta.mail-api 2025-06-12 11:11:43 +08:00
zhouhao 0782dac6b6 fix: 修复无法发送邮件问题 2025-06-12 09:26:28 +08:00
zhouhao 791ca8b2c8 refactor: 升级hsweb-utils依赖 2025-06-11 19:00:46 +08:00
zhouhao 1db6e43a25 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-06-11 18:22:57 +08:00
zhouhao 0cb9988e20 refactor: 升级hsweb-utils依赖 2025-06-11 18:21:49 +08:00
PengyuDeng 2e1907b74d
refactor: 只保留一个OrgDimensionType (#643) 2025-06-10 19:17:26 +08:00
zhouhao 50b2ca9b44 fix: 修复es聚合查询错误 2025-06-10 14:40:09 +08:00
zhouhao 2157880905 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-06-09 14:47:22 +08:00
zhouhao beaef36d91 refactor: 修复条件失效问题 2025-06-09 14:46:48 +08:00
zhouhao 2ffd0f3183 doc: add license 2025-06-07 22:21:49 +08:00
zhouhao 9fb37d826c fix: 修复修改插件后不生效问题 2025-06-07 19:30:50 +08:00
zhouhao 75dea78c0a refactor: 优化聚合查询逻辑 2025-06-05 18:23:07 +08:00
zhouhao f27a8c8685 refactor: 优化物模型事件ddl 2025-06-03 09:33:55 +08:00
zhouhao fc62dab895 refactor: 优化 2025-05-30 16:23:15 +08:00
zhouhao ab95bb1d37 fix: license 2025-05-30 12:03:07 +08:00
zhouhao 22cdb6862a fix: 修复保存协议不生效问题 2025-05-30 11:27:10 +08:00
zhouhao a809a8c33d refactor: 优化插件 2025-05-30 10:58:05 +08:00
zhouhao 0cf365e2b3 doc: add license 2025-05-29 16:48:07 +08:00
zhouhao 6e51235d49 refactor: 优化插件查询 2025-05-29 16:34:20 +08:00
zhouhao bb687f9484 refactor: 增加设备命令支持 2025-05-29 16:23:40 +08:00
zhouhao 5cfd92cd2f refactor: 增加profile 2025-05-29 09:07:12 +08:00
zhouhao 8ee188c13c refactor: 优化协议管理 2025-05-28 15:08:24 +08:00
zhouhao 8b30ab68e4 refactor: 优化buffer写入逻辑 2025-05-28 11:20:31 +08:00
zhouhao 0440a9f964 fix: 修复无法上传协议问题 2025-05-27 16:23:29 +08:00
zhouhao f052b01f07 fix: 修复索引不存在聚合查询报错问题 2025-05-27 13:53:03 +08:00
zhouhao 4d7285a3d5 Merge remote-tracking branch 'origin/2.10' into 2.10 2025-05-27 10:45:45 +08:00
zhouhao 02962158b7 refactor: 移除es配置类 2025-05-27 10:45:30 +08:00
zhouhao ff2e36e77a fix: license 缺失 2025-05-26 20:03:17 +08:00
zhouhao 94342f13dc Merge remote-tracking branch 'origin/master' into 2.10
# Conflicts:
#	jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/UiResourceConfiguration.java
#	jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/ui/UiMenuResourceProvider.java
#	jetlinks-components/common-component/src/main/java/org/jetlinks/community/resource/ui/UiResourceProvider.java
#	jetlinks-components/common-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
2025-05-26 19:05:20 +08:00
Zhang Ji fc651ecd9d
fix(场景联动): 修复执行动作的内置参数获取可能失效的问题 (#636) 2025-05-26 19:03:49 +08:00
zhouhao 54143df3e1 doc: add license header 2025-05-23 12:37:38 +08:00
zhouhao 2667b3d8a0 Merge branch '2.10' of github.com:jetlinks/jetlinks-community into 2.10 2025-05-23 10:45:47 +08:00
老周 54763c8cbd
feat: 增加插件功能支持 (#635)
* refactor: 升级版本

* feat: 增加插件功能支持
2025-05-23 09:04:20 +08:00
zhouhao 3d7515ad24 fix: 修复监控无数据问题 2025-05-22 20:06:25 +08:00
zhouhao 20b70771e4 refactor: 升级版本 2025-05-22 10:32:48 +08:00
zhouhao 4f6c622e4c refactor: 增加认证模块前端依赖 2025-05-22 10:12:12 +08:00
zhouhao 20bc184a9f refactor: 优化配置 2025-05-21 20:20:47 +08:00
zhouhao b962f822c5 refactor: 优化配置 2025-05-21 20:05:43 +08:00
zhouhao dbceadc5e0 Merge remote-tracking branch 'origin/master' 2025-05-20 15:32:09 +08:00
zhouhao 520ca24fed refactor: 增加ui resource接口 2025-05-20 15:31:49 +08:00
zhouhao b7adcf0a0e refactor: 优化 2025-05-15 17:25:14 +08:00
zhouhao 06b9c3e1e3 refactor: 优化 2025-05-15 16:56:13 +08:00
zhouhao f194946492 refactor: 优化配置 2025-05-15 15:00:34 +08:00
zhouhao 3be58dce1e refactor: 优化构建逻辑 2025-05-15 13:38:58 +08:00
zhouhao 545e66e130 refactor: 优化配置 2025-05-15 11:56:47 +08:00
zhouhao dab9244249 refactor: 优化配置 2025-05-13 14:03:57 +08:00
PengyuDeng c57dbda68a
build(doc):升级springdoc (#631)
* build(doc):升级springdoc

* Update pom.xml

* Update SpringDocCustomizerConfiguration.java

* 增加单独的类处理

* Update ResponseWrapperConverter.java

* 增加普通类型的处理

* Update ResponseWrapperConverter.java

* Update ResponseWrapperConverter.java

* Update SpringDocCustomizerConfiguration.java

* 增加泛型转换

* Update ResponseWrapperConverter.java

* Update ResponseWrapperConverter.java

* Update pom.xml

* openai参数配置化

---------

Co-authored-by: 老周 <zh.sqy@qq.com>
2025-05-12 16:23:54 +08:00
zhouhao 3ae7e668c2 refactor: 优化构建脚本 2025-05-09 16:15:54 +08:00
zhouhao 93b973a56d fix: 移除无用类 2025-05-08 17:58:21 +08:00
zhouhao f57165e320 refactor: 优化端口 2025-05-08 10:27:03 +08:00
zhouhao 09a786b2b6 refactor: 增加内嵌UI支持 2025-05-08 09:50:49 +08:00
laokou a6100d36f9
fix: 修复MqttClient网络组件定义的元数据名称和类型错误 (#632) 2025-05-06 09:25:11 +08:00
zhouhao 80ed50211a refactor: 优化标签存储 2025-04-29 18:11:00 +08:00
zhouhao 3a1f0b65de fix: 修复设备标签无法更新问题 2025-04-28 18:33:07 +08:00
zhouhao bcdc66c68a Merge branch 'master' into 2.10 2025-04-28 11:42:18 +08:00
zhouhao 2cfc78f1da Merge remote-tracking branch 'origin/master' 2025-04-28 11:41:26 +08:00
zhouhao ad34196a2a refactor: mqtt服务接入支持onClientConnect 2025-04-28 11:41:08 +08:00
老周 1e37c685cc
Update README.md 2025-04-27 18:12:06 +08:00
zhouhao 7d4ade0b37 refactor: 调整docker脚本 2025-04-27 17:35:56 +08:00
zhouhao 587c4f4d48 refactor: spring-boot3 适配 2025-04-27 17:28:32 +08:00
zhouhao 79e8f22cd5 Merge branch 'master' into 2.10
# Conflicts:
#	pom.xml
2025-04-27 10:43:26 +08:00
老周 7fdfe87e9d
build: 2.3 版本发布 (#615)
* build: 2.3 版本发布

* feat: 更新网关及设备管理相关模块代码 (#614)

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>

* feat(community): 场景联动以及通知模块2.3相关功能更新 (#612)

* feat(community): 场景联动以及通知模块2.3相关功能更新

* Update pull_request.yml

* feat(community): 场景联动清除资产相关代码

* fix(场景联动): 修复场景初始化问题

* feat(community): 注册个人订阅通道

* fix(通知模块): 订阅通道初始化

* fix(通知模块): 删除无用类

* fix(community): 解决合并冲突

---------

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>
Co-authored-by: 老周 <zh.sqy@qq.com>

* build: 升级依赖版本

* feat(community): 认证模块更新2.3相关功能 (#616)

* feat(community): 认证模块更新2.3相关功能

* fix(community): 修复自定义查询报错的问题

* fix(community): 优化邮件附件名称获取

---------

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>

* feat(community): 补充国际化相关内容,修复设备上线/离线没有日志的问题 (#619)

* feat(community): 补充国际化相关内容,修复设备上线/离线没有日志的问题

* feat(community): 补充设备物模型映射接口

---------

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>

* fix(notify): 修复消息通知相关问题 (#620)

* fix(notify): 修复消息通知相关问题

* fix(notify): 修复订阅管理数据展示错误问题

---------

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>

* fix(rule-engine): 场景联动相关bug修复 (#621)

* fix(rule-engine): 场景联动相关bug修复

* fix(rule-engine): 场景联动相关bug修复

* fix(rule-engine): 场景联动相关bug修复

* fix(rule-engine): 修复程序启动时订阅提供商禁用,但用户已订阅仍加载成功并订阅的问题

---------

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>

* fix(community): 修复认证中心以及设备相关的bug (#622)

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>

* feat(auth): 增加密码校验功能 (#625)

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>

* fix(auth): 修复组织父节点清除失败问题 (#627)

* fix(auth): 修复组织父节点清除失败问题

* fix(store): 补充存储模式名称国际化方法

---------

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>

* fix(auth): 修复用户绑定组织,查询用户关联组织失败的问题 (#628)

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>

* build: 升级依赖版本

---------

Co-authored-by: fighter-wang <118291973+fighter-wang@users.noreply.github.com>
Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>
2025-04-25 19:15:32 +08:00
老周 08a358ffab
Update pull_request.yml 2025-03-28 09:52:51 +08:00
zhouhao 0325eaddbf refactor: 优化设备注册逻辑 2025-03-26 14:46:10 +08:00
zhouhao f640e2b45f refactor: 优化会话处理逻辑 2025-03-26 11:06:02 +08:00
zhouhao 091b8c4f0c fix: 修复编译错误 2025-03-24 10:13:21 +08:00
zhouhao d3adf0e752 refactor: 优化设备会话持久化逻辑 2025-03-24 09:53:50 +08:00
zhouhao 7b7b96f096 refactor: 优化DeviceGatewayHelper 2025-03-19 10:54:27 +08:00
zhouhao c01d31606c refactor: 优化资源释放 2025-03-13 10:25:32 +08:00
zhouhao 757f3f6f77 feat: 升级依赖 2025-03-07 11:01:33 +08:00
老周 fd2d73d4eb
Update README.md 2025-02-17 09:12:45 +08:00
老周 2e487a42e2
feat(基础模块): 增加命令模式支持 (#607)
可使用 CommandSupportManagerProviders相关方法来执行服务命令进行解耦.
2025-02-13 12:33:18 +08:00
bestfeng1020 694595d446
fix(docker配置): 修复文件路径挂载错误 (#602)
* fix(docker配置): 修复文件路径挂载错误

* Update docker-compose.yml

---------

Co-authored-by: 老周 <zh.sqy@qq.com>
2025-02-12 11:52:22 +08:00
Zhang Ji 27a9fd6618
feat(场景联动): 添加设备数据执行动作,扩展数组条件,优化国际化 (#605) 2025-01-23 18:57:15 +08:00
bestfeng1020 ad6279ed22
feat(readme): 添加新QQ群信息 (#604)
feat(readme): 添加新QQ群信息
2025-01-13 15:29:35 +08:00
bestfeng1020 bc0c01d01f
fix(通用模块): 修复TDengine排序逻辑 (#599) 2024-12-31 10:35:05 +08:00
fighter-wang 6e40f6fb8a
fix(基础模块): 修复阿里云语音仅拨打一个用户号码的问题 (#597)
Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>
2024-12-18 16:15:42 +08:00
zhouhao 647f6ec9f3 fix(基础模块): 修复mqtt client 设备会话恢复后gatewayId为null问题 2024-12-16 17:52:17 +08:00
老周 7d79927d4d
Merge pull request #594 from zxl1951/fix-td-save
fix(存储策略): 涛思数据库存储使用无模式写入创建表缺失messageId列
2024-12-11 19:36:27 +08:00
ZxL 806db31c4a fix(存储策略): 涛思数据库存储使用无模式写入创建表缺失messageId列 2024-12-11 17:17:36 +08:00
老周 cd6f2f1049
Merge pull request #593 from jetlinks/bestfeng1020-patch-1
feat(readme): 删除DTU售卖信息
2024-12-11 10:06:40 +08:00
bestfeng1020 89eb2d66a8
feat(readme): 删除DTU售卖信息
feat(readme): 删除DTU售卖信息
2024-12-11 09:53:49 +08:00
fighter-wang 9c26d7d6c3
feat(设备管理): 新增在reactorQL中获取设备属性上报时间 (#592)
Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>
2024-12-09 13:53:14 +08:00
fighter-wang b890f4e520
fix(关系配置): 关系配置增加反转关系名称字段 (#591) 2024-12-05 19:50:57 +08:00
zhouhao f2bed57dc6 Merge remote-tracking branch 'origin/master' 2024-12-05 16:57:17 +08:00
zhouhao 13dd4e5e40 refactor: 优化mqtt 客户端配置 2024-12-05 16:56:59 +08:00
bestfeng1020 13ea87ff46
feat(docker镜像配置): 修改docker镜像版本 (#590) 2024-12-03 19:40:46 +08:00
zhouhao d2772e8881 refactor: 优化列式存储策略数字类型转换逻辑 2024-11-06 15:50:59 +08:00
zhouhao b5944c6d3a fix: #580 2024-10-29 15:02:06 +08:00
zhouhao 77abf004f7 Merge remote-tracking branch 'origin/master' 2024-10-21 17:26:09 +08:00
zhouhao 945bf2ea82 refactor(基础模块): 优化设备连接 2024-10-21 17:25:56 +08:00
dependabot[bot] b83dc75943
build(deps): bump commons-io:commons-io from 2.11.0 to 2.15.1 in /jetlinks-components/io-component (#576)
* build(deps): bump commons-io:commons-io

Bumps commons-io:commons-io from 2.11.0 to 2.14.0.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update pom.xml

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 老周 <zh.sqy@qq.com>
2024-10-08 13:32:20 +08:00
dependabot[bot] cc55f4ad6c
build(deps): bump org.elasticsearch:elasticsearch (#546)
Bumps [org.elasticsearch:elasticsearch](https://github.com/elastic/elasticsearch) from 7.17.13 to 7.17.23.
- [Release notes](https://github.com/elastic/elasticsearch/releases)
- [Changelog](https://github.com/elastic/elasticsearch/blob/main/CHANGELOG.md)
- [Commits](https://github.com/elastic/elasticsearch/compare/v7.17.13...v7.17.23)

---
updated-dependencies:
- dependency-name: org.elasticsearch:elasticsearch
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-08 09:41:40 +08:00
XiXuanHao 9b9545ecca
fix(产品管理): 产品启用时会重复触发DeviceProductDeployEvent事件 (#513)
Co-authored-by: XIXUANHAO <xi_xh@foxmail.com>
2024-10-08 09:40:17 +08:00
zhouhao 6dc16ce715 Merge remote-tracking branch 'origin/master' 2024-09-27 11:09:34 +08:00
zhouhao e3d7831073 build(maven): 2.3.0-SNAPSHOT 2024-09-27 11:09:22 +08:00
PengyuDeng e855993511
fix: 修改日志文件的文件名 (#573) 2024-09-14 16:19:45 +08:00
zhouhao 01d0a033d3 Merge remote-tracking branch 'origin/master' 2024-09-12 15:41:18 +08:00
zhouhao f1733e5880 build: 升级jetlinks-core版本1.2.3-SNAPSHOT 2024-09-12 15:41:05 +08:00
fighter-wang 0f78fec18f
feat(设备管理): 增加解析文件为属性物模型功能 (#569)
Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>
2024-09-12 11:57:10 +08:00
bestfeng1020 4d9e8c5b00
fix(README): 修正二次开发文档链接 (#571)
fix(README): 修正二次开发文档链接
2024-09-11 16:41:26 +08:00
zhouhao 600766c4a5 Merge remote-tracking branch 'origin/master' 2024-09-10 17:06:53 +08:00
zhouhao e8b6f27afe feat: 优化LocalFileThingsDataManager性能 2024-09-10 17:06:39 +08:00
fighter-wang 824466037b
feat(菜单管理): 新增清空菜单授权功能 (#570)
Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>
2024-09-06 14:55:20 +08:00
fighter-wang bf7e19c92e
fix(邮件通知): 优化获取邮件附件,过大时的抛错国际化 (#568)
* fix(邮件通知): 优化获取邮件附件,过大时的抛错国际化

* fix(邮件通知): 优化使用通用的国际化信息

* fix(邮件通知): 代码优化

---------

Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>
2024-09-03 18:32:39 +08:00
Zhang Ji 3ce520ebe6
fix(场景联动): 修复重启服务后,场景联动未初始化的问题 (#566) 2024-09-02 12:23:46 +08:00
PengyuDeng 412fbd6e2c
feat(权限管理): 分页获取用户详情,新增组织信息 (#560) 2024-09-02 09:33:12 +08:00
jk9991xx 594b2937d5
refactor: 移除重复代码
Co-authored-by: lmx <18570651508@wo.cn>
2024-09-02 09:32:15 +08:00
fighter-wang bf5ba69bcb
fix(场景联动): 修复场景联动使用指标值无法触发问题 (#562)
Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com>
2024-08-29 15:50:38 +08:00
zhouhao 845206fe76 feat: 优化topic解析 2024-08-29 12:02:32 +08:00
fighter-wang 99e15697db
fix(场景联动): 修复场景联动中设备属性翻译问题 (#561) 2024-08-28 16:39:53 +08:00
老周 39687d8302
build(maven): Release 2.2.0 (#557)
* build(maven): Release 2.2.0

* build(maven): 升级spring版本
2024-08-21 11:44:58 +08:00
liusq f754740646
feat(rule-engine): 告警场景2.2相关功能更新 (#552) 2024-08-21 10:58:03 +08:00
PengyuDeng f946c97ce5
refactor(基础模块): 优化菜单ID生成策略. (#556) 2024-08-19 17:17:58 +08:00
fighter-wang 65e5ac9046
fix(场景联动): 修复保存场景联动时缺少actionId以及features字段问题 (#549) 2024-08-07 10:17:57 +08:00
zhouhao c2ae9306a2 Merge remote-tracking branch 'origin/master' 2024-08-02 16:28:00 +08:00
zhouhao 7322bf1fe7 refactor(基础模块): 优化存储策略以及ID生成策略. 2024-08-02 16:27:49 +08:00
PengyuDeng a9b2754a41
feat(系统监控): 监控信息推送至消息总线。 (#544) 2024-07-19 15:00:19 +08:00
PengyuDeng 19604d8328
add(场景联动): 增加批量启动、禁用场景的接口 (#539) 2024-07-16 09:36:18 +08:00
老周 2dd0ef3c93
feat(基础模块): MqttClient设备会话支持可恢复. (#538) 2024-07-10 10:59:22 +08:00
PengyuDeng 8b3509442f
refactor(告警模块):告警级别信息增加拓展 (#536)
用户可能需要对告警级别所含信息进行拓展,如我司每个告警级别对应一个不同的播报音。播报音在前端配置,由浏览器读出。
2024-07-08 16:27:50 +08:00
PengyuDeng ed6540fbca
add(规则引擎-场景联动): 场景联动增加读取属性后回复触发场景联动的拓展 (#533)
需要结合前端一起拓展,前端的实现基于属性上报,把属性上报reportProperty修改为eadPropertyReply即可。
2024-07-08 10:12:07 +08:00
fighter-wang a4b48c7ec5
fix(消息通知管理): 修复订阅管理中订阅配置关闭后,查看个人中心依然展示订阅配置及通道问题 (#534) 2024-07-05 17:12:38 +08:00
PengyuDeng 1ad1e448ee
refactor(基础模块): 优化es索引配置,增加拓展性。 (#526) 2024-06-24 10:34:59 +08:00
fighter-wang fee733672c
fix(消息通知模块): 修复场景联动设备告警短信通知无法发送问题 (#525) 2024-06-21 16:01:55 +08:00
fighter-wang fdd8f4004c
fix(设备模块): 新增网关设备批量解绑网关子设备功能 (#522) 2024-06-19 09:44:01 +08:00
bestfeng1020 8515c9b385
fix(告警记录): 添加通过告警配置Id查询告警日志接口 (#520) 2024-06-17 12:40:45 +08:00
zhouhao 7e24f3d006 refactor: 优化PersistenceBuffer逻辑 2024-06-11 11:05:41 +08:00
zhouhao af11c55ad9 refactor: 优化tcp判断 2024-05-31 10:18:18 +08:00
zhouhao 0bdc14668e Merge remote-tracking branch 'origin/master' 2024-05-30 18:25:40 +08:00
zhouhao 173680bd92 refactor: ThingsDataManager增加标签实现 2024-05-30 18:25:26 +08:00
Zhang Ji 76d1b07210
fix(TDengine): 修复like条件语法错误 (#514) 2024-05-30 10:02:59 +08:00
zhouhao 3d722235c0 refactor: 优化系统配置逻辑 2024-05-27 17:27:11 +08:00
Zhang Ji 53a5d0c8e5
fix(网络组件): 修复关闭mqtt网关禁用逻辑错误 (#512) 2024-05-24 16:44:56 +08:00
tancong 2b8f571e9a
feat(规则引擎): 增加场景分支executeAnyway配置.优化场景条件分支逻辑. (#511) 2024-05-23 17:12:50 +08:00
zhouhao 86b073b389 refactor: 统一bouncycastle版本 2024-05-21 17:42:57 +08:00
zhouhao a0c17312cc Merge branch 'refactor-session-duration' 2024-05-21 16:50:34 +08:00
zhouhao 39eb4eb47d Merge branch 'master' of github.com:jetlinks/jetlinks-community 2024-05-21 16:50:26 +08:00
zhouhao a819096019 refactor: 优化索引模版创建逻辑 2024-05-21 09:45:44 +08:00
老周 dcc43cb47a
refactor: 修改设备会话统计时长类型为long (#510) 2024-05-14 10:45:28 +08:00
zhouhao 3d7d3efc86 refactor: 修改设备会话统计时长类型为long 2024-05-14 10:41:43 +08:00
fighter-wang 8d7d900289
fix(短信通知模块): 修复产品/设备发生告警,短信通知内置函数未进行格式化问题 (#507) 2024-05-09 19:23:38 +08:00
fighter-wang ff0f7ddfa2
fix(场景联动): 修复场景联动触发条件,当选择为事件时,json参数为enum,没有enum枚举选项问题 (#503) 2024-05-06 17:44:31 +08:00
dependabot[bot] 884d7ea34f
build(deps): bump org.bouncycastle:bcprov-jdk18on from 1.76 to 1.78 (#501)
* build(deps): bump org.bouncycastle:bcprov-jdk18on from 1.76 to 1.78

Bumps [org.bouncycastle:bcprov-jdk18on](https://github.com/bcgit/bc-java) from 1.76 to 1.78.
- [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html)
- [Commits](https://github.com/bcgit/bc-java/commits)

---
updated-dependencies:
- dependency-name: org.bouncycastle:bcprov-jdk18on
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update pom.xml

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: 老周 <zh.sqy@qq.com>
2024-05-06 17:44:07 +08:00
fighter-wang 16599ba067
fix(消息通知): 修复产品类型告警,展示产品ID 转换为 展示产品名称 (#499) 2024-04-24 14:01:15 +08:00
zhouhao 9157c88024 refactor: 优化子设备注销消息逻辑 2024-04-19 11:34:13 +08:00
zhouhao a83ba95124 refactor: 优化nashorn支持 2024-04-19 11:33:23 +08:00
zhouhao 05abda43c7 fix: 修复标签条件获取错误问题 2024-04-18 14:07:10 +08:00
zhouhao 49345c9062 fix: 修复无法访问文件问题 2024-04-07 16:16:01 +08:00
老周 16fee41d89
refactor: 优化文件地址逻辑 (#493)
* refactor: 优化文件地址逻辑
2024-04-07 15:01:18 +08:00
老周 b61b0e3568
refactor: 使用新的协议加载逻辑. (#491)
使用eventbus来传递协议变更事件
2024-04-07 15:00:19 +08:00
zhouhao cf2c781414 fix: 修复2字节时小端模式失效问题 2024-03-28 15:12:22 +08:00
bestfeng1020 0f874dd4fb
修改付费支持价格单位 (#485) 2024-03-27 16:37:13 +08:00
老周 a6281edb78
refactor(基础模块): 使用FileManager来存储静态文件 (#484) 2024-03-27 15:37:47 +08:00
zhouhao 0074468c29 fix(基础模块): 修复es查询返回array类型数据转换错误问题. 2024-03-12 18:02:31 +08:00
zhouhao 121c065d27 refactor: 优化设备上线离线消息时间戳逻辑 2024-03-12 12:32:33 +08:00
zhouhao 330930c2e1 refactor: 优化聚合查询逻辑 2024-03-12 10:02:40 +08:00
zhouhao cfc4f877a7 refactor: 优化Setting保存逻辑 2024-02-26 10:56:04 +08:00
zhouhao 3e11d6500d refactor: 移除无用依赖 2024-01-31 10:05:14 +08:00
zhouhao b77cdcb606 refactor: 优化设备接入网关,支持decodeContext.handleMessage 2024-01-25 16:34:33 +08:00
zhouhao 3e8c477b1b Merge remote-tracking branch 'origin/master' 2024-01-11 17:22:09 +08:00
zhouhao 28d323a2f8 refactor: 优化设备注销逻辑 2024-01-11 17:21:55 +08:00
老周 828f2743e6
Update README.md 2024-01-08 11:03:59 +08:00
zhouhao 270583033a Merge remote-tracking branch 'origin/master' 2023-12-28 15:58:10 +08:00
zhouhao 0b6c8451d3 refactor: 优化设备数据查询 2023-12-28 15:57:46 +08:00
PengyuDeng 454fa46df1
add(文件管理): 增加删除文件接口 (#466)
---------

Co-authored-by: 老周 <zh.sqy@qq.com>
2023-12-27 16:32:15 +08:00
zhouhao 73fa7607e9 Merge remote-tracking branch 'origin/master' 2023-12-27 10:06:23 +08:00
zhouhao a7114c6591 refactor: 优化包含字符串的处理逻辑 2023-12-27 10:06:09 +08:00
fighter-wang 46360dfa66
fix(设备管理): 修复数据查询设备指定属性列表数据不全问题 (#463) 2023-12-26 13:59:29 +08:00
PengyuDeng 9b87ed740c
fix: 配置文件增加接口扫描路径 (#464) 2023-12-26 13:59:06 +08:00
zhouhao a20814b68d refactor: 初始化数据时不触发crud事件 2023-12-21 10:13:10 +08:00
zhouhao 27ff0510a6 refactor: 优化文件导入 2023-12-20 14:58:58 +08:00
老周 7f028885fe
fix(基础模块): 修复在极端情况下物属性缓存可能出现污染. (#458) 2023-12-18 17:48:22 +08:00
tancong a780869dd3
fix(订阅模块): 其他用户无法订阅消息修复 (#455) 2023-12-12 17:39:32 +08:00
liusq 51f7afc9e1
feat(文档): 更新项目结构说明 (#453) 2023-12-06 18:24:38 +08:00
liusq 4cb4422537
feat(角色分组): 社区版移植角色分组功能 (#451) 2023-12-01 13:36:53 +08:00
ningqingsheng a5b3039be9
feat: 文件管理api文档补充 (#447) 2023-11-30 10:28:09 +08:00
zhouhao eb2dabeebb Merge remote-tracking branch 'origin/master' 2023-11-28 17:02:44 +08:00
zhouhao acf38bc7e1 refactor: 优化规则日志记录逻辑 2023-11-28 17:02:34 +08:00
dependabot[bot] 6203f481d9
build(deps): bump org.elasticsearch:elasticsearch (#446)
Bumps [org.elasticsearch:elasticsearch](https://github.com/elastic/elasticsearch) from 7.17.13 to 7.17.14.
- [Release notes](https://github.com/elastic/elasticsearch/releases)
- [Changelog](https://github.com/elastic/elasticsearch/blob/main/CHANGELOG.md)
- [Commits](https://github.com/elastic/elasticsearch/compare/v7.17.13...v7.17.14)

---
updated-dependencies:
- dependency-name: org.elasticsearch:elasticsearch
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-23 10:02:29 +08:00
tancong af02ab4fb5
fix(用户): 超级管理员初始化类型修改 (#443) 2023-11-16 10:13:31 +08:00
tancong 47975b0da2
feat(告警模块): 其他类型的告警,每个场景联动产生一条告警记录。告警日志添加告警说明 (#441) 2023-11-13 19:03:40 +08:00
tancong d9603381e0
feat(用户模块): 添加用户类型查询 (#438) 2023-11-08 10:53:33 +08:00
zhouhao 4f1c89ccc2 Merge remote-tracking branch 'origin/master' 2023-11-06 17:32:43 +08:00
zhouhao f0a59a6e07 refactor: 优化MQTT设备接入网关 2023-11-06 17:32:31 +08:00
ningqingsheng b078d88f82
fix: 修复并完善ReactorUtils.limit()方法 (#436) 2023-11-03 11:31:16 +08:00
zhouhao efad62a75b refactor: 优化设备接入网关 2023-11-02 14:16:30 +08:00
zhouhao edd1b9aba2 Merge remote-tracking branch 'origin/master' 2023-11-01 09:26:47 +08:00
zhouhao 865b0ec8ec refactor: 优化通知模版处理逻辑 2023-11-01 09:26:37 +08:00
dependabot[bot] a1e88bc608
build(deps): bump org.json:json from 20230227 to 20231013 (#435)
Bumps [org.json:json](https://github.com/douglascrockford/JSON-java) from 20230227 to 20231013.
- [Release notes](https://github.com/douglascrockford/JSON-java/releases)
- [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md)
- [Commits](https://github.com/douglascrockford/JSON-java/commits)

---
updated-dependencies:
- dependency-name: org.json:json
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-31 17:05:07 +08:00
老周 2814fac218
fix: 修复在设备里单独定义物模型时,订阅的数据格式不对问题. (#432)
* fix: 修复在设备里单独定义物模型时,订阅的数据格式不对问题.

* fix: 修复编译错误
2023-10-31 16:03:14 +08:00
dependabot[bot] 827dc1ef6b
build(deps): bump org.elasticsearch:elasticsearch from 7.17.5 to 7.17.13 (#434)
Bumps [org.elasticsearch:elasticsearch](https://github.com/elastic/elasticsearch) from 7.17.5 to 7.17.13.
- [Release notes](https://github.com/elastic/elasticsearch/releases)
- [Changelog](https://github.com/elastic/elasticsearch/blob/main/CHANGELOG.md)
- [Commits](https://github.com/elastic/elasticsearch/compare/v7.17.5...v7.17.13)

---
updated-dependencies:
- dependency-name: org.elasticsearch:elasticsearch
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-31 16:02:49 +08:00
老周 a46c4fd07c
fix: 修复阿里云语音通知可能提示参数过长问题 (#433) 2023-10-30 10:06:46 +08:00
Zhang Ji 807f1629ca
fix(设备消息): 修复设备消息订阅报错 (#431)
PropertyMessage类型没有默认的解码实现,已改为订阅ThingMessage
2023-10-26 14:08:30 +08:00
zhouhao 7746a59b03 fix: 修复启动报错 2023-10-20 12:38:58 +08:00
老周 98861ac1ed
refactor: 使用新的eventbus实现,增加相关订阅优先级支持. (#430) 2023-10-20 11:37:21 +08:00
老周 00997d9ee2
fix: 修复ThingsDataManager获取属性缓存数据可能错误. (#429) 2023-10-20 09:45:20 +08:00
老周 8b4ae09cf5
refactor: 设备接入网关同一个连接上报的消息使用串行处理. (#427) 2023-10-18 22:12:17 -05:00
bestfeng1020 cb17ae9d8b
定价修改 (#426)
修改硬件支持定价为699
2023-10-17 06:10:36 -05:00
zhouhao 9ef7f41aa9 revert: 回退reactor版本到 2020.0.31 2023-10-16 17:39:25 +08:00
老周 0fe35ae1ec
build:使用bcprov-jdk18on替代bcprov-jdk15on (#424) 2023-10-06 20:55:01 -05:00
zhouhao 19f90e3cef refactor: 设置默认externalHost为127.0.0.1 2023-09-26 10:12:49 +08:00
zhouhao 448b132c56 Merge remote-tracking branch 'origin/master' 2023-09-19 15:31:19 +08:00
zhouhao 1cfecd50ec build: 统一bouncycastle版本 2023-09-19 15:31:07 +08:00
老周 5e849c882c
build: 升级依赖版本 (#422) 2023-09-19 02:28:38 -05:00
bestfeng1020 f79ca77c9d
fix(ES查询): 解决动态terms条件转ES查询条件错误问题 (#421) 2023-09-13 14:49:53 +08:00
bestfeng1020 fdc0cbd7e0
feat(联系方式): 添加qq群5联系方式 (#418)
* feat(联系方式): 添加qq群5联系方式

* Update README.md

---------

Co-authored-by: 老周 <zh.sqy@qq.com>
2023-08-31 10:25:40 +08:00
老周 fcfbeaa373
build: 升级guava版本到32.1.2-jre (#416) 2023-08-29 09:22:52 +08:00
bestfeng1020 6abafda194
fix(docker): 规范社区办docker镜像命名 (#415)
* fix(docker): 规范社区办docker镜像命名

* fix(docker): 规范社区办docker镜像命名
2023-08-29 09:20:31 +08:00
zhouhao 4de7de6148 build: 优化.editorconfig配置内容 2023-08-28 14:45:09 +08:00
老周 b0027125ea
feat: 切换新的DeviceDataManager实现,使用ThingsDataManager桥接实现。 (#412) 2023-08-18 11:50:11 +08:00
老周 d972bde5c6
feat: 透传数据解析脚本中增加json()和jsonArray() api. (#410) 2023-08-18 10:36:33 +08:00
老周 3420f36110
fix: 修复透传解析脚本无法使用onDownstream,onUpstream函数注册回调. (#409) 2023-08-18 09:44:22 +08:00
老周 f84621178b
fix: 修复开启链路追踪后可能报错问题 (#407) 2023-08-17 16:10:52 +08:00
老周 7d750a1e0c
build: 优化maven子模块的relativePath配置 (#406) 2023-08-17 16:10:35 +08:00
老周 85b35bc320
build: 升级 jetlinks core,easyorm,hsweb 依赖版本 (#405)
jetlinks.version:1.2.2-SNAPSHOT
easyorm.version:4.1.2-SNAPSHOT
hsweb.framework.version: 4.0.17-SNAPSHOT
2023-08-17 14:32:16 +08:00
tancong 0ec335c2d9
feat(tcp解析): 粘拆包脚本解析器相关补全提示 (#404) 2023-08-16 13:47:21 +08:00
tancong 20276d0c19
refactor(设备模块): 添加指标查询和保存指标功能 (#403) 2023-08-09 11:57:58 +08:00
老周 7ffcf4bc4b
build: 升级依赖 reactor-excel:1.0.6-SNAPSHOT (#402) 2023-08-08 16:58:11 +08:00
tancong 118065b15c
fix(场景联动): 解决指标场景告警不触发问题 (#398) 2023-08-08 10:50:17 +08:00
zhouhao 276ffdf100 build: 开启下一个版本 2.2.0-SNAPSHOT 2023-08-07 14:03:58 +08:00
PengyuDeng dacee03833
doc(告警模块):修改有歧义的字段描述(#396) 2023-08-07 09:21:58 +08:00
tancong 25da12e83c
fix(场景联动): 解决并行场景告警不触发问题 (#394) 2023-08-04 21:00:26 +08:00
zhouhao 66eee1bf26 Merge remote-tracking branch 'origin/master' 2023-08-04 18:42:42 +08:00
zhouhao ec53681151 build: 升级maven依赖版本 2023-08-04 18:42:30 +08:00
tancong 21646b4024
fix(设备模块): 解决tag枚举类型设置无参数问题 (#393) 2023-08-04 15:48:19 +08:00
老周 e0f980aed9
Update maven-wrapper.properties 2023-08-04 15:46:30 +08:00
zhouhao ab7e603706 fix(规则引擎): 修复mysql下索引长度错误问题 2023-08-03 14:17:06 +08:00
zhouhao 83a6f39ea0 feat(基础模块): 增加更高效的对象属性操作器 2023-08-02 10:40:51 +08:00
zhouhao 92ba89714f refactor(规则引擎): 优化定时任务 2023-08-02 10:33:10 +08:00
tancong b1319225e8
fix(设备模块): 解决修改设备物模型后,设备物模型脱离产品物模型问题 (#387)
* fix(设备模块): 解决修改设备物模型后,设备物模型脱离产品物模型问题
2023-08-02 09:20:24 +08:00
tancong 943d0739da
fix(设备模块): 解决设备tag没有返回dataType字段 (#389) 2023-08-01 16:32:44 +08:00
老周 714b2cadd2
build: 修改nexus私服地址 2023-08-01 16:15:22 +08:00
zhouhao aa9e2423c5 Merge remote-tracking branch 'origin/master' 2023-08-01 09:19:45 +08:00
zhouhao 5134b955d8 fix(设备管理): 修复ts文件缺失问题 2023-08-01 09:19:33 +08:00
bestfeng1020 e8a79edca0
feat(readme): DTU接入平台的视频文档说明 (#386) 2023-07-31 12:11:33 +08:00
bestfeng1020 054d42ef06
fix(系统配置): 优化base-path请求验证超时提示 (#383)
* fix(系统配置): 优化base-path请求验证超时提示

* fix(系统配置): 优化base-path请求验证超时提示

* fix(系统配置): 优化国际化提示格式
2023-07-31 11:17:45 +08:00
gyl 9f374dd7b8
fix(产品分类): 修复初始化失败 (#385) 2023-07-28 13:52:05 +08:00
tancong 33a8dbb692
fix: 重构场景联动,迁移指标函数 (#384)
* fix: 重构场景联动,迁移指标函数
2023-07-28 10:53:41 +08:00
zhouhao 71b4d7578a fix(设备管理): 修复创建设备时被填充无关的信息到configuration中 2023-07-27 13:44:32 +08:00
zhouhao d4e8a430f4 build(maven): 升级r2dbc-mysql 0.9.3 2023-07-27 10:59:35 +08:00
zhouhao 440345cf82 refactor(设备管理): 优化设备名称同步逻辑 2023-07-27 10:01:53 +08:00
Zhang Ji a9d1928f07
fix(通知管理): 修复收信人解析为空字符串导致无法发送的问题 (#381)
* feat(基础模块): 增加通用导入工具

* feat(设备): 导入设备数据,并提供日志下载

* feat(设备接入网关): 修改MQTT服务网关时,重新加载网络组件

* feat(设备接入网关): 修改MQTT服务网关时,重新加载网络组件

* fix(通知管理): 修复收信人解析为空字符串导致无法发送的问题
2023-07-26 10:12:58 +08:00
bestfeng1020 c10db4499e
fix(服务支持): 修改服务支持的联系二维码不图片分辨率的问题 (#380)
* fix(服务支持): 修改服务支持的联系二维码不图片分辨率的问题

* fix(服务支持): 修改服务支持的联系二维码不图片分辨率的问题
2023-07-26 10:10:34 +08:00
bestfeng1020 ba1d08cef6
Merge pull request #376 from tancongsir/move-notify-manager
feat(通知模块): 重构用户个人通知订阅
2023-07-24 16:11:54 +08:00
tancongsir ccc8ea41c2 feat(通知模块): 重构用户个人通知订阅 2023-07-24 16:09:32 +08:00
tancongsir d0484545c4 feat(订阅模块): 企业版订阅模块迁移 2023-07-24 14:48:02 +08:00
bestfeng1020 045c5f6848
fix(服务支持): 修改服务支持的联系二维码不显示的问题 (#374) 2023-07-24 11:25:35 +08:00
bestfeng1020 c64f2421b1
feat(服务支持): 添加付费服务支持联系二维码 (#370) 2023-07-21 18:09:17 +08:00
bestfeng1020 c15a1fd7fe
feat(服务支持): 添加JetLinks服务器支持说明 (#369) 2023-07-21 13:22:12 +08:00
tancong 679ceb6858
fix(认证模块): 修复更新不存在的角色可能报错问题 (#368) 2023-07-20 18:52:05 +08:00
zhouhao 142c720f39 feat(基础模块): 增加脚本默认允许访问的类 2023-07-19 10:15:05 +08:00
zhouhao 2bf841c408 feat(设备管理): 增加查询列式存储时全部属性的API 2023-07-19 10:14:44 +08:00
tancong 69282edfce
fix(设备管理): 删除设备后,解绑子设备 (#365) 2023-07-18 19:37:25 +08:00
tancong 85f3e65fdb
fix(设备管理): 修复设备导入空指针异常 (#362) 2023-07-18 14:14:21 +08:00
tancong 0d8b175a15
refactor(认证模块): 加密key校验 (#364) 2023-07-18 14:09:49 +08:00
tancong ae7f083e95
fix(告警中心): 新增告警配置时默认启用 (#358) 2023-07-18 10:45:36 +08:00
tancong a26474a243
perf(设备接入网关): 设备接入网关文案(中文)修改 (#355) 2023-07-18 10:45:07 +08:00
tancong 333c95bdb3
refactor(基础模块): 优化excel导入数字类型格式错误提示 (#356) 2023-07-18 10:31:08 +08:00
tancong 998616fe37
fix(设备管理): 优化设备导入校验 (#354) 2023-07-18 10:29:56 +08:00
tancong 3b0871dab2
doc(基础模块): 修复文档说明错误 (#359) 2023-07-18 10:25:50 +08:00
tancong db16c2009f
fix(通知): 修复获取企业微信部门可能错误问题 (#351) 2023-07-18 10:25:19 +08:00
Zhang Ji a90d180fa4
feat(设备接入网关): 修改MQTT服务网关时,重新加载网络组件 (#336)
* feat(基础模块): 增加通用导入工具

* feat(设备): 导入设备数据,并提供日志下载

* feat(设备接入网关): 修改MQTT服务网关时,重新加载网络组件
2023-07-11 16:30:48 +08:00
bestfeng1020 5271799511
fix(场景联动):修复设备选择器条件会无限叠加问题 (#334) 2023-07-11 10:20:13 +08:00
bestfeng1020 98508e5a62
fix(文档):修复文案跳转链接错误 (#332) 2023-07-06 15:11:09 +08:00
dependabot[bot] e442009ce6
build(deps): bump grpc-protobuf (#331)
Bumps [grpc-protobuf](https://github.com/grpc/grpc-java) from 1.47.0 to 1.53.0.
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.47.0...v1.53.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-protobuf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-06 09:26:49 +08:00
bestfeng1020 7dafe5c0b8
fix(系统配置):解决base-path校验可能失效问题 (#330) 2023-07-05 18:23:21 +08:00
本宫在,尔等都是妃 4b5fa4a0dc
fix(通知管理): 修复邮件收件方不显示自定义的发件人昵称 (#327) 2023-07-03 13:47:18 +08:00
老周 6259181f26
Update maven-wrapper.properties 2023-06-30 11:44:11 +08:00
Zhang Ji 563f9a328e
feat(设备): 导入设备数据,并提供日志下载 (#326)
* feat(基础模块): 增加通用导入工具

* feat(设备): 导入设备数据,并提供日志下载
2023-06-30 11:43:38 +08:00
zhouhao 81a5f8999a feat(基础模块): 优化链路追踪 2023-06-28 09:40:30 +08:00
zhouhao 2756a31a35 feat(基础模块): 优化设备指令下发变量获取逻辑 2023-06-26 19:39:44 +08:00
zhouhao f7e12a8306 Merge remote-tracking branch 'origin/master' 2023-06-26 11:45:05 +08:00
zhouhao d45adde412 feat(认证模块): 增加登录时密码加密以及限制密码错误次数 2023-06-26 11:44:52 +08:00
bestfeng1020 3df3b90bd9
fix(系统配置):解决批量保存系统配置可能导致的mysql死锁问题 (#324) 2023-06-26 10:28:59 +08:00
dependabot[bot] d6aa72bee9
Bump snakeyaml from 1.32 to 2.0 (#253)
Bumps [snakeyaml](https://bitbucket.org/snakeyaml/snakeyaml) from 1.32 to 2.0.
- [Commits](https://bitbucket.org/snakeyaml/snakeyaml/branches/compare/snakeyaml-2.0..snakeyaml-1.32)

---
updated-dependencies:
- dependency-name: org.yaml:snakeyaml
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-25 15:40:17 +08:00
老周 6009a8a233
Update maven-wrapper.properties 2023-06-15 17:53:02 +08:00
dependabot[bot] f28098e3c1
build(deps): bump guava from 31.0.1-jre to 32.0.0-jre (#320)
Bumps [guava](https://github.com/google/guava) from 31.0.1-jre to 32.0.0-jre.
- [Release notes](https://github.com/google/guava/releases)
- [Commits](https://github.com/google/guava/commits)

---
updated-dependencies:
- dependency-name: com.google.guava:guava
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-15 17:50:49 +08:00
zhouhao 122750aa93 feat(协议管理): 优化协议加载错误提示 2023-06-13 14:54:33 +08:00
zhouhao 52a35c353a Merge remote-tracking branch 'origin/master'
# Conflicts:
#	jetlinks-components/common-component/src/main/resources/i18n/common-component/messages_en.properties
#	jetlinks-components/common-component/src/main/resources/i18n/common-component/messages_zh.properties
2023-06-13 13:37:40 +08:00
zhouhao 509afa40df feat(基础模块): 增加jvm异常处理 2023-06-13 13:37:13 +08:00
zhouhao d7c0806b84 fix: buffer size error 2023-06-10 16:48:23 +08:00
bestfeng1020 db1df5e121
feat(系统配置): base-path值正确性校验 (#318) 2023-06-09 19:27:22 +08:00
zhouhao 8b85aa305d feat(基础模块): 移除内存判断 2023-06-08 12:40:55 +08:00
zhouhao d20e705a21 feat(基础模块): 设备会话增加通过配置文件进行相关配置 2023-06-07 15:50:19 +08:00
Zhang Ji c4cb274569
fix(场景联动): 设备触发添加所属产品作为条件 (#314)
---------

Co-authored-by: zhou-hao <zh.sqy@qq.com>
2023-06-07 11:09:15 +08:00
zhouhao 1355f5209f Merge remote-tracking branch 'origin/master' 2023-06-07 10:29:12 +08:00
zhouhao 09636925a9 feat(基础模块): 优化脚本中的JSON支持 2023-06-07 10:29:02 +08:00
Zhang Ji 65ccee4f6d
fix(关系): 优化固定值的判断 (#313)
* fix(阿里云短信): 解决短信模板和标签只能查询第一页数据问题 (#258)

* feat(查询条件): 添加设备查询条件构造器 (#260)

* feat(仪表盘): 系统监控添加历史记录支持

* fix(关系): 优化固定值的判断

value为空时可能抛出异常

* fix(关系): 优化固定值的判断

value为空时可能抛出异常

* fix(通知): 还原版本

---------

Co-authored-by: zhou-hao <zh.sqy@qq.com>
2023-06-06 19:02:35 +08:00
bestfeng1020 5d739c43fb
fix(用户管理): 解决用户管理类型不存在问题 (#312) 2023-06-06 18:52:12 +08:00
zhangji 8fac4eab36 Revert "fix(关系): 优化固定值的判断"
This reverts commit 015df6d6
2023-06-06 17:37:19 +08:00
zhangji 015df6d6c9 fix(关系): 优化固定值的判断
value为空时可能抛出异常
2023-06-06 16:00:23 +08:00
bestfeng1020 8efe92cda0
fix(READEME): 修改产品文地址 (#307) 2023-06-02 17:44:54 +08:00
zhouhao ad66985fb1 feat(基础模块): 优化es查询条件值类型转换 2023-06-01 15:15:51 +08:00
bestfeng1020 ec66ae9ae7
fix(设备管理): 添加post方式的设备属性列表查询接口 (#302) 2023-06-01 15:15:17 +08:00
老周 3f01310a9d
doc: 修复注释错误 (#297) 2023-05-26 16:14:10 +08:00
老周 c7d1aac44c
Update README.md 2023-05-22 18:10:32 +08:00
zhouhao a7306ca921 Merge remote-tracking branch 'origin/master' 2023-05-19 14:34:51 +08:00
zhouhao 981351aaeb feat(网络组件): 增加模版配置,可通过jetlinks.network....设置网络组件的默认配置选项. 2023-05-19 14:34:35 +08:00
bestfeng1020 4c370c1924
feat(系统配置): base-path值正确性校验 (#286)
* feat(系统配置): base-path值正确性校验

* feat(系统配置): base-path值正确性校验

* feat(系统配置): base-path值正确性校验
2023-05-19 11:59:58 +08:00
Zhang Ji 015570066a
feat(仪表盘): 系统监控添加历史记录支持 (#284) 2023-05-16 19:02:32 +08:00
zhouhao 7f49a4e932 feat(maven): 升级依赖版本 2023-05-11 19:21:41 +08:00
zhouhao f8a3199138 fix(设备消息): 修复设备上线消息错误 2023-05-10 13:42:31 +08:00
zhouhao 3f3f06a8e6 fix(系统配置): 修复可能无法报错系统配置 2023-05-09 15:51:00 +08:00
zhouhao a2cd2688cb Merge remote-tracking branch 'origin/master' 2023-05-08 16:45:36 +08:00
zhouhao a9b906a0d5 fix(设备会话): 修复设置DeviceOnlineMessage header无效 2023-05-08 16:45:22 +08:00
老周 72362b2508
Update README.md 2023-05-05 19:16:32 +08:00
zeje edb692d1a4
优化重置设备配置信息 (#277)
Co-authored-by: 蔡泽智 <czz@eviewgps.com>
2023-05-05 12:02:22 +08:00
bestfeng1020 309e5a57dc
修复通过场景联动发送阿里云短信失败问题 (#279) 2023-05-04 16:46:09 +08:00
bestfeng1020 fc5f9a5a25
fix(docker镜像版本): 修改前端镜像版本 (#278) 2023-04-27 16:24:25 +08:00
bestfeng1020 6a2035fee6
feat(通知订阅): 支持告警消息站内信通知 (#274) 2023-04-21 20:20:37 +08:00
bestfeng1020 d51aa6ac1f
fix(网络组件): 设置支持路由设置类型的网络组件可以被复用 (#273) 2023-04-21 18:22:35 +08:00
zhouhao 7eb601b849 refactor(maven): 升级reactor 2020.0.31 2023-04-20 10:25:08 +08:00
zhouhao b89aa1138a refactor(maven): 升级依赖版本 2023-04-20 10:15:48 +08:00
zhouhao c145abb6b3 refactor(maven): 升级依赖版本 2023-04-20 10:15:30 +08:00
zhouhao 3f4297ed06 refactor(基础模块): 优化协议加载日志打印 2023-04-20 10:10:23 +08:00
zhouhao 32a3452f7b feat(maven): 2.1.0-SNAPSHOT 2023-04-20 10:09:58 +08:00
bestfeng1020 5fc41d5bf1
feat(产品): 根据指定的接入方式获取产品需要的配置定义 (#266) 2023-04-18 15:52:30 +08:00
dependabot[bot] dc4b582fe7
Bump json from 20180130 to 20230227 (#264)
Bumps [json](https://github.com/douglascrockford/JSON-java) from 20180130 to 20230227.
- [Release notes](https://github.com/douglascrockford/JSON-java/releases)
- [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md)
- [Commits](https://github.com/douglascrockford/JSON-java/commits)

---
updated-dependencies:
- dependency-name: org.json:json
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-17 09:34:31 +08:00
zhouhao 037c790550 fix(TDEngine): 修复TDEngine数据库事件精度不是毫秒时无法按时间查询问题 2023-04-14 17:50:10 +08:00
zhouhao cf52a6ce68 Merge remote-tracking branch 'origin/master' 2023-04-13 16:58:16 +08:00
zhouhao 9a3d67e3fb refactor(配置): 本地地址使用127.0.0.1 2023-04-13 16:58:04 +08:00
bestfeng1020 b0d7b65148
feat(查询条件): 添加设备查询条件构造器 (#259) 2023-04-07 17:22:05 +08:00
bestfeng1020 e6e400b365
fix(阿里云短信): 解决短信模板和标签只能查询第一页数据问题 (#257) 2023-04-07 15:55:26 +08:00
zhouhao cb6ced51b5 Merge remote-tracking branch 'origin/master' 2023-04-07 10:35:36 +08:00
zhouhao 887bda8ebf feat(设备管理): 透传消息解析支持子设备会话创建 2023-04-07 10:35:23 +08:00
ayan 0ce219c04f fix(菜单管理)添加菜单ID为空时的默认值 2023-03-30 16:17:13 +08:00
zhouhao 8957c8b386 feat(日志): 访问日志增加过滤支持 2023-03-27 10:26:45 +08:00
zhouhao 4a421c980f feat(监控): 优化micrometer初始化逻辑,增加ignore配置 2023-03-27 10:19:46 +08:00
zhouhao 547e495d56 feat(设备管理): 增加产品保存时自动同步相关信息到设备表 2023-03-24 16:19:39 +08:00
zhouhao dd2ea79640 refactor(基础模块): 移除无用代码 2023-03-23 14:32:47 +08:00
zhouhao 67901c3732 feat(maven): Update new r2dbc-mysql driver #mirromutth/r2dbc-mysql/issues/251 2023-03-23 14:32:28 +08:00
zhouhao 552dfd550b fix(基础模块): 修复js脚本中无法使用console问题 2023-03-14 16:51:09 +08:00
zhouhao d0208967f0 fix(设备管理): 导入设备时,忽略空字符串的配置 2023-03-09 11:12:04 +08:00
zhouhao 3fc1b11e29 fix(设备管理): 修复注销产品后设备中心中的缓存未清理问题 2023-03-08 14:20:45 +08:00
zhouhao a6503b49e9 feat(TDEngine): 优化错误处理,查询时找不到表时不抛出错误。 2023-03-06 17:40:10 +08:00
zhouhao 6a186d6eba feat(设备管理): 优化设备详情的标签排序逻辑 2023-03-02 09:46:07 +08:00
zhouhao 703012c2c3 Merge remote-tracking branch 'origin/master' 2023-02-28 15:55:33 +08:00
zhouhao 0efeff2dd7 feat(repo): add build profile 2023-02-28 15:55:16 +08:00
bestfeng1020 d2262e278a
fix(接口缺失): 添加菜单和权限数据验证接口 (#245) 2023-02-27 15:08:40 +08:00
bestfeng1020 1e58dbb944
添加透传消息转换支持 (#237) 2023-02-15 13:52:10 +08:00
zhouhao 57211334c3 RUN true 2023-02-13 12:38:29 +08:00
zhouhao 6640f1bdf0 优化 2023-02-13 12:01:31 +08:00
zhouhao 2076a69a56 优化 2023-02-10 13:59:03 +08:00
zhouhao aaa6889b8d Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2023-02-10 13:57:11 +08:00
zhouhao 8172baf52a Merge branch '2.0'
# Conflicts:
#	jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/VertxMqttClient.java
#	jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/MqttServerDeviceGateway.java
#	jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/server/vertx/VertxMqttConnection.java
#	jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/server/vertx/VertxMqttServerProvider.java
#	jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/client/VertxTcpClient.java
#	jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/server/TcpServerProvider.java
#	jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/message/DeviceMessageMeasurement.java
#	jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceMessageBusinessHandler.java
#	pom.xml
2023-02-10 13:51:42 +08:00
zhouhao bf6e75876b 2.0 2023-02-10 13:50:53 +08:00
zhouhao 8a0728c566 升级依赖 2023-01-18 11:54:13 +08:00
zhouhao c1198ebdfe 优化http接入,增加websocket支持 2023-01-10 17:24:26 +08:00
zhouhao a0e1b6bad6 修复查询事件条件错误 2023-01-10 15:12:09 +08:00
zhouhao 661281b6ff Merge remote-tracking branch 'origin/2.0' into 2.0 2023-01-10 13:34:58 +08:00
zhouhao a77f5dbbc6 fixed #232 2023-01-10 13:34:44 +08:00
Zhang Ji 599ca456f3
更新README (#231) 2023-01-10 11:43:12 +08:00
Zhang Ji 203f97afc9
同步README文档链接 (#230) 2023-01-10 11:42:44 +08:00
zhouhao d76b599ddd 修复并行只执行第一个动作问题 2023-01-08 10:01:06 +08:00
zhouhao b32d2fddbc 增加长度字段粘拆包解析规则 2023-01-06 13:51:38 +08:00
zhouhao b81f0b176d 修复状态类型错误 2023-01-04 15:56:20 +08:00
ayan 29bc67e3a4 启动服务时自动启动场景 2023-01-04 14:32:04 +08:00
ayan 4701ac0c29 修改代码 @see 的包路径 2023-01-04 14:00:52 +08:00
ayan 778e06134b LocalWorker 2022-12-29 17:02:35 +08:00
zhouhao da4b53846e ClusterWorker 2022-12-29 15:46:14 +08:00
ayan 7004a92f57 Merge remote-tracking branch 'origin/2.0' into 2.0 2022-12-29 15:16:30 +08:00
ayan b6fbccacb1 更新官方jar文件 2022-12-29 15:16:14 +08:00
bestfeng1020 8cb5ebcd06
重构场景联动 (#227)
* 重构场景联动
2022-12-29 11:44:31 +08:00
老周 2c6b85e8af
Update README.md 2022-12-23 17:10:36 +08:00
zhouhao ecae631bd9 优化mqtt5支持 2022-12-22 15:05:17 +08:00
zhouhao 575d08c191 优化tcp 2022-12-22 15:05:11 +08:00
zhouhao b61f452d46 Merge remote-tracking branch 'origin/2.0' into 2.0 2022-12-13 18:05:55 +08:00
zhouhao 6c3d99ff0d 启动服务时自动启动场景 2022-12-13 18:05:40 +08:00
zhouhao d4c14be015 优化通知 2022-12-13 18:02:16 +08:00
zhouhao 9975d3ea00 优化webhook通知 2022-12-13 18:02:05 +08:00
zhouhao 4c1de801ac 优化邮件通知 2022-12-13 18:01:52 +08:00
zhouhao 6fd335c84a 移除无用字段 2022-12-12 14:13:41 +08:00
zhouhao d24ba36dc0 调整类型 2022-12-12 14:11:08 +08:00
zhouhao 8cae9a395e 优化初始化顺序 2022-12-06 11:16:16 +08:00
zhouhao af364756d7 新版模拟器 2022-12-01 16:46:29 +08:00
zhouhao a0465f79b3 Merge remote-tracking branch 'origin/2.0' into 2.0 2022-12-01 09:59:34 +08:00
zhouhao 4831e89a13 优化日志配置 2022-12-01 09:59:22 +08:00
liujq 6607301e59 重构场景联动 2022-11-29 14:51:50 +08:00
liujq 1cd01b1701 协议包更新 2022-11-28 11:01:20 +08:00
liujq d1746f323f Merge remote-tracking branch 'origin/2.0' into 2.0 2022-11-28 09:53:08 +08:00
liujq 37cb0e51d4 协议包更新 2022-11-28 09:52:49 +08:00
zhouhao d3e0e524db 默认js 2022-11-24 21:18:32 +08:00
zhouhao 4c5ec7cfdf 优化脚本 2022-11-24 11:06:20 +08:00
zhouhao 07fa602ede 优化脚本功能 2022-11-24 10:55:28 +08:00
zhouhao ec010f0aef 增加缺失的接口 2022-11-18 14:43:35 +08:00
zhouhao d66f0dd513 修复依赖错误 2022-11-17 17:34:40 +08:00
zhouhao 5c238cd789 relativePath 2022-11-17 17:32:43 +08:00
zhouhao a7b7e31388 Merge remote-tracking branch 'origin/2.0' into 2.0 2022-11-17 11:36:55 +08:00
zhouhao 8ee311a9ef 增加tdengine 2022-11-17 11:36:28 +08:00
liujq 06ccfdfe31 修复docker启动配置文件 2022-11-16 18:06:17 +08:00
liujq 95ac6b7af3 支持local协议 2022-11-11 10:53:37 +08:00
liujq a219e6414e 支持local协议 2022-11-10 16:25:14 +08:00
zhouhao db89b37be8 Merge remote-tracking branch 'origin/2.0' into 2.0 2022-11-10 09:46:26 +08:00
zhouhao a96e189af1 fix password error 2022-11-10 09:45:58 +08:00
zhouhao ddb6a263df 优化条件处理 2022-11-08 11:02:44 +08:00
zhouhao 1e3d888c22 注册DeviceMessageSendTaskExecutorProvider 2022-11-08 11:02:02 +08:00
zhouhao 9b2b6d224f getLong 2022-11-07 10:28:53 +08:00
zhouhao 1cbe4638ed getLong 2022-11-07 10:25:39 +08:00
zhouhao 5339951174 修改es配置 2022-11-03 11:36:44 +08:00
zhouhao fbba69a28f 优化资源释放 2022-11-02 18:20:23 +08:00
zhouhao c67d7b99ce 优化资源释放 2022-11-02 18:13:30 +08:00
zhouhao 7540ab9cac 修复tcp可能内存泄漏 2022-11-02 16:58:12 +08:00
zhouhao a83af4ba9f 修复tcp可能内存泄漏 2022-11-02 16:57:42 +08:00
zhouhao b99b921c15 优化标签校验 2022-11-02 16:28:57 +08:00
zhouhao cc7e40b11d 优化标签校验 2022-11-02 16:28:37 +08:00
zhouhao 1a0d2637e6 统一依赖版本 2022-11-02 13:39:53 +08:00
zhouhao 6a4e1d9617 修复列式模式查询结果错误 2022-11-01 18:18:38 +08:00
zhouhao 812ecd26b3 增加自动刷新会话 2022-11-01 10:23:25 +08:00
zhouhao 1f36074090 增加自动刷新会话 2022-11-01 10:22:54 +08:00
zhouhao a8b0cf5df2 Merge remote-tracking branch 'origin/2.0' into 2.0 2022-10-31 10:11:02 +08:00
zhouhao 4c86f9d971 优化状态同步 2022-10-31 10:08:07 +08:00
zhouhao 1bc1ee0fdd 优化状态同步 2022-10-31 10:06:32 +08:00
ayan b6a3ec7061 Merge remote-tracking branch 'origin/2.0' into 2.0 2022-10-28 15:16:39 +08:00
ayan bea90951a9 添加设备调试websocket支持 2022-10-28 15:16:25 +08:00
zhouhao 77b5c876a3 优化存储 2022-10-28 11:30:37 +08:00
ayan acf84eacdd 更新官网协议包 2022-10-28 10:37:41 +08:00
ayan 99a25e39ec 修复设备详情中无网关ID返回问题 2022-10-28 10:28:31 +08:00
ayan f63d837654 Merge remote-tracking branch 'origin/2.0' into 2.0 2022-10-27 10:51:30 +08:00
ayan 92e181d0b3 解决mvn打包zip文件异常问题 2022-10-27 10:49:20 +08:00
zhouhao 6f2003ce16 Merge remote-tracking branch 'origin/2.0' into 2.0 2022-10-26 17:50:19 +08:00
zhouhao 6f4686c022 remove serializer 2022-10-26 17:50:07 +08:00
ayan 7f4d52e618 优化docker-compsoe配置 2022-10-26 17:23:21 +08:00
ayan af605d76bc Merge remote-tracking branch 'origin/2.0' into 2.0 2022-10-26 17:20:42 +08:00
ayan 47ebc48edb 增加默认协议添加接口 2022-10-26 17:20:26 +08:00
zhouhao 26d058d546 优化配置 2022-10-26 14:36:16 +08:00
zhouhao ec13e0cbab 禁用es过期日志 2022-10-26 11:51:04 +08:00
ayan 6ca937a3d9 删除错误的dev配置文件 2022-10-25 18:23:19 +08:00
bestfeng1020 88b94ac837
代码优化 (#212)
* 阿里云通知被叫号码支持空值

* 修改订阅状态显示文案

* docker启动配置文件更新
2022-10-25 14:46:10 +08:00
dependabot[bot] 3749e9e06d
Bump commons-text (#209)
Bumps commons-text from 1.9 to 1.10.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-text
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-24 17:23:24 +08:00
dependabot[bot] c939f28248
Bump commons-text from 1.9 to 1.10.0 (#210)
Bumps commons-text from 1.9 to 1.10.0.

---
updated-dependencies:
- dependency-name: org.apache.commons:commons-text
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-24 17:22:57 +08:00
老周 fdfa4e3e96
Update maven.yml 2022-10-24 17:19:19 +08:00
老周 c8d8926b0f
Update maven.yml 2022-10-24 17:17:59 +08:00
zhouhao 9e4baa2993 Merge remote-tracking branch 'origin/master' 2022-10-24 11:12:45 +08:00
zhouhao eda3a24a80 优化子设备会话处理 2022-10-24 11:12:32 +08:00
zhouhao cc0db07345 优化子设备会话处理 2022-10-24 11:11:42 +08:00
zhouhao 6dcb26c02a Merge remote-tracking branch 'origin/2.0' into 2.0 2022-10-24 11:06:06 +08:00
zhouhao ae3dbc5ddf 优化 2022-10-24 11:05:29 +08:00
bestfeng1020 f408d72159
同步2.0相关功能代码 (#206) 2022-10-21 19:06:52 +08:00
老周 1a25e693b9
Merge pull request #204 from jetlinks/fix-bug
同步协议模块代码
2022-10-18 18:22:29 +08:00
ayan 7d7a6f2d93 优化协议处理 2022-10-18 17:39:44 +08:00
ayan d5d3c0dd9d 产品分类支持 2022-10-18 17:26:52 +08:00
ayan 9795f7a11a 菜单身份验证初始化服务 2022-10-18 17:25:57 +08:00
ayan 84c6e48f0e 添加获取指定传输协议的消息协议接口 2022-10-18 17:24:38 +08:00
ayan 14d41cc706 Merge remote-tracking branch 'origin/2.0' into 2.0 2022-10-18 16:49:40 +08:00
ayan cc66d06503 添加默认开放的网络端口 2022-10-18 16:49:25 +08:00
老周 eb7f78c6d1
Merge pull request #202 from jetlinks/fix-bug
解决产品未选择网关时,查询产品配置抛出的NPE
2022-10-18 10:18:47 +08:00
ayan 00cb7ce8de 移除规则引擎控制器 2022-10-17 17:51:06 +08:00
ayan 3ddc3c7a6d 解决产品未选泽网关时,查询产品配置抛出的NPE 2022-10-17 14:52:53 +08:00
老周 0cc7378389
Merge pull request #200 from vvsd/oscs_fix_cd36lv0au51of7vbl1j0
fix(sec): upgrade org.bouncycastle:bcprov-jdk15on to 1.69
2022-10-12 17:54:41 +08:00
vvsd d5f8a6f7b0 update org.bouncycastle:bcprov-jdk15on 1.67 to 1.69 2022-10-12 15:21:39 +08:00
zhouhao 779eddd4c8 优化线程池逻辑 2022-10-09 17:05:09 +08:00
zhouhao 4c18ef264f 优化线程池逻辑 2022-10-09 13:53:07 +08:00
zhouhao 3cc59ec882 增加拓展查询条件信息 2022-09-28 17:58:00 +08:00
zhouhao c7f4b19ed1 2.0 2022-09-28 17:56:32 +08:00
zhouhao 4a9977903e 优化仓库地址 2022-09-28 10:03:52 +08:00
zhouhao 0490e3a6e2 优化依赖 2022-09-28 10:03:30 +08:00
zhouhao bd3bf2fa91 2.0 2022-09-26 17:40:16 +08:00
zhouhao 2c84fee332 优化配置 2022-09-26 17:38:29 +08:00
zhouhao 0242ce4188 优化配置 2022-09-26 17:28:55 +08:00
zhouhao 279e21a5d3 使用es实现物数据存储 2022-09-26 17:28:42 +08:00
zhouhao e0fe2a5960 使用物模块数据存储策略来进行设备数据存储 2022-09-26 17:28:18 +08:00
zhouhao b8adab7af3 增加通用物模块 2022-09-26 17:27:59 +08:00
zhouhao 8d2d26a28a 使用新的script api 2022-09-26 17:27:37 +08:00
zhouhao c2e39e9ca9 增加脚本模块 2022-09-26 17:25:35 +08:00
zhouhao 14cc4224ef 增加通用配置功能 2022-09-26 17:25:24 +08:00
zhouhao 4fcda29b7a 增加角色、机构管理,优化菜单管理 2022-09-26 17:25:07 +08:00
zhouhao 143095d440 优化子设备注册 2022-09-21 13:54:45 +08:00
zhouhao d5598e2c71 fix error 2022-09-21 11:08:26 +08:00
zhouhao 5bf47c59bd Merge remote-tracking branch 'origin/master' 2022-09-20 15:34:43 +08:00
zhouhao 0aac04ba10 优化es数据写入性能 2022-09-20 15:34:23 +08:00
老周 03fea2398e
Update README.md 2022-09-19 11:43:18 +08:00
zhouhao de028b763f ④群 2022-09-15 13:59:39 +08:00
zhouhao 74b2837435 使用parallel线程池处理数据 2022-09-15 11:06:00 +08:00
zhouhao 417a7ab4b0 移除弃用的类 2022-09-13 09:47:17 +08:00
zhouhao c671a1cb52 Merge remote-tracking branch 'origin/master' 2022-09-07 20:50:25 +08:00
zhouhao a8595c9aec 优化类型 2022-09-07 20:50:13 +08:00
老周 5da5107335
Merge pull request #197 from jetlinks/dependabot/maven/jetlinks-components/notify-component/notify-email/org.jsoup-jsoup-1.15.3
Bump jsoup from 1.14.3 to 1.15.3 in /jetlinks-components/notify-component/notify-email
2022-09-02 14:47:26 +08:00
dependabot[bot] ecdb1db3f0
Bump jsoup in /jetlinks-components/notify-component/notify-email
Bumps [jsoup](https://github.com/jhy/jsoup) from 1.14.3 to 1.15.3.
- [Release notes](https://github.com/jhy/jsoup/releases)
- [Changelog](https://github.com/jhy/jsoup/blob/master/CHANGES)
- [Commits](https://github.com/jhy/jsoup/compare/jsoup-1.14.3...jsoup-1.15.3)

---
updated-dependencies:
- dependency-name: org.jsoup:jsoup
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-01 23:48:20 +00:00
zhouhao 24a45f5b67 优化mqtt client topic处理 2022-08-19 13:38:56 +08:00
zhouhao a3d080d1ee RpcDeviceOperationBroker 2022-08-11 18:04:51 +08:00
zhouhao a00016018a 优化使用脚本作为粘拆包规则时的tcp性能 2022-08-05 19:05:01 +08:00
zhouhao 31299b2e28 增加api配置信息 2022-08-04 14:24:55 +08:00
zhouhao f31f71dc14 优化自动注册以及设备会话管理 2022-08-03 17:43:43 +08:00
zhouhao 80185b911a 调整状态同步逻辑 2022-08-03 14:07:14 +08:00
zhouhao a895e9e5d3 fix package error 2022-07-28 12:27:58 +08:00
zhouhao 90d5ea7872 优化文件管理 2022-07-27 10:14:27 +08:00
zhouhao 65e8988639 优化es索引管理 2022-07-14 14:08:29 +08:00
zhouhao cc992f8207 优化设备会话 2022-07-12 20:51:36 +08:00
zhouhao bc5f21fe20 优化设备会话 2022-07-12 19:40:09 +08:00
zhouhao 9daee536f9 优化设备会话 2022-07-12 19:31:48 +08:00
zhouhao 1b3dd4400a 优化dashboard 2022-07-08 17:02:35 +08:00
zhouhao 824e6241cd 增加设备会话统计指标 2022-07-08 17:02:15 +08:00
zhouhao def407d81d upgrade netty.version 2022-07-01 10:16:09 +08:00
zhouhao 7dcfabf503 优化告警逻辑 2022-07-01 09:21:39 +08:00
zhouhao dcad116f3b hsweb 4.0.15-SNAPSHOT 2022-06-27 18:17:55 +08:00
zhouhao 3fc16470b0 优化设备会话处理 2022-06-27 18:17:37 +08:00
zhouhao bc985539e2 优化MQTT 2022-06-27 15:40:37 +08:00
zhouhao 0a683684ed 优化设备会话处理 2022-06-27 15:36:39 +08:00
zhouhao 677e421848 ignore dev/ 2022-06-23 14:14:25 +08:00
zhouhao 6e97c2edcc add Badge 2022-06-22 18:08:07 +08:00
zhouhao edec71c78c Merge remote-tracking branch 'origin/1.13' 2022-06-20 18:16:56 +08:00
zhouhao 426394328b Merge branch '1.20'
# Conflicts:
#	jetlinks-standalone/pom.xml
#	pom.xml
2022-06-20 14:41:22 +08:00
zhouhao e5cb557787 优化说明 2022-06-20 14:38:51 +08:00
zhouhao a9594d28c4 优化设备会话处理 2022-06-20 14:35:29 +08:00
zhouhao 291cfec2c7 停止时压缩文件 2022-06-20 14:35:21 +08:00
zhouhao 2968cf9b4f 升级依赖 2022-06-20 14:32:20 +08:00
zhouhao cb3fd4989e use apache Base64 2022-06-20 09:49:09 +08:00
zhouhao 35947b3567 修复windows下文件名可能非法问题 2022-06-15 10:23:58 +08:00
zhouhao cabc095367 优化 2022-06-14 17:51:43 +08:00
zhouhao 5f871ebd13 优化 2022-06-14 17:09:41 +08:00
zhouhao ede32aa7fc Merge branch 'master' into 1.20
# Conflicts:
#	jetlinks-components/io-component/pom.xml
#	jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/file/FileManagerConfiguration.java
#	jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/file/FileProperties.java
#	jetlinks-components/io-component/src/main/java/org/jetlinks/community/io/file/web/FileManagerController.java
#	jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/parser/strateies/DelimitedPayloadParserBuilder.java
#	jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailNotifier.java
#	pom.xml
2022-06-14 17:07:28 +08:00
zhouhao 6760b7e817 增加文件管理 2022-06-13 14:45:37 +08:00
zhouhao 79e3db9266 增加文件管理 2022-06-13 14:45:34 +08:00
zhouhao aab9215293 优化 2022-05-24 17:22:40 +08:00
zhouhao 72e3011946 优化 2022-05-24 17:20:40 +08:00
zhouhao e41ce546dd fastjson 1.2.83 2022-05-24 15:04:30 +08:00
zhouhao 34cb265917 优化 2022-05-17 18:01:08 +08:00
zhouhao 890fe14b31 优化集群 2022-05-17 17:51:39 +08:00
zhouhao 33864d544c 协议包以数据库为准 2022-05-17 17:51:28 +08:00
zhouhao 47c14686c1 优化滚动查询 2022-05-11 15:09:33 +08:00
zhouhao fc412d652e Merge remote-tracking branch 'origin/1.20' into 1.20 2022-05-10 16:53:22 +08:00
zhouhao e35ee69588 优化在线数量统计 2022-05-10 16:53:08 +08:00
zhouhao cf8d1285eb 修复状态错误 2022-05-08 13:32:15 +08:00
zhouhao 30a996070b remove ReactiveHashCommands.java 2022-05-07 19:29:37 +08:00
zhouhao de9bd5b570 修改跨域 2022-05-07 14:16:07 +08:00
zhouhao 024c3d9649 1.20.0-SNAPSHOT 2022-05-07 14:15:09 +08:00
1718 changed files with 127636 additions and 22098 deletions

View File

@ -3,10 +3,318 @@ root = true
[*]
charset = utf-8
end_of_line = lf
[*.java]
indent_style = space
indent_size = tab
indent_style = space
insert_final_newline = false
max_line_length = 120
tab_width = 4
trim_trailing_whitespace = true
insert_final_newline = false
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

View File

@ -1,26 +1,27 @@
name: Auto Deploy Docker
on: [push]
on:
push:
branches: [ "master","2.0","2.1","2.2" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Cache Maven Repository
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ~/.m2
key: jetlinks-community-maven-repository
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 && cd jetlinks-standalone && docker build -t registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) .
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) .
- 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-standalone:$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
run: docker push registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-community:$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)

45
.github/workflows/maven_build_2.1x.yml vendored Normal file
View File

@ -0,0 +1,45 @@
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

@ -19,7 +19,7 @@ jobs:
with:
java-version: 1.8
- name: Cache Maven Repository
uses: actions/cache@v1
uses: actions/cache@v4.2.3
with:
path: ~/.m2
key: jetlinks-community-maven-repository

27
.github/workflows/pull_request_2.1x.yml vendored Normal file
View File

@ -0,0 +1,27 @@
# 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

5
.gitignore vendored
View File

@ -27,4 +27,7 @@ hs_err_pid*
docker/data
!device-simulator.jar
!demo-protocol-1.0.jar
application-local.yml
application-local.yml
dev/
.DS_Store
.java-version

View File

@ -1 +1 @@
distributionUrl=https://downloads.apache.org/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.zip
distributionUrl=https://archive.apache.org/dist/maven/maven-3/3.9.3/binaries/apache-maven-3.9.3-bin.zip

View File

@ -0,0 +1,11 @@
<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>

111
README.md
View File

@ -1,53 +1,66 @@
# JetLinks 物联网基础平台
![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.12--RELEASE-brightgreen)
![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)
![jetlinks](https://visitor-badge.glitch.me/badge?page_id=jetlinks)
[![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)
[![QQ①群2021514](https://img.shields.io/badge/QQ①群-2021514-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=LGf0OPQqvLGdJIZST3VTcypdVWhdfAOG&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⑥群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 基于Java8,Spring Boot 2.x,WebFlux,Netty,Vert.x,Reactor等开发,
JetLinks 2.1x 基于Java 17,Spring Boot 3.x,WebFlux,Netty,Vert.x,Reactor等开发,
是一个开箱即用,可二次开发的企业级物联网基础平台。平台实现了物联网相关的众多基础功能,
能帮助你快速建立物联网相关业务系统。
## 核心特性
支持统一物模型管理,多种设备,多种厂家,统一管理。
#### 开放源代码
统一设备连接管理,多协议适配(TCP,MQTT,UDP,CoAP,HTTP等),屏蔽网络编程复杂性,灵活接入不同厂家不同协议的设备
全部源代码开放,可自由拓展功能,不再受制于人.前后端分离,接口全开放
灵活的规则引擎,设备告警,消息通知,数据转发.
#### 部署简单
强大的ReactorQL引擎,使用SQL来处理实时数据.
最小化运行仅需要`java 17`,`redis`,`timescaledb`即可,无需部署大量中间件。
地理位置:统一管理地理位置信息,支持区域搜索.
#### 统一设备接入,海量设备管理
官方QQ: ①群 [2021514](https://qm.qq.com/cgi-bin/qm/qr?k=LGf0OPQqvLGdJIZST3VTcypdVWhdfAOG&jump_from=webapi)
, ②群 [324606263](https://qm.qq.com/cgi-bin/qm/qr?k=IMas2cH-TNsYxUcY8lRbsXqPnA2sGHYQ&jump_from=webapi)
TCP/UDP/MQTT/HTTP、TLS/DTLS、不同厂商、不同设备、不同报文、统一接入统一管理。
#### 规则引擎
灵活的规则模型配置,支持多种规则模型以及自定义规则模型. 设备告警,场景联动,均由统一的规则引擎管理。
#### 数据权限控制
灵活的非侵入数据权限控制。可实现菜单、按钮、数据三维维度的数据权限控制。可控制单条数据的操作权限。
## 技术栈
1. [Spring Boot 2.3.x](https://spring.io/projects/spring-boot)
1. [Spring Boot 3.4.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/) 响应式编程框架
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) 业务功能基础框架
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) 业务功能数据管理
## 架构
![platform](./platform.svg)
![platform](./platform.png)
## 设备接入流程
![flow](./flow.svg)
![device-flow](./device-flow.png)
## 模块
@ -55,15 +68,61 @@ JetLinks 基于Java8,Spring Boot 2.x,WebFlux,Netty,Vert.x,Reactor等开发,
--jetlinks-community
------|----docker
------|------|----dev-env # 启动开发环境
------|------|----run-all # 启动全部,通过http://localhost:9000 访问系统.
------|------|----run-all # 启动全部,通过http://localhost:8848 访问系统.
------|----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)
## 文档
[快速开始](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)
[产品文档](https://hanta.yuque.com/px7kg1/yfac2l)
[快速开始](https://hanta.yuque.com/px7kg1/yfac2l/raspyc4p1asfuxks)
[开发文档](https://hanta.yuque.com/px7kg1/nn1gdr)
[![Stargazers over time](https://starchart.cc/jetlinks/jetlinks-community.svg?variant=adaptive)](https://starchart.cc/jetlinks/jetlinks-community)

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
dockerImage=registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
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")"
if [ $? -ne 0 ];then
echo "构建失败!"

BIN
device-flow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

View File

@ -1,50 +1,24 @@
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
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:
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

1
docker/run-all/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
data

View File

@ -1,104 +1,92 @@
version: '2'
version: '3'
services:
redis:
image: redis:5.0.4
container_name: jetlinks-ce-redis
# ports:
# - "6379:6379"
volumes:
- "redis-volume:/data"
command: redis-server --appendonly yes --requirepass "JetLinks@redis"
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.13.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.13.0-SNAPSHOT
container_name: jetlinks-ce
ports:
- "8848:8848" # API端口
- "1883-1890:1883-1890" # 预留
- "8000-8010:8000-8010" # 预留
volumes:
- "jetlinks-volume:/application/static/upload" # 持久化上传的文件
- "jetlinks-protocol-volume:/application/data/protocols"
environment:
- "JAVA_OPTS=-Duser.language=zh -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"
- "spring.data.elasticsearch.client.reactive.endpoints=elasticsearch:9200"
# - "spring.data.elasticsearch.client.reactive.username=admin"
# - "spring.data.elasticsearch.client.reactive.password=admin"
# - "spring.reactor.debug-agent.enabled=false" #设置为false能提升性能
- "spring.redis.host=redis"
- "spring.redis.port=6379"
- "spring.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"
links:
- redis:redis
- postgres:postgres
- elasticsearch:elasticsearch
depends_on:
- postgres
- redis
- elasticsearch
volumes:
postgres-volume:
redis-volume:
elasticsearch-volume:
jetlinks-volume:
jetlinks-protocol-volume:
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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@ -5,7 +5,8 @@
<parent>
<artifactId>jetlinks-components</artifactId>
<groupId>org.jetlinks.community</groupId>
<version>1.13.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -18,6 +19,11 @@
<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>
@ -44,5 +50,59 @@
<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,6 +1,24 @@
/*
* 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;
/**
* 数据验证配置常量类
@ -18,5 +36,10 @@ 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

@ -0,0 +1,31 @@
/*
* 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,17 +1,35 @@
/*
* 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.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
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 java.io.IOException;
import java.math.BigDecimal;
import java.util.function.BiFunction;
import java.util.function.Function;
@Getter
@AllArgsConstructor
@ -28,50 +46,13 @@ public class Interval {
public static final String hours = "h";
public static final String minutes = "m";
public static final String seconds = "s";
public static final String millis = "S";
private BigDecimal number;
private String expression;
public boolean isFixed() {
return expression.equalsIgnoreCase(hours) ||
expression.equals(minutes) ||
expression.equals(seconds);
}
public boolean isCalendar() {
return expression.equals(days) ||
expression.equals(month) ||
expression.equals(year);
}
@Override
public String toString() {
return (number) + expression;
}
public static Interval ofSeconds(int seconds) {
return of(seconds, Interval.seconds);
}
public static Interval ofDays(int days) {
return of(days, Interval.days);
}
public static Interval ofHours(int hours) {
return of(hours, Interval.hours);
}
public static Interval ofMonth(int month) {
return of(month, Interval.month);
}
public static Interval of(int month, String expression) {
return new Interval(new BigDecimal(month), expression);
}
public static Interval of(String expr) {
public Interval(String expr) {
char[] chars = expr.toCharArray();
int numIndex = 0;
for (char c : expr.toCharArray()) {
@ -79,12 +60,57 @@ public class Interval {
numIndex++;
} else {
BigDecimal val = new BigDecimal(chars, 0, numIndex);
return new Interval(val, expr.substring(numIndex));
this.expression = expr.substring(numIndex);
this.number = val;
}
}
if (this.expression == null) {
throw new IllegalArgumentException("can not parse interval expression:" + expr);
}
}
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() {
@ -107,6 +133,31 @@ public class Interval {
}
}
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
@ -121,6 +172,54 @@ public class Interval {
}
return of(node.textValue());
}
}
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)));
}
}

View File

@ -0,0 +1,174 @@
/*
* 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

@ -0,0 +1,27 @@
/*
* 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

@ -0,0 +1,68 @@
/*
* 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

@ -0,0 +1,80 @@
/*
* 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

@ -0,0 +1,26 @@
/*
* 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,31 +1,113 @@
/*
* 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> productId = Key.of("productId");
Key<String> uid = Key.of("_uid");
//产品名称
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();
@ -45,5 +127,54 @@ public interface PropertyConstants {
};
}
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,16 +1,37 @@
/*
* 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";
@ -24,8 +45,8 @@ public interface PropertyMetadataConstants {
static boolean isManual(DeviceMessage message) {
return message
.getHeader(PropertyMetadataConstants.Source.headerKey)
.map(PropertyMetadataConstants.Source.manual::equals)
.getHeader(Source.headerKey)
.map(Source.manual::equals)
.orElse(false);
}
@ -45,56 +66,88 @@ public interface PropertyMetadataConstants {
.orElse(false);
}
/**
* 判断属性是否为规则
*
* @param metadata 物模型
* @return 是否规则
*/
static boolean isRule(PropertyMetadata metadata) {
return metadata
.getExpand(id)
.map(rule::equals)
.orElse(false);
}
}
/**
* 属性读写模式
*/
interface AccessMode {
String id = "accessMode";
String id = "type";
//
String read = "r";
String read = "read";
//
String write = "w";
String write = "write";
//上报
String report = "u";
String report = "report";
static boolean isRead(PropertyMetadata property) {
return property
.getExpand(id)
.map(val -> val.toString().contains(read))
.orElse(true);
.orElse(false);
}
static boolean isWrite(PropertyMetadata property) {
return property
.getExpand(id)
.map(val -> val.toString().contains(write))
.orElseGet(() -> property
.getExpand("readOnly")
.map(readOnly -> !CastUtils.castBoolean(readOnly))
.orElse(true)
);
.orElse(false);
}
static boolean isReport(PropertyMetadata property) {
return property
.getExpand(id)
.map(val -> val.toString().contains(report))
.orElse(true);
.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

@ -0,0 +1,79 @@
/*
* 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

@ -0,0 +1,25 @@
/*
* 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

@ -0,0 +1,876 @@
/*
* 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,7 +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;
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;
@ -15,7 +31,7 @@ public interface ValueObject {
default Optional<Object> get(String name) {
return Optional.ofNullable(values())
.map(map -> map.get(name));
.map(map -> map.get(name));
}
default Optional<Integer> getInt(String name) {
@ -44,9 +60,8 @@ public interface ValueObject {
.map(Interval::of);
}
default Interval getInterval(String name,Interval defaultValue) {
return getString(name)
.map(Interval::of)
default Interval getInterval(String name, Interval defaultValue) {
return getInterval(name)
.orElse(defaultValue);
}
@ -56,9 +71,14 @@ public interface ValueObject {
}
default Optional<Date> getDate(String name) {
return get(name)
.map(String::valueOf)
.map(TimeUtils::parseDate);
return this
.get(name)
.map(d -> {
if (d instanceof Date) {
return (Date) d;
}
return TimeUtils.parseDate(String.valueOf(d));
});
}
default Date getDate(String name, Date defaultValue) {
@ -83,7 +103,8 @@ public interface ValueObject {
}
default Optional<Boolean> getBoolean(String name) {
return get(name, Boolean.class);
return get(name)
.map(CastUtils::castBoolean);
}
default boolean getBoolean(String name, boolean defaultValue) {
@ -95,8 +116,9 @@ public interface ValueObject {
.map(obj -> FastBeanCopier.DEFAULT_CONVERT.convert(obj, type, FastBeanCopier.EMPTY_CLASS_ARRAY));
}
static ValueObject of(Map<String, Object> mapVal) {
return () -> mapVal;
@SuppressWarnings("unchecked")
static ValueObject of(Map<String, ?> mapVal) {
return () -> (Map<String, Object>) mapVal;
}
default <T> T as(Class<T> type) {

View File

@ -1,3 +1,18 @@
/*
* 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;
@ -8,6 +23,6 @@ public class Version {
private final String edition = "community";
private final String version = "1.12.0-SNAPSHOT";
private final String version = "2.11.0-SNAPSHOT";
}

View File

@ -0,0 +1,79 @@
/*
* 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

@ -0,0 +1,81 @@
/*
* 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

@ -0,0 +1,448 @@
/*
* 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

@ -0,0 +1,37 @@
/*
* 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

@ -0,0 +1,96 @@
/*
* 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

@ -0,0 +1,148 @@
/*
* 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

@ -0,0 +1,70 @@
/*
* 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

@ -0,0 +1,53 @@
/*
* 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

@ -0,0 +1,220 @@
/*
* 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

@ -0,0 +1,46 @@
/*
* 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

@ -0,0 +1,25 @@
/*
* 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

@ -0,0 +1,74 @@
/*
* 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

@ -0,0 +1,74 @@
/*
* 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

@ -0,0 +1,59 @@
/*
* 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

@ -0,0 +1,22 @@
/*
* 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

@ -0,0 +1,52 @@
/*
* 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

@ -0,0 +1,30 @@
/*
* 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

@ -0,0 +1,159 @@
/*
* 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

@ -0,0 +1,117 @@
/*
* 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

@ -0,0 +1,136 @@
/*
* 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

@ -0,0 +1,52 @@
/*
* 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

@ -0,0 +1,322 @@
/*
* 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

@ -0,0 +1,58 @@
/*
* 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

@ -0,0 +1,90 @@
/*
* 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

@ -0,0 +1,25 @@
/*
* 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

@ -0,0 +1,48 @@
/*
* 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

@ -0,0 +1,61 @@
/*
* 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

@ -0,0 +1,64 @@
/*
* 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

@ -0,0 +1,98 @@
/*
* 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

@ -0,0 +1,153 @@
/*
* 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

@ -0,0 +1,50 @@
/*
* 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

@ -0,0 +1,49 @@
/*
* 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

@ -0,0 +1,53 @@
/*
* 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

@ -0,0 +1,89 @@
/*
* 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

@ -0,0 +1,51 @@
/*
* 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

@ -0,0 +1,47 @@
/*
* 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

@ -0,0 +1,47 @@
/*
* 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

@ -0,0 +1,71 @@
/*
* 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

@ -0,0 +1,43 @@
/*
* 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

@ -0,0 +1,38 @@
/*
* 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

@ -0,0 +1,33 @@
/*
* 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

@ -0,0 +1,24 @@
/*
* 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

@ -0,0 +1,46 @@
/*
* 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

@ -0,0 +1,116 @@
/*
* 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

@ -0,0 +1,63 @@
/*
* 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

@ -0,0 +1,110 @@
/*
* 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

@ -0,0 +1,146 @@
/*
* 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,33 +1,95 @@
/*
* 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;
@Configuration
@AutoConfiguration
@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) {
@ -94,26 +156,132 @@ public class CommonConfiguration {
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 BeanPostProcessor globalReactorQlFeatureRegister() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(@Nonnull Object bean, @Nonnull String beanName) throws BeansException {
if (bean instanceof Feature) {
DefaultReactorQLMetadata.addGlobal(((Feature) bean));
}
return 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

@ -0,0 +1,41 @@
/*
* 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,3 +1,18 @@
/*
* 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;

View File

@ -0,0 +1,39 @@
/*
* 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

@ -0,0 +1,103 @@
/*
* 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

@ -0,0 +1,109 @@
/*
* 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

@ -0,0 +1,86 @@
/*
* 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

@ -0,0 +1,72 @@
/*
* 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

@ -0,0 +1,74 @@
/*
* 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

@ -0,0 +1,24 @@
/*
* 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

@ -0,0 +1,110 @@
/*
* 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

@ -0,0 +1,38 @@
/*
* 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

@ -0,0 +1,96 @@
/*
* 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

@ -0,0 +1,55 @@
/*
* 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

@ -0,0 +1,56 @@
/*
* 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);
}

View File

@ -0,0 +1,42 @@
/*
* 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.doc;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.ezorm.core.param.Term;
import java.util.List;
/**
* 文档专用,描述仅有查询功能的动态查询参数
*
* @author zhouhao
* @since 1.5
* @see org.hswebframework.web.api.crud.entity.QueryParamEntity
*/
@Getter
@Setter
public class QueryConditionOnly {
@Schema(description = "where条件表达式,与terms参数不能共存.语法: name = 张三 and age > 16")
private String where;
@Schema(description = "查询条件集合")
private List<Term> terms;
}

View File

@ -0,0 +1,30 @@
/*
* 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.event;
import org.jetlinks.community.Operation;
import org.jetlinks.community.OperationType;
import reactor.core.publisher.Flux;
public interface OperationAssetProvider {
OperationType[] getSupportTypes();
Flux<String> createTopics(Operation operation, String original);
Flux<String> getAssetTypes(String operationType);
}

View File

@ -0,0 +1,50 @@
/*
* 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.event;
import lombok.extern.slf4j.Slf4j;
import org.jetlinks.community.Operation;
import org.jetlinks.community.OperationType;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class OperationAssetProviders {
private static final Map<String, OperationAssetProvider> providers = new ConcurrentHashMap<>();
public static void register(OperationAssetProvider provider) {
for (OperationType supportType : provider.getSupportTypes()) {
OperationAssetProvider old = providers.put(supportType.getId(), provider);
if (old != null && old != provider) {
log.warn("operation asset provider [{}] already exists,will be replaced by [{}]", old, provider);
}
}
}
public static Optional<OperationAssetProvider> lookup(Operation operation) {
return lookup(operation.getType().getId());
}
public static Optional<OperationAssetProvider> lookup(String operationType) {
return Optional.ofNullable(providers.get(operationType));
}
}

View File

@ -0,0 +1,92 @@
/*
* 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.event;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.core.utils.SerializeUtils;
import org.jetlinks.community.Operation;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
@Getter
@Setter
public class SystemEvent implements Externalizable {
private static final long serialVersionUID = 1L;
private Level level;
private String code;
private Operation operation;
/**
* 描述详情不同的类型详情内容不同
*
* @see org.jetlinks.community.monitor.ExecutionMonitorInfo
*/
private Object detail;
private long timestamp;
public SystemEvent(Level level, String code, Operation operation, Object detail) {
this.level = level;
this.code = code;
this.operation = operation;
this.detail = detail;
this.timestamp = System.currentTimeMillis();
}
public SystemEvent() {
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeByte(level.ordinal());
out.writeUTF(code);
operation.writeExternal(out);
SerializeUtils.writeObject(detail, out);
out.writeLong(timestamp);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
level = Level.values()[in.readByte()];
code = in.readUTF();
operation = new Operation();
operation.readExternal(in);
detail = SerializeUtils.readObject(in);
timestamp = in.readLong();
}
public enum Level {
info,
warn,
error
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.event;
import lombok.AllArgsConstructor;
import org.jetlinks.core.event.EventBus;
import org.springframework.context.ApplicationEventPublisher;
/**
* 推送系统事件到事件总线topic: /sys-event/{operationType}/{operationId}/{level}
*
* @author zhouhao
* @since 2.0
*/
@AllArgsConstructor
public class SystemEventDispatcher implements SystemEventHandler {
private final EventBus eventBus;
private final ApplicationEventPublisher eventPublisher;
@Override
public final void handle(SystemEvent event) {
String topic = SystemEventHandler
.topic(event.getOperation().getType().getId(),
event.getOperation().getSource().getId(),
event.getLevel().name());
eventPublisher.publishEvent(event);
OperationAssetProvider provider = OperationAssetProviders
.lookup(event.getOperation())
.orElse(null);
//对数据权限控制的支持
if (provider != null) {
provider
.createTopics(event.getOperation(), topic)
.flatMap(_topic -> eventBus.publish(_topic, event))
.subscribe();
} else {
eventBus.publish(topic, event)
.subscribe();
}
}
}

View File

@ -0,0 +1,37 @@
/*
* 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.event;
import org.jetlinks.core.utils.StringBuilderUtils;
public interface SystemEventHandler {
static String topic(String operationType, String operationId, String level) {
return StringBuilderUtils
.buildString(operationType, operationId, level, (a, b, c, builder) -> {
// /sys-event/{operationType}/{operationId}/{level}
builder.append("/sys-event/")
.append(a)
.append('/')
.append(b)
.append('/')
.append(c);
});
}
void handle(SystemEvent event);
}

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