refactor(media): 抽出 MediaStorage 接口为对象存储切换铺路
backend-ci / test (push) Failing after 5m7s

本地磁盘实现 MediaStorageService 改为实现接口,五处注入点
改为依赖接口;行为不变。后期新增 S3 实现以配置切换即可。
This commit is contained in:
jianghaifeng committed 2026-11-05 11:31:31 +08:00
1 parent 32ada4c2bf
commit 3df66c2f4c
8 files changed
+70 -14

No files matched your search

@@ -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;
/**
@@ -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());
@@ -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 实现后以配置切换,业务代码不感知。
*
* <p>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);
}
@@ -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 {
@@ -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<MediaRenditionRespVO> renditions(Long mediaAssetId) {
@@ -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;
/** 单集播放使用。 */
@@ -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;
@@ -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");