Compare commits

...

202 Commits
2.2 ... 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
1583 changed files with 66863 additions and 13976 deletions

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

3
.gitignore vendored
View File

@ -29,4 +29,5 @@ docker/data
!demo-protocol-1.0.jar
application-local.yml
dev/
.DS_Store
.DS_Store
.java-version

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>

View File

@ -1,22 +1,22 @@
# JetLinks 物联网基础平台
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/jetlinks/jetlinks-community/maven.yml?branch=master)
![Version](https://img.shields.io/badge/version-2.1--RELEASE-brightgreen)
![Version](https://img.shields.io/badge/version-2.11-brightgreen)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/e8d527d692c24633aba4f869c1c5d6ad)](https://app.codacy.com/gh/jetlinks/jetlinks-community?utm_source=github.com&utm_medium=referral&utm_content=jetlinks/jetlinks-community&utm_campaign=Badge_Grade_Settings)
[![OSCS Status](https://www.oscs1024.com/platform/badge/jetlinks/jetlinks-community.svg?size=small)](https://www.oscs1024.com/project/jetlinks/jetlinks-community?ref=badge_small)
[![star](https://img.shields.io/github/stars/jetlinks/jetlinks-community?style=social)](https://github.com/jetlinks/jetlinks-community)
[![star](https://gitee.com/jetlinks/jetlinks-community/badge/star.svg?theme=gvp)](https://gitee.com/jetlinks/jetlinks-community/stargazers)
[![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等开发,
是一个开箱即用,可二次开发的企业级物联网基础平台。平台实现了物联网相关的众多基础功能,
能帮助你快速建立物联网相关业务系统。
## 核心特性
@ -24,25 +24,35 @@ JetLinks 基于Java8,Spring Boot 2.x,WebFlux,Netty,Vert.x,Reactor等开发,
全部源代码开放,可自由拓展功能,不再受制于人.前后端分离,接口全开放。
#### 部署简单
最小化运行仅需要`java 17`,`redis`,`timescaledb`即可,无需部署大量中间件。
#### 统一设备接入,海量设备管理
TCP/UDP/MQTT/HTTP、TLS/DTLS、不同厂商、不同设备、不同报文、统一接入统一管理。
#### 规则引擎
灵活的规则模型配置,支持多种规则模型以及自定义规则模型. 设备告警,场景联动,均由统一的规则引擎管理。
#### 数据权限控制
灵活的非侵入数据权限控制。可实现菜单、按钮、数据三维维度的数据权限控制。可控制单条数据的操作权限。
## 技术栈
1. [Spring Boot 2.7.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) 业务功能数据管理
## 架构
@ -58,7 +68,7 @@ TCP/UDP/MQTT/HTTP、TLS/DTLS、不同厂商、不同设备、不同报文、统
--jetlinks-community
------|----docker
------|------|----dev-env # 启动开发环境
------|------|----run-all # 启动全部,通过http://localhost:9000 访问系统.
------|------|----run-all # 启动全部,通过http://localhost:8848 访问系统.
------|----jetlinks-components # 公共组件模块
------|-------|----common-component # 通用组件.
------|-------|----configuration-component # 通用配置.
@ -89,23 +99,20 @@ TCP/UDP/MQTT/HTTP、TLS/DTLS、不同厂商、不同设备、不同报文、统
------|----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+元 | 定制化开发 |
| 硬件支持 | 提供JetLinks自有硬件邮寄到手JetLinks DTU、报警器、温度传感器并提供完整的接入视频文档 | 699元 | 硬件包邮<br /> [视频文档](https://hanta.yuque.com/px7kg1/yfac2l/pgi0eydsmlyb7q1w)|
| 其他服务 | 企业版源码购买;定制化开发;定制化时长、功能服务等 | 面议 | 面议 |
| 服务项 | 服务内容 | 服务收费 | 服务方式 |
|----------|-----------------------------------------------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 基础问题答疑 | 问题答疑 | 免费 | 技术交流群支持 [![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+元 | 定制化开发 |
| 其他服务 | 企业版源码购买;定制化开发;定制化时长、功能服务等 | 面议 | 面议 |
### **付费**服务支持或商务合作请联系
@ -113,7 +120,6 @@ TCP/UDP/MQTT/HTTP、TLS/DTLS、不同厂商、不同设备、不同报文、统
## 文档
[产品文档](https://hanta.yuque.com/px7kg1/yfac2l)
[快速开始](https://hanta.yuque.com/px7kg1/yfac2l/raspyc4p1asfuxks)
[开发文档](https://hanta.yuque.com/px7kg1/nn1gdr)

View File

@ -1,50 +1,24 @@
version: '2'
services:
redis:
image: redis:5.0.4
container_name: jetlinks-ce-redis
ports:
- "6379:6379"
volumes:
- "./data/redis:/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:
- "./data/pg:/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,118 +1,92 @@
version: '2'
version: '3'
services:
redis:
image: redis:5.0.4
container_name: jetlinks-ce-redis
# ports:
# - "6379:6379"
volumes:
- "./data/redis:/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:
# - ./data/elasticsearch:/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:
- "5601:5601"
depends_on:
- elasticsearch
postgres:
image: postgres:11-alpine
container_name: jetlinks-ce-postgres
volumes:
- "./data/postgres:/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-vue:2.2.0-SNAPSHOT
container_name: jetlinks-ce-ui
ports:
- 9000:80
environment:
- "API_BASE_PATH=http://jetlinks:8848/" #API根路径
volumes:
- "./data/jetlinks-ui:/usr/share/nginx/html/upload"
links:
- jetlinks:jetlinks
jetlinks:
image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-community:2.2.0-SNAPSHOT
container_name: jetlinks-ce
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
ports:
- "8848:8848" # API端口
- "1883-1890:1883-1890" # 预留
- "8800-8810:8800-8810" # 预留
- "5060-5061:5060-5061" # 预留
volumes:
- "./data/jetlinks:/application/static/upload" # 持久化上传的文件
- "./data/jetlinks/:/application/data/files"
- "./data/jetlinks/:/application/data/protocols"
- "./entrypoint.sh:/entrypoint.sh"
#entrypoint: /entrypoint.sh -d redis:5601,postgres:5432,elasticsearch:9200 'echo "start jetlinks service here"';
environment:
# - "SLEEP_SECOND=4"
- "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.elasticsearch.uris=elasticsearch:9200"
# - "spring.elasticsearch.username=admin"
# - "spring.elasticsearch.password=admin"
# - "spring.reactor.debug-agent.enabled=false" #设置为false能提升性能
- "spring.redis.host=redis"
- "spring.redis.port=6379"
- "file.manager.storage-base-path=/application/data/files"
- "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"
- "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
- elasticsearch:elasticsearch
depends_on:
- postgres
- redis
- elasticsearch

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>jetlinks-components</artifactId>
<groupId>org.jetlinks.community</groupId>
<version>2.2.0-SNAPSHOT</version>
<version>2.11.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -19,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>
@ -75,9 +80,12 @@
<dependency>
<groupId>com.cronutils</groupId>
<artifactId>cron-utils</artifactId>
<version>9.2.0</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
</exclusion>
<exclusion>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
@ -90,5 +98,11 @@
<artifactId>spring-data-redis</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.glassfish.expressly</groupId>
<artifactId>expressly</artifactId>
</dependency>
</dependencies>
</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;
/**
* 数据验证配置常量类
@ -20,4 +38,8 @@ public interface ConfigMetadataConstants {
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,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 com.alibaba.fastjson.annotation.JSONType;
@ -37,6 +52,23 @@ public class Interval {
private String expression;
public Interval(String expr) {
char[] chars = expr.toCharArray();
int numIndex = 0;
for (char c : expr.toCharArray()) {
if (c == '-' || c == '.' || (c >= '0' && c <= '9')) {
numIndex++;
} else {
BigDecimal val = new BigDecimal(chars, 0, numIndex);
this.expression = expr.substring(numIndex);
this.number = val;
}
}
if (this.expression == null) {
throw new IllegalArgumentException("can not parse interval expression:" + expr);
}
}
@Override
public String toString() {
return (number) + expression;
@ -52,6 +84,11 @@ public class Interval {
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);
@ -73,20 +110,7 @@ public class Interval {
}
public static Interval of(String expr) {
char[] chars = expr.toCharArray();
int numIndex = 0;
for (char c : expr.toCharArray()) {
if (c == '-' || c == '.' || (c >= '0' && c <= '9')) {
numIndex++;
} else {
BigDecimal val = new BigDecimal(chars, 0, numIndex);
return new Interval(val, expr.substring(numIndex));
}
}
throw new IllegalArgumentException("can not parse interval expression:" + expr);
return new Interval(expr);
}
public String getDefaultFormat() {

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.AllArgsConstructor;

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 org.hswebframework.web.exception.I18nSupportException;

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

@ -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.AllArgsConstructor;

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,10 +1,28 @@
/*
* 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;
@ -13,32 +31,75 @@ import java.util.function.Supplier;
* @author wangzheng
* @since 1.0
*/
@Generated
public interface PropertyConstants {
//机构ID
Key<String> orgId = Key.of("orgId");
//设备名称
Key<String> deviceName = Key.of("deviceName");
//产品名称
Key<String> productName = Key.of("productName");
//产品ID
Key<String> productId = Key.of("productId");
Key<String> uid = Key.of("_uid");
//设备创建者
Key<String> creatorId = Key.of("creatorId");
/**
* 关系信息.值格式:
* <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> {
@ -89,13 +150,14 @@ public interface PropertyConstants {
@Override
public T getDefaultValue() {
return defaultValue.get();
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;
@ -108,10 +170,11 @@ public interface PropertyConstants {
@Override
public T getDefaultValue() {
return defaultValue.get();
return defaultValue == null ? null : defaultValue.get();
}
};
}
}
}

View File

@ -1,6 +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;
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;
@ -56,38 +72,34 @@ public interface PropertyMetadataConstants {
* 属性读写模式
*/
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);
}
}
@ -95,7 +107,7 @@ public interface PropertyMetadataConstants {
String id = "metrics";
static Map<String,Object> metricsToExpands(List<PropertyMetric> metrics) {
static Map<String, Object> metricsToExpands(List<PropertyMetric> metrics) {
return Collections.singletonMap(id, metrics);
}
@ -111,7 +123,7 @@ public interface PropertyMetadataConstants {
.getExpand(id)
.map(obj -> {
for (PropertyMetric propertyMetric : ConverterUtils.convertToList(obj, PropertyMetric::of)) {
if(Objects.equals(metric, propertyMetric.getId())){
if (Objects.equals(metric, propertyMetric.getId())) {
return propertyMetric;
}
}
@ -120,4 +132,22 @@ public interface PropertyMetadataConstants {
}
}
interface Group {
ConfigKey<String> id = ConfigKey.of("groupId", "分组ID", String.class);
ConfigKey<String> name = ConfigKey.of("groupName", "分组名称", String.class);
static String getId(PropertyMetadata metadata) {
return metadata
.getExpand(id)
.orElse("");
}
static String getName(PropertyMetadata metadata) {
return metadata
.getExpand(name)
.orElse("");
}
}
}

View File

@ -1,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 io.swagger.v3.oas.annotations.media.Schema;
@ -6,7 +21,7 @@ import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.community.utils.ConverterUtils;
import org.springframework.util.StringUtils;
import javax.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotBlank;
import java.util.Map;
import java.util.function.Function;

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 java.time.ZonedDateTime;

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 com.cronutils.builder.CronBuilder;
@ -15,7 +30,10 @@ 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;
@ -25,8 +43,8 @@ import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import javax.annotation.Nonnull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.*;
import java.time.temporal.ChronoUnit;
@ -35,6 +53,7 @@ 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
@ -45,6 +64,9 @@ public class TimerSpec implements Serializable {
@NotNull
private Trigger trigger;
@Schema(description = "使用日程标签进行触发")
private Set<String> scheduleTags;
//Cron表达式
@Schema(description = "触发方式为[cron]时不能为空")
private String cron;
@ -58,9 +80,15 @@ public class TimerSpec implements Serializable {
@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;
@ -68,6 +96,17 @@ public class TimerSpec implements Serializable {
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;
@ -82,14 +121,25 @@ public class TimerSpec implements Serializable {
public Predicate<LocalDateTime> createTimeFilter() {
Predicate<LocalDateTime> range = createRangeFilter();
//周期执行指定了to,表示只在时间范围段内执行
if (mod == ExecuteMod.period) {
LocalTime to = period.toLocalTime();
LocalTime from = period.fromLocalTime();
Predicate<LocalDateTime> predicate
= time -> time.toLocalTime().compareTo(from) >= 0;
if (to != null) {
predicate = predicate.and(time -> time.toLocalTime().compareTo(to) <= 0);
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);
}
@ -232,6 +282,13 @@ public class TimerSpec implements Serializable {
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());
}
@ -280,6 +337,21 @@ public class TimerSpec implements Serializable {
}
@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);
}
@ -370,11 +442,40 @@ public class TimerSpec implements Serializable {
}
private TimerIterable periodIterable() {
Assert.notNull(period, "period can not be null");
List<Period> periods = periods();
Assert.notEmpty(periods, "period or periods can not be null");
Predicate<LocalDateTime> filter = createTimeFilter();
Duration duration = Duration.of(period.every, period.unit.temporal);
LocalTime time = period.fromLocalTime();
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;
@ -389,6 +490,21 @@ public class TimerSpec implements Serializable {
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;
}
@ -432,7 +548,110 @@ public class TimerSpec implements Serializable {
};
}
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();
}
@ -448,7 +667,6 @@ public class TimerSpec implements Serializable {
return timeList;
}
public Flux<Long> flux() {
return flux(Schedulers.parallel());
}
@ -457,9 +675,111 @@ public class TimerSpec implements Serializable {
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 Function<ZonedDateTime, Duration> spec;
final Scheduler scheduler;
@Override
@ -530,9 +850,14 @@ public class TimerSpec implements Serializable {
}
public enum Trigger {
//按周
week,
//按月
month,
cron
//cron表达式
cron,
// 多个触发组合
multi
}
public enum ExecuteMod {

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 org.hswebframework.web.bean.FastBeanCopier;

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 = "2.2.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

@ -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.authorize;
import lombok.Getter;

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

@ -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.buffer;
import lombok.Getter;
@ -23,6 +38,15 @@ public class BufferProperties {
//最大重试次数,超过此次数的数据将会放入死队列.
private long maxRetryTimes = 64;
//文件操作的最大并行度,默认为1,不建议设置超过4.
private int fileConcurrency = 1;
//消费策略 默认先进先出
private ConsumeStrategy strategy = ConsumeStrategy.FIFO;
//淘汰策略
private BufferEvictionSpec eviction = new BufferEvictionSpec();
public boolean isExceededRetryCount(int count) {
return maxRetryTimes > 0 && count >= maxRetryTimes;
}

View File

@ -1,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.buffer;
import lombok.AllArgsConstructor;
@ -10,19 +25,18 @@ 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;
/**
* @author zhouhao
* @since 2.0
*/
@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,
@ -32,10 +46,17 @@ public class BufferSettings {
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 处理逻辑
@ -50,16 +71,23 @@ public class BufferSettings {
//最大重试次数,超过此次数的数据将会放入死队列.
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);
5,
1,
ConsumeStrategy.FIFO);
}
public static BufferSettings create(BufferProperties properties) {
@ -70,64 +98,122 @@ public class BufferSettings {
return create(properties.getFilePath(), fileName).properties(properties);
}
public BufferSettings bufferSize(int bufferSize) {
public BufferSettings eviction(BufferEviction eviction) {
return new BufferSettings(filePath,
fileName,
eviction,
retryWhenError,
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes);
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);
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings parallelism(int parallelism) {
return new BufferSettings(filePath,
fileName,
eviction,
retryWhenError,
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes);
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings maxRetry(int maxRetryTimes) {
return new BufferSettings(filePath,
fileName,
eviction,
retryWhenError,
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes);
maxRetryTimes,
fileConcurrency,
strategy);
}
public BufferSettings retryWhenError(Predicate<Throwable> retryWhenError) {
return new BufferSettings(filePath,
fileName,
eviction,
Objects.requireNonNull(retryWhenError),
bufferSize,
bufferTimeout,
parallelism,
maxRetryTimes);
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.getMaxRetryTimes(),
properties.getFileConcurrency(),
properties.getStrategy()
);
}

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.buffer;
/**

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

@ -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.buffer;
public interface MemoryUsage {

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.buffer;
import com.google.common.collect.Collections2;
@ -5,14 +20,16 @@ import io.netty.buffer.*;
import io.netty.util.ReferenceCountUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.h2.mvstore.WriteBuffer;
import org.h2.mvstore.type.BasicDataType;
import org.jetlinks.community.codec.Serializers;
import org.jetlinks.core.cache.FileQueue;
import org.jetlinks.core.cache.FileQueueProxy;
import org.jetlinks.core.utils.SerializeUtils;
import org.jetlinks.community.codec.Serializers;
import org.jetlinks.community.utils.FormatUtils;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -21,9 +38,15 @@ import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.SignalType;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import javax.annotation.Nonnull;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.StandardMBean;
import java.io.*;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Array;
import java.nio.ByteBuffer;
import java.nio.file.Path;
@ -38,6 +61,7 @@ import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* 支持持久化的缓存批量操作工具,用于支持数据的批量操作,如批量写入数据到数据库等.
@ -46,11 +70,13 @@ import java.util.function.Supplier;
*
* <pre>{@code
*
* BufferWriter<Data> writer = BufferWriter
* PersistenceBuffer<Data> writer = PersistenceBuffer
* .<Data>create(
* "./data/buffer", //文件目录
* "my-data.queue", //文件名
* Data::new,
* buffer->{
* // 返回false表示不重试
* return saveData(buffer);
* })
* .bufferSize(1000)//缓冲大小,当缓冲区超过此数量时将会立即执行写出操作.
@ -66,9 +92,9 @@ import java.util.function.Supplier;
*
* @param <T> 数据类型,需要实现Serializable接口
* @author zhouhao
* @since pro 2.0
* @since 2.0
*/
public class PersistenceBuffer<T extends Serializable> implements Disposable {
public class PersistenceBuffer<T extends Serializable> implements EvictionContext, Disposable {
@SuppressWarnings("all")
private final static AtomicIntegerFieldUpdater<PersistenceBuffer> WIP =
AtomicIntegerFieldUpdater.newUpdater(PersistenceBuffer.class, "wip");
@ -125,11 +151,16 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
//刷新缓冲区定时任务
private Disposable intervalFlush;
//独立的读写调度器
private Scheduler writer, reader;
private Throwable lastError;
private volatile Boolean disposed = false;
private boolean started = false;
private final PersistenceBufferMBeanImpl<T> monitor = new PersistenceBufferMBeanImpl<>(this);
public PersistenceBuffer(String filePath,
String fileName,
Supplier<T> newInstance,
@ -228,6 +259,7 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
.name(fileName)
.path(path)
.option("valueType", dataType)
.option("concurrency", settings.getFileConcurrency())
.build());
this.remainder = queue.size();
//死队列,用于存放失败的数据
@ -238,8 +270,27 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
.option("valueType", dataType)
.build());
this.deadSize = this.deadQueue.size();
this.buffer = newBuffer();
initScheduler();
registerMbean();
}
private void initScheduler() {
shutdownScheduler();
this.writer = settings.getFileConcurrency() > 1
? Schedulers.newParallel(name + "-writer", settings.getFileConcurrency())
: Schedulers.newSingle(name + "-writer");
this.reader = Schedulers.newSingle(name + "-reader");
}
private void shutdownScheduler() {
if (this.writer != null) {
this.writer.dispose();
}
if (this.reader != null) {
this.reader.dispose();
}
}
public synchronized void start() {
@ -286,6 +337,7 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
//直接写入queue,而不是使用write,等待后续有新的数据进入再重试
if (queue.offer(buf)) {
// REMAINDER.incrementAndGet(this);
settings.getEviction().tryEviction(this);
} else {
dead(buf);
}
@ -313,12 +365,46 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
}
return;
}
// remainder ++
monitor.in();
// REMAINDER.incrementAndGet(this);
queue.offer(data);
drain();
//尝试执行淘汰策略
settings.getEviction().tryEviction(this);
}
//异步写入数据到buffer
public Mono<Void> writeAsync(T data) {
if (isDisposed()) {
return Mono.fromRunnable(() -> write(data));
}
return Mono
.fromRunnable(() -> write(data))
.subscribeOn(writer)
// 切换到parallel线程池,避免浪费writer线程性能
// 但是线程切换本身也是消耗
.publishOn(Schedulers.parallel())
.then();
}
//异步写入数据到buffer
public Mono<Void> writeAsync(Collection<T> data) {
if (isDisposed()) {
return Mono.fromRunnable(() -> data.forEach(this::write));
}
return Mono
.fromRunnable(() -> data.forEach(this::write))
.subscribeOn(writer)
.publishOn(Schedulers.parallel())
.then();
}
//写入数据到buffer,此操作可能阻塞
@Deprecated
public void write(T data) {
write(new Buf<>(data, instanceBuilder));
}
@ -328,6 +414,9 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
if (this.intervalFlush != null) {
this.intervalFlush.dispose();
}
for (FlushSubscriber subscriber : new ArrayList<>(flushing)) {
subscriber.doCancel();
}
}
@SneakyThrows
@ -368,7 +457,9 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
deadQueue.close();
queue = null;
deadQueue = null;
shutdownScheduler();
}
unregisterMbean();
}
@Override
@ -377,7 +468,37 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
}
public long size() {
return queue == null ? 0 : queue.size();
return queue == null || disposed ? 0 : queue.size() + buffer().size();
}
public long size(BufferType type) {
return type == BufferType.buffer ? size() : deadQueue == null || disposed ? 0 : deadQueue.size();
}
@Override
public void removeLatest(BufferType type) {
if (type == BufferType.buffer) {
if (queue.removeLast() != null) {
monitor.dropped();
}
} else {
if (deadQueue.removeLast() != null) {
// DEAD_SZIE.decrementAndGet(this);
}
}
}
@Override
public void removeOldest(BufferType type) {
if (type == BufferType.buffer) {
if (queue.removeFirst() != null) {
monitor.dropped();
}
} else {
if (deadQueue.removeFirst() != null) {
// DEAD_SZIE.decrementAndGet(this);
}
}
}
private void intervalFlush() {
@ -446,7 +567,7 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
logger.debug("write {} data,size:{},remainder:{},requeue: {}.take up time: {} ms",
name,
buffer.size(),
queue.size(),
size(),
doRequeue,
System.currentTimeMillis() - startWith);
}
@ -535,6 +656,7 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
}
buffer.forEach(Buf::reset);
flushing.remove(this);
monitor.out(size, System.currentTimeMillis() - startWith);
// wip--
WIP.decrementAndGet(PersistenceBuffer.this);
drain();
@ -556,22 +678,27 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
if (!started) {
return;
}
//当前未执行完成的操作小于并行度才请求
// 当前未执行完成的操作小于并行度才请求
if (WIP.incrementAndGet(this) <= settings.getParallelism()) {
int size = settings.getBufferSize();
for (int i = 0; i < size; i++) {
if (isDisposed()) {
break;
}
Buf<T> poll = queue.poll();
if (poll != null) {
onNext(poll);
} else {
break;
}
}
// 使用boundedElastic线程执行poll,避免阻塞线程
reader
.schedule(() -> {
int size = settings.getBufferSize();
for (int i = 0; i < size && started; i++) {
Buf<T> poll = settings.getStrategy() == ConsumeStrategy.LIFO
? queue.removeLast()
: queue.poll();
if (poll != null) {
onNext(poll);
} else {
break;
}
}
WIP.decrementAndGet(this);
});
} else {
WIP.decrementAndGet(this);
}
WIP.decrementAndGet(this);
}
private void onNext(@Nonnull Buf<T> value) {
@ -739,7 +866,7 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
if (obj.data instanceof String) {
return ((String) obj.data).length() * 2;
}
return 10_000;
return 4096;
}
@Override
@ -802,6 +929,228 @@ public class PersistenceBuffer<T extends Serializable> implements Disposable {
}
}
private ObjectName objectName;
void registerMbean() {
try {
String safeName = name.replaceAll("[\\s\\\\/:*?\"<>|]", "_");
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
objectName = new ObjectName("org.jetlinks:type=PersistenceBuffer,name=" + safeName);
mBeanServer.registerMBean(new StandardMBean(monitor, PersistenceBufferMBean.class), objectName);
} catch (Throwable error) {
logger.warn("registerMBean {} error ", name, error);
}
}
void unregisterMbean() {
try {
if (objectName != null) {
MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
mBeanServer.unregisterMBean(objectName);
}
} catch (Throwable ignore) {
}
}
@RequiredArgsConstructor
private static class PersistenceBufferMBeanImpl<T extends Serializable> implements PersistenceBufferMBean {
@SuppressWarnings("all")
private static final AtomicLongFieldUpdater<PersistenceBufferMBeanImpl>
IN = AtomicLongFieldUpdater.newUpdater(PersistenceBufferMBeanImpl.class, "in"),
OUT = AtomicLongFieldUpdater.newUpdater(PersistenceBufferMBeanImpl.class, "out"),
COST = AtomicLongFieldUpdater.newUpdater(PersistenceBufferMBeanImpl.class, "cost"),
OPT = AtomicLongFieldUpdater.newUpdater(PersistenceBufferMBeanImpl.class, "opt"),
DROPPED = AtomicLongFieldUpdater.newUpdater(PersistenceBufferMBeanImpl.class, "dropped");
private final PersistenceBuffer<T> buffer;
private volatile long
//写入数量
in,
//写出数量
out,
//写出次数
opt,
//写出总耗时
cost,
//淘汰数量
dropped;
private final long[] costDist = new long[5];
private void dropped() {
DROPPED.incrementAndGet(this);
}
private void in() {
IN.incrementAndGet(this);
}
private void out(long outSize, long cost) {
COST.addAndGet(this, cost);
OUT.addAndGet(this, outSize);
OPT.incrementAndGet(this);
if (cost < 50) {
costDist[0]++;
} else if (cost < 200) {
costDist[1]++;
} else if (cost < 1000) {
costDist[2]++;
} else if (cost < 5000) {
costDist[3]++;
} else {
costDist[4]++;
}
}
@Override
public String getMonitor() {
return String.format(
"\nqueue(in %s,out %s,dropped %s);\nconsume(opt %s,cost %s ms);\ndist[0-50ms(%s),50-200ms(%s),0.2-1s(%s),1-5s(%s),>5s(%s)]\n",
in, out, dropped, opt, cost, costDist[0], costDist[1], costDist[2], costDist[3], costDist[4]);
}
@Override
public void resetMonitor() {
IN.set(this, 0);
OUT.set(this, 0);
OPT.set(this, 0);
COST.set(this, 0);
Arrays.fill(costDist, 0);
}
@Override
public long getRemainder() {
return buffer.queue.size();
}
@Override
public long getDeadSize() {
return buffer.deadQueue.size();
}
@Override
public long getWip() {
return buffer.wip;
}
@Override
public String getLastError() {
Throwable error = buffer.lastError;
return error == null ? "nil"
: ExceptionUtils.getRootCauseMessage(error) + ":" + ExceptionUtils.getStackTrace(error);
}
@Override
public String getStoragePath() {
return buffer.settings.getFilePath();
}
@Override
public List<String> getDataBytes() {
File[] files = new File(buffer.settings.getFilePath())
.listFiles(filter -> filter.getName().startsWith(getSafeFileName(buffer.settings.getFileName())));
if (files == null) {
return Collections.emptyList();
}
return Arrays
.stream(files)
.map(file -> file.getName() + " " + FormatUtils.formatDataSize(file.length()))
.collect(Collectors.toList());
}
@Override
public void flush() {
buffer.queue.flush();
buffer.deadQueue.flush();
}
@Override
public void retryDead(int maxSize) {
//单次请求最大重试次数
maxSize = Math.min(50_0000, maxSize);
while (maxSize-- > 0) {
Buf<T> buf = buffer.deadQueue.poll();
if (buf == null) {
break;
}
buf.retry = 0;
if (!buffer.queue.offer(buf)) {
buffer.deadQueue.offer(buf);
break;
}
}
buffer.drain();
}
@Override
public String getSettings() {
return String.format("\nbufferSize: %s" +
",bufferTimeout: %s" +
",parallelism: %s" +
",maxRetryTimes: %s" +
",fileConcurrency: %s" + "\nEviction:%s ",
buffer.settings.getBufferSize(),
buffer.settings.getBufferTimeout(),
buffer.settings.getParallelism(),
buffer.settings.getMaxRetryTimes(),
buffer.settings.getFileConcurrency(),
buffer.settings.getEviction());
}
@Override
public long recovery(String fileName, boolean dead) {
return buffer.recovery(fileName, dead);
}
@Override
public List<Object> peekDead(int size) {
size = size <= 0 ? 1 : Math.min(1024, size);
List<Object> result = new ArrayList<>(size);
for (Buf<T> tBuf : buffer.deadQueue) {
if (size-- <= 0) {
break;
}
result.add(tBuf.data);
}
return result;
}
}
public interface PersistenceBufferMBean {
String getSettings();
String getMonitor();
void resetMonitor();
String getStoragePath();
long getRemainder();
long getDeadSize();
long getWip();
String getLastError();
List<String> getDataBytes();
void flush();
void retryDead(int maxSize);
long recovery(String fileName, boolean dead);
List<Object> peekDead(int size);
}
public interface FlushContext<T> {
//标记错误信息

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

@ -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.codec;
import java.io.InputStream;

View File

@ -1,10 +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.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 {
@ -67,5 +88,72 @@ public class Serializers {
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

@ -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.command.rule.data;
@ -9,7 +24,6 @@ import lombok.Setter;
import org.jetlinks.community.terms.TermSpec;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
/**
@ -49,6 +63,9 @@ public class AlarmInfo implements Serializable {
@Schema(description = "告警来源ID")
private String sourceId;
@Schema(description = "告警来源的创建人ID")
private String sourceCreatorId;
@Schema(description = "告警来源名称")
private String sourceName;
@ -65,4 +82,8 @@ public class AlarmInfo implements Serializable {
* 告警触发条件
*/
private TermSpec termSpec;
@Schema(description = "告警时间")
private Long alarmTime;
}

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.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;

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.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;
@ -18,6 +33,9 @@ public class RelieveInfo extends AlarmInfo{
@Schema(description = "解除原因")
private String relieveReason;
@Schema(description = "解除时间")
private Long relieveTime;
@Schema(description = "解除说明")
private String describe;

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.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;

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.config;
import org.jetlinks.community.ValueObject;

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.config;
import io.swagger.v3.oas.annotations.media.Schema;

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.config;
import io.swagger.v3.oas.annotations.media.Schema;

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.config;
/**

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.config;
import java.util.List;

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.config;
import lombok.Getter;

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.config;
import lombok.AllArgsConstructor;

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.config.entity;
import io.swagger.v3.oas.annotations.media.Schema;

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.config.verification;
import io.swagger.v3.oas.annotations.Operation;

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.config.web;
import io.swagger.v3.oas.annotations.Operation;

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.alibaba.fastjson.JSON;
@ -7,19 +22,26 @@ import lombok.Generated;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.Converter;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.web.api.crud.entity.EntityFactory;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.cache.ReactiveCacheManager;
import org.hswebframework.web.dict.EnumDict;
import org.hswebframework.web.dict.defaults.DefaultItemDefine;
import org.jetlinks.community.Interval;
import org.jetlinks.community.JvmErrorException;
import org.jetlinks.community.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;
@ -30,12 +52,12 @@ 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.event.EventBus;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.rpc.RpcManager;
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;
@ -43,29 +65,31 @@ 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.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
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) {
@ -149,6 +173,23 @@ public class CommonConfiguration {
}
}, 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)) {
@ -165,21 +206,27 @@ public class CommonConfiguration {
}
@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());
};
@ -223,6 +270,12 @@ public class CommonConfiguration {
return referenceManager;
}
@Bean
public CommandServiceEndpointRegister commandServiceEndpointRegister() {
return new CommandServiceEndpointRegister();
}
@Configuration
@ConditionalOnClass(ReactiveRedisOperations.class)
static class DefaultUserBindServiceConfiguration {

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

@ -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.dictionary;
import lombok.AllArgsConstructor;

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.dictionary;
import org.hswebframework.web.dict.EnumDict;

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.dictionary;
import org.hswebframework.ezorm.rdb.mapping.annotation.Codec;

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.dictionary;
import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;

View File

@ -1,5 +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.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;
@ -9,15 +27,22 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@AutoConfiguration
public class DictionaryConfiguration {
@Configuration
@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) {
@ -44,5 +69,6 @@ public class DictionaryConfiguration {
DefaultDictionaryItemService itemService) {
return new DictionaryInitManager(initInfo, defaultDictionaryService, itemService);
}
}
}

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.dictionary;
public interface DictionaryConstants {

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.dictionary;
import lombok.AllArgsConstructor;
@ -7,16 +22,21 @@ 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 implements EntityEventListenerCustomizer {
public class DictionaryEventHandler {
private final DefaultDictionaryItemService itemService;
@ -68,28 +88,23 @@ public class DictionaryEventHandler implements EntityEventListenerCustomizer {
);
}
@Override
public void customize(EntityEventListenerConfigure configure) {
configure.enable(DictionaryItemEntity.class);
configure.enable(DictionaryEntity.class);
}
/**
* 监听字典删除前事件阻止删除分类标识为系统的字典
*
* @param event 字典删除前事件
*/
@EventListener
public void handleDictionaryBeforeDelete(EntityBeforeDeleteEvent<DictionaryEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.any(dictionary ->
StringUtils.equals(dictionary.getClassified(), DictionaryConstants.CLASSIFIED_SYSTEM))
.flatMap(any -> {
if (any) {
return Mono.error(() -> new BusinessException("error.system_dictionary_can_not_delete"));
}
return Mono.empty();
})
Flux.fromIterable(event.getEntity())
.any(dictionary ->
StringUtils.equals(dictionary.getClassified(), DictionaryConstants.CLASSIFIED_SYSTEM))
.flatMap(any -> {
if (any) {
return Mono.error(() -> new BusinessException("error.system_dictionary_can_not_delete"));
}
return Mono.empty();
})
);
}
}

View File

@ -1,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.dictionary;
import org.apache.commons.collections4.CollectionUtils;

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.dictionary;
import lombok.Getter;

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.dictionary;
import com.fasterxml.jackson.core.JacksonException;

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.dictionary;
import org.hswebframework.web.dict.EnumDict;

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.doc;
import io.swagger.v3.oas.annotations.media.Schema;

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);
}

View File

@ -0,0 +1,28 @@
/*
* 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.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Component;
@Component
public class SystemEventHandlerRegister {
public SystemEventHandlerRegister(ObjectProvider<SystemEventHandler> handlers){
handlers.forEach(SystemEventHolder::register);
}
}

View File

@ -0,0 +1,69 @@
/*
* 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 reactor.core.Disposable;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@Slf4j
public class SystemEventHolder {
private static final List<SystemEventHandler> eventHandlers = new CopyOnWriteArrayList<>();
public static Disposable register(SystemEventHandler handler) {
eventHandlers.add(handler);
return () -> eventHandlers.remove(handler);
}
public static void error(Operation operation, String code, Object detail) {
log.error("{} {} :{}", operation, code, detail);
if (eventHandlers.isEmpty()) {
return;
}
fireEvent(new SystemEvent(SystemEvent.Level.error, code, operation, detail));
}
public static void warn(Operation operation, String code, Object detail) {
log.warn("{} {} :{}", operation, code, detail);
if (eventHandlers.isEmpty()) {
return;
}
fireEvent(new SystemEvent(SystemEvent.Level.warn, code, operation, detail));
}
public static void info(Operation operation, String code, Object detail) {
log.info("{} {} :{}", operation, code, detail);
if (eventHandlers.isEmpty()) {
return;
}
fireEvent(new SystemEvent(SystemEvent.Level.info, code, operation, detail));
}
private static void fireEvent(SystemEvent event) {
for (SystemEventHandler eventHandler : eventHandlers) {
try {
eventHandler.handle(event);
} catch (Throwable e) {
log.warn("handle system log error", e);
}
}
}
}

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.form.type;
import lombok.Getter;

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.form.type;
import org.hswebframework.ezorm.core.ValueCodec;

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.form.type;
import org.jetlinks.community.spi.Provider;

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.form.type;
import lombok.Getter;

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