mirror of
https://gitee.com/zhijiantianya/ruoyi-vue-pro.git
synced 2026-06-29 10:31:36 +08:00
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 69d736c3c6 | |||
| 3526353cdf | |||
| f8fffcea1b | |||
| 1a2c03bc7e | |||
| 82a3a8387c | |||
| 1c0d2e1e51 | |||
| 582fead405 | |||
| 20bf581acf | |||
| 2c38430124 | |||
| fb1648ecba | |||
| e923bc661d | |||
| 315be160a8 | |||
| d1517cddb7 | |||
| 00269fd911 | |||
| efe4200181 | |||
| 5266c6b1d5 | |||
| db06292ea3 | |||
| 9209e8da1c | |||
| 3583109f90 | |||
| e5f7b010e2 | |||
| 813069abf4 | |||
| 34a7399a65 | |||
| 62f7d34952 | |||
| 63398bf3d0 | |||
| a0c41623f2 | |||
| bf7c6db58c | |||
| d2075d5c18 | |||
| d23232aab0 | |||
| 87670d18fd | |||
| cdcecd0d4a | |||
| 659023bb35 | |||
| 18a5c46284 | |||
| 05d4aae65d | |||
| 3d40fc81dd | |||
| ed53ca3de9 | |||
| e958657373 | |||
| 446f601c8a | |||
| dae1f79e5e | |||
| 5c0e695f34 | |||
| c11a14b9da | |||
| 6f8baa3110 | |||
| d7308aa9eb | |||
| b62722598a | |||
| ce12cbf7d1 | |||
| 5a2169b688 | |||
| 61a00b8437 | |||
| 716bbb9813 | |||
| c9780d5952 | |||
| 3e96bb498b | |||
| 15ba083de2 | |||
| 9a9dbf0e97 | |||
| 3c3919545a | |||
| 2980c6e3eb | |||
| 09cb5b6433 | |||
| 90390dfdfa | |||
| d6333fc353 | |||
| d1b6534886 | |||
| 972b386d93 | |||
| fc4b677b00 | |||
| 5c03b97775 | |||
| 500f0a72ce | |||
| 73e30a4f37 | |||
| 120cbc9123 | |||
| e5b711409c | |||
| 69b93ca75a | |||
| 6489047a7d | |||
| 1c6a77806b | |||
| cd919daf64 | |||
| d2636a7787 | |||
| f3e0ca27d9 | |||
| e2a9b2d3e5 | |||
| 3201288036 | |||
| b845d62e8b | |||
| bf37095259 | |||
| 5d90760c39 | |||
| 882660a3a7 | |||
| e90fc607f0 | |||
| ec8b356ba6 | |||
| c61811a622 | |||
| a49b1431e5 | |||
| 2af0e40fe7 | |||
| f63d4e20b9 | |||
| 2505d61b08 | |||
| fc509837a1 | |||
| 66d6825657 | |||
| c58eb12896 | |||
| 81d89ba350 | |||
| 0cbf35f7f0 | |||
| 4a86bd23d8 | |||
| 41cdc951e1 | |||
| 66ebb71b8a | |||
| c64bb81cae | |||
| e52d7d33be | |||
| 19cb2b69f1 | |||
| 80cdfbf36e | |||
| 10ba70e107 | |||
| 75928525ca | |||
| fa62ace6af | |||
| 95bb9744c1 | |||
| 848fcdf329 | |||
| d10b4595a2 | |||
| e4be51b14a | |||
| 4d53944771 | |||
| 124576005a | |||
| 7c42632a50 | |||
| 40e52c3856 | |||
| feeaac729c | |||
| 2598c033a9 | |||
| 6b6d676a6b | |||
| 79311ecc71 | |||
| 27c30279a1 | |||
| d8d81e835f | |||
| 72d18b056b | |||
| 1f08a2725e | |||
| e01acfb18e | |||
| 41e4283f99 | |||
| c1884c3196 | |||
| d30bf0601c | |||
| 075dd83b5f | |||
| d6775a5619 | |||
| 073d860a78 | |||
| c761f5258a | |||
| d64555697f | |||
| b1d6baaad8 | |||
| d6a6a01252 | |||
| a207412e8c | |||
| 9c452ee612 |
8
.github/workflows/yudao-ui-admin.yml
vendored
8
.github/workflows/yudao-ui-admin.yml
vendored
@ -37,14 +37,14 @@ jobs:
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: yudao-ui-admin/pnpm-lock.yaml
|
||||
cache: "yarn"
|
||||
cache-dependency-path: yudao-ui-admin/yarn.lock
|
||||
|
||||
- name: Install deps
|
||||
run: pnpm install
|
||||
run: node --version && yarn --version && yarn install
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build:prod
|
||||
run: yarn build:prod
|
||||
|
||||
# 查看 workflow 的文档来获取更多信息
|
||||
# @see https://github.com/crazy-max/ghaction-github-pages
|
||||
|
||||
240
README.md
240
README.md
@ -1,3 +1,10 @@
|
||||
**严肃声明:现在、未来都不会有商业版本,所有功能全部开源!**
|
||||
|
||||
**拒绝虚假开源,售卖商业版,程序员不骗程序员!!**
|
||||
|
||||
**「我喜欢写代码,乐此不疲」**
|
||||
**「我喜欢做开源,以此为乐」**
|
||||
|
||||
## 🐯 平台简介
|
||||
|
||||
**芋道**,一套**全部开源**的**企业级**的快速开发平台,毫无保留给个人及企业免费使用。
|
||||
@ -8,15 +15,16 @@
|
||||
* 后端采用 Spring Boot、MySQL + MyBatis Plus、Redis + Redisson。
|
||||
* 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统。
|
||||
* 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能。
|
||||
* 支持 SaaS 多租户系统,可自定义每个租户的权限,提供透明化的多租户底层封装。
|
||||
* 工作流使用 Activiti ,支持动态表单、在线设计流程、多种任务分配方式。
|
||||
* 高效率开发,使用代码生成器可以一键生成前后端代码 + 单元测试 + Swagger 接口文档 + Validator 参数校验。
|
||||
* 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款。
|
||||
* 集成阿里云、腾讯云、云片等短信渠道,集成阿里云、腾讯云、七牛云等云存储服务。
|
||||
|
||||
| 项目名 | 说明 | 传说门 |
|
||||
| ---- | ---- | ---- |
|
||||
| `ruoyi-vue-pro` | Spring Boot 版本 | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro)** [Github](https://github.com/YunaiV/ruoyi-vue-pro) |
|
||||
| `ruoyi-vue-cloud` | Spring Cloud 版本 | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-cloud)** [Github](https://github.com/YunaiV/onemall) |
|
||||
| 项目名 | 说明 | 传说门 |
|
||||
|--------------------|------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `ruoyi-vue-pro` | Spring Boot 多模块 | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-pro)** [Github](https://github.com/YunaiV/ruoyi-vue-pro) |
|
||||
| `ruoyi-vue-cloud` | Spring Cloud 微服务 | **[Gitee](https://gitee.com/zhijiantianya/ruoyi-vue-cloud)** [Github](https://github.com/YunaiV/onemall) |
|
||||
| `Spring-Boot-Labs` | Spring Boot & Cloud 入门 | **[Gitee](https://gitee.com/zhijiantianya/SpringBoot-Labs)** [Github](https://github.com/YunaiV/SpringBoot-Labs) |
|
||||
|
||||
## 🐶 在线体验
|
||||
@ -37,7 +45,6 @@
|
||||
* 支付系统
|
||||
* 商城系统
|
||||
* 基础设施
|
||||
* 研发工具
|
||||
|
||||
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
|
||||
>
|
||||
@ -48,42 +55,43 @@
|
||||
|
||||
### 系统功能
|
||||
|
||||
| | 功能 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 |
|
||||
| ⭐️ | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 |
|
||||
| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 |
|
||||
| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 |
|
||||
| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
|
||||
| | 岗位管理 | 配置系统用户所属担任职务 |
|
||||
| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 |
|
||||
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
|
||||
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、云片等主流短信平台 |
|
||||
| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
|
||||
| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 |
|
||||
| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 |
|
||||
| | 通知公告 | 系统通知公告信息发布维护 |
|
||||
| | 功能 | 描述 |
|
||||
|-----|-------|---------------------------------|
|
||||
| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 |
|
||||
| ⭐️ | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 |
|
||||
| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 |
|
||||
| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 |
|
||||
| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 |
|
||||
| | 岗位管理 | 配置系统用户所属担任职务 |
|
||||
| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 |
|
||||
| 🚀 | 租户套餐 | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限 |
|
||||
| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 |
|
||||
| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、云片等主流短信平台 |
|
||||
| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 |
|
||||
| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 |
|
||||
| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 |
|
||||
| | 通知公告 | 系统通知公告信息发布维护 |
|
||||
|
||||
### 工作流程
|
||||
|
||||
| | 功能 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| 🚀 | 流程模型 | 配置工作流的流程模型,支持文件导入与在线设计流程图,提供 7 种任务分配规则 |
|
||||
| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
|
||||
| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 |
|
||||
| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 |
|
||||
| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转发、委派、退回等操作 |
|
||||
| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 |
|
||||
| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
|
||||
| | 功能 | 描述 |
|
||||
|-----|-------|----------------------------------------|
|
||||
| 🚀 | 流程模型 | 配置工作流的流程模型,支持文件导入与在线设计流程图,提供 7 种任务分配规则 |
|
||||
| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
|
||||
| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 |
|
||||
| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 |
|
||||
| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转发、委派、退回等操作 |
|
||||
| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 |
|
||||
| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
|
||||
|
||||
### 支付系统
|
||||
|
||||
| | 功能 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| 🚀 | 商户信息 | 管理商户信息,支持 Saas 场景下的多商户功能 |
|
||||
| 🚀 | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 |
|
||||
| 🚀 | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单 |
|
||||
| 🚀 | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单 |
|
||||
| | 功能 | 描述 |
|
||||
|-----|------|---------------------------|
|
||||
| 🚀 | 商户信息 | 管理商户信息,支持 Saas 场景下的多商户功能 |
|
||||
| 🚀 | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 |
|
||||
| 🚀 | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单 |
|
||||
| 🚀 | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单 |
|
||||
|
||||
ps:核心功能已经实现,正在对接微信小程序中...
|
||||
|
||||
@ -100,32 +108,27 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
||||
|
||||
### 基础设施
|
||||
|
||||
| | 功能 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
|
||||
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
|
||||
| 🚀 | 文件服务 | 支持本地文件存储,同时支持兼容 Amazon S3 协议的云服务、开源组件 |
|
||||
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
|
||||
| | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 |
|
||||
| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
|
||||
| 🚀 | 消息队列 | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
|
||||
| 🚀 |Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
|
||||
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 |
|
||||
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 |
|
||||
| 🚀 | 分布式锁 | 基于 Redis 实现分布式锁,满足并发场景 |
|
||||
| 🚀 | 幂等组件 | 基于 Redis 实现幂等组件,解决重复请求问题 |
|
||||
| 🚀 | 服务保障 | 基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能 |
|
||||
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
|
||||
| 🚀 | 单元测试 |基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
|
||||
|
||||
### 研发工具
|
||||
|
||||
| | 功能 | 描述 |
|
||||
| --- | --- | --- |
|
||||
| 🚀 | 代码生成 |前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 |
|
||||
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
|
||||
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
|
||||
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 |
|
||||
| | 功能 | 描述 |
|
||||
|-----|----------|----------------------------------------------|
|
||||
| 🚀 | 代码生成 | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 |
|
||||
| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 |
|
||||
| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 |
|
||||
| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 |
|
||||
| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 |
|
||||
| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 |
|
||||
| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等 |
|
||||
| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 |
|
||||
| | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 |
|
||||
| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 |
|
||||
| 🚀 | 消息队列 | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 |
|
||||
| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 |
|
||||
| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 |
|
||||
| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 |
|
||||
| 🚀 | 分布式锁 | 基于 Redis 实现分布式锁,满足并发场景 |
|
||||
| 🚀 | 幂等组件 | 基于 Redis 实现幂等组件,解决重复请求问题 |
|
||||
| 🚀 | 服务保障 | 基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能 |
|
||||
| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 |
|
||||
| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 |
|
||||
|
||||
## 🐨 技术栈
|
||||
|
||||
@ -145,82 +148,79 @@ ps:核心功能已经实现,正在对接微信小程序中...
|
||||
|
||||
### 后端
|
||||
|
||||
| 框架 | 说明 | 版本 | 学习指南 |
|
||||
| --- | --- |----------| --- |
|
||||
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.5.9 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
|
||||
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 | |
|
||||
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.8 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
||||
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.4.3.4 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
|
||||
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
||||
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 | |
|
||||
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.16.8 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
|
||||
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.15 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
|
||||
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.5.4 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
|
||||
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.0 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
|
||||
| [Activiti](https://github.com/Activiti/Activiti) | 工作流引擎 | 7.1.0.M6 | [文档](TODO) |
|
||||
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
|
||||
| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.2 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
|
||||
| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.0 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
|
||||
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
|
||||
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.4.2 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
|
||||
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.12.6 | |
|
||||
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
|
||||
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
|
||||
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.7.2 | - |
|
||||
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 3.9.0 | - |
|
||||
| 框架 | 说明 | 版本 | 学习指南 |
|
||||
|---------------------------------------------------------------------------------------------|------------------|----------|----------------------------------------------------------------|
|
||||
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.5.10 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
|
||||
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 | |
|
||||
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.8 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
||||
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.1 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
|
||||
| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
|
||||
| [Redis](https://redis.io/) | key-value 数据库 | 5.0 | |
|
||||
| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.16.8 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yudao) |
|
||||
| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.16 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yudao) |
|
||||
| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.5.5 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yudao) |
|
||||
| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.2 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yudao) |
|
||||
| [Activiti](https://github.com/Activiti/Activiti) | 工作流引擎 | 7.1.0.M6 | [文档](TODO) |
|
||||
| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yudao) |
|
||||
| [Knife4j](https://gitee.com/xiaoym/knife4j) | Swagger 增强 UI 实现 | 3.0.2 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yudao) |
|
||||
| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.0 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao) |
|
||||
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.5.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
|
||||
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.4.2 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
|
||||
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.12.6 | |
|
||||
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.4.1 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
|
||||
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.16.14 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
|
||||
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.7.2 | - |
|
||||
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 3.9.0 | - |
|
||||
|
||||
### 前端
|
||||
|
||||
| 框架 | 说明 | 版本 |
|
||||
| --- | --- | --- |
|
||||
| [Vue](https://cn.vuejs.org/index.html) | JavaScript 框架 | 2.6.12 |
|
||||
| [Vue Element Admin](https://ant.design/docs/react/introduce-cn) | 后台前端解决方案 | - |
|
||||
| 框架 | 说明 | 版本 |
|
||||
|------------------------------------------------------------------------------|---------------|--------|
|
||||
| [Vue](https://cn.vuejs.org/index.html) | JavaScript 框架 | 2.6.12 |
|
||||
| [Vue Element Admin](https://panjiachen.github.io/vue-element-admin-site/zh/) | 后台前端解决方案 | - |
|
||||
|
||||
## 🐷 演示图
|
||||
|
||||
### 系统功能
|
||||
|
||||
| 模块 | biu | biu | biu |
|
||||
| --- | --- | --- | --- |
|
||||
| 登录 & 首页 |  |  |  |
|
||||
| 用户 & 租户 |  |  |  |
|
||||
| 部门 & 岗位 |  |  | - |
|
||||
| 菜单 & 角色 |  |  | - |
|
||||
| 审计日志 |  |  | - |
|
||||
| 短信 |  |  |  |
|
||||
| 字典 |  |  | - |
|
||||
| 错误码 & 通知 |  |  | - |
|
||||
| 模块 | biu | biu | biu |
|
||||
|----------|--------------------------------------------------------------------|------------------------------------------------------------------|------------------------------------------------------------------|
|
||||
| 登录 & 首页 |  |  |  |
|
||||
| 用户 |  |  | - |
|
||||
| 租户 & 套餐 |  |  | - |
|
||||
| 部门 & 岗位 |  |  | - |
|
||||
| 菜单 & 角色 |  |  | - |
|
||||
| 审计日志 |  |  | - |
|
||||
| 短信 |  |  |  |
|
||||
| 字典 |  |  | - |
|
||||
| 错误码 & 通知 |  |  | - |
|
||||
|
||||
### 工作流程
|
||||
|
||||
| 模块 | biu | biu | biu |
|
||||
| --- | --- | --- | --- |
|
||||
| 流程模型 |  |  |  |
|
||||
| 表单 & 分组 |  |  | - |
|
||||
| 我的流程 |  |  |  |
|
||||
| 模块 | biu | biu | biu |
|
||||
|---------|------------------------------------------------------------------------|------------------------------------------------------------------------|------------------------------------------------------------------------|
|
||||
| 流程模型 |  |  |  |
|
||||
| 表单 & 分组 |  |  | - |
|
||||
| 我的流程 |  |  |  |
|
||||
| 待办 & 已办 |  |  |  |
|
||||
| OA 请假 |  |  |  |
|
||||
| OA 请假 |  |  |  |
|
||||
|
||||
### 支付系统
|
||||
|
||||
| 模块 | biu | biu | biu |
|
||||
| --- | --- | --- | --- |
|
||||
| 模块 | biu | biu | biu |
|
||||
|---------|------------------------------------------------------------------|------------------------------------------------------------------------|------------------------------------------------------------------------|
|
||||
| 商家 & 应用 |  |  |  |
|
||||
| 支付 & 退款 |  |  | --- |
|
||||
| 支付 & 退款 |  |  | --- |
|
||||
|
||||
### 基础设施
|
||||
|
||||
| 模块 | biu | biu | biu |
|
||||
| --- | --- | --- | --- |
|
||||
| 文件 & 配置 |  |  | - |
|
||||
| 定时任务 |  |  | - |
|
||||
| API 日志 |  |  | - |
|
||||
| MySQL & Redis |  |  | - |
|
||||
| 监控平台 |  |  |  |
|
||||
| 模块 | biu | biu | biu |
|
||||
|---------------|----------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------|
|
||||
| 代码生成 |  |  | - |
|
||||
| 文档 |  |  | - |
|
||||
| 文件 & 配置 |  |  |  |
|
||||
| 定时任务 |  |  | - |
|
||||
| API 日志 |  |  | - |
|
||||
| MySQL & Redis |  |  | - |
|
||||
| 监控平台 |  |  |  |
|
||||
|
||||
### 研发工具
|
||||
|
||||
| 模块 | biu | biu | biu |
|
||||
| --- | --- | --- | --- |
|
||||
| 代码生成 |  |  | - |
|
||||
| 文档 |  |  | - |
|
||||
|
||||
3
pom.xml
3
pom.xml
@ -17,7 +17,6 @@
|
||||
<module>yudao-module-bpm</module>
|
||||
<module>yudao-module-system</module>
|
||||
<module>yudao-module-infra</module>
|
||||
<module>yudao-module-tool</module>
|
||||
<module>yudao-module-pay</module>
|
||||
</modules>
|
||||
|
||||
@ -26,7 +25,7 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>1.5.0-snapshot</revision>
|
||||
<revision>1.6.1-snapshot</revision>
|
||||
<!-- Maven 相关 -->
|
||||
<java.version>1.8</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
|
||||
1517
sql/bpm-flowable.sql
Normal file
1517
sql/bpm-flowable.sql
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -14,9 +14,9 @@
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>1.5.0-snapshot</revision>
|
||||
<revision>1.6.1-snapshot</revision>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.boot.version>2.5.9</spring.boot.version>
|
||||
<spring.boot.version>2.5.10</spring.boot.version>
|
||||
<!-- Web 相关 -->
|
||||
<knife4j.version>3.0.2</knife4j.version>
|
||||
<swagger-annotations.version>1.5.22</swagger-annotations.version>
|
||||
@ -28,7 +28,7 @@
|
||||
<dynamic-datasource.version>3.5.0</dynamic-datasource.version>
|
||||
<redisson.version>3.16.6</redisson.version>
|
||||
<!-- Config 配置中心相关 -->
|
||||
<apollo.version>1.7.0</apollo.version>
|
||||
<apollo.version>1.9.2</apollo.version>
|
||||
<!-- Job 定时任务相关 -->
|
||||
<!-- 服务保障相关 -->
|
||||
<lock4j.version>2.2.0</lock4j.version>
|
||||
@ -52,8 +52,12 @@
|
||||
<velocity.version>2.2</velocity.version>
|
||||
<screw.version>1.0.5</screw.version>
|
||||
<guava.version>30.1.1-jre</guava.version>
|
||||
<guice.version>5.1.0</guice.version>
|
||||
<transmittable-thread-local.version>2.12.2</transmittable-thread-local.version>
|
||||
<commons-net.version>3.8.0</commons-net.version>
|
||||
<jsch.version>0.1.55</jsch.version>
|
||||
<!-- 三方云服务相关 -->
|
||||
<minio.version>8.2.2</minio.version>
|
||||
<aliyun-java-sdk-core.version>4.5.25</aliyun-java-sdk-core.version>
|
||||
<aliyun-java-sdk-dysmsapi.version>2.1.0</aliyun-java-sdk-dysmsapi.version>
|
||||
<yunpian-java-sdk.version>1.2.7</yunpian-java-sdk.version>
|
||||
@ -399,6 +403,11 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<!-- 工作流相关 flowable -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-flowable</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flowable</groupId>
|
||||
<artifactId>flowable-spring-boot-starter-basic</artifactId>
|
||||
@ -482,13 +491,40 @@
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<version>${guice.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>transmittable-thread-local</artifactId> <!-- 解决 ThreadLocal 父子线程的传值问题 -->
|
||||
<version>${transmittable-thread-local.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 -->
|
||||
<version>${commons-net.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.jcraft</groupId>
|
||||
<artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
|
||||
<version>${jsch.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 三方云服务相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-file</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
<version>${minio.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- SMS SDK begin -->
|
||||
<dependency>
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
<module>yudao-spring-boot-starter-web</module>
|
||||
<module>yudao-spring-boot-starter-security</module>
|
||||
|
||||
<module>yudao-spring-boot-starter-file</module>
|
||||
<module>yudao-spring-boot-starter-monitor</module>
|
||||
<module>yudao-spring-boot-starter-protection</module>
|
||||
<module>yudao-spring-boot-starter-config</module>
|
||||
@ -35,12 +36,13 @@
|
||||
<module>yudao-spring-boot-starter-biz-social</module>
|
||||
<module>yudao-spring-boot-starter-biz-tenant</module>
|
||||
<module>yudao-spring-boot-starter-biz-data-permission</module>
|
||||
<module>yudao-spring-boot-starter-flowable</module>
|
||||
</modules>
|
||||
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<description>
|
||||
该包是技术组件,每个子包,代表一个组件。每个组件包括两部分:
|
||||
1. core 包:是该组件的核心分装
|
||||
1. core 包:是该组件的核心封装
|
||||
2. config 包:是该组件基于 Spring 的配置
|
||||
|
||||
技术组件,也分成两类:
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
package cn.iocoder.yudao.framework.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 文档地址
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum DocumentEnum {
|
||||
|
||||
REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档");
|
||||
|
||||
private final String url;
|
||||
private final String memo;
|
||||
|
||||
}
|
||||
@ -3,7 +3,7 @@ package cn.iocoder.yudao.framework.common.enums;
|
||||
/**
|
||||
* Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期
|
||||
*
|
||||
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
|
||||
* 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@ -29,6 +29,8 @@ public interface WebFilterOrderEnum {
|
||||
|
||||
int ACTIVITI_FILTER = -98; // 需要保证在 Spring Security 过滤后面
|
||||
|
||||
int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面
|
||||
|
||||
int DEMO_FILTER = Integer.MAX_VALUE;
|
||||
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.common.util.collection;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BinaryOperator;
|
||||
@ -125,6 +126,15 @@ public class CollectionUtils {
|
||||
return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet())));
|
||||
}
|
||||
|
||||
public static <T, K> Map<K, T> convertImmutableMap(Collection<T> from, Function<T, K> keyFunc) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
ImmutableMap.Builder<K, T> builder = ImmutableMap.builder();
|
||||
from.forEach(item -> builder.put(keyFunc.apply(item), item));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static boolean containsAny(Collection<?> source, Collection<?> candidates) {
|
||||
return org.springframework.util.CollectionUtils.containsAny(source, candidates);
|
||||
}
|
||||
@ -140,6 +150,15 @@ public class CollectionUtils {
|
||||
return from.stream().filter(predicate).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public static <T, V extends Comparable<? super V>> V getMaxValue(List<T> from, Function<T, V> valueFunc) {
|
||||
if (CollUtil.isEmpty(from)) {
|
||||
return null;
|
||||
}
|
||||
assert from.size() > 0; // 断言,避免告警
|
||||
T t = from.stream().max(Comparator.comparing(valueFunc)).get();
|
||||
return valueFunc.apply(t);
|
||||
}
|
||||
|
||||
public static <T> void addIfNotNull(Collection<T> coll, T item) {
|
||||
if (item == null) {
|
||||
return;
|
||||
@ -147,4 +166,7 @@ public class CollectionUtils {
|
||||
coll.add(item);
|
||||
}
|
||||
|
||||
public static <T> Collection<T> singleton(T deptId) {
|
||||
return deptId == null ? Collections.emptyList() : Collections.singleton(deptId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,13 +22,40 @@ public class FileUtils {
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static File createTempFile(String data) {
|
||||
// 创建文件,通过 UUID 保证唯一
|
||||
File file = File.createTempFile(IdUtil.simpleUUID(), null);
|
||||
// 标记 JVM 退出时,自动删除
|
||||
file.deleteOnExit();
|
||||
File file = createTempFile();
|
||||
// 写入内容
|
||||
FileUtil.writeUtf8String(data, file);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建临时文件
|
||||
* 该文件会在 JVM 退出时,进行删除
|
||||
*
|
||||
* @param data 文件内容
|
||||
* @return 文件
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static File createTempFile(byte[] data) {
|
||||
File file = createTempFile();
|
||||
// 写入内容
|
||||
FileUtil.writeBytes(data, file);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建临时文件,无内容
|
||||
* 该文件会在 JVM 退出时,进行删除
|
||||
*
|
||||
* @return 文件
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static File createTempFile() {
|
||||
// 创建文件,通过 UUID 保证唯一
|
||||
File file = File.createTempFile(IdUtil.simpleUUID(), null);
|
||||
// 标记 JVM 退出时,自动删除
|
||||
file.deleteOnExit();
|
||||
return file;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.common.util.json;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@ -55,7 +56,6 @@ public class JsonUtils {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(text, clazz);
|
||||
} catch (IOException e) {
|
||||
@ -64,11 +64,26 @@ public class JsonUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串解析成指定类型的对象
|
||||
* 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下,
|
||||
* 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。
|
||||
*
|
||||
* @param text 字符串
|
||||
* @param clazz 类型
|
||||
* @return 对象
|
||||
*/
|
||||
public static <T> T parseObject2(String text, Class<T> clazz) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return null;
|
||||
}
|
||||
return JSONUtil.toBean(text, clazz);
|
||||
}
|
||||
|
||||
public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
|
||||
if (ArrayUtil.isEmpty(bytes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(bytes, clazz);
|
||||
} catch (IOException e) {
|
||||
@ -90,7 +105,6 @@ public class JsonUtils {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
|
||||
} catch (IOException e) {
|
||||
|
||||
@ -39,8 +39,8 @@ public class ValidationUtils {
|
||||
&& PATTERN_XML_NCNAME.matcher(str).matches();
|
||||
}
|
||||
|
||||
public static void validate(Validator validator, Object reqVO, Class<?>... groups) {
|
||||
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(reqVO, groups);
|
||||
public static void validate(Validator validator, Object object, Class<?>... groups) {
|
||||
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
|
||||
if (CollUtil.isNotEmpty(constraintViolations)) {
|
||||
throw new ConstraintViolationException(constraintViolations);
|
||||
}
|
||||
|
||||
@ -13,7 +13,6 @@ import org.activiti.bpmn.model.Process;
|
||||
import org.activiti.engine.impl.identity.Authentication;
|
||||
import org.activiti.engine.impl.util.io.BytesStreamSource;
|
||||
|
||||
import javax.xml.bind.Element;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@ -13,10 +13,12 @@ import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.sf.jsqlparser.expression.Alias;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
import net.sf.jsqlparser.expression.NullValue;
|
||||
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
|
||||
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
|
||||
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
|
||||
@ -51,6 +53,8 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
||||
private static final String DEPT_COLUMN_NAME = "dept_id";
|
||||
private static final String USER_COLUMN_NAME = "user_id";
|
||||
|
||||
static final Expression EXPRESSION_NULL = new NullValue();
|
||||
|
||||
private final DeptDataPermissionFrameworkService deptDataPermissionService;
|
||||
|
||||
/**
|
||||
@ -110,10 +114,12 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
||||
Expression deptExpression = this.buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds());
|
||||
Expression userExpression = this.buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId());
|
||||
if (deptExpression == null && userExpression == null) {
|
||||
log.error("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
|
||||
// TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据
|
||||
log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]",
|
||||
JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
|
||||
throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
|
||||
loginUser.getId(), tableName, tableAlias.getName()));
|
||||
// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
|
||||
// loginUser.getId(), tableName, tableAlias.getName()));
|
||||
return EXPRESSION_NULL;
|
||||
}
|
||||
if (deptExpression == null) {
|
||||
return userExpression;
|
||||
|
||||
@ -18,6 +18,7 @@ import org.mockito.MockedStatic;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static cn.iocoder.yudao.framework.datapermission.core.dept.rule.DeptDataPermissionRule.EXPRESSION_NULL;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
|
||||
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
@ -137,10 +138,9 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
|
||||
when(deptDataPermissionFrameworkService.getDeptDataPermission(same(loginUser))).thenReturn(deptDataPermission);
|
||||
|
||||
// 调用
|
||||
NullPointerException exception = assertThrows(NullPointerException.class,
|
||||
() -> rule.getExpression(tableName, tableAlias));
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertEquals("LoginUser(1) Table(t_user/u) 构建的条件为空", exception.getMessage());
|
||||
assertSame(EXPRESSION_NULL, expression);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(PayProperties.class)
|
||||
public class YudaoPayAutoConfiguration {
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package cn.iocoder.yudao.framework.pay.core.client.impl;
|
||||
|
||||
import cn.hutool.extra.validation.ValidationUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.AbstractPayCodeMapping;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClient;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.PayClientConfig;
|
||||
@ -11,7 +10,6 @@ import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedReqDTO;
|
||||
import cn.iocoder.yudao.framework.pay.core.client.dto.PayRefundUnifiedRespDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
|
||||
|
||||
/**
|
||||
@ -26,7 +24,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
* 渠道编号
|
||||
*/
|
||||
private final Long channelId;
|
||||
|
||||
/**
|
||||
* 渠道编码
|
||||
*/
|
||||
@ -40,10 +37,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
*/
|
||||
protected Config config;
|
||||
|
||||
protected Double calculateAmount(Long amount) {
|
||||
return amount / 100.0;
|
||||
}
|
||||
|
||||
public AbstractPayClient(Long channelId, String channelCode, Config config, AbstractPayCodeMapping codeMapping) {
|
||||
this.channelId = channelId;
|
||||
this.channelCode = channelCode;
|
||||
@ -75,6 +68,10 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
this.init();
|
||||
}
|
||||
|
||||
protected Double calculateAmount(Long amount) {
|
||||
return amount / 100.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return channelId;
|
||||
@ -96,12 +93,9 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected abstract PayCommonResult<?> doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO)
|
||||
throws Throwable;
|
||||
|
||||
|
||||
@Override
|
||||
public PayCommonResult<PayRefundUnifiedRespDTO> unifiedRefund(PayRefundUnifiedReqDTO reqDTO) {
|
||||
PayCommonResult<PayRefundUnifiedRespDTO> resp;
|
||||
@ -115,7 +109,6 @@ public abstract class AbstractPayClient<Config extends PayClientConfig> implemen
|
||||
return resp;
|
||||
}
|
||||
|
||||
|
||||
protected abstract PayCommonResult<PayRefundUnifiedRespDTO> doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable;
|
||||
|
||||
}
|
||||
|
||||
@ -27,11 +27,11 @@ public class PayClientFactoryImpl implements PayClientFactory {
|
||||
* 支付客户端 Map
|
||||
* key:渠道编号
|
||||
*/
|
||||
private final ConcurrentMap<Long, AbstractPayClient<?>> channelIdClients = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<Long, AbstractPayClient<?>> clients = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public PayClient getPayClient(Long channelId) {
|
||||
AbstractPayClient<?> client = channelIdClients.get(channelId);
|
||||
AbstractPayClient<?> client = clients.get(channelId);
|
||||
if (client == null) {
|
||||
log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId);
|
||||
}
|
||||
@ -42,11 +42,11 @@ public class PayClientFactoryImpl implements PayClientFactory {
|
||||
@SuppressWarnings("unchecked")
|
||||
public <Config extends PayClientConfig> void createOrUpdatePayClient(Long channelId, String channelCode,
|
||||
Config config) {
|
||||
AbstractPayClient<Config> client = (AbstractPayClient<Config>) channelIdClients.get(channelId);
|
||||
AbstractPayClient<Config> client = (AbstractPayClient<Config>) clients.get(channelId);
|
||||
if (client == null) {
|
||||
client = this.createPayClient(channelId, channelCode, config);
|
||||
client.init();
|
||||
channelIdClients.put(client.getId(), client);
|
||||
clients.put(client.getId(), client);
|
||||
} else {
|
||||
client.refresh(config);
|
||||
}
|
||||
@ -69,7 +69,7 @@ public class PayClientFactoryImpl implements PayClientFactory {
|
||||
case ALIPAY_PC: return (AbstractPayClient<Config>) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config);
|
||||
}
|
||||
// 创建失败,错误日志 + 抛出异常
|
||||
log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", config);
|
||||
log.error("[createPayClient][配置({}) 找不到合适的客户端实现]", config);
|
||||
throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", config));
|
||||
}
|
||||
|
||||
|
||||
@ -56,5 +56,4 @@ public enum PayChannelEnum {
|
||||
return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -14,21 +14,28 @@ import java.util.Set;
|
||||
@Data
|
||||
public class TenantProperties {
|
||||
|
||||
// /**
|
||||
// * 租户是否开启
|
||||
// */
|
||||
// private static final Boolean ENABLE_DEFAULT = true;
|
||||
//
|
||||
// /**
|
||||
// * 是否开启
|
||||
// */
|
||||
// private Boolean enable = ENABLE_DEFAULT;
|
||||
/**
|
||||
* 需要多租户的表
|
||||
*
|
||||
* 由于多租户并不作为 yudao 项目的重点功能,更多是扩展性的功能,所以采用正向配置需要多租户的表。
|
||||
* 如果需要,你可以改成 ignoreTables 来取消部分不需要的表
|
||||
* 租户是否开启
|
||||
*/
|
||||
private Set<String> tables;
|
||||
private static final Boolean ENABLE_DEFAULT = true;
|
||||
|
||||
/**
|
||||
* 是否开启
|
||||
*/
|
||||
private Boolean enable = ENABLE_DEFAULT;
|
||||
|
||||
/**
|
||||
* 需要忽略多租户的请求
|
||||
*
|
||||
* 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API!
|
||||
*/
|
||||
private Set<String> ignoreUrls;
|
||||
|
||||
/**
|
||||
* 需要忽略多租户的表
|
||||
*
|
||||
* 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟
|
||||
*/
|
||||
private Set<String> ignoreTables;
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
package cn.iocoder.yudao.framework.tenant.config;
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
||||
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator;
|
||||
import cn.iocoder.yudao.framework.tenant.core.mq.TenantRedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
|
||||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
|
||||
import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
|
||||
import cn.iocoder.yudao.framework.web.config.WebProperties;
|
||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
|
||||
@EnableConfigurationProperties(TenantProperties.class)
|
||||
public class YudaoTenantAutoConfiguration {
|
||||
|
||||
// ========== AOP ==========
|
||||
|
||||
@Bean
|
||||
public TenantIgnoreAspect tenantIgnoreAspect() {
|
||||
return new TenantIgnoreAspect();
|
||||
}
|
||||
|
||||
// ========== DB ==========
|
||||
|
||||
@Bean
|
||||
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
|
||||
MybatisPlusInterceptor interceptor) {
|
||||
TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
|
||||
// 添加到 interceptor 中
|
||||
// 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
|
||||
MyBatisUtils.addInterceptor(interceptor, inner, 0);
|
||||
return inner;
|
||||
}
|
||||
|
||||
// ========== WEB ==========
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
|
||||
FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
|
||||
registrationBean.setFilter(new TenantContextWebFilter());
|
||||
registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
|
||||
return registrationBean;
|
||||
}
|
||||
|
||||
// ========== Security ==========
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter(TenantProperties tenantProperties,
|
||||
WebProperties webProperties,
|
||||
GlobalExceptionHandler globalExceptionHandler,
|
||||
TenantFrameworkService tenantFrameworkService) {
|
||||
FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
|
||||
registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties,
|
||||
globalExceptionHandler, tenantFrameworkService));
|
||||
registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
|
||||
return registrationBean;
|
||||
}
|
||||
|
||||
// ========== MQ ==========
|
||||
|
||||
@Bean
|
||||
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
|
||||
return new TenantRedisMessageInterceptor();
|
||||
}
|
||||
|
||||
// ========== Job ==========
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
|
||||
public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) {
|
||||
return new BeanPostProcessor() {
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (!(bean instanceof JobHandler)) {
|
||||
return bean;
|
||||
}
|
||||
// 有 TenantJob 注解的情况下,才会进行处理
|
||||
if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) {
|
||||
return bean;
|
||||
}
|
||||
|
||||
// 使用 TenantJobHandlerDecorator 装饰
|
||||
return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.tenant.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 多租户针对 DB 的自动配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(TenantProperties.class)
|
||||
public class YudaoTenantDatabaseAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties,
|
||||
MybatisPlusInterceptor interceptor) {
|
||||
TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));
|
||||
// 添加到 interceptor 中
|
||||
// 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定
|
||||
MyBatisUtils.addInterceptor(interceptor, inner, 0);
|
||||
return inner;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.tenant.config;
|
||||
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator;
|
||||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 多租户针对 Job 的自动配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
public class YudaoTenantJobAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
|
||||
public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) {
|
||||
return new BeanPostProcessor() {
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (!(bean instanceof JobHandler)) {
|
||||
return bean;
|
||||
}
|
||||
// 有 TenantJob 注解的情况下,才会进行处理
|
||||
if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) {
|
||||
return bean;
|
||||
}
|
||||
|
||||
// 使用 TenantJobHandlerDecorator 装饰
|
||||
return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.tenant.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.mq.TenantRedisMessageInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 多租户针对 MQ 的自动配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
public class YudaoTenantMQAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
|
||||
return new TenantRedisMessageInterceptor();
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.tenant.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 多租户针对 Web 的自动配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
public class YudaoTenantSecurityAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<TenantSecurityWebFilter> tenantSecurityWebFilter() {
|
||||
FilterRegistrationBean<TenantSecurityWebFilter> registrationBean = new FilterRegistrationBean<>();
|
||||
registrationBean.setFilter(new TenantSecurityWebFilter());
|
||||
registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER);
|
||||
return registrationBean;
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.tenant.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.tenant.core.web.TenantContextWebFilter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 多租户针对 Web 的自动配置
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
public class YudaoTenantWebAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {
|
||||
FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();
|
||||
registrationBean.setFilter(new TenantContextWebFilter());
|
||||
registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);
|
||||
return registrationBean;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.aop;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* 忽略租户,标记指定方法不进行租户的自动过滤
|
||||
*
|
||||
* 注意,只有 DB 的场景会过滤,其它场景暂时不过滤:
|
||||
* 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的
|
||||
* 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Target({ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
public @interface TenantIgnore {
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.aop;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
|
||||
/**
|
||||
* 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。
|
||||
* 例如说,一个定时任务,读取所有数据,进行处理。
|
||||
* 又例如说,读取所有数据,进行缓存。
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Aspect
|
||||
@Slf4j
|
||||
public class TenantIgnoreAspect {
|
||||
|
||||
@Around("@annotation(tenantIgnore)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable {
|
||||
Boolean oldIgnore = TenantContextHolder.isIgnore();
|
||||
try {
|
||||
TenantContextHolder.setIgnore(true);
|
||||
// 执行逻辑
|
||||
return joinPoint.proceed();
|
||||
} finally {
|
||||
TenantContextHolder.setIgnore(oldIgnore);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -9,12 +9,15 @@ import com.alibaba.ttl.TransmittableThreadLocal;
|
||||
*/
|
||||
public class TenantContextHolder {
|
||||
|
||||
/**
|
||||
* 当前租户编号
|
||||
*/
|
||||
private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 租户编号 - 空
|
||||
* 是否忽略租户
|
||||
*/
|
||||
private static final Long TENANT_ID_NULL = 0L;
|
||||
private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();
|
||||
|
||||
/**
|
||||
* 获得租户编号。
|
||||
@ -33,26 +36,31 @@ public class TenantContextHolder {
|
||||
public static Long getRequiredTenantId() {
|
||||
Long tenantId = getTenantId();
|
||||
if (tenantId == null) {
|
||||
throw new NullPointerException("TenantContextHolder 不存在租户编号");
|
||||
throw new NullPointerException("TenantContextHolder 不存在租户编号"); // TODO 芋艿:增加文档链接
|
||||
}
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在一些前端场景下,可能无法请求带上租户。例如说,<img /> 方式获取图片等
|
||||
* 此时,暂时的解决方案,是在该接口的 Controller 方法上,调用该方法
|
||||
* TODO 芋艿:思考有没更合适的方案,目标是去掉该方法
|
||||
*/
|
||||
public static void setNullTenantId() {
|
||||
TENANT_ID.set(TENANT_ID_NULL);
|
||||
}
|
||||
|
||||
public static void setTenantId(Long tenantId) {
|
||||
TENANT_ID.set(tenantId);
|
||||
}
|
||||
|
||||
public static void setIgnore(Boolean ignore) {
|
||||
IGNORE.set(ignore);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前是否忽略租户
|
||||
*
|
||||
* @return 是否忽略
|
||||
*/
|
||||
public static boolean isIgnore() {
|
||||
return Boolean.TRUE.equals(IGNORE.get());
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
TENANT_ID.remove();
|
||||
IGNORE.remove();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -3,12 +3,10 @@ package cn.iocoder.yudao.framework.tenant.core.db;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
|
||||
import lombok.AllArgsConstructor;
|
||||
import net.sf.jsqlparser.expression.Expression;
|
||||
import net.sf.jsqlparser.expression.StringValue;
|
||||
import net.sf.jsqlparser.expression.LongValue;
|
||||
|
||||
/**
|
||||
* 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能
|
||||
@ -22,18 +20,13 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
|
||||
|
||||
@Override
|
||||
public Expression getTenantId() {
|
||||
return new StringValue(TenantContextHolder.getRequiredTenantId().toString());
|
||||
return new LongValue( TenantContextHolder.getRequiredTenantId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean ignoreTable(String tableName) {
|
||||
// 如果实体类继承 TenantBaseDO 类,则是多租户表,不进行忽略
|
||||
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
|
||||
if (tableInfo != null && TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) {
|
||||
return false;
|
||||
}
|
||||
// 不包含,说明要过滤
|
||||
return !CollUtil.contains(properties.getTables(), tableName);
|
||||
return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户
|
||||
|| CollUtil.contains(properties.getIgnoreTables(), tableName); // 情况二,忽略多租户的表
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.job;
|
||||
|
||||
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker;
|
||||
|
||||
/**
|
||||
* 多租户 JobHandlerInvoker 拓展实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class TenantJobHandlerInvoker extends JobHandlerInvoker {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -9,12 +9,12 @@ import java.time.Duration;
|
||||
/**
|
||||
* 多租户拓展的 RedisKeyDefine 实现类
|
||||
*
|
||||
* 由于 Redis 不同于 MySQL 有 column 字段,所以无法通过类似 WHERE tenant_id = ? 的方式过滤
|
||||
* 由于 Redis 不同于 MySQL 有 column 字段,无法通过类似 WHERE tenant_id = ? 的方式过滤
|
||||
* 所以需要通过在 Redis Key 上增加后缀的方式,进行租户之间的隔离。具体的步骤是:
|
||||
* 1. 假设 Redis Key 是 user:%d,示例是 user:1;对应到多租户的 Redis Key 是 user:%d:%d,
|
||||
* 2. 在 Redis DAO 中,需要使用 {@link #formatKey(Object...)} 方法,进行 Redis Key 的格式化
|
||||
*
|
||||
* 注意,大多数情况下,并不用使用 TenantRedisKeyDefine 实现。主要的使用场景,是 Redis Key 可能存在冲突的情况。
|
||||
* 注意,大多数情况下,并不用使用 TenantRedisKeyDefine 实现。主要的使用场景,还是 Redis Key 可能存在冲突的情况。
|
||||
* 例如说,租户 1 和 2 都有一个手机号作为 Key,则他们会存在冲突的问题
|
||||
*
|
||||
* @author 芋道源码
|
||||
|
||||
@ -1,13 +1,19 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.security;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.tenant.config.TenantProperties;
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
|
||||
import cn.iocoder.yudao.framework.web.config.WebProperties;
|
||||
import cn.iocoder.yudao.framework.web.core.filter.ApiRequestFilter;
|
||||
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
@ -18,34 +24,92 @@ import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 多租户 Security Web 过滤器
|
||||
* 校验用户访问的租户,是否是其所在的租户,避免越权问题
|
||||
* 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。
|
||||
* 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。
|
||||
* 3. 校验租户是合法,例如说被禁用、到期
|
||||
*
|
||||
* 校验用户访问的租户,是否是其所在的租户,
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class TenantSecurityWebFilter extends OncePerRequestFilter {
|
||||
public class TenantSecurityWebFilter extends ApiRequestFilter {
|
||||
|
||||
private final TenantProperties tenantProperties;
|
||||
|
||||
private final AntPathMatcher pathMatcher;
|
||||
|
||||
private final GlobalExceptionHandler globalExceptionHandler;
|
||||
private final TenantFrameworkService tenantFrameworkService;
|
||||
|
||||
public TenantSecurityWebFilter(TenantProperties tenantProperties,
|
||||
WebProperties webProperties,
|
||||
GlobalExceptionHandler globalExceptionHandler,
|
||||
TenantFrameworkService tenantFrameworkService) {
|
||||
super(webProperties);
|
||||
this.tenantProperties = tenantProperties;
|
||||
this.pathMatcher = new AntPathMatcher();
|
||||
this.globalExceptionHandler = globalExceptionHandler;
|
||||
this.tenantFrameworkService = tenantFrameworkService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
// 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。
|
||||
LoginUser user = SecurityFrameworkUtils.getLoginUser();
|
||||
assert user != null; // shouldNotFilter 已经校验
|
||||
// 校验租户是否匹配。
|
||||
if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) {
|
||||
log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
|
||||
user.getTenantId(), user.getId(), user.getUserType(),
|
||||
TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
|
||||
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
|
||||
"您无权访问该租户的数据"));
|
||||
return;
|
||||
if (user != null) {
|
||||
// 如果获取不到租户编号,则尝试使用登陆用户的租户编号
|
||||
if (tenantId == null) {
|
||||
tenantId = user.getTenantId();
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
// 如果传递了租户编号,则进行比对租户编号,避免越权问题
|
||||
} else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) {
|
||||
log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]",
|
||||
user.getTenantId(), user.getId(), user.getUserType(),
|
||||
TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod());
|
||||
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(),
|
||||
"您无权访问该租户的数据"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//检查是否是忽略的 URL, 如果是则允许访问
|
||||
if (!isIgnoreUrl(request)) {
|
||||
// 2. 如果请求未带租户的编号,不允许访问。
|
||||
if (tenantId == null) {
|
||||
log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod());
|
||||
ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(),
|
||||
"租户的请求未传递,请进行排查"));
|
||||
return;
|
||||
}
|
||||
// 3. 校验租户是合法,例如说被禁用、到期
|
||||
try {
|
||||
tenantFrameworkService.validTenant(tenantId);
|
||||
} catch (Throwable ex) {
|
||||
CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
|
||||
ServletUtils.writeJSON(response, result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 继续过滤
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
return SecurityFrameworkUtils.getLoginUser() == null;
|
||||
private boolean isIgnoreUrl(HttpServletRequest request) {
|
||||
// 快速匹配,保证性能
|
||||
if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) {
|
||||
return true;
|
||||
}
|
||||
// 逐个 Ant 路径匹配
|
||||
for (String url : tenantProperties.getIgnoreUrls()) {
|
||||
if (pathMatcher.match(url, request.getRequestURI())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -16,4 +16,11 @@ public interface TenantFrameworkService {
|
||||
*/
|
||||
List<Long> getTenantIds();
|
||||
|
||||
/**
|
||||
* 校验租户是否合法
|
||||
*
|
||||
* @param id 租户编号
|
||||
*/
|
||||
void validTenant(Long id);
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.framework.tenant.core.util;
|
||||
|
||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||
|
||||
/**
|
||||
* 多租户 Util
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class TenantUtils {
|
||||
|
||||
/**
|
||||
* 使用指定租户,执行对应的逻辑
|
||||
*
|
||||
* 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
|
||||
* 当然,执行完成后,还是会恢复回去
|
||||
*
|
||||
* @param tenantId 租户编号
|
||||
* @param runnable 逻辑
|
||||
*/
|
||||
public static void execute(Long tenantId, Runnable runnable) {
|
||||
Long oldTenantId = TenantContextHolder.getTenantId();
|
||||
Boolean oldIgnore = TenantContextHolder.isIgnore();
|
||||
try {
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
TenantContextHolder.setIgnore(false);
|
||||
// 执行逻辑
|
||||
runnable.run();
|
||||
} finally {
|
||||
TenantContextHolder.setTenantId(oldTenantId);
|
||||
TenantContextHolder.setIgnore(oldIgnore);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,6 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.iocoder.yudao.framework.tenant.config.YudaoTenantDatabaseAutoConfiguration,\
|
||||
cn.iocoder.yudao.framework.tenant.config.YudaoTenantWebAutoConfiguration,\
|
||||
cn.iocoder.yudao.framework.tenant.config.YudaoTenantJobAutoConfiguration,\
|
||||
cn.iocoder.yudao.framework.tenant.config.YudaoTenantMQAutoConfiguration,\
|
||||
cn.iocoder.yudao.framework.tenant.config.YudaoTenantSecurityAutoConfiguration
|
||||
cn.iocoder.yudao.framework.tenant.config.YudaoTenantAutoConfiguration
|
||||
|
||||
78
yudao-framework/yudao-spring-boot-starter-file/pom.xml
Normal file
78
yudao-framework/yudao-spring-boot-starter-file/pom.xml
Normal file
@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-spring-boot-starter-file</artifactId>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>文件客户端,支持多种存储器
|
||||
1. file:本地磁盘
|
||||
2. ftp:FTP 服务器
|
||||
2. sftp:SFTP 服务器
|
||||
4. db:数据库
|
||||
5. s3:支持 S3 协议的云存储服务,例如说 MinIO、阿里云、华为云、腾讯云、七牛云等等
|
||||
</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Spring 核心 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类相关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-net</groupId>
|
||||
<artifactId>commons-net</artifactId> <!-- 解决 ftp 连接 -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.jcraft</groupId>
|
||||
<artifactId>jsch</artifactId> <!-- 解决 sftp 连接 -->
|
||||
</dependency>
|
||||
|
||||
<!-- 三方云服务相关 -->
|
||||
<dependency>
|
||||
<groupId>io.minio</groupId>
|
||||
<artifactId>minio</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Test 测试相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@ -0,0 +1,21 @@
|
||||
package cn.iocoder.yudao.framework.file.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.file.core.client.FileClientFactory;
|
||||
import cn.iocoder.yudao.framework.file.core.client.FileClientFactoryImpl;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 文件配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration
|
||||
public class YudaoFileAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public FileClientFactory fileClientFactory() {
|
||||
return new FileClientFactoryImpl();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 文件客户端的抽象类,提供模板方法,减少子类的冗余代码
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractFileClient<Config extends FileClientConfig> implements FileClient {
|
||||
|
||||
/**
|
||||
* 配置编号
|
||||
*/
|
||||
private final Long id;
|
||||
/**
|
||||
* 文件配置
|
||||
*/
|
||||
protected Config config;
|
||||
|
||||
public AbstractFileClient(Long id, Config config) {
|
||||
this.id = id;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public final void init() {
|
||||
doInit();
|
||||
log.info("[init][配置({}) 初始化完成]", config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义初始化
|
||||
*/
|
||||
protected abstract void doInit();
|
||||
|
||||
public final void refresh(Config config) {
|
||||
// 判断是否更新
|
||||
if (config.equals(this.config)) {
|
||||
return;
|
||||
}
|
||||
log.info("[refresh][配置({})发生变化,重新初始化]", config);
|
||||
this.config = config;
|
||||
// 初始化
|
||||
this.init();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文件的 URL 访问地址
|
||||
* 使用场景:local、ftp、db,通过 FileController 的 getFile 来获取文件内容
|
||||
*
|
||||
* @param domain 自定义域名
|
||||
* @param path 文件路径
|
||||
* @return URL 访问地址
|
||||
*/
|
||||
protected String formatFileUrl(String domain, String path) {
|
||||
return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), path);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client;
|
||||
|
||||
/**
|
||||
* 文件客户端
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface FileClient {
|
||||
|
||||
/**
|
||||
* 获得客户端编号
|
||||
*
|
||||
* @return 客户端编号
|
||||
*/
|
||||
Long getId();
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param content 文件流
|
||||
* @param path 相对路径
|
||||
* @return 完整路径,即 HTTP 访问地址
|
||||
* @throws Exception 上传文件时,抛出 Exception 异常
|
||||
*/
|
||||
String upload(byte[] content, String path) throws Exception;
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param path 相对路径
|
||||
* @throws Exception 删除文件时,抛出 Exception 异常
|
||||
*/
|
||||
void delete(String path) throws Exception;
|
||||
|
||||
/**
|
||||
* 获得文件的内容
|
||||
*
|
||||
* @param path 相对路径
|
||||
* @return 文件的内容
|
||||
*/
|
||||
byte[] getContent(String path) throws Exception;
|
||||
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
|
||||
/**
|
||||
* 文件客户端的配置
|
||||
* 不同实现的客户端,需要不同的配置,通过子类来定义
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
|
||||
// @JsonTypeInfo 注解的作用,Jackson 多态
|
||||
// 1. 序列化到时数据库时,增加 @class 属性。
|
||||
// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型
|
||||
public interface FileClientConfig {
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client;
|
||||
|
||||
public interface FileClientFactory {
|
||||
|
||||
/**
|
||||
* 获得文件客户端
|
||||
*
|
||||
* @param configId 配置编号
|
||||
* @return 文件客户端
|
||||
*/
|
||||
FileClient getFileClient(Long configId);
|
||||
|
||||
/**
|
||||
* 创建文件客户端
|
||||
*
|
||||
* @param configId 配置编号
|
||||
* @param storage 存储器的枚举 {@link cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum}
|
||||
* @param config 文件配置
|
||||
*/
|
||||
<Config extends FileClientConfig> void createOrUpdateFileClient(Long configId, Integer storage, Config config);
|
||||
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.iocoder.yudao.framework.file.core.enums.FileStorageEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* 文件客户端的工厂实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class FileClientFactoryImpl implements FileClientFactory {
|
||||
|
||||
/**
|
||||
* 文件客户端 Map
|
||||
* key:配置编号
|
||||
*/
|
||||
private final ConcurrentMap<Long, AbstractFileClient<?>> clients = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public FileClient getFileClient(Long configId) {
|
||||
AbstractFileClient<?> client = clients.get(configId);
|
||||
if (client == null) {
|
||||
log.error("[getFileClient][配置编号({}) 找不到客户端]", configId);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <Config extends FileClientConfig> void createOrUpdateFileClient(Long configId, Integer storage, Config config) {
|
||||
AbstractFileClient<Config> client = (AbstractFileClient<Config>) clients.get(configId);
|
||||
if (client == null) {
|
||||
client = this.createFileClient(configId, storage, config);
|
||||
client.init();
|
||||
clients.put(client.getId(), client);
|
||||
} else {
|
||||
client.refresh(config);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <Config extends FileClientConfig> AbstractFileClient<Config> createFileClient(
|
||||
Long configId, Integer storage, Config config) {
|
||||
FileStorageEnum storageEnum = FileStorageEnum.getByStorage(storage);
|
||||
Assert.notNull(storageEnum, String.format("文件配置(%s) 为空", storageEnum));
|
||||
// 创建客户端
|
||||
return (AbstractFileClient<Config>) ReflectUtil.newInstance(storageEnum.getClientClass(), configId, config);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.db;
|
||||
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
|
||||
|
||||
/**
|
||||
* 基于 DB 存储的文件客户端的配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class DBFileClient extends AbstractFileClient<DBFileClientConfig> {
|
||||
|
||||
private DBFileContentFrameworkDAO dao;
|
||||
|
||||
public DBFileClient(Long id, DBFileClientConfig config) {
|
||||
super(id, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(byte[] content, String path) {
|
||||
getDao().insert(getId(), path, content);
|
||||
// 拼接返回路径
|
||||
return super.formatFileUrl(config.getDomain(), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
getDao().delete(getId(), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent(String path) {
|
||||
return getDao().selectContent(getId(), path);
|
||||
}
|
||||
|
||||
private DBFileContentFrameworkDAO getDao() {
|
||||
// 延迟获取,因为 SpringUtil 初始化太慢
|
||||
if (dao == null) {
|
||||
dao = SpringUtil.getBean(DBFileContentFrameworkDAO.class);
|
||||
}
|
||||
return dao;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.db;
|
||||
|
||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* 基于 DB 存储的文件客户端的配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class DBFileClientConfig implements FileClientConfig {
|
||||
|
||||
/**
|
||||
* 自定义域名
|
||||
*/
|
||||
@NotEmpty(message = "domain 不能为空")
|
||||
@URL(message = "domain 必须是 URL 格式")
|
||||
private String domain;
|
||||
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.db;
|
||||
|
||||
/**
|
||||
* 文件内容 Framework DAO 接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface DBFileContentFrameworkDAO {
|
||||
|
||||
/**
|
||||
* 插入文件内容
|
||||
*
|
||||
* @param configId 配置编号
|
||||
* @param path 路径
|
||||
* @param content 内容
|
||||
*/
|
||||
void insert(Long configId, String path, byte[] content);
|
||||
|
||||
/**
|
||||
* 删除文件内容
|
||||
*
|
||||
* @param configId 配置编号
|
||||
* @param path 路径
|
||||
*/
|
||||
void delete(Long configId, String path);
|
||||
|
||||
/**
|
||||
* 获得文件内容
|
||||
*
|
||||
* @param configId 配置编号
|
||||
* @param path 路径
|
||||
* @return 内容
|
||||
*/
|
||||
byte[] selectContent(Long configId, String path);
|
||||
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.ftp;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.CharsetUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.ftp.Ftp;
|
||||
import cn.hutool.extra.ftp.FtpException;
|
||||
import cn.hutool.extra.ftp.FtpMode;
|
||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Ftp 文件客户端
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> {
|
||||
|
||||
private Ftp ftp;
|
||||
|
||||
public FtpFileClient(Long id, FtpFileClientConfig config) {
|
||||
super(id, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
// 补全风格。例如说 Linux 是 /,Windows 是 \
|
||||
if (!config.getBasePath().endsWith(File.separator)) {
|
||||
config.setBasePath(config.getBasePath() + File.separator);
|
||||
}
|
||||
// 初始化 Ftp 对象
|
||||
this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
|
||||
CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(byte[] content, String path) {
|
||||
// 执行写入
|
||||
String filePath = getFilePath(path);
|
||||
String fileName = FileUtil.getName(filePath);
|
||||
String dir = StrUtil.removeSuffix(filePath, fileName);
|
||||
boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content));
|
||||
if (!success) {
|
||||
throw new FtpException(StrUtil.format("上海文件到目标目录 ({}) 失败", filePath));
|
||||
}
|
||||
// 拼接返回路径
|
||||
return super.formatFileUrl(config.getDomain(), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
String filePath = getFilePath(path);
|
||||
ftp.delFile(filePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent(String path) {
|
||||
String filePath = getFilePath(path);
|
||||
String fileName = FileUtil.getName(filePath);
|
||||
String dir = StrUtil.removeSuffix(path, fileName);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ftp.download(dir, fileName, out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
private String getFilePath(String path) {
|
||||
return config.getBasePath() + path;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.ftp;
|
||||
|
||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Ftp 文件客户端的配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class FtpFileClientConfig implements FileClientConfig {
|
||||
|
||||
/**
|
||||
* 基础路径
|
||||
*/
|
||||
@NotEmpty(message = "基础路径不能为空")
|
||||
private String basePath;
|
||||
|
||||
/**
|
||||
* 自定义域名
|
||||
*/
|
||||
@NotEmpty(message = "domain 不能为空")
|
||||
@URL(message = "domain 必须是 URL 格式")
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 主机地址
|
||||
*/
|
||||
@NotEmpty(message = "host 不能为空")
|
||||
private String host;
|
||||
/**
|
||||
* 主机端口
|
||||
*/
|
||||
@NotNull(message = "port 不能为空")
|
||||
private Integer port;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@NotEmpty(message = "用户名不能为空")
|
||||
private String username;
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotEmpty(message = "密码不能为空")
|
||||
private String password;
|
||||
/**
|
||||
* 连接模式
|
||||
*
|
||||
* 使用 {@link cn.hutool.extra.ftp.FtpMode} 对应的字符串
|
||||
*/
|
||||
@NotEmpty(message = "连接模式不能为空")
|
||||
private String mode;
|
||||
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.local;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 本地文件客户端
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> {
|
||||
|
||||
public LocalFileClient(Long id, LocalFileClientConfig config) {
|
||||
super(id, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
// 补全风格。例如说 Linux 是 /,Windows 是 \
|
||||
if (!config.getBasePath().endsWith(File.separator)) {
|
||||
config.setBasePath(config.getBasePath() + File.separator);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(byte[] content, String path) {
|
||||
// 执行写入
|
||||
String filePath = getFilePath(path);
|
||||
FileUtil.writeBytes(content, filePath);
|
||||
// 拼接返回路径
|
||||
return super.formatFileUrl(config.getDomain(), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
String filePath = getFilePath(path);
|
||||
FileUtil.del(filePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent(String path) {
|
||||
String filePath = getFilePath(path);
|
||||
return FileUtil.readBytes(filePath);
|
||||
}
|
||||
|
||||
private String getFilePath(String path) {
|
||||
return config.getBasePath() + path;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.local;
|
||||
|
||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
/**
|
||||
* 本地文件客户端的配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class LocalFileClientConfig implements FileClientConfig {
|
||||
|
||||
/**
|
||||
* 基础路径
|
||||
*/
|
||||
@NotEmpty(message = "基础路径不能为空")
|
||||
private String basePath;
|
||||
|
||||
/**
|
||||
* 自定义域名
|
||||
*/
|
||||
@NotEmpty(message = "domain 不能为空")
|
||||
@URL(message = "domain 必须是 URL 格式")
|
||||
private String domain;
|
||||
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.s3;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
|
||||
import io.minio.*;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
||||
import static cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN;
|
||||
|
||||
/**
|
||||
* 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务
|
||||
*
|
||||
* S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
||||
|
||||
private MinioClient client;
|
||||
|
||||
public S3FileClient(Long id, S3FileClientConfig config) {
|
||||
super(id, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
// 补全 domain
|
||||
if (StrUtil.isEmpty(config.getDomain())) {
|
||||
config.setDomain(buildDomain());
|
||||
}
|
||||
// 初始化客户端
|
||||
client = MinioClient.builder()
|
||||
.endpoint(buildEndpointURL()) // Endpoint URL
|
||||
.region(buildRegion()) // Region
|
||||
.credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 endpoint 构建调用云服务的 URL 地址
|
||||
*
|
||||
* @return URI 地址
|
||||
*/
|
||||
private String buildEndpointURL() {
|
||||
// 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
|
||||
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
|
||||
return config.getEndpoint();
|
||||
}
|
||||
return StrUtil.format("https://{}", config.getEndpoint());
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 bucket + endpoint 构建访问的 Domain 地址
|
||||
*
|
||||
* @return Domain 地址
|
||||
*/
|
||||
private String buildDomain() {
|
||||
// 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO
|
||||
if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {
|
||||
return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());
|
||||
}
|
||||
// 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名
|
||||
return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于 bucket 构建 region 地区
|
||||
*
|
||||
* @return region 地区
|
||||
*/
|
||||
private String buildRegion() {
|
||||
// 阿里云必须有 region,否则会报错
|
||||
if (config.getEndpoint().contains(ENDPOINT_ALIYUN)) {
|
||||
return StrUtil.subBefore(config.getEndpoint(), '.', false)
|
||||
.replaceAll("-internal", ""); // 去除内网 Endpoint 的后缀
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(byte[] content, String path) throws Exception {
|
||||
// 执行上传
|
||||
client.putObject(PutObjectArgs.builder()
|
||||
.bucket(config.getBucket()) // bucket 必须传递
|
||||
.object(path) // 相对路径作为 key
|
||||
.stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容
|
||||
.build());
|
||||
// 拼接返回路径
|
||||
return config.getDomain() + "/" + path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) throws Exception {
|
||||
client.removeObject(RemoveObjectArgs.builder()
|
||||
.bucket(config.getBucket()) // bucket 必须传递
|
||||
.object(path) // 相对路径作为 key
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent(String path) throws Exception {
|
||||
GetObjectResponse response = client.getObject(GetObjectArgs.builder()
|
||||
.bucket(config.getBucket()) // bucket 必须传递
|
||||
.object(path) // 相对路径作为 key
|
||||
.build());
|
||||
return IoUtil.readBytes(response);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.s3;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.AssertTrue;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* S3 文件客户端的配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class S3FileClientConfig implements FileClientConfig {
|
||||
|
||||
public static final String ENDPOINT_QINIU = "qiniucs.com";
|
||||
public static final String ENDPOINT_ALIYUN = "aliyuncs.com";
|
||||
|
||||
/**
|
||||
* 节点地址
|
||||
* 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO 。例如说,http://127.0.0.1:9000
|
||||
* 2. 阿里云:https://help.aliyun.com/document_detail/31837.html
|
||||
* 3. 腾讯云:https://cloud.tencent.com/document/product/436/6224
|
||||
* 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname
|
||||
* 5. 华为云:https://developer.huaweicloud.com/endpoint?OBS
|
||||
*/
|
||||
@NotNull(message = "endpoint 不能为空")
|
||||
private String endpoint;
|
||||
/**
|
||||
* 自定义域名
|
||||
* 1. MinIO:通过 Nginx 配置
|
||||
* 2. 阿里云:https://help.aliyun.com/document_detail/31836.html
|
||||
* 3. 腾讯云:https://cloud.tencent.com/document/product/436/11142
|
||||
* 4. 七牛云:https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name
|
||||
* 5. 华为云:https://support.huaweicloud.com/usermanual-obs/obs_03_0032.html
|
||||
*/
|
||||
@URL(message = "domain 必须是 URL 格式")
|
||||
private String domain;
|
||||
/**
|
||||
* 存储 Bucket
|
||||
*/
|
||||
@NotNull(message = "bucket 不能为空")
|
||||
private String bucket;
|
||||
|
||||
/**
|
||||
* 访问 Key
|
||||
* 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO
|
||||
* 2. 阿里云:https://ram.console.aliyun.com/manage/ak
|
||||
* 3. 腾讯云:https://console.cloud.tencent.com/cam/capi
|
||||
* 4. 七牛云:https://portal.qiniu.com/user/key
|
||||
* 5. 华为云:https://support.huaweicloud.com/qs-obs/obs_qs_0005.html
|
||||
*/
|
||||
@NotNull(message = "accessKey 不能为空")
|
||||
private String accessKey;
|
||||
/**
|
||||
* 访问 Secret
|
||||
*/
|
||||
@NotNull(message = "accessSecret 不能为空")
|
||||
private String accessSecret;
|
||||
|
||||
@SuppressWarnings("RedundantIfStatement")
|
||||
@AssertTrue(message = "domain 不能为空")
|
||||
@JsonIgnore
|
||||
public boolean isDomainValid() {
|
||||
// 如果是七牛,必须带有 domain
|
||||
if (StrUtil.contains(endpoint, ENDPOINT_QINIU) && StrUtil.isEmpty(domain)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.sftp;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.extra.ssh.Sftp;
|
||||
import cn.iocoder.yudao.framework.common.util.io.FileUtils;
|
||||
import cn.iocoder.yudao.framework.file.core.client.AbstractFileClient;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Sftp 文件客户端
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> {
|
||||
|
||||
private Sftp sftp;
|
||||
|
||||
public SftpFileClient(Long id, SftpFileClientConfig config) {
|
||||
super(id, config);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doInit() {
|
||||
// 补全风格。例如说 Linux 是 /,Windows 是 \
|
||||
if (!config.getBasePath().endsWith(File.separator)) {
|
||||
config.setBasePath(config.getBasePath() + File.separator);
|
||||
}
|
||||
// 初始化 Ftp 对象
|
||||
this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(byte[] content, String path) {
|
||||
// 执行写入
|
||||
String filePath = getFilePath(path);
|
||||
File file = FileUtils.createTempFile(content);
|
||||
sftp.upload(filePath, file);
|
||||
// 拼接返回路径
|
||||
return super.formatFileUrl(config.getDomain(), path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String path) {
|
||||
String filePath = getFilePath(path);
|
||||
sftp.delFile(filePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getContent(String path) {
|
||||
String filePath = getFilePath(path);
|
||||
File destFile = FileUtils.createTempFile();
|
||||
sftp.download(filePath, destFile);
|
||||
return FileUtil.readBytes(destFile);
|
||||
}
|
||||
|
||||
private String getFilePath(String path) {
|
||||
return config.getBasePath() + path;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.sftp;
|
||||
|
||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Sftp 文件客户端的配置类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Data
|
||||
public class SftpFileClientConfig implements FileClientConfig {
|
||||
|
||||
/**
|
||||
* 基础路径
|
||||
*/
|
||||
@NotEmpty(message = "基础路径不能为空")
|
||||
private String basePath;
|
||||
|
||||
/**
|
||||
* 自定义域名
|
||||
*/
|
||||
@NotEmpty(message = "domain 不能为空")
|
||||
@URL(message = "domain 必须是 URL 格式")
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 主机地址
|
||||
*/
|
||||
@NotEmpty(message = "host 不能为空")
|
||||
private String host;
|
||||
/**
|
||||
* 主机端口
|
||||
*/
|
||||
@NotNull(message = "port 不能为空")
|
||||
private Integer port;
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@NotEmpty(message = "用户名不能为空")
|
||||
private String username;
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotEmpty(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package cn.iocoder.yudao.framework.file.core.enums;
|
||||
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.iocoder.yudao.framework.file.core.client.FileClient;
|
||||
import cn.iocoder.yudao.framework.file.core.client.FileClientConfig;
|
||||
import cn.iocoder.yudao.framework.file.core.client.db.DBFileClient;
|
||||
import cn.iocoder.yudao.framework.file.core.client.db.DBFileClientConfig;
|
||||
import cn.iocoder.yudao.framework.file.core.client.ftp.FtpFileClient;
|
||||
import cn.iocoder.yudao.framework.file.core.client.ftp.FtpFileClientConfig;
|
||||
import cn.iocoder.yudao.framework.file.core.client.local.LocalFileClient;
|
||||
import cn.iocoder.yudao.framework.file.core.client.local.LocalFileClientConfig;
|
||||
import cn.iocoder.yudao.framework.file.core.client.s3.S3FileClient;
|
||||
import cn.iocoder.yudao.framework.file.core.client.s3.S3FileClientConfig;
|
||||
import cn.iocoder.yudao.framework.file.core.client.sftp.SftpFileClient;
|
||||
import cn.iocoder.yudao.framework.file.core.client.sftp.SftpFileClientConfig;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 文件存储器枚举
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum FileStorageEnum {
|
||||
|
||||
DB(1, DBFileClientConfig.class, DBFileClient.class),
|
||||
|
||||
LOCAL(10, LocalFileClientConfig.class, LocalFileClient.class),
|
||||
FTP(11, FtpFileClientConfig.class, FtpFileClient.class),
|
||||
SFTP(12, SftpFileClientConfig.class, SftpFileClient.class),
|
||||
|
||||
S3(20, S3FileClientConfig.class, S3FileClient.class),
|
||||
;
|
||||
|
||||
/**
|
||||
* 存储器
|
||||
*/
|
||||
private final Integer storage;
|
||||
|
||||
/**
|
||||
* 配置类
|
||||
*/
|
||||
private final Class<? extends FileClientConfig> configClass;
|
||||
/**
|
||||
* 客户端类
|
||||
*/
|
||||
private final Class<? extends FileClient> clientClass;
|
||||
|
||||
public static FileStorageEnum getByStorage(Integer storage) {
|
||||
return ArrayUtil.firstMatch(o -> o.getStorage().equals(storage), values());
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.iocoder.yudao.framework.file.config.YudaoFileAutoConfiguration
|
||||
@ -1,4 +1,4 @@
|
||||
/**
|
||||
* 占位,避免 package 无法提交到 Git 仓库
|
||||
*/
|
||||
package cn.iocoder.yudao.module.tool.controller.app;
|
||||
package cn.iocoder.yudao.framework.file.config;
|
||||
@ -0,0 +1,39 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.ftp;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.extra.ftp.FtpMode;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class FtpFileClientTest {
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void test() {
|
||||
// 创建客户端
|
||||
FtpFileClientConfig config = new FtpFileClientConfig();
|
||||
config.setDomain("http://127.0.0.1:48080");
|
||||
config.setBasePath("/home/ftp");
|
||||
config.setHost("kanchai.club");
|
||||
config.setPort(221);
|
||||
config.setUsername("");
|
||||
config.setPassword("");
|
||||
config.setMode(FtpMode.Passive.name());
|
||||
FtpFileClient client = new FtpFileClient(0L, config);
|
||||
client.init();
|
||||
// 上传文件
|
||||
String path = IdUtil.fastSimpleUUID() + ".jpg";
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
String fullPath = client.upload(content, path);
|
||||
System.out.println("访问地址:" + fullPath);
|
||||
if (false) {
|
||||
byte[] bytes = client.getContent(path);
|
||||
System.out.println("文件内容:" + bytes);
|
||||
}
|
||||
if (false) {
|
||||
client.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.local;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class LocalFileClientTest {
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void test() {
|
||||
// 创建客户端
|
||||
LocalFileClientConfig config = new LocalFileClientConfig();
|
||||
config.setDomain("http://127.0.0.1:48080");
|
||||
config.setBasePath("/Users/yunai/file_test");
|
||||
LocalFileClient client = new LocalFileClient(0L, config);
|
||||
client.init();
|
||||
// 上传文件
|
||||
String path = IdUtil.fastSimpleUUID() + ".jpg";
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
String fullPath = client.upload(content, path);
|
||||
System.out.println("访问地址:" + fullPath);
|
||||
client.delete(path);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.s3;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import javax.validation.Validation;
|
||||
|
||||
public class S3FileClientTest {
|
||||
|
||||
@Test
|
||||
@Disabled // MinIO,如果要集成测试,可以注释本行
|
||||
public void testMinIO() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
config.setAccessKey("admin");
|
||||
config.setAccessSecret("password");
|
||||
config.setBucket("yudaoyuanma");
|
||||
config.setDomain(null);
|
||||
// 默认 9000 endpoint
|
||||
config.setEndpoint("http://127.0.0.1:9000");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 阿里云 OSS,如果要集成测试,可以注释本行
|
||||
public void testAliyun() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
config.setAccessKey(System.getenv("ALIYUN_ACCESS_KEY"));
|
||||
config.setAccessSecret(System.getenv("ALIYUN_SECRET_KEY"));
|
||||
config.setBucket("yunai-aoteman");
|
||||
config.setDomain(null); // 如果有自定义域名,则可以设置。http://ali-oss.iocoder.cn
|
||||
// 默认北京的 endpoint
|
||||
config.setEndpoint("oss-cn-beijing.aliyuncs.com");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 腾讯云 COS,如果要集成测试,可以注释本行
|
||||
public void testQCloud() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY"));
|
||||
config.setAccessSecret(System.getenv("QCLOUD_SECRET_KEY"));
|
||||
config.setBucket("aoteman-1255880240");
|
||||
config.setDomain(null); // 如果有自定义域名,则可以设置。http://tengxun-oss.iocoder.cn
|
||||
// 默认上海的 endpoint
|
||||
config.setEndpoint("cos.ap-shanghai.myqcloud.com");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 七牛云存储,如果要集成测试,可以注释本行
|
||||
public void testQiniu() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
// config.setAccessKey(System.getenv("QINIU_ACCESS_KEY"));
|
||||
// config.setAccessSecret(System.getenv("QINIU_SECRET_KEY"));
|
||||
config.setAccessKey("b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8");
|
||||
config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP");
|
||||
config.setBucket("ruoyi-vue-pro");
|
||||
config.setDomain("http://test.yudao.iocoder.cn"); // 如果有自定义域名,则可以设置。http://static.yudao.iocoder.cn
|
||||
// 默认上海的 endpoint
|
||||
config.setEndpoint("s3-cn-south-1.qiniucs.com");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled // 华为云存储,如果要集成测试,可以注释本行
|
||||
public void testHuaweiCloud() throws Exception {
|
||||
S3FileClientConfig config = new S3FileClientConfig();
|
||||
// 配置成你自己的
|
||||
// config.setAccessKey(System.getenv("HUAWEI_CLOUD_ACCESS_KEY"));
|
||||
// config.setAccessSecret(System.getenv("HUAWEI_CLOUD_SECRET_KEY"));
|
||||
config.setBucket("yudao");
|
||||
config.setDomain(null); // 如果有自定义域名,则可以设置。
|
||||
// 默认上海的 endpoint
|
||||
config.setEndpoint("obs.cn-east-3.myhuaweicloud.com");
|
||||
|
||||
// 执行上传
|
||||
testExecuteUpload(config);
|
||||
}
|
||||
|
||||
private void testExecuteUpload(S3FileClientConfig config) throws Exception {
|
||||
// 校验配置
|
||||
ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config);
|
||||
// 创建 Client
|
||||
S3FileClient client = new S3FileClient(0L, config);
|
||||
client.init();
|
||||
// 上传文件
|
||||
String path = IdUtil.fastSimpleUUID() + ".jpg";
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
String fullPath = client.upload(content, path);
|
||||
System.out.println("访问地址:" + fullPath);
|
||||
// 读取文件
|
||||
if (true) {
|
||||
byte[] bytes = client.getContent(path);
|
||||
System.out.println("文件内容:" + bytes.length);
|
||||
}
|
||||
// 删除文件
|
||||
if (false) {
|
||||
client.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package cn.iocoder.yudao.framework.file.core.client.sftp;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class SftpFileClientTest {
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void test() {
|
||||
// 创建客户端
|
||||
SftpFileClientConfig config = new SftpFileClientConfig();
|
||||
config.setDomain("http://127.0.0.1:48080");
|
||||
config.setBasePath("/home/ftp");
|
||||
config.setHost("kanchai.club");
|
||||
config.setPort(222);
|
||||
config.setUsername("");
|
||||
config.setPassword("");
|
||||
SftpFileClient client = new SftpFileClient(0L, config);
|
||||
client.init();
|
||||
// 上传文件
|
||||
String path = IdUtil.fastSimpleUUID() + ".jpg";
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
String fullPath = client.upload(content, path);
|
||||
System.out.println("访问地址:" + fullPath);
|
||||
if (false) {
|
||||
byte[] bytes = client.getContent(path);
|
||||
System.out.println("文件内容:" + bytes);
|
||||
}
|
||||
if (false) {
|
||||
client.delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 占位,避免 package 无法提交到 Git 仓库
|
||||
*/
|
||||
package cn.iocoder.yudao.framework.file.core.enums;
|
||||
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
37
yudao-framework/yudao-spring-boot-starter-flowable/pom.xml
Normal file
37
yudao-framework/yudao-spring-boot-starter-flowable/pom.xml
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>yudao-framework</artifactId>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>yudao-spring-boot-starter-flowable</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- flowable 工作流相关 -->
|
||||
<dependency>
|
||||
<groupId>org.flowable</groupId>
|
||||
<artifactId>flowable-spring-boot-starter-basic</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.flowable</groupId>
|
||||
<artifactId>flowable-spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@ -0,0 +1,22 @@
|
||||
package cn.iocoder.yudao.framework.flowable.config;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
|
||||
import cn.iocoder.yudao.framework.flowable.core.web.FlowableWebFilter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class YudaoFlowableConfiguration {
|
||||
|
||||
/**
|
||||
* 配置 flowable Web 过滤器
|
||||
*/
|
||||
@Bean
|
||||
public FilterRegistrationBean<FlowableWebFilter> flowableWebFilter() {
|
||||
FilterRegistrationBean<FlowableWebFilter> registrationBean = new FilterRegistrationBean<>();
|
||||
registrationBean.setFilter(new FlowableWebFilter());
|
||||
registrationBean.setOrder(WebFilterOrderEnum.FLOWABLE_FILTER);
|
||||
return registrationBean;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
package cn.iocoder.yudao.framework.flowable.core;
|
||||
@ -0,0 +1,62 @@
|
||||
package cn.iocoder.yudao.framework.flowable.core.util;
|
||||
|
||||
import org.flowable.bpmn.converter.BpmnXMLConverter;
|
||||
import org.flowable.bpmn.model.BpmnModel;
|
||||
import org.flowable.bpmn.model.FlowElement;
|
||||
import org.flowable.common.engine.impl.identity.Authentication;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class FlowableUtils {
|
||||
|
||||
public static void setAuthenticatedUserId(Long userId) {
|
||||
Authentication.setAuthenticatedUserId(String.valueOf(userId));
|
||||
}
|
||||
|
||||
public static void clearAuthenticatedUserId() {
|
||||
Authentication.setAuthenticatedUserId(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得 BPMN 流程中,指定的元素们
|
||||
*
|
||||
* @param model
|
||||
* @param clazz 指定元素。例如说,{@link org.flowable.bpmn.model.UserTask}、{@link org.flowable.bpmn.model.Gateway} 等等
|
||||
* @return 元素们
|
||||
*/
|
||||
public static <T extends FlowElement> List<T> getBpmnModelElements(BpmnModel model, Class<T> clazz) {
|
||||
List<T> result = new ArrayList<>();
|
||||
model.getProcesses().forEach(process -> {
|
||||
process.getFlowElements().forEach(flowElement -> {
|
||||
if (flowElement.getClass().isAssignableFrom(clazz)) {
|
||||
result.add((T) flowElement);
|
||||
}
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 比较 两个bpmnModel 是否相同
|
||||
* @param oldModel 老的bpmn model
|
||||
* @param newModel 新的bpmn model
|
||||
*/
|
||||
public static boolean equals(BpmnModel oldModel, BpmnModel newModel) {
|
||||
// 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较
|
||||
return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel));
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 bpmnModel 转换成 byte[]
|
||||
* @param model bpmnModel
|
||||
*/
|
||||
public static byte[] getBpmnBytes(BpmnModel model) {
|
||||
if (model == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
BpmnXMLConverter converter = new BpmnXMLConverter();
|
||||
return converter.convertToXML(model);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package cn.iocoder.yudao.framework.flowable.core.web;
|
||||
|
||||
import cn.iocoder.yudao.framework.flowable.core.util.FlowableUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
/**
|
||||
* flowable Web 过滤器,将 userId 设置到 {@link org.flowable.common.engine.impl.identity.Authentication} 中
|
||||
*
|
||||
* @author jason
|
||||
*/
|
||||
public class FlowableWebFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
try {
|
||||
// 设置工作流的用户
|
||||
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||
if (userId != null) {
|
||||
FlowableUtils.setAuthenticatedUserId(userId);
|
||||
}
|
||||
// 过滤
|
||||
chain.doFilter(request, response);
|
||||
} finally {
|
||||
// 清理
|
||||
FlowableUtils.clearAuthenticatedUserId();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
package cn.iocoder.yudao.framework.flowable;
|
||||
@ -0,0 +1,2 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
cn.iocoder.yudao.framework.flowable.config.YudaoFlowableConfiguration
|
||||
@ -1,6 +1,9 @@
|
||||
package cn.iocoder.yudao.framework.mq.config;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.system.SystemUtil;
|
||||
import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
|
||||
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
|
||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
||||
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
|
||||
@ -10,10 +13,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisServerCommands;
|
||||
import org.springframework.data.redis.connection.stream.Consumer;
|
||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
||||
import org.springframework.data.redis.connection.stream.ReadOffset;
|
||||
import org.springframework.data.redis.connection.stream.StreamOffset;
|
||||
import org.springframework.data.redis.core.RedisCallback;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.listener.ChannelTopic;
|
||||
@ -22,6 +27,7 @@ import org.springframework.data.redis.stream.DefaultStreamMessageListenerContain
|
||||
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* 消息队列配置类
|
||||
@ -73,6 +79,7 @@ public class YudaoMQAutoConfiguration {
|
||||
public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
|
||||
RedisMQTemplate redisMQTemplate, List<AbstractStreamMessageListener<?>> listeners) {
|
||||
RedisTemplate<String, ?> redisTemplate = redisMQTemplate.getRedisTemplate();
|
||||
checkRedisVersion(redisTemplate);
|
||||
// 第一步,创建 StreamMessageListenerContainer 容器
|
||||
// 创建 options 配置
|
||||
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
|
||||
@ -118,4 +125,19 @@ public class YudaoMQAutoConfiguration {
|
||||
return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Redis 版本号,是否满足最低的版本号要求!
|
||||
*/
|
||||
private static void checkRedisVersion(RedisTemplate<String, ?> redisTemplate) {
|
||||
// 获得 Redis 版本
|
||||
Properties info = redisTemplate.execute((RedisCallback<Properties>) RedisServerCommands::info);
|
||||
String version = MapUtil.getStr(info, "redis_version");
|
||||
// 校验最低版本必须大于等于 5.0.0
|
||||
int majorVersion = Integer.parseInt(StrUtil.subBefore(version, '.', false));
|
||||
if (majorVersion < 5) {
|
||||
throw new IllegalStateException(StrUtil.format("您当前的 Redis 版本为 {},小于最低要求的 5.0.0 版本!" +
|
||||
"请参考 {} 文档进行安装。", version, DocumentEnum.REDIS_INSTALL.getUrl()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -43,12 +43,16 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
|
||||
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
|
||||
}
|
||||
|
||||
default Integer selectCount(String field, Object value) {
|
||||
return selectCount(new QueryWrapper<T>().eq(field, value)).intValue();
|
||||
default Long selectCount() {
|
||||
return selectCount(new QueryWrapper<T>());
|
||||
}
|
||||
|
||||
default Integer selectCount(SFunction<T, ?> field, Object value) {
|
||||
return selectCount(new LambdaQueryWrapper<T>().eq(field, value)).intValue();
|
||||
default Long selectCount(String field, Object value) {
|
||||
return selectCount(new QueryWrapper<T>().eq(field, value));
|
||||
}
|
||||
|
||||
default Long selectCount(SFunction<T, ?> field, Object value) {
|
||||
return selectCount(new LambdaQueryWrapper<T>().eq(field, value));
|
||||
}
|
||||
|
||||
default List<T> selectList() {
|
||||
@ -76,4 +80,7 @@ public interface BaseMapperX<T> extends BaseMapper<T> {
|
||||
entities.forEach(this::insert);
|
||||
}
|
||||
|
||||
default void updateBatch(T update) {
|
||||
update(update, new QueryWrapper<>());
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,6 @@ import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.time.Duration;
|
||||
@ -24,11 +23,6 @@ public class SecurityProperties {
|
||||
*/
|
||||
@NotNull(message = "Token 过期时间不能为空")
|
||||
private Duration tokenTimeout;
|
||||
/**
|
||||
* Token 秘钥
|
||||
*/
|
||||
@NotEmpty(message = "Token 秘钥不能为空")
|
||||
private String tokenSecret;
|
||||
/**
|
||||
* Session 过期时间
|
||||
*
|
||||
|
||||
@ -66,7 +66,7 @@ public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
|
||||
* 注意,在线上环境下,一定要关闭该功能!!!
|
||||
*
|
||||
* @param request 请求
|
||||
* @param token 模拟的 token,格式为 {@link SecurityProperties#getTokenSecret()} + 用户编号
|
||||
* @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号
|
||||
* @return 模拟的 LoginUser
|
||||
*/
|
||||
private LoginUser mockLoginUser(HttpServletRequest request, String token) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package cn.iocoder.yudao.module.system.test;
|
||||
package cn.iocoder.yudao.framework.test.config;
|
||||
|
||||
import com.github.fppt.jedismock.RedisServer;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
|
||||
@ -9,6 +9,11 @@ import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Redis 测试 Configuration,主要实现内嵌 Redis 的启动
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Lazy(false) // 禁止延迟加载
|
||||
@EnableConfigurationProperties(RedisProperties.class)
|
||||
@ -20,7 +25,7 @@ public class RedisTestConfiguration {
|
||||
@Bean
|
||||
public RedisServer redisServer(RedisProperties properties) throws IOException {
|
||||
RedisServer redisServer = new RedisServer(properties.getPort());
|
||||
// TODO 芋艿:一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。
|
||||
// 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。
|
||||
try {
|
||||
redisServer.start();
|
||||
} catch (Exception ignore) {}
|
||||
@ -0,0 +1,52 @@
|
||||
package cn.iocoder.yudao.framework.test.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
|
||||
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer;
|
||||
import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer;
|
||||
import org.springframework.boot.sql.init.DatabaseInitializationSettings;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
/**
|
||||
* SQL 初始化的测试 Configuration
|
||||
*
|
||||
* 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢?
|
||||
* 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true,开启延迟加载。此时,会导致 DataSourceInitializationConfiguration 初始化
|
||||
* 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈!
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class)
|
||||
@ConditionalOnSingleCandidate(DataSource.class)
|
||||
@ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator")
|
||||
@Lazy(value = false) // 禁止延迟加载
|
||||
@EnableConfigurationProperties(SqlInitializationProperties.class)
|
||||
public class SqlInitializationTestConfiguration {
|
||||
|
||||
@Bean
|
||||
public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource,
|
||||
SqlInitializationProperties initializationProperties) {
|
||||
DatabaseInitializationSettings settings = createFrom(initializationProperties);
|
||||
return new DataSourceScriptDatabaseInitializer(dataSource, settings);
|
||||
}
|
||||
|
||||
static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) {
|
||||
DatabaseInitializationSettings settings = new DatabaseInitializationSettings();
|
||||
settings.setSchemaLocations(properties.getSchemaLocations());
|
||||
settings.setDataLocations(properties.getDataLocations());
|
||||
settings.setContinueOnError(properties.isContinueOnError());
|
||||
settings.setSeparator(properties.getSeparator());
|
||||
settings.setEncoding(properties.getEncoding());
|
||||
settings.setMode(properties.getMode());
|
||||
return settings;
|
||||
}
|
||||
|
||||
}
|
||||
@ -12,7 +12,7 @@
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>用户的认证、权限的校验</description>
|
||||
<description>Web 框架,全局异常、API 日志等</description>
|
||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<dependencies>
|
||||
|
||||
@ -8,15 +8,11 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import springfox.documentation.RequestHandler;
|
||||
import springfox.documentation.builders.ApiInfoBuilder;
|
||||
import springfox.documentation.builders.ExampleBuilder;
|
||||
import springfox.documentation.builders.PathSelectors;
|
||||
import springfox.documentation.service.ApiInfo;
|
||||
import springfox.documentation.service.ApiKey;
|
||||
import springfox.documentation.service.AuthorizationScope;
|
||||
import springfox.documentation.service.Contact;
|
||||
import springfox.documentation.service.SecurityReference;
|
||||
import springfox.documentation.service.SecurityScheme;
|
||||
import springfox.documentation.builders.RequestParameterBuilder;
|
||||
import springfox.documentation.service.*;
|
||||
import springfox.documentation.spi.DocumentationType;
|
||||
import springfox.documentation.spi.service.contexts.SecurityContext;
|
||||
import springfox.documentation.spring.web.plugins.Docket;
|
||||
@ -24,7 +20,6 @@ import springfox.documentation.swagger2.annotations.EnableSwagger2;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static springfox.documentation.builders.RequestHandlerSelectors.basePackage;
|
||||
|
||||
@ -37,8 +32,8 @@ import static springfox.documentation.builders.RequestHandlerSelectors.basePacka
|
||||
@EnableSwagger2
|
||||
@EnableKnife4j
|
||||
@ConditionalOnClass({Docket.class, ApiInfoBuilder.class})
|
||||
@ConditionalOnProperty(prefix = "yudao.swagger", value = "enable", matchIfMissing = true)
|
||||
// 允许使用 swagger.enable=false 禁用 Swagger
|
||||
@ConditionalOnProperty(prefix = "yudao.swagger", value = "enable", matchIfMissing = true)
|
||||
@EnableConfigurationProperties(SwaggerProperties.class)
|
||||
public class YudaoSwaggerAutoConfiguration {
|
||||
|
||||
@ -62,9 +57,12 @@ public class YudaoSwaggerAutoConfiguration {
|
||||
.paths(PathSelectors.any())
|
||||
.build()
|
||||
.securitySchemes(securitySchemes())
|
||||
.globalRequestParameters(globalRequestParameters())
|
||||
.securityContexts(securityContexts());
|
||||
}
|
||||
|
||||
// ========== apiInfo ==========
|
||||
|
||||
/**
|
||||
* API 摘要信息
|
||||
*/
|
||||
@ -77,6 +75,8 @@ public class YudaoSwaggerAutoConfiguration {
|
||||
.build();
|
||||
}
|
||||
|
||||
// ========== securitySchemes ==========
|
||||
|
||||
/**
|
||||
* 安全模式,这里配置通过请求头 Authorization 传递 token 参数
|
||||
*/
|
||||
@ -105,4 +105,12 @@ public class YudaoSwaggerAutoConfiguration {
|
||||
return new AuthorizationScope[]{new AuthorizationScope("global", "accessEverything")};
|
||||
}
|
||||
|
||||
// ========== globalRequestParameters ==========
|
||||
|
||||
private static List<RequestParameter> globalRequestParameters() {
|
||||
RequestParameterBuilder tenantParameter = new RequestParameterBuilder().name("tenant-id").description("租户编号")
|
||||
.in(ParameterType.HEADER).example(new ExampleBuilder().value(1L).build());
|
||||
return Collections.singletonList(tenantParameter.build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
package cn.iocoder.yudao.framework.web.core.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.web.config.WebProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 过滤 /admin-api、/app-api 等 API 请求的过滤器
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ApiRequestFilter extends OncePerRequestFilter {
|
||||
|
||||
protected final WebProperties webProperties;
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
// 只过滤 API 请求的地址
|
||||
return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(),
|
||||
webProperties.getAppApi().getPrefix());
|
||||
}
|
||||
|
||||
}
|
||||
@ -43,8 +43,8 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF = new ErrorCode(1009004002, "流程取消失败,该流程不是你发起的");
|
||||
|
||||
// ========== 流程任务 1-009-005-000 ==========
|
||||
ErrorCode TASK_COMPLETE_FAIL_NOT_EXISTS = new ErrorCode(1009004000, "审批任务失败,原因:该任务不处于未审批");
|
||||
ErrorCode TASK_COMPLETE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1009004001, "审批任务失败,原因:该任务的审批人不是你");
|
||||
ErrorCode TASK_COMPLETE_FAIL_NOT_EXISTS = new ErrorCode(1009005000, "审批任务失败,原因:该任务不处于未审批");
|
||||
ErrorCode TASK_COMPLETE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1009005001, "审批任务失败,原因:该任务的审批人不是你");
|
||||
|
||||
// ========== 流程任务分配规则 1-009-006-000 ==========
|
||||
ErrorCode TASK_ASSIGN_RULE_EXISTS = new ErrorCode(1009006000, "流程({}) 的任务({}) 已经存在分配规则");
|
||||
@ -55,7 +55,7 @@ public interface ErrorCodeConstants {
|
||||
|
||||
// ========== 动态表单模块 1-009-010-000 ==========
|
||||
ErrorCode FORM_NOT_EXISTS = new ErrorCode(1009010000, "动态表单不存在");
|
||||
ErrorCode FORM_FIELD_REPEAT = new ErrorCode(1009010000, "表单项({}) 和 ({}) 使用了相同的字段名({})");
|
||||
ErrorCode FORM_FIELD_REPEAT = new ErrorCode(1009010001, "表单项({}) 和 ({}) 使用了相同的字段名({})");
|
||||
|
||||
// ========== 用户组模块 1-009-011-000 ==========
|
||||
ErrorCode USER_GROUP_NOT_EXISTS = new ErrorCode(1009011000, "用户组不存在");
|
||||
|
||||
@ -32,6 +32,10 @@
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-operatelog</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.iocoder.boot</groupId>
|
||||
<artifactId>yudao-spring-boot-starter-biz-data-permission</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
|
||||
@ -88,4 +88,12 @@ public interface BpmFormService {
|
||||
*/
|
||||
PageResult<BpmFormDO> getFormPage(BpmFormPageReqVO pageReqVO);
|
||||
|
||||
/**
|
||||
* 校验流程表单已配置
|
||||
*
|
||||
* @param configStr configStr 字段
|
||||
* @return 流程表单
|
||||
*/
|
||||
BpmFormDO checkFormConfig(String configStr);
|
||||
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package cn.iocoder.yudao.module.bpm.service.definition;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormCreateReqVO;
|
||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO;
|
||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormUpdateReqVO;
|
||||
@ -8,18 +9,20 @@ import cn.iocoder.yudao.module.bpm.convert.definition.BpmFormConvert;
|
||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
|
||||
import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmFormMapper;
|
||||
import cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
|
||||
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmFormFieldRespDTO;
|
||||
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* 动态表单 Service 实现类
|
||||
@ -87,6 +90,29 @@ public class BpmFormServiceImpl implements BpmFormService {
|
||||
return formMapper.selectPage(pageReqVO);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public BpmFormDO checkFormConfig(String configStr) {
|
||||
BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(configStr, BpmModelMetaInfoRespDTO.class);
|
||||
if (metaInfo == null || metaInfo.getFormType() == null) {
|
||||
throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG);
|
||||
}
|
||||
// 校验表单存在
|
||||
if (Objects.equals(metaInfo.getFormType(), BpmModelFormTypeEnum.NORMAL.getType())) {
|
||||
BpmFormDO form = getForm(metaInfo.getFormId());
|
||||
if (form == null) {
|
||||
throw exception(FORM_NOT_EXISTS);
|
||||
}
|
||||
return form;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void checkKeyNCName(String key) {
|
||||
if (!ValidationUtils.isXmlNCName(key)) {
|
||||
throw exception(MODEL_KEY_VALID);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 校验 Field,避免 field 重复
|
||||
*
|
||||
|
||||
@ -5,7 +5,7 @@ import lombok.Data;
|
||||
|
||||
/**
|
||||
* BPM 流程 MetaInfo Response DTO
|
||||
* 主要用于 {@link org.activiti.engine.repository.Model#setMetaInfo(String)} 的存储
|
||||
* 主要用于 { Model#setMetaInfo(String)} 的存储
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@ -22,7 +22,6 @@ public interface BpmMessageService {
|
||||
*/
|
||||
void sendMessageWhenProcessInstanceApprove(@Valid BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO);
|
||||
|
||||
|
||||
/**
|
||||
* 发送流程实例被不通过的消息
|
||||
*
|
||||
|
||||
@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.bpm.test;
|
||||
|
||||
import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
|
||||
import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
|
||||
import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration;
|
||||
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
|
||||
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
@ -21,7 +21,6 @@ import org.springframework.test.context.jdbc.Sql;
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class)
|
||||
@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件
|
||||
@Sql(scripts = "/sql/create_tables.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD) // 每个单元测试结束前,创建表
|
||||
@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB
|
||||
public class BaseDbUnitTest {
|
||||
|
||||
@ -31,7 +30,7 @@ public class BaseDbUnitTest {
|
||||
DataSourceAutoConfiguration.class, // Spring DB 自动配置类
|
||||
DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
|
||||
DruidDataSourceAutoConfigure.class, // Druid 自动配置类
|
||||
SqlInitializationAutoConfiguration.class,
|
||||
SqlInitializationTestConfiguration.class, // SQL 初始化
|
||||
// MyBatis 配置类
|
||||
YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
|
||||
MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
|
||||
|
||||
@ -16,6 +16,9 @@ spring:
|
||||
druid:
|
||||
async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
|
||||
initial-size: 1 # 单元测试,配置为 1,提升启动速度
|
||||
sql:
|
||||
init:
|
||||
schema-locations: classpath:/sql/create_tables.sql
|
||||
|
||||
mybatis:
|
||||
lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
-- bpm 开头的 DB
|
||||
DELETE FROM "bpm_form";
|
||||
DELETE FROM "bpm_user_group";
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
-- bpm 开头的 DB
|
||||
CREATE TABLE IF NOT EXISTS "bpm_user_group" (
|
||||
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
"name" varchar(63) NOT NULL,
|
||||
@ -11,7 +10,7 @@ CREATE TABLE IF NOT EXISTS "bpm_user_group" (
|
||||
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"deleted" bit NOT NULL DEFAULT FALSE,
|
||||
PRIMARY KEY ("id")
|
||||
) COMMENT '用户组';
|
||||
) COMMENT '用户组';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "bpm_form" (
|
||||
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-module-bpm-impl-activiti</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
|
||||
@ -8,7 +8,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 流程实例 Api 实现类
|
||||
* Activiti 流程实例 Api 实现类
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
|
||||
@ -93,5 +93,4 @@ public class BpmModelController {
|
||||
bpmModelService.updateModelState(reqVO.getId(), reqVO.getState());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package cn.iocoder.yudao.module.bpm.controller.admin.definition;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleCreateReqVO;
|
||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleRespVO;
|
||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleUpdateReqVO;
|
||||
import cn.iocoder.yudao.module.bpm.service.definition.BpmTaskAssignRuleService;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiImplicitParam;
|
||||
import io.swagger.annotations.ApiImplicitParams;
|
||||
@ -32,7 +32,7 @@ public class BpmTaskAssignRuleController {
|
||||
@ApiOperation(value = "获得任务分配规则列表")
|
||||
@ApiImplicitParams({
|
||||
@ApiImplicitParam(name = "modelId", value = "模型编号", example = "1024", dataTypeClass = String.class),
|
||||
@ApiImplicitParam(name = "processDefinitionId", value = "刘晨定义的编号", example = "2048", dataTypeClass = String.class)
|
||||
@ApiImplicitParam(name = "processDefinitionId", value = "流程定义的编号", example = "2048", dataTypeClass = String.class)
|
||||
})
|
||||
@PreAuthorize("@ss.hasPermission('bpm:task-assign-rule:query')")
|
||||
public CommonResult<List<BpmTaskAssignRuleRespVO>> getTaskAssignRuleList(
|
||||
|
||||
@ -37,7 +37,7 @@ public class BpmTaskController {
|
||||
@GetMapping("done-page")
|
||||
@ApiOperation("获取 Done 已办任务分页")
|
||||
@PreAuthorize("@ss.hasPermission('bpm:task:query')")
|
||||
public CommonResult<PageResult<BpmTaskDonePageItemRespVO>> getTodoTaskPage(@Valid BpmTaskDonePageReqVO pageVO) {
|
||||
public CommonResult<PageResult<BpmTaskDonePageItemRespVO>> getDoneTaskPage(@Valid BpmTaskDonePageReqVO pageVO) {
|
||||
return success(taskService.getDoneTaskPage(getLoginUserId(), pageVO));
|
||||
}
|
||||
|
||||
|
||||
@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.bpm.convert.definition;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
|
||||
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
|
||||
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
|
||||
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
|
||||
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO;
|
||||
import org.activiti.engine.impl.persistence.entity.SuspensionState;
|
||||
import org.activiti.engine.repository.Deployment;
|
||||
import org.activiti.engine.repository.Model;
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
package cn.iocoder.yudao.module.bpm.convert.definition;
|
||||
|
||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageItemRespVO;
|
||||
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
|
||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
|
||||
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO;
|
||||
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO;
|
||||
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
|
||||
import org.activiti.engine.impl.persistence.entity.SuspensionState;
|
||||
import org.activiti.engine.repository.Deployment;
|
||||
import org.activiti.engine.repository.ProcessDefinition;
|
||||
|
||||
@ -44,7 +44,7 @@ public class BpmActivityBehaviorFactory extends DefaultActivityBehaviorFactory {
|
||||
|
||||
@Override
|
||||
public UserTaskActivityBehavior createUserTaskActivityBehavior(UserTask userTask) {
|
||||
BpmUserTaskActivitiBehavior userTaskActivityBehavior = new BpmUserTaskActivitiBehavior(userTask);
|
||||
BpmUserTaskActivityBehavior userTaskActivityBehavior = new BpmUserTaskActivityBehavior(userTask);
|
||||
userTaskActivityBehavior.setBpmTaskRuleService(bpmTaskRuleService);
|
||||
userTaskActivityBehavior.setPermissionApi(permissionApi);
|
||||
userTaskActivityBehavior.setDeptApi(deptApi);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user