Compare commits

...

316 Commits

Author SHA1 Message Date
zhouhao e395e4041e build: 升级依赖 2025-07-23 10:06:47 +08:00
zhouhao e134dfec6f build: 升级依赖版本 2025-06-30 13:54:01 +08:00
zhouhao 3a45ac86da refactor: 升级依赖 2025-06-19 16:05:08 +08:00
zhouhao f99b648b88 refactor: 优化兼容性 2025-06-12 14:05:48 +08:00
zhouhao d4645ad089 refactor: 优化查询条件转换 2025-06-03 19:14:08 +08:00
zhouhao 88e0dd4667 Merge remote-tracking branch 'origin/master' 2025-05-27 10:41:27 +08:00
zhouhao 1bd136f3d2 refactor: 移除无用统计 2025-05-27 10:41:14 +08:00
Zhang Ji fc651ecd9d
fix(场景联动): 修复执行动作的内置参数获取可能失效的问题 (#636) 2025-05-26 19:03:49 +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
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 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
老周 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
老周 fd2d73d4eb
Update README.md 2025-02-17 09:12:45 +08:00
老周 2e487a42e2
feat(基础模块): 增加命令模式支持 (#607)
可使用 CommandSupportManagerProviders相关方法来执行服务命令进行解耦.
2025-02-13 12:33:18 +08:00
bestfeng1020 694595d446
fix(docker配置): 修复文件路径挂载错误 (#602)
* fix(docker配置): 修复文件路径挂载错误

* Update docker-compose.yml

---------

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

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

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

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

* Update pom.xml

---------

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

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

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

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

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

---------

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

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

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

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

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

* Update pom.xml

---------

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

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

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

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

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

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

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

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

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

* Update README.md

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

value为空时可能抛出异常

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

value为空时可能抛出异常

* fix(通知): 还原版本

---------

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-17 09:34:31 +08:00
zhouhao 037c790550 fix(TDEngine): 修复TDEngine数据库事件精度不是毫秒时无法按时间查询问题 2023-04-14 17:50:10 +08:00
zhouhao cf52a6ce68 Merge remote-tracking branch 'origin/master' 2023-04-13 16:58:16 +08:00
zhouhao 9a3d67e3fb refactor(配置): 本地地址使用127.0.0.1 2023-04-13 16:58:04 +08:00
bestfeng1020 b0d7b65148
feat(查询条件): 添加设备查询条件构造器 (#259) 2023-04-07 17:22:05 +08:00
bestfeng1020 e6e400b365
fix(阿里云短信): 解决短信模板和标签只能查询第一页数据问题 (#257) 2023-04-07 15:55:26 +08:00
zhouhao cb6ced51b5 Merge remote-tracking branch 'origin/master' 2023-04-07 10:35:36 +08:00
zhouhao 887bda8ebf feat(设备管理): 透传消息解析支持子设备会话创建 2023-04-07 10:35:23 +08:00
ayan 0ce219c04f fix(菜单管理)添加菜单ID为空时的默认值 2023-03-30 16:17:13 +08:00
zhouhao 8957c8b386 feat(日志): 访问日志增加过滤支持 2023-03-27 10:26:45 +08:00
zhouhao 4a421c980f feat(监控): 优化micrometer初始化逻辑,增加ignore配置 2023-03-27 10:19:46 +08:00
zhouhao 547e495d56 feat(设备管理): 增加产品保存时自动同步相关信息到设备表 2023-03-24 16:19:39 +08:00
zhouhao dd2ea79640 refactor(基础模块): 移除无用代码 2023-03-23 14:32:47 +08:00
zhouhao 67901c3732 feat(maven): Update new r2dbc-mysql driver #mirromutth/r2dbc-mysql/issues/251 2023-03-23 14:32:28 +08:00
zhouhao 552dfd550b fix(基础模块): 修复js脚本中无法使用console问题 2023-03-14 16:51:09 +08:00
zhouhao d0208967f0 fix(设备管理): 导入设备时,忽略空字符串的配置 2023-03-09 11:12:04 +08:00
zhouhao 3fc1b11e29 fix(设备管理): 修复注销产品后设备中心中的缓存未清理问题 2023-03-08 14:20:45 +08:00
zhouhao a6503b49e9 feat(TDEngine): 优化错误处理,查询时找不到表时不抛出错误。 2023-03-06 17:40:10 +08:00
zhouhao 6a186d6eba feat(设备管理): 优化设备详情的标签排序逻辑 2023-03-02 09:46:07 +08:00
zhouhao 703012c2c3 Merge remote-tracking branch 'origin/master' 2023-02-28 15:55:33 +08:00
zhouhao 0efeff2dd7 feat(repo): add build profile 2023-02-28 15:55:16 +08:00
bestfeng1020 d2262e278a
fix(接口缺失): 添加菜单和权限数据验证接口 (#245) 2023-02-27 15:08:40 +08:00
bestfeng1020 1e58dbb944
添加透传消息转换支持 (#237) 2023-02-15 13:52:10 +08:00
zhouhao 57211334c3 RUN true 2023-02-13 12:38:29 +08:00
zhouhao 6640f1bdf0 优化 2023-02-13 12:01:31 +08:00
zhouhao 2076a69a56 优化 2023-02-10 13:59:03 +08:00
zhouhao aaa6889b8d Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
2023-02-10 13:57:11 +08:00
zhouhao 8172baf52a Merge branch '2.0'
# Conflicts:
#	jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/client/VertxMqttClient.java
#	jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/gateway/device/MqttServerDeviceGateway.java
#	jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/server/vertx/VertxMqttConnection.java
#	jetlinks-components/network-component/mqtt-component/src/main/java/org/jetlinks/community/network/mqtt/server/vertx/VertxMqttServerProvider.java
#	jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/client/VertxTcpClient.java
#	jetlinks-components/network-component/tcp-component/src/main/java/org/jetlinks/community/network/tcp/server/TcpServerProvider.java
#	jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/message/DeviceMessageMeasurement.java
#	jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceMessageBusinessHandler.java
#	pom.xml
2023-02-10 13:51:42 +08:00
Zhang Ji 203f97afc9
同步README文档链接 (#230) 2023-01-10 11:42:44 +08:00
老周 2c6b85e8af
Update README.md 2022-12-23 17:10:36 +08:00
zhouhao 9b2b6d224f getLong 2022-11-07 10:28:53 +08:00
zhouhao c67d7b99ce 优化资源释放 2022-11-02 18:13:30 +08:00
zhouhao 7540ab9cac 修复tcp可能内存泄漏 2022-11-02 16:58:12 +08:00
zhouhao 812ecd26b3 增加自动刷新会话 2022-11-01 10:23:25 +08:00
zhouhao 4c86f9d971 优化状态同步 2022-10-31 10:08:07 +08:00
dependabot[bot] 3749e9e06d
Bump commons-text (#209)
Bumps commons-text from 1.9 to 1.10.0.

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-24 17:22:57 +08:00
老周 c8d8926b0f
Update maven.yml 2022-10-24 17:17:59 +08:00
zhouhao 9e4baa2993 Merge remote-tracking branch 'origin/master' 2022-10-24 11:12:45 +08:00
zhouhao eda3a24a80 优化子设备会话处理 2022-10-24 11:12:32 +08:00
老周 0cc7378389
Merge pull request #200 from vvsd/oscs_fix_cd36lv0au51of7vbl1j0
fix(sec): upgrade org.bouncycastle:bcprov-jdk15on to 1.69
2022-10-12 17:54:41 +08:00
vvsd d5f8a6f7b0 update org.bouncycastle:bcprov-jdk15on 1.67 to 1.69 2022-10-12 15:21:39 +08:00
zhouhao 4c18ef264f 优化线程池逻辑 2022-10-09 13:53:07 +08:00
zhouhao 4a9977903e 优化仓库地址 2022-09-28 10:03:52 +08:00
752 changed files with 39712 additions and 6038 deletions

View File

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

View File

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

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

3
.gitignore vendored
View File

@ -28,4 +28,5 @@ docker/data
!device-simulator.jar
!demo-protocol-1.0.jar
application-local.yml
dev/
dev/
.DS_Store

View File

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

View File

@ -1,15 +1,18 @@
# JetLinks 物联网基础平台
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/jetlinks/jetlinks-community/Auto%20Deploy%20Docker?label=docker)
![Version](https://img.shields.io/badge/version-2.0--RELEASE-brightgreen)
![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/jetlinks/jetlinks-community/maven.yml?branch=master)
![Version](https://img.shields.io/badge/version-2.3-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)
![jetlinks](https://visitor-badge.glitch.me/badge?page_id=jetlinks)
[![star](https://img.shields.io/github/stars/jetlinks/jetlinks-community?style=social)](https://github.com/jetlinks/jetlinks-community)
[![star](https://gitee.com/jetlinks/jetlinks-community/badge/star.svg?theme=gvp)](https://gitee.com/jetlinks/jetlinks-community/stargazers)
[![QQ①群2021514](https://img.shields.io/badge/QQ①群-2021514-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=LGf0OPQqvLGdJIZST3VTcypdVWhdfAOG&jump_from=webapi)
[![QQ②群324606263](https://img.shields.io/badge/QQ②群-324606263-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=IMas2cH-TNsYxUcY8lRbsXqPnA2sGHYQ&jump_from=webapi)
[![QQ③群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⑥群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等开发,
是一个开箱即用,可二次开发的企业级物联网基础平台。平台实现了物联网相关的众多基础功能,
@ -58,13 +61,62 @@ TCP/UDP/MQTT/HTTP、TLS/DTLS、不同厂商、不同设备、不同报文、统
------|------|----dev-env # 启动开发环境
------|------|----run-all # 启动全部,通过http://localhost:9000 访问系统.
------|----jetlinks-components # 公共组件模块
------|-------|----common-component # 通用组件.
------|-------|----configuration-component # 通用配置.
------|-------|----dashboard-component # 仪表盘.
------|-------|----datasource-component # 数据源.
------|-------|----elasticsearch-component # elasticsearch集成.
------|-------|----gateway-component # 网关组件,消息网关,设备接入.
------|-------|----io-component # IO 组件,Excel导入导出等.
------|-------|----logging-component # 日志组件
------|-------|----network-component # 网络组件,MQTT,TCP,CoAP,UDP等
------|-------|----notify-component # 通知组件,短信,右键等通知
------|-------|----protocol-component # 协议组件
------|-------|----relation-component # 关系组件
------|-------|----rule-engine-component # 规则引擎
------|-------|----script-component # 脚本组件
------|-------|----timeseries-component # 时序数据组件
------|-------|----tdengine-component # TDengine集成
------|-------|----things-component # 物组件
------|----jetlinks-manager # 业务管理模块
------|-------|----authentication-manager # 用户,权限管理
------|-------|----device-manager # 设备管理
------|-------|----logging-manager # 日志管理
------|-------|----network-manager # 网络组件管理
------|-------|----notify-manager # 通知管理
------|-------|----visualization-manager # 数据可视化管理
------|-------|----rule-engine-manager # 规则引擎管理
------|----jetlinks-standalone # 服务启动模块
------|----simulator # 设备模拟器
```
## 服务支持
我们提供了各种服务方式帮助您深入了解物联网平台和代码,通过产品文档、技术交流群、付费教学等方式,你将获得如下服务:
| 服务项 | 服务内容 | 服务收费 | 服务方式 |
|-----------|-----------------|--------|-------------|
| 基础问题答疑 | 问题答疑 | 免费 | 技术交流群支持 [![QQ⑤群554591908](https://img.shields.io/badge/QQ⑤群-554591908-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=jiirLiyFUecy_gsankzVQ-cl6SrZCnv9&&jump_from=webapi) [![QQ④群780133058](https://img.shields.io/badge/QQ④群-780133058-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=Gj47w9kg7TlV5ceD5Bqew_M_O0PIjh_l&jump_from=webapi) [![QQ③群647954464](https://img.shields.io/badge/QQ③群-647954464-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=K5m27CkhDn3B_Owr-g6rfiTBC5DKEY59&jump_from=webapi) [![QQ②群324606263](https://img.shields.io/badge/QQ②群-324606263-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=IMas2cH-TNsYxUcY8lRbsXqPnA2sGHYQ&jump_from=webapi) [![QQ①群2021514](https://img.shields.io/badge/QQ①群-2021514-brightgreen)](https://qm.qq.com/cgi-bin/qm/qr?k=LGf0OPQqvLGdJIZST3VTcypdVWhdfAOG&jump_from=webapi) |
| 系统部署 | 系统部署 | 免费 | 文档自助。[源码部署](https://hanta.yuque.com/px7kg1/yfac2l/vvoa3u2ztymtp4oh) [Docker部署](https://hanta.yuque.com/px7kg1/yfac2l/mzq23z4iey5ev1a5) |
| 产品使用 | 教学产品各功能使用 | 免费 | 文档自助。[产品文档](https://hanta.yuque.com/px7kg1/yfac2l) |
| 二次开发 | 教学平台源码开发过程、工具使用等;| 免费 | 文档自助。[开发文档](https://hanta.yuque.com/px7kg1/dev) |
| 系统部署 | 在客户指定的网络和硬件环境中完成社区版服务部署;提供**模拟**设备接入到平台中,并能完成正常设备上线、数据上下行 | 199元 | 线上部署支持 |
| 技术支持 | 提供各类部署、功能使用中遇到的问题答疑 | 100元 | 半小时内 线上远程支持|
| 设备接入协议开发 | 根据提供的设备型号,编写并提供接入平台协议包的源码。| 3000+元 | 定制化开发 |
| 其他服务 | 企业版源码购买;定制化开发;定制化时长、功能服务等 | 面议 | 面议 |
### **付费**服务支持或商务合作请联系
![qrCode.jpg](./qrCode.png)
## 文档
[快速开始](http://doc.jetlinks.cn/install-deployment/start-with-source.html)
[开发文档](http://doc.jetlinks.cn/dev-guide/start.html)
[常见问题](http://doc.jetlinks.cn/common-problems/install.html)
[产品文档](https://hanta.yuque.com/px7kg1/yfac2l)
[快速开始](https://hanta.yuque.com/px7kg1/yfac2l/raspyc4p1asfuxks)
[开发文档](https://hanta.yuque.com/px7kg1/nn1gdr)
[![Stargazers over time](https://starchart.cc/jetlinks/jetlinks-community.svg?variant=adaptive)](https://starchart.cc/jetlinks/jetlinks-community)

View File

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

View File

@ -48,7 +48,7 @@ services:
POSTGRES_DB: jetlinks
TZ: Asia/Shanghai
ui:
image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-pro:2.0.0
image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-ui-vue:2.3.0-SNAPSHOT
container_name: jetlinks-ce-ui
ports:
- 9000:80
@ -59,7 +59,7 @@ services:
links:
- jetlinks:jetlinks
jetlinks:
image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-standalone:2.0.0-SNAPSHOT
image: registry.cn-shenzhen.aliyuncs.com/jetlinks/jetlinks-community:2.3.0-SNAPSHOT
container_name: jetlinks-ce
ports:
@ -68,10 +68,8 @@ services:
- "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"
- "./data/jetlinks/upload:/application/static/upload"
- "./data/jetlinks:/application/data"
#entrypoint: /entrypoint.sh -d redis:5601,postgres:5432,elasticsearch:9200 'echo "start jetlinks service here"';
environment:
# - "SLEEP_SECOND=4"
@ -115,4 +113,4 @@ services:
depends_on:
- postgres
- redis
- elasticsearch
- elasticsearch

View File

@ -5,7 +5,8 @@
<parent>
<artifactId>jetlinks-components</artifactId>
<groupId>org.jetlinks.community</groupId>
<version>2.0.0-SNAPSHOT</version>
<version>2.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
@ -18,6 +19,11 @@
<version>${jetlinks.version}</version>
</dependency>
<dependency>
<groupId>org.jetlinks</groupId>
<artifactId>jetlinks-supports</artifactId>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-authorization-api</artifactId>
@ -48,12 +54,22 @@
<dependency>
<groupId>de.ruedigermoeller</groupId>
<artifactId>fst</artifactId>
<version>2.57</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<artifactId>h2-mvstore</artifactId>
</dependency>
<dependency>
<groupId>org.jetlinks.sdk</groupId>
<artifactId>jetlinks-sdk-api</artifactId>
<version>${jetlinks.sdk.version}</version>
</dependency>
<dependency>
<groupId>org.hswebframework.web</groupId>
<artifactId>hsweb-system-dictionary</artifactId>
</dependency>
<dependency>
@ -73,5 +89,11 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
</project>

View File

@ -1,6 +1,9 @@
package org.jetlinks.community;
import org.jetlinks.core.config.ConfigKey;
import org.jetlinks.core.metadata.MergeOption;
import java.util.Map;
/**
* 数据验证配置常量类
@ -20,4 +23,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,16 @@
package org.jetlinks.community;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class DynamicOperationType implements OperationType {
private String id;
private String name;
}

View File

@ -1,17 +1,20 @@
package org.jetlinks.community;
import com.alibaba.fastjson.annotation.JSONType;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
import lombok.*;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.function.BiFunction;
import java.util.function.Function;
@Getter
@AllArgsConstructor
@ -28,44 +31,43 @@ public class Interval {
public static final String hours = "h";
public static final String minutes = "m";
public static final String seconds = "s";
public static final String millis = "S";
private BigDecimal number;
private String expression;
public boolean isFixed() {
return expression.equalsIgnoreCase(hours) ||
expression.equals(minutes) ||
expression.equals(seconds);
}
public boolean isCalendar() {
return expression.equals(days) ||
expression.equals(month) ||
expression.equals(year);
}
@Override
public String toString() {
return (number) + expression;
}
@Generated
public static Interval ofSeconds(int seconds) {
return of(seconds, Interval.seconds);
}
@Generated
public static Interval ofDays(int days) {
return of(days, Interval.days);
}
@Generated
public static Interval ofHours(int hours) {
return of(hours, Interval.hours);
}
@Generated
public static Interval ofMonth(int month) {
return of(month, Interval.month);
}
@Generated
public static Interval ofMinutes(int month) {
return of(month, Interval.minutes);
}
@Generated
public static Interval of(int month, String expression) {
return new Interval(new BigDecimal(month), expression);
}
@ -107,6 +109,31 @@ public class Interval {
}
}
public IntervalUnit getUnit() {
switch (expression) {
case year:
return IntervalUnit.YEARS;
case quarter:
return IntervalUnit.QUARTER;
case month:
return IntervalUnit.MONTHS;
case weeks:
return IntervalUnit.WEEKS;
case days:
return IntervalUnit.DAYS;
case hours:
return IntervalUnit.HOURS;
case minutes:
return IntervalUnit.MINUTES;
case seconds:
return IntervalUnit.SECONDS;
case millis:
return IntervalUnit.MILLIS;
}
throw new UnsupportedOperationException("unsupported interval express:" + expression);
}
public static class IntervalJSONDeserializer extends JsonDeserializer<Interval> {
@Override
@ -121,6 +148,54 @@ public class Interval {
}
return of(node.textValue());
}
}
public static class IntervalJSONSerializer extends JsonSerializer<Interval> {
@Override
public void serialize(Interval value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value.toString());
}
}
public long toMillis() {
return getUnit().toMillis(number.intValue());
}
/**
* 对指定的时间戳按周期取整
*
* @param timestamp 时间戳
* @return 取整后的值
*/
public long round(long timestamp) {
return getUnit().truncatedTo(timestamp, number.intValue());
}
/**
* 按当前周期对指定的时间范围进行迭代,每次迭代一个周期的时间戳
*
* @param from 时间从
* @param to 时间止
* @return 迭代器
*/
public Iterable<Long> iterate(long from, long to) {
return getUnit().iterate(from, to, number.intValue());
}
public <T> Flux<T> generate(long from, long to, Function<Long, T> converter) {
return Flux
.fromIterable(iterate(from, to))
.map(converter);
}
public <T> Flux<T> generateWithFormat(long from,
long to,
String pattern,
BiFunction<Long, String, T> converter) {
DateTimeFormatter formatter = DateTimeFormat.forPattern(pattern);
return generate(from, to, t -> converter.apply(t, new DateTime(t).toString(formatter)));
}
}

View File

@ -0,0 +1,159 @@
package org.jetlinks.community;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Iterator;
/**
* 时间间隔单位可用于计算时间范围的间隔周期
*
* @author zhouhao
* @since 1.12
*/
@AllArgsConstructor
public enum IntervalUnit {
MILLIS(1, ChronoUnit.MILLIS),
SECONDS(1, ChronoUnit.SECONDS),
MINUTES(1, ChronoUnit.MINUTES),
HOURS(1, ChronoUnit.HOURS),
DAYS(1, ChronoUnit.DAYS),
WEEKS(1, ChronoUnit.WEEKS) {
@Override
protected LocalDateTime doTruncateTo(LocalDateTime time) {
return time.truncatedTo(ChronoUnit.DAYS)
.minusDays(time.getDayOfWeek().getValue() - 1);
}
},
MONTHS(1, ChronoUnit.MONTHS) {
@Override
protected LocalDateTime doTruncateTo(LocalDateTime time) {
return time.truncatedTo(ChronoUnit.DAYS)
.withDayOfMonth(1);
}
},
//季度
QUARTER(3, ChronoUnit.MONTHS) {
@Override
protected LocalDateTime doTruncateTo(LocalDateTime time) {
return time
.withMonth(time.getMonth().firstMonthOfQuarter().getValue())
.truncatedTo(ChronoUnit.DAYS)
.withDayOfMonth(1);
}
},
YEARS(1, ChronoUnit.YEARS) {
@Override
protected LocalDateTime doTruncateTo(LocalDateTime time) {
return time.truncatedTo(ChronoUnit.DAYS)
.withDayOfYear(1);
}
},
//不分区,永远返回0
FOREVER(1, ChronoUnit.FOREVER) {
@Override
public long truncatedTo(long timestamp) {
return 0;
}
@Override
public Iterable<Long> iterate(long from, long to, int duration) {
return () -> new Iterator<Long>() {
private boolean nexted;
@Override
public boolean hasNext() {
return !nexted;
}
@Override
public Long next() {
nexted = true;
return 0L;
}
};
}
};
@Getter
private final int durationOfUnit;
@Getter
private final ChronoUnit unit;
protected LocalDateTime doTruncateTo(LocalDateTime time) {
return time.truncatedTo(unit);
}
protected LocalDateTime next(LocalDateTime time, int duration) {
return time.plus((long) durationOfUnit * duration, unit);
}
protected long next(long timestamp) {
return next(timestamp, 1);
}
protected long next(long timestamp, int duration) {
return this
.next(LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC), duration)
.toInstant(ZoneOffset.UTC)
.toEpochMilli();
}
public long truncatedTo(long timestamp) {
return this
.doTruncateTo(LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC))
.toInstant(ZoneOffset.UTC)
.toEpochMilli();
}
public final long truncatedTo(long timestamp, int duration) {
long ts = truncatedTo(timestamp);
//指定了多个周期
if (Math.abs(duration) > 1) {
ts = next(ts, duration);
}
return ts;
}
public long toMillis(int duration) {
return duration * durationOfUnit * unit.getDuration().toMillis();
}
public final Iterable<Long> iterate(long from, long to) {
return iterate(from, to, 1);
}
/**
* 迭代时间区间的每一个周期时间
*
* @param from 时间从
* @param to 时间止
* @param duration 间隔数量,比如 2天为一个间隔
* @return 每个间隔的时间戳迭代器
*/
public Iterable<Long> iterate(long from, long to, int duration) {
return () -> new Iterator<Long>() {
long _from = truncatedTo(Math.min(from, to));
final long _to = truncatedTo(Math.max(from, to));
@Override
public boolean hasNext() {
return _from <= _to;
}
@Override
public Long next() {
long that = truncatedTo(_from, duration);
_from = IntervalUnit.this.next(_from, duration);
return that;
}
};
}
}

View File

@ -0,0 +1,12 @@
package org.jetlinks.community;
import org.hswebframework.web.exception.I18nSupportException;
public class JvmErrorException extends I18nSupportException {
public JvmErrorException(Throwable cause) {
super("error.jvm_error",cause);
}
}

View File

@ -0,0 +1,53 @@
package org.jetlinks.community;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetlinks.core.utils.SerializeUtils;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
/**
* 描述,用于对某些操作的通用描述.
*
* @author zhouhao
* @since 2.0
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class Operation implements Externalizable {
/**
* 操作来源
*/
private OperationSource source;
/**
* 操作类型比如: transparent-codec等
*/
private OperationType type;
@Override
public String toString() {
return type.getId() + "(" + type.getName() + "):[" + source.getId() + "]";
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
source.writeExternal(out);
SerializeUtils.writeObject(type.getId(), out);
SerializeUtils.writeObject(type.getName(), out);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
source = new OperationSource();
source.readExternal(in);
type = new DynamicOperationType((String) SerializeUtils.readObject(in), (String) SerializeUtils.readObject(in));
}
}

View File

@ -0,0 +1,65 @@
package org.jetlinks.community;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetlinks.core.utils.SerializeUtils;
import reactor.util.context.Context;
import reactor.util.context.ContextView;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Optional;
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
@Getter
@Setter
public class OperationSource implements Externalizable {
private static final long serialVersionUID = 1L;
/**
* ID,type对应操作的唯一标识
*/
private String id;
/**
* 操作源名称
*/
private String name;
/**
* 操作目标,通常为ID对应的详情数据
*/
private Object data;
public static OperationSource of(String id, Object data) {
return of(id, id, data);
}
public static Context ofContext(String id, String name, Object data) {
return Context.of(OperationSource.class, of(id, name, data));
}
public static Optional<OperationSource> fromContext(ContextView ctx) {
return ctx.getOrEmpty(OperationSource.class);
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeUTF(id);
SerializeUtils.writeObject(name, out);
SerializeUtils.writeObject(data, out);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
id = in.readUTF();
name = (String) SerializeUtils.readObject(in);
data = SerializeUtils.readObject(in);
}
}

View File

@ -0,0 +1,11 @@
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,13 @@
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 +16,82 @@ 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
*
* @see org.jetlinks.pro.tenant.TenantMember
*/
Key<List<String>> tenantId = Key.of("tenantId");
//分组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
*
* @see org.jetlinks.pro.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 +142,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 +162,11 @@ public interface PropertyConstants {
@Override
public T getDefaultValue() {
return defaultValue.get();
return defaultValue == null ? null : defaultValue.get();
}
};
}
}
}

View File

@ -29,8 +29,8 @@ public interface PropertyMetadataConstants {
static boolean isManual(DeviceMessage message) {
return message
.getHeader(PropertyMetadataConstants.Source.headerKey)
.map(PropertyMetadataConstants.Source.manual::equals)
.getHeader(Source.headerKey)
.map(Source.manual::equals)
.orElse(false);
}

View File

@ -15,21 +15,30 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.ezorm.core.param.Term;
import org.hswebframework.web.exception.ValidationException;
import org.hswebframework.web.i18n.LocaleUtils;
import org.jetlinks.community.terms.I18nSpec;
import org.reactivestreams.Subscription;
import org.springframework.util.Assert;
import reactor.core.CoreSubscriber;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;
import javax.annotation.Nonnull;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZonedDateTime;
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@Getter
@Setter
@ -40,6 +49,9 @@ public class TimerSpec implements Serializable {
@NotNull
private Trigger trigger;
@Schema(description = "使用日程标签进行触发")
private Set<String> scheduleTags;
//Cron表达式
@Schema(description = "触发方式为[cron]时不能为空")
private String cron;
@ -53,9 +65,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;
@ -63,6 +81,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;
@ -77,26 +106,49 @@ 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);
}
if (mod == ExecuteMod.once){
if (mod == ExecuteMod.once) {
LocalTime onceTime = once.localTime();
Predicate<LocalDateTime> predicate
= time -> time.toLocalTime().compareTo(onceTime) == 0;
= time -> compareOnceTime(time.toLocalTime(), onceTime) == 0;
return predicate.and(range);
}
return range;
}
public int compareOnceTime(LocalTime time1, LocalTime time2) {
int cmp = Integer.compare(time1.getHour(), time2.getHour());
if (cmp == 0) {
cmp = Integer.compare(time1.getMinute(), time2.getMinute());
if (cmp == 0) {
cmp = Integer.compare(time1.getSecond(), time2.getSecond());
//不比较纳秒
}
}
return cmp;
}
public String toCronExpression() {
return toCron().asString();
}
@ -215,6 +267,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());
}
@ -263,6 +322,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);
}
@ -280,10 +354,23 @@ public class TimerSpec implements Serializable {
* @return 构造器
*/
public Function<ZonedDateTime, Duration> nextDurationBuilder() {
Function<ZonedDateTime, ZonedDateTime> nextTime = nextTimeBuilder();
return time -> Duration.between(time, nextTime.apply(time));
return nextDurationBuilder(ZonedDateTime.now());
}
public Function<ZonedDateTime, Duration> nextDurationBuilder(ZonedDateTime baseTime) {
Iterator<ZonedDateTime> it = iterable().iterator(baseTime);
return (time) -> {
Duration duration;
do {
duration = Duration.between(time, time = it.next());
}
while (duration.toMillis() < 0);
return duration;
};
}
/**
* 创建一个时间构造器,通过构造器来获取下一次时间
* <pre>{@code
@ -340,11 +427,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;
@ -359,6 +475,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;
}
@ -386,7 +517,7 @@ public class TimerSpec implements Serializable {
public ZonedDateTime next() {
ZonedDateTime dateTime = current;
int max = MAX_IT_TIMES;
if (dateTime.toLocalTime().compareTo(onceTime) != 0){
if (!dateTime.toLocalTime().equals(onceTime)) {
dateTime = onceTime.atDate(dateTime.toLocalDate()).atZone(dateTime.getZone());
}
do {
@ -402,8 +533,111 @@ 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.cron || trigger == null) && cron != null){
if (trigger == Trigger.multi) {
return multiSpecIterable();
}
if ((trigger == Trigger.cron || trigger == null) && cron != null) {
return cronIterable();
}
return mod == ExecuteMod.period ? periodIterable() : onceIterable();
@ -418,10 +652,197 @@ public class TimerSpec implements Serializable {
return timeList;
}
public Flux<Long> flux() {
return flux(Schedulers.parallel());
}
public Flux<Long> flux(Scheduler scheduler) {
return new TimerFlux(nextDurationBuilder(), scheduler);
}
@Override
public String toString() {
if (getTrigger() == null) {
return null;
}
switch (getTrigger()) {
case week: {
return weekDesc();
}
case month: {
return monthDesc();
}
case cron: {
return getCron();
}
}
return null;
}
private String weekDesc() {
I18nSpec spec = new I18nSpec();
spec.setCode("message.timer_spec_desc");
List<I18nSpec> args = new ArrayList<>();
if (when == null || when.isEmpty()) {
args.add(I18nSpec.of("message.timer_spec_desc_everyday", "每天"));
} else {
String week = when
.stream()
.map(weekNum -> LocaleUtils.resolveMessage("message.timer_spec_desc_week_" + weekNum))
.collect(Collectors.joining(LocaleUtils.resolveMessage("message.timer_spec_desc_seperator")));
args.add(I18nSpec.of(
"message.timer_spec_desc_everyweek",
"每周" + week,
week));
}
args.add(timerModDesc());
spec.setArgs(args);
return spec.resolveI18nMessage();
}
private String monthDesc() {
I18nSpec spec = new I18nSpec();
spec.setCode("message.timer_spec_desc");
List<I18nSpec> args = new ArrayList<>();
if (when == null || when.isEmpty()) {
args.add(I18nSpec.of("message.timer_spec_desc_everyday", "每天"));
} else {
String month = when
.stream()
.map(monthNum -> {
switch (monthNum) {
case 1:
return LocaleUtils.resolveMessage("message.timer_spec_desc_month_1", monthNum);
case 2:
return LocaleUtils.resolveMessage("message.timer_spec_desc_month_2", monthNum);
case 3:
return LocaleUtils.resolveMessage("message.timer_spec_desc_month_3", monthNum);
default:
return LocaleUtils.resolveMessage("message.timer_spec_desc_month", monthNum);
}
})
.collect(Collectors.joining(LocaleUtils.resolveMessage("message.timer_spec_desc_seperator")));
args.add(I18nSpec.of(
"message.timer_spec_desc_everymonth",
"每月" + month,
month));
}
args.add(timerModDesc());
spec.setArgs(args);
return spec.resolveI18nMessage();
}
private I18nSpec timerModDesc() {
switch (getMod()) {
case period: {
if (getPeriod() == null) {
break;
}
return I18nSpec.of(
"message.timer_spec_desc_period",
getPeriod().getFrom() + "-" + getPeriod().getTo() +
"" + getPeriod().getEvery() + getPeriod().getUnit().name(),
I18nSpec.of("message.timer_spec_desc_period_duration",
getPeriod().getFrom() + "-" + getPeriod().getTo(),
getPeriod().getFrom(),
getPeriod().getTo()),
getPeriod().getEvery(),
I18nSpec.of("message.timer_spec_desc_period_" + getPeriod().getUnit().name(),
getPeriod().getUnit().name())
);
}
case once: {
// [time]执行1次
if (getOnce() == null) {
break;
}
return I18nSpec.of("message.timer_spec_desc_period_once", getOnce().getTime(), getOnce().getTime());
}
}
return I18nSpec.of("", "");
}
@AllArgsConstructor
static class TimerFlux extends Flux<Long> {
final Function<ZonedDateTime, Duration> spec;
final Scheduler scheduler;
@Override
public void subscribe(@Nonnull CoreSubscriber<? super Long> coreSubscriber) {
TimerSubscriber subscriber = new TimerSubscriber(spec, scheduler, coreSubscriber);
coreSubscriber.onSubscribe(subscriber);
}
}
static class TimerSubscriber implements Subscription {
final Function<ZonedDateTime, Duration> spec;
final CoreSubscriber<? super Long> subscriber;
final Scheduler scheduler;
long count;
Disposable scheduling;
public TimerSubscriber(Function<ZonedDateTime, Duration> spec,
Scheduler scheduler,
CoreSubscriber<? super Long> subscriber) {
this.scheduler = scheduler;
this.spec = spec;
this.subscriber = subscriber;
}
@Override
public void request(long l) {
trySchedule();
}
@Override
public void cancel() {
if (scheduling != null) {
scheduling.dispose();
}
}
public void onNext() {
if (canSchedule()) {
subscriber.onNext(count++);
}
trySchedule();
}
void trySchedule() {
if (scheduling != null) {
scheduling.dispose();
}
ZonedDateTime now = ZonedDateTime.ofInstant(Instant.ofEpochMilli(scheduler.now(TimeUnit.MILLISECONDS)), ZoneId.systemDefault());
Duration delay = spec.apply(now);
scheduling = scheduler
.schedule(
this::onNext,
delay.toMillis(),
TimeUnit.MILLISECONDS
);
}
protected boolean canSchedule() {
return true;
}
}
public enum Trigger {
//按周
week,
//按月
month,
cron
//cron表达式
cron,
// 多个触发组合
multi
}
public enum ExecuteMod {

View File

@ -2,6 +2,7 @@ package org.jetlinks.community;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.community.utils.TimeUtils;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.springframework.util.StringUtils;
import java.time.Duration;
@ -15,7 +16,7 @@ public interface ValueObject {
default Optional<Object> get(String name) {
return Optional.ofNullable(values())
.map(map -> map.get(name));
.map(map -> map.get(name));
}
default Optional<Integer> getInt(String name) {
@ -44,9 +45,8 @@ public interface ValueObject {
.map(Interval::of);
}
default Interval getInterval(String name,Interval defaultValue) {
return getString(name)
.map(Interval::of)
default Interval getInterval(String name, Interval defaultValue) {
return getInterval(name)
.orElse(defaultValue);
}
@ -56,9 +56,14 @@ public interface ValueObject {
}
default Optional<Date> getDate(String name) {
return get(name)
.map(String::valueOf)
.map(TimeUtils::parseDate);
return this
.get(name)
.map(d -> {
if (d instanceof Date) {
return (Date) d;
}
return TimeUtils.parseDate(String.valueOf(d));
});
}
default Date getDate(String name, Date defaultValue) {
@ -83,7 +88,8 @@ public interface ValueObject {
}
default Optional<Boolean> getBoolean(String name) {
return get(name, Boolean.class);
return get(name)
.map(CastUtils::castBoolean);
}
default boolean getBoolean(String name, boolean defaultValue) {
@ -95,8 +101,9 @@ public interface ValueObject {
.map(obj -> FastBeanCopier.DEFAULT_CONVERT.convert(obj, type, FastBeanCopier.EMPTY_CLASS_ARRAY));
}
static ValueObject of(Map<String, Object> mapVal) {
return () -> mapVal;
@SuppressWarnings("unchecked")
static ValueObject of(Map<String, ?> mapVal) {
return () -> (Map<String, Object>) mapVal;
}
default <T> T as(Class<T> type) {

View File

@ -8,6 +8,6 @@ public class Version {
private final String edition = "community";
private final String version = "2.0.0-SNAPSHOT";
private final String version = "2.2.0-SNAPSHOT";
}

View File

@ -0,0 +1,64 @@
package org.jetlinks.community.annotation.command;
import org.jetlinks.core.annotation.command.CommandHandler;
import org.springframework.stereotype.Indexed;
import java.lang.annotation.*;
/**
* 标记一个类为命令服务支持端点,用于对外提供命令支持
* <pre>{@code
*
* @CommandService("myService")
* public class MyCommandService{
*
* }
*
* }</pre>
*
* @author zhouhao
* @since 1.2.3
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Indexed
public @interface CommandService {
/**
* 服务标识
*
* @return 服务标识
*/
String id();
/**
* 服务名称
*
* @return 服务名称
*/
String name();
/**
* 服务描述
*
* @return 服务描述
*/
String[] description() default {};
/**
* 是否根据注解扫描注册服务
*
* @return 是否注册服务
*/
boolean autoRegistered() default true;
/**
* 命令定义,用于声明支持的命令
*
* @return 命令定义
*/
CommandHandler[] commands() default {};
}

View File

@ -0,0 +1,66 @@
package org.jetlinks.community.authorize;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.web.authorization.Authentication;
import org.hswebframework.web.authorization.DefaultDimensionType;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;
@Getter
@Setter
public class AuthenticationSpec implements Serializable {
private static final long serialVersionUID = 3512105446265694264L;
private RoleSpec role;
private List<PermissionSpec> permissions;
@Getter
@Setter
public static class RoleSpec {
private List<String> idList;
}
@Getter
@Setter
public static class PermissionSpec implements Serializable {
private static final long serialVersionUID = 7188197046015343251L;
private String id;
private List<String> actions;
}
public boolean isGranted(Authentication auth) {
return createFilter().test(auth);
}
public Predicate<Authentication> createFilter() {
RoleSpec role = this.role;
List<PermissionSpec> permissions = this.permissions;
List<Predicate<Authentication>> all = new ArrayList<>();
if (null != role && role.getIdList() != null) {
all.add(auth -> auth.hasDimension(DefaultDimensionType.role.getId(), role.getIdList()));
}
if (null != permissions) {
for (PermissionSpec permission : permissions) {
all.add(auth -> auth.hasPermission(permission.getId(), permission.getActions()));
}
}
Predicate<Authentication> temp = null;
for (Predicate<Authentication> predicate : all) {
if (temp == null) {
temp = predicate;
} else {
temp = temp.and(predicate);
}
}
return temp == null ? auth -> true : temp;
}
}

View File

@ -0,0 +1,433 @@
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,22 @@
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,81 @@
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,133 @@
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,55 @@
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

@ -22,4 +22,17 @@ 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

@ -10,19 +10,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 +31,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 +56,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 +83,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

@ -0,0 +1,31 @@
package org.jetlinks.community.buffer;
/**
* 已缓冲的数据
*
* @param <T> 数据类型
* @author zhouhao
* @since 2.2
*/
public interface Buffered<T> {
/**
* @return 数据
*/
T getData();
/**
* @return 当前重试次数
*/
int getRetryTimes();
/**
* 标记是否重试此数据
*/
void retry(boolean retry);
/**
* 标记此数据为死信
*/
void dead();
}

View File

@ -0,0 +1,10 @@
package org.jetlinks.community.buffer;
public enum ConsumeStrategy {
// 先进先出
FIFO,
// 后进先出
LIFO
}

View File

@ -0,0 +1,59 @@
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,59 @@
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,44 @@
package org.jetlinks.community.buffer;
/**
* 缓冲淘汰上下文
*
* @author zhouhao
* @since 2.0
*/
public interface EvictionContext {
/**
* 获取指定类型的数据量
*
* @param type 类型
* @return 数据量
*/
long size(BufferType type);
/**
* 删除最新的数据
*
* @param type 类型
*/
void removeLatest(BufferType type);
/**
* 删除最旧的数据
*
* @param type 类型
*/
void removeOldest(BufferType type);
/**
* @return 缓冲区名称, 用于区分多个不同的缓冲区
*/
String getName();
enum BufferType {
//缓冲区
buffer,
//死数据
dead
}
}

View File

@ -0,0 +1,37 @@
package org.jetlinks.community.buffer;
import lombok.AllArgsConstructor;
import java.util.Map;
@AllArgsConstructor
class SizeLimitEviction extends AbstractBufferEviction {
private final long bufferLimit;
private final long deadLimit;
@Override
public boolean doEviction(EvictionContext context) {
boolean anyEviction = false;
if (bufferLimit > 0 && context.size(EvictionContext.BufferType.buffer) >= bufferLimit) {
context.removeOldest(EvictionContext.BufferType.buffer);
anyEviction = true;
}
if (deadLimit > 0 && context.size(EvictionContext.BufferType.dead) >= deadLimit) {
context.removeOldest(EvictionContext.BufferType.dead);
anyEviction = true;
}
return anyEviction;
}
@Override
protected void applyEventData(Map<String, Object> data) {
data.put("bufferLimit", bufferLimit);
data.put("deadLimit", deadLimit);
}
@Override
public String toString() {
return "SizeLimit(buffer=" + bufferLimit + ", dead=" + deadLimit + ")";
}
}

View File

@ -0,0 +1,102 @@
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,121 @@
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,37 @@
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,307 @@
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,43 @@
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,75 @@
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,83 @@
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,138 @@
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,35 @@
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,34 @@
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,38 @@
package org.jetlinks.community.command.rule;
import io.swagger.v3.oas.annotations.media.Schema;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.core.command.AbstractCommand;
import org.jetlinks.core.command.CommandMetadataResolver;
import org.jetlinks.core.command.CommandUtils;
import org.jetlinks.core.metadata.FunctionMetadata;
import org.jetlinks.core.metadata.SimpleFunctionMetadata;
import org.jetlinks.community.command.rule.data.AlarmInfo;
import org.jetlinks.community.command.rule.data.AlarmResult;
import org.springframework.core.ResolvableType;
import reactor.core.publisher.Mono;
@Schema(title = "触发告警命令")
public class TriggerAlarmCommand extends AbstractCommand<Mono<AlarmResult>,TriggerAlarmCommand> {
private static final long serialVersionUID = 7056867872399432831L;
@Schema(description = "告警传参信息")
public AlarmInfo getAlarmInfo() {
return FastBeanCopier.copy(readable(), new AlarmInfo());
}
public TriggerAlarmCommand setAlarmInfo(AlarmInfo alarmInfo) {
return with(FastBeanCopier.copy(alarmInfo, writable()));
}
public static FunctionMetadata metadata() {
SimpleFunctionMetadata metadata = new SimpleFunctionMetadata();
metadata.setId(CommandUtils.getCommandIdByType(TriggerAlarmCommand.class));
metadata.setName("触发告警命令");
metadata.setInputs(CommandMetadataResolver.resolveInputs(ResolvableType.forClass(AlarmInfo.class)));
metadata.setOutput(CommandMetadataResolver.resolveOutput(ResolvableType.forClass(TriggerAlarmCommand.class)));
return metadata;
}
}

View File

@ -0,0 +1,74 @@
package org.jetlinks.community.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.jetlinks.community.terms.TermSpec;
import java.io.Serializable;
import java.util.Map;
/**
* 触发告警参数
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class AlarmInfo implements Serializable {
private static final long serialVersionUID = -2316376361116648370L;
@Schema(description = "告警配置ID")
private String alarmConfigId;
@Schema(description = "告警名称")
private String alarmName;
@Schema(description = "告警说明")
private String description;
@Schema(description = "告警级别")
private int level;
@Schema(description = "告警目标类型")
private String targetType;
@Schema(description = "告警目标ID")
private String targetId;
@Schema(description = "告警目标名称")
private String targetName;
@Schema(description = "告警来源类型")
private String sourceType;
@Schema(description = "告警来源ID")
private String sourceId;
@Schema(description = "告警来源的创建人ID")
private String sourceCreatorId;
@Schema(description = "告警来源名称")
private String sourceName;
/**
* 标识告警触发的配置来自什么业务功能
*/
@Schema(description = "告警配置源")
private String alarmConfigSource;
@Schema(description = "告警数据")
private Map<String, Object> data;
/**
* 告警触发条件
*/
private TermSpec termSpec;
@Schema(description = "告警时间")
private Long alarmTime;
}

View File

@ -0,0 +1,36 @@
package org.jetlinks.community.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.io.Serializable;
/**
* 告警结果
*/
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class AlarmResult implements Serializable {
private static final long serialVersionUID = -1752497262936740164L;
@Schema(description = "告警ID")
private String recordId;
@Schema(description = "是否重复告警")
private boolean alarming;
@Schema(description = "当前首次触发")
private boolean firstAlarm;
@Schema(description = "上一次告警时间")
private long lastAlarmTime;
@Schema(description = "首次告警或者解除告警后的再一次告警时间")
private long alarmTime;
}

View File

@ -0,0 +1,32 @@
package org.jetlinks.community.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 解除告警参数
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RelieveInfo extends AlarmInfo{
@Schema(description = "解除原因")
private String relieveReason;
@Schema(description = "解除时间")
private Long relieveTime;
@Schema(description = "解除说明")
private String describe;
/**
* 告警解除类型人工user系统system
*/
@Schema(description = "告警解除类型")
private String alarmRelieveType;
}

View File

@ -0,0 +1,32 @@
package org.jetlinks.community.command.rule.data;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 解除警告结果
*/
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RelieveResult extends AlarmResult{
@Schema(description = "告警级别")
private int level;
@Schema(description = "告警原因描述")
private String actualDesc;
@Schema(description = "解除原因")
private String relieveReason;
@Schema(description = "解除时间")
private long relieveTime;
@Schema(description = "解除说明")
private String describe;
}

View File

@ -3,6 +3,8 @@ package org.jetlinks.community.config;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.web.cache.ReactiveCache;
import org.hswebframework.web.cache.ReactiveCacheManager;
import org.jetlinks.community.ValueObject;
import org.jetlinks.community.config.entity.ConfigEntity;
import reactor.core.publisher.Flux;
@ -18,6 +20,13 @@ public class SimpleConfigManager implements ConfigManager, ConfigScopeManager {
private final ReactiveRepository<ConfigEntity, String> repository;
private final ReactiveCache<Map<String, Object>> cache;
public SimpleConfigManager(ReactiveRepository<ConfigEntity, String> repository, ReactiveCacheManager cacheManager) {
this.repository = repository;
this.cache = cacheManager.getCache("system-config");
}
@Override
public void addScope(ConfigScope scope,
List<ConfigPropertyDef> properties) {
@ -55,35 +64,38 @@ public class SimpleConfigManager implements ConfigManager, ConfigScopeManager {
.filter(def -> null != def.getDefaultValue())
.collectMap(ConfigPropertyDef::getKey, ConfigPropertyDef::getDefaultValue),
//数据库配置的值
repository
.createQuery()
.where(ConfigEntity::getScope, scope)
.fetch()
.filter(val -> MapUtils.isNotEmpty(val.getProperties()))
.<Map<String, Object>>reduce(new LinkedHashMap<>(), (l, r) -> {
l.putAll(r.getProperties());
return l;
}),
cache
.getMono(scope, () -> getPropertiesNow(scope)),
(defaults, values) -> {
defaults.forEach(values::putIfAbsent);
return values;
Map<String, Object> properties = new HashMap<>(values);
defaults.forEach(properties::putIfAbsent);
return properties;
}
).map(ValueObject::of);
)
.map(ValueObject::of);
}
private Mono<Map<String, Object>> getPropertiesNow(String scope) {
return repository
.createQuery()
.where(ConfigEntity::getScope, scope)
.fetch()
.filter(val -> MapUtils.isNotEmpty(val.getProperties()))
.reduce(new LinkedHashMap<>(), (l, r) -> {
l.putAll(r.getProperties());
return l;
});
}
@Override
public Mono<Void> setProperties(String scope, Map<String, Object> values) {
return Flux
.fromIterable(values.entrySet())
.map(e -> {
ConfigEntity entity = new ConfigEntity();
entity.setProperties(values);
entity.setScope(scope);
entity.getId();
return entity;
})
.as(repository::save)
.then();
ConfigEntity entity = new ConfigEntity();
entity.setProperties(values);
entity.setScope(scope);
entity.getId();
return repository
.save(entity)
.then(cache.evict(scope));
}
}

View File

@ -6,6 +6,7 @@ import lombok.Setter;
import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
import org.hswebframework.web.api.crud.entity.GenericEntity;
import org.hswebframework.web.crud.annotation.EnableEntityEvent;
import org.hswebframework.web.utils.DigestUtils;
import org.springframework.util.StringUtils;
@ -20,6 +21,7 @@ import java.util.Map;
})
@Getter
@Setter
@EnableEntityEvent
public class ConfigEntity extends GenericEntity<String> {
@Column(length = 64, nullable = false, updatable = false)

View File

@ -0,0 +1,95 @@
package org.jetlinks.community.config.verification;
import io.swagger.v3.oas.annotations.Operation;
import org.hswebframework.web.crud.events.EntitySavedEvent;
import org.hswebframework.web.exception.BusinessException;
import org.jetlinks.community.config.entity.ConfigEntity;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.net.UnknownHostException;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeoutException;
/**
* @author bestfeng
*/
@RestController
public class ConfigVerificationService {
private final WebClient webClient;
private static final String PATH_VERIFICATION_URI = "/system/config/base-path/verification";
public ConfigVerificationService() {
this.webClient = WebClient
.builder()
.build();
}
@GetMapping(value = PATH_VERIFICATION_URI)
@Operation(description = "basePath配置验证接口")
public Mono<String> basePathValidate() {
return Mono.just("auth:"+PATH_VERIFICATION_URI);
}
@EventListener
public void handleConfigSavedEvent(EntitySavedEvent<ConfigEntity> event){
//base-path校验
event.async(
Flux.fromIterable(event.getEntity())
.filter(config -> Objects.equals(config.getScope(), "paths"))
.flatMap(config-> doBasePathValidate(config.getProperties().get("base-path")))
);
}
public Mono<Void> doBasePathValidate(Object basePath) {
if (basePath == null) {
return Mono.empty();
}
URI uri = URI.create(CastUtils.castString(CastUtils.castString(basePath).concat(PATH_VERIFICATION_URI)));
if (Objects.equals(uri.getHost(), "127.0.0.1")){
return Mono.error(new BusinessException("error.base_path_host_error", 500, "127.0.0.1"));
}
if (Objects.equals(uri.getHost(), "localhost")){
return Mono.error(new BusinessException("error.base_path_host_error", 500, "localhost"));
}
return webClient
.get()
.uri(uri)
.exchangeToMono(cr -> {
if (cr.statusCode().is2xxSuccessful()) {
return cr.bodyToMono(String.class)
.filter(r-> r.contains("auth:"+PATH_VERIFICATION_URI))
.switchIfEmpty(Mono.error(()-> new BusinessException("error.base_path_error")));
}
return Mono.defer(() -> Mono.error(new BusinessException("error.base_path_error")));
})
.timeout(Duration.ofSeconds(3), Mono.error(TimeoutException::new))
.onErrorResume(err -> {
while (err != null) {
if (err instanceof TimeoutException) {
return Mono.error(() -> new BusinessException("error.base_path_validate_request_timeout"));
} else if (err instanceof UnknownHostException) {
return Mono.error(() -> new BusinessException("error.base_path_DNS_resolution_failed"));
}
err = err.getCause();
}
return Mono.error(() -> new BusinessException("error.base_path_error"));
})
.then();
}
}

View File

@ -37,14 +37,14 @@ public class SystemConfigManagerController {
@GetMapping("/scopes")
@QueryAction
@Operation(description = "获取配置作用域")
@Operation(summary = "获取配置作用域")
public Flux<ConfigScope> getConfigScopes() {
return configManager.getScopes();
}
@GetMapping("/{scope}")
@Authorize(ignore = true)
@Operation(description = "获取作用域下的全部配置信息")
@Operation(summary = "获取作用域下的全部配置信息")
public Mono<Map<String, Object>> getConfigs(@PathVariable String scope) {
return Authentication
.currentReactive()
@ -63,7 +63,7 @@ public class SystemConfigManagerController {
@GetMapping("/{scope}/_detail")
@QueryAction
@Operation(description = "获取作用域下的配置信息")
@Operation(summary = "获取作用域下的配置信息")
public Flux<ConfigPropertyValue> getConfigDetail(@PathVariable String scope) {
return configManager
.getProperties(scope)
@ -75,7 +75,7 @@ public class SystemConfigManagerController {
@PostMapping("/scopes")
@QueryAction
@Operation(description = "获取作用域下的配置详情")
@Operation(summary = "获取作用域下的配置详情")
public Flux<Scope> getConfigDetail(@RequestBody Mono<List<String>> scopeMono) {
return scopeMono
.flatMapMany(scopes -> Flux
@ -86,7 +86,7 @@ public class SystemConfigManagerController {
@PostMapping("/{scope}")
@SaveAction
@Operation(description = "保存配置")
@Operation(summary = "保存配置")
public Mono<Void> saveConfig(@PathVariable String scope,
@RequestBody Mono<Map<String, Object>> properties) {
return properties.flatMap(props -> configManager.setProperties(scope, props));
@ -95,11 +95,12 @@ public class SystemConfigManagerController {
@PostMapping("/scope/_save")
@SaveAction
@Operation(description = "批量保存配置")
@Operation(summary = "批量保存配置")
@Transactional
public Mono<Void> saveConfig(@RequestBody Flux<Scope> scope) {
return scope
.flatMap(scopeConfig -> configManager.setProperties(scopeConfig.getScope(), scopeConfig.getProperties()))
.concatMap(scopeConfig -> configManager.setProperties(scopeConfig.getScope(), scopeConfig.getProperties()))
.then();
}

View File

@ -3,48 +3,72 @@ package org.jetlinks.community.configuration;
import com.alibaba.fastjson.JSON;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import lombok.Generated;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.Converter;
import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.cache.ReactiveCacheManager;
import org.hswebframework.web.dict.EnumDict;
import org.hswebframework.web.dict.defaults.DefaultItemDefine;
import org.jetlinks.community.Interval;
import org.jetlinks.community.JvmErrorException;
import org.jetlinks.community.command.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.reactorql.aggregation.InternalAggregationSupports;
import org.jetlinks.community.reactorql.function.InternalFunctionSupport;
import org.jetlinks.community.reference.DataReferenceManager;
import org.jetlinks.community.reference.DataReferenceProvider;
import org.jetlinks.community.reference.DefaultDataReferenceManager;
import org.jetlinks.community.resource.DefaultResourceManager;
import org.jetlinks.community.resource.ResourceManager;
import org.jetlinks.community.resource.ResourceProvider;
import org.jetlinks.community.resource.TypeScriptDeclareResourceProvider;
import org.jetlinks.community.resource.initialize.PermissionResourceProvider;
import org.jetlinks.community.service.DefaultUserBindService;
import org.jetlinks.community.utils.TimeUtils;
import org.jetlinks.core.rpc.RpcManager;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.metadata.types.DataTypes;
import org.jetlinks.reactor.ql.feature.Feature;
import org.jetlinks.reactor.ql.supports.DefaultReactorQLMetadata;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.jetlinks.supports.official.JetLinksDataTypeCodecs;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.ReactiveRedisOperations;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.util.unit.DataSize;
import reactor.core.Exceptions;
import reactor.core.publisher.Hooks;
import javax.annotation.Nonnull;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
@Configuration
@AutoConfiguration
@SuppressWarnings("all")
@EnableConfigurationProperties({ConfigScopeProperties.class})
public class CommonConfiguration {
static {
InternalAggregationSupports.register();
InternalFunctionSupport.register();
BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() {
@Override
public <T> T convert(Class<T> aClass, Object o) {
@ -111,6 +135,53 @@ public class CommonConfiguration {
return (T)((Long) CastUtils.castNumber(value).longValue());
}
}, Long.class);
BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() {
@Override
@Generated
public <T> T convert(Class<T> type, Object value) {
if (value instanceof String) {
return (T) DefaultItemDefine.builder()
.value(String.valueOf(value))
.build();
}
return (T) FastBeanCopier.copy(value, new DefaultItemDefine());
}
}, EnumDict.class);
BeanUtilsBean.getInstance().getConvertUtils().register(new Converter() {
@Override
@Generated
public <T> T convert(Class<T> type, Object value) {
if (value instanceof Map) {
Map<String, Object> map = ((Map) value);
String typeId = (String) map.get("type");
if (StringUtils.isEmpty(typeId)) {
return null;
}
return (T) JetLinksDataTypeCodecs.decode(DataTypes.lookup(typeId).get(), map);
}
return null;
}
}, DataType.class);
//捕获jvm错误,防止Flux被挂起
Hooks.onOperatorError((err, val) -> {
if (Exceptions.isJvmFatal(err)) {
return new JvmErrorException(err);
}
return err;
});
Hooks.onNextError((err, val) -> {
if (Exceptions.isJvmFatal(err)) {
return new JvmErrorException(err);
}
return err;
});
}
@Bean
@ -129,15 +200,18 @@ public class CommonConfiguration {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(){
return builder->{
builder.deserializerByType(DataType.class, new DataTypeJSONDeserializer());
builder.deserializerByType(Date.class,new SmartDateDeserializer());
builder.deserializerByType(EnumDict.class, new DictionaryJsonDeserializer());
};
}
@Bean
public ConfigManager configManager(ObjectProvider<ConfigScopeCustomizer> configScopeCustomizers,
ReactiveRepository<ConfigEntity, String> repository) {
ReactiveRepository<ConfigEntity, String> repository,
ReactiveCacheManager cacheManager) {
SimpleConfigManager configManager = new SimpleConfigManager(repository);
SimpleConfigManager configManager = new SimpleConfigManager(repository,cacheManager);
for (ConfigScopeCustomizer customizer : configScopeCustomizers) {
customizer.custom(configManager);
}
@ -149,6 +223,11 @@ public class CommonConfiguration {
return new PermissionResourceProvider();
}
@Bean
public TypeScriptDeclareResourceProvider typeScriptDeclareResourceProvider() {
return new TypeScriptDeclareResourceProvider();
}
@Bean
public ResourceManager resourceManager(ObjectProvider<ResourceProvider> providers) {
DefaultResourceManager manager = new DefaultResourceManager();
@ -164,4 +243,19 @@ public class CommonConfiguration {
return referenceManager;
}
@Bean
public CommandServiceEndpointRegister commandServiceEndpointRegister() {
return new CommandServiceEndpointRegister();
}
@Configuration
@ConditionalOnClass(ReactiveRedisOperations.class)
static class DefaultUserBindServiceConfiguration {
@Bean
public DefaultUserBindService defaultUserBindService(ReactiveRedisOperations<Object, Object> redis) {
return new DefaultUserBindService(redis);
}
}
}

View File

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

@ -0,0 +1,24 @@
package org.jetlinks.community.configuration;
import org.jetlinks.community.resource.ui.UiMenuResourceProvider;
import org.jetlinks.community.resource.ui.UiResourceProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
@ConditionalOnProperty(prefix = "jetlinks.ui", name = "enabled", havingValue = "true", matchIfMissing = true)
public class UiResourceConfiguration {
@Bean
public UiResourceProvider uiResourceProvider() {
return new UiResourceProvider();
}
@Bean
public UiMenuResourceProvider uiMenuResourceProvider() {
return new UiMenuResourceProvider();
}
}

View File

@ -0,0 +1,88 @@
package org.jetlinks.community.dictionary;
import lombok.AllArgsConstructor;
import org.apache.commons.collections4.MapUtils;
import org.hswebframework.web.dict.EnumDict;
import org.hswebframework.web.dictionary.entity.DictionaryItemEntity;
import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService;
import org.springframework.boot.CommandLineRunner;
import javax.annotation.Nonnull;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author bestfeng
*/
@AllArgsConstructor
public class DatabaseDictionaryManager implements DictionaryManager, CommandLineRunner{
private final DefaultDictionaryItemService dictionaryItemService;
private final Map<String, Map<String, DictionaryItemEntity>> itemStore = new ConcurrentHashMap<>();
@Nonnull
@Override
public List<EnumDict<?>> getItems(@Nonnull String dictId) {
Map<String, DictionaryItemEntity> itemEntityMap = itemStore.get(dictId);
if (MapUtils.isEmpty(itemEntityMap)) {
return Collections.emptyList();
}
return new ArrayList<>(itemEntityMap.values());
}
@Nonnull
@Override
public Optional<EnumDict<?>> getItem(@Nonnull String dictId, @Nonnull String itemId) {
Map<String, DictionaryItemEntity> itemEntityMap = itemStore.get(dictId);
if (itemEntityMap == null) {
return Optional.empty();
}
return Optional.ofNullable(itemEntityMap.get(itemId));
}
public void registerItems(List<DictionaryItemEntity> items) {
items.forEach(this::registerItem);
}
public void removeItems(List<DictionaryItemEntity> items) {
items.forEach(this::removeItem);
}
public void removeItem(DictionaryItemEntity item) {
if (item == null || item.getDictId() == null || item.getId() == null) {
return;
}
itemStore.compute(item.getDictId(), (k, v) -> {
if (v != null) {
v.remove(item.getId());
if (!v.isEmpty()) {
return v;
}
}
return null;
});
}
public void registerItem(DictionaryItemEntity item) {
if (item == null || item.getDictId() == null) {
return;
}
itemStore
.computeIfAbsent(item.getDictId(), k -> new ConcurrentHashMap<>())
.put(item.getId(), item);
}
@Override
public void run(String... args) throws Exception {
dictionaryItemService
.createQuery()
.fetch()
.doOnNext(this::registerItem)
.subscribe();
}
}

View File

@ -0,0 +1,94 @@
package org.jetlinks.community.dictionary;
import org.hswebframework.web.dict.EnumDict;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 动态数据字典工具类
*
* @author zhouhao
* @since 2.1
*/
public class Dictionaries {
static DictionaryManager HOLDER = null;
static void setup(DictionaryManager manager) {
HOLDER = manager;
}
/**
* 获取字典的所有选型
*
* @param dictId 字典ID
* @return 字典值
*/
@Nonnull
public static List<EnumDict<?>> getItems(@Nonnull String dictId) {
return HOLDER == null ? Collections.emptyList() : HOLDER.getItems(dictId);
}
/**
* 根据掩码获取枚举选项,通常用于多选时获取选项.
*
* @param dictId 枚举ID
* @param mask 掩码
* @return 选项
* @see Dictionaries#toMask(Collection)
*/
@Nonnull
public static List<EnumDict<?>> getItems(@Nonnull String dictId, long mask) {
if (HOLDER == null) {
return Collections.emptyList();
}
return HOLDER
.getItems(dictId)
.stream()
.filter(item -> item.in(mask))
.collect(Collectors.toList());
}
/**
* 查找枚举选项
*
* @param dictId 枚举ID
* @param value 选项值
* @return 选项
*/
public static Optional<EnumDict<?>> findItem(@Nonnull String dictId, Object value) {
return getItems(dictId)
.stream()
.filter(item -> item.eq(value))
.findFirst();
}
/**
* 获取字段选型
*
* @param dictId 字典ID
* @param itemId 选项ID
* @return 选项值
*/
@Nonnull
public static Optional<EnumDict<?>> getItem(@Nonnull String dictId,
@Nonnull String itemId) {
return HOLDER == null ? Optional.empty() : HOLDER.getItem(dictId, itemId);
}
public static long toMask(Collection<EnumDict<?>> items) {
long value = 0L;
for (EnumDict<?> t1 : items) {
value |= t1.getMask();
}
return value;
}
}

View File

@ -0,0 +1,71 @@
package org.jetlinks.community.dictionary;
import org.hswebframework.ezorm.rdb.mapping.annotation.Codec;
import org.hswebframework.web.dict.EnumDict;
import java.lang.annotation.*;
import java.util.Collection;
/**
* 定义字段是一个数据字典,和枚举的使用方式类似.
* <p>
* 区别是数据的值通过{@link Dictionaries}进行获取.
*
* <pre>{@code
* public class MyEntity{
*
* //数据库存储的是枚举的值
* @Column(length=32)
* @Dictionary("my_status")
* @ColumnType(javaType=String.class)
* private EnumDict<String> status;
*
* @Column
* @Dictionary("my_types")
* //使用long来存储数据,表示使用字段的序号来进行mask运算进行存储.
* @ColumnType(javaType=Long.class,jdbcType=JDBCType.BIGINT)
* private EnumDict<String>[] types;
* }
* }</pre>
* <b>️注意</b>
* <ul>
* <li>
* 字段类型只支持{@code EnumDict<String>},{@code EnumDict<String>[]},{@code List<EnumDict<String>>}
* </li>
* <li>
* 多选时建议使用位掩码来存储: {@code @ColumnType(javaType=Long.class,jdbcType=JDBCType.BIGINT) },便于查询.
* </li>
* <li>使用位掩码存储字典值时,基于{@link EnumDict#ordinal()}进行计算,因此字段选项数量不能超过64个,修改字典时,请注意序号值变化</li>
* <li>模块需要引入依赖:<pre>{@code
* <dependency>
* <groupId>org.hswebframework.web</groupId>
* <artifactId>hsweb-system-dictionary</artifactId>
* </dependency>
* }</pre></li>
* </ul>
*
*
* @author zhouhao
* @see EnumDict#getValue()
* @see EnumDict#getMask()
* @see Dictionaries
* @see Dictionaries#toMask(Collection)
* @see DictionaryManager
* @since 2.2
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Codec
public @interface Dictionary {
/**
* 数据字典ID
*
* @return 数据字典ID
* @see Dictionaries#getItem(String, String)
*/
String value();
}

View File

@ -0,0 +1,57 @@
package org.jetlinks.community.dictionary;
import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata;
import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.EnumFragmentBuilder;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.EnumInFragmentBuilder;
import org.hswebframework.web.crud.configuration.TableMetadataCustomizer;
import org.jetlinks.community.form.type.EnumFieldType;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.sql.JDBCType;
import java.util.List;
import java.util.Set;
public class DictionaryColumnCustomizer implements TableMetadataCustomizer {
@Override
public void customColumn(Class<?> entityType,
PropertyDescriptor descriptor,
Field field,
Set<Annotation> annotations,
RDBColumnMetadata column) {
Dictionary dictionary = annotations
.stream()
.filter(Dictionary.class::isInstance)
.findFirst()
.map(Dictionary.class::cast)
.orElse(null);
if (dictionary != null) {
Class<?> type = field.getType();
JDBCType jdbcType = (JDBCType) column.getType().getSqlType();
EnumFieldType codec = new EnumFieldType(
type.isArray() || List.class.isAssignableFrom(type),
dictionary.value(),
jdbcType)
.withArray(type.isArray())
.withFieldValueConverter(e -> e);
column.setValueCodec(codec);
if (codec.isToMask()) {
column.addFeature(EnumFragmentBuilder.eq);
column.addFeature(EnumFragmentBuilder.not);
column.addFeature(EnumInFragmentBuilder.of(column.getDialect()));
column.addFeature(EnumInFragmentBuilder.ofNot(column.getDialect()));
}
}
}
@Override
public void customTable(Class<?> entityType, RDBTableMetadata table) {
}
}

View File

@ -0,0 +1,48 @@
package org.jetlinks.community.dictionary;
import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService;
import org.hswebframework.web.dictionary.service.DefaultDictionaryService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DictionaryConfiguration {
@Configuration
@ConditionalOnClass(DefaultDictionaryItemService.class)
//@ConditionalOnBean(DefaultDictionaryItemService.class)
public static class DictionaryManagerConfiguration {
@Bean
public DictionaryEventHandler dictionaryEventHandler(DefaultDictionaryItemService service) {
return new DictionaryEventHandler(service);
}
@Bean
public DatabaseDictionaryManager defaultDictionaryManager(DefaultDictionaryItemService service) {
DatabaseDictionaryManager dictionaryManager = new DatabaseDictionaryManager(service);
Dictionaries.setup(dictionaryManager);
return dictionaryManager;
}
@Bean
public DictionaryColumnCustomizer dictionaryColumnCustomizer() {
return new DictionaryColumnCustomizer();
}
@Bean
@ConfigurationProperties(prefix = "jetlinks.dict")
public DictionaryInitManager dictionaryInitManager(ObjectProvider<DictionaryInitInfo> initInfo,
DefaultDictionaryService defaultDictionaryService,
DefaultDictionaryItemService itemService) {
return new DictionaryInitManager(initInfo, defaultDictionaryService, itemService);
}
}
}

View File

@ -0,0 +1,9 @@
package org.jetlinks.community.dictionary;
public interface DictionaryConstants {
/**
* 系统分类标识
*/
String CLASSIFIED_SYSTEM = "system";
}

View File

@ -0,0 +1,95 @@
package org.jetlinks.community.dictionary;
import lombok.AllArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.hswebframework.web.crud.events.*;
import org.hswebframework.web.dictionary.entity.DictionaryEntity;
import org.hswebframework.web.dictionary.entity.DictionaryItemEntity;
import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService;
import org.hswebframework.web.exception.BusinessException;
import org.springframework.context.event.EventListener;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author bestfeng
*/
@AllArgsConstructor
public class DictionaryEventHandler implements EntityEventListenerCustomizer {
private final DefaultDictionaryItemService itemService;
@EventListener
public void handleDictionaryCreated(EntityCreatedEvent<DictionaryEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.flatMap(dictionary -> {
if (!CollectionUtils.isEmpty(dictionary.getItems())) {
return Flux
.fromIterable(dictionary.getItems())
.doOnNext(item -> item.setDictId(dictionary.getId()))
.as(itemService::save);
}
return Mono.empty();
})
);
}
@EventListener
public void handleDictionarySaved(EntitySavedEvent<DictionaryEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.flatMap(dictionary -> {
if (!CollectionUtils.isEmpty(dictionary.getItems())) {
return Flux
.fromIterable(dictionary.getItems())
.doOnNext(item -> item.setDictId(dictionary.getId()))
.as(itemService::save);
}
return Mono.empty();
})
);
}
@EventListener
public void handleDictionaryDeleted(EntityDeletedEvent<DictionaryEntity> event) {
event.async(
Flux.fromIterable(event.getEntity())
.map(DictionaryEntity::getId)
.collectList()
.flatMap(dictionary -> itemService
.createDelete()
.where()
.in(DictionaryItemEntity::getDictId, dictionary)
.execute()
.then())
);
}
@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();
})
);
}
}

View File

@ -0,0 +1,23 @@
package org.jetlinks.community.dictionary;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.web.dictionary.entity.DictionaryEntity;
import reactor.core.publisher.Flux;
import java.util.Collection;
/**
* @author gyl
* @since 2.2
*/
public interface DictionaryInitInfo {
Collection<DictionaryEntity> getDict();
default Flux<DictionaryEntity> getDictAsync() {
if (CollectionUtils.isEmpty(getDict())) {
return Flux.empty();
}
return Flux.fromIterable(getDict());
}
}

View File

@ -0,0 +1,81 @@
package org.jetlinks.community.dictionary;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.hswebframework.web.crud.events.EntityEventHelper;
import org.hswebframework.web.dictionary.entity.DictionaryEntity;
import org.hswebframework.web.dictionary.entity.DictionaryItemEntity;
import org.hswebframework.web.dictionary.service.DefaultDictionaryItemService;
import org.hswebframework.web.dictionary.service.DefaultDictionaryService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.CommandLineRunner;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/**
* @author gyl
* @since 2.2
*/
@Slf4j
public class DictionaryInitManager implements CommandLineRunner {
@Getter
@Setter
private List<DictionaryEntity> inits = new ArrayList<>();
public final ObjectProvider<DictionaryInitInfo> initInfo;
private final DefaultDictionaryService defaultDictionaryService;
private final DefaultDictionaryItemService itemService;
public DictionaryInitManager(ObjectProvider<DictionaryInitInfo> initInfo, DefaultDictionaryService defaultDictionaryService, DefaultDictionaryItemService itemService) {
this.initInfo = initInfo;
this.defaultDictionaryService = defaultDictionaryService;
this.itemService = itemService;
}
@Override
public void run(String... args) {
Flux
.merge(
Flux.fromIterable(inits),
Flux
.fromIterable(initInfo)
.flatMap(DictionaryInitInfo::getDictAsync)
)
.buffer(200)
.filter(CollectionUtils::isNotEmpty)
.flatMap(collection -> {
List<DictionaryItemEntity> items = generateItems(collection);
return defaultDictionaryService
.save(collection)
.mergeWith(itemService.save(items));
})
.as(EntityEventHelper::setDoNotFireEvent)
.subscribe(ignore -> {
},
err -> log.error("init dict error", err));
}
public List<DictionaryItemEntity> generateItems(List<DictionaryEntity> dictionaryList) {
List<DictionaryItemEntity> items = new ArrayList<>();
for (DictionaryEntity dictionary : dictionaryList) {
if (!CollectionUtils.isEmpty(dictionary.getItems())) {
for (DictionaryItemEntity item : dictionary.getItems()) {
item.setDictId(dictionary.getId());
items.add(item);
}
}
}
return items;
}
}

View File

@ -0,0 +1,40 @@
package org.jetlinks.community.dictionary;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.dict.EnumDict;
import org.hswebframework.web.dict.defaults.DefaultItemDefine;
import java.io.IOException;
import java.util.Map;
public class DictionaryJsonDeserializer extends JsonDeserializer<EnumDict<?>> {
@Override
public EnumDict<?> deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException, JacksonException {
if (jsonParser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
DefaultItemDefine defaultItemDefine = new DefaultItemDefine();
defaultItemDefine.setOrdinal(jsonParser.getIntValue());
return defaultItemDefine;
}
if (jsonParser.hasToken(JsonToken.VALUE_STRING)) {
String str = jsonParser.getText().trim();
if (!str.isEmpty()) {
DefaultItemDefine defaultItemDefine = new DefaultItemDefine();
defaultItemDefine.setValue(str);
return defaultItemDefine;
}
}
if (jsonParser.hasToken(JsonToken.START_OBJECT)) {
Map<?, ?> map = ctxt.readValue(jsonParser, Map.class);
if (map != null) {
return FastBeanCopier.copy(map, new DefaultItemDefine());
}
}
return null;
}
}

View File

@ -0,0 +1,41 @@
package org.jetlinks.community.dictionary;
import org.hswebframework.web.dict.EnumDict;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Optional;
/**
* 数据字典管理器,用于获取数据字典的枚举值
*
* @author zhouhao
* @since 2.1
*/
public interface DictionaryManager {
/**
* 获取字典的所有选项
*
* @param dictId 字典ID
* @return 字典值
*/
@Nonnull
List<EnumDict<?>> getItems(@Nonnull String dictId);
/**
* 获取字段选项
*
* @param dictId 字典ID
* @param itemId 选项ID
* @return 选项值
*/
@Nonnull
Optional<EnumDict<?>> getItem(@Nonnull String dictId,
@Nonnull String itemId);
}

View File

@ -0,0 +1,15 @@
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,35 @@
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,77 @@
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,48 @@
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,22 @@
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,13 @@
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,54 @@
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

@ -0,0 +1,204 @@
package org.jetlinks.community.form.type;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.hswebframework.ezorm.core.ValueCodec;
import org.hswebframework.web.bean.FastBeanCopier;
import org.hswebframework.web.dict.EnumDict;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.metadata.types.EnumType;
import org.jetlinks.community.dictionary.Dictionaries;
import org.jetlinks.community.utils.ConverterUtils;
import org.jetlinks.reactor.ql.utils.CastUtils;
import java.sql.JDBCType;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @since 2.1
*/
@Getter
@RequiredArgsConstructor
public class EnumFieldType implements FieldType, ValueCodec<Object, Object> {
public static final String TYPE = "Enum";
private final boolean multiple;
private final String dictId;
private final JDBCType jdbcType;
private boolean array = false;
private Function<EnumDict<?>,Object> fieldValueConverter = EnumDict::getWriteJSONObject;;
public EnumFieldType withArray(boolean array) {
this.array = array;
return this;
}
public EnumFieldType withFieldValueConverter(Function<EnumDict<?>,Object> converter) {
this.fieldValueConverter = converter;
return this;
}
@Override
public final String getId() {
return TYPE;
}
public boolean isToMask() {
return multiple && jdbcType == JDBCType.BIGINT;
}
@Override
public Class<?> getJavaType() {
return isToMask() ? Long.class : String.class;
}
@Override
public JDBCType getJdbcType() {
return jdbcType;
}
@Override
public int getLength() {
return 64;
}
@Override
public DataType getDataType() {
EnumType enumType = new EnumType();
for (EnumDict<?> item : Dictionaries.getItems(dictId)) {
enumType.addElement(EnumType.Element.of(String.valueOf(item.getValue()), item.getText()));
}
enumType.setMulti(multiple);
return enumType;
}
@Override
public ValueCodec<?, ?> getCodec() {
return this;
}
@Override
public Object encode(Object value) {
//转为位掩码
if (isToMask()) {
return Dictionaries.toMask(
ConverterUtils
.convertToList(value, val -> Dictionaries.findItem(dictId, val).orElse(null))
.stream()
.filter(Objects::nonNull)
.collect(Collectors.toSet()));
}
//多选,使用逗号分隔
if (multiple) {
return ConverterUtils
.convertToList(value, val -> Dictionaries.findItem(dictId, val).orElse(null))
.stream()
.filter(Objects::nonNull)
.map(e -> String.valueOf(e.getValue()))
.collect(Collectors.joining(","));
}
return Dictionaries
.findItem(dictId, value)
.<Object>map(EnumDict::getValue)
.orElseGet(() -> {
if (value instanceof EnumDict) {
return ((EnumDict<?>) value).getValue();
}
return value;
});
}
@Override
public Object decode(Object data) {
if (multiple) {
List<Object> list;
if (isToMask()) {
list =
Dictionaries
.getItems(dictId, CastUtils.castNumber(data).longValue())
.stream()
.map(fieldValueConverter)
.collect(Collectors.toList());
} else {
list = ConverterUtils
.convertToList(data, val -> Dictionaries.findItem(dictId, val).orElse(null))
.stream()
.filter(Objects::nonNull)
.map(fieldValueConverter)
.collect(Collectors.toList());
}
if (isArray()) {
return list.toArray(new EnumDict[0]);
}
return list;
}
if(isToMask()){
return Dictionaries
.getItems(dictId, CastUtils.castNumber(data).longValue())
.stream()
.map(fieldValueConverter)
.findFirst()
.orElse(null);
}
return Dictionaries
.findItem(dictId, data)
.map(fieldValueConverter)
.orElse(fieldValueConverter.apply(EnumDict.create(String.valueOf(data))));
}
public static class Provider implements FieldTypeProvider {
@Override
public String getProvider() {
return EnumFieldType.TYPE;
}
@Override
public String getProviderName() {
return "枚举";
}
@Override
public Set<JDBCType> getSupportJdbcTypes() {
return new HashSet<>(Arrays.asList(JDBCType.VARCHAR, JDBCType.BIGINT));
}
@Override
public int getDefaultLength() {
return 64;
}
@Override
public FieldType create(FieldTypeSpec configuration) {
DictionarySpec spec = Optional
.ofNullable(configuration.getConfiguration())
.map(configSpec -> FastBeanCopier.copy(configSpec, new DictionarySpec()))
.orElse(new DictionarySpec());
return new EnumFieldType(
spec.isMultiple(),
Optional
.ofNullable(spec.getDictionaryId())
.orElseThrow(() -> new IllegalArgumentException("dictionaryId can not be null")),
configuration.getJdbcType() == null ? JDBCType.VARCHAR : configuration.getJdbcType()
);
}
}
//数据字典
@Getter
@Setter
public static class DictionarySpec {
//字典ID
private String dictionaryId;
//多选
private boolean multiple;
}
}

View File

@ -0,0 +1,28 @@
package org.jetlinks.community.form.type;
import org.hswebframework.ezorm.core.ValueCodec;
import org.jetlinks.core.metadata.DataType;
import java.sql.JDBCType;
public interface FieldType {
String getId();
Class<?> getJavaType();
JDBCType getJdbcType();
ValueCodec<?, ?> getCodec();
default int getLength() {
return 255;
}
default int getScale() {
return 2;
}
DataType getDataType();
}

View File

@ -0,0 +1,31 @@
package org.jetlinks.community.form.type;
import org.jetlinks.community.spi.Provider;
import java.sql.JDBCType;
import java.util.Set;
public interface FieldTypeProvider {
Provider<FieldTypeProvider> supports = Provider.create(FieldTypeProvider.class);
String getProvider();
default String getProviderName() {
return getProvider();
}
default int getDefaultLength() {
return 255;
}
Set<JDBCType> getSupportJdbcTypes();
FieldType create(FieldTypeSpec configuration);
static FieldType createType(FieldTypeSpec spec) {
return supports
.getNow(spec.getName())
.create(spec);
}
}

View File

@ -0,0 +1,26 @@
package org.jetlinks.community.form.type;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.community.ValueObject;
import java.sql.JDBCType;
import java.util.Map;
@Getter
@Setter
public class FieldTypeSpec implements ValueObject {
private String name;
private int length;
private int scale;
private JDBCType jdbcType;
private Map<String,Object> configuration;
@Override
public Map<String, Object> values() {
return configuration;
}
}

View File

@ -0,0 +1,296 @@
package org.jetlinks.community.lock;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
import reactor.core.Disposable;
import reactor.core.publisher.*;
import reactor.core.scheduler.Schedulers;
import reactor.util.context.Context;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Duration;
import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.function.Consumer;
class DefaultReactiveLock implements ReactiveLock {
@SuppressWarnings("all")
static final AtomicReferenceFieldUpdater<DefaultReactiveLock, LockingSubscriber>
PENDING = AtomicReferenceFieldUpdater
.newUpdater(DefaultReactiveLock.class, LockingSubscriber.class, "pending");
final Deque<LockingSubscriber<?>> queue = new ConcurrentLinkedDeque<>();
volatile LockingSubscriber<?> pending;
@Override
public <T> Flux<T> lock(Flux<T> job) {
return new LockingFlux<>(this, job);
}
@Override
public <T> Flux<T> lock(Flux<T> flux, Duration timeout) {
return new LockingFlux<>(this, flux, timeout);
}
@Override
public <T> Flux<T> lock(Flux<T> flux, Duration timeout, Flux<? extends T> fallback) {
return new LockingFlux<>(this, flux, timeout, fallback);
}
@Override
public <T> Mono<T> lock(Mono<T> job) {
return new LockingMono<>(this, job);
}
@Override
public <T> Mono<T> lock(Mono<T> mono, Duration timeout) {
return new LockingMono<>(this, mono, timeout);
}
@Override
public <T> Mono<T> lock(Mono<T> mono, Duration timeout, Mono<? extends T> fallback) {
return new LockingMono<>(this, mono, timeout, fallback);
}
protected void drain() {
if (PENDING.get(this) != null) {
return;
}
LockingSubscriber<?> locking;
for (; ; ) {
locking = queue.pollFirst();
if (locking == null) {
return;
}
if (locking.isDisposed()) {
continue;
}
if (PENDING.compareAndSet(this, null, locking)) {
//使用单独的线程池来调度,防止参与锁太多导致栈溢出.
Schedulers.parallel().schedule(locking::subscribe);
} else {
queue.addLast(locking);
}
break;
}
}
<T> void registerSubscriber(CoreSubscriber<? super T> actual,
Consumer<CoreSubscriber<? super T>> subscribeCallback,
@Nullable Duration timeout,
@Nullable Publisher<? extends T> timeoutFallback) {
registerSubscriber(new LockingSubscriber<>(
this,
actual,
subscribeCallback,
timeout,
timeoutFallback));
}
void registerSubscriber(LockingSubscriber<?> subscriber) {
if (PENDING.compareAndSet(this, null, subscriber)) {
subscriber.subscribe();
return;
}
queue.addLast(subscriber);
drain();
}
static class LockingFlux<T> extends FluxOperator<T, T> {
private final DefaultReactiveLock main;
private Duration timeout;
private Publisher<? extends T> timeoutFallback;
protected LockingFlux(DefaultReactiveLock main, Flux<? extends T> source) {
super(source);
this.main = main;
}
protected LockingFlux(DefaultReactiveLock main, Flux<? extends T> source, Duration timeout) {
super(source);
this.main = main;
this.timeout = timeout;
}
protected LockingFlux(DefaultReactiveLock main, Flux<? extends T> source, Duration timeout, Flux<? extends T> timeoutFallback) {
super(source);
this.main = main;
this.timeout = timeout;
this.timeoutFallback = timeoutFallback;
}
@Override
public void subscribe(@Nonnull CoreSubscriber<? super T> actual) {
Consumer<CoreSubscriber<? super T>> subscribeCallback = source::subscribe;
main.registerSubscriber(actual, subscribeCallback, timeout, timeoutFallback);
}
}
static class LockingMono<T> extends MonoOperator<T, T> {
private final DefaultReactiveLock main;
private Duration timeout;
private Publisher<? extends T> fallback;
protected LockingMono(DefaultReactiveLock main, Mono<? extends T> source) {
super(source);
this.main = main;
}
protected LockingMono(DefaultReactiveLock main, Mono<? extends T> source, Duration timeout) {
super(source);
this.main = main;
this.timeout = timeout;
}
protected LockingMono(DefaultReactiveLock main, Mono<? extends T> source, Duration timeout, Mono<? extends T> fallback) {
super(source);
this.main = main;
this.timeout = timeout;
this.fallback = fallback;
}
@Override
public void subscribe(@Nonnull CoreSubscriber<? super T> actual) {
Consumer<CoreSubscriber<? super T>> subscribeCallback = source::subscribe;
main.registerSubscriber(actual, subscribeCallback, timeout, fallback);
}
}
static class LockingSubscriber<T> extends BaseSubscriber<T> {
protected final DefaultReactiveLock main;
protected final CoreSubscriber<? super T> actual;
private final Consumer<CoreSubscriber<? super T>> subscriber;
private Disposable timeoutTask;
private final Publisher<? extends T> timeoutFallback;
@SuppressWarnings("all")
protected static final AtomicIntegerFieldUpdater<LockingSubscriber> statusUpdater =
AtomicIntegerFieldUpdater.newUpdater(LockingSubscriber.class, "status");
private volatile int status;
//初始
private static final int INIT = 0;
//订阅备用流
private static final int SUB_TIMEOUT_FALLBACK = -1;
//订阅原本上游流
private static final int SUB_SOURCE = 1;
//流结束
private static final int UN_SUB = -2;
public LockingSubscriber(DefaultReactiveLock main,
CoreSubscriber<? super T> actual,
Consumer<CoreSubscriber<? super T>> subscriber,
@Nullable Duration timeout,
@Nullable Publisher<? extends T> timeoutFallback) {
this.actual = actual;
this.main = main;
this.subscriber = subscriber;
this.timeoutFallback = timeoutFallback;
if (timeout != null) {
this.timeoutTask = Schedulers
.parallel()
.schedule(this::onTimeout, timeout.toMillis(), TimeUnit.MILLISECONDS);
}
}
private void onTimeout() {
if (statusUpdater.compareAndSet(this, INIT, SUB_TIMEOUT_FALLBACK)) {
//不代理订阅直接取消流及释放当前锁以免并发时等待备用流释放锁
doComplete();
if (timeoutFallback != null) {
timeoutFallback.subscribe(actual);
} else {
this.onError(new TimeoutException("Lock timed out"));
}
}
}
protected void subscribe() {
if (statusUpdater.compareAndSet(this, INIT, SUB_SOURCE)) {
if (timeoutTask != null && !timeoutTask.isDisposed()) {
timeoutTask.dispose();
}
subscriber.accept(this);
}
}
protected void complete() {
if (statusUpdater.compareAndSet(this, INIT, UN_SUB) || statusUpdater.compareAndSet(this, SUB_SOURCE, UN_SUB)) {
if (timeoutTask != null && !timeoutTask.isDisposed()) {
timeoutTask.dispose();
}
doComplete();
}
}
protected void doComplete() {
//防止非hookFinally触发的结束
if (!this.isDisposed()) {
this.cancel();
}
if (PENDING.compareAndSet(main, this, null)) {
main.drain();
}
}
@Override
protected final void hookOnError(@Nonnull Throwable throwable) {
actual.onError(throwable);
}
@Override
protected final void hookOnNext(@Nonnull T value) {
actual.onNext(value);
}
@Override
protected final void hookOnSubscribe(@Nonnull Subscription subscription) {
actual.onSubscribe(this);
}
@Override
protected final void hookOnComplete() {
actual.onComplete();
}
@Override
protected final void hookOnCancel() {
super.hookOnCancel();
}
@Override
protected final void hookFinally(@Nonnull SignalType type) {
complete();
}
@Override
@Nonnull
public Context currentContext() {
return actual.currentContext();
}
}
}

View File

@ -0,0 +1,20 @@
package org.jetlinks.community.lock;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.time.Duration;
import java.util.Map;
class DefaultReactiveLockManager implements ReactiveLockManager {
private final Map<String, ReactiveLock> cache = Caffeine
.newBuilder()
.expireAfterAccess(Duration.ofMinutes(30))
.<String, ReactiveLock>build()
.asMap();
@Override
public ReactiveLock getLock(String name) {
return cache.computeIfAbsent(name, ignore -> new DefaultReactiveLock());
}
}

View File

@ -0,0 +1,76 @@
package org.jetlinks.community.lock;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
/**
* 响应式锁
*
* @author zhouhao
* @since 2.2
*/
public interface ReactiveLock {
/**
* 对Mono进行加锁,将等待之前的lock完成后再执行.
*
* @param mono 要加锁的Mono
* @param <T> T
* @return 加锁后的Mono
*/
<T> Mono<T> lock(Mono<T> mono);
/**
* 对Mono进行加锁,将等待之前的lock完成后再执行,若等待锁时间超过{@link Duration}则报错{@link java.util.concurrent.TimeoutException}
*
* @param mono 要加锁的Mono
* @param timeout 等待锁的时间
* @param <T> T
* @return 加锁后的Mono
*/
<T> Mono<T> lock(Mono<T> mono, Duration timeout);
/**
* 对Mono进行加锁,将等待之前的lock完成后再执行,若等待锁时间超过{@link Duration}则切换到回退流
*
* @param mono 要加锁的Mono
* @param timeout 等待锁的时间
* @param fallback 发生超时时要订阅的回退流
* @param <T> T
* @return 加锁后的Mono
*/
<T> Mono<T> lock(Mono<T> mono, Duration timeout, Mono<? extends T> fallback);
/**
* 对Flux进行加锁,将等待之前的lock完成后再执行.
*
* @param flux 要加锁的Flux
* @param <T> T
* @return 加锁后的Flux
*/
<T> Flux<T> lock(Flux<T> flux);
/**
* 对Flux进行加锁,将等待之前的lock完成后再执行,若等待锁时间超过{@link Duration}则报错{@link java.util.concurrent.TimeoutException}
*
* @param flux 要加锁的Flux
* @param timeout 等待锁的时间
* @param <T> T
* @return 加锁后的Mono
*/
<T> Flux<T> lock(Flux<T> flux, Duration timeout);
/**
* 对Flux进行加锁,将等待之前的lock完成后再执行,若等待锁时间超过{@link Duration}则切换到回退流
*
* @param flux 要加锁的Flux
* @param timeout 等待锁的时间
* @param fallback 发生超时时要订阅的回退流
* @param <T> T
* @return 加锁后的Mono
*/
<T> Flux<T> lock(Flux<T> flux, Duration timeout, Flux<? extends T> fallback);
}

View File

@ -0,0 +1,39 @@
package org.jetlinks.community.lock;
/**
* 响应式锁持有器,用于通过静态方法获取锁.
* <pre>{@code
*
* Mono<MyEntity> execute(MyEntity entity){
*
* return ReactiveLockHolder
* .getLock("lock-test:"+entity.getId())
* .lock(updateAndGet(entity));
*
* }
* }</pre>
*
* @author zhouhao
* @see ReactiveLock
* @see ReactiveLockManager
* @since 2.2
*/
public class ReactiveLockHolder {
private static ReactiveLockManager lockManager = new DefaultReactiveLockManager();
static void setup(ReactiveLockManager manager) {
lockManager = manager;
}
/**
* 根据锁名称获取锁
*
* @param name 锁名称
* @return
*/
public static ReactiveLock getLock(String name) {
return lockManager.getLock(name);
}
}

View File

@ -0,0 +1,20 @@
package org.jetlinks.community.lock;
/**
* 响应式锁管理器
*
* @author zhouhao
* @see ReactiveLock
* @since 2.2
*/
public interface ReactiveLockManager {
/**
* 根据名称获取一个锁.
*
* @param name 锁名称
* @return
*/
ReactiveLock getLock(String name);
}

View File

@ -0,0 +1,35 @@
package org.jetlinks.community.reactorql.aggregation;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments;
import org.hswebframework.ezorm.rdb.operator.dml.FunctionColumn;
import org.jetlinks.community.spi.Provider;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import java.util.function.Function;
/**
* 聚合函数支持.
*
* @author zhangji 2025/1/22
* @since 2.3
*/
public interface AggregationSupport extends Function<Publisher<?>, Mono<?>> {
Provider<AggregationSupport> supports = Provider.create(AggregationSupport.class);
String getId();
String getName();
SqlFragments createSql(FunctionColumn column);
static AggregationSupport getNow(String id) {
return AggregationSupport.supports
.get(id.toUpperCase())
.orElseGet(() -> AggregationSupport.supports
.get(id.toLowerCase())
.orElseGet(() -> AggregationSupport.supports.getNow(id)));
}
}

View File

@ -0,0 +1,82 @@
package org.jetlinks.community.reactorql.aggregation;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.BatchSqlFragments;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments;
import org.hswebframework.ezorm.rdb.operator.dml.FunctionColumn;
import org.jetlinks.reactor.ql.supports.agg.MapAggFeature;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.math.MathFlux;
import java.util.Comparator;
import java.util.function.Function;
import static org.jetlinks.reactor.ql.supports.DefaultReactorQLMetadata.addGlobal;
/**
*
* @author zhangji 2025/1/22
* @since 2.3
*/
@AllArgsConstructor
public enum InternalAggregationSupports implements AggregationSupport {
COUNT("总数", Flux::count, 0),
//去重计数
DISTINCT_COUNT("总数(去重)", flux -> flux.distinct().count(), 0) {
@Override
public SqlFragments createSql(FunctionColumn column) {
return new BatchSqlFragments().addSql("count(distinct ", column.getColumn(), ")");
}
},
MIN("最小值",
numberFlux -> MathFlux.min(numberFlux.map(CastUtils::castNumber), Comparator.comparing(Number::doubleValue)), null),
MAX("最大值", numberFlux -> MathFlux.max(numberFlux.map(CastUtils::castNumber), Comparator.comparing(Number::doubleValue)), null),
AVG("平均值", numberFlux -> MathFlux.averageDouble(numberFlux.map(CastUtils::castNumber), Number::doubleValue), null),
SUM("总和", numberFlux -> MathFlux.sumDouble(numberFlux.map(CastUtils::castNumber), Number::doubleValue), 0),
FIRST("第一个值", numberFlux -> numberFlux.take(1).singleOrEmpty(), null),
LAST("最后一个值", numberFlux -> numberFlux.takeLast(1).singleOrEmpty(), null),
// MEDIAN("中位数", numberFlux -> Mono.empty(), null),//中位数
// SPREAD("极差", numberFlux -> Mono.empty(), null),//差值
// STDDEV("标准差", numberFlux -> Mono.empty(), null),//标准差
;
static {
for (InternalAggregationSupports value : values()) {
addGlobal(new MapAggFeature(value.getId(), value::apply));
AggregationSupport.supports.register(value.getId(), value);
}
}
public static void register(){
}
@Getter
private final String name;
private final Function<Flux<?>, Mono<?>> computer;
@Getter
private final Object defaultValue;
@Override
public SqlFragments createSql(FunctionColumn column) {
return new BatchSqlFragments()
.addSql(name() + "(").addSql(column.getColumn()).addSql(")");
}
@Override
public String getId() {
return name();
}
@Override
public Mono<?> apply(Publisher<?> publisher) {
return computer.apply(Flux.from(publisher));
}
}

View File

@ -0,0 +1,25 @@
package org.jetlinks.community.reactorql.function;
import lombok.Getter;
import lombok.Setter;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.metadata.PropertyMetadata;
import java.util.List;
/**
*
* @author zhangji 2025/1/22
* @since 2.3
*/
@Getter
@Setter
public class FunctionInfo {
private String id;
private String name;
private DataType outputType;
private List<PropertyMetadata> args;
}

View File

@ -0,0 +1,72 @@
package org.jetlinks.community.reactorql.function;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.community.spi.Provider;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 函数支持,用于定义在可以在ReactorQL中使用的函数.
*
* @author zhangji 2025/1/22
* @since 2.3
*/
public interface FunctionSupport {
Provider<FunctionSupport> supports = Provider.create(FunctionSupport.class);
String getId();
String getName();
/**
* 是否支持列的数据类型
*
* @param type 数据类型
* @return 是否支持
*/
boolean isSupported(DataType type);
/**
* 获取输出数据类型
*
* @return 输出数据类型
*/
DataType getOutputType();
/**
* 创建SQL函数片段
*
* @param column 列名
* @param args 参数
* @return SQL函数片段
*/
SqlFragments createSql(String column, Map<String, Object> args);
/**
* 查找支持的函数
*
* @param type 数据类型
* @return 函数信息
*/
static List<FunctionInfo> lookup(DataType type) {
return supports
.getAll()
.stream()
.filter(support -> support.isSupported(type))
.map(FunctionSupport::toInfo)
.collect(Collectors.toList());
}
default FunctionInfo toInfo() {
FunctionInfo info = new FunctionInfo();
info.setId(getId());
info.setOutputType(getOutputType());
info.setName(getName());
return info;
}
}

View File

@ -0,0 +1,70 @@
package org.jetlinks.community.reactorql.function;
import com.google.common.collect.Sets;
import lombok.AllArgsConstructor;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.metadata.types.ArrayType;
import org.jetlinks.core.metadata.types.LongType;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
/**
*
* @author zhangji 2025/1/22
* @since 2.3
*/
@AllArgsConstructor
public enum InternalFunctionSupport implements FunctionSupport {
array_len("集合长度", LongType.GLOBAL, ArrayType.ID);
private final String name;
private final DataType outputType;
private final Set<String> supportTypes;
static {
for (InternalFunctionSupport value : values()) {
InternalFunctionSupport.supports.register(value.getId(), value);
}
}
public static void register(){
}
InternalFunctionSupport(String name, DataType outputType, String... supportTypes) {
this.name = name;
this.outputType = outputType;
this.supportTypes = Collections.unmodifiableSet(
Sets.newHashSet(supportTypes)
);
}
@Override
public String getId() {
return name();
}
@Override
public String getName() {
return name;
}
@Override
public boolean isSupported(DataType type) {
return supportTypes.contains(type.getId());
}
@Override
public DataType getOutputType() {
return outputType;
}
@Override
public SqlFragments createSql(String column, Map<String, Object> args) {
return SqlFragments.of(getId() + "(", column, ")");
}
}

View File

@ -0,0 +1,424 @@
package org.jetlinks.community.reactorql.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SubSelect;
import org.apache.commons.collections4.CollectionUtils;
import org.hswebframework.ezorm.core.param.Term;
import org.hswebframework.ezorm.rdb.executor.SqlRequest;
import org.hswebframework.ezorm.rdb.executor.SqlRequests;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.BatchSqlFragments;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.EmptySqlFragments;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments;
import org.hswebframework.ezorm.rdb.operator.dml.FunctionColumn;
import org.hswebframework.ezorm.rdb.operator.dml.FunctionTerm;
import org.hswebframework.web.api.crud.entity.TermExpressionParser;
import org.hswebframework.web.bean.FastBeanCopier;
import org.jetlinks.community.reactorql.aggregation.AggregationSupport;
import org.jetlinks.community.utils.ConverterUtils;
import org.jetlinks.community.utils.ReactorUtils;
import org.jetlinks.core.metadata.Jsonable;
import org.jetlinks.reactor.ql.DefaultReactorQLContext;
import org.jetlinks.reactor.ql.ReactorQL;
import org.jetlinks.reactor.ql.ReactorQLMetadata;
import org.jetlinks.reactor.ql.ReactorQLRecord;
import org.jetlinks.reactor.ql.feature.FeatureId;
import org.jetlinks.reactor.ql.feature.ValueMapFeature;
import org.jetlinks.reactor.ql.supports.DefaultReactorQLMetadata;
import org.jetlinks.reactor.ql.utils.CastUtils;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
*
* @author zhangji 2025/1/22
* @since 2.3
*/
public class ComplexExistsFunction implements ValueMapFeature {
//集合中的元素
public static final String COL_ELEMENT = "_element";
//集合自身
public static final String COL_SELF = "_self";
//原始行
public static final String COL_ROW = "_row";
public static final ComplexExistsFunction INSTANCE = new ComplexExistsFunction();
public static final String function = "complex_exists";
static {
DefaultReactorQLMetadata.addGlobal(INSTANCE);
}
public static void register() {
}
public static ExistsSpec createExistsSpec(Object val) {
if (val instanceof ExistsSpec) {
return ((ExistsSpec) val);
}
if (val instanceof Map) {
ExistsSpec spec = new ExistsSpec();
spec.fromJson(new JSONObject((Map) val));
return spec;
}
if (val instanceof String) {
ExistsSpec spec = new ExistsSpec();
spec.setFilter(TermExpressionParser.parse(val.toString()));
return spec;
}
return FastBeanCopier.copy(val, new ExistsSpec());
}
private static ExistsProcessor createExprProcessor(Expression expr) {
String sql;
if (expr instanceof Select) {
sql = expr.toString();
} else if (expr instanceof SubSelect) {
sql = ((SubSelect) expr).getSelectBody().toString();
} else if (expr instanceof BinaryExpression) {
sql = "select 1 from t where " + expr;
} else {
throw new UnsupportedOperationException("不支持的表达式:" + expr);
}
SqlRequest request = SqlRequests.of(sql);
ReactorQL ql = ReactorQL
.builder()
.sql(request.getSql())
.build();
return new ReactorQLProcessor(request, ql);
}
private static ExistsProcessor createExprProcessor(String str) {
if (str.startsWith("select") || str.startsWith("SELECT")) {
SqlRequest request = SqlRequests.of(str);
ReactorQL ql = ReactorQL
.builder()
.sql(request.getSql())
.build();
return new ReactorQLProcessor(request, ql);
}
ExistsSpec spec = new ExistsSpec();
spec.setFilter(TermExpressionParser.parse(str));
return spec.compile();
}
@Override
public Function<ReactorQLRecord, Publisher<?>> createMapper(Expression expression, ReactorQLMetadata metadata) {
net.sf.jsqlparser.expression.Function func = ((net.sf.jsqlparser.expression.Function) expression);
ExpressionList args = func.getParameters();
if (args == null || args.getExpressions() == null || args.getExpressions().size() < 2) {
throw new UnsupportedOperationException("complex_exists函数参数错误");
}
Function<ReactorQLRecord, Mono<ExistsProcessor>> processorMapper;
List<Function<ReactorQLRecord, Publisher<?>>> mappers = args
.getExpressions()
.stream()
.skip(1)
.map(expr -> ValueMapFeature.createMapperNow(expr, metadata))
.collect(Collectors.toList());
Expression firstExpr = args.getExpressions().get(0);
//以字符串来定义 complex_exists('name is test')
if (firstExpr instanceof StringValue) {
Mono<ExistsProcessor> processor = Mono.just(createExprProcessor(((StringValue) firstExpr).getValue()));
processorMapper = record -> processor;
} else if (firstExpr instanceof NumericBind) {
int argIndex = ((NumericBind) firstExpr).getBindId() - 1;
processorMapper = record -> Mono.justOrEmpty(
record.getContext().getParameter(argIndex).map(this::transformProcessor)
);
} else if (firstExpr instanceof JdbcParameter) {
int argIndex = ((JdbcParameter) firstExpr).getIndex() - 1;
processorMapper = record -> Mono.justOrEmpty(
record.getContext().getParameter(argIndex).map(this::transformProcessor)
);
}
// complex_exists(select 1 from dual where ,arr)
else if (firstExpr instanceof JdbcNamedParameter) {
String name = ((JdbcNamedParameter) firstExpr).getName();
processorMapper = record -> Mono.justOrEmpty(
transformProcessor(record.getContext().getParameter(name))
);
} else {
Mono<ExistsProcessor> processor = Mono.just(createExprProcessor(firstExpr));
processorMapper = record -> processor;
}
return record ->
processorMapper
.apply(record)
.flatMap(processor -> {
if (mappers.size() == 1) {
return processor.apply(
record,
mappers.get(0).apply(record));
}
return processor.apply(
record,
Flux.fromIterable(mappers)
.flatMap(mapper -> mapper.apply(record))
);
});
}
@Override
public String getId() {
return FeatureId.ValueMap.of(function).getId();
}
private ExistsProcessor transformProcessor(Object value) {
if (value instanceof ExistsProcessor) {
return (ExistsProcessor) value;
}
if (value instanceof ExistsSpec) {
return ((ExistsSpec) value).compile();
}
return null;
}
public interface ExistsProcessor extends BiFunction<ReactorQLRecord, Publisher<?>, Mono<Boolean>> {
@Override
Mono<Boolean> apply(ReactorQLRecord record, Publisher<?> publisher);
Mono<Boolean> apply(ReactorQLRecord record, List<?> list);
}
@AllArgsConstructor
static class ReactorQLProcessor implements ExistsProcessor {
private final SqlRequest request;
private final ReactorQL ql;
public Mono<Boolean> apply(ReactorQLRecord record, List<?> list) {
Flux<?> data = Flux
.fromIterable(list)
.map(v -> {
Map<String, Object> _data = Maps.newHashMapWithExpectedSize(3);
_data.put(COL_ROW,record.getRecord());
_data.put(COL_ELEMENT, v);
_data.put(COL_SELF, list);
return _data;
});
DefaultReactorQLContext ctx = new DefaultReactorQLContext((t) -> data);
for (Object parameter : request.getParameters()) {
ctx.bind(parameter);
}
return ql.start(ctx)
.hasElements();
}
@Override
public Mono<Boolean> apply(ReactorQLRecord record, Publisher<?> publisher) {
if (publisher instanceof Mono) {
return ((Mono<?>) publisher)
.map(ConverterUtils::convertToList)
.flatMap(list -> apply(record, list));
}
return Flux
.from(publisher)
.as(CastUtils::flatStream)
.collectList()
.flatMap(list -> apply(record, list));
}
@Override
public String toString() {
return request.toNativeSql();
}
}
private static FunctionTerm convertFunctionTerm(Object obj) {
if (obj instanceof FunctionTerm) {
return (FunctionTerm) obj;
}
if (obj instanceof Map) {
return convertFunctionTerm(new JSONObject((Map) obj));
}
throw new UnsupportedOperationException("不支持的类型:" + obj);
}
private static FunctionTerm convertFunctionTerm(JSONObject obj) {
FunctionTerm term = new FunctionTerm();
FastBeanCopier.copy(obj, term, "terms");
JSONArray terms = obj.getJSONArray("terms");
if (terms != null) {
terms
.forEach(o -> {
term.addTerm(convertFunctionTerm((JSONObject) o));
});
}
return term;
}
@Getter
@Setter
public static class ExistsSpec implements Jsonable {
//过滤
private List<Term> filter;
//聚合
private List<FunctionTerm> aggregation;
@Override
public void fromJson(JSONObject json) {
FastBeanCopier.copy(json, this, "aggregation");
JSONArray aggregation = json.getJSONArray("aggregation");
if (aggregation != null) {
this.aggregation = aggregation
.stream()
.map(ComplexExistsFunction::convertFunctionTerm)
.collect(Collectors.toList());
}
}
//todo 其他简便配置的方式?: 任意满足,全部满足等
public void walkTerms(Consumer<Term> consumer) {
if (filter != null) {
filter.forEach(consumer);
}
if (aggregation != null) {
aggregation.forEach(consumer);
}
}
private void applyAggregation(BatchSqlFragments fragments,
AtomicInteger count,
FunctionTerm agg,
Map<String, String> distinct,
Map<Term, String> aliasMapping) {
// 大小写都支持
AggregationSupport support = AggregationSupport.getNow(agg.getFunction());
FunctionColumn col = new FunctionColumn();
col.setFunction(agg.getFunction());
col.setColumn("this['" + agg.getColumn() + "']");
col.setOpts(agg.getOpts() == null ? null : Maps.transformValues(agg.getOpts(), Object.class::cast));
SqlFragments frg = support.createSql(col);
String sqlStr = frg.toRequest().toNativeSql();
String alias = distinct.get(frg.toRequest().toNativeSql());
if (alias == null) {
alias = "_agg_" + count.incrementAndGet();
distinct.put(sqlStr, alias);
aliasMapping.put(agg, alias);
if (count.get() > 1) {
fragments.addSql(",");
}
fragments.add(frg).addSql(alias);
}
if (CollectionUtils.isNotEmpty(agg.getTerms())) {
for (Term term : agg.getTerms()) {
if (term instanceof FunctionTerm) {
applyAggregation(fragments,
count,
((FunctionTerm) term),
distinct,
aliasMapping);
}
}
}
}
private List<Term> createHavingTerm(List<? extends Term> aggregation,
Map<Term, String> aliasMapping) {
List<Term> terms = new ArrayList<>(aggregation.size());
for (Term functionTerm : aggregation) {
Term term = new Term();
term.setColumn(aliasMapping.getOrDefault(functionTerm, functionTerm.getColumn()));
term.setTermType(functionTerm.getTermType());
term.setValue(functionTerm.getValue());
term.setOptions(functionTerm.getOptions());
term.setType(Term.Type.and);
if (CollectionUtils.isNotEmpty(functionTerm.getTerms())) {
term.setTerms(createHavingTerm(functionTerm.getTerms(), aliasMapping));
}
terms.add(term);
}
return terms;
}
public ExistsProcessor compile() {
SqlFragments cols;
SqlFragments having;
if (CollectionUtils.isNotEmpty(aggregation)) {
Map<String, String> distinct = new HashMap<>();
Map<Term, String> aliasMapping = new HashMap<>();
BatchSqlFragments cols_ = new BatchSqlFragments();
AtomicInteger count = new AtomicInteger(1);
count.incrementAndGet();
cols_.addSql(COL_SELF,",",COL_ELEMENT);
for (FunctionTerm functionTerm : aggregation) {
applyAggregation(cols_, count, functionTerm, distinct, aliasMapping);
}
cols = cols_;
List<Term> havingTerms = createHavingTerm(aggregation, aliasMapping);
having = ReactorUtils.createFilterSql(havingTerms);
} else {
cols = SqlFragments.ONE;
having = EmptySqlFragments.INSTANCE;
}
SqlFragments where = ReactorUtils.createFilterSql(filter);
BatchSqlFragments fragments = new BatchSqlFragments();
if (having.isNotEmpty()) {
fragments.addSql("select 1 from (");
}
fragments.addSql("select").addFragments(cols).addSql("from t");
if (where.isNotEmpty()) {
fragments.addSql("where").addFragments(where);
}
if (having.isNotEmpty()) {
fragments.add(SqlFragments.RIGHT_BRACKET)
.add(SqlFragments.WHERE)
.addFragments(having);
}
SqlRequest request = fragments.toRequest();
ReactorQL ql = ReactorQL.builder()
.sql(request.getSql())
.build();
return new ReactorQLProcessor(request, ql);
}
}
}

View File

@ -1,5 +1,6 @@
package org.jetlinks.community.reactorql.term;
import com.google.common.collect.Sets;
import lombok.Getter;
import org.hswebframework.ezorm.core.param.Term;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql;
@ -8,65 +9,158 @@ import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments;
import org.hswebframework.web.i18n.LocaleUtils;
import org.jetlinks.core.metadata.DataType;
import org.jetlinks.core.metadata.types.*;
import org.jetlinks.community.utils.ConverterUtils;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.jetlinks.community.reactorql.term.TermType.OPTIONS_NATIVE_SQL;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Getter
public enum FixedTermTypeSupport implements TermTypeSupport {
eq("等于", "eq"),
neq("不等于", "neq"),
eq("等于", "eq") {
@Override
public boolean isSupported(DataType type) {
return !type.getType().equals(ArrayType.ID) && super.isSupported(type);
}
},
neq("不等于", "neq") {
@Override
public boolean isSupported(DataType type) {
return !type.getType().equals(ArrayType.ID) && super.isSupported(type);
}
gt("大于", "gt", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID),
gte("大于等于", "gte", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID),
lt("小于", "lt", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID),
lte("小于等于", "lte", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID),
},
notnull("不为空", "notnull", false) {
@Override
protected String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s%s", property, getName());
}
},
isnull("为空", "isnull", false) {
@Override
public String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s%s", property, getName());
}
},
btw("在...之间", "btw", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID) {
gt("大于", "gt", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID),
gte("大于等于", "gte", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID),
lt("小于", "lt", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID),
lte("小于等于", "lte", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID),
btw("在...之间", "btw", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID) {
@Override
protected Object convertValue(Object val, Term term) {
return val;
}
@Override
protected String createValueDesc(Object expect) {
return arrayToSpec(expect);
}
@Override
public String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s在%s之间", property, arrayToSpec(expect));
}
},
nbtw("不在...之间", "nbtw", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID) {
nbtw("不在...之间", "nbtw", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID) {
@Override
protected Object convertValue(Object val, Term term) {
return val;
}
@Override
protected String createValueDesc(Object expect) {
return arrayToSpec(expect);
}
@Override
public String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s不在%s之间", property, createValueDesc(expect));
}
},
in("在...之中", "in", StringType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID, EnumType.ID) {
in("在...之中", "in", StringType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID, EnumType.ID) {
@Override
protected Object convertValue(Object val, Term term) {
return val;
}
@Override
protected String createValueDesc(Object expect) {
return arrayToSpec(expect);
}
@Override
public String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s在%s之中", property, createValueDesc(expect));
}
},
nin("不在...之中", "nin", StringType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID, EnumType.ID) {
nin("不在...之中", "nin", StringType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID, EnumType.ID) {
@Override
protected Object convertValue(Object val, Term term) {
return val;
}
@Override
protected String createValueDesc(Object expect) {
return arrayToSpec(expect);
}
@Override
public String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s不在%s之中", property, createValueDesc(expect));
}
},
contains_all("全部包含在...之中", "contains_all", ArrayType.ID) {
@Override
protected Object convertValue(Object val, Term term) {
return val;
return ConverterUtils.convertToList(val);
}
@Override
protected String createValueDesc(Object expect) {
return arrayToSpec(expect);
}
@Override
public String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s全部包含在%s之中", property, createValueDesc(expect));
}
},
contains_any("任意包含在...之中", "contains_any", ArrayType.ID) {
@Override
protected Object convertValue(Object val, Term term) {
return val;
return ConverterUtils.convertToList(val);
}
@Override
protected String createValueDesc(Object expect) {
return arrayToSpec(expect);
}
@Override
public String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s任意包含在%s之中", property, createValueDesc(expect));
}
},
not_contains("不包含在...之中", "not_contains", ArrayType.ID) {
@Override
protected Object convertValue(Object val, Term term) {
return val;
return ConverterUtils.convertToList(val);
}
@Override
protected String createValueDesc(Object expect) {
return arrayToSpec(expect);
}
@Override
public String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s不包含在%s之中", property, createValueDesc(expect));
}
},
@ -77,17 +171,17 @@ public enum FixedTermTypeSupport implements TermTypeSupport {
NativeSql sql = ((NativeSql) val);
return NativeSql.of("concat('%'," + sql.getSql() + ",'%')");
}
return super.convertValue(val, term);
val = super.convertValue(val, term);
if (val instanceof String && !((String) val).contains("%")) {
val = "%" + val + "%";
}
return val;
}
},
nlike("不包含字符", "str_nlike", StringType.ID){
nlike("不包含字符", "str_nlike", StringType.ID) {
@Override
protected Object convertValue(Object val, Term term) {
if (val instanceof NativeSql) {
NativeSql sql = ((NativeSql) val);
return NativeSql.of("concat('%'," + sql.getSql() + ",'%')");
}
return super.convertValue(val, term);
return like.convertValue(val, term);
}
},
@ -97,23 +191,53 @@ public enum FixedTermTypeSupport implements TermTypeSupport {
protected void appendFunction(String column, PrepareSqlFragments fragments) {
fragments.addSql("gt(math.divi(math.sub(now(),", column, "),1000),");
}
@Override
protected String createValueDesc(Object expect) {
return arrayToSpec(expect);
}
@Override
public String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s距离当前时间大于%s秒", property, expect);
}
},
time_lt_now("距离当前时间小于...秒", "time_lt_now", DateTimeType.ID) {
@Override
protected void appendFunction(String column, PrepareSqlFragments fragments) {
fragments.addSql("lt(math.divi(math.sub(now(),", column, "),1000),");
}
@Override
protected String createValueDesc(Object expect) {
return arrayToSpec(expect);
}
@Override
public String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s距离当前时间小于%s秒", property, expect);
}
};
private final String text;
private final boolean needValue;
private final Set<String> supportTypes;
private final String function;
private FixedTermTypeSupport(String text, String function, String... supportTypes) {
FixedTermTypeSupport(String text, String function, String... supportTypes) {
this.text = text;
this.function = function;
this.supportTypes = new HashSet<>(Arrays.asList(supportTypes));
this.needValue = true;
this.supportTypes = Sets.newHashSet(supportTypes);
}
FixedTermTypeSupport(String text, String function, boolean needValue, String... supportTypes) {
this.text = text;
this.function = function;
this.needValue = needValue;
this.supportTypes = Sets.newHashSet(supportTypes);
}
@Override
@ -132,7 +256,11 @@ public enum FixedTermTypeSupport implements TermTypeSupport {
}
protected void appendFunction(String column, PrepareSqlFragments fragments) {
fragments.addSql(function + "(", column, ",");
if (needValue) {
fragments.addSql(function + "(", column, ",");
} else {
fragments.addSql(function + "(", column, ")");
}
}
@Override
@ -140,24 +268,35 @@ public enum FixedTermTypeSupport implements TermTypeSupport {
PrepareSqlFragments fragments = PrepareSqlFragments.of();
appendFunction(column, fragments);
if (term.getOptions().contains(OPTIONS_NATIVE_SQL)) {
value = NativeSql.of(String.valueOf(value));
}
value = convertValue(value, term);
if (value instanceof NativeSql) {
fragments
.addSql(((NativeSql) value).getSql())
.addParameter(((NativeSql) value).getParameters());
} else {
fragments.addSql("?")
.addParameter(value);
.addParameter(((NativeSql) value).getParameters())
.addSql(")");
} else if (needValue) {
value = convertValue(value, term);
fragments
.addSql("?")
.addParameter(value)
.addSql(")");
}
fragments.addSql(")");
return fragments;
}
static String arrayToSpec(Object value) {
if (value == null) {
return "[]";
}
List<String> list = ConverterUtils
.convertToList(value, String::valueOf);
if (list.size() > 8) {
return Stream
.concat(list.stream().limit(8), Stream.of("..."))
.collect(Collectors.joining(",", "[", "]"));
}
return list.toString();
}
@Override
public String getType() {
return name();
@ -167,4 +306,26 @@ public enum FixedTermTypeSupport implements TermTypeSupport {
public String getName() {
return LocaleUtils.resolveMessage("message.term_type_" + name(), text);
}
protected String createValueDesc(Object expect) {
return String.valueOf(expect);
}
protected String createDefaultDesc(String property, Object expect, Object actual) {
return String.format("%s%s(%s)", property, getName(), expect);
}
@Override
public String createDesc(String property, Object expect, Object actual) {
//在国际化资源文件中查找对应的描述
// {0}=属性名称,{1}=期望值
//: message.term_type_neq_desc={0}不等于{1}
return LocaleUtils.resolveMessage(
"message.term_type_" + name() + "_desc",
createDefaultDesc(property, expect, expect),
property,
createValueDesc(expect),
actual
);
}
}

View File

@ -1,20 +1,132 @@
package org.jetlinks.community.reactorql.term;
import lombok.SneakyThrows;
import org.hswebframework.ezorm.core.param.Term;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments;
import org.hswebframework.web.i18n.LocaleUtils;
import org.jetlinks.community.utils.ReactorUtils;
import org.jetlinks.core.metadata.DataType;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
/**
* 查询条件类型支持
*
* @author zhouhao
* @see org.jetlinks.community.utils.ReactorUtils#createFilter(List)
* @since 2.0
*/
public interface TermTypeSupport {
/**
* @return 条件标识
*/
String getType();
/**
* @return 条件名称
*/
String getName();
/**
* 判断是否支持特定的数据类型
*
* @param type 数据类型
* @return 是否支持
*/
boolean isSupported(DataType type);
/**
* 创建SQL片段
*
* @param column 列名
* @param value
* @param term 条件
* @return SQL片段
*/
SqlFragments createSql(String column, Object value, Term term);
/**
* 重构条件
*
* @param tableName 表名
* @param term 条件
* @param refactor 重构函数
* @return 重构后的条件
*/
default Term refactorTerm(String tableName, Term term, BiFunction<String,Term,Term> refactor){
return refactor.apply(tableName,term);
}
/**
* 判断是否已经过时,过时的条件应当不可选择.
*
* @return 是否已经过时
*/
default boolean isDeprecated() {
return false;
}
/**
* 转为条件类型
*
* @return 条件类型
*/
default TermType type() {
return TermType.of(getType(), getName());
}
default String createDesc(String property, Object expect, Object actual) {
return LocaleUtils.resolveMessage(
"message.term_" + getType() + "_desc",
String.format("%s%s(%s)", property, getName(), expect),
property,
getName(),
expect,
actual
);
}
default String createActualDesc(String property, Object actual) {
return property + " = " + actual;
}
/**
* 阻塞方式判断能否满足期望值
*
* @param expect 期望值
* @param actual 实际值
* @return 是否满足
*/
@SneakyThrows
default boolean matchBlocking(Object expect, Object actual) {
return this
.match(expect, actual)
.toFuture()
.get(1, TimeUnit.SECONDS);
}
/**
* 判断能否满足期望值
*
* @param expect 期望值
* @param actual 实际值
* @return 是否满足
*/
default Mono<Boolean> match(Object expect, Object actual) {
Term term = new Term();
term.setTermType(getType());
term.setColumn("_mock");
term.setValue(expect);
return ReactorUtils
.createFilter(Collections.singletonList(term))
.apply(Collections.singletonMap("_mock", actual));
}
}

View File

@ -35,6 +35,9 @@ public class TermTypes {
}
public static Optional<TermTypeSupport> lookupSupport(String type) {
if (type == null) {
return Optional.empty();
}
return Optional.ofNullable(supports.get(type));
}
}

View File

@ -0,0 +1,120 @@
package org.jetlinks.community.reactorql.term;
import org.hswebframework.ezorm.core.param.Term;
import org.hswebframework.ezorm.rdb.executor.SqlRequest;
import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql;
import org.jetlinks.community.reactorql.function.FunctionSupport;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* @author zhangji 2025/2/8
* @since 2.3
*/
public class TermUtils {
public static List<Term> expandTermToList(List<Term> terms) {
Map<String, List<Term>> termsMap = expandTerm(terms);
List<Term> termList = new ArrayList<>();
for (List<Term> values : termsMap.values()) {
termList.addAll(values);
}
return termList;
}
public static Map<String, List<Term>> expandTerm(List<Term> terms) {
Map<String, List<Term>> termCache = new LinkedHashMap<>();
expandTerm(terms, termCache);
return termCache;
}
private static void expandTerm(List<Term> terms, Map<String, List<Term>> container) {
if (terms == null) {
return;
}
for (Term term : terms) {
if (StringUtils.hasText(term.getColumn())) {
List<Term> termList = container.get(term.getColumn());
if (termList == null) {
List<Term> list = new ArrayList<>();
list.add(term);
container.put(term.getColumn(), list);
} else {
termList.add(term);
container.put(term.getColumn(), termList);
}
}
if (term.getTerms() != null) {
expandTerm(term.getTerms(), container);
}
}
}
public static Term refactorTerm(String tableName,
Term term,
BiFunction<String, String, String> columnRefactor) {
if (term.getColumn() == null) {
return term;
}
String[] arr = term.getColumn().split("[.]");
List<TermValue> values = TermValue.of(term);
if (values.isEmpty()) {
return term;
}
Function<TermValue, Object> parser = value -> {
//上游变量
if (value.getSource() == TermValue.Source.variable
|| value.getSource() == TermValue.Source.upper) {
term.getOptions().add(TermType.OPTIONS_NATIVE_SQL);
return columnRefactor.apply(tableName, value.getValue().toString());
}
//指标
else if (value.getSource() == TermValue.Source.metric) {
term.getOptions().add(TermType.OPTIONS_NATIVE_SQL);
return tableName + "['" + arr[1] + "_metric_" + value.getMetric() + "']";
}
//函数, : array_len() , device_prop()
else if (value.getSource() == TermValue.Source.function) {
SqlRequest request = FunctionSupport
.supports
.getNow(value.getFunction())
.createSql(columnRefactor.apply(tableName, value.getColumn()), value.getArgs())
.toRequest();
return NativeSql.of(request.getSql(), request.getParameters());
}
//手动设置值
else {
return value.getValue();
}
};
Object val;
if (values.size() == 1) {
val = parser.apply(values.get(0));
} else {
val = values
.stream()
.map(parser)
.collect(Collectors.toList());
}
if (term.getOptions().contains(TermType.OPTIONS_NATIVE_SQL) && !(val instanceof NativeSql)) {
val = NativeSql.of(String.valueOf(val));
}
term.setColumn(columnRefactor.apply(tableName, term.getColumn()));
term.setValue(val);
return term;
}
}

View File

@ -0,0 +1,105 @@
package org.jetlinks.community.reactorql.term;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import org.hswebframework.ezorm.core.param.Term;
import org.hswebframework.web.bean.FastBeanCopier;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
*
* @author zhangji 2025/2/27
* @since 2.3
*/
@Getter
@Setter
public class TermValue implements Serializable {
private static final long serialVersionUID = 1;
@Schema(description = "来源")
private Source source;
@Schema(description = "[source]为[manual]时不能为空")
private Object value;
@Schema(description = "[source]为[metric]时不能为空")
private String metric;
@Schema(description = "[source]为[function]时不能为空")
private String function;
@Schema(description = "[source]为[function]时有效")
private String column;
@Schema(description = "[source]为[function]时有效")
private Map<String, Object> args;
public static TermValue manual(Object value) {
TermValue termValue = new TermValue();
termValue.setValue(value);
termValue.setSource(Source.manual);
return termValue;
}
public static TermValue metric(String metric) {
TermValue termValue = new TermValue();
termValue.setMetric(metric);
termValue.setSource(Source.metric);
return termValue;
}
public static List<TermValue> of(Term term) {
return of(term.getValue());
}
public static List<TermValue> of(Object value) {
if (value == null) {
return Collections.emptyList();
}
if (value instanceof Map) {
return Collections.singletonList(FastBeanCopier.copy(value, new TermValue()));
}
if (value instanceof TermValue) {
return Collections.singletonList(((TermValue) value));
}
if (value instanceof Collection) {
return ((Collection<?>) value)
.stream()
.flatMap(val -> of(val).stream())
.collect(Collectors.toList());
}
return Collections.singletonList(TermValue.manual(value));
}
public enum Source {
/**
* 和manual一样,
* 兼容{@link org.jetlinks.pro.relation.utils.VariableSource.Source#fixed}
*/
fixed,
manual,
metric,
variable,
/**
* 和variable一样,兼容{@link org.jetlinks.pro.relation.utils.VariableSource.Source#upper}
*/
upper,
/**
* 函数
*
* @see org.jetlinks.pro.reactorql.function.FunctionSupport
*/
function
}
}

View File

@ -20,6 +20,8 @@ public interface DataReferenceManager {
//数据类型: 设备接入网关
String TYPE_DEVICE_GATEWAY = "device-gateway";
//数据类型: 产品
String TYPE_PRODUCT = "product";
//数据类型: 网络组件
String TYPE_NETWORK = "network";
//数据类型关系配置
@ -53,7 +55,6 @@ public interface DataReferenceManager {
*/
Flux<DataReferenceInfo> getReferences(String dataType);
/**
* 断言数据没有被引用,如果存在引用则抛出异常: {@link DataReferencedException}
*
@ -69,4 +70,18 @@ public interface DataReferenceManager {
.flatMap(list -> Mono.error(new DataReferencedException(dataType, dataId, list)));
}
/**
* 断言数据没有被引用,如果存在引用则抛出异常: {@link DataReferencedException}
*
* @param dataType 数据类型
* @param dataId 数据ID
* @return void
*/
default Mono<Void> assertNotReferenced(String dataType, String dataId, String code, Object... args) {
return this
.getReferences(dataType, dataId)
.collectList()
.filter(CollectionUtils::isNotEmpty)
.flatMap(list -> Mono.error(new DataReferencedException(dataType, dataId, list, code, args)));
}
}

View File

@ -27,4 +27,17 @@ public class DataReferencedException extends I18nSupportException {
super.setI18nCode("error.data.referenced");
}
public DataReferencedException(String dataType,
String dataId,
List<DataReferenceInfo> referenceList,
String code,
Object... args) {
this.dataType = dataType;
this.dataId = dataId;
this.referenceList = referenceList;
super.setI18nCode(code);
super.setArgs(args);
}
}

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