fix(media): 修正转码媒资 id 解析与存量分集补登记

This commit is contained in:
Zhou Weidong committed 2026-10-22 14:59:16 +08:00
1 parent a35b02dd18
commit 5ef5dd8eaf
11 files changed
+427 -31

No files matched your search

@@ -0,0 +1,25 @@
// Author: fruit.com.cn
package com.fruitshorts.media.service;
import java.util.Optional;
/**
* 媒资域读取分集视频来源的跨域端口,由 content 域实现。
*/
public interface EpisodeVideoSourcePort {
/** 按候选 id 查找分集;候选值可能是分集 id 或 video_asset_id。 */
Optional<EpisodeVideoSource> resolve(Long candidateId);
/** 回填分集的 video_asset_id。 */
void link(Long episodeId, Long mediaAssetId);
record EpisodeVideoSource(
Long episodeId,
Long dramaId,
Long videoAssetId,
String videoUrl,
Integer durationSec) {
}
}
@@ -21,6 +21,9 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.UUID;
@@ -34,6 +37,8 @@ public class MediaService {
private static final String STATUS_FAILED = "failed";
private static final String TASK_UPLOADING = "uploading";
private static final String TASK_COMPLETED = "completed";
private static final String BIZ_TYPE_EPISODE = "EPISODE";
private static final String STORAGE_TYPE_LOCAL = "LOCAL";
private static final Set<String> IMAGE_EXTENSIONS =
Set.of(".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".avif");
@@ -42,6 +47,7 @@ public class MediaService {
private final MediaAssetMapper mediaAssetMapper;
private final MediaUploadTaskMapper mediaUploadTaskMapper;
private final TranscodeService transcodeService;
private final EpisodeVideoSourcePort episodeVideoSourcePort;
public UploadInitRespVO init(UploadInitReqVO req, Long userId) {
if (req.getFileSize() > properties.getMaxFileSize()) {
@@ -203,13 +209,92 @@ public class MediaService {
/** 后台手动触发/重转:锁住媒资行,保证同一媒体的重复请求串行判定任务状态。 */
@Transactional(rollbackFor = Exception.class)
public TranscodeTriggerRespVO triggerTranscode(Long id) {
MediaAsset asset = id == null ? null : mediaAssetMapper.selectOne(
Wrappers.<MediaAsset>lambdaQuery()
.eq(MediaAsset::getId, id)
.last("for update"));
MediaAsset asset = lockAsset(id);
if (asset == null) {
asset = resolveLegacyAsset(id);
}
if (asset == null) {
throw new ServiceException(ErrorCode.DATA_NOT_FOUND, "媒资不存在");
}
return triggerAsset(asset);
}
private MediaAsset lockAsset(Long id) {
return id == null ? null : mediaAssetMapper.selectOne(
Wrappers.<MediaAsset>lambdaQuery()
.eq(MediaAsset::getId, id)
.last("for update"));
}
private MediaAsset resolveLegacyAsset(Long candidateId) {
return episodeVideoSourcePort.resolve(candidateId)
.map(this::resolveOrRegisterAsset)
.orElse(null);
}
private MediaAsset resolveOrRegisterAsset(EpisodeVideoSourcePort.EpisodeVideoSource source) {
String videoUrl = source.videoUrl();
if (videoUrl == null || videoUrl.isBlank()) {
return null;
}
String objectKey = storageService.objectKeyOf(videoUrl);
if (objectKey == null || objectKey.isBlank()) {
return null;
}
MediaAsset asset = mediaAssetMapper.selectOne(Wrappers.<MediaAsset>lambdaQuery()
.and(wrapper -> wrapper.eq(MediaAsset::getSourceUrl, videoUrl)
.or().eq(MediaAsset::getObjectKey, objectKey))
.last("limit 1"));
if (asset == null) {
asset = registerLegacyAsset(source, objectKey);
}
if (asset != null && source.episodeId() != null) {
episodeVideoSourcePort.link(source.episodeId(), asset.getId());
}
return asset;
}
private MediaAsset registerLegacyAsset(
EpisodeVideoSourcePort.EpisodeVideoSource source, String objectKey) {
Path file;
try {
file = storageService.fileFor(objectKey);
} catch (ServiceException ex) {
return null;
}
if (!Files.isRegularFile(file)) {
return null;
}
MediaAsset asset = new MediaAsset();
asset.setBizType(BIZ_TYPE_EPISODE);
asset.setFileName(file.getFileName().toString());
asset.setObjectKey(objectKey);
asset.setSourceUrl(source.videoUrl());
asset.setSizeBytes(sizeOf(file));
asset.setMd5("");
asset.setDurationSec(source.durationSec() == null ? 0 : source.durationSec());
asset.setWidth(0);
asset.setHeight(0);
asset.setStatus(STATUS_UPLOADED);
asset.setHlsUrl("");
asset.setMp4Url(source.videoUrl());
asset.setPosterUrl("");
asset.setTranscodeStatus(TranscodeService.STATUS_QUEUED);
asset.setStorageType(STORAGE_TYPE_LOCAL);
asset.setCreateBy(0L);
mediaAssetMapper.insert(asset);
return asset;
}
private long sizeOf(Path file) {
try {
return Files.size(file);
} catch (IOException ex) {
return 0L;
}
}
private TranscodeTriggerRespVO triggerAsset(MediaAsset asset) {
if (isImage(asset.getFileName())) {
throw new ServiceException(ErrorCode.BAD_REQUEST, "图片媒资无需转码");
}
@@ -106,6 +106,10 @@ public class MediaTranscodeOutputService {
transcodeService.findLatestTaskByMediaAssetId(mediaAssetId));
}
public boolean mediaAssetExists(Long mediaAssetId) {
return mediaAssetId != null && mediaAssetMapper.selectById(mediaAssetId) != null;
}
public Long resolveMediaAssetId(String videoUrl) {
if (!StringUtils.hasText(videoUrl)) {
return null;
@@ -115,7 +119,8 @@ public class MediaTranscodeOutputService {
return null;
}
MediaAsset asset = mediaAssetMapper.selectOne(Wrappers.<MediaAsset>lambdaQuery()
.eq(MediaAsset::getObjectKey, objectKey)
.and(wrapper -> wrapper.eq(MediaAsset::getSourceUrl, videoUrl)
.or().eq(MediaAsset::getObjectKey, objectKey))
.last("limit 1"));
return asset == null ? null : asset.getId();
}