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 1 package cn.iocoder.yudao.framework.common.util.io;
2 2  
3   -import cn.hutool.core.io.FileTypeUtil;
4 3 import cn.hutool.core.io.FileUtil;
5   -import cn.hutool.core.io.file.FileNameUtil;
6 4 import cn.hutool.core.util.IdUtil;
7   -import cn.hutool.core.util.StrUtil;
8   -import cn.hutool.crypto.digest.DigestUtil;
9 5 import lombok.SneakyThrows;
10 6  
11   -import java.io.ByteArrayInputStream;
12 7 import java.io.File;
13 8  
14 9 /**
... ... @@ -63,22 +58,4 @@ public class FileUtils {
63 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 1 package cn.iocoder.yudao.module.infra.api.file;
2 2  
  3 +import jakarta.validation.constraints.NotEmpty;
  4 +
3 5 /**
4 6 * 文件 API 接口
5 7 *
... ... @@ -14,28 +16,30 @@ public interface FileApi {
14 16 * @return 文件路径
15 17 */
16 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 25 * @param content 文件内容
  26 + * @param name 文件名称,允许空
25 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 36 * @param content 文件内容
  37 + * @param name 文件名称,允许空
  38 + * @param directory 目录,允许空
  39 + * @param type 文件的 MIME 类型,允许空
37 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 1 package cn.iocoder.yudao.module.infra.api.file;
2 2  
3 3 import cn.iocoder.yudao.module.infra.service.file.FileService;
  4 +import jakarta.annotation.Resource;
4 5 import org.springframework.stereotype.Service;
5 6 import org.springframework.validation.annotation.Validated;
6 7  
7   -import jakarta.annotation.Resource;
8   -
9 8 /**
10 9 * 文件 API 实现类
11 10 *
... ... @@ -19,8 +18,8 @@ public class FileApiImpl implements FileApi {
19 18 private FileService fileService;
20 19  
21 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 11 import cn.iocoder.yudao.module.infra.service.file.FileService;
12 12 import io.swagger.v3.oas.annotations.Operation;
13 13 import io.swagger.v3.oas.annotations.Parameter;
  14 +import io.swagger.v3.oas.annotations.Parameters;
14 15 import io.swagger.v3.oas.annotations.tags.Tag;
15 16 import jakarta.annotation.Resource;
16 17 import jakarta.annotation.security.PermitAll;
... ... @@ -41,14 +42,21 @@ public class FileController {
41 42 @Operation(summary = "上传文件", description = "模式一:后端上传文件")
42 43 public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {
43 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 50 @GetMapping("/presigned-url")
49 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 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 14 @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11")
15 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 19 private String uploadUrl;
19 20  
20 21 /**
... ... @@ -26,4 +27,12 @@ public class FilePresignedUrlRespVO {
26 27 example = "https://test.yudao.iocoder.cn/758d3a5387507358c7236de4c8f96de1c7f5097ff6a7722b34772fb7b76b140f.png")
27 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 14 @NotNull(message = "文件附件不能为空")
15 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 7 import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO;
8 8 import cn.iocoder.yudao.module.infra.service.file.FileService;
9 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 12 import io.swagger.v3.oas.annotations.tags.Tag;
11 13 import jakarta.annotation.Resource;
12 14 import jakarta.annotation.security.PermitAll;
... ... @@ -33,15 +35,21 @@ public class AppFileController {
33 35 @PermitAll
34 36 public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception {
35 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 43 @GetMapping("/presigned-url")
41 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 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 14 @NotNull(message = "文件附件不能为空")
15 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 26  
27 27 @Override
28 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 29 // 初始化 Ftp 对象
36 30 this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(),
37 31 CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode()));
... ... @@ -43,8 +37,8 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
43 37 String filePath = getFilePath(path);
44 38 String fileName = FileUtil.getName(filePath);
45 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 42 if (!success) {
49 43 throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath));
50 44 }
... ... @@ -55,7 +49,7 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
55 49 @Override
56 50 public void delete(String path) {
57 51 String filePath = getFilePath(path);
58   - ftp.reconnectIfTimeout();
  52 + reconnectIfTimeout();
59 53 ftp.delFile(filePath);
60 54 }
61 55  
... ... @@ -65,13 +59,17 @@ public class FtpFileClient extends AbstractFileClient&lt;FtpFileClientConfig&gt; {
65 59 String fileName = FileUtil.getName(filePath);
66 60 String dir = StrUtil.removeSuffix(filePath, fileName);
67 61 ByteArrayOutputStream out = new ByteArrayOutputStream();
68   - ftp.reconnectIfTimeout();
  62 + reconnectIfTimeout();
69 63 ftp.download(dir, fileName, out);
70 64 return out.toByteArray();
71 65 }
72 66  
73 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 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 18  
19 19 @Override
20 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 23 @Override
... ... @@ -46,7 +42,7 @@ public class LocalFileClient extends AbstractFileClient&lt;LocalFileClientConfig&gt; {
46 42 }
47 43  
48 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 22  
23 23 @Override
24 24 protected void doInit() {
25   - // 补全风格。例如说 Linux 是 /,Windows 是 \
26   - if (!config.getBasePath().endsWith(File.separator)) {
27   - config.setBasePath(config.getBasePath() + File.separator);
28   - }
29 25 // 初始化 Ftp 对象
30 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 31 // 执行写入
36 32 String filePath = getFilePath(path);
37 33 File file = FileUtils.createTempFile(content);
  34 + reconnectIfTimeout();
  35 + sftp.mkDirs(FileUtil.getParent(filePath, 1)); // 需要创建父目录,不然会报错
38 36 sftp.upload(filePath, file);
39 37 // 拼接返回路径
40 38 return super.formatFileUrl(config.getDomain(), path);
... ... @@ -43,6 +41,7 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
43 41 @Override
44 42 public void delete(String path) {
45 43 String filePath = getFilePath(path);
  44 + reconnectIfTimeout();
46 45 sftp.delFile(filePath);
47 46 }
48 47  
... ... @@ -50,12 +49,17 @@ public class SftpFileClient extends AbstractFileClient&lt;SftpFileClientConfig&gt; {
50 49 public byte[] getContent(String path) {
51 50 String filePath = getFilePath(path);
52 51 File destFile = FileUtils.createTempFile();
  52 + reconnectIfTimeout();
53 53 sftp.download(filePath, destFile);
54 54 return FileUtil.readBytes(destFile);
55 55 }
56 56  
57 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 6 import com.alibaba.ttl.TransmittableThreadLocal;
7 7 import jakarta.servlet.http.HttpServletResponse;
8 8 import lombok.SneakyThrows;
  9 +import lombok.extern.slf4j.Slf4j;
9 10 import org.apache.tika.Tika;
  11 +import org.apache.tika.mime.MimeTypeException;
  12 +import org.apache.tika.mime.MimeTypes;
10 13  
11 14 import java.io.IOException;
12 15  
... ... @@ -15,12 +18,13 @@ import java.io.IOException;
15 18 *
16 19 * @author 芋道源码
17 20 */
  21 +@Slf4j
18 22 public class FileTypeUtils {
19 23  
20 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 29 * @param data 文件内容
26 30 * @return mineType 无法识别时会返回“application/octet-stream”
... ... @@ -31,7 +35,7 @@ public class FileTypeUtils {
31 35 }
32 36  
33 37 /**
34   - * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用jar文件时,通过名字更为准确
  38 + * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用 jar 文件时,通过名字更为准确
35 39 *
36 40 * @param name 文件名
37 41 * @return mineType 无法识别时会返回“application/octet-stream”
... ... @@ -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 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 5 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
6 6 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
7 7 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
  8 +import jakarta.validation.constraints.NotEmpty;
8 9  
9 10 /**
10 11 * 文件 Service 接口
... ... @@ -24,12 +25,24 @@ public interface FileService {
24 25 /**
25 26 * 保存文件,并返回文件的访问路径
26 27 *
27   - * @param name 文件名称
28   - * @param path 文件路径
29 28 * @param content 文件内容
  29 + * @param name 文件名称,允许空
  30 + * @param directory 目录,允许空
  31 + * @param type 文件的 MIME 类型,允许空
30 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 68 */
56 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 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 5 import cn.hutool.core.lang.Assert;
4 6 import cn.hutool.core.util.StrUtil;
  7 +import cn.hutool.crypto.digest.DigestUtil;
5 8 import cn.iocoder.yudao.framework.common.pojo.PageResult;
6   -import cn.iocoder.yudao.framework.common.util.io.FileUtils;
7 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 10 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
12 11 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
13 12 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
14 13 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
15 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 19 import jakarta.annotation.Resource;
17 20 import lombok.SneakyThrows;
18 21 import org.springframework.stereotype.Service;
19 22  
  23 +import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
20 24 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
21 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 32 @Service
29 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 49 @Resource
32 50 private FileConfigService fileConfigService;
33 51  
... ... @@ -41,34 +59,82 @@ public class FileServiceImpl implements FileService {
41 59  
42 60 @Override
43 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 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 82 FileClient client = fileConfigService.getMasterFileClient();
57 83 Assert.notNull(client, "客户端(master) 不能为空");
58 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 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 138 @Override
73 139 public Long createFile(FileCreateReqVO createReqVO) {
74 140 FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);
... ... @@ -105,12 +171,4 @@ public class FileServiceImpl implements FileService {
105 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 import org.junit.jupiter.api.Disabled;
9 9 import org.junit.jupiter.api.Test;
10 10  
  11 +/**
  12 + * {@link FtpFileClient} 集成测试
  13 + *
  14 + * @author 芋道源码
  15 + */
11 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 28 @Test
14 29 @Disabled
15 30 public void test() {
... ... @@ -17,10 +32,10 @@ public class FtpFileClientTest {
17 32 FtpFileClientConfig config = new FtpFileClientConfig();
18 33 config.setDomain("http://127.0.0.1:48080");
19 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 39 config.setMode(FtpMode.Passive.name());
25 40 FtpFileClient client = new FtpFileClient(0L, config);
26 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 7 import org.junit.jupiter.api.Disabled;
8 8 import org.junit.jupiter.api.Test;
9 9  
  10 +/**
  11 + * {@link SftpFileClient} 集成测试
  12 + *
  13 + * @author 芋道源码
  14 + */
10 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 22 @Test
13 23 @Disabled
14 24 public void test() {
15 25 // 创建客户端
16 26 SftpFileClientConfig config = new SftpFileClientConfig();
17 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 33 SftpFileClient client = new SftpFileClient(0L, config);
24 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 3 import cn.hutool.core.io.resource.ResourceUtil;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 5 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
6   -import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
7 6 import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
8 7 import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
9 8 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
10 9 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
11 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 14 import org.junit.jupiter.api.Test;
13 15 import org.springframework.boot.test.mock.mockito.MockBean;
14 16 import org.springframework.context.annotation.Import;
15 17  
16   -import jakarta.annotation.Resource;
17   -
18 18 import java.time.LocalDateTime;
  19 +import java.util.concurrent.atomic.AtomicReference;
19 20  
20 21 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
21 22 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
... ... @@ -29,7 +30,7 @@ import static org.mockito.Mockito.*;
29 30 public class FileServiceImplTest extends BaseDbUnitTest {
30 31  
31 32 @Resource
32   - private FileService fileService;
  33 + private FileServiceImpl fileService;
33 34  
34 35 @Resource
35 36 private FileMapper fileMapper;
... ... @@ -37,6 +38,12 @@ public class FileServiceImplTest extends BaseDbUnitTest {
37 38 @MockBean
38 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 47 @Test
41 48 public void testGetFilePage() {
42 49 // mock 数据
... ... @@ -70,28 +77,69 @@ public class FileServiceImplTest extends BaseDbUnitTest {
70 77 AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0));
71 78 }
72 79  
  80 + /**
  81 + * content、name、directory、type 都非空
  82 + */
73 83 @Test
74   - public void testCreateFile_success() throws Exception {
  84 + public void testCreateFile_success_01() throws Exception {
75 85 // 准备参数
76   - String path = randomString();
77 86 byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
  87 + String name = "单测文件名";
  88 + String directory = randomString();
  89 + String type = "image/jpeg";
78 90 // mock Master 文件客户端
79 91 FileClient client = mock(FileClient.class);
80 92 when(fileConfigService.getMasterFileClient()).thenReturn(client);
81 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 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 104 assertEquals(result, url);
89 105 // 校验数据
90   - FileDO file = fileMapper.selectOne(FileDO::getPath, path);
  106 + FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
91 107 assertEquals(10L, file.getConfigId());
92   - assertEquals(path, file.getPath());
  108 + assertEquals(pathRef.get(), file.getPath());
93 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 143 assertEquals(content.length, file.getSize());
96 144 }
97 145  
... ... @@ -140,4 +188,122 @@ public class FileServiceImplTest extends BaseDbUnitTest {
140 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 5 import cn.iocoder.yudao.framework.common.util.collection.ArrayUtils;
6 6 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
7 7 import cn.iocoder.yudao.framework.test.core.ut.BaseDbAndRedisUnitTest;
8   -import cn.iocoder.yudao.module.infra.api.file.FileApi;
9 8 import cn.iocoder.yudao.module.member.controller.app.user.vo.AppMemberUserUpdateMobileReqVO;
10 9 import cn.iocoder.yudao.module.member.dal.dataobject.user.MemberUserDO;
11 10 import cn.iocoder.yudao.module.member.dal.mysql.user.MemberUserMapper;
12 11 import cn.iocoder.yudao.module.member.service.auth.MemberAuthServiceImpl;
13 12 import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
  13 +import jakarta.annotation.Resource;
14 14 import org.junit.jupiter.api.Disabled;
15 15 import org.junit.jupiter.api.Test;
16 16 import org.springframework.boot.test.mock.mockito.MockBean;
... ... @@ -18,7 +18,6 @@ import org.springframework.context.annotation.Import;
18 18 import org.springframework.data.redis.core.StringRedisTemplate;
19 19 import org.springframework.security.crypto.password.PasswordEncoder;
20 20  
21   -import jakarta.annotation.Resource;
22 21 import java.util.function.Consumer;
23 22  
24 23 import static cn.hutool.core.util.RandomUtil.randomEle;
... ... @@ -53,8 +52,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest {
53 52  
54 53 @MockBean
55 54 private SmsCodeApi smsCodeApi;
56   - @MockBean
57   - private FileApi fileApi;
58 55  
59 56 // TODO 芋艿:后续重构这个单测
60 57 // @Test
... ... @@ -73,25 +70,6 @@ public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest {
73 70 // // 断言
74 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 74 @Test
97 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 218  
219 219 private String uploadFile(String mediaId, File file) {
220 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 23 import lombok.extern.slf4j.Slf4j;
24 24 import org.springframework.validation.annotation.Validated;
25 25 import org.springframework.web.bind.annotation.*;
26   -import org.springframework.web.multipart.MultipartFile;
27 26  
28 27 import java.util.List;
29 28  
30   -import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
31 29 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
32 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 32 @Tag(name = "管理后台 - 用户个人中心")
36 33 @RestController
... ... @@ -79,16 +76,4 @@ public class UserProfileController {
79 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 13 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
14 14 import jakarta.validation.Valid;
15 15  
16   -import java.io.InputStream;
17 16 import java.util.Collection;
18 17 import java.util.HashMap;
19 18 import java.util.List;
... ... @@ -74,14 +73,6 @@ public interface AdminUserService {
74 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 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 83 private UserPostMapper userPostMapper;
84 84  
85 85 @Resource
86   - private FileApi fileApi;
87   - @Resource
88 86 private ConfigApi configApi;
89 87  
90 88 @Override
... ... @@ -206,19 +204,6 @@ public class AdminUserServiceImpl implements AdminUserService {
206 204 }
207 205  
208 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 207 @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_UPDATE_PASSWORD_SUB_TYPE, bizNo = "{{#id}}",
223 208 success = SYSTEM_USER_UPDATE_PASSWORD_SUCCESS)
224 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 185 public void testSendSmsCode() {
186 186 // 准备参数
187 187 String mobile = randomString();
188   - Integer scene = randomEle(SmsSceneEnum.values()).getScene();
  188 + Integer scene = SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene();
189 189 AuthSmsSendReqVO reqVO = new AuthSmsSendReqVO(mobile, scene);
190 190 // mock 方法(用户信息)
191 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 11 import cn.iocoder.yudao.module.infra.api.file.FileApi;
12 12 import cn.iocoder.yudao.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO;
13 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 18 import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
16 19 import cn.iocoder.yudao.module.system.dal.dataobject.dept.PostDO;
17 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 27 import cn.iocoder.yudao.module.system.service.dept.PostService;
25 28 import cn.iocoder.yudao.module.system.service.permission.PermissionService;
26 29 import cn.iocoder.yudao.module.system.service.tenant.TenantService;
  30 +import jakarta.annotation.Resource;
27 31 import org.junit.jupiter.api.BeforeEach;
28 32 import org.junit.jupiter.api.Test;
29 33 import org.mockito.stubbing.Answer;
... ... @@ -31,14 +35,11 @@ import org.springframework.boot.test.mock.mockito.MockBean;
31 35 import org.springframework.context.annotation.Import;
32 36 import org.springframework.security.crypto.password.PasswordEncoder;
33 37  
34   -import jakarta.annotation.Resource;
35   -import java.io.ByteArrayInputStream;
36 38 import java.util.Collection;
37 39 import java.util.List;
38 40 import java.util.Map;
39 41 import java.util.function.Consumer;
40 42  
41   -import static cn.hutool.core.util.RandomUtil.randomBytes;
42 43 import static cn.hutool.core.util.RandomUtil.randomEle;
43 44 import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
44 45 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
... ... @@ -246,26 +247,6 @@ public class AdminUserServiceImplTest extends BaseDbUnitTest {
246 247 }
247 248  
248 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 250 public void testUpdateUserPassword02_success() {
270 251 // mock 数据
271 252 AdminUserDO dbUser = randomAdminUserDO();
... ...