diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java index 5b7808baf0..3f662a85e7 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/http/HttpUtils.java @@ -49,8 +49,15 @@ public class HttpUtils { return builder.build(); } - private String append(String base, Map query, boolean fragment) { - return append(base, query, null, fragment); + public static String removeUrlQuery(String url) { + if (!StrUtil.contains(url, '?')) { + return url; + } + UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); + // 移除 query、fragment + builder.setQuery(null); + builder.setFragment(null); + return builder.build(); } /** diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index 17a4387100..dc74078c8f 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -111,9 +111,6 @@ public class GlobalExceptionHandler { if (ex instanceof AccessDeniedException) { return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); } - if (ex instanceof UncheckedExecutionException && ex.getCause() != ex) { - return allExceptionHandler(request, ex.getCause()); - } return defaultExceptionHandler(request, ex); } @@ -308,6 +305,12 @@ public class GlobalExceptionHandler { */ @ExceptionHandler(value = Exception.class) public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { + // 特殊:如果是 ServiceException 的异常,则直接返回 + // 例如说:https://gitee.com/zhijiantianya/yudao-cloud/issues/ICSSRM、https://gitee.com/zhijiantianya/yudao-cloud/issues/ICT6FM + if (ex.getCause() != null && ex.getCause() instanceof ServiceException) { + return serviceExceptionHandler((ServiceException) ex.getCause()); + } + // 情况一:处理表不存在的异常 CommonResult tableNotExistsResult = handleTableNotExists(ex); if (tableNotExistsResult != null) { diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java index 7b9fcc99a2..ace79f0231 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApi.java @@ -42,4 +42,14 @@ public interface FileApi { String createFile(@NotEmpty(message = "文件内容不能为空") byte[] content, String name, String directory, String type); + /** + * 生成文件预签名地址,用于读取 + * + * @param url 完整的文件访问地址 + * @param expirationSeconds 访问有效期,单位秒 + * @return 文件预签名地址 + */ + String presignGetUrl(@NotEmpty(message = "URL 不能为空") String url, + Integer expirationSeconds); + } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java index 903d2058db..14d5502f93 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/file/FileApiImpl.java @@ -23,4 +23,9 @@ public class FileApiImpl implements FileApi { return fileService.createFile(content, name, directory, type); } + @Override + public String presignGetUrl(String url, Integer expirationSeconds) { + return fileService.presignGetUrl(url, expirationSeconds); + } + } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java index 55e3f7fc5c..c55c3c9621 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/FileController.java @@ -51,7 +51,7 @@ public class FileController { } @GetMapping("/presigned-url") - @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") + @Operation(summary = "获取文件预签名地址(上传)", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") @Parameters({ @Parameter(name = "name", description = "文件名称", required = true), @Parameter(name = "directory", description = "文件目录") @@ -59,7 +59,7 @@ public class FileController { public CommonResult getFilePresignedUrl( @RequestParam("name") String name, @RequestParam(value = "directory", required = false) String directory) { - return success(fileService.getFilePresignedUrl(name, directory)); + return success(fileService.presignPutUrl(name, directory)); } @PostMapping("/create") diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java index 47dcde7cf5..7e354fa05b 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java @@ -42,7 +42,7 @@ public class AppFileController { } @GetMapping("/presigned-url") - @Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") + @Operation(summary = "获取文件预签名地址(上传)", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器") @Parameters({ @Parameter(name = "name", description = "文件名称", required = true), @Parameter(name = "directory", description = "文件目录") @@ -50,7 +50,7 @@ public class AppFileController { public CommonResult getFilePresignedUrl( @RequestParam("name") String name, @RequestParam(value = "directory", required = false) String directory) { - return success(fileService.getFilePresignedUrl(name, directory)); + return success(fileService.presignPutUrl(name, directory)); } @PostMapping("/create") diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java index 053b3c5101..cf1cd620ae 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/FileClient.java @@ -1,7 +1,5 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client; -import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; - /** * 文件客户端 * @@ -42,13 +40,26 @@ public interface FileClient { */ byte[] getContent(String path) throws Exception; + // ========== 文件签名,目前仅 S3 支持 ========== + /** - * 获得文件预签名地址 + * 获得文件预签名地址,用于上传 * * @param path 相对路径 * @return 文件预签名地址 */ - default FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception { + default String presignPutUrl(String path) { + throw new UnsupportedOperationException("不支持的操作"); + } + + /** + * 生成文件预签名地址,用于读取 + * + * @param url 完整的文件访问地址 + * @param expirationSeconds 访问有效期,单位秒 + * @return 文件预签名地址 + */ + default String presignGetUrl(String url, Integer expirationSeconds) { throw new UnsupportedOperationException("不支持的操作"); } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java index 7fa2a7ea9a..6e5c0229ba 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/local/LocalFileClient.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client.local; import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.IORuntimeException; import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; import java.io.File; @@ -38,7 +39,14 @@ public class LocalFileClient extends AbstractFileClient { @Override public byte[] getContent(String path) { String filePath = getFilePath(path); - return FileUtil.readBytes(filePath); + try { + return FileUtil.readBytes(filePath); + } catch (IORuntimeException ex) { + if (ex.getMessage().startsWith("File not exist:")) { + return null; + } + throw ex; + } } private String getFilePath(String path) { diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java deleted file mode 100644 index 6a1258e9e0..0000000000 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/FilePresignedUrlRespDTO.java +++ /dev/null @@ -1,29 +0,0 @@ -package cn.iocoder.yudao.module.infra.framework.file.core.client.s3; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 文件预签名地址 Response DTO - * - * @author owen - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class FilePresignedUrlRespDTO { - - /** - * 文件上传 URL(用于上传) - * - * 例如说: - */ - private String uploadUrl; - - /** - * 文件 URL(用于读取、下载等) - */ - private String url; - -} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java index a33f0d738c..94ba6a3ebb 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClient.java @@ -1,8 +1,10 @@ package cn.iocoder.yudao.module.infra.framework.file.core.client.s3; import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.module.infra.framework.file.core.client.AbstractFileClient; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; @@ -15,9 +17,11 @@ import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; import java.net.URI; +import java.net.URL; import java.time.Duration; /** @@ -27,6 +31,8 @@ import java.time.Duration; */ public class S3FileClient extends AbstractFileClient { + private static final Duration EXPIRATION_DEFAULT = Duration.ofHours(24); + private S3Client client; private S3Presigner presigner; @@ -75,7 +81,7 @@ public class S3FileClient extends AbstractFileClient { // 上传文件 client.putObject(putRequest, RequestBody.fromBytes(content)); // 拼接返回路径 - return config.getDomain() + "/" + path; + return presignGetUrl(path, null); } @Override @@ -97,23 +103,33 @@ public class S3FileClient extends AbstractFileClient { } @Override - public FilePresignedUrlRespDTO getPresignedObjectUrl(String path) { - Duration expiration = Duration.ofHours(24); - return new FilePresignedUrlRespDTO(getPresignedUrl(path, expiration), config.getDomain() + "/" + path); + public String presignPutUrl(String path) { + return presigner.presignPutObject(PutObjectPresignRequest.builder() + .signatureDuration(EXPIRATION_DEFAULT) + .putObjectRequest(b -> b.bucket(config.getBucket()).key(path)).build()) + .url().toString(); } - /** - * 生成动态的预签名上传 URL - * - * @param path 相对路径 - * @param expiration 过期时间 - * @return 生成的上传 URL - */ - private String getPresignedUrl(String path, Duration expiration) { - return presigner.presignPutObject(PutObjectPresignRequest.builder() + @Override + public String presignGetUrl(String url, Integer expirationSeconds) { + // 1. 将 url 转换为 path + String path = StrUtil.removePrefix(url, config.getDomain() + "/"); + path = HttpUtils.removeUrlQuery(path); + + // 2.1 情况一:公开访问:无需签名 + // 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名 + if (!BooleanUtil.isFalse(config.getEnablePublicAccess())) { + return config.getDomain() + "/" + path; + } + + // 2.2 情况二:私有访问:生成 GET 预签名 URL + String finalPath = path; + Duration expiration = expirationSeconds != null ? Duration.ofSeconds(expirationSeconds) : EXPIRATION_DEFAULT; + URL signedUrl = presigner.presignGetObject(GetObjectPresignRequest.builder() .signatureDuration(expiration) - .putObjectRequest(b -> b.bucket(config.getBucket()).key(path)) - .build()).url().toString(); + .getObjectRequest(b -> b.bucket(config.getBucket()).key(finalPath)).build()) + .url(); + return signedUrl.toString(); } /** diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java index c4a402b3b3..ba703ff3a8 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/client/s3/S3FileClientConfig.java @@ -73,6 +73,15 @@ public class S3FileClientConfig implements FileClientConfig { @NotNull(message = "enablePathStyleAccess 不能为空") private Boolean enablePathStyleAccess; + /** + * 是否公开访问 + * + * true:公开访问,所有人都可以访问 + * false:私有访问,只有配置的 accessKey 才可以访问 + */ + @NotNull(message = "是否公开访问不能为空") + private Boolean enablePublicAccess; + @SuppressWarnings("RedundantIfStatement") @AssertTrue(message = "domain 不能为空") @JsonIgnore diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java index 251f3c9270..29cfb40eb2 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java @@ -80,9 +80,15 @@ public class FileTypeUtils { */ public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { // 设置 header 和 contentType - response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); String contentType = getMineType(content, filename); response.setContentType(contentType); + // 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html + if (StrUtil.containsIgnoreCase(contentType, "image/")) { + // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论 + response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename)); + } else { + response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); + } // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 if (StrUtil.containsIgnoreCase(contentType, "video")) { response.setHeader("Content-Length", String.valueOf(content.length)); diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java index 6a80818fcc..9e3f42680b 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java @@ -37,14 +37,22 @@ public interface FileService { String name, String directory, String type); /** - * 生成文件预签名地址信息 + * 生成文件预签名地址信息,用于上传 * * @param name 文件名 * @param directory 目录 * @return 预签名地址信息 */ - FilePresignedUrlRespVO getFilePresignedUrl(@NotEmpty(message = "文件名不能为空") String name, - String directory); + FilePresignedUrlRespVO presignPutUrl(@NotEmpty(message = "文件名不能为空") String name, + String directory); + /** + * 生成文件预签名地址信息,用于读取 + * + * @param url 完整的文件访问地址 + * @param expirationSeconds 访问有效期,单位秒 + * @return 文件预签名地址 + */ + String presignGetUrl(String url, Integer expirationSeconds); /** * 创建文件 diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java index e476fe1336..36965b70d9 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -6,6 +6,7 @@ import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import cn.hutool.crypto.digest.DigestUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; @@ -13,7 +14,6 @@ import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresigned import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; -import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.FilePresignedUrlRespDTO; import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; import com.google.common.annotations.VisibleForTesting; import lombok.SneakyThrows; @@ -126,19 +126,27 @@ public class FileServiceImpl implements FileService { @Override @SneakyThrows - public FilePresignedUrlRespVO getFilePresignedUrl(String name, String directory) { + public FilePresignedUrlRespVO presignPutUrl(String name, String directory) { // 1. 生成上传的 path,需要保证唯一 String path = generateUploadPath(name, directory); // 2. 获取文件预签名地址 FileClient fileClient = fileConfigService.getMasterFileClient(); - FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path); - return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class, - object -> object.setConfigId(fileClient.getId()).setPath(path)); + String uploadUrl = fileClient.presignPutUrl(path); + String visitUrl = fileClient.presignGetUrl(path, null); + return new FilePresignedUrlRespVO().setConfigId(fileClient.getId()) + .setPath(path).setUploadUrl(uploadUrl).setUrl(visitUrl); + } + + @Override + public String presignGetUrl(String url, Integer expirationSeconds) { + FileClient fileClient = fileConfigService.getMasterFileClient(); + return fileClient.presignGetUrl(url, expirationSeconds); } @Override public Long createFile(FileCreateReqVO createReqVO) { + createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数 FileDO file = BeanUtils.toBean(createReqVO, FileDO.class); fileMapper.insert(file); return file.getId(); diff --git a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java index 7c622a5306..6bc8c7bfe9 100644 --- a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java +++ b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/local/LocalFileClientTest.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.infra.framework.file.core.client.local.LocalFileC import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; + public class LocalFileClientTest { @Test @@ -26,4 +28,18 @@ public class LocalFileClientTest { client.delete(path); } + @Test + @Disabled + public void testGetContent_notFound() { + // 创建客户端 + LocalFileClientConfig config = new LocalFileClientConfig(); + config.setDomain("http://127.0.0.1:48080"); + config.setBasePath("/Users/yunai/file_test"); + LocalFileClient client = new LocalFileClient(0L, config); + client.init(); + // 上传文件 + byte[] content = client.getContent(randomString()); + System.out.println(); + } + } diff --git a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java index 1933e98587..981971b9f9 100644 --- a/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java +++ b/yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/framework/file/core/s3/S3FileClientTest.java @@ -5,11 +5,11 @@ import cn.hutool.core.util.IdUtil; import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils; import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClient; import cn.iocoder.yudao.module.infra.framework.file.core.client.s3.S3FileClientConfig; +import jakarta.validation.Validation; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import javax.validation.Validation; - +@SuppressWarnings("resource") public class S3FileClientTest { @Test @@ -71,6 +71,7 @@ public class S3FileClientTest { config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP"); config.setBucket("ruoyi-vue-pro"); config.setDomain("http://test.yudao.iocoder.cn"); // 如果有自定义域名,则可以设置。http://static.yudao.iocoder.cn + config.setEnablePathStyleAccess(false); // 默认上海的 endpoint config.setEndpoint("s3-cn-south-1.qiniucs.com"); @@ -78,6 +79,32 @@ public class S3FileClientTest { testExecuteUpload(config); } + @Test + @Disabled // 七牛云存储(读私有桶),如果要集成测试,可以注释本行 + public void testQiniu_privateGet() { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 +// config.setAccessKey(System.getenv("QINIU_ACCESS_KEY")); +// config.setAccessSecret(System.getenv("QINIU_SECRET_KEY")); + config.setAccessKey("b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8"); + config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP"); + config.setBucket("ruoyi-vue-pro-private"); + config.setDomain("http://t151glocd.hn-bkt.clouddn.com"); // 如果有自定义域名,则可以设置。http://static.yudao.iocoder.cn + config.setEnablePathStyleAccess(false); + // 默认上海的 endpoint + config.setEndpoint("s3-cn-south-1.qiniucs.com"); + + // 校验配置 + ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config); + // 创建 Client + S3FileClient client = new S3FileClient(0L, config); + client.init(); + // 执行生成 URL 签名 + String path = "output.png"; + String presignedUrl = client.presignGetUrl(path, 300); + System.out.println(presignedUrl); + } + @Test @Disabled // 华为云存储,如果要集成测试,可以注释本行 public void testHuaweiCloud() throws Exception { @@ -94,7 +121,7 @@ public class S3FileClientTest { testExecuteUpload(config); } - private void testExecuteUpload(S3FileClientConfig config) throws Exception { + private void testExecuteUpload(S3FileClientConfig config) { // 校验配置 ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config); // 创建 Client diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java index 5f38b1ac5d..a06f86b150 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java @@ -541,14 +541,23 @@ public abstract class AbstractWxPayClient extends AbstractPayClient官方示例 */ private SignatureHeader getRequestHeader(Map headers) { + // 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/ICSFL6 return SignatureHeader.builder() - .signature(headers.get("wechatpay-signature")) - .nonce(headers.get("wechatpay-nonce")) - .serial(headers.get("wechatpay-serial")) - .timeStamp(headers.get("wechatpay-timestamp")) + .signature(getHeaderValue(headers, "Wechatpay-Signature", "wechatpay-signature")) + .nonce(getHeaderValue(headers, "Wechatpay-Nonce", "wechatpay-nonce")) + .serial(getHeaderValue(headers, "Wechatpay-Serial", "wechatpay-serial")) + .timeStamp(getHeaderValue(headers, "Wechatpay-Timestamp", "wechatpay-timestamp")) .build(); } + private String getHeaderValue(Map headers, String capitalizedKey, String lowercaseKey) { + String value = headers.get(capitalizedKey); + if (value != null) { + return value; + } + return headers.get(lowercaseKey); + } + // TODO @芋艿:可能是 wxjava 的 bug:https://github.com/binarywang/WxJava/issues/1557 private void fixV3HttpClientConnectionPoolShutDown() { client.getConfig().setApiV3HttpClient(null); diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java index f2388711ae..31245fd0db 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/sms/SmsLogMapper.java @@ -22,4 +22,8 @@ public interface SmsLogMapper extends BaseMapperX { .orderByDesc(SmsLogDO::getId)); } + default SmsLogDO selectByApiSerialNo(String apiSerialNo) { + return selectOne(SmsLogDO::getApiSerialNo, apiSerialNo); + } + } diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java index 19cde8c262..653458f461 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/TencentSmsClient.java @@ -119,6 +119,7 @@ public class TencentSmsClient extends AbstractSmsClient { return new SmsReceiveRespDTO() .setSuccess("SUCCESS".equals(statusObj.getStr("report_status"))) // 是否接收成功 .setErrorCode(statusObj.getStr("errmsg")) // 状态报告编码 + .setErrorMsg(statusObj.getStr("description")) // 状态报告描述 .setMobile(statusObj.getStr("mobile")) // 手机号 .setReceiveTime(statusObj.getLocalDateTime("user_receive_time", null)) // 状态报告时间 .setSerialNo(statusObj.getStr("sid")); // 发送序列号 diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java index 0c86c0f07f..49ec93aaca 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogService.java @@ -12,7 +12,7 @@ import java.util.Map; * 短信日志 Service 接口 * * @author zzf - * @date 13:48 2021/3/2 + * @since 13:48 2021/3/2 */ public interface SmsLogService { @@ -49,12 +49,13 @@ public interface SmsLogService { * 更新日志的接收结果 * * @param id 日志编号 + * @param apiSerialNo 发送编号 * @param success 是否接收成功 * @param receiveTime 用户接收时间 * @param apiReceiveCode API 接收结果的编码 * @param apiReceiveMsg API 接收结果的说明 */ - void updateSmsReceiveResult(Long id, Boolean success, + void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg); /** diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java index c79b94123c..45660e60e0 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImpl.java @@ -10,7 +10,8 @@ import cn.iocoder.yudao.module.system.enums.sms.SmsSendStatusEnum; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import javax.annotation.Resource; +import jakarta.annotation.Resource; + import java.time.LocalDateTime; import java.util.Map; import java.util.Objects; @@ -63,10 +64,17 @@ public class SmsLogServiceImpl implements SmsLogService { } @Override - public void updateSmsReceiveResult(Long id, Boolean success, LocalDateTime receiveTime, + public void updateSmsReceiveResult(Long id, String apiSerialNo, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg) { SmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? SmsReceiveStatusEnum.SUCCESS : SmsReceiveStatusEnum.FAILURE; + if (id == null || id == 0) { + SmsLogDO log = smsLogMapper.selectByApiSerialNo(apiSerialNo); + if (log == null) { + return; + } + id = log.getId(); + } smsLogMapper.updateById(SmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()) .receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); } diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java index 49af0eceac..89adc3da7c 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImpl.java @@ -184,7 +184,7 @@ public class SmsSendServiceImpl implements SmsSendService { return; } // 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新 - receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), + receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), result.getSerialNo(), result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg())); } diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java index eed34b988a..1d86cc2dae 100644 --- a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java +++ b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsLogServiceImplTest.java @@ -41,47 +41,47 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest { @Test public void testGetSmsLogPage() { - // mock 数据 - SmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到 - o.setChannelId(1L); - o.setTemplateId(10L); - o.setMobile("15601691300"); - o.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); - o.setSendTime(buildTime(2020, 11, 11)); - o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); - o.setReceiveTime(buildTime(2021, 11, 11)); - }); - smsLogMapper.insert(dbSmsLog); - // 测试 channelId 不匹配 - smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setChannelId(2L))); - // 测试 templateId 不匹配 - smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setTemplateId(20L))); - // 测试 mobile 不匹配 - smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setMobile("18818260999"))); - // 测试 sendStatus 不匹配 - smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus()))); - // 测试 sendTime 不匹配 - smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12)))); - // 测试 receiveStatus 不匹配 - smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveStatus(SmsReceiveStatusEnum.SUCCESS.getStatus()))); - // 测试 receiveTime 不匹配 - smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12)))); - // 准备参数 - SmsLogPageReqVO reqVO = new SmsLogPageReqVO(); - reqVO.setChannelId(1L); - reqVO.setTemplateId(10L); - reqVO.setMobile("156"); - reqVO.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); - reqVO.setSendTime(buildBetweenTime(2020, 11, 1, 2020, 11, 30)); - reqVO.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); - reqVO.setReceiveTime(buildBetweenTime(2021, 11, 1, 2021, 11, 30)); + // mock 数据 + SmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到 + o.setChannelId(1L); + o.setTemplateId(10L); + o.setMobile("15601691300"); + o.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); + o.setSendTime(buildTime(2020, 11, 11)); + o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + o.setReceiveTime(buildTime(2021, 11, 11)); + }); + smsLogMapper.insert(dbSmsLog); + // 测试 channelId 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setChannelId(2L))); + // 测试 templateId 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setTemplateId(20L))); + // 测试 mobile 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setMobile("18818260999"))); + // 测试 sendStatus 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus()))); + // 测试 sendTime 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12)))); + // 测试 receiveStatus 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveStatus(SmsReceiveStatusEnum.SUCCESS.getStatus()))); + // 测试 receiveTime 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12)))); + // 准备参数 + SmsLogPageReqVO reqVO = new SmsLogPageReqVO(); + reqVO.setChannelId(1L); + reqVO.setTemplateId(10L); + reqVO.setMobile("156"); + reqVO.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); + reqVO.setSendTime(buildBetweenTime(2020, 11, 1, 2020, 11, 30)); + reqVO.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + reqVO.setReceiveTime(buildBetweenTime(2021, 11, 1, 2021, 11, 30)); - // 调用 - PageResult pageResult = smsLogService.getSmsLogPage(reqVO); - // 断言 - assertEquals(1, pageResult.getTotal()); - assertEquals(1, pageResult.getList().size()); - assertPojoEquals(dbSmsLog, pageResult.getList().get(0)); + // 调用 + PageResult pageResult = smsLogService.getSmsLogPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSmsLog, pageResult.getList().get(0)); } @Test @@ -153,13 +153,14 @@ public class SmsLogServiceImplTest extends BaseDbUnitTest { smsLogMapper.insert(dbSmsLog); // 准备参数 Long id = dbSmsLog.getId(); + String apiSerialNo = dbSmsLog.getApiSerialNo(); Boolean success = randomBoolean(); LocalDateTime receiveTime = randomLocalDateTime(); String apiReceiveCode = randomString(); String apiReceiveMsg = randomString(); // 调用 - smsLogService.updateSmsReceiveResult(id, success, receiveTime, apiReceiveCode, apiReceiveMsg); + smsLogService.updateSmsReceiveResult(id, apiSerialNo, success, receiveTime, apiReceiveCode, apiReceiveMsg); // 断言 dbSmsLog = smsLogMapper.selectById(id); assertEquals(success ? SmsReceiveStatusEnum.SUCCESS.getStatus() diff --git a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java index 487c6f7fee..ffcc0587d4 100644 --- a/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java +++ b/yudao-module-system/src/test/java/cn/iocoder/yudao/module/system/service/sms/SmsSendServiceImplTest.java @@ -291,7 +291,7 @@ public class SmsSendServiceImplTest extends BaseMockitoUnitTest { // 调用 smsSendService.receiveSmsStatus(channelCode, text); // 断言 - receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()), + receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSerialNo()), eq(result.getSuccess()), eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode()))); }