From 78b7bac43b1b38c41a0f9721037d471eaa94b637 Mon Sep 17 00:00:00 2001 From: jlzhou <12020042@qq.com> Date: Thu, 24 Oct 2024 17:56:08 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E5=88=A0=E9=99=A4=E6=BA=90ruoyi-oss?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=EF=BC=8C=E4=BD=BF=E7=94=A8x-file-storage?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E6=96=B0=E6=A8=A1=E5=9D=97ruoyi-file?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=A7=E6=96=87=E4=BB=B6=E5=88=86?= =?UTF-8?q?=E7=89=87=E4=B8=8A=E4=BC=A0=EF=BC=8C=E5=B9=B6=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=96=87=E4=BB=B6=E5=92=8C=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/auto/components/WFileUploader.vue | 653 ++++++++++++++++++ .../src/auto/components/WImageUploader.vue | 527 ++++++++++++++ admin-ui/src/auto/components/WImageView.vue | 18 +- admin-ui/src/views/demo/file/index.vue | 159 +++++ pom.xml | 16 +- ruoyi-admin/pom.xml | 2 +- .../web/controller/UploadController.java | 71 +- .../system/SysOssConfigController.java | 105 --- .../controller/system/SysOssController.java | 117 ---- .../system/SysProfileController.java | 227 +++--- .../src/main/resources/application-dev.yml | 43 +- .../resources/application-local.yml.template | 43 +- .../src/main/resources/application-prod.yml | 42 +- .../src/main/resources/application.yml | 9 +- .../java/com/ruoyi/test/PasswordTest.java | 9 + .../java/com/ruoyi/test/XFileStorageTest.java | 201 ++++++ ruoyi-admin/src/test/resources/test.png | Bin 0 -> 83176 bytes .../com/ruoyi/common/config/RuoYiConfig.java | 138 ++-- .../impl/OssUrlTranslationImpl.java | 25 - .../ruoyi/common/utils/file/FileUtils.java | 105 ++- ruoyi-oss/pom.xml | 33 - .../com/ruoyi/oss/constant/OssConstant.java | 38 - .../java/com/ruoyi/oss/core/OssClient.java | 268 ------- .../com/ruoyi/oss/entity/UploadResult.java | 24 - .../com/ruoyi/oss/enumd/AccessPolicyType.java | 55 -- .../java/com/ruoyi/oss/enumd/PolicyType.java | 35 - .../com/ruoyi/oss/exception/OssException.java | 16 - .../com/ruoyi/oss/factory/OssFactory.java | 63 -- .../ruoyi/oss/properties/OssProperties.java | 58 -- ruoyi-system-cron/pom.xml | 2 +- ruoyi-system-file/README.md | 1 + ruoyi-system-file/pom.xml | 115 +++ .../file/FileDownloadTestController.java | 30 + .../main/java/com/ruoyi/file/FileService.java | 644 +++++++++++++++++ .../java/com/ruoyi/file/FileUploadApi.java | 184 +++++ .../com/ruoyi/file/config/FileConfig.java | 18 + .../config/SpringFileStorageProperties.java | 45 ++ .../com/ruoyi/file/impl/FileServiceImpl.java | 370 ++++++++++ .../ruoyi/file/impl/FixFileStorageAspect.java | 31 + .../java/com/ruoyi/file/package-info.java | 4 + .../src/main/resources/watermark.png | Bin 0 -> 17414 bytes ruoyi-system/pom.xml | 10 +- .../system/config/DownloadFileConfig.java | 75 -- .../java/com/ruoyi/system/domain/SysOss.java | 50 -- .../com/ruoyi/system/domain/SysOssConfig.java | 89 --- .../com/ruoyi/system/domain/bo/SysOssBo.java | 46 -- .../system/domain/bo/SysOssConfigBo.java | 107 --- .../system/domain/vo/SysOssConfigVo.java | 90 --- .../com/ruoyi/system/domain/vo/SysOssVo.java | 58 -- .../system/mapper/SysOssConfigMapper.java | 16 - .../com/ruoyi/system/mapper/SysOssMapper.java | 13 - .../runner/SystemApplicationRunner.java | 4 +- .../com/ruoyi/system/service/FileService.java | 121 ---- .../system/service/ISysOssConfigService.java | 65 -- .../ruoyi/system/service/ISysOssService.java | 396 ----------- .../ruoyi/system/service/SysLoginService.java | 7 +- .../system/service/impl/FileServiceImpl.java | 85 --- .../service/impl/SysOssConfigServiceImpl.java | 170 ----- .../service/impl/SysOssServiceImpl.java | 399 ----------- .../mapper/system/SysOssConfigMapper.xml | 27 - .../resources/mapper/system/SysOssMapper.xml | 18 - ruoyi.sql | 72 +- script/docker/nginx/conf/nginx.conf | 13 +- 63 files changed, 3413 insertions(+), 3062 deletions(-) create mode 100644 admin-ui/src/auto/components/WFileUploader.vue create mode 100644 admin-ui/src/auto/components/WImageUploader.vue create mode 100644 admin-ui/src/views/demo/file/index.vue delete mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysOssConfigController.java delete mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysOssController.java create mode 100644 ruoyi-admin/src/test/java/com/ruoyi/test/XFileStorageTest.java create mode 100644 ruoyi-admin/src/test/resources/test.png delete mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/OssUrlTranslationImpl.java delete mode 100644 ruoyi-oss/pom.xml delete mode 100644 ruoyi-oss/src/main/java/com/ruoyi/oss/constant/OssConstant.java delete mode 100644 ruoyi-oss/src/main/java/com/ruoyi/oss/core/OssClient.java delete mode 100644 ruoyi-oss/src/main/java/com/ruoyi/oss/entity/UploadResult.java delete mode 100644 ruoyi-oss/src/main/java/com/ruoyi/oss/enumd/AccessPolicyType.java delete mode 100644 ruoyi-oss/src/main/java/com/ruoyi/oss/enumd/PolicyType.java delete mode 100644 ruoyi-oss/src/main/java/com/ruoyi/oss/exception/OssException.java delete mode 100644 ruoyi-oss/src/main/java/com/ruoyi/oss/factory/OssFactory.java delete mode 100644 ruoyi-oss/src/main/java/com/ruoyi/oss/properties/OssProperties.java create mode 100644 ruoyi-system-file/README.md create mode 100644 ruoyi-system-file/pom.xml create mode 100644 ruoyi-system-file/src/main/java/com/ruoyi/file/FileDownloadTestController.java create mode 100644 ruoyi-system-file/src/main/java/com/ruoyi/file/FileService.java create mode 100644 ruoyi-system-file/src/main/java/com/ruoyi/file/FileUploadApi.java create mode 100644 ruoyi-system-file/src/main/java/com/ruoyi/file/config/FileConfig.java create mode 100644 ruoyi-system-file/src/main/java/com/ruoyi/file/config/SpringFileStorageProperties.java create mode 100644 ruoyi-system-file/src/main/java/com/ruoyi/file/impl/FileServiceImpl.java create mode 100644 ruoyi-system-file/src/main/java/com/ruoyi/file/impl/FixFileStorageAspect.java create mode 100644 ruoyi-system-file/src/main/java/com/ruoyi/file/package-info.java create mode 100644 ruoyi-system-file/src/main/resources/watermark.png delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/config/DownloadFileConfig.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOss.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOssConfig.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssBo.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssVo.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssConfigMapper.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssMapper.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/FileService.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssConfigService.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/FileServiceImpl.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java delete mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java delete mode 100644 ruoyi-system/src/main/resources/mapper/system/SysOssConfigMapper.xml delete mode 100644 ruoyi-system/src/main/resources/mapper/system/SysOssMapper.xml diff --git a/admin-ui/src/auto/components/WFileUploader.vue b/admin-ui/src/auto/components/WFileUploader.vue new file mode 100644 index 0000000..04605fe --- /dev/null +++ b/admin-ui/src/auto/components/WFileUploader.vue @@ -0,0 +1,653 @@ + + + \ No newline at end of file diff --git a/admin-ui/src/auto/components/WImageUploader.vue b/admin-ui/src/auto/components/WImageUploader.vue new file mode 100644 index 0000000..eeaa546 --- /dev/null +++ b/admin-ui/src/auto/components/WImageUploader.vue @@ -0,0 +1,527 @@ + + + \ No newline at end of file diff --git a/admin-ui/src/auto/components/WImageView.vue b/admin-ui/src/auto/components/WImageView.vue index 445f03f..786e9c2 100644 --- a/admin-ui/src/auto/components/WImageView.vue +++ b/admin-ui/src/auto/components/WImageView.vue @@ -1,9 +1,5 @@ - - \ No newline at end of file diff --git a/admin-ui/src/views/demo/file/index.vue b/admin-ui/src/views/demo/file/index.vue new file mode 100644 index 0000000..667de97 --- /dev/null +++ b/admin-ui/src/views/demo/file/index.vue @@ -0,0 +1,159 @@ + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index ee0ac58..179cd96 100644 --- a/pom.xml +++ b/pom.xml @@ -275,21 +275,20 @@ ruoyi-system-cron ${ruoyi-vue-plus.version} - com.ruoyi - ruoyi-common + ruoyi-system-file ${ruoyi-vue-plus.version} + com.ruoyi - ruoyi-common-websocket + ruoyi-common ${ruoyi-vue-plus.version} - com.ruoyi - ruoyi-oss + ruoyi-common-websocket ${ruoyi-vue-plus.version} @@ -340,11 +339,6 @@ ruoyi-demo ${ruoyi-vue-plus.version} - - com.github.gotson - webp-imageio - 0.2.2 - com.github.binarywang weixin-java-miniapp @@ -372,9 +366,9 @@ ruoyi-framework ruoyi-system ruoyi-system-cron + ruoyi-system-file ruoyi-common ruoyi-demo - ruoyi-oss ruoyi-sms pom diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 1530cf1..d469040 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -70,7 +70,7 @@ com.ruoyi - ruoyi-oss + ruoyi-system-file diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/UploadController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/UploadController.java index 699d785..2769406 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/UploadController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/UploadController.java @@ -1,56 +1,67 @@ package com.ruoyi.web.controller; -import cn.dev33.satoken.annotation.SaIgnore; import cn.hutool.core.util.ObjectUtil; -import com.ruoyi.common.annotation.Dev; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.exception.ServiceException; -import com.ruoyi.system.service.ISysOssService; +import com.ruoyi.file.FileService; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.time.Duration; @RequiredArgsConstructor @RestController @RequestMapping("/") public class UploadController { - private final ISysOssService iSysOssService; + private final FileService fileService; - @Log(title = "OSS对象存储", businessType = BusinessType.INSERT) - @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public R upload(@RequestPart("file") MultipartFile file, String pre) { - if (ObjectUtil.isNull(file)) { - throw new ServiceException("文件为空"); - } - return R.ok(iSysOssService.upload(file, pre)); - } - @Log(title = "OSS对象存储", businessType = BusinessType.INSERT) - @PostMapping(value = "/uploadImg", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public R uploadImg(@RequestPart("file") MultipartFile file, String pre) { - if (ObjectUtil.isNull(file)) { - throw new ServiceException("文件为空"); - } - if(!file.getContentType().startsWith("image/")){ - throw new ServiceException("不是图片"); - } - return R.ok(iSysOssService.uploadImgs(file, pre)); + @Log(title = "OSS对象存储", businessType = BusinessType.INSERT) + @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R upload(@RequestPart("file") MultipartFile file, String pre) { + if (ObjectUtil.isNull(file)) { + throw new ServiceException("文件为空"); } +// return R.ok(iSysOssService.upload(file, pre)); + return R.ok(fileService.setPrefix(pre).save(file));//TODO: fileService + } - /** - * 下载OSS对象 - * - * @param ossId OSS对象ID - */ - @PostMapping("/download/{ossId}") - public void download(@PathVariable Long ossId, HttpServletResponse response) throws IOException { - iSysOssService.download(ossId, response); + @Log(title = "OSS对象存储", businessType = BusinessType.INSERT) + @PostMapping(value = "/uploadImg", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R uploadImg(@RequestPart("file") MultipartFile file, String pre) { + if (ObjectUtil.isNull(file)) { + throw new ServiceException("文件为空"); + } + if (!file.getContentType().startsWith("image/")) { + throw new ServiceException("不是图片"); } +// return R.ok(iSysOssService.uploadImgs(file, pre)); + + return R.ok(fileService.setPrefix(pre).setThumbnail().saveImage(file));//TODO: fileService + } + + + /** + * 下载 + * + * @param url + * @param request + * @param response + * @throws IOException + */ + @PostMapping("/download") + public ModelAndView download(String url, HttpServletRequest request, HttpServletResponse response) { +// iSysOssService.download(ossId, response); + fileService.download(url, Duration.ofMinutes(30), request, response);//TODO: fileService + return null; + } } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysOssConfigController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysOssConfigController.java deleted file mode 100644 index 8dc4876..0000000 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysOssConfigController.java +++ /dev/null @@ -1,105 +0,0 @@ -package com.ruoyi.web.controller.system; - -import cn.dev33.satoken.annotation.SaCheckPermission; -import com.ruoyi.common.annotation.Log; -import com.ruoyi.common.annotation.RepeatSubmit; -import com.ruoyi.common.core.controller.BaseController; -import com.ruoyi.common.core.domain.PageQuery; -import com.ruoyi.common.core.domain.R; -import com.ruoyi.common.core.page.TableDataInfo; -import com.ruoyi.common.core.validate.AddGroup; -import com.ruoyi.common.core.validate.EditGroup; -import com.ruoyi.common.core.validate.QueryGroup; -import com.ruoyi.common.enums.BusinessType; -import com.ruoyi.system.domain.bo.SysOssConfigBo; -import com.ruoyi.system.domain.vo.SysOssConfigVo; -import com.ruoyi.system.service.ISysOssConfigService; -import lombok.RequiredArgsConstructor; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import java.util.Arrays; - -/** - * 对象存储配置 - * - * @author Lion Li - * @author 孤舟烟雨 - * @date 2021-08-13 - */ -@Validated -@RequiredArgsConstructor -@RestController -@RequestMapping("/system/oss/config") -public class SysOssConfigController extends BaseController { - - private final ISysOssConfigService iSysOssConfigService; - - /** - * 查询对象存储配置列表 - */ - @SaCheckPermission("system:oss:list") - @GetMapping("/list") - public TableDataInfo list(@Validated(QueryGroup.class) SysOssConfigBo bo, PageQuery pageQuery) { - return iSysOssConfigService.queryPageList(bo, pageQuery); - } - - /** - * 获取对象存储配置详细信息 - * - * @param ossConfigId OSS配置ID - */ - @SaCheckPermission("system:oss:query") - @GetMapping("/{ossConfigId}") - public R getInfo(@NotNull(message = "主键不能为空") - @PathVariable Long ossConfigId) { - return R.ok(iSysOssConfigService.queryById(ossConfigId)); - } - - /** - * 新增对象存储配置 - */ - @SaCheckPermission("system:oss:add") - @Log(title = "对象存储配置", businessType = BusinessType.INSERT) - @RepeatSubmit() - @PostMapping() - public R add(@Validated(AddGroup.class) @RequestBody SysOssConfigBo bo) { - return toAjax(iSysOssConfigService.insertByBo(bo)); - } - - /** - * 修改对象存储配置 - */ - @SaCheckPermission("system:oss:edit") - @Log(title = "对象存储配置", businessType = BusinessType.UPDATE) - @RepeatSubmit() - @PutMapping() - public R edit(@Validated(EditGroup.class) @RequestBody SysOssConfigBo bo) { - return toAjax(iSysOssConfigService.updateByBo(bo)); - } - - /** - * 删除对象存储配置 - * - * @param ossConfigIds OSS配置ID串 - */ - @SaCheckPermission("system:oss:remove") - @Log(title = "对象存储配置", businessType = BusinessType.DELETE) - @DeleteMapping("/{ossConfigIds}") - public R remove(@NotEmpty(message = "主键不能为空") - @PathVariable Long[] ossConfigIds) { - return toAjax(iSysOssConfigService.deleteWithValidByIds(Arrays.asList(ossConfigIds), true)); - } - - /** - * 状态修改 - */ - @SaCheckPermission("system:oss:edit") - @Log(title = "对象存储状态修改", businessType = BusinessType.UPDATE) - @PutMapping("/changeStatus") - public R changeStatus(@RequestBody SysOssConfigBo bo) { - return toAjax(iSysOssConfigService.updateOssConfigStatus(bo)); - } -} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysOssController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysOssController.java deleted file mode 100644 index 606895a..0000000 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysOssController.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.ruoyi.web.controller.system; - - -import cn.dev33.satoken.annotation.SaCheckPermission; -import cn.hutool.core.convert.Convert; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.http.HttpException; -import cn.hutool.http.HttpUtil; -import com.ruoyi.common.annotation.Log; -import com.ruoyi.common.core.controller.BaseController; -import com.ruoyi.common.core.domain.PageQuery; -import com.ruoyi.common.core.domain.R; -import com.ruoyi.common.core.page.TableDataInfo; -import com.ruoyi.common.core.validate.QueryGroup; -import com.ruoyi.common.enums.BusinessType; -import com.ruoyi.common.exception.ServiceException; -import com.ruoyi.common.utils.file.FileUtils; -import com.ruoyi.oss.core.OssClient; -import com.ruoyi.oss.factory.OssFactory; -import com.ruoyi.system.domain.SysOss; -import com.ruoyi.system.domain.bo.SysOssBo; -import com.ruoyi.system.domain.vo.SysOssVo; -import com.ruoyi.system.service.ISysOssService; -import lombok.RequiredArgsConstructor; -import org.springframework.http.MediaType; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; - -import javax.servlet.http.HttpServletResponse; -import javax.validation.constraints.NotEmpty; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * 文件上传 控制层 - * - * @author Lion Li - */ -@Validated -@RequiredArgsConstructor -@RestController -@RequestMapping("/system/oss") -public class SysOssController extends BaseController { - - private final ISysOssService iSysOssService; - - /** - * 查询OSS对象存储列表 - */ - @SaCheckPermission("system:oss:list") - @GetMapping("/list") - public TableDataInfo list(@Validated(QueryGroup.class) SysOssBo bo, PageQuery pageQuery) { - return iSysOssService.queryPageList(bo, pageQuery); - } - - /** - * 查询OSS对象基于id串 - * - * @param ossIds OSS对象ID串 - */ - @SaCheckPermission("system:oss:list") - @GetMapping("/listByIds/{ossIds}") - public R> listByIds(@NotEmpty(message = "主键不能为空") - @PathVariable Long[] ossIds) { - List list = iSysOssService.listByIds(Arrays.asList(ossIds)); - return R.ok(list); - } - - /** - * 上传OSS对象存储 - * - * @param file 文件 - */ - @SaCheckPermission("system:oss:upload") - @Log(title = "OSS对象存储", businessType = BusinessType.INSERT) - @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public R> upload(@RequestPart("file") MultipartFile file) { - if (ObjectUtil.isNull(file)) { - throw new ServiceException("上传文件不能为空"); - } - SysOssVo oss = iSysOssService.upload(file); - Map map = new HashMap<>(2); - map.put("url", oss.getUrl()); - map.put("fileName", oss.getOriginalName()); - map.put("ossId", oss.getOssId().toString()); - return R.ok(map); - } - - /** - * 下载OSS对象 - * - * @param ossId OSS对象ID - */ - @SaCheckPermission("system:oss:download") - @GetMapping("/download/{ossId}") - public void download(@PathVariable Long ossId, HttpServletResponse response) throws IOException { - iSysOssService.download(ossId,response); - } - - /** - * 删除OSS对象存储 - * - * @param ossIds OSS对象ID串 - */ - @SaCheckPermission("system:oss:remove") - @Log(title = "OSS对象存储", businessType = BusinessType.DELETE) - @DeleteMapping("/{ossIds}") - public R remove(@NotEmpty(message = "主键不能为空") - @PathVariable Long[] ossIds) { - return toAjax(iSysOssService.deleteWithValidByIds(Arrays.asList(ossIds), true)); - } - -} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java index 4aa7d5a..a7c0151 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysProfileController.java @@ -5,7 +5,6 @@ import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.StrUtil; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.annotation.RepeatSubmit; -import com.ruoyi.common.constant.UserConstants; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.R; import com.ruoyi.common.core.domain.entity.SysUser; @@ -13,10 +12,7 @@ import com.ruoyi.common.enums.BusinessType; import com.ruoyi.common.helper.LoginHelper; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.file.MimeTypeUtils; -import com.ruoyi.system.domain.SysOss; -import com.ruoyi.system.domain.vo.SysOssVo; -import com.ruoyi.system.service.FileService; -import com.ruoyi.system.service.ISysOssService; +import com.ruoyi.file.FileService; import com.ruoyi.system.service.ISysUserService; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; @@ -40,122 +36,125 @@ import java.util.Map; @RequestMapping("/system/user/profile") public class SysProfileController extends BaseController { - private final ISysUserService userService; - private final ISysOssService iSysOssService; - - - /** - * 个人信息 - */ - @GetMapping - public R> profile() { - SysUser user = userService.selectUserById(getUserId()); - Map ajax = new HashMap<>(); - ajax.put("user", user); - ajax.put("roleGroup", userService.selectUserRoleGroup(user.getUserName())); - ajax.put("postGroup", userService.selectUserPostGroup(user.getUserName())); - return R.ok(ajax); + private final ISysUserService userService; + + private final FileService fileService; + + + /** + * 个人信息 + */ + @GetMapping + public R> profile() { + SysUser user = userService.selectUserById(getUserId()); + Map ajax = new HashMap<>(); + ajax.put("user", user); + ajax.put("roleGroup", userService.selectUserRoleGroup(user.getUserName())); + ajax.put("postGroup", userService.selectUserPostGroup(user.getUserName())); + return R.ok(ajax); + } + + /** + * 修改用户 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping + public R updateProfile(@RequestBody SysUser user) { + if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) { + return R.fail("手机已存在"); } - - /** - * 修改用户 - */ - @Log(title = "个人信息", businessType = BusinessType.UPDATE) - @PutMapping - public R updateProfile(@RequestBody SysUser user) { - if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) { - return R.fail("手机已存在"); - } - if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) { - return R.fail("邮箱已存在"); - } - user.setUserId(getUserId()); - user.setUserName(null); - user.setPassword(null); - user.setAvatar(null); - user.setDeptId(null); - if (userService.updateUserProfile(user) > 0) { - return R.ok(); - } - return R.fail("修改个人异常"); + if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user)) { + return R.fail("邮箱已存在"); + } + user.setUserId(getUserId()); + user.setUserName(null); + user.setPassword(null); + user.setAvatar(null); + user.setDeptId(null); + if (userService.updateUserProfile(user) > 0) { + return R.ok(); } - @Log(title = "绑定用户名", businessType = BusinessType.UPDATE) - @RepeatSubmit - @PutMapping("bindUserName") - public R bindUserName(String userName) { - SysUser u = new SysUser(); - u.setUserId(getUserId()); - u.setUserName(userName); - String oldUsername = userService.selectUserById(u.getUserId()).getUserName(); - if(!(StrUtil.isBlank(oldUsername) || oldUsername.startsWith("_"))) { - return R.fail("只能绑定一次"); - } - - if(!userService.checkUserNameUnique(u)){ - return R.fail("账户已存在"); - } - - - if (userService.updateUserProfile(u) > 0) { - return R.ok(); - } - return R.fail("绑定帐号异常"); + return R.fail("修改个人异常"); + } + + @Log(title = "绑定用户名", businessType = BusinessType.UPDATE) + @RepeatSubmit + @PutMapping("bindUserName") + public R bindUserName(String userName) { + SysUser u = new SysUser(); + u.setUserId(getUserId()); + u.setUserName(userName); + String oldUsername = userService.selectUserById(u.getUserId()).getUserName(); + if (!(StrUtil.isBlank(oldUsername) || oldUsername.startsWith("_"))) { + return R.fail("只能绑定一次"); } + if (!userService.checkUserNameUnique(u)) { + return R.fail("账户已存在"); + } - /** - * 重置密码 - * - * @param newPassword 旧密码 - * @param oldPassword 新密码 - */ - @Log(title = "个人信息", businessType = BusinessType.UPDATE) - @PutMapping("/updatePwd") - public R updatePwd(String oldPassword, String newPassword) { - SysUser user = userService.selectUserById(LoginHelper.getUserId()); - String userName = user.getUserName(); - String password = user.getPassword(); - if (StrUtil.isNotBlank(password) && !BCrypt.checkpw(oldPassword, password)) { - return R.fail("旧密码错误"); - } - if (BCrypt.checkpw(newPassword, password)) { - return R.fail("新旧密码相同"); - } - - if (userService.resetUserPwd(userName, BCrypt.hashpw(newPassword)) > 0) { - return R.ok(); - } - return R.fail("修改密码异常"); + if (userService.updateUserProfile(u) > 0) { + return R.ok(); + } + return R.fail("绑定帐号异常"); + } + + + /** + * 重置密码 + * + * @param newPassword 旧密码 + * @param oldPassword 新密码 + */ + @Log(title = "个人信息", businessType = BusinessType.UPDATE) + @PutMapping("/updatePwd") + public R updatePwd(String oldPassword, String newPassword) { + SysUser user = userService.selectUserById(LoginHelper.getUserId()); + String userName = user.getUserName(); + String password = user.getPassword(); + if (StrUtil.isNotBlank(password) && !BCrypt.checkpw(oldPassword, password)) { + return R.fail("旧密码错误"); + } + if (BCrypt.checkpw(newPassword, password)) { + return R.fail("新旧密码相同"); } - /** - * 头像上传 - * - * @param avatarfile 用户头像 - */ - @Log(title = "用户头像", businessType = BusinessType.UPDATE) - @PostMapping(value = "/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public R> avatar(@RequestPart("avatarfile") MultipartFile avatarfile) throws IOException { - Map ajax = new HashMap<>(); - if (!avatarfile.isEmpty()) { - String extension = FileUtil.extName(avatarfile.getOriginalFilename()); - if(StrUtil.isBlank(extension)){//NOTE: 修复h5上传问题 - extension="png"; - } - if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) { - return R.fail("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式"); - } - - SysOssVo oss = iSysOssService.uploadImgs(avatarfile,"avatar",400,400,null); - String avatar = oss.getUrl(); - - - if (userService.updateUserAvatar(getUsername(), avatar)) { - ajax.put("imgUrl", avatar); - return R.ok(ajax); - } - } - return R.fail("上传图片异常,请联系管理员"); + if (userService.resetUserPwd(userName, BCrypt.hashpw(newPassword)) > 0) { + return R.ok(); + } + return R.fail("修改密码异常"); + } + + /** + * 头像上传 + * + * @param avatarfile 用户头像 + */ + @Log(title = "用户头像", businessType = BusinessType.UPDATE) + @PostMapping(value = "/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R> avatar(@RequestPart("avatarfile") MultipartFile avatarfile) throws IOException { + Map ajax = new HashMap<>(); + if (!avatarfile.isEmpty()) { + String extension = FileUtil.extName(avatarfile.getOriginalFilename()); + if (StrUtil.isBlank(extension)) {//NOTE: 修复h5上传问题 + extension = "png"; + } + if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) { + return R.fail("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式"); + } + +// SysOssVo oss = iSysOssService.uploadImgs(avatarfile,"avatar",400,400,null); +// String avatar = oss.getUrl(); + + String avatar = fileService.setSize(400, 400).saveImage(avatarfile);//TODO: fileService + + + if (userService.updateUserAvatar(getUsername(), avatar)) { + ajax.put("imgUrl", avatar); + return R.ok(ajax); + } } + return R.fail("上传图片异常,请联系管理员"); + } } diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 2132d60..34bb80b 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -3,12 +3,43 @@ ruoyi: # 是否是开发模式 dev: true - # 本地文件存储配置 - upload: - # 资源访问前缀 - pre: /upload - # 物理保存地址 - save-path: /.data/upload + + # 文件存储配置 + file: + max-width: 1500 + max-height: 1500 + th-width: 200 + th-height: 200 + watermark: classpath:/watermark.png + default-platform: minio #默认使用的存储平台 + local-plus: + - platform: local # 存储平台标识 + enable-storage: true #启用存储 + enable-access: true #启用访问(线上请使用 Nginx 配置,效率更高) + path-patterns: /upload/** # 访问路径 + storage-path: /upload/ # 存储路径 + domain: "/upload/" # 访问域名,例如:“http://127.0.0.1:8030/file/”,注意后面要和 path-patterns 保持一致,“/”结尾,本地存储建议使用相对路径,方便后期更换域名 + base-path: "" # 基础路径 + minio: + - platform: minio + enable-storage: true # 启用存储 + access-key: ${ruoyi.name} + secret-key: ${ruoyi.name}1415926 + end-point: http://192.168.3.222:9000 + bucket-name: files + domain: "/files/" # 访问域名,注意“/”结尾,例如:http://minio.abc.com/abc/ + base-path: "" # 基础路径 + aliyun-oss: + - platform: aliyun # 存储平台标识 + enable-storage: true # 启用存储 + access-key: LTAI5tKkFMwc4SuDF8LpgRQ3 + secret-key: 74S18FfuyxTd85iYKifsVXjY5DhVAB + end-point: https://oss-cn-shenzhen.aliyuncs.com + bucket-name: base-2024 + domain: "https://base-2024.oss-cn-shenzhen.aliyuncs.com/" # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/ + base-path: "" # 基础路径 + + logging: level: diff --git a/ruoyi-admin/src/main/resources/application-local.yml.template b/ruoyi-admin/src/main/resources/application-local.yml.template index 2132d60..34bb80b 100644 --- a/ruoyi-admin/src/main/resources/application-local.yml.template +++ b/ruoyi-admin/src/main/resources/application-local.yml.template @@ -3,12 +3,43 @@ ruoyi: # 是否是开发模式 dev: true - # 本地文件存储配置 - upload: - # 资源访问前缀 - pre: /upload - # 物理保存地址 - save-path: /.data/upload + + # 文件存储配置 + file: + max-width: 1500 + max-height: 1500 + th-width: 200 + th-height: 200 + watermark: classpath:/watermark.png + default-platform: minio #默认使用的存储平台 + local-plus: + - platform: local # 存储平台标识 + enable-storage: true #启用存储 + enable-access: true #启用访问(线上请使用 Nginx 配置,效率更高) + path-patterns: /upload/** # 访问路径 + storage-path: /upload/ # 存储路径 + domain: "/upload/" # 访问域名,例如:“http://127.0.0.1:8030/file/”,注意后面要和 path-patterns 保持一致,“/”结尾,本地存储建议使用相对路径,方便后期更换域名 + base-path: "" # 基础路径 + minio: + - platform: minio + enable-storage: true # 启用存储 + access-key: ${ruoyi.name} + secret-key: ${ruoyi.name}1415926 + end-point: http://192.168.3.222:9000 + bucket-name: files + domain: "/files/" # 访问域名,注意“/”结尾,例如:http://minio.abc.com/abc/ + base-path: "" # 基础路径 + aliyun-oss: + - platform: aliyun # 存储平台标识 + enable-storage: true # 启用存储 + access-key: LTAI5tKkFMwc4SuDF8LpgRQ3 + secret-key: 74S18FfuyxTd85iYKifsVXjY5DhVAB + end-point: https://oss-cn-shenzhen.aliyuncs.com + bucket-name: base-2024 + domain: "https://base-2024.oss-cn-shenzhen.aliyuncs.com/" # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/ + base-path: "" # 基础路径 + + logging: level: diff --git a/ruoyi-admin/src/main/resources/application-prod.yml b/ruoyi-admin/src/main/resources/application-prod.yml index b766f71..a40cc6e 100644 --- a/ruoyi-admin/src/main/resources/application-prod.yml +++ b/ruoyi-admin/src/main/resources/application-prod.yml @@ -3,12 +3,42 @@ ruoyi: # 是否是开发模式 dev: false - # 本地文件存储配置 - upload: - # 资源访问前缀 - pre: /upload - # 物理保存地址 - save-path: /server/upload + + + # 文件存储配置 + file: + max-width: 1500 + max-height: 1500 + th-width: 200 + th-height: 200 + watermark: classpath:/watermark.png + default-platform: minio #默认使用的存储平台 + local-plus: + - platform: local # 存储平台标识 + enable-storage: true #启用存储 + enable-access: true #启用访问(线上请使用 Nginx 配置,效率更高) + path-patterns: /upload/** # 访问路径 + storage-path: /server/upload/ # 存储路径 + domain: "/upload/" # 访问域名,例如:“http://127.0.0.1:8030/file/”,注意后面要和 path-patterns 保持一致,“/”结尾,本地存储建议使用相对路径,方便后期更换域名 + base-path: "" # 基础路径 + minio: + - platform: minio + enable-storage: true # 启用存储 + access-key: ${ruoyi.name} + secret-key: ${ruoyi.name}1415926 + end-point: http://minio:9000 + bucket-name: files + domain: "/files/" # 访问域名,注意“/”结尾,例如:http://minio.abc.com/abc/ + base-path: "" # 基础路径 + aliyun-oss: + - platform: aliyun # 存储平台标识 + enable-storage: true # 启用存储 + access-key: XXXXXXXXXXXXXXXXXXXXXX + secret-key: XXXXXXXXXXXXXXXXXXXXXXXXXXXX + end-point: https://oss-cn-shenzhen.aliyuncs.com + bucket-name: base2024 + domain: "https://base2024.oss-cn-shenzhen.aliyuncs.com/" # 访问域名,注意“/”结尾,例如:https://abc.oss-cn-shanghai.aliyuncs.com/ + base-path: "" # 基础路径 --- # 临时文件存储位置 避免临时文件被系统清理报错 diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index f778ffb..cc644c7 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -14,12 +14,6 @@ ruoyi: addressEnabled: true # 缓存懒加载 cacheLazy: false - # 本地文件存储配置 - upload: - # 资源访问前缀 - pre: /upload - # 物理保存地址 - save-path: /upload # 小程序的用户默认设置 default-user: # 所在单位 @@ -34,6 +28,9 @@ ruoyi: # 岗位组 post-ids: - 4 + - + + --- # 验证码配置 diff --git a/ruoyi-admin/src/test/java/com/ruoyi/test/PasswordTest.java b/ruoyi-admin/src/test/java/com/ruoyi/test/PasswordTest.java index def913f..6495f20 100644 --- a/ruoyi-admin/src/test/java/com/ruoyi/test/PasswordTest.java +++ b/ruoyi-admin/src/test/java/com/ruoyi/test/PasswordTest.java @@ -1,9 +1,12 @@ package com.ruoyi.test; import cn.dev33.satoken.secure.BCrypt; +import cn.hutool.core.util.URLUtil; +import lombok.SneakyThrows; import org.junit.jupiter.api.Test; import java.io.File; +import java.net.URL; public class PasswordTest { @@ -16,4 +19,10 @@ public class PasswordTest { public void abc() { new File("E:\\upload\\2023\\7\\12\\75s0.jpg").delete(); } + + @Test + public void urlEncode() { + System.out.println(URLUtil.encode("/files/default/2024/10/24/553vijcx66we/长沙销售部.txt")); + } + } diff --git a/ruoyi-admin/src/test/java/com/ruoyi/test/XFileStorageTest.java b/ruoyi-admin/src/test/java/com/ruoyi/test/XFileStorageTest.java new file mode 100644 index 0000000..9360921 --- /dev/null +++ b/ruoyi-admin/src/test/java/com/ruoyi/test/XFileStorageTest.java @@ -0,0 +1,201 @@ +package com.ruoyi.test; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.HexUtil; +import com.ruoyi.TestSuper; +import com.ruoyi.file.FileService; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import org.dromara.x.file.storage.core.FileInfo; +import org.dromara.x.file.storage.core.FileStorageService; +import org.dromara.x.file.storage.core.tika.TikaFactory; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.time.Duration; +import java.time.LocalDate; + +@NoArgsConstructor +public class XFileStorageTest extends TestSuper { + + @Autowired + private FileStorageService fileStorageService;//注入实列 + + @Autowired + private FileService fileService; + + @Autowired + private TikaFactory tikaFactory; + + + public InputStream getInstream() { + return this.getClass().getResourceAsStream("/test.png"); + } + + @Test + @DisplayName("分片上传") + @Disabled + @SneakyThrows + public void testMultipartUpload() { + File file = new File("D:\\test.mp4"); + String uploadId = fileService.setPlatform("aliyun").setUri("/test.mp4").multipartUploadInit(); + try ( + InputStream in = new FileInputStream(file); + ) { + byte[] bs = new byte[5 * 1024 * 1024];//每一片5MB + int len = 0; + int partNumber = 0; + try { + while ((len = in.read(bs)) > 0) { + partNumber++; + ByteArrayInputStream bin = new ByteArrayInputStream(bs, 0, len); + fileService.multipartUpload(uploadId, partNumber, bin); + out("上传分片成功:" + partNumber + " len:" + len); + + } + out("分片上传成功:" + fileService.multipartUploadComplete(uploadId)); + } catch (Exception e) { + out("分片上传失败", e); + fileService.multipartUploadAbort(uploadId); + } + + } + } + + + @Test + @DisplayName("分片上传") + @Disabled + @SneakyThrows + public void testMultipartUpload1() { + File file = new File("D:\\test.mp4");//文件大概6M +// File file = new File("D:\\VMware-images\\CentOS-7-x86_64-Minimal-2009.iso");;//900M+ + FileInfo fileInfo = fileStorageService.initiateMultipartUpload().setPlatform("aliyun").setPath("default/").setSaveFilename(file.getName()).init(); + try ( + InputStream in = new FileInputStream(file); + ) { + byte[] bs = new byte[5 * 1024 * 1024];//每一片5MB + int len = 0; + int partNumber = 1; + try { + while ((len = in.read(bs)) > 0) { + partNumber++; + ByteArrayInputStream bin = new ByteArrayInputStream(bs, 0, len); + fileStorageService.uploadPart(fileInfo, partNumber, bin).upload(); + out("上传分片成功:" + partNumber + " len:" + len); + + } + out("分片上传成功:" + fileStorageService.completeMultipartUpload(fileInfo).complete().getUrl()); + } catch (Exception e) { + out("分片上传失败", e); + fileStorageService.abortMultipartUpload(fileInfo).abort(); + } + } + } + + + @Test + @Disabled + @DisplayName("读取配置信息") + public void testConfig() throws Exception { + out(fileStorageService.getProperties()); + } + + @Test + @Disabled + @DisplayName("保存文件") + @SneakyThrows + public void saveTest() { +// out("保存随机文件名:" + fileService.setFilename("test.png").save(getInstream())); +// out("保存文件名:" + fileService.setKeepFilename().setFilename("test.png").save(getInstream())); +// out("指定保存规则:" + fileService.setFilename("test.png").setRule("/{yyyy}/{MM}/{dd}/{id}.{ext}").save(getInstream())); + out("指定保存路径:" + fileService.setUri("/aa/bb/cc/a.png").save(getInstream())); + out("aliyun指定保存路径:" + fileService.setPlatform("aliyun").setUri("/aa/bb/cc/a.png").save(getInstream())); + out("local指定保存路径:" + fileService.setPlatform("local").setUri("/aa/bb/cc/a.png").save(getInstream())); +// out("指定路径前缀保存随机文件名:" + fileService.setPrefix("test").setFilename("test.png").save(getInstream())); +// out("指定平台保存随机文件名:" + fileService.setPlatform("local").setFilename("test.png").save(getInstream())); + + } + + @Test + @Disabled + @DisplayName("保存图片") + @SneakyThrows + public void saveImage() { +// out("保存图片:" + fileService.saveImage(getInstream())); +// out("保存图片+缩略图:" + fileService.setThumbnail().saveImage(getInstream())); +// out("保存图片+调整大小+缩略图:" + fileService.setSize(500,500).setThumbnail().saveImage(getInstream())); +// out("保存图片+水印+调整大小+缩略图:" + fileService.setPlatform("aliyun").setWatermark().setSize(700, 700).setThumbnail().saveImage(getInstream())); + out("保存图片+水印+调整大小+缩略图+源文件:" + fileService.setPlatform("local").setFilename("test.png").setSaveSrc().setWatermark().setSize(700, 700).setThumbnail().saveImage(getInstream())); + } + + + private String uri = "/files/default/aa/bb/cc/a.png"; + + private String aliyun = "https://base-2024.oss-cn-shenzhen.aliyuncs.com/default/aa/bb/cc/a.png"; + + + @Test + @Disabled + @DisplayName("删除文件") + @SneakyThrows + public void deleteTest() { +// fileService.delete(uri); +// fileService.setPlatform("local").delete("/upload/default/aa/bb/cc/a.png"); + fileService.setPlatform("aliyun").delete(aliyun); + } + + @Test + @Disabled + @DisplayName("下载文件") + @SneakyThrows + public void downloadTest() { +// FileInfo fileInfo = new FileInfo(); +// fileInfo.setPath("default/aa/bb/cc/"); +// fileInfo.setFilename("a.png"); +// fileInfo.setPlatform(fileStorageService.getFileStorage("aliyun").getPlatform()); +// out(HexUtil.encodeHex(fileStorageService.download(fileInfo).bytes())); +// out(HexUtil.encodeHex(fileService.download(uri).bytes())); + out(HexUtil.encodeHex(fileService.setPlatform("aliyun").download(aliyun).bytes())); + } + + @Test + @Disabled + @DisplayName("生成预签名URL") + @SneakyThrows + public void generatePresignedUrlTest() { + out(fileService.generatePresignedUrl(uri, Duration.ofMinutes(30))); + out(fileService.setPlatform("aliyun").generatePresignedUrl(aliyun, Duration.ofMinutes(30))); + try { + out(fileService.setPlatform("local").generatePresignedUrl(uri, Duration.ofMinutes(30))); + } catch (Exception e) { + out("不支持预签名", e); + } + } + + @Test + @Disabled + @DisplayName("类型检测") + @SneakyThrows + public void contentTypeTest() { + System.out.println(tikaFactory.getTika().detect(getInstream())); + } + + @Test + @Disabled + @DisplayName("转换格式") + @SneakyThrows + public void testWebp() { + BufferedImage image = ImageIO.read(getInstream()); + ImageIO.write(image, "webp", new File("/test.webp")); + } +} diff --git a/ruoyi-admin/src/test/resources/test.png b/ruoyi-admin/src/test/resources/test.png new file mode 100644 index 0000000000000000000000000000000000000000..32e6b0cb6e68a357ac3399bbb8e150f5f2517b3a GIT binary patch literal 83176 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%0D9fJo9a|sunnNBg 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

|@FJRcc9IH=a8? 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 literal 0 HcmV?d00001 diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java index bbd984c..495546b 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/config/RuoYiConfig.java @@ -1,6 +1,5 @@ package com.ruoyi.common.config; -import com.baomidou.mybatisplus.annotation.TableField; import com.ruoyi.common.enums.UserStatus; import com.ruoyi.common.enums.UserType; import lombok.Data; @@ -16,97 +15,68 @@ import org.springframework.stereotype.Component; @Data @Component -@ConfigurationProperties(prefix = "ruoyi") +@ConfigurationProperties(prefix = "ruoyi", ignoreInvalidFields = true) public class RuoYiConfig { + /** + * 开发模式 + */ + private Boolean dev = false; + /** + * 项目名称 + */ + private String name; + + /** + * 版本 + */ + private String version; + + /** + * 版权年份 + */ + private String copyrightYear; + + /** + * 实例演示开关 + */ + private boolean demoEnabled; + + /** + * 缓存懒加载 + */ + private boolean cacheLazy; + + /** + * 获取地址开关 + */ + @Getter + private static boolean addressEnabled; + + public void setAddressEnabled(boolean addressEnabled) { + RuoYiConfig.addressEnabled = addressEnabled; + } + + private DefaultUser defaultUser = new DefaultUser(); + + @Data + public static class DefaultUser { + private Long deptId = 100L; + + private String userType = UserType.APP_USER.getUserType(); /** - * 开发模式 + * 帐号状态(0正常 1停用) */ - private Boolean dev=false; + private String status = UserStatus.OK.getCode(); /** - * 项目名称 + * 角色组 */ - private String name; + private Long[] roleIds = {2L}; /** - * 版本 + * 岗位组 */ - private String version; + private Long[] postIds = {4L}; - /** - * 版权年份 - */ - private String copyrightYear; - - /** - * 实例演示开关 - */ - private boolean demoEnabled; - - /** - * 缓存懒加载 - */ - private boolean cacheLazy; - - /** - * 获取地址开关 - */ - @Getter - private static boolean addressEnabled; - - public void setAddressEnabled(boolean addressEnabled) { - RuoYiConfig.addressEnabled = addressEnabled; - } - - private DefaultUser defaultUser = new DefaultUser(); - - private Tencentcloud tencentcloud = new Tencentcloud(); - - @Data - public static class DefaultUser { - private Long deptId = 100L; - - private String userType = UserType.APP_USER.getUserType(); - /** - * 帐号状态(0正常 1停用) - */ - private String status = UserStatus.OK.getCode(); - /** - * 角色组 - */ - private Long[] roleIds = {2L}; - - /** - * 岗位组 - */ - private Long[] postIds = {4L}; - - } - - - public Upload upload = new Upload(); - - /** - * 本地文件存储配置 - */ - @Data - public static class Upload { - - /** - * 资源地址前缀 - */ - public String pre = "/upload"; - - /** - * 保存位置 - */ - public String savePath="/upload"; - } + } - @Data - public static class Tencentcloud { - private String secretId = "AKIDoeWFoKdhaLuFLD1sX2LRItFMI2f7NRRh"; - private String secretKey = "RkspdHuOflngNgnhXRL4Zpq096pLhrmQ"; - private String ocrEndpoint = "ocr.ap-guangzhou.tencentcloudapi.com"; - private String ocrRegion = "ap-guangzhou"; - } } diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/OssUrlTranslationImpl.java b/ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/OssUrlTranslationImpl.java deleted file mode 100644 index 69ebd9a..0000000 --- a/ruoyi-common/src/main/java/com/ruoyi/common/translation/impl/OssUrlTranslationImpl.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.ruoyi.common.translation.impl; - -import com.ruoyi.common.annotation.TranslationType; -import com.ruoyi.common.constant.TransConstant; -import com.ruoyi.common.core.service.OssService; -import com.ruoyi.common.translation.TranslationInterface; -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Component; - -/** - * OSS翻译实现 - * - * @author Lion Li - */ -@Component -@AllArgsConstructor -@TranslationType(type = TransConstant.OSS_ID_TO_URL) -public class OssUrlTranslationImpl implements TranslationInterface { - - private final OssService ossService; - - public String translation(Object key, String other) { - return ossService.selectUrlByIds(key.toString()); - } -} diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java index 1ef2229..fda2462 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/file/FileUtils.java @@ -1,6 +1,8 @@ package com.ruoyi.common.utils.file; import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.UUID; +import com.ruoyi.common.utils.redis.RedisUtils; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -8,6 +10,7 @@ import javax.servlet.http.HttpServletResponse; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.Duration; /** * 文件处理工具类 @@ -17,36 +20,74 @@ import java.nio.charset.StandardCharsets; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class FileUtils extends FileUtil { - /** - * 下载文件名重新编码 - * - * @param response 响应对象 - * @param realFileName 真实文件名 - */ - public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException { - String percentEncodedFileName = percentEncode(realFileName); - - StringBuilder contentDispositionValue = new StringBuilder(); - contentDispositionValue.append("attachment; filename=") - .append(percentEncodedFileName) - .append(";") - .append("filename*=") - .append("utf-8''") - .append(percentEncodedFileName); - - response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename"); - response.setHeader("Content-disposition", contentDispositionValue.toString()); - response.setHeader("download-filename", percentEncodedFileName); - } - - /** - * 百分号编码工具方法 - * - * @param s 需要百分号编码的字符串 - * @return 百分号编码后的字符串 - */ - public static String percentEncode(String s) throws UnsupportedEncodingException { - String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); - return encode.replaceAll("\\+", "%20"); - } + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename"); + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } + + public static final String UPLOAD_KEY_PREFIX = "UPLOAD:KEY:"; + + /** + * 获取上传凭证 + * @param duration 有效期 + * @return + */ + public static String getUploadKey(Duration duration) { + String key = UUID.fastUUID().toString(true); + RedisUtils.setCacheObject(UPLOAD_KEY_PREFIX + key, true, duration); + return key; + } + + /** + * 获取上传凭证,有效期30分钟 + * @return + */ + public static String getUploadKey() { + return getUploadKey(Duration.ofMinutes(60)); + } + + /** + * 检查上传凭证是否存在 + * @param key + * @return + */ + public static boolean exitisUploadKey(String key){ + return RedisUtils.isExistsObject(UPLOAD_KEY_PREFIX + key); + } + + /** + * 删除上传凭证 + * @param key + */ + public static void removeUploadKey(String key){ + RedisUtils.deleteObject(UPLOAD_KEY_PREFIX + key); + } } diff --git a/ruoyi-oss/pom.xml b/ruoyi-oss/pom.xml deleted file mode 100644 index c2e1e99..0000000 --- a/ruoyi-oss/pom.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - ruoyi-vue-plus - com.ruoyi - 4.6.0 - - 4.0.0 - - ruoyi-oss - - - OSS对象存储模块 - - - - - - - com.ruoyi - ruoyi-common - - - - com.amazonaws - aws-java-sdk-s3 - - - - - diff --git a/ruoyi-oss/src/main/java/com/ruoyi/oss/constant/OssConstant.java b/ruoyi-oss/src/main/java/com/ruoyi/oss/constant/OssConstant.java deleted file mode 100644 index 06202d0..0000000 --- a/ruoyi-oss/src/main/java/com/ruoyi/oss/constant/OssConstant.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.ruoyi.oss.constant; - -import java.util.Arrays; -import java.util.List; - -/** - * 对象存储常量 - * - * @author Lion Li - */ -public interface OssConstant { - - /** - * 默认配置KEY - */ - String DEFAULT_CONFIG_KEY = "sys_oss:default_config"; - - /** - * 预览列表资源开关Key - */ - String PEREVIEW_LIST_RESOURCE_KEY = "sys.oss.previewListResource"; - - /** - * 系统数据ids - */ - List SYSTEM_DATA_IDS = Arrays.asList(1L, 2L, 3L, 4L); - - /** - * 云服务商 - */ - String[] CLOUD_SERVICE = new String[] {"aliyun", "qcloud", "qiniu", "obs"}; - - /** - * https 状态 - */ - String IS_HTTPS = "Y"; - -} diff --git a/ruoyi-oss/src/main/java/com/ruoyi/oss/core/OssClient.java b/ruoyi-oss/src/main/java/com/ruoyi/oss/core/OssClient.java deleted file mode 100644 index 76258be..0000000 --- a/ruoyi-oss/src/main/java/com/ruoyi/oss/core/OssClient.java +++ /dev/null @@ -1,268 +0,0 @@ -package com.ruoyi.oss.core; - -import cn.hutool.core.io.IoUtil; -import cn.hutool.core.util.IdUtil; -import com.amazonaws.ClientConfiguration; -import com.amazonaws.HttpMethod; -import com.amazonaws.Protocol; -import com.amazonaws.auth.AWSCredentials; -import com.amazonaws.auth.AWSCredentialsProvider; -import com.amazonaws.auth.AWSStaticCredentialsProvider; -import com.amazonaws.auth.BasicAWSCredentials; -import com.amazonaws.client.builder.AwsClientBuilder; -import com.amazonaws.services.s3.AmazonS3; -import com.amazonaws.services.s3.AmazonS3Client; -import com.amazonaws.services.s3.AmazonS3ClientBuilder; -import com.amazonaws.services.s3.model.*; -import com.ruoyi.common.utils.DateUtils; -import com.ruoyi.common.utils.StringUtils; -import com.ruoyi.oss.constant.OssConstant; -import com.ruoyi.oss.entity.UploadResult; -import com.ruoyi.oss.enumd.AccessPolicyType; -import com.ruoyi.oss.enumd.PolicyType; -import com.ruoyi.oss.exception.OssException; -import com.ruoyi.oss.properties.OssProperties; -import lombok.Getter; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.net.URL; -import java.util.Date; - -/** - * S3 存储协议 所有兼容S3协议的云厂商均支持 - * 阿里云 腾讯云 七牛云 minio - * - * @author Lion Li - */ -public class OssClient { - - private final String configKey; - - @Getter - private final OssProperties properties; - - private final AmazonS3 client; - - public OssClient(String configKey, OssProperties ossProperties) { - this.configKey = configKey; - this.properties = ossProperties; - try { - AwsClientBuilder.EndpointConfiguration endpointConfig = - new AwsClientBuilder.EndpointConfiguration(properties.getEndpoint(), properties.getRegion()); - - AWSCredentials credentials = new BasicAWSCredentials(properties.getAccessKey(), properties.getSecretKey()); - AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials); - ClientConfiguration clientConfig = new ClientConfiguration(); - if (OssConstant.IS_HTTPS.equals(properties.getIsHttps())) { - clientConfig.setProtocol(Protocol.HTTPS); - } else { - clientConfig.setProtocol(Protocol.HTTP); - } - AmazonS3ClientBuilder build = AmazonS3Client.builder() - .withEndpointConfiguration(endpointConfig) - .withClientConfiguration(clientConfig) - .withCredentials(credentialsProvider) - .disableChunkedEncoding(); - if (!StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE)) { - // minio 使用https限制使用域名访问 需要此配置 站点填域名 - build.enablePathStyleAccess(); - } - this.client = build.build(); - - createBucket(); - } catch (Exception e) { - if (e instanceof OssException) { - throw e; - } - throw new OssException("配置错误! 请检查系统配置:[" + e.getMessage() + "]"); - } - } - - public void createBucket() { - try { - String bucketName = properties.getBucketName(); - if (client.doesBucketExistV2(bucketName)) { - return; - } - CreateBucketRequest createBucketRequest = new CreateBucketRequest(bucketName); - AccessPolicyType accessPolicy = getAccessPolicy(); - createBucketRequest.setCannedAcl(accessPolicy.getAcl()); - client.createBucket(createBucketRequest); - client.setBucketPolicy(bucketName, getPolicy(bucketName, accessPolicy.getPolicyType())); - } catch (Exception e) { - throw new OssException("创建Bucket失败, 请核对配置信息:[" + e.getMessage() + "]"); - } - } - - public UploadResult upload(byte[] data, String path, String contentType) { - return upload(new ByteArrayInputStream(data), path, contentType); - } - - public UploadResult upload(InputStream inputStream, String path, String contentType) { - if (!(inputStream instanceof ByteArrayInputStream)) { - inputStream = new ByteArrayInputStream(IoUtil.readBytes(inputStream)); - } - try { - ObjectMetadata metadata = new ObjectMetadata(); - metadata.setContentType(contentType); - metadata.setContentLength(inputStream.available()); - PutObjectRequest putObjectRequest = new PutObjectRequest(properties.getBucketName(), path, inputStream, metadata); - // 设置上传对象的 Acl 为公共读 - putObjectRequest.setCannedAcl(getAccessPolicy().getAcl()); - client.putObject(putObjectRequest); - } catch (Exception e) { - throw new OssException("上传文件失败,请检查配置信息:[" + e.getMessage() + "]"); - } - return UploadResult.builder().url(getUrl() + "/" + path).filename(path).build(); - } - - public void delete(String path) { - path = path.replace(getUrl() + "/", ""); - try { - client.deleteObject(properties.getBucketName(), path); - } catch (Exception e) { - throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]"); - } - } - - public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) { - return upload(data, getPath(properties.getPrefix(), suffix), contentType); - } - - public UploadResult uploadSuffix(InputStream inputStream, String suffix, String contentType) { - return upload(inputStream, getPath(properties.getPrefix(), suffix), contentType); - } - - /** - * 获取文件元数据 - * - * @param path 完整文件路径 - */ - public ObjectMetadata getObjectMetadata(String path) { - path = path.replace(getUrl() + "/", ""); - S3Object object = client.getObject(properties.getBucketName(), path); - return object.getObjectMetadata(); - } - - public InputStream getObjectContent(String path) { - path = path.replace(getUrl() + "/", ""); - S3Object object = client.getObject(properties.getBucketName(), path); - return object.getObjectContent(); - } - - public String getUrl() { - String domain = properties.getDomain(); - String endpoint = properties.getEndpoint(); - String header = OssConstant.IS_HTTPS.equals(properties.getIsHttps()) ? "https://" : "http://"; - // 云服务商直接返回 - if (StringUtils.containsAny(endpoint, OssConstant.CLOUD_SERVICE)) { - if (StringUtils.isNotBlank(domain)) { - if(domain.contains("{root}")){ - return domain.replace("{root}", ""); - }else if(domain.contains("//")){ - return domain; - }else{ - return header + domain; - } - - } - return header + properties.getBucketName() + "." + endpoint; - } - // minio 单独处理 - if (StringUtils.isNotBlank(domain)) { - if(domain.equalsIgnoreCase("{root}")){ - return domain.replace("{root}", "")+ "/" + properties.getBucketName(); - }else if(domain.contains("//")){ - return domain + "/" + properties.getBucketName(); - }else{ - return header + domain + "/" + properties.getBucketName(); - } - } - return header + endpoint + "/" + properties.getBucketName(); - } - - public String getPath(String prefix, String suffix) { - // 生成uuid - String uuid = IdUtil.fastSimpleUUID(); - // 文件路径 - String path = DateUtils.datePath() + "/" + uuid; - if (StringUtils.isNotBlank(prefix)) { - path = prefix + "/" + path; - } - return path + suffix; - } - - - public String getConfigKey() { - return configKey; - } - - /** - * 获取私有URL链接 - * - * @param objectKey 对象KEY - * @param second 授权时间 - */ - public String getPrivateUrl(String objectKey, Integer second) { - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(properties.getBucketName(), objectKey) - .withMethod(HttpMethod.GET) - .withExpiration(new Date(System.currentTimeMillis() + 1000L * second)); - URL url = client.generatePresignedUrl(generatePresignedUrlRequest); - return url.toString(); - } - - /** - * 检查配置是否相同 - */ - public boolean checkPropertiesSame(OssProperties properties) { - return this.properties.equals(properties); - } - - /** - * 获取当前桶权限类型 - * - * @return 当前桶权限类型code - */ - public AccessPolicyType getAccessPolicy() { - return AccessPolicyType.getByType(properties.getAccessPolicy()); - } - - private static String getPolicy(String bucketName, PolicyType policyType) { - StringBuilder builder = new StringBuilder(); - builder.append("{\n\"Statement\": [\n{\n\"Action\": [\n"); - if (policyType == PolicyType.WRITE) { - builder.append("\"s3:GetBucketLocation\",\n\"s3:ListBucketMultipartUploads\"\n"); - } else if (policyType == PolicyType.READ_WRITE) { - builder.append("\"s3:GetBucketLocation\",\n\"s3:ListBucket\",\n\"s3:ListBucketMultipartUploads\"\n"); - } else { - builder.append("\"s3:GetBucketLocation\"\n"); - } - builder.append("],\n\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("\"\n},\n"); - if (policyType == PolicyType.READ) { - builder.append("{\n\"Action\": [\n\"s3:ListBucket\"\n],\n\"Effect\": \"Deny\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("\"\n},\n"); - } - builder.append("{\n\"Action\": "); - switch (policyType) { - case WRITE: - builder.append("[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n"); - break; - case READ_WRITE: - builder.append("[\n\"s3:AbortMultipartUpload\",\n\"s3:DeleteObject\",\n\"s3:GetObject\",\n\"s3:ListMultipartUploadParts\",\n\"s3:PutObject\"\n],\n"); - break; - default: - builder.append("\"s3:GetObject\",\n"); - break; - } - builder.append("\"Effect\": \"Allow\",\n\"Principal\": \"*\",\n\"Resource\": \"arn:aws:s3:::"); - builder.append(bucketName); - builder.append("/*\"\n}\n],\n\"Version\": \"2012-10-17\"\n}\n"); - return builder.toString(); - } - -} diff --git a/ruoyi-oss/src/main/java/com/ruoyi/oss/entity/UploadResult.java b/ruoyi-oss/src/main/java/com/ruoyi/oss/entity/UploadResult.java deleted file mode 100644 index 379d283..0000000 --- a/ruoyi-oss/src/main/java/com/ruoyi/oss/entity/UploadResult.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ruoyi.oss.entity; - -import lombok.Builder; -import lombok.Data; - -/** - * 上传返回体 - * - * @author Lion Li - */ -@Data -@Builder -public class UploadResult { - - /** - * 文件路径 - */ - private String url; - - /** - * 文件名 - */ - private String filename; -} diff --git a/ruoyi-oss/src/main/java/com/ruoyi/oss/enumd/AccessPolicyType.java b/ruoyi-oss/src/main/java/com/ruoyi/oss/enumd/AccessPolicyType.java deleted file mode 100644 index 1cae670..0000000 --- a/ruoyi-oss/src/main/java/com/ruoyi/oss/enumd/AccessPolicyType.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.ruoyi.oss.enumd; - -import com.amazonaws.services.s3.model.CannedAccessControlList; -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * 桶访问策略配置 - * - * @author 陈賝 - */ -@Getter -@AllArgsConstructor -public enum AccessPolicyType { - - /** - * private - */ - PRIVATE("0", CannedAccessControlList.Private, PolicyType.WRITE), - - /** - * public - */ - PUBLIC("1", CannedAccessControlList.PublicRead, PolicyType.READ), - - /** - * custom - */ - CUSTOM("2",CannedAccessControlList.PublicRead, PolicyType.READ); - - /** - * 桶 权限类型 - */ - private final String type; - - /** - * 文件对象 权限类型 - */ - private final CannedAccessControlList acl; - - /** - * 桶策略类型 - */ - private final PolicyType policyType; - - public static AccessPolicyType getByType(String type) { - for (AccessPolicyType value : values()) { - if (value.getType().equals(type)) { - return value; - } - } - throw new RuntimeException("'type' not found By " + type); - } - -} diff --git a/ruoyi-oss/src/main/java/com/ruoyi/oss/enumd/PolicyType.java b/ruoyi-oss/src/main/java/com/ruoyi/oss/enumd/PolicyType.java deleted file mode 100644 index 606f0f4..0000000 --- a/ruoyi-oss/src/main/java/com/ruoyi/oss/enumd/PolicyType.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.ruoyi.oss.enumd; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * minio策略配置 - * - * @author Lion Li - */ -@Getter -@AllArgsConstructor -public enum PolicyType { - - /** - * 只读 - */ - READ("read-only"), - - /** - * 只写 - */ - WRITE("write-only"), - - /** - * 读写 - */ - READ_WRITE("read-write"); - - /** - * 类型 - */ - private final String type; - -} diff --git a/ruoyi-oss/src/main/java/com/ruoyi/oss/exception/OssException.java b/ruoyi-oss/src/main/java/com/ruoyi/oss/exception/OssException.java deleted file mode 100644 index 540b1cc..0000000 --- a/ruoyi-oss/src/main/java/com/ruoyi/oss/exception/OssException.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.ruoyi.oss.exception; - -/** - * OSS异常类 - * - * @author Lion Li - */ -public class OssException extends RuntimeException { - - private static final long serialVersionUID = 1L; - - public OssException(String msg) { - super(msg); - } - -} diff --git a/ruoyi-oss/src/main/java/com/ruoyi/oss/factory/OssFactory.java b/ruoyi-oss/src/main/java/com/ruoyi/oss/factory/OssFactory.java deleted file mode 100644 index 78b5352..0000000 --- a/ruoyi-oss/src/main/java/com/ruoyi/oss/factory/OssFactory.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.ruoyi.oss.factory; - -import com.ruoyi.common.constant.CacheNames; -import com.ruoyi.common.utils.JsonUtils; -import com.ruoyi.common.utils.StringUtils; -import com.ruoyi.common.utils.redis.CacheUtils; -import com.ruoyi.common.utils.redis.RedisUtils; -import com.ruoyi.oss.constant.OssConstant; -import com.ruoyi.oss.core.OssClient; -import com.ruoyi.oss.exception.OssException; -import com.ruoyi.oss.properties.OssProperties; -import lombok.extern.slf4j.Slf4j; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 文件上传Factory - * - * @author Lion Li - */ -@Slf4j -public class OssFactory { - - private static final Map CLIENT_CACHE = new ConcurrentHashMap<>(); - - /** - * 获取默认实例 - */ - public static OssClient instance() { - // 获取redis 默认类型 - String configKey = RedisUtils.getCacheObject(OssConstant.DEFAULT_CONFIG_KEY); - if (StringUtils.isEmpty(configKey)) { - throw new OssException("文件存储服务类型无法找到!"); - } - return instance(configKey); - } - - /** - * 根据类型获取实例 - */ - public static OssClient instance(String configKey) { - String json = CacheUtils.get(CacheNames.SYS_OSS_CONFIG, configKey); - if (json == null) { - throw new OssException("系统异常, '" + configKey + "'配置信息不存在!"); - } - OssProperties properties = JsonUtils.parseObject(json, OssProperties.class); - OssClient client = CLIENT_CACHE.get(configKey); - if (client == null) { - CLIENT_CACHE.put(configKey, new OssClient(configKey, properties)); - log.info("创建OSS实例 key => {}", configKey); - return CLIENT_CACHE.get(configKey); - } - // 配置不相同则重新构建 - if (!client.checkPropertiesSame(properties)) { - CLIENT_CACHE.put(configKey, new OssClient(configKey, properties)); - log.info("重载OSS实例 key => {}", configKey); - return CLIENT_CACHE.get(configKey); - } - return client; - } - -} diff --git a/ruoyi-oss/src/main/java/com/ruoyi/oss/properties/OssProperties.java b/ruoyi-oss/src/main/java/com/ruoyi/oss/properties/OssProperties.java deleted file mode 100644 index 781a170..0000000 --- a/ruoyi-oss/src/main/java/com/ruoyi/oss/properties/OssProperties.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.ruoyi.oss.properties; - -import lombok.Data; - -/** - * OSS对象存储 配置属性 - * - * @author Lion Li - */ -@Data -public class OssProperties { - - /** - * 访问站点 - */ - private String endpoint; - - /** - * 自定义域名 - */ - private String domain; - - /** - * 前缀 - */ - private String prefix; - - /** - * ACCESS_KEY - */ - private String accessKey; - - /** - * SECRET_KEY - */ - private String secretKey; - - /** - * 存储空间名 - */ - private String bucketName; - - /** - * 存储区域 - */ - private String region; - - /** - * 是否https(Y=是,N=否) - */ - private String isHttps; - - /** - * 桶权限类型(0private 1public 2custom) - */ - private String accessPolicy; - -} diff --git a/ruoyi-system-cron/pom.xml b/ruoyi-system-cron/pom.xml index 70ecd4d..6b4f7be 100644 --- a/ruoyi-system-cron/pom.xml +++ b/ruoyi-system-cron/pom.xml @@ -31,7 +31,7 @@ com.ruoyi - ruoyi-system + ruoyi-common diff --git a/ruoyi-system-file/README.md b/ruoyi-system-file/README.md new file mode 100644 index 0000000..b80fc2b --- /dev/null +++ b/ruoyi-system-file/README.md @@ -0,0 +1 @@ +# 系统文件模块 \ No newline at end of file diff --git a/ruoyi-system-file/pom.xml b/ruoyi-system-file/pom.xml new file mode 100644 index 0000000..7fc7060 --- /dev/null +++ b/ruoyi-system-file/pom.xml @@ -0,0 +1,115 @@ + + + 4.0.0 + + com.ruoyi + ruoyi-vue-plus + 4.6.0 + + + ruoyi-system-file + + + 系统文件模块 + + + + + com.ruoyi + ruoyi-common + + + + com.github.gotson + webp-imageio + 0.2.2 + + + + org.dromara.x-file-storage + x-file-storage-spring + 2.2.1 + + + + + com.huaweicloud + esdk-obs-java + 3.22.12 + + + + com.aliyun.oss + aliyun-sdk-oss + 3.17.4 + + + + com.qiniu + qiniu-java-sdk + 7.12.1 + + + + com.qcloud + cos_api + 5.6.137 + + + + io.minio + minio + 8.5.2 + + + + + com.amazonaws + aws-java-sdk-s3 + 1.12.429 + + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + + com.jcraft + jsch + 0.1.55 + + + + commons-net + commons-net + 3.9.0 + + + + + cn.hutool + hutool-extra + + + + + org.apache.commons + commons-pool2 + 2.11.1 + + + + + com.github.lookfirst + sardine + 5.10 + + + + diff --git a/ruoyi-system-file/src/main/java/com/ruoyi/file/FileDownloadTestController.java b/ruoyi-system-file/src/main/java/com/ruoyi/file/FileDownloadTestController.java new file mode 100644 index 0000000..7a9781e --- /dev/null +++ b/ruoyi-system-file/src/main/java/com/ruoyi/file/FileDownloadTestController.java @@ -0,0 +1,30 @@ +package com.ruoyi.file; + +import cn.dev33.satoken.annotation.SaIgnore; +import com.ruoyi.common.annotation.Dev; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.time.Duration; + +@Controller +@RequiredArgsConstructor +@RequestMapping("/file/test/") +public class FileDownloadTestController { + + private final FileService fileService; + + @GetMapping("/download") + @SaIgnore + @Dev + public ModelAndView downloadFile(String url,String p, HttpServletRequest request, HttpServletResponse response) { + fileService.setPlatform(p).download(url, Duration.ofHours(1), request, response); + return null; + } +} diff --git a/ruoyi-system-file/src/main/java/com/ruoyi/file/FileService.java b/ruoyi-system-file/src/main/java/com/ruoyi/file/FileService.java new file mode 100644 index 0000000..3b86952 --- /dev/null +++ b/ruoyi-system-file/src/main/java/com/ruoyi/file/FileService.java @@ -0,0 +1,644 @@ +package com.ruoyi.file; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.img.Img; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.lang.UUID; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.symmetric.SymmetricAlgorithm; +import cn.hutool.crypto.symmetric.SymmetricCrypto; +import com.ruoyi.common.utils.IdUtils; +import lombok.Data; +import lombok.SneakyThrows; +import org.apache.tika.Tika; +import org.dromara.x.file.storage.core.Downloader; +import org.dromara.x.file.storage.core.FileInfo; +import org.springframework.web.multipart.MultipartFile; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + + +public interface FileService { + + /** + * 生成保留文件名的规则 + */ + String RULE_KEEP_FILENAME = "/{yyyy}/{MM}/{dd}/{id36}/{filename}.{ext}"; + + /** + * 生成随机文件名的规则 + */ + String RULE_RANDOM_FILENAME = "/{yyyy}/{MM}/{dd}/{id36}.{ext}"; + + /** + * 默认前缀 + */ + String DEFAULT_PREFIX = "default"; + + + /** + * 默认图片文件名 + */ + String DEFAULT_IMAGE_FILENAME = "a.webp"; + + /** + * 缩略图的扩展名 + */ + String THUMBNAIL_EXT = ".min.webp"; + + /** + * 源文件的扩展名 + */ + String SRC_EXT = ".zip"; + + /** + * webp图片格式扩展名 + */ + String IMAGE_WEBP = "webp"; + + + ThreadLocal paramThreadLocal = ThreadLocal.withInitial(() -> new Param()); + + + /** + * 初始化分片上传 + * + * @return 分配上传编号 + */ + String multipartUploadInit(); + + /** + * 分片上传 + * + * @param uploadId + * @param partNumber + * @param inputStream + */ + void multipartUpload(String uploadId, Integer partNumber, InputStream inputStream); + + + /** + * 分片上传完成 + * + * @param uploadId + * @return + */ + String multipartUploadComplete(String uploadId); + + + /** + * 分片上传取消 + * + * @param uploadId + */ + void multipartUploadAbort(String uploadId); + + List multipartUploadListParts(String uploadId); + + /** + * 删除 + * + * @param url + */ + void delete(String url); + + /** + * 下载 + * + * @param url + * @return + */ + Downloader download(String url); + + /** + * 下载文件 + * 1.支持预签名的跳转 + * 2.本地模式的读取下载 + * 3.否则,直接读取下载 + * + * @param url url + * @param exp 过期时间 + * @param request 请求 + * @param response 响应 + */ + void download(String url, Duration exp, HttpServletRequest request, HttpServletResponse response); + + /** + * 生成预签名URL + * + * @param url 原始URL + * @param exp 过期时间 + * @return + */ + String generatePresignedUrl(String url, Duration exp); + + /** + * 在保存方法之前执行 + * 设置平台 + * + * @param platform 平台 + * @return + */ + default FileService setPlatform(String platform) { + paramThreadLocal.get().setPlatform(platform); + return this; + } + + /** + * 在保存方法之前执行 + * 设置路径前缀 + * + * @param prefix 前缀 + * @return + */ + default FileService setPrefix(String prefix) { + if (StrUtil.isBlank(prefix)) { + throw new RuntimeException("路径前缀不能为空"); + } + if (!prefix.matches("^[\\w\\-]+$")) { + throw new RuntimeException("路径前缀规则错误"); + } + paramThreadLocal.get().setPrefix(prefix); + return this; + } + + /** + * 在保存方法之前执行 + * 设置文件名 + * 保存inputstream流需要设置 + * + * @param filename + * @return + */ + default FileService setFilename(String filename) { + paramThreadLocal.get().setFilename(filename); + return this; + } + + /** + * 在保存方法之前执行 + * 使用RULE_KEEP_FILENAME规则生成保存路径 + * 不执行使用RULE_RANDOM_FILENAME规则生成保存路径 + * + * @return + */ + default FileService setKeepFilename() { + return setKeepFilename(true); + } + + /** + * 在保存方法之前执行 + * 使用RULE_KEEP_FILENAME规则生成保存路径 + * 不执行使用RULE_RANDOM_FILENAME规则生成保存路径 + * + * @return + */ + default FileService setKeepFilename(Boolean keepFilename) { + paramThreadLocal.get().setKeepFilename(keepFilename); + return this; + } + + /** + * 在保存方法之前执行 + * 设置保存路径,执行该方法后,setKeepFilename,setFilename,setRule将无效 + * + * @param uri 保存路径 + * @return + */ + default FileService setUri(String uri) { + paramThreadLocal.get().setUri(uri); + return this; + } + + /** + * 在保存方法之前执行 + * 使用rule规则生成保存路径 + * + * @param rule + * @return + */ + default FileService setRule(String rule) { + paramThreadLocal.get().setRule(rule); + return this; + } + + /** + * 在saveImage方法之前执行 + * 调整图片大小 + * + * @param maxWidth + * @param maxHeight + * @return + */ + default FileService setSize(int maxWidth, int maxHeight) { + paramThreadLocal.get().setMaxWidth(maxWidth); + paramThreadLocal.get().setMaxHeight(maxHeight); + return this; + } + + + /** + * 在saveImage方法之前执行 + * 调整图片大小(默认值) + * + * @return + */ + FileService setSize(); + + /** + * 在saveImage方法之前执行 + * 设置水印 + * + * @param watermark + * @return + */ + default FileService setWatermark(BufferedImage watermark) { + paramThreadLocal.get().setWatermark(watermark); + return this; + } + + /** + * 是否添加默认水印 + * @param watermark + * @return + */ + default FileService setWatermark(Boolean watermark) { + if(watermark){ + this.setWatermark(); + }else { + paramThreadLocal.get().setWatermark(null); + } + return this; + } + + /** + * 在saveImage方法之前执行 + * 设置默认水印 + * + * @return + */ + FileService setWatermark(); + + /** + * 在saveImage方法之前执行 + * 设置同时生成缩略图 + * + * @return + */ + default FileService setThumbnail() { + return setThumbnail(true); + } + + /** + * 在saveImage方法之前执行 + * 设置是否同时生成缩略图 + * + * @return + */ + FileService setThumbnail(boolean thumbnail); + + default FileService setSaveSrc(){ + return setSaveSrc(true); + } + + default FileService setSaveSrc(boolean saveSrc){ + paramThreadLocal.get().setSaveSrc(saveSrc); + return this; + } + + /** + * 在saveImage方法之前执行 + * 设置同时生成缩略图,并指定尺寸 + * + * @param width + * @param height + * @return + */ + default FileService setThumbnail(int width, int height) { + paramThreadLocal.get().setThWidth(width); + paramThreadLocal.get().setThHeight(height); + return this; + } + + + /** + * 保存文件 + * + * @param in + * @return + */ + String save(InputStream in); + + /** + * 保存文件 + * + * @param file + * @return + */ + @SneakyThrows + default String save(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new RuntimeException("文件不能为空"); + } + return setFilename(file.getOriginalFilename()).save(file.getInputStream()); + } + + /** + * 保存图片 + * + * @param in + * @return + */ + String saveImage(InputStream in); + + /** + * 保存图片 + * + * @param file + * @return + */ + @SneakyThrows + default String saveImage(MultipartFile file) { + if (file == null || file.isEmpty()) { + throw new RuntimeException("文件不能为空"); + } + return setFilename(file.getOriginalFilename()).saveImage(file.getInputStream()); + } + + + /** + * 获取类型提取工具 + * + * @return + */ + Tika getTika(); + + /** + * 获取文件类型 + * + * @param in + * @return + */ + @SneakyThrows + default String getContentType(InputStream in) { + return getTika().detect(in); + } + + /** + * 是否是图片文件 + * + * @param in + * @return + */ + @SneakyThrows + default boolean isImage(InputStream in) { + return getContentType(in).startsWith("image/"); + } + + + default InputStream formatImage(MultipartFile file, int maxWidth, int maxHeight, + BufferedImage watermark) { + try { + if (file == null || file.isEmpty()) { + throw new RuntimeException("上传图片不能为空"); + } + if (!file.getContentType().startsWith("image/")) { + throw new RuntimeException("上传的文件不是图片"); + } + return formatImage(file.getInputStream(), maxWidth, maxHeight, watermark); + } catch (IOException e) { + throw new RuntimeException("处理图片错误", e); + } + } + + default InputStream formatImage(Image img, int maxWidth, int maxHeight, BufferedImage watermark) { + if (img == null) { + throw new RuntimeException("图片不能为空"); + } + return formatImage(Img.from(img), maxWidth, maxHeight, watermark); + } + + default InputStream formatImage(InputStream in, int maxWidth, int maxHeight, BufferedImage watermark) { + return formatImage(Img.from(in), maxWidth, maxHeight, watermark); + } + + default InputStream formatImage(Img img, int maxWidth, int maxHeight, BufferedImage watermark) { + try { + int w = img.getImg().getWidth(null); + int h = img.getImg().getHeight(null); + if (maxWidth > 0 && maxHeight > 0) { + if (w > maxWidth || h > maxHeight) { + int outWidth = 0; + int outHeight = 0; + outHeight = maxWidth * h / w; + if (outHeight > maxHeight) { + outHeight = maxHeight; + outWidth = outHeight * w / h; + } else { + outWidth = maxWidth; + } + img = img.scale(outWidth, outHeight); + w = outWidth; + h = outHeight; + } + } + + + if (watermark != null) { + int ww = watermark.getWidth(null); + int wh = watermark.getHeight(null); + if (w > ww && h > wh) { + img = img.pressImage(watermark, 0, 0, 1f); + } + } + + + ByteArrayOutputStream pout = new ByteArrayOutputStream(); + +// img.setTargetImageType(IMAGE_WEBP).write(pout); + ImageIO.write(ImgUtil.toBufferedImage(img.getImg()), IMAGE_WEBP, pout); + + ByteArrayInputStream pin = new ByteArrayInputStream(pout.toByteArray()); + pout.close(); + pout = null; + return pin; + } catch (Exception e) { + throw new RuntimeException("处理图片错误", e); + } + } + + /** + * 生成存放的URI并保留文件名 + * + * @param filename 文件名 + * @param prefix 前缀 + * @return + */ + default String generateURIKeepFilename(String prefix, String filename) { + return generateURI(prefix, RULE_KEEP_FILENAME, filename); + } + + /** + * 生成存放的URI并保留文件名 + * + * @param filename 文件名 + * @return + */ + default String generateURIKeepFilename(String filename) { + return generateURI(DEFAULT_PREFIX, RULE_KEEP_FILENAME, filename); + } + + /** + * 生成存放的URI并随机文件名 + * + * @param filename 文件名 + * @param prefix 前缀 + * @return + */ + default String generateURIRandomFilename(String prefix, String filename) { + return generateURI(prefix, RULE_RANDOM_FILENAME, filename); + } + + + /** + * 生成存放的URI并随机文件名 + * + * @param filename 文件名 + * @return + */ + default String generateURIRandomFilename(String filename) { + return generateURI(DEFAULT_PREFIX, RULE_RANDOM_FILENAME, filename); + } + + /** + *

+   * - 根据存放规则和文件名生成存放的URI
+   * - 支持
+   * 	- {yyyy}/{MM}/{dd}/{HH}/{mm}/{ss} 年月日时分秒
+   * 	- {UUID} 32位的唯一标志
+   *  - {i} 自增id
+   * 	- {id} 当日int类型的唯一id,防止重复建议+年月日路径
+   * 	- {id16} 当日int类型的唯一id16进制表示,防止重复建议+年月日路径
+   * 	- {id36} 当日int类型的唯一id36进制表示,防止重复建议+年月日路径
+   * 	- {filename} 文件基础名称
+   * 	- {ext} 扩展名
+   * 
+ * + * @param prefix 前缀 + * @param rule 规则 + * @param filename 文件名 + * @return + */ + default String generateURI(String prefix, String rule, String filename) { + if (StrUtil.isBlank(prefix)) { + prefix = DEFAULT_PREFIX; + } + LocalDateTime now = LocalDateTime.now(); + if (rule.contains("{yyyy}")) { + rule = rule.replace("{yyyy}", "" + now.getYear()); + } + if (rule.contains("{MM}")) { + rule = rule.replace("{MM}", String.format("%02d", now.getMonthValue())); + } + if (rule.contains("{dd}")) { + rule = rule.replace("{dd}", String.format("%02d", now.getDayOfMonth())); + } + if (rule.contains("{HH}")) { + rule = rule.replace("{HH}", String.format("%02d", now.getHour())); + } + if (rule.contains("{mm}")) { + rule = rule.replace("{mm}", String.format("%02d", now.getMinute())); + } + if (rule.contains("{ss}")) { + rule = rule.replace("{ss}", String.format("%02d", now.getSecond())); + } + if (rule.contains("{UUID}")) { + rule = rule.replace("{UUID}", UUID.fastUUID().toString(true)); + } + if (rule.contains("{i}")) { + rule = rule.replace("{i}", IdUtils.nextId(Id.groupName).toString()); + } + if (rule.contains("{id}")) { + rule = rule.replace("{id}", Long.toString(id.nextId())); + } + if (rule.contains("{id16}")) { + rule = rule.replace("{id16}", Long.toString(id.nextId(), 16)); + } + if (rule.contains("{id36}")) { + rule = rule.replace("{id36}", Long.toString(id.nextId(), 36)); + } + if (rule.contains("{filename}")) { + String temp = null; + if (filename.contains(".")) { + temp = filename.substring(0, filename.lastIndexOf(".")); + } else { + temp = filename; + } + rule = rule.replace("{filename}", temp); + } + + if (rule.contains("{ext}")) { + String temp = null; + if (filename.contains(".")) { + temp = filename.substring(filename.lastIndexOf(".") + 1); + } else { + temp = ""; + } + rule = rule.replace("{ext}", temp.toLowerCase()); + } + return prefix + rule; + } + + Id id = new Id(); + + static class Id { + + private static final String groupName = "file:id"; + + private SymmetricCrypto crypto; + + private String today = DateUtil.today(); + + public synchronized Long nextId() { + if (crypto == null || !today.equals(DateUtil.today())) { + today = DateUtil.today(); + byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), today.getBytes()).getEncoded(); + crypto = new SymmetricCrypto(SymmetricAlgorithm.DES, key); + } + ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES); + buffer.putInt(IdUtils.nextDayId(groupName).intValue()); + return Math.abs(ByteBuffer.wrap(crypto.encrypt(buffer.array())).getLong()); + } + + } + + @Data + public static class Param { + private String platform; + private String prefix = FileService.DEFAULT_PREFIX; + private String filename; + private boolean keepFilename = false; + private boolean saveSrc = false; + private String rule; + private String uri; + private int maxWidth = 0; + private int maxHeight = 0; + private BufferedImage watermark; + private int thWidth = 0; + private int thHeight = 0; + + } +} diff --git a/ruoyi-system-file/src/main/java/com/ruoyi/file/FileUploadApi.java b/ruoyi-system-file/src/main/java/com/ruoyi/file/FileUploadApi.java new file mode 100644 index 0000000..aebbc26 --- /dev/null +++ b/ruoyi-system-file/src/main/java/com/ruoyi/file/FileUploadApi.java @@ -0,0 +1,184 @@ +package com.ruoyi.file; + +import cn.dev33.satoken.annotation.SaIgnore; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.ruoyi.common.annotation.Dev; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.file.FileUtils; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.InputStream; +import java.time.Duration; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/file/upload/") +@SaIgnore +public class FileUploadApi { + + public final FileService fileService; + + + /** + * 获取上传凭证, + * 实际业务中,需要在对应的业务中完成凭证的获取 + * + * @return + */ + @GetMapping("getUploadKey") + @Dev + public R getUploadKey() { + return R.ok().setData(FileUtils.getUploadKey()); + } + + /** + * 删除上传凭证 + * + * @param key + */ + @GetMapping("removeUploadKey-{key}") + public void removeUploadKey(@PathVariable String key) { + FileUtils.removeUploadKey(key); + } + + @GetMapping("exitisUploadKey-{key}") + public Boolean exitisUploadKey(@PathVariable String key) { + return FileUtils.exitisUploadKey(key); + } + + + /** + * 上传文件 + * + * @param file + * @param prefix + * @param key + * @param keepFilename + * @return + */ + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R upload(@RequestPart("file") MultipartFile file, String prefix, String key, @RequestParam(defaultValue = "false") Boolean keepFilename) { + if (ObjectUtil.isNull(file)) { + throw new ServiceException("文件必填"); + } + if (!FileUtils.exitisUploadKey(key)) { + throw new ServiceException("上传凭证无效"); + } + return R.ok().setData(fileService.setPrefix(prefix).setKeepFilename(keepFilename).save(file)); + } + + + @PostMapping("multipartUploadInit") + public R multipartUploadInit(String filename, String prefix, String key, @RequestParam(defaultValue = "false") Boolean keepFilename) { + if (StrUtil.isBlank(filename)) { + throw new ServiceException("文件名必填"); + } + if (!FileUtils.exitisUploadKey(key)) { + throw new ServiceException("上传凭证无效"); + } + return R.ok().setData(fileService.setPrefix(prefix).setFilename(filename).setKeepFilename(keepFilename).multipartUploadInit()); + } + + @PostMapping(value = "multipartUpload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @SneakyThrows + public R multipartUpload(@RequestPart("file") MultipartFile file, String uploadId, Integer partNumber) { + if (StrUtil.isBlank(uploadId)) { + throw new ServiceException("上传标识必填"); + } + fileService.multipartUpload(uploadId, partNumber, file.getInputStream()); + return R.ok(); + } + + @GetMapping("multipartUploadListParts") + public R multipartUploadListParts(String uploadId) { + if (StrUtil.isBlank(uploadId)) { + throw new ServiceException("上传标识必填"); + } + return R.ok().setData(fileService.multipartUploadListParts(uploadId)); + } + + @PostMapping("multipartUploadComplete") + public R multipartUploadComplete(String uploadId) { + if (StrUtil.isBlank(uploadId)) { + throw new ServiceException("上传标识必填"); + } + return R.ok().setData(fileService.multipartUploadComplete(uploadId)); + } + + @PostMapping("multipartUploadAbort") + public R multipartUploadAbort(String uploadId) { + if (StrUtil.isBlank(uploadId)) { + throw new ServiceException("上传标识必填"); + } + try { + fileService.multipartUploadAbort(uploadId); + } catch (Exception e) { + + } + return R.ok(); + } + + + /** + * 上传图片 + * + * @param file + * @param prefix + * @param key + * @param saveSrc + * @param saveThumbnail + * @return + */ + @PostMapping(value = "image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + public R uploadImage(@RequestPart("file") MultipartFile file, String prefix, String key, + @RequestParam(defaultValue = "false") Boolean saveSrc, + @RequestParam(defaultValue = "true") Boolean watermark, + @RequestParam(defaultValue = "true") Boolean saveThumbnail + ) { + if (ObjectUtil.isNull(file)) { + throw new ServiceException("文件必填"); + } + if (!FileUtils.exitisUploadKey(key)) { + throw new ServiceException("上传凭证无效"); + } + + return R.ok().setData(fileService.setPrefix(prefix).setWatermark(watermark).setSaveSrc(saveSrc).setThumbnail(saveThumbnail).saveImage(file)); + } + + @GetMapping("/download") + @SneakyThrows + public ModelAndView downloadFile(String url, String key, HttpServletRequest request, HttpServletResponse response) { + if (StrUtil.isBlank(url)) { + response.sendError(404, "URL不能为空"); + return null; + } + if (!FileUtils.exitisUploadKey(key)) { + response.sendError(404, "上传凭证无效"); + return null; + } + fileService.download(url, Duration.ofHours(1), request, response); + return null; + } + + @PostMapping("/remove") + public R remove(String url, String key){ + if (StrUtil.isBlank(url)) { + throw new ServiceException("URL不能为空"); + } + if (!FileUtils.exitisUploadKey(key)) { + throw new ServiceException("上传凭证无效"); + } + fileService.delete(url); + return R.ok(); + } + +} diff --git a/ruoyi-system-file/src/main/java/com/ruoyi/file/config/FileConfig.java b/ruoyi-system-file/src/main/java/com/ruoyi/file/config/FileConfig.java new file mode 100644 index 0000000..3b57c0d --- /dev/null +++ b/ruoyi-system-file/src/main/java/com/ruoyi/file/config/FileConfig.java @@ -0,0 +1,18 @@ +package com.ruoyi.file.config; + + +import org.dromara.x.file.storage.spring.EnableFileStorage; +import org.dromara.x.file.storage.spring.FileStorageAutoConfiguration; +import org.dromara.x.file.storage.spring.SpringFileStorageProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +//@EnableFileStorage +@Import({FileStorageAutoConfiguration.class}) +public class FileConfig { + + +} diff --git a/ruoyi-system-file/src/main/java/com/ruoyi/file/config/SpringFileStorageProperties.java b/ruoyi-system-file/src/main/java/com/ruoyi/file/config/SpringFileStorageProperties.java new file mode 100644 index 0000000..09c4a82 --- /dev/null +++ b/ruoyi-system-file/src/main/java/com/ruoyi/file/config/SpringFileStorageProperties.java @@ -0,0 +1,45 @@ +package com.ruoyi.file.config; + + +import cn.hutool.core.img.ImgUtil; +import lombok.AccessLevel; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import org.springframework.context.annotation.Configuration; + +import javax.annotation.PostConstruct; +import java.awt.image.BufferedImage; +import java.io.File; + + +@Configuration +@ConfigurationProperties(prefix = "ruoyi.file") +@Data +@Slf4j +public class SpringFileStorageProperties extends org.dromara.x.file.storage.spring.SpringFileStorageProperties { + + private Integer maxWidth; + private Integer maxHeight; + private File watermark; + private Integer thWidth; + private Integer thHeight; + @Getter + @Setter(AccessLevel.PRIVATE) + private BufferedImage watermarkImage; + + @PostConstruct + public void init() { + try { + if (watermark != null && watermark.isFile()) { + watermarkImage = ImgUtil.read(watermark); + log.warn("水印图片加载成功:" + watermark.toString()); + } + } catch (Exception e) { + log.warn("初始化水印失败", e); + } + } +} diff --git a/ruoyi-system-file/src/main/java/com/ruoyi/file/impl/FileServiceImpl.java b/ruoyi-system-file/src/main/java/com/ruoyi/file/impl/FileServiceImpl.java new file mode 100644 index 0000000..7b95183 --- /dev/null +++ b/ruoyi-system-file/src/main/java/com/ruoyi/file/impl/FileServiceImpl.java @@ -0,0 +1,370 @@ +package com.ruoyi.file.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.img.ImgUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.lang.UUID; +import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.URLUtil; +import cn.hutool.core.util.ZipUtil; +import com.ruoyi.common.utils.HttpDownloadUtil; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.common.utils.redis.RedisUtils; +import com.ruoyi.file.FileService; +import com.ruoyi.file.config.SpringFileStorageProperties; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.tika.Tika; +import org.dromara.x.file.storage.core.Downloader; +import org.dromara.x.file.storage.core.FileInfo; +import org.dromara.x.file.storage.core.FileStorageService; +import org.dromara.x.file.storage.core.constant.Constant; +import org.dromara.x.file.storage.core.platform.FileStorage; +import org.dromara.x.file.storage.core.platform.LocalPlusFileStorage; + +import org.dromara.x.file.storage.core.platform.UpyunUssFileStorage; +import org.dromara.x.file.storage.core.presigned.GeneratePresignedUrlResult; +import org.dromara.x.file.storage.core.tika.TikaFactory; +import org.dromara.x.file.storage.core.upload.FilePartInfo; +import org.dromara.x.file.storage.core.upload.MultipartUploadSupportInfo; +import org.dromara.x.file.storage.core.upload.UploadPretreatment; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.*; +import java.io.*; +import java.time.Duration; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@Slf4j +@RequiredArgsConstructor +public class FileServiceImpl implements FileService { + + private final FileStorageService service; + private final SpringFileStorageProperties properties; + private final TikaFactory tikaFactory; + public static final String CACHE_KEY_PREFIX = "file:multipart:"; + + + @Override + public String multipartUploadInit() { + try { + + UploadPretreatment p = new UploadPretreatment(); + Param param = paramThreadLocal.get(); + initUploadPretreatment(p, param); + FileStorage fileStorage = service.getFileStorage(p.getPlatform()); + MultipartUploadSupportInfo supportInfo = fileStorage.isSupportMultipartUpload(); + if (!(supportInfo.getIsSupport() && supportInfo.getIsSupportAbort() && supportInfo.getIsSupportListParts())) { + throw new RuntimeException("不支持分片上传"); + } + boolean isUpyunUss = fileStorage instanceof UpyunUssFileStorage; + FileInfo fileInfo = service.initiateMultipartUpload() + .setPlatform(p.getPlatform()) + .setPath(p.getPath()) + .setSaveFilename(p.getSaveFilename()) + .putMetadata(isUpyunUss, "X-Upyun-Multi-Part-Size", String.valueOf(5 * 1024 * 1024))// 设置 Metadata,不需要可以不写 + .init(); + + String uploadId = UUID.fastUUID().toString(true); + RedisUtils.setCacheObject(CACHE_KEY_PREFIX + uploadId, fileInfo, Duration.ofMinutes(60)); + return uploadId; + } finally { + paramThreadLocal.remove(); + } + } + + @Override + public void multipartUpload(String uploadId, Integer partNumber, InputStream inputStream) { + FileInfo fileInfo = RedisUtils.getCacheObject(CACHE_KEY_PREFIX + uploadId); + if (fileInfo == null) { + throw new RuntimeException("未初始化分片上传"); + } + service.uploadPart(fileInfo, partNumber, inputStream).upload(); + } + + + @Override + public String multipartUploadComplete(String uploadId) { + FileInfo fileInfo = RedisUtils.getCacheObject(CACHE_KEY_PREFIX + uploadId); + if (fileInfo == null) { + throw new RuntimeException("未初始化分片上传"); + } + service.completeMultipartUpload(fileInfo).complete(); + RedisUtils.deleteObject(CACHE_KEY_PREFIX + uploadId); + return fileInfo.getUrl(); + } + + @Override + public void multipartUploadAbort(String uploadId) { + FileInfo fileInfo = RedisUtils.getCacheObject(CACHE_KEY_PREFIX + uploadId); + + if (fileInfo == null) { + throw new RuntimeException("未初始化分片上传"); + } + service.abortMultipartUpload(fileInfo).abort(); + RedisUtils.deleteObject(CACHE_KEY_PREFIX + uploadId); + } + + @Override + public List multipartUploadListParts(String uploadId) { + FileInfo fileInfo = RedisUtils.getCacheObject(CACHE_KEY_PREFIX + uploadId); + return service.listParts(fileInfo).listParts().getList().stream().map(FilePartInfo::getPartNumber).collect(Collectors.toList()); + } + + @Override + public void delete(String url) { + try { + FileInfo fileInfo = getFileInfoByURL(url); + service.delete(fileInfo); + } finally { + paramThreadLocal.remove(); + } + } + + @Override + public Downloader download(String url) { + try { + FileInfo fileInfo = getFileInfoByURL(url); + return service.download(fileInfo); + } finally { + paramThreadLocal.remove(); + } + } + + @SneakyThrows + public void download(String url, Duration exp, HttpServletRequest request, HttpServletResponse response) { + + try { + + FileInfo fileInfo = getFileInfoByURL(url); + FileStorage fileStorage = service.getFileStorage(fileInfo.getPlatform()); + + if (fileStorage.isSupportPresignedUrl()) { + url = generatePresignedUrlInner(url, null, fileInfo, fileStorage); + log.debug("跳转:"+url); + response.reset(); + response.setStatus(HttpServletResponse.SC_FOUND); + response.setHeader("Location", url); + response.getWriter().print(""); + response.flushBuffer(); + } else { + if (fileStorage instanceof LocalPlusFileStorage) { + LocalPlusFileStorage fs = (LocalPlusFileStorage) fileStorage; + File file = new File(new File(fs.getStoragePath(), fileInfo.getPath()), fileInfo.getFilename()); + log.debug("下载本地文件:" + file.getAbsolutePath()); + if (!file.isFile()) { + throw new FileNotFoundException("文件不存在"); + } + HttpDownloadUtil.download(request, response, file); + + } else { + response.reset(); + FileUtils.setAttachmentResponseHeader(response, fileInfo.getFilename()); + service.download(fileInfo).outputStream(response.getOutputStream()); + response.flushBuffer(); + } + } + + + } catch (Exception e) { + log.warn("下载文件失败", e); + response.sendError(404, "下载文件失败"); + } finally { + paramThreadLocal.remove(); + } + } + + @Override + @SneakyThrows + public String generatePresignedUrl(String url, Duration exp) { + try { + FileInfo fileInfo = getFileInfoByURL(url); + FileStorage fileStorage = service.getFileStorage(fileInfo.getPlatform()); + return generatePresignedUrlInner(url, exp, fileInfo, fileStorage); + } finally { + paramThreadLocal.remove(); + } + } + + @SneakyThrows + private String generatePresignedUrlInner(String url, Duration exp, FileInfo fileInfo, FileStorage fileStorage) { + if (!fileStorage.isSupportPresignedUrl()) { + throw new RuntimeException("不支持预签名URL"); + } + GeneratePresignedUrlResult downloadResult = service.generatePresignedUrl() + .setPlatform(fileInfo.getPlatform()) // 存储平台,不传使用默认的 + .setPath(fileInfo.getPath()) // 文件路径 + .setFilename(fileInfo.getFilename()) // 文件名,也可以换成缩略图的文件名 + .setMethod(Constant.GeneratePresignedUrl.Method.GET) // 签名方法 + .setExpiration(exp == null ? new Date(System.currentTimeMillis() + 600000) : new Date(System.currentTimeMillis() + exp.toMillis())) // 过期时间 10 分钟 + .putResponseHeaders( + Constant.Metadata.CONTENT_DISPOSITION, "attachment;filename=" + (fileInfo.getFilename().endsWith("." + FileService.IMAGE_WEBP + FileService.SRC_EXT) ? "原图.zip" : fileInfo.getFilename())) + + .generatePresignedUrl(); + log.debug("生成访问预签名 URL 结果:{}", downloadResult); + String q = downloadResult.getUrl().substring(downloadResult.getUrl().indexOf("?")); + return URLUtil.encode(url) + q; + } + + @Override + public FileService setSize() { + return setSize(properties.getMaxWidth(), properties.getMaxHeight()); + } + + @Override + public FileService setWatermark() { + return setWatermark(properties.getWatermarkImage()); + } + + @Override + public FileService setThumbnail(boolean thumbnail) { + if (thumbnail) { + return setThumbnail(properties.getThWidth(), properties.getThHeight()); + } else { + return setThumbnail(0, 0); + } + } + + @Override + @SneakyThrows + public String save(InputStream in) { + try { + UploadPretreatment p = service.of(in); + Param param = paramThreadLocal.get(); + initUploadPretreatment(p, param); + return p.upload().getUrl(); + } finally { + paramThreadLocal.remove(); + } + } + + + @Override + @SneakyThrows + public String saveImage(InputStream in) { + try { + byte[] imageBytes = IoUtil.readBytes(in, true); + Image image = ImgUtil.read(new ByteArrayInputStream(imageBytes)); + Param param = paramThreadLocal.get(); + param.setKeepFilename(false); + String srcFilename = param.getFilename(); + param.setFilename(FileService.DEFAULT_IMAGE_FILENAME); + UploadPretreatment p = service.of(formatImage(image, param.getMaxWidth(), param.getMaxHeight(), param.getWatermark())); + initUploadPretreatment(p, param); + String url = p.upload().getUrl(); + if (param.getThHeight() > 0 && param.getThWidth() > 0) { + UploadPretreatment p1 = service.of(formatImage(image, param.getThWidth(), param.getThHeight(), null)); + String platform = param.getPlatform(); + if (StrUtil.isNotBlank(platform)) { + p1 = p1.setPlatform(platform); + } + p1.setPath(p.getPath()); + p1.setSaveFilename(p.getSaveFilename() + FileService.THUMBNAIL_EXT); + p1.upload(); + } + + if (param.isSaveSrc() && StrUtil.isNotBlank(srcFilename)) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ZipUtil.zip(out, new String[]{srcFilename}, new InputStream[]{new ByteArrayInputStream(imageBytes)}); + UploadPretreatment p1 = service.of(new ByteArrayInputStream(out.toByteArray())); + String platform = param.getPlatform(); + if (StrUtil.isNotBlank(platform)) { + p1 = p1.setPlatform(platform); + } + p1.setPath(p.getPath()); + p1.setSaveFilename(p.getSaveFilename() + FileService.SRC_EXT); + p1.upload(); + } + + return url; + } finally { + paramThreadLocal.remove(); + } + } + + + @Override + public Tika getTika() { + return tikaFactory.getTika(); + } + + private FileInfo getFileInfoByURL(String url) { + if (StrUtil.isBlank(url)) { + throw new RuntimeException("URL不能为空"); + } + FileInfo fileInfo = new FileInfo(); + String platform = paramThreadLocal.get().getPlatform(); + FileStorage fileStorage = null; + if (StrUtil.isBlank(platform)) { + fileStorage = service.getFileStorage(); + } else { + fileStorage = service.getFileStorage(platform); + } + fileInfo.setPlatform(fileStorage.getPlatform()); + String domain = null; + try { + domain = (String) BeanUtil.getProperty(fileStorage, "domain"); + } catch (Exception e) { + } + if (StrUtil.isBlank(domain)) { + int index = url.indexOf("://"); + if (index > -1) { + url = url.substring(url.indexOf("/", index + 4) + 1); + } else if (url.startsWith("//")) { + url = url.substring(url.indexOf("/", 3) + 1); + } else if (url.startsWith("/")) { + url = url.substring(1); + } + index = url.indexOf("/"); + url = url.substring(index + 1); + index = url.lastIndexOf("/"); + + fileInfo.setPath(url.substring(0, index + 1)); + fileInfo.setFilename(url.substring(index + 1)); + } else { + url = url.substring(domain.length()); + int index = url.lastIndexOf("/"); + fileInfo.setPath(url.substring(0, index + 1)); + fileInfo.setFilename(url.substring(index + 1)); + } + + + return fileInfo; + } + + private void initUploadPretreatment(UploadPretreatment p, Param param) { + + String platform = param.getPlatform(); + if (StrUtil.isNotBlank(platform)) { + p = p.setPlatform(platform); + } else { + p = p.setPlatform(service.getFileStorage().getPlatform()); + } + String uri = param.getUri(); + if (StrUtil.isBlank(uri)) { + String rule = param.getRule(); + if (StrUtil.isBlank(rule)) { + if (param.isKeepFilename()) { + rule = RULE_KEEP_FILENAME; + } else { + rule = RULE_RANDOM_FILENAME; + } + } + uri = generateURI(param.getPrefix(), rule, param.getFilename()); + } else { + uri = param.getPrefix() + uri; + } + + int index = uri.lastIndexOf("/") + 1; + String path = uri.substring(0, index); + String filename = uri.substring(index); + log.debug("path={},filename={}", path, filename); + p.setPath(path).setSaveFilename(filename); + } +} diff --git a/ruoyi-system-file/src/main/java/com/ruoyi/file/impl/FixFileStorageAspect.java b/ruoyi-system-file/src/main/java/com/ruoyi/file/impl/FixFileStorageAspect.java new file mode 100644 index 0000000..e2e0658 --- /dev/null +++ b/ruoyi-system-file/src/main/java/com/ruoyi/file/impl/FixFileStorageAspect.java @@ -0,0 +1,31 @@ +package com.ruoyi.file.impl; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.dromara.x.file.storage.core.FileInfo; +import org.dromara.x.file.storage.core.FileStorageService; +import org.dromara.x.file.storage.core.aspect.CompleteMultipartUploadAspectChain; +import org.dromara.x.file.storage.core.aspect.FileStorageAspect; +import org.dromara.x.file.storage.core.aspect.ListPartsAspectChain; +import org.dromara.x.file.storage.core.platform.AliyunOssFileStorage; +import org.dromara.x.file.storage.core.platform.FileStorage; +import org.dromara.x.file.storage.core.recorder.FileRecorder; +import org.dromara.x.file.storage.core.tika.ContentTypeDetect; +import org.dromara.x.file.storage.core.upload.CompleteMultipartUploadPretreatment; +import org.dromara.x.file.storage.core.upload.FilePartInfoList; +import org.dromara.x.file.storage.core.upload.ListPartsPretreatment; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RequiredArgsConstructor +public class FixFileStorageAspect implements FileStorageAspect { + + @Override + public FilePartInfoList listParts(ListPartsAspectChain chain, ListPartsPretreatment pre, FileStorage fileStorage) { + if(fileStorage instanceof AliyunOssFileStorage) { + pre.setPartNumberMarker(null); + } + return chain.next(pre,fileStorage); + } +} diff --git a/ruoyi-system-file/src/main/java/com/ruoyi/file/package-info.java b/ruoyi-system-file/src/main/java/com/ruoyi/file/package-info.java new file mode 100644 index 0000000..054fc7e --- /dev/null +++ b/ruoyi-system-file/src/main/java/com/ruoyi/file/package-info.java @@ -0,0 +1,4 @@ +/** + * 文件模块 + */ +package com.ruoyi.file; diff --git a/ruoyi-system-file/src/main/resources/watermark.png b/ruoyi-system-file/src/main/resources/watermark.png new file mode 100644 index 0000000000000000000000000000000000000000..aec18b587940437a0f7e06523ee989484782d55e GIT binary patch literal 17414 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 literal 0 HcmV?d00001 diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml index 6a7f2af..44fd9e3 100644 --- a/ruoyi-system/pom.xml +++ b/ruoyi-system/pom.xml @@ -23,11 +23,7 @@ ruoyi-common
- - - com.ruoyi - ruoyi-oss - + @@ -35,10 +31,6 @@ ruoyi-sms - - com.github.gotson - webp-imageio - diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/config/DownloadFileConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/config/DownloadFileConfig.java deleted file mode 100644 index 1833821..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/config/DownloadFileConfig.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.ruoyi.system.config; - -import cn.hutool.core.io.file.FileNameUtil; -import cn.hutool.core.util.URLUtil; -import com.ruoyi.common.config.RuoYiConfig; -import com.ruoyi.common.utils.HttpDownloadUtil; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.web.servlet.ServletRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.File; -import java.io.IOException; - -/** - * 本地文件公开下载配置 - * 方式一:SpringBoot静态资源配置 - * 方式二:Servlet - */ -@Configuration -@RequiredArgsConstructor -@Slf4j -public class DownloadFileConfig { - - private final RuoYiConfig config; - - @Bean - public WebMvcConfigurer DownloadFileWebMvcConfigurer() { - return new WebMvcConfigurer(){ - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - //加入外部静态资源html文件夹 - String path = new File(new File(config.upload.savePath).getAbsolutePath()).toURI().toString(); - registry.addResourceHandler(config.upload.pre+"/**").addResourceLocations(path); - log.info("添加静态资源目: {}/** ={}",config.upload.pre,path); - - } - }; - } - - - public ServletRegistrationBean DownloadFileServletRegistrationBean() { - ServletRegistrationBean bean = new ServletRegistrationBean(); - bean.setServlet(new HttpServlet() { - @Override - protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { - String uri = req.getRequestURI().substring(config.upload.pre.length()); - uri = URLUtil.decode(uri); - File down = new File(config.upload.savePath, uri); - if(down.isFile()) { - try { - HttpDownloadUtil.download(req, resp,down, - FileNameUtil.getName(uri)); - resp.getOutputStream().close(); - } catch (Exception e) { - resp.reset(); - resp.sendError(404); - } - }else { - resp.sendError(404); - } - } - }); - bean.addUrlMappings(config.upload.pre+"/*"); - return bean; - } -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOss.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOss.java deleted file mode 100644 index 968304b..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOss.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.ruoyi.system.domain; - -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.ruoyi.common.core.domain.BaseEntity; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * OSS对象存储对象 - * - * @author Lion Li - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("sys_oss") -public class SysOss extends BaseEntity { - - /** - * 对象存储主键 - */ - @TableId(value = "oss_id") - private Long ossId; - - /** - * 文件名 - */ - private String fileName; - - /** - * 原名 - */ - private String originalName; - - /** - * 文件后缀名 - */ - private String fileSuffix; - - /** - * URL地址 - */ - private String url; - - /** - * 服务商 - */ - private String service; - -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOssConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOssConfig.java deleted file mode 100644 index ac5e5a3..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/SysOssConfig.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.ruoyi.system.domain; - -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.ruoyi.common.core.domain.BaseEntity; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * 对象存储配置对象 sys_oss_config - * - * @author Lion Li - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("sys_oss_config") -public class SysOssConfig extends BaseEntity { - - /** - * 主建 - */ - @TableId(value = "oss_config_id") - private Long ossConfigId; - - /** - * 配置key - */ - private String configKey; - - /** - * accessKey - */ - private String accessKey; - - /** - * 秘钥 - */ - private String secretKey; - - /** - * 桶名称 - */ - private String bucketName; - - /** - * 前缀 - */ - private String prefix; - - /** - * 访问站点 - */ - private String endpoint; - - /** - * 自定义域名 - */ - private String domain; - - /** - * 是否https(0否 1是) - */ - private String isHttps; - - /** - * 域 - */ - private String region; - - /** - * 是否默认(0=是,1=否) - */ - private String status; - - /** - * 扩展字段 - */ - private String ext1; - - /** - * 备注 - */ - private String remark; - - /** - * 桶权限类型(0private 1public 2custom) - */ - private String accessPolicy; -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssBo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssBo.java deleted file mode 100644 index f874b20..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssBo.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.ruoyi.system.domain.bo; - -import com.ruoyi.common.core.domain.BaseEntity; -import lombok.Data; -import lombok.EqualsAndHashCode; - -/** - * OSS对象存储分页查询对象 sys_oss - * - * @author Lion Li - */ -@Data -@EqualsAndHashCode(callSuper = true) -public class SysOssBo extends BaseEntity { - - /** - * ossId - */ - private Long ossId; - - /** - * 文件名 - */ - private String fileName; - - /** - * 原名 - */ - private String originalName; - - /** - * 文件后缀名 - */ - private String fileSuffix; - - /** - * URL地址 - */ - private String url; - - /** - * 服务商 - */ - private String service; - -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java deleted file mode 100644 index 2e257e8..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/bo/SysOssConfigBo.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.ruoyi.system.domain.bo; - -import com.ruoyi.common.core.domain.BaseEntity; -import com.ruoyi.common.core.validate.AddGroup; -import com.ruoyi.common.core.validate.EditGroup; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; - -/** - * 对象存储配置业务对象 sys_oss_config - * - * @author Lion Li - * @author 孤舟烟雨 - * @date 2021-08-13 - */ - -@Data -@EqualsAndHashCode(callSuper = true) -public class SysOssConfigBo extends BaseEntity { - - /** - * 主建 - */ - @NotNull(message = "主建不能为空", groups = {EditGroup.class}) - private Long ossConfigId; - - /** - * 配置key - */ - @NotBlank(message = "配置key不能为空", groups = {AddGroup.class, EditGroup.class}) - @Size(min = 2, max = 100, message = "configKey长度必须介于{min}和{max} 之间") - private String configKey; - - /** - * accessKey - */ - @NotBlank(message = "accessKey不能为空", groups = {AddGroup.class, EditGroup.class}) - @Size(min = 2, max = 100, message = "accessKey长度必须介于{min}和{max} 之间") - private String accessKey; - - /** - * 秘钥 - */ - @NotBlank(message = "secretKey不能为空", groups = {AddGroup.class, EditGroup.class}) - @Size(min = 2, max = 100, message = "secretKey长度必须介于{min}和{max} 之间") - private String secretKey; - - /** - * 桶名称 - */ - @NotBlank(message = "桶名称不能为空", groups = {AddGroup.class, EditGroup.class}) - @Size(min = 2, max = 100, message = "bucketName长度必须介于{min}和{max}之间") - private String bucketName; - - /** - * 前缀 - */ - private String prefix; - - /** - * 访问站点 - */ - @NotBlank(message = "访问站点不能为空", groups = {AddGroup.class, EditGroup.class}) - @Size(min = 2, max = 100, message = "endpoint长度必须介于{min}和{max}之间") - private String endpoint; - - /** - * 自定义域名 - */ - private String domain; - - /** - * 是否https(Y=是,N=否) - */ - private String isHttps; - - /** - * 是否默认(0=是,1=否) - */ - private String status; - - /** - * 域 - */ - private String region; - - /** - * 扩展字段 - */ - private String ext1; - - /** - * 备注 - */ - private String remark; - - /** - * 桶权限类型(0private 1public 2custom) - */ - @NotBlank(message = "桶权限类型不能为空", groups = {AddGroup.class, EditGroup.class}) - private String accessPolicy; - -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java deleted file mode 100644 index 9edc60c..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssConfigVo.java +++ /dev/null @@ -1,90 +0,0 @@ -package com.ruoyi.system.domain.vo; - -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import lombok.Data; - - -/** - * 对象存储配置视图对象 sys_oss_config - * - * @author Lion Li - * @author 孤舟烟雨 - * @date 2021-08-13 - */ -@Data -@ExcelIgnoreUnannotated -public class SysOssConfigVo { - - private static final long serialVersionUID = 1L; - - /** - * 主建 - */ - private Long ossConfigId; - - /** - * 配置key - */ - private String configKey; - - /** - * accessKey - */ - private String accessKey; - - /** - * 秘钥 - */ - private String secretKey; - - /** - * 桶名称 - */ - private String bucketName; - - /** - * 前缀 - */ - private String prefix; - - /** - * 访问站点 - */ - private String endpoint; - - /** - * 自定义域名 - */ - private String domain; - - /** - * 是否https(Y=是,N=否) - */ - private String isHttps; - - /** - * 域 - */ - private String region; - - /** - * 是否默认(0=是,1=否) - */ - private String status; - - /** - * 扩展字段 - */ - private String ext1; - - /** - * 备注 - */ - private String remark; - - /** - * 桶权限类型(0private 1public 2custom) - */ - private String accessPolicy; - -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssVo.java b/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssVo.java deleted file mode 100644 index 53f5f8d..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/domain/vo/SysOssVo.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.ruoyi.system.domain.vo; - -import lombok.Data; - -import java.util.Date; - -/** - * OSS对象存储视图对象 sys_oss - * - * @author Lion Li - */ -@Data -public class SysOssVo { - - private static final long serialVersionUID = 1L; - - /** - * 对象存储主键 - */ - private Long ossId; - - /** - * 文件名 - */ - private String fileName; - - /** - * 原名 - */ - private String originalName; - - /** - * 文件后缀名 - */ - private String fileSuffix; - - /** - * URL地址 - */ - private String url; - - /** - * 创建时间 - */ - private Date createTime; - - /** - * 上传人 - */ - private String createBy; - - /** - * 服务商 - */ - private String service; - - -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssConfigMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssConfigMapper.java deleted file mode 100644 index 72f29a7..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssConfigMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.ruoyi.system.mapper; - -import com.ruoyi.common.core.mapper.BaseMapperPlus; -import com.ruoyi.system.domain.SysOssConfig; -import com.ruoyi.system.domain.vo.SysOssConfigVo; - -/** - * 对象存储配置Mapper接口 - * - * @author Lion Li - * @author 孤舟烟雨 - * @date 2021-08-13 - */ -public interface SysOssConfigMapper extends BaseMapperPlus { - -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssMapper.java deleted file mode 100644 index edbaed6..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysOssMapper.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.ruoyi.system.mapper; - -import com.ruoyi.common.core.mapper.BaseMapperPlus; -import com.ruoyi.system.domain.SysOss; -import com.ruoyi.system.domain.vo.SysOssVo; - -/** - * 文件上传 数据层 - * - * @author Lion Li - */ -public interface SysOssMapper extends BaseMapperPlus { -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/runner/SystemApplicationRunner.java b/ruoyi-system/src/main/java/com/ruoyi/system/runner/SystemApplicationRunner.java index e9ad6ee..ffeb516 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/runner/SystemApplicationRunner.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/runner/SystemApplicationRunner.java @@ -3,7 +3,6 @@ package com.ruoyi.system.runner; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.system.service.ISysConfigService; import com.ruoyi.system.service.ISysDictTypeService; -import com.ruoyi.system.service.ISysOssConfigService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.ApplicationArguments; @@ -23,11 +22,10 @@ public class SystemApplicationRunner implements ApplicationRunner { private final RuoYiConfig ruoyiConfig; private final ISysConfigService configService; private final ISysDictTypeService dictTypeService; - private final ISysOssConfigService ossConfigService; @Override public void run(ApplicationArguments args) throws Exception { - ossConfigService.init(); + log.info("初始化OSS配置成功"); if (ruoyiConfig.isCacheLazy()) { return; diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/FileService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/FileService.java deleted file mode 100644 index 0d3b3fa..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/FileService.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.ruoyi.system.service; - -import cn.hutool.core.date.DateUtil; -import cn.hutool.core.img.Img; -import cn.hutool.core.lang.UUID; -import cn.hutool.core.util.StrUtil; - -import java.awt.Image; -import java.awt.image.BufferedImage; -import java.io.*; -import java.nio.ByteBuffer; -import java.util.Calendar; - -import cn.hutool.crypto.SecureUtil; -import cn.hutool.crypto.symmetric.SymmetricAlgorithm; -import cn.hutool.crypto.symmetric.SymmetricCrypto; -import com.ruoyi.common.utils.IdUtils; -import com.ruoyi.common.utils.spring.SpringUtils; -import org.springframework.web.multipart.MultipartFile; - -/** - *
- * - 本地文件保存服务
- * Author : J.L.Zhou
- * E-Mail : 2233875735@qq.com
- * Tel : 151 1104 7708
- * Date : 2021-6-1 11:48:16
- * Version : 1.0
- * Copyright 2021 jlzhou.top Inc. All rights reserved.
- * Warning: this content is only for internal circulation of the company.
- *          It is forbidden to divulge it or use it for other commercial purposes.
- * 
- */ -public interface FileService { - - - - /** - * - 保存文件,文件名随机 - * - * @param in - * @param filename - 原文件名 - * @param pre - 存放路径前缀 - * @return - 保存的文件URL - * @throws 400-499 - */ - default String save(InputStream in, String filename, String pre) { - return save(in, filename, pre, true); - } - - /** - * - * @param in - * @param filename - 原文件名 - * @param pre - 存放路径前缀 - * @param randomName - 是否随机文件名 - * @return - * @throws 400-499 - */ - default String save(InputStream in, String filename, String pre, boolean randomName) { - if (StrUtil.isNotBlank(pre) && !pre.matches("^[\\w\\/]+$")) { - throw new RuntimeException("存放路径前缀只能有单词字符组成"); - } - try { - if (in == null || in.available() == 0) { - throw new RuntimeException("读取上传文件长度错误"); - } - } catch (IOException e) { - throw new RuntimeException("读取上传文件长度错误", e); - } - if (StrUtil.isEmpty(filename)) { - throw new RuntimeException("文件名为空"); - } - filename = filename.replace(" ", ""); - String name = (StrUtil.isNotBlank(pre)?(pre+"/"):"") - + generateURI("{yyyy}/{MM}/{dd}/{id36}" + (randomName ? ".{ext}" : "/{filename}.{ext}"), filename); - - return save(in, name); - - } - - String save(InputStream in, String filename); - - /** - * - 删除文件 - * - * @param file 文件的URL - */ - void delete(String file); - - /** - * 获取保存的文件对象 - * @param file - * @return - */ - File getFile(String file); - - /** - *
-	 * - 根据存放规则和文件名生成存放的URI
-	 * - 支持
-	 * 	- {yyyy}/{MM}/{dd}/{HH}/{mm}/{ss} 年月日时分秒
-	 * 	- {UUID} 32位的唯一标志
-	 * 	- {id} int类型的唯一id,防止重复建议+年月日路径
-	 * 	- {id16} int类型的唯一id16进制表示,防止重复建议+年月日路径
-	 * 	- {id36} int类型的唯一id36进制表示,防止重复建议+年月日路径
-	 * 	- {filename} 文件基础名称
-	 * 	- {ext} 扩展名
-	 * 
- * - * @param rule - * @param filename - * @return - */ - default String generateURI(String rule, String filename) { - return SpringUtils.getBean(ISysOssService.class).generateURI(rule, filename); - } - - - -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssConfigService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssConfigService.java deleted file mode 100644 index 80d874f..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssConfigService.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.ruoyi.system.service; - -import com.ruoyi.common.core.domain.PageQuery; -import com.ruoyi.common.core.page.TableDataInfo; -import com.ruoyi.system.domain.bo.SysOssConfigBo; -import com.ruoyi.system.domain.vo.SysOssConfigVo; - -import java.util.Collection; - -/** - * 对象存储配置Service接口 - * - * @author Lion Li - * @author 孤舟烟雨 - * @date 2021-08-13 - */ -public interface ISysOssConfigService { - - /** - * 初始化OSS配置 - */ - void init(); - - /** - * 查询单个 - */ - SysOssConfigVo queryById(Long ossConfigId); - - /** - * 查询列表 - */ - TableDataInfo queryPageList(SysOssConfigBo bo, PageQuery pageQuery); - - - /** - * 根据新增业务对象插入对象存储配置 - * - * @param bo 对象存储配置新增业务对象 - * @return - */ - Boolean insertByBo(SysOssConfigBo bo); - - /** - * 根据编辑业务对象修改对象存储配置 - * - * @param bo 对象存储配置编辑业务对象 - * @return - */ - Boolean updateByBo(SysOssConfigBo bo); - - /** - * 校验并删除数据 - * - * @param ids 主键集合 - * @param isValid 是否校验,true-删除前校验,false-不校验 - * @return - */ - Boolean deleteWithValidByIds(Collection ids, Boolean isValid); - - /** - * 启用停用状态 - */ - int updateOssConfigStatus(SysOssConfigBo bo); - -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java deleted file mode 100644 index aae7bd8..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ISysOssService.java +++ /dev/null @@ -1,396 +0,0 @@ -package com.ruoyi.system.service; - -import cn.hutool.core.date.DateUtil; -import cn.hutool.core.img.Img; -import cn.hutool.core.lang.UUID; -import cn.hutool.core.util.HexUtil; -import cn.hutool.crypto.SecureUtil; -import cn.hutool.crypto.symmetric.SymmetricAlgorithm; -import cn.hutool.crypto.symmetric.SymmetricCrypto; -import com.ruoyi.common.core.domain.PageQuery; -import com.ruoyi.common.core.page.TableDataInfo; -import com.ruoyi.common.helper.DataPermissionHelper; -import com.ruoyi.common.utils.IdUtils; -import com.ruoyi.system.domain.SysOss; -import com.ruoyi.system.domain.bo.SysOssBo; -import com.ruoyi.system.domain.vo.SysOssVo; -import org.slf4j.LoggerFactory; -import org.springframework.web.multipart.MultipartFile; - -import javax.servlet.http.HttpServletResponse; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.function.Supplier; - -/** - * 文件上传 服务层 - * - * @author Lion Li - */ -public interface ISysOssService { - - /** - * 服务商 - */ - public static enum Service { - minio, qiniu, aliyun, qcloud, image, upload; - } - - String IMAGE_WEBP = "webp"; - - /** - * 默认路径前缀 - */ - String PRE_DEFAULT = "default"; - - /** - * 设置指定服务商 - * - * @param handle - */ - void setService(Service service, Runnable handle); - - public T setService(Service service, Supplier handle); - - /** - * 只保存,不存放记录 - * - * @param handle 处理执行方法 - */ - void ignore(Runnable handle); - - - /** - * 只保存,不存放记录 - * - * @param handle 处理执行方法 - */ - public T ignore(Supplier handle); - - - TableDataInfo queryPageList(SysOssBo sysOss, PageQuery pageQuery); - - List listByIds(Collection ossIds); - - SysOssVo getById(Long ossId); - - default SysOssVo upload(MultipartFile file) { - return upload(file, PRE_DEFAULT); - } - - /** - * @param file - * @param pre - 图片保存路径前缀 - * @return - */ - default SysOssVo upload(MultipartFile file, String pre) { - if (file == null || file.isEmpty()) { - throw new RuntimeException("文件不能为空"); - } - try { - return save(file.getInputStream(), file.getOriginalFilename(), file.getContentType(), pre); - } catch (IOException e) { - throw new RuntimeException(e.getMessage(), e); - } - } - - - /** - * 保存文件, rule:{yyyy}/{MM}/{dd}/{id36}.{ext} - * - * @param in 输入流 - * @param filename 文件名 - * @param contentType 文件类型 - * @param pre 路径前缀 - * @return - */ - default SysOssVo save(InputStream in, String filename, String contentType, String pre) { - return save(in, filename, contentType, pre, "{yyyy}/{MM}/{dd}/{id36}.{ext}"); - } - - /** - * 保存文件,保留文件名,rule:{yyyy}/{MM}/{dd}/{id36}/{filename}.{ext} - * - * @param in 输入流 - * @param filename 文件名 - * @param contentType 文件类型 - * @param pre 路径前缀 - * @return - */ - default SysOssVo saveFilename(InputStream in, String filename, String contentType, String pre) { - return save(in, filename, contentType, pre, "{yyyy}/{MM}/{dd}/{id36}/{filename}.{ext}"); - } - - /** - * 保存文件,存放路径规则rule: - *
-   * - 根据存放规则和文件名生成存放的URI
-   * - 支持
-   * 	- {yyyy}/{MM}/{dd}/{HH}/{mm}/{ss} 年月日时分秒
-   * 	- {UUID} 32位的唯一标志
-   * 	- {i} 自增id
-   * 	- {id} 当日int类型的唯一id,防止重复建议+年月日路径
-   * 	- {id16} 当日int类型的唯一id16进制表示,防止重复建议+年月日路径
-   * 	- {id36} 当日int类型的唯一id36进制表示,防止重复建议+年月日路径
-   * 	- {filename} 文件基础名称
-   * 	- {ext} 扩展名
-   * 
- * - * @param in 输入流 - * @param filename 文件名 - * @param contentType 文件类型 - * @param pre 路径前缀 - * @param rule 路径规则 - * @return - */ - SysOssVo save(InputStream in, String filename, String contentType, String pre, String rule); - - SysOssVo uploadImgs(MultipartFile file, String pre); - - default SysOssVo uploadImgs(MultipartFile file, String pre, int maxWidth, int maxHeight, BufferedImage watermark) { - if (file == null || file.isEmpty()) { - throw new RuntimeException("图片不能为空"); - } - try { - return uploadImgs(file.getInputStream(), pre, maxWidth, maxHeight, watermark); - } catch (IOException e) { - throw new RuntimeException(e.getMessage(), e); - } - } - - /** - * @param in - * @param pre - 图片保存路径前缀 - * @param maxWidth - 缩放到指定的宽 小于1表示不缩放 - * @param maxHeight - 缩放到指定的高 小于1表示不缩放 - * @param watermark - 水印图片 null表示不加水印 - * @return - */ - SysOssVo uploadImgs(InputStream in, String pre, int maxWidth, int maxHeight, - BufferedImage watermark); - - void download(Long ossId, HttpServletResponse response) throws IOException; - - void download(String url, Service service, HttpServletResponse response) throws IOException; - - InputStream download(Long ossId) throws IOException; - - InputStream download(SysOssVo sysOss) throws IOException; - - InputStream download(String url, Service service) throws IOException; - - Boolean deleteWithValidByIds(Collection ids, Boolean isValid); - - - default InputStream formatImage(MultipartFile file, int maxWidth, int maxHeight, - BufferedImage watermark) { - try { - if (file == null || file.isEmpty()) { - throw new RuntimeException("上传图片不能为空"); - } - if (!file.getContentType().startsWith("image/")) { - throw new RuntimeException("上传的文件不是图片"); - } - return formatImage(file.getInputStream(), maxWidth, maxHeight, watermark); - } catch (IOException e) { - throw new RuntimeException("处理图片错误", e); - } - } - - default InputStream formatImage(Image img, int maxWidth, int maxHeight, BufferedImage watermark) { - if (img == null) { - throw new RuntimeException("图片不能为空"); - } - return formatImage(Img.from(img), maxWidth, maxHeight, watermark); - } - - default InputStream formatImage(InputStream in, int maxWidth, int maxHeight, BufferedImage watermark) { - return formatImage(Img.from(in), maxWidth, maxHeight, watermark); - } - - default InputStream formatImage(Img img, int maxWidth, int maxHeight, BufferedImage watermark) { - try { - int w = img.getImg().getWidth(null); - int h = img.getImg().getHeight(null); - if (maxWidth > 0 && maxHeight > 0) { - if (w > maxWidth || h > maxHeight) { - int outWidth = 0; - int outHeight = 0; - outHeight = maxWidth * h / w; - if (outHeight > maxHeight) { - outHeight = maxHeight; - outWidth = outHeight * w / h; - } else { - outWidth = maxWidth; - } - img = img.scale(outWidth, outHeight); - w = outWidth; - h = outHeight; - - } - } - - - if (watermark != null) { - int ww = watermark.getWidth(null); - int wh = watermark.getHeight(null); - if (w > ww && h > wh) { - img = img.pressImage(watermark, 0, 0, 1f); - } - } - - ByteArrayOutputStream pout = new ByteArrayOutputStream(); - - img.setTargetImageType(IMAGE_WEBP).write(pout); - - ByteArrayInputStream pin = new ByteArrayInputStream(pout.toByteArray()); - pout.close(); - pout = null; - return pin; - } catch (Exception e) { - throw new RuntimeException("处理图片错误", e); - } - } - - - /** - *
-   * - 根据存放规则和文件名生成存放的URI
-   * - 支持
-   * 	- {yyyy}/{MM}/{dd}/{HH}/{mm}/{ss} 年月日时分秒
-   * 	- {UUID} 32位的唯一标志
-   *  - {i} 自增id
-   * 	- {id} 当日int类型的唯一id,防止重复建议+年月日路径
-   * 	- {id16} 当日int类型的唯一id16进制表示,防止重复建议+年月日路径
-   * 	- {id36} 当日int类型的唯一id36进制表示,防止重复建议+年月日路径
-   * 	- {filename} 文件基础名称
-   * 	- {ext} 扩展名
-   * 
- * - * @param rule - * @param filename - * @return - */ - default String generateURI(String rule, String filename) { - Calendar c = Calendar.getInstance(); - if (rule.contains("{yyyy}")) { - rule = rule.replace("{yyyy}", "" + c.get(Calendar.YEAR)); - } - if (rule.contains("{MM}")) { - rule = rule.replace("{MM}", String.format("%02d", c.get(Calendar.MONTH) + 1)); - } - if (rule.contains("{dd}")) { - rule = rule.replace("{dd}", String.format("%02d", c.get(Calendar.DATE))); - } - if (rule.contains("{HH}")) { - rule = rule.replace("{HH}", String.format("%02d", c.get(Calendar.HOUR_OF_DAY))); - } - if (rule.contains("{mm}")) { - rule = rule.replace("{mm}", String.format("%02d", c.get(Calendar.MINUTE))); - } - if (rule.contains("{ss}")) { - rule = rule.replace("{ss}", String.format("%02d", c.get(Calendar.SECOND))); - } - if (rule.contains("{UUID}")) { - rule = rule.replace("{UUID}", UUID.fastUUID().toString(true)); - } - if (rule.contains("{i}")) { - rule = rule.replace("{i}", IdUtils.nextId(Id.groupName).toString()); - } - if (rule.contains("{id}")) { - rule = rule.replace("{id}", Long.toString(id.nextId())); - } - if (rule.contains("{id16}")) { - rule = rule.replace("{id16}", Long.toString(id.nextId(), 16)); - } - if (rule.contains("{id36}")) { - rule = rule.replace("{id36}", Long.toString(id.nextId(), 36)); - } - if (rule.contains("{filename}")) { - String temp = null; - if (filename.contains(".")) { - temp = filename.substring(0, filename.lastIndexOf(".")); - } else { - temp = filename; - } - rule = rule.replace("{filename}", temp); - } - - if (rule.contains("{ext}")) { - String temp = null; - if (filename.contains(".")) { - temp = filename.substring(filename.lastIndexOf(".") + 1); - } else { - temp = ""; - } - rule = rule.replace("{ext}", temp.toLowerCase()); - } - return rule; - } - - Id id = new Id(); - - static class Id { - - private static final String groupName = "file:id"; - - private SymmetricCrypto crypto; - - private String today = DateUtil.today(); - - public synchronized Long nextId() { - if (crypto == null || !today.equals(DateUtil.today())) { - today = DateUtil.today(); - byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(), today.getBytes()).getEncoded(); - crypto = new SymmetricCrypto(SymmetricAlgorithm.DES, key); - } - ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES); - buffer.putInt(IdUtils.nextDayId(groupName).intValue()); - return Math.abs(ByteBuffer.wrap(crypto.encrypt(buffer.array())).getLong()); - } - - } - -// public static void main(String[] args) { -// byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DES.getValue(),DateUtil.today().getBytes()).getEncoded(); -// System.out.println(HexUtil.encodeHexStr(key)); -//// byte[] key = HexUtil.decodeHex("a359f3fe88445c192f20d573c80af163"); -// SymmetricCrypto aes = new SymmetricCrypto(SymmetricAlgorithm.DES, key); -// -// -// ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES); -// -// System.out.println(buffer.array().length); -// System.out.println(aes.encrypt(buffer.array()).length); -// buffer.clear(); -// buffer.putInt(1); -// System.out.println(ByteBuffer.wrap(aes.encrypt(buffer.array())).getLong()); -// buffer.clear(); -// buffer.putInt(2); -// System.out.println(ByteBuffer.wrap(aes.encrypt(buffer.array())).getLong()); -// buffer.clear(); -// buffer.putInt(3); -// System.out.println(ByteBuffer.wrap(aes.encrypt(buffer.array())).getLong()); -// } - - SysOssVo url(SysOssVo oss, int second); - - default SysOssVo url(SysOssVo oss) { - return url(oss, 120); - } - - String url(Service service,String url,int second); - - default String url(Service service,String url) { - return url(service,url, 120); - } -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java index 21fdd61..cfd3a89 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/SysLoginService.java @@ -8,7 +8,6 @@ import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.ruoyi.common.config.RuoYiConfig; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.constant.Constants; @@ -16,11 +15,9 @@ import com.ruoyi.common.core.domain.event.LogininforEvent; import com.ruoyi.common.core.domain.dto.RoleDTO; import com.ruoyi.common.core.domain.entity.SysUser; import com.ruoyi.common.core.domain.model.LoginUser; -import com.ruoyi.common.core.domain.model.XcxLoginUser; import com.ruoyi.common.enums.DeviceType; import com.ruoyi.common.enums.LoginType; import com.ruoyi.common.enums.UserStatus; -import com.ruoyi.common.enums.UserType; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.exception.user.CaptchaException; import com.ruoyi.common.exception.user.CaptchaExpireException; @@ -63,8 +60,6 @@ public class SysLoginService { private final IdentifierGenerator id; - private final ISysOssService ossService; - private final RuoYiConfig config; @@ -139,7 +134,7 @@ public class SysLoginService { } String url = null; try { - url = ossService.uploadImgs(avatar,"avatar",400,400,null).getUrl(); +// url = ossService.uploadImgs(avatar,"avatar",400,400,null).getUrl(); }catch (Exception e){ log.debug("保存头像失败",e); // throw new RuntimeException("保存头像失败",e); diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/FileServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/FileServiceImpl.java deleted file mode 100644 index 1470632..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/FileServiceImpl.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.ruoyi.system.service.impl; - -import cn.hutool.core.io.IoUtil; -import cn.hutool.core.util.StrUtil; -import com.ruoyi.common.config.RuoYiConfig; -import com.ruoyi.system.service.FileService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; - -/** - *
- * - 文件存储的本地存储实现
- * Author : J.L.Zhou
- * E-Mail : 2233875735@qq.com
- * Tel : 151 1104 7708
- * Date : 2021-09-16 14:17
- * Version : 1.0
- * Copyright 2021 jlzhou.top Inc. All rights reserved.
- * Warning: this content is only for internal circulation of the company.
- *          It is forbidden to divulge it or use it for other commercial purposes.
- * 
- **/ -@Service("upload") -@RequiredArgsConstructor -@Slf4j -public class FileServiceImpl implements FileService { - - public final RuoYiConfig config; - - @Override - public void delete(String filename) { - if (StrUtil.isEmpty(filename)) { - throw new RuntimeException("文件名不能为空"); - } - try { - if (filename.startsWith(config.upload.pre)) { - filename = filename.substring(config.upload.pre.length()); - } - if (filename.indexOf("?") > -1) { - filename = filename.substring(0, filename.indexOf("?")); - } - File file = new File(config.upload.savePath, filename); - log.debug("file:"+file.exists()); - file.delete(); - log.info("删除:{}",file.getAbsolutePath()); - } catch (Exception e) { - throw new RuntimeException("删除错误", e); - } - } - - @Override - public File getFile(String filename) { - if (StrUtil.isEmpty(filename)) { - throw new RuntimeException("文件名不能为空"); - } - - if (filename.startsWith(config.upload.pre)) { - filename = filename.substring(config.upload.pre.length()); - } - if (filename.indexOf("?") > -1) { - filename = filename.substring(0, filename.indexOf("?")); - } - return new File(config.upload.savePath, filename); - } - - @Override - public String save(InputStream in, String filename) { - File file = new File(config.upload.savePath, filename); - file.getParentFile().mkdirs(); - try( - FileOutputStream out = new FileOutputStream(file); - ) { - IoUtil.copy(in, out); - return config.upload.pre + "/" + filename; - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java deleted file mode 100644 index 2e90e50..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssConfigServiceImpl.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.ruoyi.system.service.impl; - -import cn.hutool.core.bean.BeanUtil; -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.util.ObjectUtil; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.ruoyi.common.constant.CacheNames; -import com.ruoyi.common.core.domain.PageQuery; -import com.ruoyi.common.core.page.TableDataInfo; -import com.ruoyi.common.exception.ServiceException; -import com.ruoyi.common.utils.JsonUtils; -import com.ruoyi.common.utils.StringUtils; -import com.ruoyi.common.utils.redis.CacheUtils; -import com.ruoyi.common.utils.redis.RedisUtils; -import com.ruoyi.oss.constant.OssConstant; -import com.ruoyi.system.domain.SysOssConfig; -import com.ruoyi.system.domain.bo.SysOssConfigBo; -import com.ruoyi.system.domain.vo.SysOssConfigVo; -import com.ruoyi.system.mapper.SysOssConfigMapper; -import com.ruoyi.system.service.ISysOssConfigService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Collection; -import java.util.List; - -/** - * 对象存储配置Service业务层处理 - * - * @author Lion Li - * @author 孤舟烟雨 - * @date 2021-08-13 - */ -@Slf4j -@RequiredArgsConstructor -@Service -public class SysOssConfigServiceImpl implements ISysOssConfigService { - - private final SysOssConfigMapper baseMapper; - - /** - * 项目启动时,初始化参数到缓存,加载配置类 - */ - @Override - public void init() { - List list = baseMapper.selectList(); - // 加载OSS初始化配置 - for (SysOssConfig config : list) { - String configKey = config.getConfigKey(); - if ("0".equals(config.getStatus())) { - RedisUtils.setCacheObject(OssConstant.DEFAULT_CONFIG_KEY, configKey); - } - CacheUtils.put(CacheNames.SYS_OSS_CONFIG, config.getConfigKey(), JsonUtils.toJsonString(config)); - } - } - - @Override - public SysOssConfigVo queryById(Long ossConfigId) { - return baseMapper.selectVoById(ossConfigId); - } - - @Override - public TableDataInfo queryPageList(SysOssConfigBo bo, PageQuery pageQuery) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); - return TableDataInfo.build(result); - } - - - private LambdaQueryWrapper buildQueryWrapper(SysOssConfigBo bo) { - LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.eq(StringUtils.isNotBlank(bo.getConfigKey()), SysOssConfig::getConfigKey, bo.getConfigKey()); - lqw.like(StringUtils.isNotBlank(bo.getBucketName()), SysOssConfig::getBucketName, bo.getBucketName()); - lqw.eq(StringUtils.isNotBlank(bo.getStatus()), SysOssConfig::getStatus, bo.getStatus()); - return lqw; - } - - @Override - public Boolean insertByBo(SysOssConfigBo bo) { - SysOssConfig config = BeanUtil.toBean(bo, SysOssConfig.class); - validEntityBeforeSave(config); - boolean flag = baseMapper.insert(config) > 0; - if (flag) { - CacheUtils.put(CacheNames.SYS_OSS_CONFIG, config.getConfigKey(), JsonUtils.toJsonString(config)); - } - return flag; - } - - @Override - public Boolean updateByBo(SysOssConfigBo bo) { - SysOssConfig config = BeanUtil.toBean(bo, SysOssConfig.class); - validEntityBeforeSave(config); - LambdaUpdateWrapper luw = new LambdaUpdateWrapper<>(); - luw.set(ObjectUtil.isNull(config.getPrefix()), SysOssConfig::getPrefix, ""); - luw.set(ObjectUtil.isNull(config.getRegion()), SysOssConfig::getRegion, ""); - luw.set(ObjectUtil.isNull(config.getExt1()), SysOssConfig::getExt1, ""); - luw.set(ObjectUtil.isNull(config.getRemark()), SysOssConfig::getRemark, ""); - luw.eq(SysOssConfig::getOssConfigId, config.getOssConfigId()); - boolean flag = baseMapper.update(config, luw) > 0; - if (flag) { - CacheUtils.put(CacheNames.SYS_OSS_CONFIG, config.getConfigKey(), JsonUtils.toJsonString(config)); - } - return flag; - } - - /** - * 保存前的数据校验 - */ - private void validEntityBeforeSave(SysOssConfig entity) { - if (StringUtils.isNotEmpty(entity.getConfigKey()) && !checkConfigKeyUnique(entity)) { - throw new ServiceException("操作配置'" + entity.getConfigKey() + "'失败, 配置key已存在!"); - } - } - - @Override - public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { - if (isValid) { - if (CollUtil.containsAny(ids, OssConstant.SYSTEM_DATA_IDS)) { - throw new ServiceException("系统内置, 不可删除!"); - } - } - List list = CollUtil.newArrayList(); - for (Long configId : ids) { - SysOssConfig config = baseMapper.selectById(configId); - list.add(config); - } - boolean flag = baseMapper.deleteBatchIds(ids) > 0; - if (flag) { - list.forEach(sysOssConfig -> - CacheUtils.evict(CacheNames.SYS_OSS_CONFIG, sysOssConfig.getConfigKey())); - } - return flag; - } - - /** - * 判断configKey是否唯一 - */ - private boolean checkConfigKeyUnique(SysOssConfig sysOssConfig) { - long ossConfigId = ObjectUtil.isNull(sysOssConfig.getOssConfigId()) ? -1L : sysOssConfig.getOssConfigId(); - SysOssConfig info = baseMapper.selectOne(new LambdaQueryWrapper() - .select(SysOssConfig::getOssConfigId, SysOssConfig::getConfigKey) - .eq(SysOssConfig::getConfigKey, sysOssConfig.getConfigKey())); - if (ObjectUtil.isNotNull(info) && info.getOssConfigId() != ossConfigId) { - return false; - } - return true; - } - - /** - * 启用禁用状态 - */ - @Override - @Transactional(rollbackFor = Exception.class) - public int updateOssConfigStatus(SysOssConfigBo bo) { - SysOssConfig sysOssConfig = BeanUtil.toBean(bo, SysOssConfig.class); - int row = baseMapper.update(null, new LambdaUpdateWrapper() - .set(SysOssConfig::getStatus, "1")); - row += baseMapper.updateById(sysOssConfig); - if (row > 0) { - RedisUtils.setCacheObject(OssConstant.DEFAULT_CONFIG_KEY, sysOssConfig.getConfigKey()); - } - return row; - } - -} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java deleted file mode 100644 index 9cb69b3..0000000 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysOssServiceImpl.java +++ /dev/null @@ -1,399 +0,0 @@ -package com.ruoyi.system.service.impl; - -import cn.hutool.core.convert.Convert; -import cn.hutool.core.io.IoUtil; -import cn.hutool.core.io.file.FileNameUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.ruoyi.common.constant.CacheNames; -import com.ruoyi.common.core.domain.PageQuery; -import com.ruoyi.common.core.page.TableDataInfo; -import com.ruoyi.common.core.service.OssService; -import com.ruoyi.common.exception.ServiceException; -import com.ruoyi.common.utils.BeanCopyUtils; -import com.ruoyi.common.utils.StringUtils; -import com.ruoyi.common.utils.file.FileUtils; -import com.ruoyi.common.utils.spring.SpringUtils; -import com.ruoyi.oss.core.OssClient; -import com.ruoyi.oss.entity.UploadResult; -import com.ruoyi.oss.enumd.AccessPolicyType; -import com.ruoyi.oss.factory.OssFactory; -import com.ruoyi.system.domain.SysOss; -import com.ruoyi.system.domain.bo.SysOssBo; -import com.ruoyi.system.domain.vo.SysOssVo; -import com.ruoyi.system.mapper.SysOssMapper; -import com.ruoyi.system.service.FileService; -import com.ruoyi.system.service.ISysConfigService; -import com.ruoyi.system.service.ISysOssService; -import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.http.MediaType; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import javax.servlet.http.HttpServletResponse; -import java.awt.image.BufferedImage; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.*; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -/** - * 文件上传 服务层实现 - * - * @author Lion Li - */ -@RequiredArgsConstructor -@Service -public class SysOssServiceImpl implements ISysOssService, OssService { - - private static final String UPLOAD = "UPLOAD"; - private final ISysConfigService configService; - private final FileService fileService; - private final SysOssMapper baseMapper; - private static ThreadLocal IGNORE_THREAD_LOCAL = new ThreadLocal<>(); - private static ThreadLocal SERVICE_THREAD_LOCAL = new ThreadLocal<>(); - - @Override - public void setService(Service service, Runnable handle) { - SERVICE_THREAD_LOCAL.set(service); - try { - handle.run(); - } finally { - SERVICE_THREAD_LOCAL.remove(); - } - } - - @Override - public T setService(Service service, Supplier handle) { - SERVICE_THREAD_LOCAL.set(service); - try { - return handle.get(); - } finally { - SERVICE_THREAD_LOCAL.remove(); - } - } - - @Override - public void ignore(Runnable handle) { - IGNORE_THREAD_LOCAL.set(true); - try { - handle.run(); - } finally { - IGNORE_THREAD_LOCAL.remove(); - } - } - - @Override - public T ignore(Supplier handle) { - IGNORE_THREAD_LOCAL.set(true); - try { - return handle.get(); - } finally { - IGNORE_THREAD_LOCAL.remove(); - } - } - - @Override - public TableDataInfo queryPageList(SysOssBo bo, PageQuery pageQuery) { - LambdaQueryWrapper lqw = buildQueryWrapper(bo); - Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); - List filterResult = result.getRecords().stream().map(this::url).collect(Collectors.toList()); - result.setRecords(filterResult); - return TableDataInfo.build(result); - } - - @Override - public List listByIds(Collection ossIds) { - List list = new ArrayList<>(); - for (Long id : ossIds) { - SysOssVo vo = SpringUtils.getBean(ISysOssService.class).getById(id); - if (ObjectUtil.isNotNull(vo)) { - list.add(this.url(vo)); - } - } - return list; - } - - @Override - public String selectUrlByIds(String ossIds) { - List list = new ArrayList<>(); - for (Long id : StringUtils.splitTo(ossIds, Convert::toLong)) { - SysOssVo vo = SpringUtils.getBean(ISysOssService.class).getById(id); - if (ObjectUtil.isNotNull(vo)) { - list.add(this.url(vo).getUrl()); - } - } - return String.join(StringUtils.SEPARATOR, list); - } - - private LambdaQueryWrapper buildQueryWrapper(SysOssBo bo) { - Map params = bo.getParams(); - LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); - lqw.like(StringUtils.isNotBlank(bo.getFileName()), SysOss::getFileName, bo.getFileName()); - lqw.like(StringUtils.isNotBlank(bo.getOriginalName()), SysOss::getOriginalName, bo.getOriginalName()); - lqw.eq(StringUtils.isNotBlank(bo.getFileSuffix()), SysOss::getFileSuffix, bo.getFileSuffix()); - lqw.eq(StringUtils.isNotBlank(bo.getUrl()), SysOss::getUrl, bo.getUrl()); - lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null, - SysOss::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime")); - lqw.eq(StringUtils.isNotBlank(bo.getCreateBy()), SysOss::getCreateBy, bo.getCreateBy()); - lqw.eq(StringUtils.isNotBlank(bo.getService()), SysOss::getService, bo.getService()); - return lqw; - } - - @Cacheable(cacheNames = CacheNames.SYS_OSS, key = "#ossId") - @Override - public SysOssVo getById(Long ossId) { - return baseMapper.selectVoById(ossId); - } - - @Override - public void download(Long ossId, HttpServletResponse response) throws IOException { - SysOssVo sysOss = SpringUtils.getBean(ISysOssService.class).getById(ossId); - if (ObjectUtil.isNull(sysOss)) { - throw new ServiceException("文件数据不存在!"); - } - FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName()); - response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8"); - if (UPLOAD.equals(sysOss.getService())) { - try (InputStream inputStream = new FileInputStream(fileService.getFile(sysOss.getUrl()))) { - int available = inputStream.available(); - IoUtil.copy(inputStream, response.getOutputStream()); - response.setContentLength(available); - } catch (Exception e) { - throw new ServiceException(e.getMessage()); - } - } else { - OssClient storage = OssFactory.instance(sysOss.getService()); - try (InputStream inputStream = storage.getObjectContent(sysOss.getUrl())) { - int available = inputStream.available(); - IoUtil.copy(inputStream, response.getOutputStream()); - response.setContentLength(available); - } catch (Exception e) { - throw new ServiceException(e.getMessage()); - } - } - } - - public void download(String url, Service service, HttpServletResponse response) throws IOException { - FileUtils.setAttachmentResponseHeader(response, FileNameUtil.getName(url)); - response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8"); - if (UPLOAD.equalsIgnoreCase(service.name())) { - try (InputStream inputStream = new FileInputStream(fileService.getFile(url))) { - int available = inputStream.available(); - IoUtil.copy(inputStream, response.getOutputStream()); - response.setContentLength(available); - } catch (Exception e) { - throw new ServiceException(e.getMessage()); - } - } else { - OssClient storage = OssFactory.instance(service.name()); - try (InputStream inputStream = storage.getObjectContent(url)) { - int available = inputStream.available(); - IoUtil.copy(inputStream, response.getOutputStream()); - response.setContentLength(available); - } catch (Exception e) { - throw new ServiceException(e.getMessage()); - } - } - } - - @Override - public InputStream download(Long ossId) throws IOException { - SysOssVo sysOss = SpringUtils.getBean(ISysOssService.class).getById(ossId); - return download(sysOss); - } - - @Override - public InputStream download(SysOssVo sysOss) throws IOException { - if (ObjectUtil.isNull(sysOss)) { - throw new ServiceException("文件数据不存在!"); - } - if (UPLOAD.equals(sysOss.getService())) { - return new FileInputStream(fileService.getFile(sysOss.getUrl())); - } else { - OssClient storage = OssFactory.instance(sysOss.getService()); - return storage.getObjectContent(sysOss.getUrl()); - } - } - - @Override - public InputStream download(String url, Service service) throws IOException { - if (UPLOAD.equalsIgnoreCase(service.name())) { - return new FileInputStream(fileService.getFile(url)); - } else { - OssClient storage = OssFactory.instance(service.name()); - return storage.getObjectContent(url); - } - } - - - @Override - public SysOssVo save(InputStream in, String originalFileName, String contentType, String pre, String rule) { - String suffix = StringUtils.substring(originalFileName, originalFileName.lastIndexOf("."), originalFileName.length()); - SysOss oss = new SysOss(); - oss.setOriginalName(originalFileName); - oss.setFileSuffix(suffix); - if (configService.selectOssEnabled()) { - OssClient storage = SERVICE_THREAD_LOCAL.get() == null ? OssFactory.instance() : OssFactory.instance(SERVICE_THREAD_LOCAL.get().name()); - UploadResult uploadResult; - try { - String path = ""; - if (StrUtil.isNotBlank(storage.getProperties().getPrefix())) { - path += storage.getProperties().getPrefix() + "/"; - - } - if (StrUtil.isBlank(pre)) { - pre = PRE_DEFAULT; - } - path += pre + "/" + generateURI(rule, originalFileName); - uploadResult = storage.upload(in, path, contentType); - } catch (Exception e) { - throw new RuntimeException(e.getMessage(), e); - } - // 保存文件信息 - - oss.setUrl(uploadResult.getUrl()); - oss.setFileName(uploadResult.getFilename()); - oss.setService(storage.getConfigKey()); - - } else { - try { - String url = fileService.save(in, originalFileName, pre); - oss.setUrl(url); - oss.setFileName(url); - oss.setService(UPLOAD); - } catch (Exception e) { - throw new RuntimeException("保存文件失败", e); - } - } - if (IGNORE_THREAD_LOCAL.get() == null) { - baseMapper.insert(oss); - } - - SysOssVo sysOssVo = new SysOssVo(); - BeanCopyUtils.copy(oss, sysOssVo); - return sysOssVo; - } - - public SysOssVo uploadImgs(MultipartFile file, String pre) { - return uploadImgs(file, pre, configService.selectImageMaxWidth(), configService.selectImageMaxHeight(), configService.getWatermark()); - } - - @Override - public SysOssVo uploadImgs(InputStream inputStream, String pre, int maxWidth, int maxHeight, BufferedImage watermark) { - String originalFileName = "temp.webp"; - String suffix = StringUtils.substring(originalFileName, originalFileName.lastIndexOf("."), originalFileName.length()); - SysOss oss = new SysOss(); - oss.setFileSuffix(suffix); - oss.setOriginalName(originalFileName); - if (configService.selectOssEnabled()) { - - OssClient storage = SERVICE_THREAD_LOCAL.get() == null ? OssFactory.instance() : OssFactory.instance(SERVICE_THREAD_LOCAL.get().name()); - UploadResult uploadResult; - - try ( - InputStream in = formatImage(inputStream, maxWidth, maxWidth, watermark) - ) { - String path = ""; - if (StrUtil.isNotBlank(storage.getProperties().getPrefix())) { - - path += storage.getProperties().getPrefix() + "/"; - - } - if (StrUtil.isNotBlank(pre)) { - path += pre + "/"; - } - path += generateURI("{yyyy}/{MM}/{dd}/{id36}.webp", "a.webp"); - - uploadResult = storage.upload(in, path, "image/webp"); - } catch (IOException e) { - throw new RuntimeException(e.getMessage(), e); - } - // 保存文件信息 - - oss.setUrl(uploadResult.getUrl()); - oss.setFileName(uploadResult.getFilename()); - oss.setService(storage.getConfigKey()); - } else { - try ( - InputStream in = formatImage(inputStream, maxWidth, maxWidth, watermark) - ) { - String url = fileService.save(in, originalFileName + ".webp", pre); - oss.setUrl(url); - oss.setFileName(url); - oss.setService(UPLOAD); - } catch (IOException e) { - throw new RuntimeException(e.getMessage(), e); - } - } - if (IGNORE_THREAD_LOCAL.get() == null) { - baseMapper.insert(oss); - } - SysOssVo sysOssVo = new SysOssVo(); - BeanCopyUtils.copy(oss, sysOssVo); - return sysOssVo; - } - - @Override - public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { - if (isValid) { - // 做一些业务上的校验,判断是否需要校验 - } - List list = baseMapper.selectBatchIds(ids); - for (SysOss sysOss : list) { - if (UPLOAD.equals(sysOss.getService())) { - fileService.delete(sysOss.getUrl()); - } else { - OssClient storage = OssFactory.instance(sysOss.getService()); - storage.delete(sysOss.getUrl()); - } - } - return baseMapper.deleteBatchIds(ids) > 0; - } - - @Override - public SysOssVo url(SysOssVo oss, int second) { - if (UPLOAD.equals(oss.getService())) { - return oss; - } else { - oss.setUrl(query(oss.getService(), oss.getUrl(), second)); - return oss; - } - } - - private String query(String service, String url, int second) { - OssClient storage = OssFactory.instance(service); - // 仅修改桶类型为 private 的URL,临时URL时长为120s - if (AccessPolicyType.PRIVATE == storage.getAccessPolicy()) { - String old = url; - if (url.indexOf("://") > -1) { - url = url.substring(url.indexOf("/", url.indexOf("//") + 2)); - } - if (url.startsWith("/")) { - url = url.substring(1); - } - url = url.substring(url.indexOf("/") + 1); - - String queryString = storage.getPrivateUrl(url, second); - queryString = queryString.substring(queryString.indexOf("?")); - return old + queryString; - } - return url; - } - - @Override - public String url(Service service, String url, int second) { - if (Service.upload.equals(service)) { - return url; - } else { - return query(service.name(), url, second); - } - } - -} diff --git a/ruoyi-system/src/main/resources/mapper/system/SysOssConfigMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysOssConfigMapper.xml deleted file mode 100644 index 77dc40e..0000000 --- a/ruoyi-system/src/main/resources/mapper/system/SysOssConfigMapper.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ruoyi-system/src/main/resources/mapper/system/SysOssMapper.xml b/ruoyi-system/src/main/resources/mapper/system/SysOssMapper.xml deleted file mode 100644 index a1e4ca8..0000000 --- a/ruoyi-system/src/main/resources/mapper/system/SysOssMapper.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/ruoyi.sql b/ruoyi.sql index d337b21..cea197e 100644 --- a/ruoyi.sql +++ b/ruoyi.sql @@ -11,7 +11,7 @@ Target Server Version : 100617 File Encoding : 65001 - Date: 11/10/2024 17:51:13 + Date: 24/10/2024 17:46:10 */ SET NAMES utf8mb4; @@ -209,6 +209,14 @@ CREATE TABLE `sys_logininfor` ( -- Records of sys_logininfor -- ---------------------------- INSERT INTO `sys_logininfor` VALUES (20241011000000001, 'admin', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2024-10-11 17:28:10'); +INSERT INTO `sys_logininfor` VALUES (20241012000000001, 'admin', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2024-10-12 17:19:10'); +INSERT INTO `sys_logininfor` VALUES (20241014000000001, 'admin', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2024-10-14 09:04:16'); +INSERT INTO `sys_logininfor` VALUES (20241022000000001, 'admin', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2024-10-22 10:58:47'); +INSERT INTO `sys_logininfor` VALUES (20241022000000002, 'admin', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2024-10-22 14:04:30'); +INSERT INTO `sys_logininfor` VALUES (20241023000000001, 'admin', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2024-10-23 09:03:22'); +INSERT INTO `sys_logininfor` VALUES (20241023000000002, 'admin', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2024-10-23 15:11:46'); +INSERT INTO `sys_logininfor` VALUES (20241024000000001, 'admin', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2024-10-24 08:49:02'); +INSERT INTO `sys_logininfor` VALUES (20241024000000002, 'admin', '127.0.0.1', '内网IP', 'MSEdge', 'Windows 10 or Windows Server 2016', '0', '登录成功', '2024-10-24 14:27:40'); -- ---------------------------- -- Table structure for sys_menu @@ -347,6 +355,7 @@ INSERT INTO `sys_menu` VALUES (2001, '定时任务查询', 2000, 1, '#', '', NUL INSERT INTO `sys_menu` VALUES (2002, '定时任务新增', 2000, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'sys:cron:add', '#', 'admin', '2024-08-08 10:02:52', '', NULL, ''); INSERT INTO `sys_menu` VALUES (2003, '定时任务修改', 2000, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'sys:cron:update', '#', 'admin', '2024-08-08 10:02:52', '', NULL, ''); INSERT INTO `sys_menu` VALUES (2004, '定时任务删除', 2000, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'sys:cron:remove', '#', 'admin', '2024-08-08 10:02:52', '', NULL, ''); +INSERT INTO `sys_menu` VALUES (20241022000000001, '文件图片上传', 5, 99, 'file', 'demo/file/index', NULL, 1, 1, 'C', '0', '0', 'demo:file:index', 'upload', 'admin', '2024-10-22 11:01:10', 'admin', '2024-10-22 11:01:10', ''); -- ---------------------------- -- Table structure for sys_notice @@ -426,63 +435,8 @@ INSERT INTO `sys_oper_log` VALUES (20241011000000016, '测试单表', 1, 'com.ru INSERT INTO `sys_oper_log` VALUES (20241011000000017, '测试单表', 1, 'com.ruoyi.demo.controller.TestDemoController.add()', 'POST', 1, 'admin', '', '/demo/demo', '127.0.0.1', '内网IP', '{\"createBy\":\"admin\",\"createTime\":\"2023-08-16 10:35:31\",\"id\":1,\"deptId\":102,\"userId\":4,\"orderNum\":1,\"testKey\":\"测试数据权限\",\"value\":\"测试\"}', '', 1, '\r\n### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry \'1\' for key \'PRIMARY\'\r\n### The error may exist in com/ruoyi/demo/mapper/TestDemoMapper.java (best guess)\r\n### The error may involve com.ruoyi.demo.mapper.TestDemoMapper.insert-Inline\r\n### The error occurred while setting parameters\r\n### SQL: INSERT INTO test_demo ( id, dept_id, user_id, create_user_id, login_ip, update_user_id, order_num, test_key, value, create_by, create_time, update_by, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )\r\n### Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry \'1\' for key \'PRIMARY\'\n; Duplicate entry \'1\' for key \'PRIMARY\'; nested exception is java.sql.SQLIntegrityConstraintViolationException: Duplicate entry \'1\' for key \'PRIMARY\'', '2024-10-11 17:49:26'); INSERT INTO `sys_oper_log` VALUES (20241011000000018, '测试单表', 1, 'com.ruoyi.demo.controller.TestDemoController.add()', 'POST', 1, 'admin', '', '/demo/demo', '127.0.0.1', '内网IP', '{\"createBy\":\"admin\",\"createTime\":\"2023-08-16 10:35:31\",\"id\":1,\"deptId\":102,\"userId\":4,\"orderNum\":1,\"testKey\":\"测试数据权限\",\"value\":\"测试\"}', '', 1, '\r\n### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry \'1\' for key \'PRIMARY\'\r\n### The error may exist in com/ruoyi/demo/mapper/TestDemoMapper.java (best guess)\r\n### The error may involve com.ruoyi.demo.mapper.TestDemoMapper.insert-Inline\r\n### The error occurred while setting parameters\r\n### SQL: INSERT INTO test_demo ( id, dept_id, user_id, create_user_id, login_ip, update_user_id, order_num, test_key, value, create_by, create_time, update_by, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )\r\n### Cause: java.sql.SQLIntegrityConstraintViolationException: Duplicate entry \'1\' for key \'PRIMARY\'\n; Duplicate entry \'1\' for key \'PRIMARY\'; nested exception is java.sql.SQLIntegrityConstraintViolationException: Duplicate entry \'1\' for key \'PRIMARY\'', '2024-10-11 17:49:28'); INSERT INTO `sys_oper_log` VALUES (20241011000000019, '测试单表', 2, 'com.ruoyi.demo.controller.TestDemoController.edit()', 'PUT', 1, 'admin', '', '/demo/demo', '127.0.0.1', '内网IP', '{\"createBy\":\"admin\",\"createTime\":\"2023-08-16 10:35:31\",\"id\":1,\"deptId\":102,\"userId\":4,\"orderNum\":1,\"testKey\":\"测试数据权限\",\"value\":\"测试\"}', '{\"code\":200,\"msg\":\"操作成功\"}', 0, '', '2024-10-11 17:50:05'); - --- ---------------------------- --- Table structure for sys_oss --- ---------------------------- -DROP TABLE IF EXISTS `sys_oss`; -CREATE TABLE `sys_oss` ( - `oss_id` bigint(20) NOT NULL COMMENT '对象存储主键', - `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '文件名', - `original_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '原名', - `file_suffix` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '文件后缀名', - `url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'URL地址', - `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', - `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '上传人', - `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', - `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新人', - `service` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'minio' COMMENT '服务商', - PRIMARY KEY (`oss_id`) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'OSS对象存储表' ROW_FORMAT = Compact; - --- ---------------------------- --- Records of sys_oss --- ---------------------------- - --- ---------------------------- --- Table structure for sys_oss_config --- ---------------------------- -DROP TABLE IF EXISTS `sys_oss_config`; -CREATE TABLE `sys_oss_config` ( - `oss_config_id` bigint(20) NOT NULL COMMENT '主建', - `config_key` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '配置key', - `access_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'accessKey', - `secret_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '秘钥', - `bucket_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '桶名称', - `prefix` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '前缀', - `endpoint` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '访问站点', - `domain` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '自定义域名', - `is_https` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'N' COMMENT '是否https(Y=是,N=否)', - `region` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '域', - `access_policy` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1' COMMENT '桶权限类型(0=private 1=public 2=custom)', - `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '1' COMMENT '状态(0=正常,1=停用)', - `ext1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '扩展字段', - `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者', - `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', - `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者', - `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', - `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', - PRIMARY KEY (`oss_config_id`) USING BTREE -) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '对象存储配置表' ROW_FORMAT = Dynamic; - --- ---------------------------- --- Records of sys_oss_config --- ---------------------------- -INSERT INTO `sys_oss_config` VALUES (1, 'minio', 'base2024', 'base20241415926', 'files', '', '192.168.3.222:9000', '{root}', 'N', '', '1', '0', '', 'admin', '2023-04-28 11:22:31', 'admin', '2024-08-08 09:50:44', '正式环境中访问站点修改为minio:9000'); -INSERT INTO `sys_oss_config` VALUES (2, 'qiniu', 'XXXXXXXXXXXXXXX', 'XXXXXXXXXXXXXXX', 'ruoyi', '', 's3-cn-north-1.qiniucs.com', '', 'N', '', '1', '1', '', 'admin', '2023-08-16 10:35:08', 'admin', '2023-08-16 10:35:08', NULL); -INSERT INTO `sys_oss_config` VALUES (3, 'aliyun', 'LTAI5tQMkJBHbYoDcBBrc1Kv', '25MWcjkWRlqTD0pSJrthqXe05CpjWS', 'test-data-resources', 'xxx', 'oss-cn-shenzhen.aliyuncs.com', '', 'Y', '', '1', '1', '', 'admin', '2023-08-16 10:35:08', 'admin', '2024-05-17 08:29:25', ''); -INSERT INTO `sys_oss_config` VALUES (4, 'qcloud', 'XXXXXXXXXXXXXXX', 'XXXXXXXXXXXXXXX', 'ruoyi-1250000000', '', 'cos.ap-beijing.myqcloud.com', '', 'N', 'ap-beijing', '1', '1', '', 'admin', '2023-08-16 10:35:08', 'admin', '2023-08-16 10:35:08', NULL); -INSERT INTO `sys_oss_config` VALUES (5, 'image', 'ruoyi', 'ruoyi123', 'ruoyi', 'image', '127.0.0.1:9000', '', 'N', '', '1', '1', '', 'admin', '2023-08-16 10:35:08', 'admin', '2023-08-16 10:35:08', NULL); +INSERT INTO `sys_oper_log` VALUES (20241014000000020, '定时任务', 1, 'com.ruoyi.cron.api.CronTaskApi.add()', 'POST', 1, 'admin', '', '/system/cron/', '127.0.0.1', '内网IP', '{\"id\":202410140000001,\"taskId\":\"5553036b32681350546531d871d5edc9\",\"groupId\":0,\"enabled\":true,\"createTime\":\"2024-10-14 09:04\",\"paramELs\":[],\"userId\":1}', '', 0, '', '2024-10-14 09:04:34'); +INSERT INTO `sys_oper_log` VALUES (20241022000000021, '菜单管理', 1, 'com.ruoyi.web.controller.system.SysMenuController.add()', 'POST', 1, 'admin', '', '/system/menu', '127.0.0.1', '内网IP', '{\"createBy\":\"admin\",\"createTime\":\"2024-10-22 11:01:10\",\"updateBy\":\"admin\",\"updateTime\":\"2024-10-22 11:01:10\",\"parentId\":5,\"children\":[],\"menuId\":\"20241022000000001\",\"menuName\":\"文件图片上传\",\"orderNum\":99,\"path\":\"file\",\"component\":\"demo/file/index\",\"isFrame\":\"1\",\"isCache\":\"1\",\"menuType\":\"C\",\"visible\":\"0\",\"status\":\"0\",\"perms\":\"demo:file:index\",\"icon\":\"upload\"}', '{\"code\":200,\"msg\":\"操作成功\"}', 0, '', '2024-10-22 11:01:10'); -- ---------------------------- -- Table structure for sys_post @@ -677,7 +631,7 @@ CREATE TABLE `sys_user` ( -- ---------------------------- -- Records of sys_user -- ---------------------------- -INSERT INTO `sys_user` VALUES (1, 100, NULL, 'admin', '超级管理员', 'sys_user', 'admin@evolvecloud.cn', '13888888888', '1', '', '$2a$10$.ja7BDq5b8jxd6snbRvz8eAmg0loaDb05LR6SpR2F42huJb7GaOD6', '0', '0', '127.0.0.1', '2024-10-11 17:28:10', 'admin', '2024-01-03 10:35:07', 'admin', '2024-10-11 17:28:10', '管理员'); +INSERT INTO `sys_user` VALUES (1, 100, NULL, 'admin', '超级管理员', 'sys_user', 'admin@evolvecloud.cn', '13888888888', '1', '', '$2a$10$.ja7BDq5b8jxd6snbRvz8eAmg0loaDb05LR6SpR2F42huJb7GaOD6', '0', '0', '127.0.0.1', '2024-10-24 14:27:40', 'admin', '2024-01-03 10:35:07', 'admin', '2024-10-24 14:27:40', '管理员'); -- ---------------------------- -- Table structure for sys_user_post diff --git a/script/docker/nginx/conf/nginx.conf b/script/docker/nginx/conf/nginx.conf index 9bcc263..1ab66c5 100644 --- a/script/docker/nginx/conf/nginx.conf +++ b/script/docker/nginx/conf/nginx.conf @@ -33,14 +33,6 @@ http { } - upstream monitor-admin { - server 127.0.0.1:9090; - } - - upstream xxljob-admin { - server 127.0.0.1:9100; - } - server { listen 80; server_name localhost; @@ -68,10 +60,7 @@ http { # return 200 '{"msg":"演示模式,不允许操作","code":500}'; # } - # 限制外网访问内网 actuator 相关路径 - location ~ ^(/[^/]*)?/actuator(/.*)?$ { - return 403; - } + location / { root /usr/share/nginx/html;