mirror of
https://github.com/continew-org/continew-admin.git
synced 2025-09-08 22:57:12 +08:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
dd386231be | |||
2d86b0f249 | |||
b5acdb1c1c | |||
cb3184e9f1 | |||
f2258d821b | |||
b305dd7e53 | |||
fac8922933 | |||
5bc5666be9 | |||
61a6cac714 | |||
84e7f60dd4 | |||
b1a3e20494 | |||
![]() |
af0f58a096 | ||
21b753e5eb | |||
ac825032e2 | |||
![]() |
2bb2f96857 | ||
df6e294cbd | |||
3551d16f24 | |||
7ad12effae | |||
e4f4554eef | |||
6129b9fd1f | |||
7f05453d8c | |||
![]() |
93d8168e9f |
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -17,7 +17,7 @@ body:
|
||||
required: true
|
||||
- label: 查阅过 [使用指南](https://continew.top/admin/backend/structure.html) 和 [常见问题](https://continew.top/admin/faq.html) ,仍然认为很有必要
|
||||
required: true
|
||||
- label: 查阅过 [需求墙](https://continew.top/admin/other/feature.html),仍没有该功能计划
|
||||
- label: 查阅过 [需求墙](https://continew.top/admin/feature.html),仍没有该功能计划
|
||||
required: true
|
||||
- label: 搜索了项目 Issues,没有其他人提交过类似的 Feature(如果对应 Feature 尚未实现,您可以先订阅关注该 Issue,为了方便后来者查找问题解决方法,请避免创建重复的 Issue)
|
||||
required: true
|
||||
|
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
password: ${{ secrets.SERVER_PASSWORD }}
|
||||
source: ./continew-server/target/app/*
|
||||
target: /docker/continew-admin
|
||||
target: ${{ secrets.SERVER_PATH }}/continew-admin
|
||||
strip_components: 3
|
||||
# 5、启动
|
||||
- name: Start
|
||||
@@ -47,6 +47,6 @@ jobs:
|
||||
username: ${{ secrets.SERVER_USERNAME }}
|
||||
password: ${{ secrets.SERVER_PASSWORD }}
|
||||
script: |
|
||||
cd /docker
|
||||
cd ${{ secrets.SERVER_PATH }}
|
||||
docker-compose up --force-recreate --build -d continew-server
|
||||
docker images | grep none | awk '{print $3}' | xargs docker rmi
|
||||
docker images | grep none | awk '{print $3}' | xargs -r docker rmi
|
||||
|
137
README.md
137
README.md
@@ -1,7 +1,7 @@
|
||||
# ContiNew Admin 多租户中后台管理框架
|
||||
|
||||
<a href="https://github.com/continew-org/continew-admin" title="Release" target="_blank">
|
||||
<img src="https://img.shields.io/badge/RELEASE-v4.0.0-%23ff3f59.svg" alt="Release" />
|
||||
<img src="https://img.shields.io/badge/SNAPSHOT-v4.1.0-%23ff3f59.svg" alt="Release" />
|
||||
</a>
|
||||
<a href="https://github.com/continew-org/continew-starter" title="ContiNew Starter" target="_blank">
|
||||
<img src="https://img.shields.io/badge/ContiNew Starter-2.13.4-%236CB52D.svg" alt="ContiNew Starter" />
|
||||
@@ -84,11 +84,13 @@ ContiNew Admin(Continue New Admin),页面现代美观,且专注设计与
|
||||
|
||||
**AI 编程纪元已经开启,基于 ContiNew 项目开发,让 AI 助手“学习”更优雅的代码规范,“写出”更优质的代码。**
|
||||
|
||||
1.**甄选技术栈:** ContiNew(Continue New) 项目致力于持续迭代优化,让技术不掉队。在技术选型时,进行深度广泛地调研,从流行度、成熟度和发展潜力等多方面甄选技术栈。
|
||||
**1.长期稳定:** 自 2022 年 12 月 8 日创建,2023 年 3 月 26 日发布 v1.0.0,截至今日,ContiNew Admin 已累计发布 25 个版本,ContiNew Starter 已累计发布 43 个版本。
|
||||
|
||||
2.**Starter 组件:** 从 v2.1.0 版本开始,抽取并封装后端基础组件及各框架集成配置到 ContiNew Starter 项目,且 **[已发布至 Maven 中央仓库](https://central.sonatype.com/search?q=continew-starter&namespace=top.continew)**,可在你的任意项目中直接引入所需依赖使用。即使你不用脚手架项目,难道能让你搭项目框架更快、更爽、更省力的 Starter 也要 Say No 吗?
|
||||
**2.甄选技术栈:** ContiNew(Continue New) 项目致力于持续迭代优化,确保技术栈紧跟时代。在技术选型时,我们进行了深度广泛的调研,从流行度、成熟度和发展潜力等多维度精心挑选技术栈。
|
||||
|
||||
3.**CRUD 套件:** 封装通用增删改查套件,适配后端各分层,几分钟即可提供一套 CRUD API,包括新增、修改、批量删除、查询详情、分页列表查询、全部列表查询、树型列表查询、导出到 Excel,甚至是字典列表(用于下拉选项场景),且 API 支持按实际所需开放或扩展。
|
||||
**3.Starter 组件:** 从 v2.1.0 版本开始,我们将后端基础组件及各框架集成配置抽取并封装到 ContiNew Starter 项目中,极大降低上手和升级难度。且 **[已发布至 Maven 中央仓库](https://central.sonatype.com/search?q=continew-starter&namespace=top.continew)**,你可以在任意项目中直接引入所需依赖使用。即使你不使用完整的中后台框架,这些能让你搭项目框架更快、更爽、更省力的 Starter 组件,难道不香吗?
|
||||
|
||||
**4.CRUD 套件:** 封装通用增删改查套件,适配后端各分层架构,几分钟即可提供一套完整的 CRUD API,包括新增、修改、批量删除、查询详情、分页列表、全部列表、树型列表、Excel 导出,甚至是字典列表(用于下拉选项场景)。所有 API 均可根据实际需求灵活开放或扩展。
|
||||
|
||||
```java
|
||||
@Tag(name = "部门管理 API")
|
||||
@@ -97,35 +99,42 @@ ContiNew Admin(Continue New Admin),页面现代美观,且专注设计与
|
||||
public class DeptController extends BaseController<DeptService, DeptResp, DeptDetailResp, DeptQuery, DeptReq> {}
|
||||
```
|
||||
|
||||
4.**代码生成器:** 提供代码生成器,已配套前、后端代码生成模板,数据表设计完之后,简单配置一下即可生成前、后端 80% 的代码,包含 CRUD API、权限控制、参数校验、接口文档等内容。如果业务不复杂,也可能就是 95% 的代码。
|
||||
**5.代码生成器:** 同步提供了代码生成器,配套前后端代码生成模板。数据表设计完成后,简单配置即可生成前后端 80% 的代码,包括 CRUD API、权限控制、参数校验、接口文档等内容。若业务不复杂,甚至能覆盖 95% 的代码量。
|
||||
|
||||
5.**改善开发体验:** 持续优化及适配能改善开发体验的组件。
|
||||
**6.提升开发体验:** 持续优化并适配各类能提升开发体验的组件。
|
||||
|
||||
- 适配 ContiNew Starter 各组件,针对 Spring 基础配置、通用解决方案以及流行框架进行了深度封装的 starter 集合,改善你在开发每个 Spring Boot Web 项目的体验。(时间日期及枚举参数自动转换、默认线程池、跨域、加密、脱敏、限流、幂等、License、日志、异常及响应通用解决方案等等,更多细节可查看 Starter 源码)
|
||||
- 适配 Crane4j 数据填充组件,减少因为一个用户名而产生的联表回填;
|
||||
- 适配 SpEL Validator 基于 SpEL 的 Java 参数校验,使用 SpEL 表达式,强化基础参数校验。例如:当其中一个字段为 xxx 时,另一个字段不能为空等等;
|
||||
- 适配 P6Spy SQL 性能分析组件,开发期间方便监控 SQL 执行;
|
||||
- 适配 TLog 链路追踪组件,方便在杂乱的日志文件中追踪你某次请求的日志记录;
|
||||
- 适配 JetCache 缓存框架(比 Spring Cache 更强大易用),通过注解声明即可快速实现方法级缓存,极大改善编码式缓存体验,且支持灵活的二级缓存配置、分布式自动刷新等能力;
|
||||
- 前端适配 Vue Devtools(Vue 官方提供的调试浏览器插件),极大提高 Vue 开发及调试效率
|
||||
- ContiNew Starter 组件集合:针对 Spring 基础配置、通用解决方案及流行框架进行深度封装,改善你开发每个 Spring Boot Web 项目的体验(包含时间日期及枚举参数自动转换、默认线程池、跨域、加密、脱敏、限流、幂等、License、日志、异常及响应通用解决方案等);
|
||||
- Crane4j 数据填充组件:减少因单个字段(如用户名)而产生的联表查询;
|
||||
- SpEL Validator:基于 SpEL 表达式的参数校验,强化复杂场景下的参数验证(如:当某字段为特定值时,另一字段不能为空);
|
||||
- P6Spy SQL 性能分析:开发期间可方便地监控 SQL 执行性能;
|
||||
- TLog 链路追踪:在繁杂的日志中快速定位某次请求的完整日志;
|
||||
- JetCache 缓存框架:通过注解即可实现方法级缓存,支持灵活的二级缓存配置和分布式自动刷新;
|
||||
|
||||
6.**Almost最佳后端规范:** 后端严格遵循阿里巴巴 Java 编码规范,注释覆盖率 > 45%,接口参数示例 100%,代码分层使用体验佳,变量、方法命名清晰统一,前端代码也使用严格的 ESLint、StyleLint 等检查。良好的设计,代码复用率极高!写代码时,让你有一种无需多写,理应如此的感觉。我是代码洁癖,我实际写的时候很清楚这到底是不是乱吹。
|
||||
**7.Almost 最佳后端规范:** 后端严格遵循阿里巴巴 Java 编码规范,注释覆盖率 > 45%,接口参数示例 100%。代码分层清晰,变量与方法命名统一规范,前端代码同样采用严格的 ESLint、StyleLint 等检查。优秀的设计带来极高的代码复用率!开发时,你会有一种“无需多写,理应如此”的流畅感。
|
||||
|
||||
7.**卓越工程:** 后端采用模块化工程结构,并适配了统一项目版本号、编译项目自动代码格式化等插件,提供了自定义打包部署结构配置(配置文件、三方依赖和主程序分离),提供全套环境及应用的 Docker Compose 部署脚本。为了减少您开发新项目时的改造耗时,项目品牌配置持续进行深度聚合,简单的配置和结构修改即可快速开始独属于你的新项目。我们还进行了全局 Lombok 配置,继承场景默认自动生效 @EqualsAndHashCode(callSuper = true)、@ToString(callSuper = true),不需要你手动添加了,并且主动禁用了部分 Lombok 注解,例如:@Val、@Log4j...,杜绝“又菜又爱玩”的 partner 滥用。
|
||||
**8.卓越工程化实践:** 后端采用模块化工程结构,集成了统一版本管理、编译时自动代码格式化等插件。提供自定义打包部署配置(配置文件、第三方依赖与主程序分离),以及全套环境和应用的 Docker Compose 部署脚本。
|
||||
|
||||
8.**业务脚手架:** 有颜有料,不止是说说而已,持续打磨 UI 设计与色彩主题。提供基于 RBAC 的权限控制、通用数据权限,包含丰富的通用业务功能:第三方登录,邮箱、短信(生产级炸弹漏洞处理方案),个人中心、用户管理、角色管理、部门管理、系统配置(基础站点配置、邮件配置、安全配置)、系统日志、消息中心、通知公告等,设计用心,逻辑合理闭环。
|
||||
为减少新项目开发的改造成本,我们持续深度聚合项目品牌配置,通过简单的配置和结构修改,即可快速启动你的专属项目。
|
||||
|
||||
> 一个好的脚手架项目,不仅仅是提供一系列组件集成与配置,也不仅仅是封装一堆好用的工具,还更应该提供一系列通用基础业务解决方案及设计,为初创团队项目减负。
|
||||
我们还进行了全局 Lombok 配置,继承场景默认自动应用 `@EqualsAndHashCode(callSuper = true)` 和 `@ToString(callSuper = true)`,无需手动添加。同时主动禁用了部分 Lombok 注解(如 `@Val`、`@Log4j` 等),避免“又菜又爱玩”的 partner 滥用。
|
||||
|
||||
9.**质量与安全:** CI 已集成 Sonar、Codacy,Push 即扫描代码质量,定期扫描 CVE 漏洞,及时解决潜在问题。封装数据库字段加密、JSON 脱敏、XSS 过滤等工具,提供诸多安全解决方案。
|
||||
**9.全能业务脚手架:** 支持 **SaaS 租户架构**,基于 RBAC 的权限控制与通用数据权限管理。精心设计的 UI 界面与色彩主题,兼具美观与实用性。内置丰富的通用业务解决方案:第三方登录、邮箱/短信服务(含生产级漏洞处理方案)、个人中心、用户管理、角色管理、组织管理、系统配置、系统日志、消息中心、通知公告等,逻辑闭环,开箱即用。
|
||||
|
||||
由于篇幅有限,且项目正处于高速发展期,更多功能正在陆续上线(敬请关注仓库或群内动态)。另外像最基本的统一异常、错误处理,基础线程池等配置就不在此赘述,细节优化详情请 clone 代码查看。
|
||||
> 优秀的中后台框架不仅提供组件集成与配置,封装好用的工具,更应提供通用基础业务设计及解决方案,为初创团队减负。
|
||||
|
||||
**10.质量与安全并重:** 我们高度重视项目质量与安全,CI 已集成 Sonar、Codacy,代码提交即自动扫描质量问题。定期扫描 CVE 漏洞,及时解决潜在风险。封装了数据库字段加密、JSON 脱敏、XSS 过滤等工具,提供全方位的安全解决方案。
|
||||
|
||||
许多项目在开发或交付过程中需满足 Sonarqube 等质量指标,使用 ContiNew Admin 框架,让你从一开始就站在高质量的起点。
|
||||
|
||||
---
|
||||
|
||||
由于篇幅有限,且项目正处于高速发展期,更多功能正在持续开发中,敬请关注仓库或加入交流群了解最新动态。至于统一异常处理、错误处理、基础线程池配置(默认线程参数、线程上下文支持异步传递)等基础特性,这里不再赘述,更多细节优化欢迎克隆代码体验。
|
||||
> Talk is cheap, show the code.
|
||||
|
||||
## 系统功能
|
||||
|
||||
> [!TIP]
|
||||
> 更多功能和优化正在赶来💦,最新项目计划、进展请进群或关注 [需求墙](https://continew.top/admin/other/feature.html) 和 [更新日志](https://continew.top/admin/changelog/)。
|
||||
> 更多功能和优化正在赶来💦,最新项目计划、进展请进群或关注 [需求墙](https://continew.top/admin/feature.html) 和 [更新日志](https://continew.top/admin/changelog/)。
|
||||
> 功能不会用?请查看 [功能手册](https://continew.top/admin/function/tenant/management.html)。
|
||||
|
||||
- 仪表盘:提供工作台、分析页,工作台提供功能快捷导航入口、最新公告、动态;分析页提供全面数据可视化能力
|
||||
@@ -292,9 +301,9 @@ continew-admin
|
||||
│ │ ├─ main
|
||||
│ │ │ ├─ java/top/continew/admin
|
||||
│ │ │ │ ├─ config (配置)
|
||||
│ │ │ │ ├─ controller
|
||||
│ │ │ │ │ ├─ common(通用相关 API)
|
||||
│ │ │ │ │ └─ monitor(系统监控相关 API)
|
||||
│ │ │ │ │ ├─ log(操作日志配置)
|
||||
│ │ │ │ │ └─ satoken(SaToken 认证配置)
|
||||
│ │ │ │ ├─ controller(通用 API)
|
||||
│ │ │ │ ├─ job (定时任务)
|
||||
│ │ │ │ └─ ContiNewAdminApplication.java(ContiNew Admin 启动程序)
|
||||
│ │ │ └─ resources
|
||||
@@ -323,9 +332,11 @@ continew-admin
|
||||
│ │ │ │ │ │ ├─ req(系统认证相关请求参数(Request))
|
||||
│ │ │ │ │ │ └─ resp(系统认证相关响应参数(Response))
|
||||
│ │ │ │ │ ├─ enums(系统认证相关枚举)
|
||||
│ │ │ │ │ ├─ constant(系统认证相关常量)
|
||||
│ │ │ │ │ ├─ handler(系统认证相关处理器)
|
||||
│ │ │ │ │ └─ config(系统认证相关配置)
|
||||
│ │ │ │ └─ system(系统管理相关业务)
|
||||
│ │ │ │ ├─ api(系统管理相关公共业务 API 实现)
|
||||
│ │ │ │ ├─ controller(系统管理相关 API)
|
||||
│ │ │ │ ├─ service(系统管理相关业务接口及实现类)
|
||||
│ │ │ │ ├─ mapper(系统管理相关 Mapper)
|
||||
@@ -335,8 +346,10 @@ continew-admin
|
||||
│ │ │ │ │ ├─ req(系统管理相关请求参数(Request))
|
||||
│ │ │ │ │ └─ resp(系统管理相关响应参数(Response))
|
||||
│ │ │ │ ├─ enums(系统管理相关枚举)
|
||||
│ │ │ │ ├─ constant(系统管理相关常量)
|
||||
│ │ │ │ ├─ util(系统管理相关工具类)
|
||||
│ │ │ │ ├─ validation(系统管理相关参数校验工具类)
|
||||
│ │ │ │ ├─ container(系统管理相关 Crane4j 数据填充容器配置)
|
||||
│ │ │ │ └─ config(系统管理相关配置)
|
||||
│ │ │ └─ resources
|
||||
│ │ │ └─ mapper(系统管理相关 Mapper XML 文件目录)
|
||||
@@ -355,23 +368,26 @@ continew-admin
|
||||
│ │ │ │ │ ├─ req(能力开放相关请求参数(Request))
|
||||
│ │ │ │ │ └─ resp(能力开放相关响应参数(Response))
|
||||
│ │ │ │ ├─ util(能力开放相关工具类)
|
||||
│ │ │ │ ├─ handler(能力开放相关处理器)
|
||||
│ │ │ │ ├─ sign(能力开放相关 API 参数签名算法)
|
||||
│ │ │ │ └─ config(能力开放相关配置)
|
||||
│ │ │ └─ test(测试相关代码目录)
|
||||
│ │ └─ pom.xml
|
||||
│ ├─ continew-plugin-tenant(租户插件模块)
|
||||
│ │ ├─ src
|
||||
│ │ │ ├─ main/java/top/continew/admin/tenant
|
||||
│ │ │ │ ├─ api(租户相关公共业务 API 实现)
|
||||
│ │ │ │ ├─ controller(租户相关 API)
|
||||
│ │ │ │ ├─ service(租户相关业务接口及实现类)
|
||||
│ │ │ │ ├─ mapper(租户相关 Mapper)
|
||||
│ │ │ │ ├─ model(租户相关模型)
|
||||
│ │ │ │ │ ├─ enums(租户相关枚举)
|
||||
│ │ │ │ │ ├─ entity(租户相关实体)
|
||||
│ │ │ │ │ ├─ query(租户相关查询条件)
|
||||
│ │ │ │ │ ├─ req(租户相关请求参数(Request))
|
||||
│ │ │ │ │ └─ resp(租户相关响应参数(Response))
|
||||
│ │ │ │ ├─ util(租户相关工具类)
|
||||
│ │ │ │ ├─ enums(租户相关枚举)
|
||||
│ │ │ │ ├─ constant(租户相关常量类)
|
||||
│ │ │ │ ├─ util(租户相关工具类)
|
||||
│ │ │ │ └─ config(租户相关配置)
|
||||
│ │ │ └─ test(测试相关代码目录)
|
||||
│ │ └─ pom.xml
|
||||
@@ -380,14 +396,15 @@ continew-admin
|
||||
│ │ │ ├─ main/java/top/continew/admin/schedule
|
||||
│ │ │ │ ├─ controller(任务调度相关 API)
|
||||
│ │ │ │ ├─ service(代码生成器相关业务接口及实现类)
|
||||
│ │ │ │ ├─ api(任务调度中心相关 API)
|
||||
│ │ │ │ ├─ api(任务调度中心相关 Feign API)
|
||||
│ │ │ │ ├─ model(任务调度相关模型)
|
||||
│ │ │ │ │ ├─ query(任务调度相关查询条件)
|
||||
│ │ │ │ │ ├─ req(任务调度相关请求参数(Request))
|
||||
│ │ │ │ │ └─ resp(任务调度相关响应参数(Response))
|
||||
│ │ │ │ ├─ constant(任务调度相关常量类)
|
||||
│ │ │ │ ├─ enums(任务调度相关枚举)
|
||||
│ │ │ │ ├─ constant(任务调度相关常量类)
|
||||
│ │ │ │ ├─ exception(任务调度相关异常)
|
||||
│ │ │ │ ├─ annotation(任务调度相关注解)
|
||||
│ │ │ │ └─ config(任务调度相关配置)
|
||||
│ │ │ └─ test(测试相关代码目录)
|
||||
│ │ └─ pom.xml
|
||||
@@ -415,6 +432,7 @@ continew-admin
|
||||
├─ continew-common(公共模块,存放公共工具类,公共配置等)
|
||||
│ ├─ src
|
||||
│ │ ├─ main/java/top/continew/admin/common
|
||||
│ │ │ ├─ api(公共业务 API)
|
||||
│ │ │ ├─ base(公共基类)
|
||||
│ │ │ │ ├─ controller(控制器基类)
|
||||
│ │ │ │ ├─ mapper(Mapper 接口基类)
|
||||
@@ -422,14 +440,15 @@ continew-admin
|
||||
│ │ │ │ │ ├─ entity(实体基类)
|
||||
│ │ │ │ │ └─ resp(列表、详情响应基类)
|
||||
│ │ │ │ └─ service(业务接口及实现基类)
|
||||
│ │ │ ├─ service(公共服务接口)
|
||||
│ │ │ ├─ model(公共模型)
|
||||
│ │ │ │ ├─ dto(公共数据传输对象(DTO))
|
||||
│ │ │ │ └─ req(公共请求参数(Request))
|
||||
│ │ │ ├─ context(公共上下文)
|
||||
│ │ │ ├─ constant(公共常量类)
|
||||
│ │ │ ├─ enums(公共枚举)
|
||||
│ │ │ ├─ constant(公共常量类)
|
||||
│ │ │ ├─ util(公共工具类)
|
||||
│ │ │ └─ config(公共配置)
|
||||
│ │ │ ├─ crud(CRUD 配置)
|
||||
│ │ │ ├─ mybatis(MyBatis Plus 配置)
|
||||
│ │ │ ├─ websocket(Websocket 配置)
|
||||
│ │ │ ├─ doc(接口文档配置)
|
||||
@@ -470,57 +489,49 @@ continew-admin
|
||||
└─ pom.xml(包含版本锁定及全局插件相关配置)
|
||||
```
|
||||
|
||||
## 贡献指南
|
||||
## 参与贡献
|
||||
|
||||
ContiNew Admin 致力于提供开箱即用,持续舒适的开发体验。作为一个开源项目,Creator 的初心是希望 ContiNew Admin 依托开源协作模式,提升技术透明度、放大集体智慧、共创优秀实践,源源不断地为企业级项目开发提供助力。
|
||||
ContiNew(Continue New)系列项目致力于通过持续迭代,为开发者提供舒适的开发体验。作为开源社区,我们的初衷是希望通过开源协作模式,提升技术透明度、放大集体智慧、共创优秀实践,源源不断地为企业级项目开发提供助力。
|
||||
|
||||
我们非常欢迎广大社区用户为 ContiNew Admin **贡献(开发,测试、文档、答疑等)** 或优化代码,欢迎各位感兴趣的小伙伴儿,[添加微信](https://continew.top/discussion.html) 讨论或认领任务。
|
||||
我们诚挚邀请广大社区用户为 ContiNew 项目贡献力量,包括但不限于 Issue 排查、测试验证、代码开发与重构等。每一份贡献,都是推动项目进步的重要力量(请查阅 [贡献指南](https://continew.top/about/contributing.html))。欢迎各位感兴趣的小伙伴儿,[添加微信](https://continew.top/discussion.html) 讨论或认领任务。
|
||||
|
||||
### 分支说明
|
||||
|
||||
ContiNew Admin 的分支目前分为下个大版本的开发分支和上个大版本的维护分支,PR 前请注意对应分支是否处于维护状态,版本支持情况请查看 [更新日志/版本支持](https://continew.top/admin/changelog/)。
|
||||
ContiNew 系列项目采用清晰的分支策略,确保开发与维护有序进行。提交 PR 前,请确认目标分支是否处于活跃维护状态,版本支持情况请查看 [更新日志#版本支持](https://continew.top/admin/changelog/)。
|
||||
|
||||
| 分支 | 说明 |
|
||||
| ----- | ------------------------------------------------------------ |
|
||||
| dev | 开发分支,默认为下个大版本的 SNAPSHOT 版本,接受新功能或新功能优化 PR |
|
||||
| x.x.x | 维护分支,在 vx.x.x 版本维护期终止前(一般为下个大版本发布前),用于修复上个版本的 Bug,只接受已有功能修复,不接受新功能 PR |
|
||||
| dev | 开发分支,用于下个大版本的 SNAPSHOT 开发,接受新功能或功能优化 PR |
|
||||
| x.x.x | 维护分支,用于特定版本(如 vx.x.x)的 bug 修复,仅接受已有功能的修复 PR,不接受新功能 |
|
||||
|
||||
### 贡献代码
|
||||
### 流程步骤
|
||||
|
||||
如果您想提交新功能或优化现有代码,可以按照以下步骤操作:
|
||||
若您希望提交新功能或优化现有代码,请遵循以下步骤:
|
||||
|
||||
1. 首先,在 Gitee 或 Github 上将项目 fork 到您自己的仓库
|
||||
2. 然后,将 fork 过来的项目(即您的项目)克隆到本地
|
||||
3. 基于当前仍在维护的分支(例如:dev),切出来一个新的分支(例如:feat/tenant)(不要修改源分支,源分支仅做同步 continew 最新代码用)
|
||||
4. 在新分支开始修改代码,修改完成后,将代码 commit 并 push 到您的远程仓库
|
||||
5. 在 Gitee 或 Github 上新建 pull request(pr),选择好源和目标,按模板要求填写说明信息后提交即可(多多参考 [已批准合并的 pr 记录](https://github.com/continew-org/continew-admin/pulls?q=is%3Apr+is%3Amerged),会大大增加批准合并率)
|
||||
6. 提交后,会提示你需要签署 CLA(Contributor License Agreement,贡献者协议),请确保你的 commit 所用邮箱和对应平台绑定邮箱一致(如果不一致,可以在本地通过 `git reset --soft HEAD~1` 回退,然后使用正确邮箱重新提交,最后 `git push -f` 即可,不需要重新创建 PR),然后使用该邮箱签署即可
|
||||
7. 最后,耐心等待维护者合并您的请求即可
|
||||
8. PR 合并后,下次 PR 时请先同步最新代码,然后再次从步骤 3 开始
|
||||
|
||||
以下是向 `continew-admin` 项目提交 pull request 为例的简单步骤:
|
||||
|
||||
```
|
||||
1.continew/continew-admin -> fork -> your/continew-admin
|
||||
2.git clone your/continew-admin
|
||||
3.dev -> feat/tenant
|
||||
4.feat/tenant 本地开发,开发完 push 到 your/continew-admin
|
||||
5.your/continew-admin:feat/tenant -> continew/continew-admin:dev
|
||||
6.阅读并签署 CLA
|
||||
7.PR 合并完成,删除 feat/tenant 分支
|
||||
8.强制从 continew/continew-admin 覆盖更新 your/continew-admin,然后重复步骤 3...
|
||||
```
|
||||
1. 在开源平台上将项目 fork 到您的个人仓库
|
||||
2. 将 fork 的项目克隆到本地开发环境
|
||||
3. 基于当前维护的分支(如 dev)创建新分支(如 feat/newFeature),请勿直接修改源分支(源分支仅做同步 ContiNew 最新代码用)
|
||||
4. 在新分支上进行代码修改,完成后提交并 push 到您的远程仓库
|
||||
5. 在开源平台上创建 pull request (PR),选择正确的源分支和目标分支,按模板填写说明信息(参考 [已合并的 PR](https://github.com/continew-org/continew-admin-ui/pulls?q=is%3Apr+is%3Amerged) 可提高合并率)
|
||||
6. 提交 PR 后,系统会提示签署 CLA(贡献者协议)。请确保 commit 使用的邮箱与平台绑定邮箱一致(如果不一致,可以在本地通过 `git reset --soft HEAD~1` 回退,然后使用正确邮箱重新提交,最后 `git push -f` 即可,不需要重新创建 PR),然后使用该邮箱签署即可
|
||||
7. 耐心等待维护者审核并合并您的 PR(建议通过交流群进行快捷沟通)
|
||||
8. PR 合并后,下次贡献前请先同步最新代码,再重复步骤 3 开始
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 欢迎大家为 ContiNew Admin 贡献代码,我们非常感谢您的支持!为了更好地管理项目,维护者有一些要求:
|
||||
> 为了确保项目质量和协作效率,请注意以下事项:
|
||||
>
|
||||
> 1. 请确保代码、配置文件的结构和命名规范良好,完善的代码注释甚至包括接口文档参数示例,并遵循阿里巴巴的 <a href="https://github.com/continew-org/continew-admin/blob/dev/.style/Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C(%E9%BB%84%E5%B1%B1%E7%89%88).pdf" target="_blank">《Java开发手册(黄山版)》</a> 中的代码规范,保证代码质量和可维护性
|
||||
> 2. 在提交代码前,请按照 [Angular 提交规范](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular) 编写 commit 的 message(建议在 IntelliJ IDEA 中下载并安装 Git Commit Template 插件,以便按照规范进行 commit)
|
||||
> 3. 提交代码之前,请关闭所有代码窗口,执行 `mvn compile` 命令(代码格式化插件会在项目编译时对全局代码进行格式修正),编译通过后,不要再打开查看任何代码窗口,直接提交即可,以免不同的 IDE 配置会自动进行代码格式化
|
||||
> 1. 代码和配置文件请参考已有风格,遵循清晰的结构与命名规范,提供完善的注释(包括接口文档和参数示例),后端代码请符合阿里巴巴 <a href="https://github.com/continew-org/continew-admin/blob/dev/.style/Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C(%E9%BB%84%E5%B1%B1%E7%89%88).pdf" target="_blank">《Java开发手册(黄山版)》</a> 中的代码规范
|
||||
> 2. 提交后端代码前请关闭所有代码窗口,执行 `mvn compile` 命令进行代码格式化(ContiNew 项目后端编译时会自动执行插件进行代码格式修正)。编译通过后请勿再次打开代码窗口,避免不同 IDE 配置导致的格式差异
|
||||
> 3. 提交时,请按照 [Angular 提交规范](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular) 编写 commit message(参考已有风格)
|
||||
|
||||
## 反馈交流
|
||||
|
||||
欢迎各位小伙伴儿扫描下方二维码加入项目交流群,与项目维护团队及其他大佬用户实时交流讨论。
|
||||
感谢您对 ContiNew 开源项目的关注与支持!我们非常重视每一位用户的反馈和建议,这是推动项目不断进步的动力。 欢迎扫描下方二维码加入我们的官方交流群,与项目维护团队及其他大佬用户实时交流探讨。
|
||||
|
||||
- 与项目核心团队直接沟通,获取第一手项目动态
|
||||
- 解决使用过程中遇到的问题,分享经验心得
|
||||
- 参与功能讨论和需求收集,影响项目未来发展
|
||||
- 结识志同道合的技术爱好者,扩展人脉圈
|
||||
|
||||
<div align="left">
|
||||
<img src=".image/qrcode.jpg" alt="二维码" height="230px" />
|
||||
|
@@ -31,12 +31,18 @@
|
||||
<groupId>org.dromara.x-file-storage</groupId>
|
||||
<artifactId>x-file-storage-spring</artifactId>
|
||||
</dependency>
|
||||
<!-- Amazon S3(Amazon Simple Storage Service,亚马逊简单存储服务,通用存储协议 S3,兼容主流云厂商对象存储) -->
|
||||
<!-- Amazon S3(Amazon Simple Storage Service,亚马逊简单存储服务,通用存储协议 S3,兼容主流云厂商对象存储)后续会移除替换1.x的版本 -->
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-s3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Amazon S3 2.x version (Amazon Simple Storage Service,亚马逊简单存储服务,通用存储协议 S3,兼容主流云厂商对象存储) -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- FreeMarker(模板引擎) -->
|
||||
<dependency>
|
||||
<groupId>org.freemarker</groupId>
|
||||
|
@@ -32,7 +32,8 @@ public interface MenuApi {
|
||||
* 查询树结构列表
|
||||
*
|
||||
* @param excludeMenuIds 排除的菜单 ID 列表
|
||||
* @param isSimple 是否是简单树结构
|
||||
* @return 树结构列表
|
||||
*/
|
||||
List<Tree<Long>> listTree(List<Long> excludeMenuIds);
|
||||
List<Tree<Long>> listTree(List<Long> excludeMenuIds, boolean isSimple);
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ public class DefaultDataPermissionUserDataProvider implements DataPermissionUser
|
||||
|
||||
@Override
|
||||
public boolean isFilter() {
|
||||
return !UserContextHolder.isSuperAdminUser() && !UserContextHolder.isTenantAdminUser();
|
||||
return !UserContextHolder.isSuperAdmin() && !UserContextHolder.isTenantAdmin();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -60,9 +60,14 @@ public class RegexConstants {
|
||||
public static final String PACKAGE_NAME = "^(?:[a-zA-Z_][a-zA-Z0-9_]*\\.)*[a-zA-Z_][a-zA-Z0-9_]*$";
|
||||
|
||||
/**
|
||||
* HTTP 域名 URL 正则(非 IP 地址)
|
||||
* HTTP URL 正则
|
||||
*/
|
||||
public static final String HTTP_DOMAIN_URL = "^(https?:\\/\\/)([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}(\\/[^\\s]*)?$";
|
||||
public static final String URL_HTTP = "^(https?)://[\\w-+&@#/%?=~_|!:,.;]*[\\w-+&@#/%=~_|]$";
|
||||
|
||||
/**
|
||||
* HTTP URL 正则(非 IP 地址)
|
||||
*/
|
||||
public static final String URL_HTTP_NOT_IP = "^(https?:\\/\\/)([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}(\\/[^\\s]*)?$";
|
||||
|
||||
/**
|
||||
* HTTP HOST 正则
|
||||
|
@@ -132,11 +132,11 @@ public class UserContext implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为超级管理员用户
|
||||
* 是否为超级管理员
|
||||
*
|
||||
* @return true:是;false:否
|
||||
*/
|
||||
public boolean isSuperAdminUser() {
|
||||
public boolean isSuperAdmin() {
|
||||
if (CollUtil.isEmpty(roleCodes)) {
|
||||
return false;
|
||||
}
|
||||
@@ -144,11 +144,11 @@ public class UserContext implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为租户管理员用户
|
||||
* 是否为租户管理员
|
||||
*
|
||||
* @return true:是;false:否
|
||||
*/
|
||||
public boolean isTenantAdminUser() {
|
||||
public boolean isTenantAdmin() {
|
||||
if (CollUtil.isEmpty(roleCodes)) {
|
||||
return false;
|
||||
}
|
||||
|
@@ -181,22 +181,22 @@ public class UserContextHolder {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为超级管理员用户
|
||||
* 是否为超级管理员
|
||||
*
|
||||
* @return true:是;false:否
|
||||
*/
|
||||
public static boolean isSuperAdminUser() {
|
||||
public static boolean isSuperAdmin() {
|
||||
StpUtil.checkLogin();
|
||||
return getContext().isSuperAdminUser();
|
||||
return getContext().isSuperAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为租户管理员用户
|
||||
* 是否为租户管理员
|
||||
*
|
||||
* @return true:是;false:否
|
||||
*/
|
||||
public static boolean isTenantAdminUser() {
|
||||
public static boolean isTenantAdmin() {
|
||||
StpUtil.checkLogin();
|
||||
return getContext().isTenantAdminUser();
|
||||
return getContext().isTenantAdmin();
|
||||
}
|
||||
}
|
||||
|
@@ -334,7 +334,7 @@ public class GeneratorServiceImpl implements GeneratorService {
|
||||
if (!isBackend) {
|
||||
fileName = ".vue".equals(extension) && "index".equals(classNameSuffix)
|
||||
? "index.vue"
|
||||
: this.getFrontendFileName(classNamePrefix, className, extension);
|
||||
: this.getFrontendFileName(classNamePrefix, classNameSuffix, extension);
|
||||
}
|
||||
generatePreview.setFileName(fileName);
|
||||
generatePreview.setContent(engine.getTemplate(templateConfig.getTemplatePath())
|
||||
|
@@ -68,5 +68,5 @@ export function export${classNamePrefix}(query: ${classNamePrefix}Query) {
|
||||
|
||||
/** @desc 查询${businessName}字典 */
|
||||
export function list${classNamePrefix}Dict(query?: ${classNamePrefix}Query) {
|
||||
return http.get<LabelValueState[]>(`${BASE_URL}/dict`, query)
|
||||
return http.get<LabelValueState[]>(`${'$'}{BASE_URL}/dict`, query)
|
||||
}
|
||||
|
@@ -100,15 +100,15 @@
|
||||
</template>
|
||||
</GiTable>
|
||||
|
||||
<${classNamePrefix}AddModal ref="${classNamePrefix}AddModalRef" @save-success="search" />
|
||||
<${classNamePrefix}DetailDrawer ref="${classNamePrefix}DetailDrawerRef" />
|
||||
<AddModal ref="AddModalRef" @save-success="search" />
|
||||
<DetailDrawer ref="DetailDrawerRef" />
|
||||
</GiPageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TableInstance } from '@arco-design/web-vue'
|
||||
import ${classNamePrefix}AddModal from './${classNamePrefix}AddModal.vue'
|
||||
import ${classNamePrefix}DetailDrawer from './${classNamePrefix}DetailDrawer.vue'
|
||||
import AddModal from './AddModal.vue'
|
||||
import DetailDrawer from './DetailDrawer.vue'
|
||||
import { type ${classNamePrefix}Resp, type ${classNamePrefix}Query, delete${classNamePrefix}, export${classNamePrefix}, list${classNamePrefix} } from '@/apis/${apiModuleName}/${apiName}'
|
||||
import { useDownload, useTable } from '@/hooks'
|
||||
import { useDict } from '@/hooks/app'
|
||||
@@ -185,21 +185,21 @@ const onExport = () => {
|
||||
useDownload(() => export${classNamePrefix}(queryForm))
|
||||
}
|
||||
|
||||
const ${classNamePrefix}AddModalRef = ref<InstanceType<typeof ${classNamePrefix}AddModal>>()
|
||||
const AddModalRef = ref<InstanceType<typeof AddModal>>()
|
||||
// 新增
|
||||
const onAdd = () => {
|
||||
${classNamePrefix}AddModalRef.value?.onAdd()
|
||||
AddModalRef.value?.onAdd()
|
||||
}
|
||||
|
||||
// 修改
|
||||
const onUpdate = (record: ${classNamePrefix}Resp) => {
|
||||
${classNamePrefix}AddModalRef.value?.onUpdate(record.id)
|
||||
AddModalRef.value?.onUpdate(record.id)
|
||||
}
|
||||
|
||||
const ${classNamePrefix}DetailDrawerRef = ref<InstanceType<typeof ${classNamePrefix}DetailDrawer>>()
|
||||
const DetailDrawerRef = ref<InstanceType<typeof DetailDrawer>>()
|
||||
// 详情
|
||||
const onDetail = (record: ${classNamePrefix}Resp) => {
|
||||
${classNamePrefix}DetailDrawerRef.value?.onOpen(record.id)
|
||||
DetailDrawerRef.value?.onOpen(record.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@@ -22,6 +22,7 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import top.continew.admin.common.api.system.MenuApi;
|
||||
import top.continew.admin.common.base.controller.BaseController;
|
||||
@@ -56,7 +57,7 @@ public class PackageController extends BaseController<PackageService, PackageRes
|
||||
@Operation(summary = "查询租户套餐菜单", description = "查询租户套餐菜单树列表")
|
||||
@SaCheckPermission("tenant:package:list")
|
||||
@GetMapping("/menu/tree")
|
||||
public List<Tree<Long>> listMenuTree() {
|
||||
return menuApi.listTree(tenantExtensionProperties.getIgnoreMenus());
|
||||
public List<Tree<Long>> listMenuTree(@RequestParam(required = false, defaultValue = "true") Boolean isSimple) {
|
||||
return menuApi.listTree(tenantExtensionProperties.getIgnoreMenus(), isSimple);
|
||||
}
|
||||
}
|
@@ -45,6 +45,20 @@ public class TenantQuery implements Serializable {
|
||||
@Query(columns = {"name", "description"}, type = QueryType.LIKE)
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
@Schema(description = "编码", example = "T0stxiJK6RMH")
|
||||
@Query(type = QueryType.EQ)
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 域名
|
||||
*/
|
||||
@Schema(description = "域名", example = "admin.continew.top")
|
||||
@Query(type = QueryType.LIKE)
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 套餐 ID
|
||||
*/
|
||||
|
@@ -33,9 +33,9 @@ import java.io.Serializable;
|
||||
public class TenantAdminUserPwdUpdateReq implements Serializable {
|
||||
|
||||
/**
|
||||
* 新密码(加密)
|
||||
* 新密码
|
||||
*/
|
||||
@Schema(description = "新密码(加密)", example = "E7c72TH+LDxKTwavjM99W1MdI9Lljh79aPKiv3XB9MXcplhm7qJ1BJCj28yaflbdVbfc366klMtjLIWQGqb0qw==")
|
||||
@Schema(description = "新密码", example = "RSA 公钥加密的新密码")
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
private String password;
|
||||
}
|
||||
|
@@ -97,9 +97,9 @@ public class TenantReq implements Serializable {
|
||||
private String adminUsername;
|
||||
|
||||
/**
|
||||
* 管理员密码(加密)
|
||||
* 管理员密码
|
||||
*/
|
||||
@Schema(description = "管理员密码(加密)", example = "E7c72TH+LDxKTwavjM99W1MdI9Lljh79aPKiv3XB9MXcplhm7qJ1BJCj28yaflbdVbfc366klMtjLIWQGqb0qw==")
|
||||
@Schema(description = "管理员密码", example = "RSA 公钥加密的管理员密码")
|
||||
@NotBlank(message = "管理员密码不能为空", groups = CrudValidationGroup.Create.class)
|
||||
private String adminPassword;
|
||||
|
||||
|
@@ -47,6 +47,7 @@ import top.continew.starter.core.util.validation.CheckUtils;
|
||||
import top.continew.starter.extension.crud.annotation.CrudRequestMapping;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Sa-Token 配置
|
||||
@@ -110,7 +111,7 @@ public class SaTokenConfiguration {
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void configureSaTokenExcludes() {
|
||||
String[] beanNames = applicationContext.getBeanDefinitionNames();
|
||||
List<String> additionalExcludes = Arrays.stream(beanNames).parallel().map(beanName -> {
|
||||
List<String> additionalExcludes = Arrays.stream(beanNames).map(beanName -> {
|
||||
Object bean = applicationContext.getBean(beanName);
|
||||
Class<?> clazz = bean.getClass();
|
||||
if (AopUtils.isAopProxy(bean)) {
|
||||
@@ -130,10 +131,11 @@ public class SaTokenConfiguration {
|
||||
}).filter(Objects::nonNull).toList();
|
||||
if (!additionalExcludes.isEmpty()) {
|
||||
// 合并现有的 excludes 和新扫描到的
|
||||
List<String> allExcludes = new ArrayList<>(Arrays.asList(properties.getSecurity().getExcludes()));
|
||||
allExcludes.addAll(additionalExcludes);
|
||||
// 转回数组
|
||||
properties.getSecurity().setExcludes(allExcludes.toArray(new String[0]));
|
||||
String[] existingExcludes = Optional.ofNullable(properties.getSecurity().getExcludes())
|
||||
.orElse(new String[0]);
|
||||
String[] combinedExcludes = Stream.concat(Arrays.stream(existingExcludes), additionalExcludes.stream())
|
||||
.toArray(String[]::new);
|
||||
properties.getSecurity().setExcludes(combinedExcludes);
|
||||
}
|
||||
log.debug("缓存 CRUD API 权限前缀完成:{}", CrudApiPermissionPrefixCache.getAll().values());
|
||||
}
|
||||
|
@@ -6,7 +6,7 @@ application:
|
||||
# 描述
|
||||
description: 持续迭代优化的前后端分离中后台管理系统框架,开箱即用,持续提供舒适的开发体验。
|
||||
# 版本
|
||||
version: 4.0.0
|
||||
version: 4.1.0-SNAPSHOT
|
||||
starter: 2.13.4
|
||||
# 基本包
|
||||
base-package: top.continew.admin
|
||||
@@ -236,9 +236,10 @@ continew-starter.rate-limiter:
|
||||
sa-token:
|
||||
# Token 名称(同时也是 cookie 名称)
|
||||
token-name: Authorization
|
||||
## 提示:通过 [系统管理/系统配置/客户端配置]功能 动态维护,如果不需要可移除相关配置代码,放开下方注释
|
||||
# 是否启用动态 activeTimeout 功能
|
||||
dynamic-active-timeout: true
|
||||
## 提示:请通过页面功能 [系统管理/系统配置/客户端配置] 动态调整 timeout、active-timeout 配置项
|
||||
## 如果不需要动态配置,请将 dynamic-active-timeout 设为 false,并取消下方 timeout 和 active-timeout 配置的注释,最好再移除相关登录处理代码
|
||||
# # Token 有效期(单位:秒,默认 30 天,-1 代表永不过期)
|
||||
# timeout: 86400
|
||||
# # Token 最低活跃频率(单位:秒,默认 -1,代表不限制,永不冻结。如果 token 超过此时间没有访问系统就会被冻结)
|
||||
|
@@ -43,9 +43,9 @@ public class AccountLoginReq extends LoginReq {
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 密码(加密)
|
||||
* 密码
|
||||
*/
|
||||
@Schema(description = "密码(加密)", example = "HHwZoiBwCfh0xLdWOAd0bHOkEZlIMMOQKJyeFUw9T3ArrhL57od2i42s1o0sSXKkeHPJXvQsninhPFH2lArDDQ==")
|
||||
@Schema(description = "密码", example = "RSA 公钥加密的密码")
|
||||
@NotBlank(message = "密码不能为空")
|
||||
private String password;
|
||||
|
||||
|
@@ -27,6 +27,7 @@ import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -140,6 +141,12 @@ public class UserInfoResp implements Serializable {
|
||||
@Schema(description = "角色编码集合", example = "[\"test\"]")
|
||||
private Set<String> roles;
|
||||
|
||||
/**
|
||||
* 角色名称列表
|
||||
*/
|
||||
@Schema(description = "角色名称列表", example = "测试人员")
|
||||
private List<String> roleNames;
|
||||
|
||||
public LocalDate getRegistrationDate() {
|
||||
return createTime.toLocalDate();
|
||||
}
|
||||
|
@@ -39,11 +39,11 @@ public class MenuApiImpl implements MenuApi {
|
||||
private final MenuService baseService;
|
||||
|
||||
@Override
|
||||
public List<Tree<Long>> listTree(List<Long> excludeMenuIds) {
|
||||
public List<Tree<Long>> listTree(List<Long> excludeMenuIds, boolean isSimple) {
|
||||
MenuQuery query = new MenuQuery();
|
||||
query.setStatus(DisEnableStatusEnum.ENABLE);
|
||||
// 过滤掉租户不能使用的菜单
|
||||
query.setExcludeMenuIdList(excludeMenuIds);
|
||||
return baseService.tree(query, null, true);
|
||||
return baseService.tree(query, null, isSimple);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.constant;
|
||||
|
||||
/**
|
||||
* 分片上传常量
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2025/7/30 17:40
|
||||
*/
|
||||
public class MultipartUploadConstants {
|
||||
//todo 后续改为从配置文件读取
|
||||
/**
|
||||
* MD5到uploadId的映射前缀
|
||||
* <p>
|
||||
* 用于存储文件MD5到uploadId的映射关系,实现基于MD5的双列Map结构。
|
||||
* 键格式:multipart:md5_to_upload:{md5}
|
||||
* 值格式:Hash结构,包含uploadId和fileInfo
|
||||
* </p>
|
||||
*/
|
||||
public static final String MD5_TO_UPLOAD_ID_PREFIX = "multipart:md5_to_upload:";
|
||||
|
||||
/**
|
||||
* 分片上传信息前缀
|
||||
* <p>
|
||||
* 用于存储分片上传的初始化信息,包含uploadId、bucket、path等基本信息。
|
||||
* 键格式:multipart:upload:{uploadId}
|
||||
* 值格式:JSON字符串,包含MultipartInitResp的序列化数据
|
||||
* </p>
|
||||
*/
|
||||
public static final String MULTIPART_UPLOAD_PREFIX = "multipart:upload:";
|
||||
|
||||
/**
|
||||
* 分片信息前缀
|
||||
* <p>
|
||||
* 用于存储所有分片的上传信息,使用Hash结构存储。
|
||||
* 键格式:multipart:parts:{uploadId}
|
||||
* 值格式:Hash结构,field为分片编号,value为FilePartInfo的JSON序列化数据
|
||||
* </p>
|
||||
*/
|
||||
public static final String MULTIPART_PARTS_PREFIX = "multipart:parts:";
|
||||
|
||||
/**
|
||||
* 元数据前缀
|
||||
* <p>
|
||||
* 用于存储分片上传的元数据信息,如文件名、大小、类型等。
|
||||
* 键格式:multipart:metadata:{uploadId}
|
||||
* 值格式:Hash结构,field为元数据键,value为元数据值
|
||||
* </p>
|
||||
*/
|
||||
public static final String MULTIPART_METADATA_PREFIX = "multipart:metadata:";
|
||||
|
||||
/**
|
||||
* 过期时间前缀
|
||||
* <p>
|
||||
* 用于存储分片上传的过期时间,用于定期清理过期数据。
|
||||
* 键格式:multipart:expire:{uploadId}
|
||||
* 值格式:ISO格式的时间字符串
|
||||
* </p>
|
||||
*/
|
||||
public static final String MULTIPART_EXPIRE_PREFIX = "multipart:expire:";
|
||||
|
||||
/**
|
||||
* 默认过期时间(小时)
|
||||
* <p>
|
||||
* 分片上传缓存数据的默认过期时间,超过此时间的数据会被自动清理。
|
||||
* 设置为24小时,平衡存储空间和用户体验。
|
||||
* </p>
|
||||
*/
|
||||
public static final long DEFAULT_EXPIRE_HOURS = 24;
|
||||
|
||||
/**
|
||||
* 临时文件夹
|
||||
* <p>
|
||||
* 分片上传的临时文件夹名称
|
||||
* </p>
|
||||
*/
|
||||
public static final String TEMP_DIR_NAME = "temp";
|
||||
|
||||
/**
|
||||
* 分片大小
|
||||
*/
|
||||
public static final long MULTIPART_UPLOAD_PART_SIZE = 5 * 1024 * 1024;
|
||||
}
|
@@ -60,7 +60,7 @@ public class FileController extends BaseController<FileService, FileResp, FileRe
|
||||
/**
|
||||
* 上传文件
|
||||
* <p>
|
||||
* 公共上传文件请使用 {@link top.continew.admin.controller.common.CommonController#upload}
|
||||
* 公共上传文件请使用 {@link CommonController#upload}
|
||||
* </p>
|
||||
*
|
||||
* @param file 文件
|
||||
@@ -69,7 +69,7 @@ public class FileController extends BaseController<FileService, FileResp, FileRe
|
||||
* @throws IOException /
|
||||
*/
|
||||
@Operation(summary = "上传文件", description = "上传文件")
|
||||
@Parameter(name = "parentPath", description = "上级目录", example = "/", in = ParameterIn.QUERY)
|
||||
@Parameter(name = "parentPath", description = "上级目录(默认:/yyyy/MM/dd)", example = "/", in = ParameterIn.QUERY)
|
||||
@SaCheckPermission("system:file:upload")
|
||||
@PostMapping("/upload")
|
||||
public FileUploadResp upload(@NotNull(message = "文件不能为空") @RequestPart MultipartFile file,
|
||||
|
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import top.continew.admin.system.model.entity.FileDO;
|
||||
import top.continew.admin.system.model.req.MultipartUploadInitReq;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadResp;
|
||||
import top.continew.admin.system.service.MultipartUploadService;
|
||||
|
||||
/**
|
||||
* 分片上传控制器
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2025/7/30 16:38
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/system/multipart-upload")
|
||||
@RequiredArgsConstructor
|
||||
public class MultipartUploadController {
|
||||
|
||||
private final MultipartUploadService multipartUploadService;
|
||||
|
||||
/**
|
||||
* 初始化分片上传
|
||||
*
|
||||
* @param multiPartUploadInitReq 分片上传信息
|
||||
* @return 初始化响应
|
||||
*/
|
||||
@Operation(summary = "初始化分片上传", description = "初始化分片上传,返回uploadId等信息")
|
||||
@PostMapping("/init")
|
||||
public MultipartUploadInitResp initMultipartUpload(@RequestBody @Valid MultipartUploadInitReq multiPartUploadInitReq) {
|
||||
return multipartUploadService.initMultipartUpload(multiPartUploadInitReq);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传分片
|
||||
*
|
||||
* @param file 分片文件
|
||||
* @param uploadId 上传ID
|
||||
* @param partNumber 分片编号
|
||||
* @param path 文件路径
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Operation(summary = "上传分片", description = "上传单个分片")
|
||||
@PostMapping("/part")
|
||||
public MultipartUploadResp uploadPart(@RequestPart("file") MultipartFile file,
|
||||
@RequestParam("uploadId") String uploadId,
|
||||
@RequestParam("partNumber") Integer partNumber,
|
||||
@RequestParam("path") String path) {
|
||||
return multipartUploadService.uploadPart(file, uploadId, partNumber, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并分片
|
||||
*
|
||||
* @param uploadId 上传ID
|
||||
*/
|
||||
@Operation(summary = "完成分片上传", description = "合并所有分片,完成上传")
|
||||
@GetMapping("/complete/{uploadId}")
|
||||
public FileDO completeMultipartUpload(@PathVariable String uploadId) {
|
||||
return multipartUploadService.completeMultipartUpload(uploadId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消分片上传
|
||||
*
|
||||
* @param uploadId 上传ID
|
||||
*/
|
||||
@Operation(summary = "取消分片上传", description = "删除缓存信息,分片数据")
|
||||
@GetMapping("/cancel/{uploadId}")
|
||||
public void cancelMultipartUpload(@PathVariable String uploadId) {
|
||||
multipartUploadService.cancelMultipartUpload(uploadId);
|
||||
}
|
||||
|
||||
}
|
@@ -25,7 +25,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import top.continew.admin.system.model.query.OptionQuery;
|
||||
import top.continew.admin.system.model.req.OptionReq;
|
||||
import top.continew.admin.system.model.req.OptionResetValueReq;
|
||||
import top.continew.admin.system.model.req.OptionValueResetReq;
|
||||
import top.continew.admin.system.model.resp.OptionResp;
|
||||
import top.continew.admin.system.service.OptionService;
|
||||
|
||||
@@ -65,7 +65,7 @@ public class OptionController {
|
||||
@SaCheckPermission(value = {"system:siteConfig:update", "system:securityConfig:update", "system:loginConfig:update",
|
||||
"system:mailConfig:update"}, mode = SaMode.OR)
|
||||
@PatchMapping("/value")
|
||||
public void resetValue(@RequestBody @Valid OptionResetValueReq req) {
|
||||
public void resetValue(@RequestBody @Valid OptionValueResetReq req) {
|
||||
baseService.resetValue(req);
|
||||
}
|
||||
}
|
@@ -32,7 +32,7 @@ import top.continew.admin.common.base.controller.BaseController;
|
||||
import top.continew.admin.system.model.query.RoleQuery;
|
||||
import top.continew.admin.system.model.query.RoleUserQuery;
|
||||
import top.continew.admin.system.model.req.RoleReq;
|
||||
import top.continew.admin.system.model.req.RoleUpdatePermissionReq;
|
||||
import top.continew.admin.system.model.req.RolePermissionUpdateReq;
|
||||
import top.continew.admin.system.model.resp.role.RoleDetailResp;
|
||||
import top.continew.admin.system.model.resp.role.RolePermissionResp;
|
||||
import top.continew.admin.system.model.resp.role.RoleResp;
|
||||
@@ -75,7 +75,7 @@ public class RoleController extends BaseController<RoleService, RoleResp, RoleDe
|
||||
@Operation(summary = "修改权限", description = "修改角色的功能权限")
|
||||
@SaCheckPermission("system:role:updatePermission")
|
||||
@PutMapping("/{id}/permission")
|
||||
public void updatePermission(@PathVariable("id") Long id, @RequestBody @Valid RoleUpdatePermissionReq req) {
|
||||
public void updatePermission(@PathVariable("id") Long id, @RequestBody @Valid RolePermissionUpdateReq req) {
|
||||
baseService.updatePermission(id, req);
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.dao;
|
||||
|
||||
import top.continew.admin.system.model.resp.file.FilePartInfo;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分片上传持久化接口
|
||||
* <p>
|
||||
* 纯粹的缓存操作,不包含业务逻辑:
|
||||
* 1. MD5到uploadId的映射管理
|
||||
* 2. 分片信息缓存
|
||||
* 3. 上传状态缓存
|
||||
* </p>
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2.14.0
|
||||
*/
|
||||
public interface MultipartUploadDao {
|
||||
|
||||
/**
|
||||
* 根据MD5获取uploadId
|
||||
*
|
||||
* @param md5 文件MD5值
|
||||
* @return uploadId,如果不存在则返回null
|
||||
*/
|
||||
String getUploadIdByMd5(String md5);
|
||||
|
||||
/**
|
||||
* 缓存MD5到uploadId的映射
|
||||
*
|
||||
* @param md5 文件MD5值
|
||||
* @param uploadId 上传ID
|
||||
*/
|
||||
void setMd5Mapping(String md5, String uploadId);
|
||||
|
||||
/**
|
||||
* 删除MD5映射
|
||||
*
|
||||
* @param md5 文件MD5值
|
||||
*/
|
||||
void deleteMd5Mapping(String md5);
|
||||
|
||||
/**
|
||||
* 设置缓存分片上传信息
|
||||
*
|
||||
* @param uploadId 上传ID
|
||||
* @param initResp 初始化响应
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
void setMultipartUpload(String uploadId, MultipartUploadInitResp initResp, Map<String, String> metadata);
|
||||
|
||||
/**
|
||||
* 获取分片上传信息
|
||||
*
|
||||
* @param uploadId 上传ID
|
||||
* @return 分片上传信息,如果不存在则返回null
|
||||
*/
|
||||
MultipartUploadInitResp getMultipartUpload(String uploadId);
|
||||
|
||||
/**
|
||||
* 删除分片上传信息
|
||||
*
|
||||
* @param uploadId 上传ID
|
||||
*/
|
||||
void deleteMultipartUpload(String uploadId);
|
||||
|
||||
void deleteMultipartUploadAll(String uploadId);
|
||||
|
||||
/**
|
||||
* 设置缓存分片信息
|
||||
*
|
||||
* @param uploadId 上传ID
|
||||
* @param filePartInfo 分片信息
|
||||
*/
|
||||
void setFilePart(String uploadId, FilePartInfo filePartInfo);
|
||||
|
||||
/**
|
||||
* 获取所有分片信息
|
||||
*
|
||||
* @param uploadId 上传ID
|
||||
* @return 分片信息列表
|
||||
*/
|
||||
List<FilePartInfo> getFileParts(String uploadId);
|
||||
|
||||
/**
|
||||
* 删除所有分片信息
|
||||
*
|
||||
* @param uploadId 上传ID
|
||||
*/
|
||||
void deleteFileParts(String uploadId);
|
||||
|
||||
/**
|
||||
* 检查分片是否存在
|
||||
*
|
||||
* @param uploadId 上传ID
|
||||
* @param partNumber 分片编号
|
||||
* @return 是否存在
|
||||
*/
|
||||
boolean existsFilePart(String uploadId, int partNumber);
|
||||
|
||||
/**
|
||||
* 清理过期的缓存数据
|
||||
*/
|
||||
void cleanupExpiredData();
|
||||
}
|
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.dao.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import top.continew.admin.system.constant.MultipartUploadConstants;
|
||||
import top.continew.admin.system.dao.MultipartUploadDao;
|
||||
import top.continew.admin.system.model.resp.file.FilePartInfo;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
|
||||
import top.continew.starter.cache.redisson.util.RedisUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Redis分片上传缓存实现
|
||||
* <p>
|
||||
* 核心功能:
|
||||
* 1. MD5到uploadId的映射管理
|
||||
* 2. 分片信息缓存
|
||||
* 3. 上传状态缓存
|
||||
* </p>
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2025/7/30 17:40
|
||||
*/
|
||||
@Slf4j
|
||||
@Repository
|
||||
public class RedisMultipartUploadDaoDaoImpl implements MultipartUploadDao {
|
||||
|
||||
@Override
|
||||
public String getUploadIdByMd5(String md5) {
|
||||
String md5Key = MultipartUploadConstants.MD5_TO_UPLOAD_ID_PREFIX + md5;
|
||||
try {
|
||||
return RedisUtils.hGet(md5Key, "uploadId");
|
||||
} catch (Exception e) {
|
||||
log.error("根据MD5获取uploadId失败: md5={}", md5, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMd5Mapping(String md5, String uploadId) {
|
||||
String md5Key = MultipartUploadConstants.MD5_TO_UPLOAD_ID_PREFIX + md5;
|
||||
try {
|
||||
RedisUtils.hSet(md5Key, "uploadId", uploadId);
|
||||
RedisUtils.expire(md5Key, Duration.ofHours(MultipartUploadConstants.DEFAULT_EXPIRE_HOURS));
|
||||
log.debug("缓存MD5映射: md5={}, uploadId={}", md5, uploadId);
|
||||
} catch (Exception e) {
|
||||
log.error("缓存MD5映射失败: md5={}, uploadId={}", md5, uploadId, e);
|
||||
throw new RuntimeException("缓存MD5映射失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMd5Mapping(String md5) {
|
||||
String md5Key = MultipartUploadConstants.MD5_TO_UPLOAD_ID_PREFIX + md5;
|
||||
try {
|
||||
RedisUtils.delete(md5Key);
|
||||
log.debug("删除MD5映射: md5={}", md5);
|
||||
} catch (Exception e) {
|
||||
log.error("删除MD5映射失败: md5={}", md5, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getMd5Mapping(String uploadId) {
|
||||
List<Object> list = RedisUtils.getList(MultipartUploadConstants.MD5_TO_UPLOAD_ID_PREFIX);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMultipartUpload(String uploadId, MultipartUploadInitResp initResp, Map<String, String> metadata) {
|
||||
String key = MultipartUploadConstants.MULTIPART_UPLOAD_PREFIX + uploadId;
|
||||
String metadataKey = MultipartUploadConstants.MULTIPART_METADATA_PREFIX + uploadId;
|
||||
|
||||
try {
|
||||
// 缓存初始化信息
|
||||
RedisUtils.set(key, JSONUtil.toJsonStr(initResp), Duration
|
||||
.ofHours(MultipartUploadConstants.DEFAULT_EXPIRE_HOURS));
|
||||
|
||||
// 缓存元数据
|
||||
if (metadata != null && !metadata.isEmpty()) {
|
||||
for (Map.Entry<String, String> entry : metadata.entrySet()) {
|
||||
RedisUtils.hSet(metadataKey, entry.getKey(), entry.getValue());
|
||||
}
|
||||
RedisUtils.expire(metadataKey, Duration.ofHours(MultipartUploadConstants.DEFAULT_EXPIRE_HOURS));
|
||||
}
|
||||
|
||||
log.debug("缓存分片上传信息: uploadId={}", uploadId);
|
||||
} catch (Exception e) {
|
||||
log.error("缓存分片上传信息失败: uploadId={}", uploadId, e);
|
||||
throw new RuntimeException("缓存分片上传信息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultipartUploadInitResp getMultipartUpload(String uploadId) {
|
||||
String key = MultipartUploadConstants.MULTIPART_UPLOAD_PREFIX + uploadId;
|
||||
try {
|
||||
Object value = RedisUtils.get(key);
|
||||
if (value != null) {
|
||||
return JSONUtil.toBean(value.toString(), MultipartUploadInitResp.class);
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
log.error("获取分片上传信息失败: uploadId={}", uploadId, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMultipartUpload(String uploadId) {
|
||||
try {
|
||||
String key = MultipartUploadConstants.MULTIPART_UPLOAD_PREFIX + uploadId;
|
||||
String metadataKey = MultipartUploadConstants.MULTIPART_METADATA_PREFIX + uploadId;
|
||||
String expireKey = MultipartUploadConstants.MULTIPART_EXPIRE_PREFIX + uploadId;
|
||||
|
||||
// 先获取MD5信息,再删除数据
|
||||
MultipartUploadInitResp initResp = getMultipartUpload(uploadId);
|
||||
String fileMd5 = initResp.getFileMd5();
|
||||
if (StrUtil.isNotBlank(fileMd5)) {
|
||||
deleteMd5Mapping(fileMd5);
|
||||
}
|
||||
|
||||
// 删除分片上传相关数据
|
||||
RedisUtils.delete(key);
|
||||
RedisUtils.delete(metadataKey);
|
||||
RedisUtils.delete(expireKey);
|
||||
|
||||
log.debug("删除分片上传信息: uploadId={}", uploadId);
|
||||
} catch (Exception e) {
|
||||
log.error("删除分片上传信息失败: uploadId={}", uploadId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteMultipartUploadAll(String uploadId) {
|
||||
this.deleteMultipartUpload(uploadId);
|
||||
this.deleteFileParts(uploadId);
|
||||
// this.deleteMd5Mapping();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFilePart(String uploadId, FilePartInfo partInfo) {
|
||||
String key = MultipartUploadConstants.MULTIPART_PARTS_PREFIX + uploadId;
|
||||
String partKey = partInfo.getPartNumber().toString();
|
||||
|
||||
try {
|
||||
RedisUtils.hSet(key, partKey, JSONUtil.toJsonStr(partInfo));
|
||||
RedisUtils.expire(key, Duration.ofHours(MultipartUploadConstants.DEFAULT_EXPIRE_HOURS));
|
||||
log.debug("缓存分片信息: uploadId={}, partNumber={}", uploadId, partKey);
|
||||
} catch (Exception e) {
|
||||
log.error("缓存分片信息失败: uploadId={}, partNumber={}", uploadId, partKey, e);
|
||||
throw new RuntimeException("缓存分片信息失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FilePartInfo> getFileParts(String uploadId) {
|
||||
String key = MultipartUploadConstants.MULTIPART_PARTS_PREFIX + uploadId;
|
||||
|
||||
try {
|
||||
Map<String, Object> entries = RedisUtils.hGetAll(key);
|
||||
if (entries.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
return entries.values()
|
||||
.stream()
|
||||
.map(value -> JSONUtil.toBean(value.toString(), FilePartInfo.class))
|
||||
.sorted(Comparator.comparing(FilePartInfo::getPartNumber))
|
||||
.collect(Collectors.toList());
|
||||
} catch (Exception e) {
|
||||
log.error("获取分片列表失败: uploadId={}", uploadId, e);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteFileParts(String uploadId) {
|
||||
String key = MultipartUploadConstants.MULTIPART_PARTS_PREFIX + uploadId;
|
||||
|
||||
try {
|
||||
RedisUtils.delete(key);
|
||||
log.debug("删除所有分片信息: uploadId={}", uploadId);
|
||||
} catch (Exception e) {
|
||||
log.error("删除所有分片信息失败: uploadId={}", uploadId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsFilePart(String uploadId, int partNumber) {
|
||||
String key = MultipartUploadConstants.MULTIPART_PARTS_PREFIX + uploadId;
|
||||
String partKey = String.valueOf(partNumber);
|
||||
return RedisUtils.hExists(key, partKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanupExpiredData() {
|
||||
try {
|
||||
// 获取所有分片上传的过期时间
|
||||
Collection<String> keys = RedisUtils.keys(MultipartUploadConstants.MULTIPART_EXPIRE_PREFIX + "*");
|
||||
if (keys.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
List<String> expiredUploadIds = new ArrayList<>();
|
||||
|
||||
for (String key : keys) {
|
||||
String uploadId = key.substring(MultipartUploadConstants.MULTIPART_EXPIRE_PREFIX.length());
|
||||
Object value = RedisUtils.get(key);
|
||||
|
||||
if (value != null) {
|
||||
try {
|
||||
LocalDateTime expireTime = LocalDateTime.parse(value
|
||||
.toString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME);
|
||||
if (now.isAfter(expireTime)) {
|
||||
expiredUploadIds.add(uploadId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析过期时间失败: uploadId={}, value={}", uploadId, value);
|
||||
expiredUploadIds.add(uploadId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除过期的数据
|
||||
for (String uploadId : expiredUploadIds) {
|
||||
deleteMultipartUpload(uploadId);
|
||||
deleteFileParts(uploadId);
|
||||
log.info("清理过期数据: uploadId={}", uploadId);
|
||||
}
|
||||
|
||||
log.info("清理过期数据完成: count={}", expiredUploadIds.size());
|
||||
} catch (Exception e) {
|
||||
log.error("清理过期数据失败", e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -65,7 +65,7 @@ public enum StorageTypeEnum implements BaseEnum<Integer> {
|
||||
public void validate(StorageReq req) {
|
||||
ValidationUtils.validate(req, ValidationGroup.Storage.OSS.class);
|
||||
ValidationUtils.throwIf(StrUtil.isNotBlank(req.getDomain()) && !ReUtil
|
||||
.isMatch(RegexConstants.HTTP_DOMAIN_URL, req.getDomain()), "域名格式不正确");
|
||||
.isMatch(RegexConstants.URL_HTTP_NOT_IP, req.getDomain()), "域名格式不正确");
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.factory;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.S3Configuration;
|
||||
import software.amazon.awssdk.utils.SdkAutoCloseable;
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 异步 S3 客户端工厂
|
||||
* <p>支持多 endpoint / 多 accessKey 的动态客户端池</p>
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class S3ClientFactory {
|
||||
|
||||
private final ConcurrentHashMap<String, S3Client> CLIENT_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
public S3Client getClient(StorageDO storage) {
|
||||
String key = storage.getEndpoint() + "|" + storage.getAccessKey();
|
||||
return CLIENT_CACHE.computeIfAbsent(key, k -> {
|
||||
StaticCredentialsProvider auth = StaticCredentialsProvider.create(AwsBasicCredentials.create(storage
|
||||
.getAccessKey(), storage.getSecretKey()));
|
||||
return S3Client.builder()
|
||||
.credentialsProvider(auth)
|
||||
.endpointOverride(URI.create(storage.getEndpoint()))
|
||||
.region(Region.US_EAST_1)
|
||||
.serviceConfiguration(S3Configuration.builder().chunkedEncodingEnabled(false).build())
|
||||
.build();
|
||||
});
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void closeAll() {
|
||||
CLIENT_CACHE.values().forEach(SdkAutoCloseable::close);
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.factory;/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.admin.system.handler.StorageHandler;
|
||||
import top.continew.starter.core.exception.BaseException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 存储处理器工厂
|
||||
* <p>按类型分发 StorageHandler</p>
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2025/07/24 13:35
|
||||
*/
|
||||
@Component
|
||||
public class StorageHandlerFactory {
|
||||
private final Map<StorageTypeEnum, StorageHandler> HANDLER_MAP = new ConcurrentHashMap<>();
|
||||
|
||||
@Autowired
|
||||
public StorageHandlerFactory(List<StorageHandler> handlers) {
|
||||
for (StorageHandler handler : handlers) {
|
||||
HANDLER_MAP.put(handler.getType(), handler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类型的存储处理器
|
||||
*
|
||||
* @param type 存储类型
|
||||
* @return StorageHandler
|
||||
*/
|
||||
public StorageHandler createHandler(StorageTypeEnum type) {
|
||||
return Optional.ofNullable(HANDLER_MAP.get(type))
|
||||
.orElseThrow(() -> new BaseException(StrUtil.format("不存在此类型存储处理器:{}: ", type)));
|
||||
}
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.handler;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
import top.continew.admin.system.model.req.MultipartUploadInitReq;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadResp;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 存储类型处理器
|
||||
* <p>
|
||||
* 专注于文件操作,不包含业务逻辑
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2025/7/30 17:15
|
||||
*/
|
||||
public interface StorageHandler {
|
||||
|
||||
MultipartUploadInitResp initMultipartUpload(StorageDO storageDO, MultipartUploadInitReq req);
|
||||
|
||||
/**
|
||||
* 分片上传
|
||||
*
|
||||
* @param storageDO 存储实体
|
||||
* @param path 存储路径
|
||||
* @param uploadId 文件名
|
||||
* @param file 文件对象
|
||||
* @return {@link MultipartUploadResp} 分片上传结果
|
||||
*/
|
||||
MultipartUploadResp uploadPart(StorageDO storageDO,
|
||||
String path,
|
||||
String uploadId,
|
||||
Integer partNumber,
|
||||
MultipartFile file);
|
||||
|
||||
/**
|
||||
* 合并分片
|
||||
*
|
||||
* @param storageDO 存储实体
|
||||
* @param uploadId 上传Id
|
||||
*/
|
||||
void completeMultipartUpload(StorageDO storageDO,
|
||||
List<MultipartUploadResp> parts,
|
||||
String path,
|
||||
String uploadId,
|
||||
boolean needVerify);
|
||||
|
||||
/**
|
||||
* 清楚分片
|
||||
*
|
||||
* @param storageDO 存储实体
|
||||
* @param uploadId 上传Id
|
||||
*/
|
||||
void cleanPart(StorageDO storageDO, String uploadId);
|
||||
|
||||
/**
|
||||
* 获取存储类型
|
||||
*
|
||||
* @return 存储类型
|
||||
*/
|
||||
StorageTypeEnum getType();
|
||||
}
|
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.handler.impl;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import top.continew.admin.system.constant.MultipartUploadConstants;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.admin.system.handler.StorageHandler;
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
import top.continew.admin.system.model.req.MultipartUploadInitReq;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadResp;
|
||||
import top.continew.admin.system.service.FileService;
|
||||
import top.continew.starter.core.exception.BaseException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 本地存储处理器
|
||||
* <p>实现分片上传、合并、取消等操作。</p>
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2023/7/30 22:58
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LocalStorageHandler implements StorageHandler {
|
||||
|
||||
private final FileService fileService;
|
||||
|
||||
@Override
|
||||
public MultipartUploadInitResp initMultipartUpload(StorageDO storageDO, MultipartUploadInitReq req) {
|
||||
String uploadId = UUID.randomUUID().toString();
|
||||
String bucket = storageDO.getBucketName(); // 本地存储中,bucket是存储根路径
|
||||
String parentPath = req.getParentPath();
|
||||
String fileName = req.getFileName();
|
||||
StrUtil.blankToDefault(parentPath, StrUtil.SLASH);
|
||||
String relativePath = StrUtil.endWith(parentPath, StrUtil.SLASH)
|
||||
? parentPath + fileName
|
||||
: parentPath + StrUtil.SLASH + fileName;
|
||||
try {
|
||||
// 创建临时目录用于存储分片
|
||||
String tempDirPath = buildTempDirPath(bucket, uploadId);
|
||||
FileUtil.mkdir(tempDirPath);
|
||||
fileService.createParentDir(parentPath, storageDO);
|
||||
// 构建返回结果
|
||||
MultipartUploadInitResp result = new MultipartUploadInitResp();
|
||||
result.setBucket(bucket);
|
||||
result.setFileId(UUID.randomUUID().toString());
|
||||
result.setUploadId(uploadId);
|
||||
result.setPlatform(storageDO.getCode());
|
||||
result.setFileName(fileName);
|
||||
result.setFileMd5(req.getFileMd5());
|
||||
result.setFileSize(req.getFileSize());
|
||||
result.setExtension(FileUtil.extName(fileName));
|
||||
result.setContentType(req.getContentType());
|
||||
result.setPath(relativePath);
|
||||
result.setParentPath(parentPath);
|
||||
result.setPartSize(MultipartUploadConstants.MULTIPART_UPLOAD_PART_SIZE);
|
||||
log.info("本地存储初始化分片上传成功: uploadId={}, path={}", uploadId, parentPath);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("本地存储初始化分片上传失败: {}", e.getMessage(), e);
|
||||
throw new BaseException("本地存储初始化分片上传失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultipartUploadResp uploadPart(StorageDO storageDO,
|
||||
String path,
|
||||
String uploadId,
|
||||
Integer partNumber,
|
||||
MultipartFile file) {
|
||||
try {
|
||||
long size = file.getSize();
|
||||
String bucket = storageDO.getBucketName();
|
||||
// 获取临时目录路径
|
||||
String tempDirPath = buildTempDirPath(bucket, uploadId);
|
||||
|
||||
// 确保临时目录存在
|
||||
File tempDir = new File(tempDirPath);
|
||||
if (!tempDir.exists()) {
|
||||
FileUtil.mkdir(tempDirPath);
|
||||
}
|
||||
|
||||
// 保存分片文件
|
||||
String partFilePath = tempDirPath + File.separator + String.format("part_%s", partNumber);
|
||||
File partFile = new File(partFilePath);
|
||||
file.transferTo(partFile);
|
||||
|
||||
// 计算ETag (使用MD5)
|
||||
String etag = DigestUtil.md5Hex(partFile);
|
||||
|
||||
// 构建返回结果
|
||||
MultipartUploadResp result = new MultipartUploadResp();
|
||||
result.setPartNumber(partNumber);
|
||||
result.setPartETag(etag);
|
||||
result.setPartSize(size);
|
||||
result.setSuccess(true);
|
||||
|
||||
log.info("本地存储分片上传成功: uploadId={}, partNumber={}, etag={}", uploadId, partNumber, etag);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("本地存储分片上传失败: uploadId={}, partNumber={}, error={}", uploadId, partNumber, e.getMessage(), e);
|
||||
|
||||
MultipartUploadResp result = new MultipartUploadResp();
|
||||
result.setPartNumber(partNumber);
|
||||
result.setSuccess(false);
|
||||
result.setErrorMessage(e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeMultipartUpload(StorageDO storageDO,
|
||||
List<MultipartUploadResp> parts,
|
||||
String path,
|
||||
String uploadId,
|
||||
boolean needVerify) {
|
||||
String bucket = storageDO.getBucketName(); // 本地存储中,bucket是存储根路径
|
||||
String tempDirPath = buildTempDirPath(bucket, uploadId);
|
||||
|
||||
try {
|
||||
// 本地存储不需要验证,直接使用传入的分片信息
|
||||
Path targetPath = Paths.get(bucket, path);
|
||||
Files.createDirectories(targetPath.getParent());
|
||||
|
||||
// 合并分片
|
||||
try (OutputStream out = Files
|
||||
.newOutputStream(targetPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
|
||||
// 按分片编号排序
|
||||
List<MultipartUploadResp> sortedParts = parts.stream()
|
||||
.filter(MultipartUploadResp::isSuccess)
|
||||
.sorted(Comparator.comparingInt(MultipartUploadResp::getPartNumber))
|
||||
.toList();
|
||||
|
||||
// 逐个读取并写入
|
||||
for (MultipartUploadResp part : sortedParts) {
|
||||
Path partPath = Paths.get(tempDirPath, String.format("part_%s", part.getPartNumber()));
|
||||
|
||||
if (!Files.exists(partPath)) {
|
||||
throw new BaseException("分片文件不存在: partNumber=" + part.getPartNumber());
|
||||
}
|
||||
|
||||
Files.copy(partPath, out);
|
||||
}
|
||||
}
|
||||
// 清理临时文件
|
||||
cleanupTempFiles(tempDirPath);
|
||||
|
||||
log.info("本地存储分片合并成功: uploadId={}, targetPath={}", uploadId, targetPath);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("本地存储分片合并失败: uploadId={}, path={}, error={}", uploadId, path, e.getMessage(), e);
|
||||
throw new BaseException("完成分片上传失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanPart(StorageDO storageDO, String uploadId) {
|
||||
try {
|
||||
String bucket = storageDO.getBucketName();
|
||||
// 获取临时目录路径
|
||||
String tempDirPath = buildTempDirPath(bucket, uploadId);
|
||||
|
||||
// 清理临时文件
|
||||
cleanupTempFiles(tempDirPath);
|
||||
|
||||
log.info("本地存储分片清理成功: uploadId={}", uploadId);
|
||||
} catch (Exception e) {
|
||||
log.error("本地存储分片清理失败: uploadId={}, error={}", uploadId, e.getMessage(), e);
|
||||
throw new BaseException("本地存储分片清理失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageTypeEnum getType() {
|
||||
return StorageTypeEnum.LOCAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建临时目录路径
|
||||
*
|
||||
* @param bucket 存储桶(本地存储根路径)
|
||||
* @param uploadId 上传ID
|
||||
* @return 临时目录路径
|
||||
*/
|
||||
private String buildTempDirPath(String bucket, String uploadId) {
|
||||
return StrUtil
|
||||
.appendIfMissing(bucket, File.separator) + MultipartUploadConstants.TEMP_DIR_NAME + File.separator + uploadId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建目标文件路径
|
||||
*
|
||||
* @param bucket 存储桶(本地存储根路径)
|
||||
* @param path 文件路径
|
||||
* @return 目标文件路径
|
||||
*/
|
||||
private String buildTargetDirPath(String bucket, String path) {
|
||||
return StrUtil.appendIfMissing(bucket, File.separator) + path;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时文件
|
||||
*
|
||||
* @param tempDirPath 临时目录路径
|
||||
*/
|
||||
private void cleanupTempFiles(String tempDirPath) {
|
||||
try {
|
||||
FileUtil.del(tempDirPath);
|
||||
} catch (Exception e) {
|
||||
log.warn("清理临时文件失败: {}, {}", tempDirPath, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,326 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.handler.impl;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.*;
|
||||
import top.continew.admin.system.constant.MultipartUploadConstants;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.admin.system.factory.S3ClientFactory;
|
||||
import top.continew.admin.system.handler.StorageHandler;
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
import top.continew.admin.system.model.req.MultipartUploadInitReq;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadResp;
|
||||
import top.continew.admin.system.service.FileService;
|
||||
import top.continew.starter.core.exception.BaseException;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* S3存储处理器
|
||||
* <p>使用AWS SDK 2.x版本API。实现分片上传、合并、取消等操作。</p>
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2025/07/30 20:10
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class S3StorageHandler implements StorageHandler {
|
||||
|
||||
private final S3ClientFactory s3ClientFactory;
|
||||
|
||||
private final FileService fileService;
|
||||
|
||||
@Override
|
||||
public MultipartUploadInitResp initMultipartUpload(StorageDO storageDO, MultipartUploadInitReq req) {
|
||||
String bucket = storageDO.getBucketName();
|
||||
String parentPath = req.getParentPath();
|
||||
String fileName = req.getFileName();
|
||||
String contentType = req.getContentType();
|
||||
StrUtil.blankToDefault(parentPath, StrUtil.SLASH);
|
||||
String relativePath = StrUtil.endWith(parentPath, StrUtil.SLASH)
|
||||
? parentPath + fileName
|
||||
: parentPath + StrUtil.SLASH + fileName;
|
||||
|
||||
fileService.createParentDir(parentPath, storageDO);
|
||||
try {
|
||||
// 构建请求
|
||||
CreateMultipartUploadRequest.Builder requestBuilder = CreateMultipartUploadRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(buildS3Key(relativePath))
|
||||
.contentType(contentType);
|
||||
|
||||
// 添加元数据 暂时注释掉 mataData传递中文会导致签名校验不通过
|
||||
// if (metaData != null && !metaData.isEmpty()) {
|
||||
// requestBuilder.metadata(metaData);
|
||||
// }
|
||||
|
||||
S3Client s3Client = s3ClientFactory.getClient(storageDO);
|
||||
log.info("S3初始化分片上传: bucket={}, key={}, contentType={}", bucket, buildS3Key(relativePath), contentType);
|
||||
|
||||
// 执行请求
|
||||
CreateMultipartUploadResponse response = s3Client.createMultipartUpload(requestBuilder.build());
|
||||
String uploadId = response.uploadId();
|
||||
// 构建返回结果
|
||||
MultipartUploadInitResp result = new MultipartUploadInitResp();
|
||||
result.setBucket(bucket);
|
||||
result.setFileId(UUID.randomUUID().toString());
|
||||
|
||||
result.setUploadId(uploadId);
|
||||
result.setPlatform(storageDO.getCode());
|
||||
result.setFileName(fileName);
|
||||
result.setFileMd5(req.getFileMd5());
|
||||
result.setFileSize(req.getFileSize());
|
||||
result.setExtension(FileUtil.extName(fileName));
|
||||
result.setContentType(req.getContentType());
|
||||
result.setPath(relativePath);
|
||||
result.setParentPath(parentPath);
|
||||
result.setPartSize(MultipartUploadConstants.MULTIPART_UPLOAD_PART_SIZE);
|
||||
log.info("S3初始化分片上传成功: uploadId={}, path={}", uploadId, relativePath);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new BaseException("S3初始化分片上传失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultipartUploadResp uploadPart(StorageDO storageDO,
|
||||
String path,
|
||||
String uploadId,
|
||||
Integer partNumber,
|
||||
MultipartFile file) {
|
||||
try {
|
||||
String bucket = storageDO.getBucketName();
|
||||
// 读取数据到内存(注意:实际使用时可能需要优化大文件处理)
|
||||
byte[] bytes = file.getBytes();
|
||||
|
||||
// 构建请求
|
||||
UploadPartRequest request = UploadPartRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(buildS3Key(path))
|
||||
.uploadId(uploadId)
|
||||
.partNumber(partNumber)
|
||||
.contentLength((long)bytes.length)
|
||||
.build();
|
||||
|
||||
// 执行上传
|
||||
S3Client s3Client = s3ClientFactory.getClient(storageDO);
|
||||
UploadPartResponse response = s3Client.uploadPart(request, RequestBody.fromBytes(bytes));
|
||||
// 构建返回结果
|
||||
MultipartUploadResp result = new MultipartUploadResp();
|
||||
result.setPartNumber(partNumber);
|
||||
result.setPartETag(response.eTag());
|
||||
result.setSuccess(true);
|
||||
log.info("S3上传分片成功: partNumber={} for key={} with uploadId={}", partNumber, path, uploadId);
|
||||
log.info("上传分片ETag: {}", response.eTag());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
MultipartUploadResp result = new MultipartUploadResp();
|
||||
result.setPartNumber(partNumber);
|
||||
result.setSuccess(false);
|
||||
result.setErrorMessage(e.getMessage());
|
||||
log.error("S3上传分片失败: partNumber={} for key={} with uploadId={} errorMessage={}", partNumber, path, uploadId, e
|
||||
.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeMultipartUpload(StorageDO storageDO,
|
||||
List<MultipartUploadResp> parts,
|
||||
String path,
|
||||
String uploadId,
|
||||
boolean needVerify) {
|
||||
if (path == null) {
|
||||
throw new BaseException("无效的uploadId: " + uploadId);
|
||||
}
|
||||
String bucket = storageDO.getBucketName();
|
||||
S3Client s3Client = s3ClientFactory.getClient(storageDO);
|
||||
// 如果需要验证,比较本地记录和S3的分片信息
|
||||
if (needVerify) {
|
||||
List<MultipartUploadResp> s3Parts = listParts(bucket, path, uploadId, s3Client);
|
||||
validateParts(parts, s3Parts);
|
||||
}
|
||||
// 构建已完成的分片列表
|
||||
List<CompletedPart> completedParts = parts.stream()
|
||||
.filter(MultipartUploadResp::isSuccess)
|
||||
.map(part -> CompletedPart.builder().partNumber(part.getPartNumber()).eTag(part.getPartETag()).build())
|
||||
.sorted(Comparator.comparingInt(CompletedPart::partNumber))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 构建请求
|
||||
CompleteMultipartUploadRequest request = CompleteMultipartUploadRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(buildS3Key(path))
|
||||
.uploadId(uploadId)
|
||||
.multipartUpload(CompletedMultipartUpload.builder().parts(completedParts).build())
|
||||
.build();
|
||||
|
||||
// 完成上传
|
||||
s3Client.completeMultipartUpload(request);
|
||||
log.info("S3完成分片上传: key={}, uploadId={}, parts={}", buildS3Key(path), uploadId, completedParts.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanPart(StorageDO storageDO, String uploadId) {
|
||||
try {
|
||||
String bucket = storageDO.getBucketName();
|
||||
S3Client s3Client = s3ClientFactory.getClient(storageDO);
|
||||
|
||||
// 列出所有未完成的分片上传
|
||||
ListMultipartUploadsRequest listRequest = ListMultipartUploadsRequest.builder().bucket(bucket).build();
|
||||
|
||||
ListMultipartUploadsResponse listResponse = s3Client.listMultipartUploads(listRequest);
|
||||
|
||||
// 查找匹配的上传任务
|
||||
Optional<MultipartUpload> targetUpload = listResponse.uploads()
|
||||
.stream()
|
||||
.filter(upload -> upload.uploadId().equals(uploadId))
|
||||
.findFirst();
|
||||
|
||||
if (targetUpload.isPresent()) {
|
||||
MultipartUpload upload = targetUpload.get();
|
||||
|
||||
// 取消分片上传
|
||||
AbortMultipartUploadRequest abortRequest = AbortMultipartUploadRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(upload.key())
|
||||
.uploadId(uploadId)
|
||||
.build();
|
||||
|
||||
s3Client.abortMultipartUpload(abortRequest);
|
||||
log.info("S3清理分片上传成功: bucket={}, key={}, uploadId={}", bucket, upload.key(), uploadId);
|
||||
} else {
|
||||
log.warn("S3未找到对应的分片上传任务: uploadId={}", uploadId);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("S3清理分片上传失败: uploadId={}, error={}", uploadId, e.getMessage(), e);
|
||||
throw new BaseException("S3清理分片上传失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StorageTypeEnum getType() {
|
||||
return StorageTypeEnum.OSS;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出已上传的分片
|
||||
*/
|
||||
public List<MultipartUploadResp> listParts(String bucket, String path, String uploadId, S3Client s3Client) {
|
||||
try {
|
||||
// 构建请求
|
||||
ListPartsRequest request = ListPartsRequest.builder()
|
||||
.bucket(bucket)
|
||||
.key(buildS3Key(path))
|
||||
.uploadId(uploadId)
|
||||
.build();
|
||||
|
||||
// 获取分片列表
|
||||
ListPartsResponse response = s3Client.listParts(request);
|
||||
|
||||
// 转换结果
|
||||
return response.parts().stream().map(part -> {
|
||||
MultipartUploadResp result = new MultipartUploadResp();
|
||||
result.setPartNumber(part.partNumber());
|
||||
result.setPartETag(part.eTag());
|
||||
result.setSuccess(true);
|
||||
return result;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new BaseException("S3列出分片失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分片一致性
|
||||
*
|
||||
* @param recordParts 记录部件
|
||||
* @param s3Parts s3零件
|
||||
*/
|
||||
private void validateParts(List<MultipartUploadResp> recordParts, List<MultipartUploadResp> s3Parts) {
|
||||
Map<Integer, String> recordMap = recordParts.stream()
|
||||
.collect(Collectors.toMap(MultipartUploadResp::getPartNumber, MultipartUploadResp::getPartETag));
|
||||
|
||||
Map<Integer, String> s3Map = s3Parts.stream()
|
||||
.collect(Collectors.toMap(MultipartUploadResp::getPartNumber, MultipartUploadResp::getPartETag));
|
||||
|
||||
// 检查分片数量
|
||||
if (recordMap.size() != s3Map.size()) {
|
||||
throw new BaseException(String.format("分片数量不一致: 本地记录=%d, S3=%d", recordMap.size(), s3Map.size()));
|
||||
}
|
||||
|
||||
// 检查每个分片
|
||||
List<Integer> missingParts = new ArrayList<>();
|
||||
List<Integer> mismatchParts = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<Integer, String> entry : recordMap.entrySet()) {
|
||||
Integer partNumber = entry.getKey();
|
||||
String recordETag = entry.getValue();
|
||||
String s3ETag = s3Map.get(partNumber);
|
||||
|
||||
if (s3ETag == null) {
|
||||
missingParts.add(partNumber);
|
||||
} else if (!recordETag.equals(s3ETag)) {
|
||||
mismatchParts.add(partNumber);
|
||||
}
|
||||
}
|
||||
|
||||
if (!missingParts.isEmpty()) {
|
||||
throw new BaseException("S3缺失分片: " + missingParts);
|
||||
}
|
||||
|
||||
if (!mismatchParts.isEmpty()) {
|
||||
throw new BaseException("分片ETag不匹配: " + mismatchParts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化 S3 对象 key,去掉前导斜杠,合并多余斜杠。
|
||||
*
|
||||
* @param rawKey 你传入的完整路径,比如 "/folder//子目录//文件名.png"
|
||||
* @return 规范化后的 key,比如 "folder/子目录/文件名.png"
|
||||
*/
|
||||
public static String buildS3Key(String rawKey) {
|
||||
if (rawKey == null || rawKey.isEmpty()) {
|
||||
throw new IllegalArgumentException("key 不能为空");
|
||||
}
|
||||
// 去掉前导斜杠
|
||||
while (rawKey.startsWith("/")) {
|
||||
rawKey = rawKey.substring(1);
|
||||
}
|
||||
// 替换连续多个斜杠为一个斜杠
|
||||
rawKey = rawKey.replaceAll("/+", "/");
|
||||
return rawKey;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分片初始化请求参数
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2025/7/30 16:38
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分片初始化请求参数")
|
||||
public class MultipartUploadInitReq implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
@Schema(description = "文件名", example = "example.zip")
|
||||
@NotBlank(message = "文件名不能为空")
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@Schema(description = "文件大小", example = "1048576")
|
||||
@NotNull(message = "文件大小不能为空")
|
||||
@Min(value = 1, message = "文件大小必须大于0")
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 文件MD5值
|
||||
*/
|
||||
@Schema(description = "文件MD5值", example = "5d41402abc4b2a76b9719d911017c592")
|
||||
@NotBlank(message = "文件MD5值不能为空")
|
||||
private String fileMd5;
|
||||
|
||||
/**
|
||||
* 文件MIME类型
|
||||
*/
|
||||
@Schema(description = "文件MIME类型", example = "application/zip")
|
||||
private String contentType;
|
||||
|
||||
/**
|
||||
* 存储路径
|
||||
*/
|
||||
@Schema(description = "存储父路径", example = "/upload/files/")
|
||||
private String parentPath;
|
||||
|
||||
/**
|
||||
* 文件元信息
|
||||
*/
|
||||
@Schema(description = "文件元信息")
|
||||
private Map<String, String> metaData;
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.model.req;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分片上传请求参数
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2025/7/30 16:40
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分片上传请求参数")
|
||||
public class MultipartUploadReq {
|
||||
|
||||
/**
|
||||
* 上传ID
|
||||
*/
|
||||
@Schema(description = "上传ID")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 分片序号
|
||||
*/
|
||||
@Schema(description = "分片序号")
|
||||
private Integer partNumber;
|
||||
|
||||
/**
|
||||
* 分片ETag
|
||||
*/
|
||||
@Schema(description = "分片ETag")
|
||||
private String eTag;
|
||||
|
||||
/**
|
||||
* 存储编码
|
||||
*/
|
||||
@Schema(description = "存储编码")
|
||||
private String storageCode;
|
||||
}
|
@@ -31,7 +31,7 @@ import java.util.List;
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "参数重置请求参数")
|
||||
public class OptionResetValueReq implements Serializable {
|
||||
public class OptionValueResetReq implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
@@ -32,7 +32,7 @@ import java.util.List;
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "角色功能权限修改请求参数")
|
||||
public class RoleUpdatePermissionReq implements Serializable {
|
||||
public class RolePermissionUpdateReq implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
@@ -47,7 +47,7 @@ public class StorageReq implements Serializable {
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
@Schema(description = "名称", example = "存储1")
|
||||
@Schema(description = "名称", example = "S3对象存储")
|
||||
@NotBlank(message = "名称不能为空")
|
||||
@Length(max = 100, message = "名称长度不能超过 {max} 个字符")
|
||||
private String name;
|
||||
@@ -55,7 +55,7 @@ public class StorageReq implements Serializable {
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
@Schema(description = "编码", example = "local")
|
||||
@Schema(description = "编码", example = "s3_aliyun")
|
||||
@NotBlank(message = "编码不能为空")
|
||||
@Pattern(regexp = RegexConstants.GENERAL_CODE, message = "编码长度为 2-30 个字符,支持大小写字母、数字、下划线,以字母开头")
|
||||
private String code;
|
||||
@@ -70,7 +70,7 @@ public class StorageReq implements Serializable {
|
||||
/**
|
||||
* Access Key
|
||||
*/
|
||||
@Schema(description = "Access Key", example = "")
|
||||
@Schema(description = "Access Key", example = "LBAI4Fp4dXYcZamU5EXTBdTa")
|
||||
@Length(max = 255, message = "Access Key长度不能超过 {max} 个字符")
|
||||
@NotBlank(message = "Access Key不能为空", groups = ValidationGroup.Storage.OSS.class)
|
||||
private String accessKey;
|
||||
@@ -78,22 +78,22 @@ public class StorageReq implements Serializable {
|
||||
/**
|
||||
* Secret Key
|
||||
*/
|
||||
@Schema(description = "Secret Key", example = "")
|
||||
@NotBlank(message = "Secret Key不能为空", groups = ValidationGroup.Storage.OSS.class)
|
||||
@Schema(description = "Secret Key", example = "RSA 公钥加密的 Secret Key")
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* Endpoint
|
||||
*/
|
||||
@Schema(description = "Endpoint", example = "")
|
||||
@Schema(description = "Endpoint", example = "http://oss-cn-shanghai.aliyuncs.com")
|
||||
@Length(max = 255, message = "Endpoint长度不能超过 {max} 个字符")
|
||||
@NotBlank(message = "Endpoint不能为空", groups = ValidationGroup.Storage.OSS.class)
|
||||
@Pattern(regexp = RegexConstants.URL_HTTP, message = "Endpoint格式不正确", groups = ValidationGroup.Storage.OSS.class)
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* Bucket/存储路径
|
||||
*/
|
||||
@Schema(description = "Bucket/存储路径", example = "C:/continew-admin/data/file/")
|
||||
@Schema(description = "Bucket/存储路径", example = "continew-admin")
|
||||
@Length(max = 255, message = "Bucket长度不能超过 {max} 个字符", groups = ValidationGroup.Storage.OSS.class)
|
||||
@Length(max = 255, message = "存储路径长度不能超过 {max} 个字符", groups = ValidationGroup.Storage.Local.class)
|
||||
@NotBlank(message = "Bucket不能为空", groups = ValidationGroup.Storage.OSS.class)
|
||||
@@ -103,7 +103,7 @@ public class StorageReq implements Serializable {
|
||||
/**
|
||||
* 域名/访问路径
|
||||
*/
|
||||
@Schema(description = "域名/访问路径", example = "http://localhost:8000/file")
|
||||
@Schema(description = "域名/访问路径", example = "https://continew-admin.file.continew.top/")
|
||||
@Length(max = 255, message = "域名长度不能超过 {max} 个字符", groups = ValidationGroup.Storage.OSS.class)
|
||||
@Length(max = 255, message = "访问路径长度不能超过 {max} 个字符", groups = ValidationGroup.Storage.Local.class)
|
||||
@NotBlank(message = "访问路径不能为空", groups = ValidationGroup.Storage.Local.class)
|
||||
|
@@ -55,9 +55,9 @@ public class UserEmailUpdateReq implements Serializable {
|
||||
private String captcha;
|
||||
|
||||
/**
|
||||
* 当前密码(加密)
|
||||
* 当前密码
|
||||
*/
|
||||
@Schema(description = "当前密码(加密)", example = "SYRLSszQGcMv4kP2Yolou9zf28B9GDakR9u91khxmR7V++i5A384kwnNZxqgvT6bjT4zqpIDuMFLWSt92hQJJA==")
|
||||
@Schema(description = "当前密码", example = "RSA 公钥加密的当前密码")
|
||||
@NotBlank(message = "当前密码不能为空")
|
||||
private String oldPassword;
|
||||
}
|
||||
|
@@ -37,9 +37,9 @@ public class UserPasswordResetReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 新密码(加密)
|
||||
* 新密码
|
||||
*/
|
||||
@Schema(description = "新密码(加密)", example = "Gzc78825P5baH190lRuZFb9KJxRt/psN2jiyOMPoc5WRcCvneCwqDm3Q33BZY56EzyyVy7vQu7jQwYTK4j1+5w==")
|
||||
@Schema(description = "新密码", example = "RSA 公钥加密的新密码")
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
private String newPassword;
|
||||
}
|
||||
|
@@ -37,15 +37,15 @@ public class UserPasswordUpdateReq implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 当前密码(加密)
|
||||
* 当前密码
|
||||
*/
|
||||
@Schema(description = "当前密码(加密)", example = "E7c72TH+LDxKTwavjM99W1MdI9Lljh79aPKiv3XB9MXcplhm7qJ1BJCj28yaflbdVbfc366klMtjLIWQGqb0qw==")
|
||||
@Schema(description = "当前密码", example = "RSA 公钥加密的当前密码")
|
||||
private String oldPassword;
|
||||
|
||||
/**
|
||||
* 新密码(加密)
|
||||
* 新密码
|
||||
*/
|
||||
@Schema(description = "新密码(加密)", example = "Gzc78825P5baH190lRuZFb9KJxRt/psN2jiyOMPoc5WRcCvneCwqDm3Q33BZY56EzyyVy7vQu7jQwYTK4j1+5w==")
|
||||
@Schema(description = "新密码", example = "RSA 公钥加密的新密码")
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
private String newPassword;
|
||||
}
|
||||
|
@@ -55,9 +55,9 @@ public class UserPhoneUpdateReq implements Serializable {
|
||||
private String captcha;
|
||||
|
||||
/**
|
||||
* 当前密码(加密)
|
||||
* 当前密码
|
||||
*/
|
||||
@Schema(description = "当前密码(加密)", example = "SYRLSszQGcMv4kP2Yolou9zf28B9GDakR9u91khxmR7V++i5A384kwnNZxqgvT6bjT4zqpIDuMFLWSt92hQJJA==")
|
||||
@Schema(description = "当前密码", example = "RSA 公钥加密的当前密码")
|
||||
@NotBlank(message = "当前密码不能为空")
|
||||
private String oldPassword;
|
||||
}
|
||||
|
@@ -60,9 +60,9 @@ public class UserReq implements Serializable {
|
||||
private String nickname;
|
||||
|
||||
/**
|
||||
* 密码(加密)
|
||||
* 密码
|
||||
*/
|
||||
@Schema(description = "密码(加密)", example = "E7c72TH+LDxKTwavjM99W1MdI9Lljh79aPKiv3XB9MXcplhm7qJ1BJCj28yaflbdVbfc366klMtjLIWQGqb0qw==")
|
||||
@Schema(description = "密码", example = "RSA 公钥加密的密码")
|
||||
@NotBlank(message = "密码不能为空", groups = CrudValidationGroup.Create.class)
|
||||
private String password;
|
||||
|
||||
|
@@ -21,7 +21,6 @@ import lombok.Data;
|
||||
import top.continew.admin.common.base.model.resp.BaseDetailResp;
|
||||
import top.continew.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.starter.security.mask.annotation.JsonMask;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
@@ -68,13 +67,6 @@ public class StorageResp extends BaseDetailResp {
|
||||
@Schema(description = "Access Key", example = "")
|
||||
private String accessKey;
|
||||
|
||||
/**
|
||||
* Secret Key
|
||||
*/
|
||||
@Schema(description = "Secret Key", example = "")
|
||||
@JsonMask(left = 4, right = 3)
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* Endpoint
|
||||
*/
|
||||
|
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.model.resp.file;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 文件分片信息
|
||||
*
|
||||
* @author echo
|
||||
* @since 2.14.0
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "文件分片信息")
|
||||
public class FilePartInfo implements Serializable {
|
||||
/**
|
||||
* 文件ID
|
||||
*/
|
||||
@Schema(description = "文件ID")
|
||||
private String fileId;
|
||||
|
||||
/**
|
||||
* 分片编号(从1开始)
|
||||
*/
|
||||
@Schema(description = "分片编号(从1开始)")
|
||||
private Integer partNumber;
|
||||
|
||||
/**
|
||||
* 分片大小
|
||||
*/
|
||||
@Schema(description = "分片大小")
|
||||
private Long partSize;
|
||||
|
||||
/**
|
||||
* 分片MD5
|
||||
*/
|
||||
@Schema(description = "分片MD5")
|
||||
private String partMd5;
|
||||
|
||||
/**
|
||||
* 分片ETag(S3返回的标识)
|
||||
*/
|
||||
@Schema(description = "分片ETag")
|
||||
private String partETag;
|
||||
|
||||
/**
|
||||
* 上传ID(S3分片上传标识)
|
||||
*/
|
||||
@Schema(description = "上传ID")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 上传时间
|
||||
*/
|
||||
@Schema(description = "上传时间")
|
||||
private LocalDateTime uploadTime;
|
||||
|
||||
/**
|
||||
* 状态:UPLOADING, SUCCESS, FAILED
|
||||
*/
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 存储桶
|
||||
*/
|
||||
@Schema(description = "存储桶")
|
||||
private String bucket;
|
||||
|
||||
/**
|
||||
* 文件路径
|
||||
*/
|
||||
@Schema(description = "文件路径")
|
||||
private String path;
|
||||
|
||||
}
|
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.model.resp.file;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 分片上传初始化结果
|
||||
*
|
||||
* @author echo
|
||||
* @since 2.14.0
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分片初始化响应参数")
|
||||
public class MultipartUploadInitResp implements Serializable {
|
||||
|
||||
/**
|
||||
* 文件ID
|
||||
*/
|
||||
@Schema(description = "文件ID")
|
||||
private String fileId;
|
||||
|
||||
/**
|
||||
* 上传ID(S3返回的uploadId)
|
||||
*/
|
||||
@Schema(description = "上传ID")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 存储桶
|
||||
*/
|
||||
@Schema(description = "存储桶")
|
||||
private String bucket;
|
||||
|
||||
/**
|
||||
* 存储平台
|
||||
*/
|
||||
@Schema(description = "存储平台")
|
||||
private String platform;
|
||||
|
||||
/**
|
||||
* 文件名称
|
||||
*/
|
||||
@Schema(description = "文件名称")
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件MD5
|
||||
*/
|
||||
@Schema(description = "文件MD5")
|
||||
private String fileMd5;
|
||||
|
||||
/**
|
||||
* 文件大小
|
||||
*/
|
||||
@Schema(description = "文件大小")
|
||||
private long fileSize;
|
||||
|
||||
/**
|
||||
* 扩展名
|
||||
*/
|
||||
@Schema(description = "扩展名")
|
||||
private String extension;
|
||||
|
||||
/**
|
||||
* 内容类型
|
||||
*/
|
||||
@Schema(description = "内容类型")
|
||||
private String contentType;
|
||||
|
||||
/**
|
||||
* 文件类型
|
||||
*/
|
||||
@Schema(description = "文件类型")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 文件父路径
|
||||
*/
|
||||
@Schema(description = "文件父路径")
|
||||
private String parentPath;
|
||||
|
||||
/**
|
||||
* 文件路径
|
||||
*/
|
||||
@Schema(description = "文件路径")
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* 分片大小
|
||||
*/
|
||||
@Schema(description = "分片大小")
|
||||
private Long partSize;
|
||||
|
||||
/**
|
||||
* 已上传分片编号集合
|
||||
*/
|
||||
@Schema(description = "已上传分片编号集合")
|
||||
private Set<Integer> uploadedPartNumbers;
|
||||
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.model.resp.file;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 分片上传结果
|
||||
*
|
||||
* @author echo
|
||||
* @since 2.14.0
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "分片上传响应参数")
|
||||
public class MultipartUploadResp implements Serializable {
|
||||
/**
|
||||
* 分片编号
|
||||
*/
|
||||
@Schema(description = "分片编号")
|
||||
private Integer partNumber;
|
||||
|
||||
/**
|
||||
* 分片ETag
|
||||
*/
|
||||
@Schema(description = "分片ETag")
|
||||
private String partETag;
|
||||
|
||||
/**
|
||||
* 分片大小
|
||||
*/
|
||||
@Schema(description = "分片大小")
|
||||
private Long partSize;
|
||||
|
||||
/**
|
||||
* 是否成功
|
||||
*/
|
||||
@Schema(description = "是否成功")
|
||||
private boolean success;
|
||||
|
||||
/**
|
||||
* 错误信息
|
||||
*/
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
}
|
@@ -16,10 +16,12 @@
|
||||
|
||||
package top.continew.admin.system.service;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import org.dromara.x.file.storage.core.FileInfo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import top.continew.admin.common.base.service.BaseService;
|
||||
import top.continew.admin.system.model.entity.FileDO;
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
import top.continew.admin.system.model.query.FileQuery;
|
||||
import top.continew.admin.system.model.req.FileReq;
|
||||
import top.continew.admin.system.model.resp.file.FileResp;
|
||||
@@ -60,7 +62,7 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery,
|
||||
* @throws IOException /
|
||||
*/
|
||||
default FileInfo upload(MultipartFile file, String parentPath) throws IOException {
|
||||
return upload(file, parentPath, null);
|
||||
return upload(file, StrUtil.blankToDefault(parentPath, getDefaultParentPath()), null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +96,7 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery,
|
||||
* @throws IOException /
|
||||
*/
|
||||
default FileInfo upload(File file, String parentPath) throws IOException {
|
||||
return upload(file, parentPath, null);
|
||||
return upload(file, StrUtil.blankToDefault(parentPath, getDefaultParentPath()), null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,6 +149,18 @@ public interface FileService extends BaseService<FileResp, FileResp, FileQuery,
|
||||
*/
|
||||
Long countByStorageIds(List<Long> storageIds);
|
||||
|
||||
/**
|
||||
* 创建上级文件夹(支持多级)
|
||||
*
|
||||
* <p>
|
||||
* user/avatar/ => user(path:/user)、avatar(path:/user/avatar)
|
||||
* </p>
|
||||
*
|
||||
* @param parentPath 上级目录
|
||||
* @param storage 存储配置
|
||||
*/
|
||||
void createParentDir(String parentPath, StorageDO storage);
|
||||
|
||||
/**
|
||||
* 获取默认上级目录
|
||||
*
|
||||
|
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.service;
|
||||
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import top.continew.admin.system.model.entity.FileDO;
|
||||
import top.continew.admin.system.model.req.MultipartUploadInitReq;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadResp;
|
||||
|
||||
/**
|
||||
* 分片上传业务接口
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2025/7/3 8:42
|
||||
*/
|
||||
public interface MultipartUploadService {
|
||||
|
||||
MultipartUploadInitResp initMultipartUpload(MultipartUploadInitReq multiPartUploadInitReq);
|
||||
|
||||
MultipartUploadResp uploadPart(MultipartFile file, String uploadId, Integer partNumber, String path);
|
||||
|
||||
FileDO completeMultipartUpload(String uploadId);
|
||||
|
||||
void cancelMultipartUpload(String uploadId);
|
||||
}
|
@@ -19,7 +19,7 @@ package top.continew.admin.system.service;
|
||||
import top.continew.admin.system.enums.OptionCategoryEnum;
|
||||
import top.continew.admin.system.model.query.OptionQuery;
|
||||
import top.continew.admin.system.model.req.OptionReq;
|
||||
import top.continew.admin.system.model.req.OptionResetValueReq;
|
||||
import top.continew.admin.system.model.req.OptionValueResetReq;
|
||||
import top.continew.admin.system.model.resp.OptionResp;
|
||||
|
||||
import java.util.List;
|
||||
@@ -62,7 +62,7 @@ public interface OptionService {
|
||||
*
|
||||
* @param req 重置信息
|
||||
*/
|
||||
void resetValue(OptionResetValueReq req);
|
||||
void resetValue(OptionValueResetReq req);
|
||||
|
||||
/**
|
||||
* 根据编码查询参数值
|
||||
|
@@ -21,7 +21,7 @@ import top.continew.admin.common.context.RoleContext;
|
||||
import top.continew.admin.system.model.entity.RoleDO;
|
||||
import top.continew.admin.system.model.query.RoleQuery;
|
||||
import top.continew.admin.system.model.req.RoleReq;
|
||||
import top.continew.admin.system.model.req.RoleUpdatePermissionReq;
|
||||
import top.continew.admin.system.model.req.RolePermissionUpdateReq;
|
||||
import top.continew.admin.system.model.resp.role.RoleDetailResp;
|
||||
import top.continew.admin.system.model.resp.role.RoleResp;
|
||||
import top.continew.starter.data.service.IService;
|
||||
@@ -43,7 +43,7 @@ public interface RoleService extends BaseService<RoleResp, RoleDetailResp, RoleQ
|
||||
* @param id 角色 ID
|
||||
* @param req 请求参数
|
||||
*/
|
||||
void updatePermission(Long id, RoleUpdatePermissionReq req);
|
||||
void updatePermission(Long id, RolePermissionUpdateReq req);
|
||||
|
||||
/**
|
||||
* 分配角色给用户
|
||||
|
@@ -252,10 +252,9 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
|
||||
* 处理路径
|
||||
*
|
||||
* <p>
|
||||
* 1.如果 path 为空,则使用 {@link FileService#getDefaultParentPath()} 作为默认值 <br />
|
||||
* 2.如果 path 为 {@code /},则设置为空 <br />
|
||||
* 3.如果 path 不以 {@code /} 结尾,则添加后缀 {@code /} <br />
|
||||
* 4.如果 path 以 {@code /} 开头,则移除前缀 {@code /} <br />
|
||||
* 1.如果 path 为 {@code /},则设置为空 <br />
|
||||
* 2.如果 path 不以 {@code /} 结尾,则添加后缀 {@code /} <br />
|
||||
* 3.如果 path 以 {@code /} 开头,则移除前缀 {@code /} <br />
|
||||
* 示例:yyyy/MM/dd/
|
||||
* </p>
|
||||
*
|
||||
@@ -263,9 +262,6 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
|
||||
* @return 处理路径
|
||||
*/
|
||||
private String pretreatmentPath(String path) {
|
||||
if (StrUtil.isBlank(path)) {
|
||||
return this.getDefaultParentPath();
|
||||
}
|
||||
if (StringConstants.SLASH.equals(path)) {
|
||||
return StringConstants.EMPTY;
|
||||
}
|
||||
@@ -282,7 +278,8 @@ public class FileServiceImpl extends BaseServiceImpl<FileMapper, FileDO, FileRes
|
||||
* @param parentPath 上级目录
|
||||
* @param storage 存储配置
|
||||
*/
|
||||
private void createParentDir(String parentPath, StorageDO storage) {
|
||||
@Override
|
||||
public void createParentDir(String parentPath, StorageDO storage) {
|
||||
if (StrUtil.isBlank(parentPath) || StringConstants.SLASH.equals(parentPath)) {
|
||||
return;
|
||||
}
|
||||
|
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package top.continew.admin.system.service.impl;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import top.continew.admin.system.constant.MultipartUploadConstants;
|
||||
import top.continew.admin.system.dao.MultipartUploadDao;
|
||||
import top.continew.admin.system.enums.FileTypeEnum;
|
||||
import top.continew.admin.system.factory.StorageHandlerFactory;
|
||||
import top.continew.admin.system.handler.StorageHandler;
|
||||
import top.continew.admin.system.handler.impl.LocalStorageHandler;
|
||||
import top.continew.admin.system.model.entity.FileDO;
|
||||
import top.continew.admin.system.model.entity.StorageDO;
|
||||
import top.continew.admin.system.model.req.MultipartUploadInitReq;
|
||||
import top.continew.admin.system.model.resp.file.FilePartInfo;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
|
||||
import top.continew.admin.system.model.resp.file.MultipartUploadResp;
|
||||
import top.continew.admin.system.service.FileService;
|
||||
import top.continew.admin.system.service.MultipartUploadService;
|
||||
import top.continew.admin.system.service.StorageService;
|
||||
import top.continew.starter.core.exception.BaseException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 分片上传业务实现
|
||||
*
|
||||
* @author KAI
|
||||
* @since 2025/7/31 9:30
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MultipartUploadServiceImpl implements MultipartUploadService {
|
||||
|
||||
private final StorageService storageService;
|
||||
|
||||
private final StorageHandlerFactory storageHandlerFactory;
|
||||
|
||||
private final MultipartUploadDao multipartUploadDao;
|
||||
|
||||
private final FileService fileService;
|
||||
|
||||
@Override
|
||||
public MultipartUploadInitResp initMultipartUpload(MultipartUploadInitReq multiPartUploadInitReq) {
|
||||
// 后续可以增加storageCode参数 指定某个存储平台 当前设计是默认存储平台
|
||||
StorageDO storageDO = storageService.getByCode(null);
|
||||
// 根据文件Md5查询当前存储平台是否初始化过分片
|
||||
String uploadId = multipartUploadDao.getUploadIdByMd5(multiPartUploadInitReq.getFileMd5());
|
||||
if (StrUtil.isNotBlank(uploadId)) {
|
||||
MultipartUploadInitResp multipartUpload = multipartUploadDao.getMultipartUpload(uploadId);
|
||||
//对比存储平台和分片大小是否一致 一致则返回结果
|
||||
if (multipartUpload != null && multipartUpload.getPartSize()
|
||||
.equals(MultipartUploadConstants.MULTIPART_UPLOAD_PART_SIZE) && multipartUpload.getPlatform()
|
||||
.equals(storageDO.getCode())) {
|
||||
// 获取已上传分片信息
|
||||
List<FilePartInfo> fileParts = multipartUploadDao.getFileParts(uploadId);
|
||||
Set<Integer> partNumbers = fileParts.stream()
|
||||
.map(FilePartInfo::getPartNumber)
|
||||
.collect(Collectors.toSet());
|
||||
multipartUpload.setUploadedPartNumbers(partNumbers);
|
||||
return multipartUpload;
|
||||
}
|
||||
//todo else 待定 更换存储平台 或分片大小有变更 是否需要删除原先分片
|
||||
|
||||
}
|
||||
StorageHandler storageHandler = storageHandlerFactory.createHandler(storageDO.getType());
|
||||
//文件元信息
|
||||
Map<String, String> metaData = multiPartUploadInitReq.getMetaData();
|
||||
MultipartUploadInitResp multipartUploadInitResp = storageHandler
|
||||
.initMultipartUpload(storageDO, multiPartUploadInitReq);
|
||||
// 缓存文件信息,md5和uploadId映射
|
||||
multipartUploadDao.setMultipartUpload(multipartUploadInitResp.getUploadId(), multipartUploadInitResp, metaData);
|
||||
multipartUploadDao.setMd5Mapping(multiPartUploadInitReq.getFileMd5(), multipartUploadInitResp.getUploadId());
|
||||
return multipartUploadInitResp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultipartUploadResp uploadPart(MultipartFile file, String uploadId, Integer partNumber, String path) {
|
||||
StorageDO storageDO = storageService.getByCode(null);
|
||||
StorageHandler storageHandler = storageHandlerFactory.createHandler(storageDO.getType());
|
||||
MultipartUploadResp resp = storageHandler.uploadPart(storageDO, path, uploadId, partNumber, file);
|
||||
FilePartInfo partInfo = new FilePartInfo();
|
||||
partInfo.setUploadId(uploadId);
|
||||
partInfo.setBucket(storageDO.getBucketName());
|
||||
partInfo.setPath(path);
|
||||
partInfo.setPartNumber(partNumber);
|
||||
partInfo.setPartETag(resp.getPartETag());
|
||||
partInfo.setPartSize(resp.getPartSize());
|
||||
partInfo.setStatus("SUCCESS");
|
||||
partInfo.setUploadTime(LocalDateTime.now());
|
||||
multipartUploadDao.setFilePart(uploadId, partInfo);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileDO completeMultipartUpload(String uploadId) {
|
||||
StorageDO storageDO = storageService.getByCode(null);
|
||||
// 从 FileRecorder 获取所有分片信息
|
||||
List<FilePartInfo> recordedParts = multipartUploadDao.getFileParts(uploadId);
|
||||
MultipartUploadInitResp initResp = multipartUploadDao.getMultipartUpload(uploadId);
|
||||
// 转换为 MultipartUploadResp
|
||||
List<MultipartUploadResp> parts = recordedParts.stream().map(partInfo -> {
|
||||
MultipartUploadResp resp = new MultipartUploadResp();
|
||||
resp.setPartNumber(partInfo.getPartNumber());
|
||||
resp.setPartETag(partInfo.getPartETag());
|
||||
resp.setPartSize(partInfo.getPartSize());
|
||||
resp.setSuccess("SUCCESS".equals(partInfo.getStatus()));
|
||||
return resp;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// 如果没有记录,使用客户端传入的分片信息
|
||||
if (parts.isEmpty()) {
|
||||
throw new BaseException("没有找到任何分片信息");
|
||||
}
|
||||
|
||||
// 验证分片完整性
|
||||
validatePartsCompleteness(parts);
|
||||
|
||||
// 获取策略,判断是否需要验证
|
||||
boolean needVerify = true;
|
||||
StorageHandler storageHandler = storageHandlerFactory.createHandler(storageDO.getType());
|
||||
if (storageHandler instanceof LocalStorageHandler) {
|
||||
needVerify = false;
|
||||
}
|
||||
|
||||
// 完成上传
|
||||
storageHandler.completeMultipartUpload(storageDO, parts, initResp.getPath(), uploadId, needVerify);
|
||||
FileDO file = new FileDO();
|
||||
file.setName(initResp.getFileName().replaceFirst("^[/\\\\]+", ""));
|
||||
file.setOriginalName(initResp.getFileName().replaceFirst("^[/\\\\]+", ""));
|
||||
file.setPath(initResp.getPath());
|
||||
file.setParentPath(initResp.getParentPath());
|
||||
file.setSize(initResp.getFileSize());
|
||||
file.setSha256(initResp.getFileMd5());
|
||||
file.setExtension(initResp.getExtension());
|
||||
file.setContentType(initResp.getContentType());
|
||||
file.setType(FileTypeEnum.getByExtension(FileUtil.extName(initResp.getFileName())));
|
||||
file.setStorageId(storageDO.getId());
|
||||
fileService.save(file);
|
||||
multipartUploadDao.deleteMultipartUpload(uploadId);
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelMultipartUpload(String uploadId) {
|
||||
StorageDO storageDO = storageService.getByCode(null);
|
||||
multipartUploadDao.deleteMultipartUploadAll(uploadId);
|
||||
StorageHandler storageHandler = storageHandlerFactory.createHandler(storageDO.getType());
|
||||
storageHandler.cleanPart(storageDO, uploadId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分片完整性
|
||||
*
|
||||
* @param parts 分片信息
|
||||
*/
|
||||
private void validatePartsCompleteness(List<MultipartUploadResp> parts) {
|
||||
if (parts.isEmpty()) {
|
||||
throw new BaseException("没有找到任何分片信息");
|
||||
}
|
||||
|
||||
// 检查分片编号连续性
|
||||
List<Integer> partNumbers = parts.stream().map(MultipartUploadResp::getPartNumber).sorted().toList();
|
||||
|
||||
for (int i = 0; i < partNumbers.size(); i++) {
|
||||
if (partNumbers.get(i) != i + 1) {
|
||||
throw new BaseException("分片编号不连续,缺失分片: " + (i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否所有分片都成功
|
||||
List<Integer> failedParts = parts.stream()
|
||||
.filter(part -> !part.isSuccess())
|
||||
.map(MultipartUploadResp::getPartNumber)
|
||||
.toList();
|
||||
|
||||
if (!failedParts.isEmpty()) {
|
||||
throw new BaseException("存在失败的分片: " + failedParts);
|
||||
}
|
||||
}
|
||||
}
|
@@ -32,7 +32,7 @@ import top.continew.admin.system.mapper.OptionMapper;
|
||||
import top.continew.admin.system.model.entity.OptionDO;
|
||||
import top.continew.admin.system.model.query.OptionQuery;
|
||||
import top.continew.admin.system.model.req.OptionReq;
|
||||
import top.continew.admin.system.model.req.OptionResetValueReq;
|
||||
import top.continew.admin.system.model.req.OptionValueResetReq;
|
||||
import top.continew.admin.system.model.resp.OptionResp;
|
||||
import top.continew.admin.system.service.OptionService;
|
||||
import top.continew.starter.cache.redisson.util.RedisUtils;
|
||||
@@ -104,7 +104,7 @@ public class OptionServiceImpl implements OptionService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetValue(OptionResetValueReq req) {
|
||||
public void resetValue(OptionValueResetReq req) {
|
||||
RedisUtils.deleteByPattern(CacheConstants.OPTION_KEY_PREFIX + StringConstants.ASTERISK);
|
||||
String category = req.getCategory();
|
||||
List<String> codeList = req.getCode();
|
||||
|
@@ -36,7 +36,7 @@ import top.continew.admin.system.mapper.RoleMapper;
|
||||
import top.continew.admin.system.model.entity.RoleDO;
|
||||
import top.continew.admin.system.model.query.RoleQuery;
|
||||
import top.continew.admin.system.model.req.RoleReq;
|
||||
import top.continew.admin.system.model.req.RoleUpdatePermissionReq;
|
||||
import top.continew.admin.system.model.req.RolePermissionUpdateReq;
|
||||
import top.continew.admin.system.model.resp.MenuResp;
|
||||
import top.continew.admin.system.model.resp.role.RoleDetailResp;
|
||||
import top.continew.admin.system.model.resp.role.RoleResp;
|
||||
@@ -142,7 +142,7 @@ public class RoleServiceImpl extends BaseServiceImpl<RoleMapper, RoleDO, RoleRes
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@CacheInvalidate(key = "#id", name = CacheConstants.ROLE_MENU_KEY_PREFIX)
|
||||
public void updatePermission(Long id, RoleUpdatePermissionReq req) {
|
||||
public void updatePermission(Long id, RolePermissionUpdateReq req) {
|
||||
RoleDO role = super.getById(id);
|
||||
CheckUtils.throwIf(Boolean.TRUE.equals(role.getIsSystem()), "[{}] 是系统内置角色,不允许修改角色功能权限", role.getName());
|
||||
// 保存角色和菜单关联
|
||||
|
@@ -28,9 +28,9 @@ import org.dromara.x.file.storage.core.FileStorageServiceBuilder;
|
||||
import org.dromara.x.file.storage.core.platform.FileStorage;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import top.continew.admin.common.model.req.CommonStatusUpdateReq;
|
||||
import top.continew.admin.common.base.service.BaseServiceImpl;
|
||||
import top.continew.admin.common.enums.DisEnableStatusEnum;
|
||||
import top.continew.admin.common.model.req.CommonStatusUpdateReq;
|
||||
import top.continew.admin.common.util.SecureUtils;
|
||||
import top.continew.admin.system.enums.StorageTypeEnum;
|
||||
import top.continew.admin.system.mapper.StorageMapper;
|
||||
@@ -40,7 +40,6 @@ import top.continew.admin.system.model.req.StorageReq;
|
||||
import top.continew.admin.system.model.resp.StorageResp;
|
||||
import top.continew.admin.system.service.FileService;
|
||||
import top.continew.admin.system.service.StorageService;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.util.ExceptionUtils;
|
||||
import top.continew.starter.core.util.SpringWebUtils;
|
||||
import top.continew.starter.core.util.validation.CheckUtils;
|
||||
@@ -68,6 +67,7 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
|
||||
public void beforeCreate(StorageReq req) {
|
||||
// 解密密钥
|
||||
if (StorageTypeEnum.OSS.equals(req.getType())) {
|
||||
ValidationUtils.throwIfBlank(req.getSecretKey(), "Secret Key不能为空");
|
||||
req.setSecretKey(this.decryptSecretKey(req.getSecretKey(), null));
|
||||
}
|
||||
// 指定配置参数校验及预处理
|
||||
@@ -228,13 +228,9 @@ public class StorageServiceImpl extends BaseServiceImpl<StorageMapper, StorageDO
|
||||
* @return 解密后的 SecretKey
|
||||
*/
|
||||
private String decryptSecretKey(String encryptSecretKey, StorageDO oldStorage) {
|
||||
// 修改时,SecretKey 为空或带 *,将不更改
|
||||
if (oldStorage != null) {
|
||||
boolean isSecretKeyNotUpdate = StrUtil.isBlank(encryptSecretKey) || encryptSecretKey
|
||||
.contains(StringConstants.ASTERISK);
|
||||
if (isSecretKeyNotUpdate) {
|
||||
return oldStorage.getSecretKey();
|
||||
}
|
||||
// 修改时,SecretKey 为空将不更改
|
||||
if (oldStorage != null && StrUtil.isBlank(encryptSecretKey)) {
|
||||
return oldStorage.getSecretKey();
|
||||
}
|
||||
// 解密
|
||||
String secretKey = ExceptionUtils.exToNull(() -> SecureUtils.decryptByRsaPrivateKey(encryptSecretKey));
|
||||
|
@@ -32,16 +32,19 @@ import top.continew.admin.common.enums.RoleCodeEnum;
|
||||
import top.continew.admin.system.constant.SystemConstants;
|
||||
import top.continew.admin.system.mapper.UserRoleMapper;
|
||||
import top.continew.admin.system.model.entity.UserRoleDO;
|
||||
import top.continew.admin.system.model.entity.user.UserDO;
|
||||
import top.continew.admin.system.model.query.RoleUserQuery;
|
||||
import top.continew.admin.system.model.resp.role.RoleUserResp;
|
||||
import top.continew.admin.system.service.RoleService;
|
||||
import top.continew.admin.system.service.UserRoleService;
|
||||
import top.continew.admin.system.service.UserService;
|
||||
import top.continew.starter.core.util.CollUtils;
|
||||
import top.continew.starter.core.util.validation.CheckUtils;
|
||||
import top.continew.starter.data.util.QueryWrapperHelper;
|
||||
import top.continew.starter.extension.crud.model.query.PageQuery;
|
||||
import top.continew.starter.extension.crud.model.resp.PageResp;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -59,6 +62,9 @@ public class UserRoleServiceImpl implements UserRoleService {
|
||||
@Lazy
|
||||
@Resource
|
||||
private RoleService roleService;
|
||||
@Lazy
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Override
|
||||
@AutoOperate(type = RoleUserResp.class, on = "list")
|
||||
@@ -79,6 +85,11 @@ public class UserRoleServiceImpl implements UserRoleService {
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean assignRolesToUser(List<Long> roleIds, Long userId) {
|
||||
UserDO userDO = userService.getById(userId);
|
||||
if (Boolean.TRUE.equals(userDO.getIsSystem())) {
|
||||
Collection<Long> disjunctionRoleIds = CollUtil.disjunction(roleIds, this.listRoleIdByUserId(userId));
|
||||
CheckUtils.throwIfNotEmpty(disjunctionRoleIds, "[{}] 是系统内置用户,不允许变更角色", userDO.getNickname());
|
||||
}
|
||||
// 超级管理员和租户管理员角色不允许分配
|
||||
CheckUtils.throwIf(roleIds.contains(SystemConstants.SUPER_ADMIN_ROLE_ID), "不允许分配超级管理员角色");
|
||||
Set<String> roleCodeSet = CollUtils.mapToSet(roleService.listByUserId(userId), RoleContext::getCode);
|
||||
|
@@ -3,8 +3,6 @@
|
||||
# 若对镜像大小有严格要求,可将当前镜像替换为 alpine 版本
|
||||
FROM bellsoft/liberica-openjdk-debian:17.0.14
|
||||
|
||||
MAINTAINER Charles7c charles7c@126.com
|
||||
|
||||
ARG JAR_FILE=./bin/*.jar
|
||||
COPY ${JAR_FILE} /app/bin/app.jar
|
||||
WORKDIR /app/bin
|
||||
|
@@ -2,10 +2,13 @@ version: '3'
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0.42
|
||||
restart: always
|
||||
container_name: mysql
|
||||
restart: always
|
||||
ports:
|
||||
- '3306:3306'
|
||||
volumes:
|
||||
- /docker/mysql/conf/:/etc/mysql/conf.d/
|
||||
- /docker/mysql/data/:/var/lib/mysql/
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
MYSQL_ROOT_PASSWORD: 你的root用户密码
|
||||
@@ -13,9 +16,6 @@ services:
|
||||
MYSQL_DATABASE: continew_admin
|
||||
#MYSQL_USER: 你的数据库用户名
|
||||
#MYSQL_PASSWORD: 你的数据库密码
|
||||
volumes:
|
||||
- /docker/mysql/conf/:/etc/mysql/conf.d/
|
||||
- /docker/mysql/data/:/var/lib/mysql/
|
||||
command:
|
||||
--default-authentication-plugin=mysql_native_password
|
||||
--character-set-server=utf8mb4
|
||||
@@ -24,37 +24,42 @@ services:
|
||||
--lower_case_table_names=1
|
||||
# postgresql:
|
||||
# image: postgres:14.2
|
||||
# restart: always
|
||||
# container_name: postgresql
|
||||
# restart: always
|
||||
# ports:
|
||||
# - '5432:5432'
|
||||
# volumes:
|
||||
# - /docker/postgresql/data/:/var/lib/postgresql/data/
|
||||
# environment:
|
||||
# TZ: Asia/Shanghai
|
||||
# POSTGRES_USER: 你的用户名
|
||||
# POSTGRES_PASSWORD: 你的用户密码
|
||||
# POSTGRES_DB: continew_admin
|
||||
# volumes:
|
||||
# - /docker/postgresql/data/:/var/lib/postgresql/data/
|
||||
redis:
|
||||
image: redis:7.2.8
|
||||
restart: always
|
||||
container_name: redis
|
||||
restart: always
|
||||
ports:
|
||||
- '6379:6379'
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- /docker/redis/conf/redis.conf:/usr/local/redis/config/redis.conf
|
||||
- /docker/redis/data/:/data/
|
||||
- /docker/redis/logs/:/logs/
|
||||
command: 'redis-server /usr/local/redis/config/redis.conf --appendonly yes --requirepass 你的 Redis 密码'
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
command: 'redis-server /usr/local/redis/config/redis.conf --appendonly yes'
|
||||
continew-server:
|
||||
build: ./continew-admin
|
||||
restart: always
|
||||
container_name: continew-server
|
||||
restart: always
|
||||
ports:
|
||||
- '18000:18000'
|
||||
- '1789:1789'
|
||||
volumes:
|
||||
- /docker/continew-admin/config/:/app/config/
|
||||
- /docker/continew-admin/data/file/:/app/data/file/
|
||||
- /docker/continew-admin/logs/:/app/logs/
|
||||
- /docker/continew-admin/lib/:/app/lib/
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
DB_HOST: 172.17.0.1
|
||||
@@ -69,36 +74,33 @@ services:
|
||||
SCHEDULE_HOST: 172.17.0.1
|
||||
SCHEDULE_PORT: 1788
|
||||
SCHEDULE_TOKEN: 任务调度服务端 Token
|
||||
volumes:
|
||||
- /docker/continew-admin/config/:/app/config/
|
||||
- /docker/continew-admin/data/file/:/app/data/file/
|
||||
- /docker/continew-admin/logs/:/app/logs/
|
||||
- /docker/continew-admin/lib/:/app/lib/
|
||||
depends_on:
|
||||
- redis
|
||||
- mysql
|
||||
continew-web:
|
||||
image: nginx:1.27.0
|
||||
restart: always
|
||||
container_name: continew-web
|
||||
restart: always
|
||||
ports:
|
||||
- '80:80'
|
||||
- '443:443'
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
volumes:
|
||||
- /docker/nginx/conf/nginx.conf:/etc/nginx/nginx.conf
|
||||
- /docker/nginx/cert/:/etc/nginx/cert/
|
||||
- /docker/nginx/logs/:/var/log/nginx/
|
||||
# 前端页面目录
|
||||
- /docker/continew-admin/web/:/usr/share/nginx/html/
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
schedule-server:
|
||||
build: ./schedule-server
|
||||
restart: always
|
||||
container_name: schedule-server
|
||||
restart: always
|
||||
ports:
|
||||
- '18001:18001'
|
||||
- '1788:1788'
|
||||
volumes:
|
||||
- /docker/schedule-server/logs/:/app/logs/
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
DB_HOST: 172.17.0.1
|
||||
@@ -106,7 +108,5 @@ services:
|
||||
DB_USER: 你的数据库用户名
|
||||
DB_PWD: 你的数据库密码
|
||||
DB_NAME: continew_admin_job
|
||||
volumes:
|
||||
- /docker/schedule-server/logs/:/app/logs/
|
||||
depends_on:
|
||||
- mysql
|
||||
|
@@ -1 +0,0 @@
|
||||
Redis 数据存储目录,请确保赋予了读写权限,否则将无法写入数据
|
@@ -3,8 +3,6 @@
|
||||
# 若对镜像大小有严格要求,可将当前镜像替换为 alpine 版本
|
||||
FROM bellsoft/liberica-openjdk-debian:17.0.14
|
||||
|
||||
MAINTAINER Charles7c charles7c@126.com
|
||||
|
||||
ARG JAR_FILE=*.jar
|
||||
COPY ${JAR_FILE} /app/bin/app.jar
|
||||
WORKDIR /app/bin
|
||||
|
Reference in New Issue
Block a user