Commit 472993e25ac2c837f22a0578740775d5adb1e69f
1 parent
858a351c
fix(infra): 修复文件 URL 特殊字符编码问题
- 新增 URL Path 编解码工具,保留 + 字符语义 - 文件访问基于原始 URI 解码 path,避免中文、空格、% 等字符下载异常 - 文件客户端生成 URL 时按路径段编码,S3 兼容完整 URL 和裸 path - 优化 Content-Disposition 文件名,支持 filename 和 filename* - 下载文件名优先使用文件记录的原始名称 - 补充 HTTP、Local、S3、文件服务相关单测
Showing
12 changed files
with
233 additions
and
11 deletions
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java
| ... | ... | @@ -9,6 +9,7 @@ import jakarta.servlet.http.HttpServletRequest; |
| 9 | 9 | import org.springframework.util.StringUtils; |
| 10 | 10 | import org.springframework.web.util.UriComponents; |
| 11 | 11 | import org.springframework.web.util.UriComponentsBuilder; |
| 12 | +import org.springframework.web.util.UriUtils; | |
| 12 | 13 | |
| 13 | 14 | import java.net.URI; |
| 14 | 15 | import java.net.URLDecoder; |
| ... | ... | @@ -55,11 +56,61 @@ public class HttpUtils { |
| 55 | 56 | * @return 解码后的路径 |
| 56 | 57 | */ |
| 57 | 58 | public static String decodeUrlPath(String path) { |
| 59 | + if (StrUtil.isEmpty(path)) { | |
| 60 | + return path; | |
| 61 | + } | |
| 58 | 62 | // 先将 + 替换为 %2B,避免被 URLDecoder 解码为空格 |
| 59 | 63 | String encoded = path.replace("+", "%2B"); |
| 60 | 64 | return URLDecoder.decode(encoded, StandardCharsets.UTF_8); |
| 61 | 65 | } |
| 62 | 66 | |
| 67 | + /** | |
| 68 | + * 编码 URL 路径,按路径段编码,保留 / 分隔符 | |
| 69 | + * | |
| 70 | + * @param path URL 路径,例如 20250602/xxx.pdf | |
| 71 | + * @return 编码后的路径 | |
| 72 | + */ | |
| 73 | + public static String encodeUrlPath(String path) { | |
| 74 | + if (StrUtil.isEmpty(path)) { | |
| 75 | + return path; | |
| 76 | + } | |
| 77 | + String[] segments = path.split(StrUtil.SLASH, -1); | |
| 78 | + StringBuilder result = new StringBuilder(path.length()); | |
| 79 | + for (int i = 0; i < segments.length; i++) { | |
| 80 | + if (i > 0) { | |
| 81 | + result.append(StrUtil.SLASH); | |
| 82 | + } | |
| 83 | + result.append(encodeUrlPathSegment(segments[i])); | |
| 84 | + } | |
| 85 | + return result.toString(); | |
| 86 | + } | |
| 87 | + | |
| 88 | + /** | |
| 89 | + * 编码 URL 路径段 | |
| 90 | + * | |
| 91 | + * @param segment URL 路径段 | |
| 92 | + * @return 编码后的路径段 | |
| 93 | + */ | |
| 94 | + public static String encodeUrlPathSegment(String segment) { | |
| 95 | + return UriUtils.encodePathSegment(segment, StandardCharsets.UTF_8); | |
| 96 | + } | |
| 97 | + | |
| 98 | + public static String removeUrlPathQueryAndFragment(String path) { | |
| 99 | + if (StrUtil.isEmpty(path)) { | |
| 100 | + return path; | |
| 101 | + } | |
| 102 | + int endIndex = path.length(); | |
| 103 | + int queryIndex = path.indexOf('?'); | |
| 104 | + if (queryIndex >= 0) { | |
| 105 | + endIndex = queryIndex; | |
| 106 | + } | |
| 107 | + int fragmentIndex = path.indexOf('#'); | |
| 108 | + if (fragmentIndex >= 0 && fragmentIndex < endIndex) { | |
| 109 | + endIndex = fragmentIndex; | |
| 110 | + } | |
| 111 | + return path.substring(0, endIndex); | |
| 112 | + } | |
| 113 | + | |
| 63 | 114 | public static String replaceUrlQuery(String url, String key, String value) { |
| 64 | 115 | UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); |
| 65 | 116 | // 先移除;再添加 | ... | ... |
yudao-framework/yudao-common/src/test/java/cn/iocoder/yudao/framework/common/util/http/HttpUtilsTest.java
| ... | ... | @@ -10,6 +10,36 @@ import static org.junit.jupiter.api.Assertions.assertEquals; |
| 10 | 10 | public class HttpUtilsTest { |
| 11 | 11 | |
| 12 | 12 | @Test |
| 13 | + public void testEncodeUrlPath() { | |
| 14 | + // 准备参数 | |
| 15 | + String path = "avatar/中文 100%+文件.jpg"; | |
| 16 | + | |
| 17 | + // 调用 | |
| 18 | + String result = HttpUtils.encodeUrlPath(path); | |
| 19 | + | |
| 20 | + // 断言 | |
| 21 | + assertEquals("avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result); | |
| 22 | + } | |
| 23 | + | |
| 24 | + @Test | |
| 25 | + public void testDecodeUrlPath() { | |
| 26 | + // 准备参数:+ 是路径字符,不应该按 query parameter 语义解码为空格 | |
| 27 | + String path = "avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg"; | |
| 28 | + | |
| 29 | + // 调用 | |
| 30 | + String result = HttpUtils.decodeUrlPath(path); | |
| 31 | + | |
| 32 | + // 断言 | |
| 33 | + assertEquals("avatar/中文 100%+文件.jpg", result); | |
| 34 | + } | |
| 35 | + | |
| 36 | + @Test | |
| 37 | + public void testRemoveUrlPathQueryAndFragment() { | |
| 38 | + assertEquals("avatar/test.jpg", HttpUtils.removeUrlPathQueryAndFragment("avatar/test.jpg?token=1#preview")); | |
| 39 | + assertEquals("avatar/test.jpg", HttpUtils.removeUrlPathQueryAndFragment("avatar/test.jpg#preview?token=1")); | |
| 40 | + } | |
| 41 | + | |
| 42 | + @Test | |
| 13 | 43 | public void testReplaceUrlQuery_replace() { |
| 14 | 44 | // 准备参数 |
| 15 | 45 | String url = "https://www.iocoder.cn/path?a=1&b=2"; | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java
| 1 | 1 | package cn.iocoder.yudao.module.infra.controller.admin.file; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.io.FileUtil; | |
| 3 | 4 | import cn.hutool.core.io.IoUtil; |
| 4 | 5 | import cn.hutool.core.util.StrUtil; |
| 5 | -import cn.hutool.core.util.URLUtil; | |
| 6 | 6 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 7 | 7 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 8 | +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; | |
| 8 | 9 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 9 | 10 | import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore; |
| 10 | 11 | import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.*; |
| ... | ... | @@ -27,7 +28,6 @@ import org.springframework.validation.annotation.Validated; |
| 27 | 28 | import org.springframework.web.bind.annotation.*; |
| 28 | 29 | import org.springframework.web.multipart.MultipartFile; |
| 29 | 30 | |
| 30 | -import java.nio.charset.StandardCharsets; | |
| 31 | 31 | import java.util.List; |
| 32 | 32 | |
| 33 | 33 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| ... | ... | @@ -111,10 +111,10 @@ public class FileController { |
| 111 | 111 | if (StrUtil.isEmpty(path)) { |
| 112 | 112 | throw new IllegalArgumentException("结尾的 path 路径必须传递"); |
| 113 | 113 | } |
| 114 | - // 解码,解决中文路径的问题 | |
| 114 | + // 解码,解决中文、%、+ 等特殊字符路径的问题 | |
| 115 | 115 | // https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/ |
| 116 | 116 | // https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1432/ |
| 117 | - path = URLUtil.decode(path, StandardCharsets.UTF_8, false); | |
| 117 | + path = HttpUtils.decodeUrlPath(path); | |
| 118 | 118 | |
| 119 | 119 | // 读取内容 |
| 120 | 120 | byte[] content = fileService.getFileContent(configId, path); |
| ... | ... | @@ -123,7 +123,9 @@ public class FileController { |
| 123 | 123 | response.setStatus(HttpStatus.NOT_FOUND.value()); |
| 124 | 124 | return; |
| 125 | 125 | } |
| 126 | - writeAttachment(response, path, content); | |
| 126 | + FileDO file = fileService.getFileByConfigIdAndPath(configId, path); | |
| 127 | + String filename = file != null && StrUtil.isNotEmpty(file.getName()) ? file.getName() : FileUtil.getName(path); | |
| 128 | + writeAttachment(response, filename, content); | |
| 127 | 129 | } |
| 128 | 130 | |
| 129 | 131 | @GetMapping("/page") | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java
| ... | ... | @@ -23,4 +23,11 @@ public interface FileMapper extends BaseMapperX<FileDO> { |
| 23 | 23 | .orderByDesc(FileDO::getId)); |
| 24 | 24 | } |
| 25 | 25 | |
| 26 | + default FileDO selectLatestByConfigIdAndPath(Long configId, String path) { | |
| 27 | + return selectLastOne(new LambdaQueryWrapperX<FileDO>() | |
| 28 | + .eq(FileDO::getConfigId, configId) | |
| 29 | + .eq(FileDO::getPath, path) | |
| 30 | + .orderByAsc(FileDO::getId)); | |
| 31 | + } | |
| 32 | + | |
| 26 | 33 | } | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/AbstractFileClient.java
| 1 | 1 | package cn.iocoder.yudao.module.infra.framework.file.core.client; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; | |
| 4 | 5 | import lombok.extern.slf4j.Slf4j; |
| 5 | 6 | |
| 6 | 7 | /** |
| ... | ... | @@ -72,7 +73,7 @@ public abstract class AbstractFileClient<Config extends FileClientConfig> implem |
| 72 | 73 | * @return URL 访问地址 |
| 73 | 74 | */ |
| 74 | 75 | protected String formatFileUrl(String domain, String path) { |
| 75 | - return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), path); | |
| 76 | + return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), HttpUtils.encodeUrlPath(path)); | |
| 76 | 77 | } |
| 77 | 78 | |
| 78 | 79 | } | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java
| ... | ... | @@ -116,13 +116,18 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> { |
| 116 | 116 | @Override |
| 117 | 117 | public String presignGetUrl(String url, Integer expirationSeconds) { |
| 118 | 118 | // 1. 将 url 转换为 path |
| 119 | - String path = StrUtil.removePrefix(url, config.getDomain() + "/"); | |
| 120 | - path = HttpUtils.decodeUrlPath(HttpUtils.removeUrlQuery(path)); | |
| 119 | + // 完整 Domain URL 会解码还原为原始对象名;裸 path 约定已经是未编码的对象名 | |
| 120 | + boolean domainUrl = StrUtil.startWith(url, config.getDomain() + "/"); | |
| 121 | + String path = domainUrl ? StrUtil.removePrefix(url, config.getDomain() + "/") : url; | |
| 122 | + if (domainUrl) { | |
| 123 | + path = HttpUtils.removeUrlPathQueryAndFragment(path); | |
| 124 | + path = HttpUtils.decodeUrlPath(path); | |
| 125 | + } | |
| 121 | 126 | |
| 122 | 127 | // 2.1 情况一:公开访问:无需签名 |
| 123 | 128 | // 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名 |
| 124 | 129 | if (!BooleanUtil.isFalse(config.getEnablePublicAccess())) { |
| 125 | - return config.getDomain() + "/" + path; | |
| 130 | + return config.getDomain() + "/" + HttpUtils.encodeUrlPath(path); | |
| 126 | 131 | } |
| 127 | 132 | |
| 128 | 133 | // 2.2 情况二:私有访问:生成 GET 预签名 URL | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
| ... | ... | @@ -85,9 +85,9 @@ public class FileTypeUtils { |
| 85 | 85 | // 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html |
| 86 | 86 | if (isImage(mineType)) { |
| 87 | 87 | // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论 |
| 88 | - response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename)); | |
| 88 | + response.setHeader("Content-Disposition", buildContentDisposition("inline", filename)); | |
| 89 | 89 | } else { |
| 90 | - response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); | |
| 90 | + response.setHeader("Content-Disposition", buildContentDisposition("attachment", filename)); | |
| 91 | 91 | } |
| 92 | 92 | // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 |
| 93 | 93 | if (StrUtil.containsIgnoreCase(mineType, "video")) { |
| ... | ... | @@ -98,6 +98,29 @@ public class FileTypeUtils { |
| 98 | 98 | IoUtil.write(response.getOutputStream(), false, content); |
| 99 | 99 | } |
| 100 | 100 | |
| 101 | + private static String buildContentDisposition(String dispositionType, String filename) { | |
| 102 | + return StrUtil.format("{};filename=\"{}\";filename*=UTF-8''{}", | |
| 103 | + dispositionType, buildFallbackFilename(filename), HttpUtils.encodeUrlPathSegment(filename)); | |
| 104 | + } | |
| 105 | + | |
| 106 | + private static String buildFallbackFilename(String filename) { | |
| 107 | + if (StrUtil.isEmpty(filename)) { | |
| 108 | + return "download"; | |
| 109 | + } | |
| 110 | + StringBuilder result = new StringBuilder(filename.length()); | |
| 111 | + for (int i = 0; i < filename.length(); i++) { | |
| 112 | + char ch = filename.charAt(i); | |
| 113 | + if (ch == '"' || ch == '\\') { | |
| 114 | + result.append('\\').append(ch); | |
| 115 | + } else if (ch >= 0x20 && ch <= 0x7E) { | |
| 116 | + result.append(ch); | |
| 117 | + } else { | |
| 118 | + result.append('_'); | |
| 119 | + } | |
| 120 | + } | |
| 121 | + return result.toString(); | |
| 122 | + } | |
| 123 | + | |
| 101 | 124 | /** |
| 102 | 125 | * 判断是否是图片 |
| 103 | 126 | * | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java
| ... | ... | @@ -86,4 +86,13 @@ public interface FileService { |
| 86 | 86 | */ |
| 87 | 87 | byte[] getFileContent(Long configId, String path) throws Exception; |
| 88 | 88 | |
| 89 | + /** | |
| 90 | + * 获得文件 | |
| 91 | + * | |
| 92 | + * @param configId 配置编号 | |
| 93 | + * @param path 文件路径 | |
| 94 | + * @return 文件 | |
| 95 | + */ | |
| 96 | + FileDO getFileByConfigIdAndPath(Long configId, String path); | |
| 97 | + | |
| 89 | 98 | } | ... | ... |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java
| ... | ... | @@ -238,4 +238,9 @@ public class FileServiceImpl implements FileService { |
| 238 | 238 | return client.getContent(path); |
| 239 | 239 | } |
| 240 | 240 | |
| 241 | + @Override | |
| 242 | + public FileDO getFileByConfigIdAndPath(Long configId, String path) { | |
| 243 | + return fileMapper.selectLatestByConfigIdAndPath(configId, path); | |
| 244 | + } | |
| 245 | + | |
| 241 | 246 | } | ... | ... |
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java
| ... | ... | @@ -42,6 +42,21 @@ public class LocalFileClientTest { |
| 42 | 42 | } |
| 43 | 43 | |
| 44 | 44 | @Test |
| 45 | + public void testUpload_encodeUrlPath() { | |
| 46 | + // 准备参数 | |
| 47 | + LocalFileClient client = createClient(); | |
| 48 | + byte[] content = "test".getBytes(StandardCharsets.UTF_8); | |
| 49 | + String path = "avatar/中文 100%+文件.txt"; | |
| 50 | + | |
| 51 | + // 调用 | |
| 52 | + String url = client.upload(content, path, "text/plain"); | |
| 53 | + | |
| 54 | + // 断言 | |
| 55 | + assertEquals("http://127.0.0.1:48080/admin-api/infra/file/0/get/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.txt", url); | |
| 56 | + assertArrayEquals(content, FileUtil.readBytes(new File(tempDir, path))); | |
| 57 | + } | |
| 58 | + | |
| 59 | + @Test | |
| 45 | 60 | public void testUpload_pathInvalid() { |
| 46 | 61 | // 准备参数 |
| 47 | 62 | LocalFileClient client = createClient(); | ... | ... |
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java
| ... | ... | @@ -9,10 +9,67 @@ import jakarta.validation.Validation; |
| 9 | 9 | import org.junit.jupiter.api.Disabled; |
| 10 | 10 | import org.junit.jupiter.api.Test; |
| 11 | 11 | |
| 12 | +import static org.junit.jupiter.api.Assertions.assertEquals; | |
| 13 | +import static org.junit.jupiter.api.Assertions.assertFalse; | |
| 14 | +import static org.junit.jupiter.api.Assertions.assertTrue; | |
| 15 | + | |
| 12 | 16 | @SuppressWarnings("resource") |
| 13 | 17 | public class S3FileClientTest { |
| 14 | 18 | |
| 15 | 19 | @Test |
| 20 | + public void testPresignGetUrl_publicAccess_encodeUrlPath() { | |
| 21 | + // 准备参数 | |
| 22 | + S3FileClientConfig config = new S3FileClientConfig(); | |
| 23 | + config.setDomain("https://static.iocoder.cn"); | |
| 24 | + config.setEnablePublicAccess(true); | |
| 25 | + S3FileClient client = new S3FileClient(0L, config); | |
| 26 | + | |
| 27 | + // 调用 | |
| 28 | + String result = client.presignGetUrl("avatar/中文 100%+文件.jpg", 300); | |
| 29 | + | |
| 30 | + // 断言 | |
| 31 | + assertEquals("https://static.iocoder.cn/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result); | |
| 32 | + } | |
| 33 | + | |
| 34 | + @Test | |
| 35 | + public void testPresignGetUrl_publicAccess_decodeDomainUrl() { | |
| 36 | + // 准备参数 | |
| 37 | + S3FileClientConfig config = new S3FileClientConfig(); | |
| 38 | + config.setDomain("https://static.iocoder.cn"); | |
| 39 | + config.setEnablePublicAccess(true); | |
| 40 | + S3FileClient client = new S3FileClient(0L, config); | |
| 41 | + | |
| 42 | + // 调用 | |
| 43 | + String result = client.presignGetUrl("https://static.iocoder.cn/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg?token=1", 300); | |
| 44 | + | |
| 45 | + // 断言 | |
| 46 | + assertEquals("https://static.iocoder.cn/avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result); | |
| 47 | + } | |
| 48 | + | |
| 49 | + @Test | |
| 50 | + public void testPresignGetUrl_privateAccess_rawPath() { | |
| 51 | + // 准备参数 | |
| 52 | + S3FileClientConfig config = new S3FileClientConfig(); | |
| 53 | + config.setAccessKey("admin"); | |
| 54 | + config.setAccessSecret("password"); | |
| 55 | + config.setBucket("yudao"); | |
| 56 | + config.setDomain("http://127.0.0.1:9000/yudao"); | |
| 57 | + config.setEndpoint("http://127.0.0.1:9000"); | |
| 58 | + config.setEnablePathStyleAccess(true); | |
| 59 | + config.setEnablePublicAccess(false); | |
| 60 | + S3FileClient client = new S3FileClient(0L, config); | |
| 61 | + client.init(); | |
| 62 | + | |
| 63 | + // 调用 | |
| 64 | + String result = client.presignGetUrl("avatar/中文 100%+文件.jpg", 300); | |
| 65 | + | |
| 66 | + // 断言 | |
| 67 | + assertTrue(result.contains("/yudao/avatar/%E4%B8%AD%E6%96%87%20100%25")); | |
| 68 | + assertTrue(result.contains("%E6%96%87%E4%BB%B6.jpg")); | |
| 69 | + assertFalse(result.contains("%25E4%25B8%25AD")); | |
| 70 | + } | |
| 71 | + | |
| 72 | + @Test | |
| 16 | 73 | @Disabled // MinIO,如果要集成测试,可以注释本行 |
| 17 | 74 | public void testMinIO() throws Exception { |
| 18 | 75 | S3FileClientConfig config = new S3FileClientConfig(); | ... | ... |
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java
| ... | ... | @@ -212,6 +212,23 @@ public class FileServiceImplTest extends BaseDbUnitTest { |
| 212 | 212 | } |
| 213 | 213 | |
| 214 | 214 | @Test |
| 215 | + public void testGetFileByConfigIdAndPath() { | |
| 216 | + // mock 数据 | |
| 217 | + FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("avatar/中文 100%+文件.jpg")); | |
| 218 | + fileMapper.insert(dbFile); | |
| 219 | + FileDO latestFile = ObjectUtils.cloneIgnoreId(dbFile, o -> o.setName("最新文件名.jpg")); | |
| 220 | + fileMapper.insert(latestFile); | |
| 221 | + fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setPath("avatar/other.jpg"))); | |
| 222 | + fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setConfigId(20L))); | |
| 223 | + | |
| 224 | + // 调用 | |
| 225 | + FileDO result = fileService.getFileByConfigIdAndPath(10L, "avatar/中文 100%+文件.jpg"); | |
| 226 | + | |
| 227 | + // 断言 | |
| 228 | + AssertUtils.assertPojoEquals(latestFile, result); | |
| 229 | + } | |
| 230 | + | |
| 231 | + @Test | |
| 215 | 232 | public void testCreateFileByPresignedPath_success() { |
| 216 | 233 | // 准备参数 |
| 217 | 234 | FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> { | ... | ... |