From 3df66c2f4c698f18de04691684ace4de6b8c477f Mon Sep 17 00:00:00 2001 From: jianghaifeng Date: Thu, 5 Nov 2026 11:31:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor(media):=20=E6=8A=BD=E5=87=BA=20MediaSt?= =?UTF-8?q?orage=20=E6=8E=A5=E5=8F=A3=E4=B8=BA=E5=AF=B9=E8=B1=A1=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E5=88=87=E6=8D=A2=E9=93=BA=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本地磁盘实现 MediaStorageService 改为实现接口,五处注入点 改为依赖接口;行为不变。后期新增 S3 实现以配置切换即可。 --- .../EpisodeTranscodeBindingService.java | 2 +- .../media/service/MediaService.java | 4 +- .../media/service/MediaStorage.java | 58 +++++++++++++++++++ .../media/service/MediaStorageService.java | 7 +-- .../service/MediaTranscodeOutputService.java | 2 +- .../media/service/ThumbnailUrlResolver.java | 2 +- .../service/transcode/TranscodeWorker.java | 4 +- .../MediaServiceTranscodePipelineTest.java | 5 +- 8 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaStorage.java diff --git a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/EpisodeTranscodeBindingService.java b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/EpisodeTranscodeBindingService.java index 89c740e..831a7f8 100644 --- a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/EpisodeTranscodeBindingService.java +++ b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/EpisodeTranscodeBindingService.java @@ -19,7 +19,7 @@ import org.springframework.util.StringUtils; public class EpisodeTranscodeBindingService { private final MediaAssetMapper mediaAssetMapper; - private final MediaStorageService storageService; + private final MediaStorage storageService; private final TranscodeService transcodeService; /** diff --git a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaService.java b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaService.java index b1e7dd7..bbb7f1b 100644 --- a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaService.java +++ b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaService.java @@ -38,7 +38,7 @@ public class MediaService { Set.of(".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".avif"); private final MediaProperties properties; - private final MediaStorageService storageService; + private final MediaStorage storageService; private final MediaAssetMapper mediaAssetMapper; private final MediaUploadTaskMapper mediaUploadTaskMapper; private final TranscodeService transcodeService; @@ -104,7 +104,7 @@ public class MediaService { "分片未传完(" + chunks + "/" + task.getTotalChunks() + ")"); } - MediaStorageService.MergeResult merged = + MediaStorage.MergeResult merged = storageService.merge(req.getUploadId(), task.getObjectKey(), task.getTotalChunks()); String publicUrl = storageService.toPublicUrl(task.getObjectKey()); diff --git a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaStorage.java b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaStorage.java new file mode 100644 index 0000000..20d6961 --- /dev/null +++ b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaStorage.java @@ -0,0 +1,58 @@ +// Author: fruit.com.cn + +package com.fruitshorts.media.service; + +import org.springframework.web.multipart.MultipartFile; + +import java.nio.file.Path; + +/** + * 媒资存储抽象:当前实现为本地磁盘({@link MediaStorageService}), + * 后期新增 S3 实现后以配置切换,业务代码不感知。 + * + *

S3 实现要点:分片走 multipart upload 或本地暂存后 putObject; + * {@link #fileFor(String)} 改成「下载到临时目录 / 上传回对象存储」的适配(ffmpeg 需要本地文件); + * {@link #toPublicUrl(String)} 返回 CDN / 对象存储域名地址。 + */ +public interface MediaStorage { + + /** 合并结果:落地路径、大小与 MD5。 */ + record MergeResult(Path path, long size, String md5) { + } + + /** 存储根目录(本地实现为磁盘目录;S3 实现为本地暂存目录)。 */ + Path rootDir(); + + /** + * 把 objectKey 解析成本地可读写的路径。转码产物落地用它。 + * 实现方必须校验 objectKey 不允许跑出根目录。 + */ + Path fileFor(String objectKey); + + /** 把对外 URL 反解成 objectKey(去掉前缀与签名查询串)。 */ + String objectKeyOf(String publicUrl); + + /** 分片临时目录。 */ + Path chunkDir(String uploadId); + + /** 保存一个分片。 */ + void saveChunk(String uploadId, int index, MultipartFile file); + + /** 已收到的分片数。 */ + int countChunks(String uploadId); + + /** 按分片序号顺序合并成正式文件,返回大小与 MD5。 */ + MergeResult merge(String uploadId, String objectKey, int totalChunks); + + /** 清理分片临时目录。 */ + void cleanChunkDir(String uploadId); + + /** 删除一个媒资文件。 */ + void delete(String objectKey); + + /** 生成对象键:bizType/yyyyMM/uuid.ext。 */ + String buildObjectKey(String bizType, String originalName); + + /** objectKey 转对外访问 URL。 */ + String toPublicUrl(String objectKey); +} diff --git a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaStorageService.java b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaStorageService.java index 55ccb94..1ca2a66 100644 --- a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaStorageService.java +++ b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaStorageService.java @@ -28,20 +28,17 @@ import java.util.UUID; /** * 本地磁盘媒资存储:分片写临时目录,完成后合并成正式文件。 - * 之后要换成对象存储时,只需替换本类实现。 + * 是 {@link MediaStorage} 的本地实现;切对象存储时新增实现类即可,业务代码只依赖接口。 */ @Slf4j @Service @RequiredArgsConstructor -public class MediaStorageService { +public class MediaStorageService implements MediaStorage { private static final DateTimeFormatter MONTH_DIR = DateTimeFormatter.ofPattern("yyyyMM"); private final MediaProperties properties; - public record MergeResult(Path path, long size, String md5) { - } - public Path rootDir() { Path root = Paths.get(properties.getRootDir()).toAbsolutePath().normalize(); try { diff --git a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaTranscodeOutputService.java b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaTranscodeOutputService.java index 9385963..774afe2 100644 --- a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaTranscodeOutputService.java +++ b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/MediaTranscodeOutputService.java @@ -50,7 +50,7 @@ public class MediaTranscodeOutputService { private final MediaAssetMapper mediaAssetMapper; private final TranscodeService transcodeService; - private final MediaStorageService storageService; + private final MediaStorage storageService; private final PlaybackSigner signer; public List renditions(Long mediaAssetId) { diff --git a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/ThumbnailUrlResolver.java b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/ThumbnailUrlResolver.java index b32b0ac..43ae4bb 100644 --- a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/ThumbnailUrlResolver.java +++ b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/ThumbnailUrlResolver.java @@ -19,7 +19,7 @@ import java.util.Map; public class ThumbnailUrlResolver { private final TranscodeService transcodeService; - private final MediaStorageService storageService; + private final MediaStorage storageService; private final PlaybackSigner signer; /** 单集播放使用。 */ diff --git a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/transcode/TranscodeWorker.java b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/transcode/TranscodeWorker.java index 9ea3740..b18d600 100644 --- a/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/transcode/TranscodeWorker.java +++ b/fruitshorts-module-media/src/main/java/com/fruitshorts/media/service/transcode/TranscodeWorker.java @@ -6,7 +6,7 @@ import com.fruitshorts.media.config.MediaTranscodeProfile; import com.fruitshorts.media.dal.entity.MediaThumbnail; import com.fruitshorts.media.dal.entity.TranscodeTask; import com.fruitshorts.media.service.MediaService; -import com.fruitshorts.media.service.MediaStorageService; +import com.fruitshorts.media.service.MediaStorage; import com.fruitshorts.media.service.MediaTranscodeProfileService; import com.fruitshorts.media.service.EpisodeTranscodeCompletionPort; import com.fruitshorts.media.service.TranscodeErrorTruncator; @@ -45,7 +45,7 @@ public class TranscodeWorker { private final TranscodeService transcodeService; private final MediaService mediaService; - private final MediaStorageService storageService; + private final MediaStorage storageService; private final TranscodeRunner transcodeRunner; private final MediaTranscodeProfileService profileService; private final EpisodeTranscodeCompletionPort episodeCompletionPort; diff --git a/fruitshorts-transcode-worker/src/test/java/com/fruitshorts/worker/MediaServiceTranscodePipelineTest.java b/fruitshorts-transcode-worker/src/test/java/com/fruitshorts/worker/MediaServiceTranscodePipelineTest.java index 8b01bec..8c83c74 100644 --- a/fruitshorts-transcode-worker/src/test/java/com/fruitshorts/worker/MediaServiceTranscodePipelineTest.java +++ b/fruitshorts-transcode-worker/src/test/java/com/fruitshorts/worker/MediaServiceTranscodePipelineTest.java @@ -8,6 +8,7 @@ import com.fruitshorts.media.dal.entity.MediaUploadTask; import com.fruitshorts.media.dal.mapper.MediaAssetMapper; import com.fruitshorts.media.dal.mapper.MediaUploadTaskMapper; import com.fruitshorts.media.service.MediaService; +import com.fruitshorts.media.service.MediaStorage; import com.fruitshorts.media.service.MediaStorageService; import com.fruitshorts.media.service.EpisodeVideoSourcePort; import com.fruitshorts.media.service.TranscodeService; @@ -63,7 +64,7 @@ class MediaServiceTranscodePipelineTest { givenCompletedUpload("video.mp4"); given(storageService.countChunks("upload-1")).willReturn(1); given(storageService.merge("upload-1", "episode/202610/video.mp4", 1)) - .willReturn(new MediaStorageService.MergeResult( + .willReturn(new MediaStorage.MergeResult( Path.of("/tmp/video.mp4"), 1024L, "md5")); given(storageService.toPublicUrl("episode/202610/video.mp4")) .willReturn(SOURCE_URL); @@ -79,7 +80,7 @@ class MediaServiceTranscodePipelineTest { givenCompletedUpload("cover.jpg"); given(storageService.countChunks("upload-1")).willReturn(1); given(storageService.merge("upload-1", "episode/202610/cover.jpg", 1)) - .willReturn(new MediaStorageService.MergeResult( + .willReturn(new MediaStorage.MergeResult( Path.of("/tmp/cover.jpg"), 512L, "md5")); given(storageService.toPublicUrl("episode/202610/cover.jpg")) .willReturn("/files/episode/202610/cover.jpg");