Commit c8a14f11ec799f99b0238e310ca755310b41b0b8
Merge branch 'develop' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into master-jdk17
Showing
24 changed files
with
418 additions
and
220 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/io/FileUtils.java
| 1 | package cn.iocoder.yudao.framework.common.util.io; | 1 | package cn.iocoder.yudao.framework.common.util.io; |
| 2 | 2 | ||
| 3 | -import cn.hutool.core.io.FileTypeUtil; | ||
| 4 | import cn.hutool.core.io.FileUtil; | 3 | import cn.hutool.core.io.FileUtil; |
| 5 | -import cn.hutool.core.io.file.FileNameUtil; | ||
| 6 | import cn.hutool.core.util.IdUtil; | 4 | import cn.hutool.core.util.IdUtil; |
| 7 | -import cn.hutool.core.util.StrUtil; | ||
| 8 | -import cn.hutool.crypto.digest.DigestUtil; | ||
| 9 | import lombok.SneakyThrows; | 5 | import lombok.SneakyThrows; |
| 10 | 6 | ||
| 11 | -import java.io.ByteArrayInputStream; | ||
| 12 | import java.io.File; | 7 | import java.io.File; |
| 13 | 8 | ||
| 14 | /** | 9 | /** |
| @@ -63,22 +58,4 @@ public class FileUtils { | @@ -63,22 +58,4 @@ public class FileUtils { | ||
| 63 | return file; | 58 | return file; |
| 64 | } | 59 | } |
| 65 | 60 | ||
| 66 | - /** | ||
| 67 | - * 生成文件路径 | ||
| 68 | - * | ||
| 69 | - * @param content 文件内容 | ||
| 70 | - * @param originalName 原始文件名 | ||
| 71 | - * @return path,唯一不可重复 | ||
| 72 | - */ | ||
| 73 | - public static String generatePath(byte[] content, String originalName) { | ||
| 74 | - String sha256Hex = DigestUtil.sha256Hex(content); | ||
| 75 | - // 情况一:如果存在 name,则优先使用 name 的后缀 | ||
| 76 | - if (StrUtil.isNotBlank(originalName)) { | ||
| 77 | - String extName = FileNameUtil.extName(originalName); | ||
| 78 | - return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName; | ||
| 79 | - } | ||
| 80 | - // 情况二:基于 content 计算 | ||
| 81 | - return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content)); | ||
| 82 | - } | ||
| 83 | - | ||
| 84 | } | 61 | } |
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java
| 1 | package cn.iocoder.yudao.module.infra.api.file; | 1 | package cn.iocoder.yudao.module.infra.api.file; |
| 2 | 2 | ||
| 3 | +import jakarta.validation.constraints.NotEmpty; | ||
| 4 | + | ||
| 3 | /** | 5 | /** |
| 4 | * 文件 API 接口 | 6 | * 文件 API 接口 |
| 5 | * | 7 | * |
| @@ -14,28 +16,30 @@ public interface FileApi { | @@ -14,28 +16,30 @@ public interface FileApi { | ||
| 14 | * @return 文件路径 | 16 | * @return 文件路径 |
| 15 | */ | 17 | */ |
| 16 | default String createFile(byte[] content) { | 18 | default String createFile(byte[] content) { |
| 17 | - return createFile(null, null, content); | 19 | + return createFile(content, null, null, null); |
| 18 | } | 20 | } |
| 19 | 21 | ||
| 20 | /** | 22 | /** |
| 21 | * 保存文件,并返回文件的访问路径 | 23 | * 保存文件,并返回文件的访问路径 |
| 22 | * | 24 | * |
| 23 | - * @param path 文件路径 | ||
| 24 | * @param content 文件内容 | 25 | * @param content 文件内容 |
| 26 | + * @param name 文件名称,允许空 | ||
| 25 | * @return 文件路径 | 27 | * @return 文件路径 |
| 26 | */ | 28 | */ |
| 27 | - default String createFile(String path, byte[] content) { | ||
| 28 | - return createFile(null, path, content); | 29 | + default String createFile(byte[] content, String name) { |
| 30 | + return createFile(content, name, null, null); | ||
| 29 | } | 31 | } |
| 30 | 32 | ||
| 31 | /** | 33 | /** |
| 32 | * 保存文件,并返回文件的访问路径 | 34 | * 保存文件,并返回文件的访问路径 |
| 33 | * | 35 | * |
| 34 | - * @param name 文件名称 | ||
| 35 | - * @param path 文件路径 | ||
| 36 | * @param content 文件内容 | 36 | * @param content 文件内容 |
| 37 | + * @param name 文件名称,允许空 | ||
| 38 | + * @param directory 目录,允许空 | ||
| 39 | + * @param type 文件的 MIME 类型,允许空 | ||
| 37 | * @return 文件路径 | 40 | * @return 文件路径 |
| 38 | */ | 41 | */ |
| 39 | - String createFile(String name, String path, byte[] content); | 42 | + String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content, |
| 43 | + String name, String directory, String type); | ||
| 40 | 44 | ||
| 41 | } | 45 | } |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java
| 1 | package cn.iocoder.yudao.module.infra.api.file; | 1 | package cn.iocoder.yudao.module.infra.api.file; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.infra.service.file.FileService; | 3 | import cn.iocoder.yudao.module.infra.service.file.FileService; |
| 4 | +import jakarta.annotation.Resource; | ||
| 4 | import org.springframework.stereotype.Service; | 5 | import org.springframework.stereotype.Service; |
| 5 | import org.springframework.validation.annotation.Validated; | 6 | import org.springframework.validation.annotation.Validated; |
| 6 | 7 | ||
| 7 | -import jakarta.annotation.Resource; | ||
| 8 | - | ||
| 9 | /** | 8 | /** |
| 10 | * 文件 API 实现类 | 9 | * 文件 API 实现类 |
| 11 | * | 10 | * |
| @@ -19,8 +18,8 @@ public class FileApiImpl implements FileApi { | @@ -19,8 +18,8 @@ public class FileApiImpl implements FileApi { | ||
| 19 | private FileService fileService; | 18 | private FileService fileService; |
| 20 | 19 | ||
| 21 | @Override | 20 | @Override |
| 22 | - public String createFile(String name, String path, byte[] content) { | ||
| 23 | - return fileService.createFile(name, path, content); | 21 | + public String createFile(byte[] content, String name, String directory, String type) { |
| 22 | + return fileService.createFile(content, name, directory, type); | ||
| 24 | } | 23 | } |
| 25 | 24 | ||
| 26 | } | 25 | } |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
| @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; | @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; | ||
| 11 | import cn.iocoder.yudao.module.infra.service.file.FileService; | 11 | import cn.iocoder.yudao.module.infra.service.file.FileService; |
| 12 | import io.swagger.v3.oas.annotations.Operation; | 12 | import io.swagger.v3.oas.annotations.Operation; |
| 13 | import io.swagger.v3.oas.annotations.Parameter; | 13 | import io.swagger.v3.oas.annotations.Parameter; |
| 14 | +import io.swagger.v3.oas.annotations.Parameters; | ||
| 14 | import io.swagger.v3.oas.annotations.tags.Tag; | 15 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 15 | import jakarta.annotation.Resource; | 16 | import jakarta.annotation.Resource; |
| 16 | import jakarta.annotation.security.PermitAll; | 17 | import jakarta.annotation.security.PermitAll; |
| @@ -41,14 +42,21 @@ public class FileController { | @@ -41,14 +42,21 @@ public class FileController { | ||
| 41 | @Operation(summary = "上传文件", description = "模式一:后端上传文件") | 42 | @Operation(summary = "上传文件", description = "模式一:后端上传文件") |
| 42 | public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception { | 43 | public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception { |
| 43 | MultipartFile file = uploadReqVO.getFile(); | 44 | MultipartFile file = uploadReqVO.getFile(); |
| 44 | - String path = uploadReqVO.getPath(); | ||
| 45 | - return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); | 45 | + byte[] content = IoUtil.readBytes(file.getInputStream()); |
| 46 | + return success(fileService.createFile(content, file.getOriginalFilename(), | ||
| 47 | + uploadReqVO.getDirectory(), file.getContentType())); | ||
| 46 | } | 48 | } |
| 47 | 49 | ||
| 48 | @GetMapping("/presigned-url") | 50 | @GetMapping("/presigned-url") |
| 49 | @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") | 51 | @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") |
| 50 | - public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception { | ||
| 51 | - return success(fileService.getFilePresignedUrl(path)); | 52 | + @Parameters({ |
| 53 | + @Parameter(name = "name", description = "文件名称", required = true), | ||
| 54 | + @Parameter(name = "directory", description = "文件目录") | ||
| 55 | + }) | ||
| 56 | + public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl( | ||
| 57 | + @RequestParam("name") String name, | ||
| 58 | + @RequestParam(value = "directory", required = false) String directory) { | ||
| 59 | + return success(fileService.getFilePresignedUrl(name, directory)); | ||
| 52 | } | 60 | } |
| 53 | 61 | ||
| 54 | @PostMapping("/create") | 62 | @PostMapping("/create") |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FilePresignedUrlRespVO.java
| @@ -14,7 +14,8 @@ public class FilePresignedUrlRespVO { | @@ -14,7 +14,8 @@ public class FilePresignedUrlRespVO { | ||
| 14 | @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") | 14 | @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") |
| 15 | private Long configId; | 15 | private Long configId; |
| 16 | 16 | ||
| 17 | - @Schema(description = "文件上传 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://s3.cn-south-1.qiniucs.com/ruoyi-vue-pro/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS%2F20240217%2Fcn-south-1%2Fs3%2Faws4_request&X-Amz-Date=20240217T123222Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=a29f33770ab79bf523ccd4034d0752ac545f3c2a3b17baa1eb4e280cfdccfda5") | 17 | + @Schema(description = "文件上传 URL", requiredMode = Schema.RequiredMode.REQUIRED, |
| 18 | + example = "https://s3.cn-south-1.qiniucs.com/ruoyi-vue-pro/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=3TvrJ70gl2Gt6IBe7_IZT1F6i_k0iMuRtyEv4EyS%2F20240217%2Fcn-south-1%2Fs3%2Faws4_request&X-Amz-Date=20240217T123222Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=a29f33770ab79bf523ccd4034d0752ac545f3c2a3b17baa1eb4e280cfdccfda5") | ||
| 18 | private String uploadUrl; | 19 | private String uploadUrl; |
| 19 | 20 | ||
| 20 | /** | 21 | /** |
| @@ -26,4 +27,12 @@ public class FilePresignedUrlRespVO { | @@ -26,4 +27,12 @@ public class FilePresignedUrlRespVO { | ||
| 26 | example = "https://test.yudao.iocoder.cn/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png") | 27 | example = "https://test.yudao.iocoder.cn/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png") |
| 27 | private String url; | 28 | private String url; |
| 28 | 29 | ||
| 30 | + /** | ||
| 31 | + * 为什么要返回 path 字段? | ||
| 32 | + * | ||
| 33 | + * 前端上传完文件后,需要调用 createFile 记录下 path 路径 | ||
| 34 | + */ | ||
| 35 | + @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx.png") | ||
| 36 | + private String path; | ||
| 37 | + | ||
| 29 | } | 38 | } |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java
| @@ -14,7 +14,7 @@ public class FileUploadReqVO { | @@ -14,7 +14,7 @@ public class FileUploadReqVO { | ||
| 14 | @NotNull(message = "文件附件不能为空") | 14 | @NotNull(message = "文件附件不能为空") |
| 15 | private MultipartFile file; | 15 | private MultipartFile file; |
| 16 | 16 | ||
| 17 | - @Schema(description = "文件附件", example = "yudaoyuanma.png") | ||
| 18 | - private String path; | 17 | + @Schema(description = "文件目录", example = "XXX/YYY") |
| 18 | + private String directory; | ||
| 19 | 19 | ||
| 20 | } | 20 | } |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java
| @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned | @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned | ||
| 7 | import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO; | 7 | import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO; |
| 8 | import cn.iocoder.yudao.module.infra.service.file.FileService; | 8 | import cn.iocoder.yudao.module.infra.service.file.FileService; |
| 9 | import io.swagger.v3.oas.annotations.Operation; | 9 | import io.swagger.v3.oas.annotations.Operation; |
| 10 | +import io.swagger.v3.oas.annotations.Parameter; | ||
| 11 | +import io.swagger.v3.oas.annotations.Parameters; | ||
| 10 | import io.swagger.v3.oas.annotations.tags.Tag; | 12 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 11 | import jakarta.annotation.Resource; | 13 | import jakarta.annotation.Resource; |
| 12 | import jakarta.annotation.security.PermitAll; | 14 | import jakarta.annotation.security.PermitAll; |
| @@ -33,15 +35,21 @@ public class AppFileController { | @@ -33,15 +35,21 @@ public class AppFileController { | ||
| 33 | @PermitAll | 35 | @PermitAll |
| 34 | public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception { | 36 | public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception { |
| 35 | MultipartFile file = uploadReqVO.getFile(); | 37 | MultipartFile file = uploadReqVO.getFile(); |
| 36 | - String path = uploadReqVO.getPath(); | ||
| 37 | - return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); | 38 | + byte[] content = IoUtil.readBytes(file.getInputStream()); |
| 39 | + return success(fileService.createFile(content, file.getOriginalFilename(), | ||
| 40 | + uploadReqVO.getDirectory(), file.getContentType())); | ||
| 38 | } | 41 | } |
| 39 | 42 | ||
| 40 | @GetMapping("/presigned-url") | 43 | @GetMapping("/presigned-url") |
| 41 | @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") | 44 | @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") |
| 42 | - @PermitAll | ||
| 43 | - public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception { | ||
| 44 | - return success(fileService.getFilePresignedUrl(path)); | 45 | + @Parameters({ |
| 46 | + @Parameter(name = "name", description = "文件名称", required = true), | ||
| 47 | + @Parameter(name = "directory", description = "文件目录") | ||
| 48 | + }) | ||
| 49 | + public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl( | ||
| 50 | + @RequestParam("name") String name, | ||
| 51 | + @RequestParam(value = "directory", required = false) String directory) { | ||
| 52 | + return success(fileService.getFilePresignedUrl(name, directory)); | ||
| 45 | } | 53 | } |
| 46 | 54 | ||
| 47 | @PostMapping("/create") | 55 | @PostMapping("/create") |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java
| @@ -14,7 +14,7 @@ public class AppFileUploadReqVO { | @@ -14,7 +14,7 @@ public class AppFileUploadReqVO { | ||
| 14 | @NotNull(message = "文件附件不能为空") | 14 | @NotNull(message = "文件附件不能为空") |
| 15 | private MultipartFile file; | 15 | private MultipartFile file; |
| 16 | 16 | ||
| 17 | - @Schema(description = "文件附件", example = "yudaoyuanma.png") | ||
| 18 | - private String path; | 17 | + @Schema(description = "文件目录", example = "XXX/YYY") |
| 18 | + private String directory; | ||
| 19 | 19 | ||
| 20 | } | 20 | } |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/ftp/FtpFileClient.java
| @@ -26,12 +26,6 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { | @@ -26,12 +26,6 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { | ||
| 26 | 26 | ||
| 27 | @Override | 27 | @Override |
| 28 | protected void doInit() { | 28 | protected void doInit() { |
| 29 | - // 把配置的 \ 替换成 /, 如果路径配置 \a\test, 替换成 /a/test, 替换方法已经处理 null 情况 | ||
| 30 | - config.setBasePath(StrUtil.replace(config.getBasePath(), StrUtil.BACKSLASH, StrUtil.SLASH)); | ||
| 31 | - // ftp的路径是 / 结尾 | ||
| 32 | - if (!config.getBasePath().endsWith(StrUtil.SLASH)) { | ||
| 33 | - config.setBasePath(config.getBasePath() + StrUtil.SLASH); | ||
| 34 | - } | ||
| 35 | // 初始化 Ftp 对象 | 29 | // 初始化 Ftp 对象 |
| 36 | this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(), | 30 | this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(), |
| 37 | CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode())); | 31 | CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode())); |
| @@ -43,8 +37,8 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { | @@ -43,8 +37,8 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { | ||
| 43 | String filePath = getFilePath(path); | 37 | String filePath = getFilePath(path); |
| 44 | String fileName = FileUtil.getName(filePath); | 38 | String fileName = FileUtil.getName(filePath); |
| 45 | String dir = StrUtil.removeSuffix(filePath, fileName); | 39 | String dir = StrUtil.removeSuffix(filePath, fileName); |
| 46 | - ftp.reconnectIfTimeout(); | ||
| 47 | - boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); | 40 | + reconnectIfTimeout(); |
| 41 | + boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); // 不需要主动创建目录,ftp 内部已经处理(见源码) | ||
| 48 | if (!success) { | 42 | if (!success) { |
| 49 | throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath)); | 43 | throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath)); |
| 50 | } | 44 | } |
| @@ -55,7 +49,7 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { | @@ -55,7 +49,7 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { | ||
| 55 | @Override | 49 | @Override |
| 56 | public void delete(String path) { | 50 | public void delete(String path) { |
| 57 | String filePath = getFilePath(path); | 51 | String filePath = getFilePath(path); |
| 58 | - ftp.reconnectIfTimeout(); | 52 | + reconnectIfTimeout(); |
| 59 | ftp.delFile(filePath); | 53 | ftp.delFile(filePath); |
| 60 | } | 54 | } |
| 61 | 55 | ||
| @@ -65,13 +59,17 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { | @@ -65,13 +59,17 @@ public class FtpFileClient extends AbstractFileClient<FtpFileClientConfig> { | ||
| 65 | String fileName = FileUtil.getName(filePath); | 59 | String fileName = FileUtil.getName(filePath); |
| 66 | String dir = StrUtil.removeSuffix(filePath, fileName); | 60 | String dir = StrUtil.removeSuffix(filePath, fileName); |
| 67 | ByteArrayOutputStream out = new ByteArrayOutputStream(); | 61 | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| 68 | - ftp.reconnectIfTimeout(); | 62 | + reconnectIfTimeout(); |
| 69 | ftp.download(dir, fileName, out); | 63 | ftp.download(dir, fileName, out); |
| 70 | return out.toByteArray(); | 64 | return out.toByteArray(); |
| 71 | } | 65 | } |
| 72 | 66 | ||
| 73 | private String getFilePath(String path) { | 67 | private String getFilePath(String path) { |
| 74 | - return config.getBasePath() + path; | 68 | + return config.getBasePath() + StrUtil.SLASH + path; |
| 69 | + } | ||
| 70 | + | ||
| 71 | + private synchronized void reconnectIfTimeout() { | ||
| 72 | + ftp.reconnectIfTimeout(); | ||
| 75 | } | 73 | } |
| 76 | 74 | ||
| 77 | } | 75 | } |
| 78 | \ No newline at end of file | 76 | \ No newline at end of file |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java
| @@ -18,10 +18,6 @@ public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> { | @@ -18,10 +18,6 @@ public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> { | ||
| 18 | 18 | ||
| 19 | @Override | 19 | @Override |
| 20 | protected void doInit() { | 20 | protected void doInit() { |
| 21 | - // 补全风格。例如说 Linux 是 /,Windows 是 \ | ||
| 22 | - if (!config.getBasePath().endsWith(File.separator)) { | ||
| 23 | - config.setBasePath(config.getBasePath() + File.separator); | ||
| 24 | - } | ||
| 25 | } | 21 | } |
| 26 | 22 | ||
| 27 | @Override | 23 | @Override |
| @@ -46,7 +42,7 @@ public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> { | @@ -46,7 +42,7 @@ public class LocalFileClient extends AbstractFileClient<LocalFileClientConfig> { | ||
| 46 | } | 42 | } |
| 47 | 43 | ||
| 48 | private String getFilePath(String path) { | 44 | private String getFilePath(String path) { |
| 49 | - return config.getBasePath() + path; | 45 | + return config.getBasePath() + File.separator + path; |
| 50 | } | 46 | } |
| 51 | 47 | ||
| 52 | } | 48 | } |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/sftp/SftpFileClient.java
| @@ -22,10 +22,6 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> { | @@ -22,10 +22,6 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> { | ||
| 22 | 22 | ||
| 23 | @Override | 23 | @Override |
| 24 | protected void doInit() { | 24 | protected void doInit() { |
| 25 | - // 补全风格。例如说 Linux 是 /,Windows 是 \ | ||
| 26 | - if (!config.getBasePath().endsWith(File.separator)) { | ||
| 27 | - config.setBasePath(config.getBasePath() + File.separator); | ||
| 28 | - } | ||
| 29 | // 初始化 Ftp 对象 | 25 | // 初始化 Ftp 对象 |
| 30 | this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); | 26 | this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); |
| 31 | } | 27 | } |
| @@ -35,6 +31,8 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> { | @@ -35,6 +31,8 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> { | ||
| 35 | // 执行写入 | 31 | // 执行写入 |
| 36 | String filePath = getFilePath(path); | 32 | String filePath = getFilePath(path); |
| 37 | File file = FileUtils.createTempFile(content); | 33 | File file = FileUtils.createTempFile(content); |
| 34 | + reconnectIfTimeout(); | ||
| 35 | + sftp.mkDirs(FileUtil.getParent(filePath, 1)); // 需要创建父目录,不然会报错 | ||
| 38 | sftp.upload(filePath, file); | 36 | sftp.upload(filePath, file); |
| 39 | // 拼接返回路径 | 37 | // 拼接返回路径 |
| 40 | return super.formatFileUrl(config.getDomain(), path); | 38 | return super.formatFileUrl(config.getDomain(), path); |
| @@ -43,6 +41,7 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> { | @@ -43,6 +41,7 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> { | ||
| 43 | @Override | 41 | @Override |
| 44 | public void delete(String path) { | 42 | public void delete(String path) { |
| 45 | String filePath = getFilePath(path); | 43 | String filePath = getFilePath(path); |
| 44 | + reconnectIfTimeout(); | ||
| 46 | sftp.delFile(filePath); | 45 | sftp.delFile(filePath); |
| 47 | } | 46 | } |
| 48 | 47 | ||
| @@ -50,12 +49,17 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> { | @@ -50,12 +49,17 @@ public class SftpFileClient extends AbstractFileClient<SftpFileClientConfig> { | ||
| 50 | public byte[] getContent(String path) { | 49 | public byte[] getContent(String path) { |
| 51 | String filePath = getFilePath(path); | 50 | String filePath = getFilePath(path); |
| 52 | File destFile = FileUtils.createTempFile(); | 51 | File destFile = FileUtils.createTempFile(); |
| 52 | + reconnectIfTimeout(); | ||
| 53 | sftp.download(filePath, destFile); | 53 | sftp.download(filePath, destFile); |
| 54 | return FileUtil.readBytes(destFile); | 54 | return FileUtil.readBytes(destFile); |
| 55 | } | 55 | } |
| 56 | 56 | ||
| 57 | private String getFilePath(String path) { | 57 | private String getFilePath(String path) { |
| 58 | - return config.getBasePath() + path; | 58 | + return config.getBasePath() + File.separator + path; |
| 59 | + } | ||
| 60 | + | ||
| 61 | + private synchronized void reconnectIfTimeout() { | ||
| 62 | + sftp.reconnectIfTimeout(); | ||
| 59 | } | 63 | } |
| 60 | 64 | ||
| 61 | } | 65 | } |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
| @@ -6,7 +6,10 @@ import cn.iocoder.yudao.framework.common.util.http.HttpUtils; | @@ -6,7 +6,10 @@ import cn.iocoder.yudao.framework.common.util.http.HttpUtils; | ||
| 6 | import com.alibaba.ttl.TransmittableThreadLocal; | 6 | import com.alibaba.ttl.TransmittableThreadLocal; |
| 7 | import jakarta.servlet.http.HttpServletResponse; | 7 | import jakarta.servlet.http.HttpServletResponse; |
| 8 | import lombok.SneakyThrows; | 8 | import lombok.SneakyThrows; |
| 9 | +import lombok.extern.slf4j.Slf4j; | ||
| 9 | import org.apache.tika.Tika; | 10 | import org.apache.tika.Tika; |
| 11 | +import org.apache.tika.mime.MimeTypeException; | ||
| 12 | +import org.apache.tika.mime.MimeTypes; | ||
| 10 | 13 | ||
| 11 | import java.io.IOException; | 14 | import java.io.IOException; |
| 12 | 15 | ||
| @@ -15,12 +18,13 @@ import java.io.IOException; | @@ -15,12 +18,13 @@ import java.io.IOException; | ||
| 15 | * | 18 | * |
| 16 | * @author 芋道源码 | 19 | * @author 芋道源码 |
| 17 | */ | 20 | */ |
| 21 | +@Slf4j | ||
| 18 | public class FileTypeUtils { | 22 | public class FileTypeUtils { |
| 19 | 23 | ||
| 20 | private static final ThreadLocal<Tika> TIKA = TransmittableThreadLocal.withInitial(Tika::new); | 24 | private static final ThreadLocal<Tika> TIKA = TransmittableThreadLocal.withInitial(Tika::new); |
| 21 | 25 | ||
| 22 | /** | 26 | /** |
| 23 | - * 获得文件的 mineType,对于doc,jar等文件会有误差 | 27 | + * 获得文件的 mineType,对于 doc,jar 等文件会有误差 |
| 24 | * | 28 | * |
| 25 | * @param data 文件内容 | 29 | * @param data 文件内容 |
| 26 | * @return mineType 无法识别时会返回“application/octet-stream” | 30 | * @return mineType 无法识别时会返回“application/octet-stream” |
| @@ -31,7 +35,7 @@ public class FileTypeUtils { | @@ -31,7 +35,7 @@ public class FileTypeUtils { | ||
| 31 | } | 35 | } |
| 32 | 36 | ||
| 33 | /** | 37 | /** |
| 34 | - * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用jar文件时,通过名字更为准确 | 38 | + * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用 jar 文件时,通过名字更为准确 |
| 35 | * | 39 | * |
| 36 | * @param name 文件名 | 40 | * @param name 文件名 |
| 37 | * @return mineType 无法识别时会返回“application/octet-stream” | 41 | * @return mineType 无法识别时会返回“application/octet-stream” |
| @@ -52,6 +56,23 @@ public class FileTypeUtils { | @@ -52,6 +56,23 @@ public class FileTypeUtils { | ||
| 52 | } | 56 | } |
| 53 | 57 | ||
| 54 | /** | 58 | /** |
| 59 | + * 根据 mineType 获得文件后缀 | ||
| 60 | + * | ||
| 61 | + * 注意:如果获取不到,或者发生异常,都返回 null | ||
| 62 | + * | ||
| 63 | + * @param mineType 类型 | ||
| 64 | + * @return 后缀,例如说 .pdf | ||
| 65 | + */ | ||
| 66 | + public static String getExtension(String mineType) { | ||
| 67 | + try { | ||
| 68 | + return MimeTypes.getDefaultMimeTypes().forName(mineType).getExtension(); | ||
| 69 | + } catch (MimeTypeException e) { | ||
| 70 | + log.warn("[getExtension][获取文件后缀({}) 失败]", mineType, e); | ||
| 71 | + return null; | ||
| 72 | + } | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + /** | ||
| 55 | * 返回附件 | 76 | * 返回附件 |
| 56 | * | 77 | * |
| 57 | * @param response 响应 | 78 | * @param response 响应 |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java
| @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReq | @@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReq | ||
| 5 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; | 5 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; |
| 6 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; | 6 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; |
| 7 | import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; | 7 | import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; |
| 8 | +import jakarta.validation.constraints.NotEmpty; | ||
| 8 | 9 | ||
| 9 | /** | 10 | /** |
| 10 | * 文件 Service 接口 | 11 | * 文件 Service 接口 |
| @@ -24,12 +25,24 @@ public interface FileService { | @@ -24,12 +25,24 @@ public interface FileService { | ||
| 24 | /** | 25 | /** |
| 25 | * 保存文件,并返回文件的访问路径 | 26 | * 保存文件,并返回文件的访问路径 |
| 26 | * | 27 | * |
| 27 | - * @param name 文件名称 | ||
| 28 | - * @param path 文件路径 | ||
| 29 | * @param content 文件内容 | 28 | * @param content 文件内容 |
| 29 | + * @param name 文件名称,允许空 | ||
| 30 | + * @param directory 目录,允许空 | ||
| 31 | + * @param type 文件的 MIME 类型,允许空 | ||
| 30 | * @return 文件路径 | 32 | * @return 文件路径 |
| 31 | */ | 33 | */ |
| 32 | - String createFile(String name, String path, byte[] content); | 34 | + String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content, |
| 35 | + String name, String directory, String type); | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * 生成文件预签名地址信息 | ||
| 39 | + * | ||
| 40 | + * @param name 文件名 | ||
| 41 | + * @param directory 目录 | ||
| 42 | + * @return 预签名地址信息 | ||
| 43 | + */ | ||
| 44 | + FilePresignedUrlRespVO getFilePresignedUrl(@NotEmpty(message = "文件名不能为空") String name, | ||
| 45 | + String directory); | ||
| 33 | 46 | ||
| 34 | /** | 47 | /** |
| 35 | * 创建文件 | 48 | * 创建文件 |
| @@ -55,12 +68,4 @@ public interface FileService { | @@ -55,12 +68,4 @@ public interface FileService { | ||
| 55 | */ | 68 | */ |
| 56 | byte[] getFileContent(Long configId, String path) throws Exception; | 69 | byte[] getFileContent(Long configId, String path) throws Exception; |
| 57 | 70 | ||
| 58 | - /** | ||
| 59 | - * 生成文件预签名地址信息 | ||
| 60 | - * | ||
| 61 | - * @param path 文件路径 | ||
| 62 | - * @return 预签名地址信息 | ||
| 63 | - */ | ||
| 64 | - FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception; | ||
| 65 | - | ||
| 66 | } | 71 | } |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java
| 1 | package cn.iocoder.yudao.module.infra.service.file; | 1 | package cn.iocoder.yudao.module.infra.service.file; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.date.LocalDateTimeUtil; | ||
| 4 | +import cn.hutool.core.io.FileUtil; | ||
| 3 | import cn.hutool.core.lang.Assert; | 5 | import cn.hutool.core.lang.Assert; |
| 4 | import cn.hutool.core.util.StrUtil; | 6 | import cn.hutool.core.util.StrUtil; |
| 7 | +import cn.hutool.crypto.digest.DigestUtil; | ||
| 5 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 8 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 6 | -import cn.iocoder.yudao.framework.common.util.io.FileUtils; | ||
| 7 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 9 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 8 | -import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; | ||
| 9 | -import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; | ||
| 10 | -import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; | ||
| 11 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; | 10 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; |
| 12 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; | 11 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; |
| 13 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; | 12 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO; |
| 14 | import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; | 13 | import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; |
| 15 | import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; | 14 | import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; |
| 15 | +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; | ||
| 16 | +import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; | ||
| 17 | +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; | ||
| 18 | +import com.google.common.annotations.VisibleForTesting; | ||
| 16 | import jakarta.annotation.Resource; | 19 | import jakarta.annotation.Resource; |
| 17 | import lombok.SneakyThrows; | 20 | import lombok.SneakyThrows; |
| 18 | import org.springframework.stereotype.Service; | 21 | import org.springframework.stereotype.Service; |
| 19 | 22 | ||
| 23 | +import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN; | ||
| 20 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | 24 | import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; |
| 21 | import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; | 25 | import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; |
| 22 | 26 | ||
| @@ -28,6 +32,20 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EX | @@ -28,6 +32,20 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EX | ||
| 28 | @Service | 32 | @Service |
| 29 | public class FileServiceImpl implements FileService { | 33 | public class FileServiceImpl implements FileService { |
| 30 | 34 | ||
| 35 | + /** | ||
| 36 | + * 上传文件的前缀,是否包含日期(yyyyMMdd) | ||
| 37 | + * | ||
| 38 | + * 目的:按照日期,进行分目录 | ||
| 39 | + */ | ||
| 40 | + static boolean PATH_PREFIX_DATE_ENABLE = true; | ||
| 41 | + /** | ||
| 42 | + * 上传文件的后缀,是否包含时间戳 | ||
| 43 | + * | ||
| 44 | + * 目的:保证文件的唯一性,避免覆盖 | ||
| 45 | + * 定制:可按需调整成 UUID、或者其他方式 | ||
| 46 | + */ | ||
| 47 | + static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true; | ||
| 48 | + | ||
| 31 | @Resource | 49 | @Resource |
| 32 | private FileConfigService fileConfigService; | 50 | private FileConfigService fileConfigService; |
| 33 | 51 | ||
| @@ -41,34 +59,82 @@ public class FileServiceImpl implements FileService { | @@ -41,34 +59,82 @@ public class FileServiceImpl implements FileService { | ||
| 41 | 59 | ||
| 42 | @Override | 60 | @Override |
| 43 | @SneakyThrows | 61 | @SneakyThrows |
| 44 | - public String createFile(String name, String path, byte[] content) { | ||
| 45 | - // 计算默认的 path 名 | ||
| 46 | - String type = FileTypeUtils.getMineType(content, name); | ||
| 47 | - if (StrUtil.isEmpty(path)) { | ||
| 48 | - path = FileUtils.generatePath(content, name); | 62 | + public String createFile(byte[] content, String name, String directory, String type) { |
| 63 | + // 1.1 处理 type 为空的情况 | ||
| 64 | + if (StrUtil.isEmpty(type)) { | ||
| 65 | + type = FileTypeUtils.getMineType(content, name); | ||
| 49 | } | 66 | } |
| 50 | - // 如果 name 为空,则使用 path 填充 | 67 | + // 1.2 处理 name 为空的情况 |
| 51 | if (StrUtil.isEmpty(name)) { | 68 | if (StrUtil.isEmpty(name)) { |
| 52 | - name = path; | 69 | + name = DigestUtil.sha256Hex(content); |
| 70 | + } | ||
| 71 | + if (StrUtil.isEmpty(FileUtil.extName(name))) { | ||
| 72 | + // 如果 name 没有后缀 type,则补充后缀 | ||
| 73 | + String extension = FileTypeUtils.getExtension(type); | ||
| 74 | + if (StrUtil.isNotEmpty(extension)) { | ||
| 75 | + name = name + extension; | ||
| 76 | + } | ||
| 53 | } | 77 | } |
| 54 | 78 | ||
| 55 | - // 上传到文件存储器 | 79 | + // 2.1 生成上传的 path,需要保证唯一 |
| 80 | + String path = generateUploadPath(name, directory); | ||
| 81 | + // 2.2 上传到文件存储器 | ||
| 56 | FileClient client = fileConfigService.getMasterFileClient(); | 82 | FileClient client = fileConfigService.getMasterFileClient(); |
| 57 | Assert.notNull(client, "客户端(master) 不能为空"); | 83 | Assert.notNull(client, "客户端(master) 不能为空"); |
| 58 | String url = client.upload(content, path, type); | 84 | String url = client.upload(content, path, type); |
| 59 | 85 | ||
| 60 | - // 保存到数据库 | ||
| 61 | - FileDO file = new FileDO(); | ||
| 62 | - file.setConfigId(client.getId()); | ||
| 63 | - file.setName(name); | ||
| 64 | - file.setPath(path); | ||
| 65 | - file.setUrl(url); | ||
| 66 | - file.setType(type); | ||
| 67 | - file.setSize(content.length); | ||
| 68 | - fileMapper.insert(file); | 86 | + // 3. 保存到数据库 |
| 87 | + fileMapper.insert(new FileDO().setConfigId(client.getId()) | ||
| 88 | + .setName(name).setPath(path).setUrl(url) | ||
| 89 | + .setType(type).setSize(content.length)); | ||
| 69 | return url; | 90 | return url; |
| 70 | } | 91 | } |
| 71 | 92 | ||
| 93 | + @VisibleForTesting | ||
| 94 | + String generateUploadPath(String name, String directory) { | ||
| 95 | + // 1. 生成前缀、后缀 | ||
| 96 | + String prefix = null; | ||
| 97 | + if (PATH_PREFIX_DATE_ENABLE) { | ||
| 98 | + prefix = LocalDateTimeUtil.format(LocalDateTimeUtil.now(), PURE_DATE_PATTERN); | ||
| 99 | + } | ||
| 100 | + String suffix = null; | ||
| 101 | + if (PATH_SUFFIX_TIMESTAMP_ENABLE) { | ||
| 102 | + suffix = String.valueOf(System.currentTimeMillis()); | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + // 2.1 先拼接 suffix 后缀 | ||
| 106 | + if (StrUtil.isNotEmpty(suffix)) { | ||
| 107 | + String ext = FileUtil.extName(name); | ||
| 108 | + if (StrUtil.isNotEmpty(ext)) { | ||
| 109 | + name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext; | ||
| 110 | + } else { | ||
| 111 | + name = name + StrUtil.C_UNDERLINE + suffix; | ||
| 112 | + } | ||
| 113 | + } | ||
| 114 | + // 2.2 再拼接 prefix 前缀 | ||
| 115 | + if (StrUtil.isNotEmpty(prefix)) { | ||
| 116 | + name = prefix + StrUtil.SLASH + name; | ||
| 117 | + } | ||
| 118 | + // 2.3 最后拼接 directory 目录 | ||
| 119 | + if (StrUtil.isNotEmpty(directory)) { | ||
| 120 | + name = directory + StrUtil.SLASH + name; | ||
| 121 | + } | ||
| 122 | + return name; | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | + @Override | ||
| 126 | + @SneakyThrows | ||
| 127 | + public FilePresignedUrlRespVO getFilePresignedUrl(String name, String directory) { | ||
| 128 | + // 1. 生成上传的 path,需要保证唯一 | ||
| 129 | + String path = generateUploadPath(name, directory); | ||
| 130 | + | ||
| 131 | + // 2. 获取文件预签名地址 | ||
| 132 | + FileClient fileClient = fileConfigService.getMasterFileClient(); | ||
| 133 | + FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path); | ||
| 134 | + return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class, | ||
| 135 | + object -> object.setConfigId(fileClient.getId()).setPath(path)); | ||
| 136 | + } | ||
| 137 | + | ||
| 72 | @Override | 138 | @Override |
| 73 | public Long createFile(FileCreateReqVO createReqVO) { | 139 | public Long createFile(FileCreateReqVO createReqVO) { |
| 74 | FileDO file = BeanUtils.toBean(createReqVO, FileDO.class); | 140 | FileDO file = BeanUtils.toBean(createReqVO, FileDO.class); |
| @@ -105,12 +171,4 @@ public class FileServiceImpl implements FileService { | @@ -105,12 +171,4 @@ public class FileServiceImpl implements FileService { | ||
| 105 | return client.getContent(path); | 171 | return client.getContent(path); |
| 106 | } | 172 | } |
| 107 | 173 | ||
| 108 | - @Override | ||
| 109 | - public FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception { | ||
| 110 | - FileClient fileClient = fileConfigService.getMasterFileClient(); | ||
| 111 | - FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path); | ||
| 112 | - return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class, | ||
| 113 | - object -> object.setConfigId(fileClient.getId())); | ||
| 114 | - } | ||
| 115 | - | ||
| 116 | } | 174 | } |
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/ftp/FtpFileClientTest.java
| @@ -8,8 +8,23 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.ftp.FtpFileClien | @@ -8,8 +8,23 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.ftp.FtpFileClien | ||
| 8 | import org.junit.jupiter.api.Disabled; | 8 | import org.junit.jupiter.api.Disabled; |
| 9 | import org.junit.jupiter.api.Test; | 9 | import org.junit.jupiter.api.Test; |
| 10 | 10 | ||
| 11 | +/** | ||
| 12 | + * {@link FtpFileClient} 集成测试 | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 11 | public class FtpFileClientTest { | 16 | public class FtpFileClientTest { |
| 12 | 17 | ||
| 18 | +// docker run -d \ | ||
| 19 | +// -p 2121:21 -p 30000-30009:30000-30009 \ | ||
| 20 | +// -e FTP_USER=foo \ | ||
| 21 | +// -e FTP_PASS=pass \ | ||
| 22 | +// -e PASV_ADDRESS=127.0.0.1 \ | ||
| 23 | +// -e PASV_MIN_PORT=30000 \ | ||
| 24 | +// -e PASV_MAX_PORT=30009 \ | ||
| 25 | +// -v $(pwd)/ftp-data:/home/vsftpd \ | ||
| 26 | +// fauria/vsftpd | ||
| 27 | + | ||
| 13 | @Test | 28 | @Test |
| 14 | @Disabled | 29 | @Disabled |
| 15 | public void test() { | 30 | public void test() { |
| @@ -17,10 +32,10 @@ public class FtpFileClientTest { | @@ -17,10 +32,10 @@ public class FtpFileClientTest { | ||
| 17 | FtpFileClientConfig config = new FtpFileClientConfig(); | 32 | FtpFileClientConfig config = new FtpFileClientConfig(); |
| 18 | config.setDomain("http://127.0.0.1:48080"); | 33 | config.setDomain("http://127.0.0.1:48080"); |
| 19 | config.setBasePath("/home/ftp"); | 34 | config.setBasePath("/home/ftp"); |
| 20 | - config.setHost("kanchai.club"); | ||
| 21 | - config.setPort(221); | ||
| 22 | - config.setUsername(""); | ||
| 23 | - config.setPassword(""); | 35 | + config.setHost("127.0.0.1"); |
| 36 | + config.setPort(2121); | ||
| 37 | + config.setUsername("foo"); | ||
| 38 | + config.setPassword("pass"); | ||
| 24 | config.setMode(FtpMode.Passive.name()); | 39 | config.setMode(FtpMode.Passive.name()); |
| 25 | FtpFileClient client = new FtpFileClient(0L, config); | 40 | FtpFileClient client = new FtpFileClient(0L, config); |
| 26 | client.init(); | 41 | client.init(); |
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/sftp/SftpFileClientTest.java
| @@ -7,19 +7,29 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileCli | @@ -7,19 +7,29 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.sftp.SftpFileCli | ||
| 7 | import org.junit.jupiter.api.Disabled; | 7 | import org.junit.jupiter.api.Disabled; |
| 8 | import org.junit.jupiter.api.Test; | 8 | import org.junit.jupiter.api.Test; |
| 9 | 9 | ||
| 10 | +/** | ||
| 11 | + * {@link SftpFileClient} 集成测试 | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 10 | public class SftpFileClientTest { | 15 | public class SftpFileClientTest { |
| 11 | 16 | ||
| 17 | +// docker run -p 2222:22 -d \ | ||
| 18 | +// -v $(pwd)/sftp-data:/home/foo/upload \ | ||
| 19 | +// atmoz/sftp \ | ||
| 20 | +// foo:pass:1001 | ||
| 21 | + | ||
| 12 | @Test | 22 | @Test |
| 13 | @Disabled | 23 | @Disabled |
| 14 | public void test() { | 24 | public void test() { |
| 15 | // 创建客户端 | 25 | // 创建客户端 |
| 16 | SftpFileClientConfig config = new SftpFileClientConfig(); | 26 | SftpFileClientConfig config = new SftpFileClientConfig(); |
| 17 | config.setDomain("http://127.0.0.1:48080"); | 27 | config.setDomain("http://127.0.0.1:48080"); |
| 18 | - config.setBasePath("/home/ftp"); | ||
| 19 | - config.setHost("kanchai.club"); | ||
| 20 | - config.setPort(222); | ||
| 21 | - config.setUsername(""); | ||
| 22 | - config.setPassword(""); | 28 | + config.setBasePath("/upload"); // 注意,这个是相对路径,不是实际 linux 上的路径!!! |
| 29 | + config.setHost("127.0.0.1"); | ||
| 30 | + config.setPort(2222); | ||
| 31 | + config.setUsername("foo"); | ||
| 32 | + config.setPassword("pass"); | ||
| 23 | SftpFileClient client = new SftpFileClient(0L, config); | 33 | SftpFileClient client = new SftpFileClient(0L, config); |
| 24 | client.init(); | 34 | client.init(); |
| 25 | // 上传文件 | 35 | // 上传文件 |
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java
| @@ -3,19 +3,20 @@ package cn.iocoder.yudao.module.infra.service.file; | @@ -3,19 +3,20 @@ package cn.iocoder.yudao.module.infra.service.file; | ||
| 3 | import cn.hutool.core.io.resource.ResourceUtil; | 3 | import cn.hutool.core.io.resource.ResourceUtil; |
| 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; | 5 | import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; |
| 6 | -import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; | ||
| 7 | import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; | 6 | import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; |
| 8 | import cn.iocoder.yudao.framework.test.core.util.AssertUtils; | 7 | import cn.iocoder.yudao.framework.test.core.util.AssertUtils; |
| 9 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; | 8 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; |
| 10 | import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; | 9 | import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; |
| 11 | import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; | 10 | import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; |
| 11 | +import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; | ||
| 12 | +import jakarta.annotation.Resource; | ||
| 13 | +import org.junit.jupiter.api.BeforeEach; | ||
| 12 | import org.junit.jupiter.api.Test; | 14 | import org.junit.jupiter.api.Test; |
| 13 | import org.springframework.boot.test.mock.mockito.MockBean; | 15 | import org.springframework.boot.test.mock.mockito.MockBean; |
| 14 | import org.springframework.context.annotation.Import; | 16 | import org.springframework.context.annotation.Import; |
| 15 | 17 | ||
| 16 | -import jakarta.annotation.Resource; | ||
| 17 | - | ||
| 18 | import java.time.LocalDateTime; | 18 | import java.time.LocalDateTime; |
| 19 | +import java.util.concurrent.atomic.AtomicReference; | ||
| 19 | 20 | ||
| 20 | import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; | 21 | import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; |
| 21 | import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; | 22 | import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; |
| @@ -29,7 +30,7 @@ import static org.mockito.Mockito.*; | @@ -29,7 +30,7 @@ import static org.mockito.Mockito.*; | ||
| 29 | public class FileServiceImplTest extends BaseDbUnitTest { | 30 | public class FileServiceImplTest extends BaseDbUnitTest { |
| 30 | 31 | ||
| 31 | @Resource | 32 | @Resource |
| 32 | - private FileService fileService; | 33 | + private FileServiceImpl fileService; |
| 33 | 34 | ||
| 34 | @Resource | 35 | @Resource |
| 35 | private FileMapper fileMapper; | 36 | private FileMapper fileMapper; |
| @@ -37,6 +38,12 @@ public class FileServiceImplTest extends BaseDbUnitTest { | @@ -37,6 +38,12 @@ public class FileServiceImplTest extends BaseDbUnitTest { | ||
| 37 | @MockBean | 38 | @MockBean |
| 38 | private FileConfigService fileConfigService; | 39 | private FileConfigService fileConfigService; |
| 39 | 40 | ||
| 41 | + @BeforeEach | ||
| 42 | + public void setUp() { | ||
| 43 | + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true; | ||
| 44 | + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true; | ||
| 45 | + } | ||
| 46 | + | ||
| 40 | @Test | 47 | @Test |
| 41 | public void testGetFilePage() { | 48 | public void testGetFilePage() { |
| 42 | // mock 数据 | 49 | // mock 数据 |
| @@ -70,28 +77,69 @@ public class FileServiceImplTest extends BaseDbUnitTest { | @@ -70,28 +77,69 @@ public class FileServiceImplTest extends BaseDbUnitTest { | ||
| 70 | AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0)); | 77 | AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0)); |
| 71 | } | 78 | } |
| 72 | 79 | ||
| 80 | + /** | ||
| 81 | + * content、name、directory、type 都非空 | ||
| 82 | + */ | ||
| 73 | @Test | 83 | @Test |
| 74 | - public void testCreateFile_success() throws Exception { | 84 | + public void testCreateFile_success_01() throws Exception { |
| 75 | // 准备参数 | 85 | // 准备参数 |
| 76 | - String path = randomString(); | ||
| 77 | byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); | 86 | byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); |
| 87 | + String name = "单测文件名"; | ||
| 88 | + String directory = randomString(); | ||
| 89 | + String type = "image/jpeg"; | ||
| 78 | // mock Master 文件客户端 | 90 | // mock Master 文件客户端 |
| 79 | FileClient client = mock(FileClient.class); | 91 | FileClient client = mock(FileClient.class); |
| 80 | when(fileConfigService.getMasterFileClient()).thenReturn(client); | 92 | when(fileConfigService.getMasterFileClient()).thenReturn(client); |
| 81 | String url = randomString(); | 93 | String url = randomString(); |
| 82 | - when(client.upload(same(content), same(path), eq("image/jpeg"))).thenReturn(url); | 94 | + AtomicReference<String> pathRef = new AtomicReference<>(); |
| 95 | + when(client.upload(same(content), argThat(path -> { | ||
| 96 | + assertTrue(path.matches(directory + "/\\d{8}/" + name + "_\\d+.jpg")); | ||
| 97 | + pathRef.set(path); | ||
| 98 | + return true; | ||
| 99 | + }), eq(type))).thenReturn(url); | ||
| 83 | when(client.getId()).thenReturn(10L); | 100 | when(client.getId()).thenReturn(10L); |
| 84 | - String name = "单测文件名"; | ||
| 85 | // 调用 | 101 | // 调用 |
| 86 | - String result = fileService.createFile(name, path, content); | 102 | + String result = fileService.createFile(content, name, directory, type); |
| 87 | // 断言 | 103 | // 断言 |
| 88 | assertEquals(result, url); | 104 | assertEquals(result, url); |
| 89 | // 校验数据 | 105 | // 校验数据 |
| 90 | - FileDO file = fileMapper.selectOne(FileDO::getPath, path); | 106 | + FileDO file = fileMapper.selectOne(FileDO::getUrl, url); |
| 91 | assertEquals(10L, file.getConfigId()); | 107 | assertEquals(10L, file.getConfigId()); |
| 92 | - assertEquals(path, file.getPath()); | 108 | + assertEquals(pathRef.get(), file.getPath()); |
| 93 | assertEquals(url, file.getUrl()); | 109 | assertEquals(url, file.getUrl()); |
| 94 | - assertEquals("image/jpeg", file.getType()); | 110 | + assertEquals(type, file.getType()); |
| 111 | + assertEquals(content.length, file.getSize()); | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + /** | ||
| 115 | + * content 非空,其它都空 | ||
| 116 | + */ | ||
| 117 | + @Test | ||
| 118 | + public void testCreateFile_success_02() throws Exception { | ||
| 119 | + // 准备参数 | ||
| 120 | + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); | ||
| 121 | + // mock Master 文件客户端 | ||
| 122 | + String type = "image/jpeg"; | ||
| 123 | + FileClient client = mock(FileClient.class); | ||
| 124 | + when(fileConfigService.getMasterFileClient()).thenReturn(client); | ||
| 125 | + String url = randomString(); | ||
| 126 | + AtomicReference<String> pathRef = new AtomicReference<>(); | ||
| 127 | + when(client.upload(same(content), argThat(path -> { | ||
| 128 | + assertTrue(path.matches("\\d{8}/6318848e882d8a7e7e82789d87608f684ee52d41966bfc8cad3ce15aad2b970e_\\d+\\.jpg")); | ||
| 129 | + pathRef.set(path); | ||
| 130 | + return true; | ||
| 131 | + }), eq(type))).thenReturn(url); | ||
| 132 | + when(client.getId()).thenReturn(10L); | ||
| 133 | + // 调用 | ||
| 134 | + String result = fileService.createFile(content, null, null, null); | ||
| 135 | + // 断言 | ||
| 136 | + assertEquals(result, url); | ||
| 137 | + // 校验数据 | ||
| 138 | + FileDO file = fileMapper.selectOne(FileDO::getUrl, url); | ||
| 139 | + assertEquals(10L, file.getConfigId()); | ||
| 140 | + assertEquals(pathRef.get(), file.getPath()); | ||
| 141 | + assertEquals(url, file.getUrl()); | ||
| 142 | + assertEquals(type, file.getType()); | ||
| 95 | assertEquals(content.length, file.getSize()); | 143 | assertEquals(content.length, file.getSize()); |
| 96 | } | 144 | } |
| 97 | 145 | ||
| @@ -140,4 +188,122 @@ public class FileServiceImplTest extends BaseDbUnitTest { | @@ -140,4 +188,122 @@ public class FileServiceImplTest extends BaseDbUnitTest { | ||
| 140 | assertSame(result, content); | 188 | assertSame(result, content); |
| 141 | } | 189 | } |
| 142 | 190 | ||
| 191 | + @Test | ||
| 192 | + public void testGenerateUploadPath_AllEnabled() { | ||
| 193 | + // 准备参数 | ||
| 194 | + String name = "test.jpg"; | ||
| 195 | + String directory = "avatar"; | ||
| 196 | + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true; | ||
| 197 | + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true; | ||
| 198 | + | ||
| 199 | + // 调用 | ||
| 200 | + String path = fileService.generateUploadPath(name, directory); | ||
| 201 | + | ||
| 202 | + // 断言 | ||
| 203 | + // 格式为:avatar/yyyyMMdd/test_timestamp.jpg | ||
| 204 | + assertTrue(path.startsWith(directory + "/")); | ||
| 205 | + // 包含日期格式:8 位数字,如 20240517 | ||
| 206 | + assertTrue(path.matches(directory + "/\\d{8}/test_\\d+\\.jpg")); | ||
| 207 | + } | ||
| 208 | + | ||
| 209 | + @Test | ||
| 210 | + public void testGenerateUploadPath_PrefixEnabled_SuffixDisabled() { | ||
| 211 | + // 准备参数 | ||
| 212 | + String name = "test.jpg"; | ||
| 213 | + String directory = "avatar"; | ||
| 214 | + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true; | ||
| 215 | + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false; | ||
| 216 | + | ||
| 217 | + // 调用 | ||
| 218 | + String path = fileService.generateUploadPath(name, directory); | ||
| 219 | + | ||
| 220 | + // 断言 | ||
| 221 | + // 格式为:avatar/yyyyMMdd/test.jpg | ||
| 222 | + assertTrue(path.startsWith(directory + "/")); | ||
| 223 | + // 包含日期格式:8 位数字,如 20240517 | ||
| 224 | + assertTrue(path.matches(directory + "/\\d{8}/test\\.jpg")); | ||
| 225 | + } | ||
| 226 | + | ||
| 227 | + @Test | ||
| 228 | + public void testGenerateUploadPath_PrefixDisabled_SuffixEnabled() { | ||
| 229 | + // 准备参数 | ||
| 230 | + String name = "test.jpg"; | ||
| 231 | + String directory = "avatar"; | ||
| 232 | + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false; | ||
| 233 | + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true; | ||
| 234 | + | ||
| 235 | + // 调用 | ||
| 236 | + String path = fileService.generateUploadPath(name, directory); | ||
| 237 | + | ||
| 238 | + // 断言 | ||
| 239 | + // 格式为:avatar/test_timestamp.jpg | ||
| 240 | + assertTrue(path.startsWith(directory + "/")); | ||
| 241 | + assertTrue(path.matches(directory + "/test_\\d+\\.jpg")); | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + @Test | ||
| 245 | + public void testGenerateUploadPath_AllDisabled() { | ||
| 246 | + // 准备参数 | ||
| 247 | + String name = "test.jpg"; | ||
| 248 | + String directory = "avatar"; | ||
| 249 | + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = false; | ||
| 250 | + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = false; | ||
| 251 | + | ||
| 252 | + // 调用 | ||
| 253 | + String path = fileService.generateUploadPath(name, directory); | ||
| 254 | + | ||
| 255 | + // 断言 | ||
| 256 | + // 格式为:avatar/test.jpg | ||
| 257 | + assertEquals(directory + "/" + name, path); | ||
| 258 | + } | ||
| 259 | + | ||
| 260 | + @Test | ||
| 261 | + public void testGenerateUploadPath_NoExtension() { | ||
| 262 | + // 准备参数 | ||
| 263 | + String name = "test"; | ||
| 264 | + String directory = "avatar"; | ||
| 265 | + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true; | ||
| 266 | + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true; | ||
| 267 | + | ||
| 268 | + // 调用 | ||
| 269 | + String path = fileService.generateUploadPath(name, directory); | ||
| 270 | + | ||
| 271 | + // 断言 | ||
| 272 | + // 格式为:avatar/yyyyMMdd/test_timestamp | ||
| 273 | + assertTrue(path.startsWith(directory + "/")); | ||
| 274 | + assertTrue(path.matches(directory + "/\\d{8}/test_\\d+")); | ||
| 275 | + } | ||
| 276 | + | ||
| 277 | + @Test | ||
| 278 | + public void testGenerateUploadPath_DirectoryNull() { | ||
| 279 | + // 准备参数 | ||
| 280 | + String name = "test.jpg"; | ||
| 281 | + String directory = null; | ||
| 282 | + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true; | ||
| 283 | + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true; | ||
| 284 | + | ||
| 285 | + // 调用 | ||
| 286 | + String path = fileService.generateUploadPath(name, directory); | ||
| 287 | + | ||
| 288 | + // 断言 | ||
| 289 | + // 格式为:yyyyMMdd/test_timestamp.jpg | ||
| 290 | + assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg")); | ||
| 291 | + } | ||
| 292 | + | ||
| 293 | + @Test | ||
| 294 | + public void testGenerateUploadPath_DirectoryEmpty() { | ||
| 295 | + // 准备参数 | ||
| 296 | + String name = "test.jpg"; | ||
| 297 | + String directory = ""; | ||
| 298 | + FileServiceImpl.PATH_PREFIX_DATE_ENABLE = true; | ||
| 299 | + FileServiceImpl.PATH_SUFFIX_TIMESTAMP_ENABLE = true; | ||
| 300 | + | ||
| 301 | + // 调用 | ||
| 302 | + String path = fileService.generateUploadPath(name, directory); | ||
| 303 | + | ||
| 304 | + // 断言 | ||
| 305 | + // 格式为:yyyyMMdd/test_timestamp.jpg | ||
| 306 | + assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg")); | ||
| 307 | + } | ||
| 308 | + | ||
| 143 | } | 309 | } |
yudao-module-member/yudao-module-member-biz/src/test/java/cn/iocoder/yudao/module/member/service/user/MemberUserServiceImplTest.java
| @@ -5,12 +5,12 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | @@ -5,12 +5,12 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 5 | import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; | 5 | import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils; |
| 6 | import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; | 6 | import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; |
| 7 | import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest; | 7 | import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest; |
| 8 | -import cn.iocoder.yudao.module.infra.api.file.FileApi; | ||
| 9 | import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdateMobileReqVO; | 8 | import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdateMobileReqVO; |
| 10 | import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; | 9 | import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO; |
| 11 | import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper; | 10 | import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper; |
| 12 | import cn.iocoder.yudao.module.member.service.auth.MemberAuthServiceImpl; | 11 | import cn.iocoder.yudao.module.member.service.auth.MemberAuthServiceImpl; |
| 13 | import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; | 12 | import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi; |
| 13 | +import jakarta.annotation.Resource; | ||
| 14 | import org.junit.jupiter.api.Disabled; | 14 | import org.junit.jupiter.api.Disabled; |
| 15 | import org.junit.jupiter.api.Test; | 15 | import org.junit.jupiter.api.Test; |
| 16 | import org.springframework.boot.test.mock.mockito.MockBean; | 16 | import org.springframework.boot.test.mock.mockito.MockBean; |
| @@ -18,7 +18,6 @@ import org.springframework.context.annotation.Import; | @@ -18,7 +18,6 @@ import org.springframework.context.annotation.Import; | ||
| 18 | import org.springframework.data.redis.core.StringRedisTemplate; | 18 | import org.springframework.data.redis.core.StringRedisTemplate; |
| 19 | import org.springframework.security.crypto.password.PasswordEncoder; | 19 | import org.springframework.security.crypto.password.PasswordEncoder; |
| 20 | 20 | ||
| 21 | -import jakarta.annotation.Resource; | ||
| 22 | import java.util.function.Consumer; | 21 | import java.util.function.Consumer; |
| 23 | 22 | ||
| 24 | import static cn.hutool.core.util.RandomUtil.randomEle; | 23 | import static cn.hutool.core.util.RandomUtil.randomEle; |
| @@ -53,8 +52,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest { | @@ -53,8 +52,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest { | ||
| 53 | 52 | ||
| 54 | @MockBean | 53 | @MockBean |
| 55 | private SmsCodeApi smsCodeApi; | 54 | private SmsCodeApi smsCodeApi; |
| 56 | - @MockBean | ||
| 57 | - private FileApi fileApi; | ||
| 58 | 55 | ||
| 59 | // TODO 芋艿:后续重构这个单测 | 56 | // TODO 芋艿:后续重构这个单测 |
| 60 | // @Test | 57 | // @Test |
| @@ -73,25 +70,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest { | @@ -73,25 +70,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest { | ||
| 73 | // // 断言 | 70 | // // 断言 |
| 74 | // assertEquals(newNickName,nickname); | 71 | // assertEquals(newNickName,nickname); |
| 75 | // } | 72 | // } |
| 76 | -// | ||
| 77 | -// @Test | ||
| 78 | -// public void testUpdateAvatar_success() throws Exception { | ||
| 79 | -// // mock 数据 | ||
| 80 | -// MemberUserDO dbUser = randomUserDO(); | ||
| 81 | -// userMapper.insert(dbUser); | ||
| 82 | -// | ||
| 83 | -// // 准备参数 | ||
| 84 | -// Long userId = dbUser.getId(); | ||
| 85 | -// byte[] avatarFileBytes = randomBytes(10); | ||
| 86 | -// ByteArrayInputStream avatarFile = new ByteArrayInputStream(avatarFileBytes); | ||
| 87 | -// // mock 方法 | ||
| 88 | -// String avatar = randomString(); | ||
| 89 | -// when(fileApi.createFile(eq(avatarFileBytes))).thenReturn(avatar); | ||
| 90 | -// // 调用 | ||
| 91 | -// String str = memberUserService.updateUserAvatar(userId, avatarFile); | ||
| 92 | -// // 断言 | ||
| 93 | -// assertEquals(avatar, str); | ||
| 94 | -// } | ||
| 95 | 73 | ||
| 96 | @Test | 74 | @Test |
| 97 | @Disabled // TODO 芋艿:后续再修复 | 75 | @Disabled // TODO 芋艿:后续再修复 |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/material/MpMaterialServiceImpl.java
| @@ -218,7 +218,7 @@ public class MpMaterialServiceImpl implements MpMaterialService { | @@ -218,7 +218,7 @@ public class MpMaterialServiceImpl implements MpMaterialService { | ||
| 218 | 218 | ||
| 219 | private String uploadFile(String mediaId, File file) { | 219 | private String uploadFile(String mediaId, File file) { |
| 220 | String path = mediaId + "." + FileTypeUtil.getType(file); | 220 | String path = mediaId + "." + FileTypeUtil.getType(file); |
| 221 | - return fileApi.createFile(path, FileUtil.readBytes(file)); | 221 | + return fileApi.createFile(FileUtil.readBytes(file), path); |
| 222 | } | 222 | } |
| 223 | 223 | ||
| 224 | } | 224 | } |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserProfileController.java
| @@ -23,14 +23,11 @@ import jakarta.validation.Valid; | @@ -23,14 +23,11 @@ import jakarta.validation.Valid; | ||
| 23 | import lombok.extern.slf4j.Slf4j; | 23 | import lombok.extern.slf4j.Slf4j; |
| 24 | import org.springframework.validation.annotation.Validated; | 24 | import org.springframework.validation.annotation.Validated; |
| 25 | import org.springframework.web.bind.annotation.*; | 25 | import org.springframework.web.bind.annotation.*; |
| 26 | -import org.springframework.web.multipart.MultipartFile; | ||
| 27 | 26 | ||
| 28 | import java.util.List; | 27 | import java.util.List; |
| 29 | 28 | ||
| 30 | -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; | ||
| 31 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | 29 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 32 | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | 30 | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| 33 | -import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY; | ||
| 34 | 31 | ||
| 35 | @Tag(name = "管理后台 - 用户个人中心") | 32 | @Tag(name = "管理后台 - 用户个人中心") |
| 36 | @RestController | 33 | @RestController |
| @@ -79,16 +76,4 @@ public class UserProfileController { | @@ -79,16 +76,4 @@ public class UserProfileController { | ||
| 79 | return success(true); | 76 | return success(true); |
| 80 | } | 77 | } |
| 81 | 78 | ||
| 82 | - @Deprecated // TODO @芋艿:逐步替换到 updateUserProfile 接口 | ||
| 83 | - @RequestMapping(value = "/update-avatar", | ||
| 84 | - method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题 | ||
| 85 | - @Operation(summary = "上传用户个人头像") | ||
| 86 | - public CommonResult<String> updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception { | ||
| 87 | - if (file.isEmpty()) { | ||
| 88 | - throw exception(FILE_IS_EMPTY); | ||
| 89 | - } | ||
| 90 | - String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream()); | ||
| 91 | - return success(avatar); | ||
| 92 | - } | ||
| 93 | - | ||
| 94 | } | 79 | } |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserService.java
| @@ -13,7 +13,6 @@ import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqV | @@ -13,7 +13,6 @@ import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqV | ||
| 13 | import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; | 13 | import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; |
| 14 | import jakarta.validation.Valid; | 14 | import jakarta.validation.Valid; |
| 15 | 15 | ||
| 16 | -import java.io.InputStream; | ||
| 17 | import java.util.Collection; | 16 | import java.util.Collection; |
| 18 | import java.util.HashMap; | 17 | import java.util.HashMap; |
| 19 | import java.util.List; | 18 | import java.util.List; |
| @@ -74,14 +73,6 @@ public interface AdminUserService { | @@ -74,14 +73,6 @@ public interface AdminUserService { | ||
| 74 | void updateUserPassword(Long id, @Valid UserProfileUpdatePasswordReqVO reqVO); | 73 | void updateUserPassword(Long id, @Valid UserProfileUpdatePasswordReqVO reqVO); |
| 75 | 74 | ||
| 76 | /** | 75 | /** |
| 77 | - * 更新用户头像 | ||
| 78 | - * | ||
| 79 | - * @param id 用户 id | ||
| 80 | - * @param avatarFile 头像文件 | ||
| 81 | - */ | ||
| 82 | - String updateUserAvatar(Long id, InputStream avatarFile) throws Exception; | ||
| 83 | - | ||
| 84 | - /** | ||
| 85 | * 修改密码 | 76 | * 修改密码 |
| 86 | * | 77 | * |
| 87 | * @param id 用户编号 | 78 | * @param id 用户编号 |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImpl.java
| @@ -83,8 +83,6 @@ public class AdminUserServiceImpl implements AdminUserService { | @@ -83,8 +83,6 @@ public class AdminUserServiceImpl implements AdminUserService { | ||
| 83 | private UserPostMapper userPostMapper; | 83 | private UserPostMapper userPostMapper; |
| 84 | 84 | ||
| 85 | @Resource | 85 | @Resource |
| 86 | - private FileApi fileApi; | ||
| 87 | - @Resource | ||
| 88 | private ConfigApi configApi; | 86 | private ConfigApi configApi; |
| 89 | 87 | ||
| 90 | @Override | 88 | @Override |
| @@ -206,19 +204,6 @@ public class AdminUserServiceImpl implements AdminUserService { | @@ -206,19 +204,6 @@ public class AdminUserServiceImpl implements AdminUserService { | ||
| 206 | } | 204 | } |
| 207 | 205 | ||
| 208 | @Override | 206 | @Override |
| 209 | - public String updateUserAvatar(Long id, InputStream avatarFile) { | ||
| 210 | - validateUserExists(id); | ||
| 211 | - // 存储文件 | ||
| 212 | - String avatar = fileApi.createFile(IoUtil.readBytes(avatarFile)); | ||
| 213 | - // 更新路径 | ||
| 214 | - AdminUserDO sysUserDO = new AdminUserDO(); | ||
| 215 | - sysUserDO.setId(id); | ||
| 216 | - sysUserDO.setAvatar(avatar); | ||
| 217 | - userMapper.updateById(sysUserDO); | ||
| 218 | - return avatar; | ||
| 219 | - } | ||
| 220 | - | ||
| 221 | - @Override | ||
| 222 | @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE, bizNo = "{{#id}}", | 207 | @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE, bizNo = "{{#id}}", |
| 223 | success = SYSTEM_USER_UPDATE_PASSWORD_SUCCESS) | 208 | success = SYSTEM_USER_UPDATE_PASSWORD_SUCCESS) |
| 224 | public void updateUserPassword(Long id, String password) { | 209 | public void updateUserPassword(Long id, String password) { |
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java
| @@ -185,7 +185,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { | @@ -185,7 +185,7 @@ public class AdminAuthServiceImplTest extends BaseDbUnitTest { | ||
| 185 | public void testSendSmsCode() { | 185 | public void testSendSmsCode() { |
| 186 | // 准备参数 | 186 | // 准备参数 |
| 187 | String mobile = randomString(); | 187 | String mobile = randomString(); |
| 188 | - Integer scene = randomEle(SmsSceneEnum.values()).getScene(); | 188 | + Integer scene = SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(); |
| 189 | AuthSmsSendReqVO reqVO = new AuthSmsSendReqVO(mobile, scene); | 189 | AuthSmsSendReqVO reqVO = new AuthSmsSendReqVO(mobile, scene); |
| 190 | // mock 方法(用户信息) | 190 | // mock 方法(用户信息) |
| 191 | AdminUserDO user = randomPojo(AdminUserDO.class); | 191 | AdminUserDO user = randomPojo(AdminUserDO.class); |
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/AdminUserServiceImplTest.java
| @@ -11,7 +11,10 @@ import cn.iocoder.yudao.module.infra.api.config.ConfigApi; | @@ -11,7 +11,10 @@ import cn.iocoder.yudao.module.infra.api.config.ConfigApi; | ||
| 11 | import cn.iocoder.yudao.module.infra.api.file.FileApi; | 11 | import cn.iocoder.yudao.module.infra.api.file.FileApi; |
| 12 | import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; | 12 | import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; |
| 13 | import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; | 13 | import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; |
| 14 | -import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.*; | 14 | +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserImportExcelVO; |
| 15 | +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserImportRespVO; | ||
| 16 | +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserPageReqVO; | ||
| 17 | +import cn.iocoder.yudao.module.system.controller.admin.user.vo.user.UserSaveReqVO; | ||
| 15 | import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; | 18 | import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO; |
| 16 | import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; | 19 | import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO; |
| 17 | import cn.iocoder.yudao.module.system.dal.dataobject.dept.UserPostDO; | 20 | import cn.iocoder.yudao.module.system.dal.dataobject.dept.UserPostDO; |
| @@ -24,6 +27,7 @@ import cn.iocoder.yudao.module.system.service.dept.DeptService; | @@ -24,6 +27,7 @@ import cn.iocoder.yudao.module.system.service.dept.DeptService; | ||
| 24 | import cn.iocoder.yudao.module.system.service.dept.PostService; | 27 | import cn.iocoder.yudao.module.system.service.dept.PostService; |
| 25 | import cn.iocoder.yudao.module.system.service.permission.PermissionService; | 28 | import cn.iocoder.yudao.module.system.service.permission.PermissionService; |
| 26 | import cn.iocoder.yudao.module.system.service.tenant.TenantService; | 29 | import cn.iocoder.yudao.module.system.service.tenant.TenantService; |
| 30 | +import jakarta.annotation.Resource; | ||
| 27 | import org.junit.jupiter.api.BeforeEach; | 31 | import org.junit.jupiter.api.BeforeEach; |
| 28 | import org.junit.jupiter.api.Test; | 32 | import org.junit.jupiter.api.Test; |
| 29 | import org.mockito.stubbing.Answer; | 33 | import org.mockito.stubbing.Answer; |
| @@ -31,14 +35,11 @@ import org.springframework.boot.test.mock.mockito.MockBean; | @@ -31,14 +35,11 @@ import org.springframework.boot.test.mock.mockito.MockBean; | ||
| 31 | import org.springframework.context.annotation.Import; | 35 | import org.springframework.context.annotation.Import; |
| 32 | import org.springframework.security.crypto.password.PasswordEncoder; | 36 | import org.springframework.security.crypto.password.PasswordEncoder; |
| 33 | 37 | ||
| 34 | -import jakarta.annotation.Resource; | ||
| 35 | -import java.io.ByteArrayInputStream; | ||
| 36 | import java.util.Collection; | 38 | import java.util.Collection; |
| 37 | import java.util.List; | 39 | import java.util.List; |
| 38 | import java.util.Map; | 40 | import java.util.Map; |
| 39 | import java.util.function.Consumer; | 41 | import java.util.function.Consumer; |
| 40 | 42 | ||
| 41 | -import static cn.hutool.core.util.RandomUtil.randomBytes; | ||
| 42 | import static cn.hutool.core.util.RandomUtil.randomEle; | 43 | import static cn.hutool.core.util.RandomUtil.randomEle; |
| 43 | import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; | 44 | import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; |
| 44 | import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; | 45 | import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; |
| @@ -246,26 +247,6 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { | @@ -246,26 +247,6 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest { | ||
| 246 | } | 247 | } |
| 247 | 248 | ||
| 248 | @Test | 249 | @Test |
| 249 | - public void testUpdateUserAvatar_success() throws Exception { | ||
| 250 | - // mock 数据 | ||
| 251 | - AdminUserDO dbUser = randomAdminUserDO(); | ||
| 252 | - userMapper.insert(dbUser); | ||
| 253 | - // 准备参数 | ||
| 254 | - Long userId = dbUser.getId(); | ||
| 255 | - byte[] avatarFileBytes = randomBytes(10); | ||
| 256 | - ByteArrayInputStream avatarFile = new ByteArrayInputStream(avatarFileBytes); | ||
| 257 | - // mock 方法 | ||
| 258 | - String avatar = randomString(); | ||
| 259 | - when(fileApi.createFile(eq( avatarFileBytes))).thenReturn(avatar); | ||
| 260 | - | ||
| 261 | - // 调用 | ||
| 262 | - userService.updateUserAvatar(userId, avatarFile); | ||
| 263 | - // 断言 | ||
| 264 | - AdminUserDO user = userMapper.selectById(userId); | ||
| 265 | - assertEquals(avatar, user.getAvatar()); | ||
| 266 | - } | ||
| 267 | - | ||
| 268 | - @Test | ||
| 269 | public void testUpdateUserPassword02_success() { | 250 | public void testUpdateUserPassword02_success() { |
| 270 | // mock 数据 | 251 | // mock 数据 |
| 271 | AdminUserDO dbUser = randomAdminUserDO(); | 252 | AdminUserDO dbUser = randomAdminUserDO(); |