From f606850814fc9ba15bcbe231e84ef9056da2d795 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 13 Oct 2026 20:44:09 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20=E4=BF=AE=E5=A4=8D=E5=87=AD=E8=AF=81?= =?UTF-8?q?=E5=88=86=E7=89=87=E4=B8=8A=E4=BC=A0=20Mixed=20Content=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=9C=8D=E5=8A=A1=E7=AB=AF=E4=BB=A3=E4=BC=A0?= =?UTF-8?q?=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../springblade/file/config/FileConfig.java | 20 +++++++++-- .../file/controller/FileTaskController.java | 22 +++++++++++++ .../file/service/IFileTaskService.java | 10 ++++++ .../service/impl/FileTaskServiceImpl.java | 27 +++++++++++++++ .../service/impl/MinioFileServiceImpl.java | 33 +++++++++++++++++++ doc/nacos/blade-prod.yaml | 5 +-- 6 files changed, 112 insertions(+), 5 deletions(-) diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java b/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java index 88d14991..3e578fef 100644 --- a/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java +++ b/blade-service/blade-file/src/main/java/org/springblade/file/config/FileConfig.java @@ -3,6 +3,7 @@ package org.springblade.file.config; import com.aliyun.oss.OSSClientBuilder; import com.obs.services.ObsClient; import io.minio.MinioClient; +import lombok.extern.slf4j.Slf4j; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.tool.utils.StringUtil; import org.springblade.file.service.IFileService; @@ -20,6 +21,7 @@ import org.springframework.core.env.Environment; * @author bfhuange * @date 2024/10/21 */ +@Slf4j @Configuration public class FileConfig { @@ -63,6 +65,15 @@ public class FileConfig { private IFileService buildMinioFileService(FileStorageProperties.StoreProperties properties) { validateStoreProperties(STORAGE_MINIO, properties); + if (StringUtil.isBlank(properties.getTransformEndpoint()) + && StringUtil.isNotBlank(properties.getEndpoint()) + && properties.getEndpoint().trim().toLowerCase().startsWith("http://")) { + log.warn("MinIO transform-endpoint 未配置,endpoint={}。HTTPS 页面分片直传会被浏览器 Mixed Content 拦截,将回退为服务端代传", + properties.getEndpoint()); + } else { + log.info("MinIO 文件存储已启用 endpoint={} transformEndpoint={}", + properties.getEndpoint(), properties.getTransformEndpoint()); + } MinioClient minioClient = MinioClient.builder() .endpoint(properties.getEndpoint()) .credentials(properties.getAccessKeyId(), properties.getAccessKeySecret()) @@ -95,12 +106,15 @@ public class FileConfig { FileStorageProperties.StoreProperties mergedProperties = new FileStorageProperties.StoreProperties(); String fileStoragePrefix = "file.storage." + prefix; mergedProperties.setEndpoint(firstNotBlank(properties.getEndpoint(), environment.getProperty(fileStoragePrefix + ".endpoint"), environment.getProperty(prefix + ".endpoint"))); + // 资源管理页 /resource/oss 的外网地址写在库表,不进 Spring Environment;凭证分片必须单独配 file.storage.minio.transform-endpoint mergedProperties.setTransformEndpoint(firstNotBlank( properties.getTransformEndpoint(), environment.getProperty(fileStoragePrefix + ".transform-endpoint"), + environment.getProperty(fileStoragePrefix + ".transformEndpoint"), environment.getProperty(prefix + ".transform-endpoint"), - // 兼容 blade-resource / oss 配置,避免仅改对象存储外网地址时分片上传仍走内网 HTTP - environment.getProperty("oss.transform-endpoint") + environment.getProperty(prefix + ".transformEndpoint"), + environment.getProperty("oss.transform-endpoint"), + environment.getProperty("oss.transformEndpoint") )); mergedProperties.setAccessKeyId(firstNotBlank(properties.getAccessKeyId(), environment.getProperty(fileStoragePrefix + ".access-key-id"), environment.getProperty(fileStoragePrefix + ".access-key"), environment.getProperty(prefix + ".access-key-id"), environment.getProperty(prefix + ".access-key"))); mergedProperties.setAccessKeySecret(firstNotBlank(properties.getAccessKeySecret(), environment.getProperty(fileStoragePrefix + ".access-key-secret"), environment.getProperty(fileStoragePrefix + ".secret-key"), environment.getProperty(prefix + ".access-key-secret"), environment.getProperty(prefix + ".secret-key"))); @@ -113,7 +127,7 @@ public class FileConfig { private String firstNotBlank(String... values) { for (String value : values) { if (StringUtil.isNotBlank(value)) { - return value; + return value.trim(); } } return null; diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java b/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java index 62e4dabf..462de6e0 100644 --- a/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java +++ b/blade-service/blade-file/src/main/java/org/springblade/file/controller/FileTaskController.java @@ -111,4 +111,26 @@ public class FileTaskController extends BladeController { String uploadUrl = fileTaskService.getUploadUrl(id, partNumber); return R.data(uploadUrl); } + + @ApiLog("上传任务-服务端代传分片") + @PostMapping(value = "/uploadPart", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE) + @Parameters({ + @Parameter(name = "id", description = "文件任务id", in = ParameterIn.QUERY, schema = @Schema(type = "string")), + @Parameter(name = "partNumber", description = "分段编号", in = ParameterIn.QUERY, schema = @Schema(type = "int")) + }) + @Operation(summary = "服务端代传分片", description = "HTTPS 页面无法直连 HTTP MinIO 时,由服务端转发分片到对象存储") + public R uploadPart(Long id, Integer partNumber, org.springframework.web.multipart.MultipartFile file) throws java.io.IOException { + if (file == null || file.isEmpty()) { + return R.fail("分片内容不能为空"); + } + java.io.File tempFile = java.io.File.createTempFile("file-part-", ".bin"); + try { + file.transferTo(tempFile); + return R.data(fileTaskService.uploadPart(id, partNumber, tempFile)); + } finally { + if (tempFile.exists() && !tempFile.delete()) { + tempFile.deleteOnExit(); + } + } + } } diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java index ae394521..f47120a4 100644 --- a/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java +++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/IFileTaskService.java @@ -74,6 +74,16 @@ public interface IFileTaskService extends IService { */ String getUploadUrl(Long id, Integer partNumber); + /** + * 服务端代传分片(HTTPS 页面无法直连 HTTP MinIO 时使用) + * + * @param id 文件任务id + * @param partNumber 分段编号 + * @param partFile 分片临时文件 + * @return ETag + */ + String uploadPart(Long id, Integer partNumber, java.io.File partFile); + /** * 取消文件任务 * @param taskIds diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java index b92fc66e..25a46bab 100644 --- a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java +++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/FileTaskServiceImpl.java @@ -259,6 +259,33 @@ public class FileTaskServiceImpl extends ServiceImpl i return fileService.getUploadPartUrl(fileTask.getObjectKey(), fileTask.getUploadId(), partNumber, null); } + @Override + public String uploadPart(Long id, Integer partNumber, java.io.File partFile) { + FileTask fileTask = this.getById(id); + Func.requireNotNull(fileTask, "文件任务不存在"); + if (FileTaskStatus.COMPLETED.getCode().equals(fileTask.getStatus())) { + throw new ServiceException("文件任务已完成"); + } + if (FileTaskStatus.PAUSED.getCode().equals(fileTask.getStatus())) { + throw new ServiceException("文件任务已暂停"); + } + if (partFile == null || !partFile.exists()) { + throw new ServiceException("分片文件不存在"); + } + org.springblade.file.pojo.vo.PartEtagVO partEtag = fileService.uploadPart( + fileTask.getObjectKey(), + fileTask.getUploadId(), + partFile, + partNumber, + 0L, + partFile.length() + ); + if (partEtag == null || StringUtil.isBlank(partEtag.getEtag())) { + throw new ServiceException("分片上传未返回 ETag"); + } + return partEtag.getEtag().replace("\"", ""); + } + @Transactional(rollbackFor = Exception.class) @Override public void cancelBatchByIds(List taskIds) { diff --git a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java index 4c8f53a5..bc85679a 100644 --- a/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java +++ b/blade-service/blade-file/src/main/java/org/springblade/file/service/impl/MinioFileServiceImpl.java @@ -30,6 +30,7 @@ import io.minio.ComposeSource; import io.minio.GetObjectArgs; import io.minio.GetPresignedObjectUrlArgs; import io.minio.MinioClient; +import io.minio.ObjectWriteResponse; import io.minio.PutObjectArgs; import io.minio.RemoveObjectArgs; import io.minio.UploadObjectArgs; @@ -39,8 +40,10 @@ import lombok.extern.slf4j.Slf4j; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.tool.utils.StringUtil; import org.springblade.file.config.FileStorageProperties; +import org.springblade.file.pojo.vo.PartEtagVO; import java.io.File; +import java.io.FileInputStream; import java.io.InputStream; import java.net.URI; import java.nio.file.Files; @@ -88,6 +91,36 @@ public class MinioFileServiceImpl extends AbstractFileService { } } + @Override + public PartEtagVO uploadPart(String objectKey, String uploadId, File file, int partNumber, Long offset, Long partSize) { + long start = offset == null ? 0L : offset; + long size = partSize == null ? Math.max(0L, file.length() - start) : partSize; + try (FileInputStream inputStream = new FileInputStream(file)) { + long skipped = inputStream.skip(start); + if (skipped < start) { + throw new ServiceException("MinIO分段上传读取文件偏移失败"); + } + ObjectWriteResponse response = minioClient.putObject(PutObjectArgs.builder() + .bucket(properties.getBucketName()) + .object(partObjectKey(objectKey, uploadId, partNumber)) + .stream(inputStream, size, PART_SIZE) + .build()); + String etag = response.etag() == null ? "" : response.etag().replace("\"", ""); + return new PartEtagVO(etag, partNumber); + } catch (ServiceException serviceException) { + throw serviceException; + } catch (Exception exception) { + log.error("MinIO分段上传异常 objectKey:{} partNumber:{}", objectKey, partNumber, exception); + throw new ServiceException("MinIO分段上传异常"); + } + } + + @Override + public void cancelMultipartUpload(String objectKey, String uploadId) { + // MinIO 分片以独立对象模拟,取消时由任务层清理业务状态即可 + log.debug("MinIO取消分段上传 objectKey:{} uploadId:{}", objectKey, uploadId); + } + @Override public void completeMultipartUpload(String objectKey, String uploadId, List partEtags) { try { diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml index 5aabf2ac..889576ad 100644 --- a/doc/nacos/blade-prod.yaml +++ b/doc/nacos/blade-prod.yaml @@ -120,11 +120,12 @@ baidu: request-timeout: 30s token-refresh-advance: 1m -# 凭证分片上传等走 blade-file,外网地址需单独配置(与 /resource/oss 的 transformEndpoint 保持一致) +# 凭证分片上传走 blade-file,与 /resource/oss 库表配置相互独立;外网地址必须在此配置并发布到 Nacos 后重启 blade-file +# 不要使用可被空环境变量覆盖的 ${VAR:default},否则仍会签发 http://内网IP 预签名导致 Mixed Content file: storage: minio: - transform-endpoint: ${MINIO_TRANSFORM_ENDPOINT:https://tms-prod.xinghy56.com/oss} + transform-endpoint: https://tms-prod.xinghy56.com/oss powerjob: worker: