Commit c8a14f11ec799f99b0238e310ca755310b41b0b8

Authored by YunaiV
2 parents bf05e2d2 cce09044

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&lt;FtpFileClientConfig&gt; { @@ -26,12 +26,6 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
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&lt;FtpFileClientConfig&gt; { @@ -43,8 +37,8 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
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&lt;FtpFileClientConfig&gt; { @@ -55,7 +49,7 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
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&lt;FtpFileClientConfig&gt; { @@ -65,13 +59,17 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
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&lt;LocalFileClientConfig&gt; { @@ -18,10 +18,6 @@ public class LocalFileClient extends AbstractFileClient&lt;LocalFileClientConfig&gt; {
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&lt;LocalFileClientConfig&gt; { @@ -46,7 +42,7 @@ public class LocalFileClient extends AbstractFileClient&lt;LocalFileClientConfig&gt; {
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&lt;SftpFileClientConfig&gt; { @@ -22,10 +22,6 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
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&lt;SftpFileClientConfig&gt; { @@ -35,6 +31,8 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
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&lt;SftpFileClientConfig&gt; { @@ -43,6 +41,7 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
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&lt;SftpFileClientConfig&gt; { @@ -50,12 +49,17 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
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();