feat: 支持外部端点映射与视频元数据扩展
Some checks failed
test/timeline-server/pipeline/head There was a failure building this commit

1. 在 MinioConfig 中增加 externalEndpoint 配置,支持将生成的预签名 URL
   内部地址替换为外部访问地址。
2. 更新数据库脚本及查询逻辑,增加视频时长、缩略图 ID 等字段支持,并在
   查询列表时过滤掉作为缩略图存在的冗余记录。
3. 优化图片上传流程,增加压缩失败时的降级处理机制,防止非图片文件导致
   上传中断。
This commit is contained in:
2026-02-12 16:54:20 +08:00
parent f0d140c646
commit 1b1e1f4f87
4 changed files with 72 additions and 19 deletions

View File

@@ -0,0 +1,8 @@
ALTER TABLE `image_info`
ADD COLUMN `thumbnail_instance_id` varchar(32) DEFAULT NULL COMMENT '视频缩略图ID',
ADD COLUMN `duration` bigint DEFAULT NULL COMMENT '视频时长(秒)';ALTER TABLE `story_item`
ADD COLUMN `video_url` VARCHAR(64) COMMENT '视频文件 Instance ID',
ADD COLUMN `duration` BIGINT COMMENT '视频时长(秒)',
ADD COLUMN `thumbnail_url` VARCHAR(64) COMMENT '视频封面 Instance ID';ALTALTER TABLE story_item ADD COLUMN share_id VARCHAR(64) DEFAULT NULL COMMENT '分享ID';ER TABLE `image_info`
ADD COLUMN `thumbnail_instance_id` varchar(32) DEFAULT NULL COMMENT '视频缩略图ID',
ADD COLUMN `duration` bigint DEFAULT NULL COMMENT '视频时长(秒)';

View File

@@ -11,6 +11,7 @@ import org.springframework.context.annotation.Configuration;
@ConfigurationProperties(prefix = "minio") @ConfigurationProperties(prefix = "minio")
public class MinioConfig { public class MinioConfig {
private String endpoint; private String endpoint;
private String externalEndpoint;
private String accessKey; private String accessKey;
private String secretKey; private String secretKey;
private String bucketName; private String bucketName;

View File

@@ -90,27 +90,44 @@ public class FileServiceImpl implements FileService {
} }
} }
private String replaceWithExternalEndpoint(String url) {
String externalEndpoint = minioConfig.getExternalEndpoint();
if (externalEndpoint != null && !externalEndpoint.isEmpty()) {
String internalEndpoint = minioConfig.getEndpoint();
// 简单替换:将内部 Endpoint 替换为外部 Endpoint
// 注意MinIO 生成的 URL 肯定以配置的 Endpoint 开头
if (url.startsWith(internalEndpoint)) {
return url.replaceFirst(java.util.regex.Pattern.quote(internalEndpoint), externalEndpoint);
}
}
return url;
}
@Override @Override
public String generateUploadUrl(String fileName) throws Throwable { public String generateUploadUrl(String fileName) throws Throwable {
String userId = currentUserId(); String userId = currentUserId();
String bucket = userBucket(userId); String bucket = userBucket(userId);
createUserBucket(userId); createUserBucket(userId);
return minioClient.getPresignedObjectUrl( String url = minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder() GetPresignedObjectUrlArgs.builder()
.method(Method.PUT) .method(Method.PUT)
.bucket(bucket) .bucket(bucket)
.object(fileName).build()); .object(fileName)
.expiry(30 * 60) // 30 minutes
.build());
return replaceWithExternalEndpoint(url);
} }
@Override @Override
public String generateDownloadUrl(String fileName) throws Throwable { public String generateDownloadUrl(String fileName) throws Throwable {
String userId = currentUserId(); String userId = currentUserId();
String bucket = userBucket(userId); String bucket = userBucket(userId);
return minioClient.getPresignedObjectUrl( String url = minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder() GetPresignedObjectUrlArgs.builder()
.method(Method.GET) .method(Method.GET)
.bucket(bucket) .bucket(bucket)
.object(fileName).build()); .object(fileName).build());
return replaceWithExternalEndpoint(url);
} }
@Override @Override
@@ -121,13 +138,14 @@ public class FileServiceImpl implements FileService {
} }
String bucket = userBucket(imageInfo.getUserId()); String bucket = userBucket(imageInfo.getUserId());
// 生成预签名 URL有效期例如 1 小时 // 生成预签名 URL有效期例如 1 小时
return minioClient.getPresignedObjectUrl( String url = minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder() GetPresignedObjectUrlArgs.builder()
.method(Method.GET) .method(Method.GET)
.bucket(bucket) .bucket(bucket)
.object(imageInfo.getObjectKey()) .object(imageInfo.getObjectKey())
.expiry(3600) // 1小时 .expiry(3600) // 1小时
.build()); .build());
return replaceWithExternalEndpoint(url);
} }
@Override @Override
@@ -237,7 +255,7 @@ public class FileServiceImpl implements FileService {
.bucket(bucket) .bucket(bucket)
.object(info.getObjectKey()) .object(info.getObjectKey())
.build()); .build());
urls.add(url); urls.add(replaceWithExternalEndpoint(url));
} }
return urls; return urls;
} }
@@ -267,20 +285,39 @@ public class FileServiceImpl implements FileService {
log.info("当前文件已存在不进行minio文件上传"); log.info("当前文件已存在不进行minio文件上传");
} else { } else {
// 1. 上传到 MinIO // 1. 上传到 MinIO
// 对原图进行压缩 // 尝试对原图进行压缩
ByteArrayOutputStream compressedOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream compressedOutputStream = new ByteArrayOutputStream();
boolean compressionSuccess = false;
try {
Thumbnails.of(image.getInputStream()) Thumbnails.of(image.getInputStream())
.scale(1.0) // 保持原图尺寸 .scale(1.0) // 保持原图尺寸
.outputQuality(0.8) // 设置压缩质量 .outputQuality(0.8) // 设置压缩质量
.toOutputStream(compressedOutputStream); .toOutputStream(compressedOutputStream);
ByteArrayInputStream compressedInputStream = new ByteArrayInputStream(compressedOutputStream.toByteArray()); compressionSuccess = true;
} catch (Exception e) {
log.warn("图片压缩失败(可能是格式不支持或非图片文件),降级为直接上传原图: {}", image.getOriginalFilename(), e);
}
if (compressionSuccess) {
ByteArrayInputStream compressedInputStream = new ByteArrayInputStream(
compressedOutputStream.toByteArray());
minioClient.putObject(PutObjectArgs.builder() minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket) .bucket(bucket)
.object(objectKey) .object(objectKey)
.stream(compressedInputStream, compressedInputStream.available(), -1) .stream(compressedInputStream, compressedInputStream.available(), -1)
.contentType(image.getContentType()) .contentType(image.getContentType())
.build()); .build());
} else {
// 压缩失败,直接上传原图
try (InputStream inputStream = image.getInputStream()) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucket)
.object(objectKey)
.stream(inputStream, image.getSize(), -1)
.contentType(image.getContentType())
.build());
}
}
// 生成并上传低分辨率版本 // 生成并上传低分辨率版本
try (InputStream inputStream = image.getInputStream()) { try (InputStream inputStream = image.getInputStream()) {
ByteArrayOutputStream lowResOutputStream = new ByteArrayOutputStream(); ByteArrayOutputStream lowResOutputStream = new ByteArrayOutputStream();
@@ -296,10 +333,10 @@ public class FileServiceImpl implements FileService {
.stream(lowResInputStream, lowResInputStream.available(), -1) .stream(lowResInputStream, lowResInputStream.available(), -1)
.contentType(image.getContentType()) .contentType(image.getContentType())
.build()); .build());
log.info("低分辨率版本已生成并上传: {}", lowResolutionObjectKey); log.info("低分辨率版本已生成并上传: {}", lowResolutionObjectKey);
} catch (Exception e) { } catch (Exception e) {
log.error("生成低分辨率版本失败", e); log.error("生成低分辨率版本失败", e);
// 低分辨率生成失败不影响主流程
} }
} }
fileHashMapper.insertFileHash(new FileHash(imageInfo.getInstanceId(), hash)); fileHashMapper.insertFileHash(new FileHash(imageInfo.getInstanceId(), hash));

View File

@@ -22,7 +22,14 @@
</select> </select>
<select id="selectListByOwnerId" resultType="com.timeline.file.entity.ImageInfo" parameterType="java.util.Map"> <select id="selectListByOwnerId" resultType="com.timeline.file.entity.ImageInfo" parameterType="java.util.Map">
SELECT * FROM image_info WHERE user_id = #{ownerId} AND is_deleted = 0 SELECT * FROM image_info t1
WHERE user_id = #{ownerId}
AND is_deleted = 0
AND NOT EXISTS (
SELECT 1 FROM image_info t2
WHERE t2.thumbnail_instance_id = t1.instance_id
AND t2.is_deleted = 0
)
</select> </select>
<select id="selectByInstanceId" resultType="com.timeline.file.entity.ImageInfo"> <select id="selectByInstanceId" resultType="com.timeline.file.entity.ImageInfo">