feat: 支持视频及缩略图元数据存储
All checks were successful
test/timeline-server/pipeline/head This commit looks good

在文件服务和故事服务中增加了对视频、持续时间及缩略图相关字段的支持。

- 在 `ImageInfo` 和 `StoryItem` 实体类中添加 `duration`、`thumbnailInstanceId` 等字段
- 更新 MyBatis 映射文件以支持新字段的持久化
- 在 `FileService` 中新增 `generateVideoUrl` 接口用于获取视频预签名地址
- 调整 `saveFileMetadata` 接口返回生成的 `instanceId`
- 优化了部分代码的格式和缩进
This commit is contained in:
2026-02-12 14:43:57 +08:00
parent 0349bf3c70
commit f0d140c646
9 changed files with 1656 additions and 1080 deletions

View File

File diff suppressed because it is too large Load Diff

View File

@@ -24,10 +24,12 @@ import java.util.Map;
public class FileController {
@Autowired
private FileService fileService;
@GetMapping("/hello")
public String hello() {
return "file service hello";
}
@GetMapping("/create-default-bucket")
public String createDefaultBucket() throws Throwable {
fileService.createBucketIfNotExist();
@@ -46,21 +48,31 @@ public class FileController {
fileService.createUserBucket(userId);
return ResponseEntity.success("bucket created for current user");
}
@GetMapping("/get-upload-url/{fileName}")
public ResponseEntity<String> getUploadUrl(@PathVariable String fileName) throws Throwable {
String uploadUrl = fileService.generateUploadUrl(fileName);
return ResponseEntity.success(uploadUrl);
}
@GetMapping("/get-download-url/{fileName}")
public ResponseEntity<String> getDownloadUrl(@PathVariable String fileName) throws Throwable {
String downloadUrl = fileService.generateDownloadUrl(fileName);
return ResponseEntity.success(downloadUrl);
}
@GetMapping("/get-video-url/{instanceId}")
public ResponseEntity<String> getVideoUrl(@PathVariable String instanceId) throws Throwable {
String videoUrl = fileService.generateVideoUrl(instanceId);
return ResponseEntity.success(videoUrl);
}
@PostMapping("/uploaded")
public ResponseEntity<String> uploaded(@RequestBody ImageInfoVo imageInfoVo) throws Throwable {
fileService.saveFileMetadata(imageInfoVo);
return ResponseEntity.success("上传成功");
String instanceId = fileService.saveFileMetadata(imageInfoVo);
return ResponseEntity.success(instanceId);
}
@PostMapping("/upload-image")
public ResponseEntity<String> uploadCover(@RequestPart("image") MultipartFile image) throws Throwable {
String objectKey = fileService.uploadImage(image);
@@ -73,12 +85,14 @@ public class FileController {
response.setContentType("image/jpeg");
IOUtils.copy(inputStream, response.getOutputStream());
}
@RequestMapping(value = "/image-low-res/{instanceId}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
public void fetchImageLowRes(@PathVariable String instanceId, HttpServletResponse response) throws Throwable {
InputStream inputStream = fileService.fetchImageLowRes(instanceId);
response.setContentType("image/jpeg");
IOUtils.copy(inputStream, response.getOutputStream());
}
/**
* 上传图片后绑定到某个 StoryItem
*/
@@ -119,12 +133,15 @@ public class FileController {
fileService.removeImageFromStoryItem(imageInstanceId, storyItemId);
return ResponseEntity.success("图片已从故事项中移除");
}
@GetMapping("/image/list")
public ResponseEntity<Map<String, Object>> getImagesListByOwnerId(@SpringQueryMap ImageInfoVo imageInfoVo) throws Throwable {
public ResponseEntity<Map<String, Object>> getImagesListByOwnerId(@SpringQueryMap ImageInfoVo imageInfoVo)
throws Throwable {
imageInfoVo.setOwnerId(UserContextUtils.getCurrentUserId());
Map<String, Object> images = fileService.getImagesListByOwnerId(imageInfoVo);
return ResponseEntity.success(images);
}
@DeleteMapping("/image/{imageInstanceId}")
public ResponseEntity<String> deleteImage(@PathVariable String imageInstanceId) throws Throwable {
fileService.deleteImage(imageInstanceId);

View File

@@ -16,4 +16,6 @@ public class ImageInfo {
private String userId;
private Integer isDeleted;
private LocalDateTime updateTime;
private String thumbnailInstanceId;
private Long duration;
}

View File

@@ -13,20 +13,34 @@ import java.util.Map;
@Service
public interface FileService {
void createBucketIfNotExist() throws Throwable;
void createUserBucket(String userId) throws Throwable;
String generateUploadUrl(String fileName) throws Throwable;
String generateDownloadUrl(String fileName) throws Throwable;
void saveFileMetadata(ImageInfoVo imageInfoVo);
String saveFileMetadata(ImageInfoVo imageInfoVo);
List<ImageInfo> listAllImages();
void deleteImage(String objectKey) throws Throwable;
void associateImageWithStoryItem(String imageInstanceId, String storyItemId, String userId);
List<String> getStoryItemImages(String storyItemId);
void removeImageFromStoryItem(String imageInstanceId, String storyItemId);
ArrayList<String> getAllImageUrls(List<String> images) throws Throwable;
String uploadImage(MultipartFile cover) throws Throwable;
InputStream fetchImage(String coverKey) throws Throwable;
InputStream fetchImageLowRes(String instanceId) throws Throwable;
String generateVideoUrl(String instanceId) throws Throwable;
Map<String, Object> getImagesListByOwnerId(ImageInfoVo imageInfoVo);
}

View File

@@ -42,6 +42,7 @@ public class FileServiceImpl implements FileService {
private CommonRelationMapper commonRelationMapper;
@Autowired
private FileHashMapper fileHashMapper;
public FileServiceImpl(MinioClient minioClient, MinioConfig minioConfig) {
this.minioClient = minioClient;
this.minioConfig = minioConfig;
@@ -62,7 +63,8 @@ public class FileServiceImpl implements FileService {
@Override
public void createBucketIfNotExist() throws Throwable {
try {
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(minioConfig.getBucketName()).build());
boolean found = minioClient
.bucketExists(BucketExistsArgs.builder().bucket(minioConfig.getBucketName()).build());
if (!found) {
log.info("bucket不存在创建bucket{}", minioConfig.getBucketName());
minioClient.makeBucket(MakeBucketArgs.builder().bucket(minioConfig.getBucketName()).build());
@@ -97,8 +99,7 @@ public class FileServiceImpl implements FileService {
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(bucket)
.object(fileName).build()
);
.object(fileName).build());
}
@Override
@@ -109,22 +110,42 @@ public class FileServiceImpl implements FileService {
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucket)
.object(fileName).build()
);
.object(fileName).build());
}
@Override
public void saveFileMetadata(ImageInfoVo imageInfoVo) {
public String generateVideoUrl(String instanceId) throws Throwable {
ImageInfo imageInfo = imageInfoMapper.selectByInstanceId(instanceId);
if (imageInfo == null) {
throw new CustomException(404, "视频文件不存在");
}
String bucket = userBucket(imageInfo.getUserId());
// 生成预签名 URL有效期例如 1 小时
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(bucket)
.object(imageInfo.getObjectKey())
.expiry(3600) // 1小时
.build());
}
@Override
public String saveFileMetadata(ImageInfoVo imageInfoVo) {
try {
ImageInfo imageInfo = new ImageInfo();
imageInfo.setInstanceId(IdUtils.randomUuidUpper());
String instanceId = IdUtils.randomUuidUpper();
imageInfo.setInstanceId(instanceId);
imageInfo.setObjectKey(imageInfoVo.getObjectKey());
imageInfo.setImageName(imageInfoVo.getImageName());
imageInfo.setContentType(imageInfoVo.getContentType());
imageInfo.setSize(imageInfoVo.getSize());
imageInfo.setUploadTime(LocalDateTime.now());
imageInfo.setUserId(currentUserId());
imageInfo.setThumbnailInstanceId(imageInfoVo.getThumbnailInstanceId());
imageInfo.setDuration(imageInfoVo.getDuration());
imageInfoMapper.insert(imageInfo);
return instanceId;
} catch (Exception e) {
log.error("保存图片元数据失败", e);
throw new CustomException(500, "保存图片信息失败");
@@ -174,6 +195,7 @@ public class FileServiceImpl implements FileService {
throw new CustomException(500, "删除图片失败");
}
}
@Override
public void associateImageWithStoryItem(String imageInstanceId, String storyItemId, String userId) {
try {
@@ -214,8 +236,7 @@ public class FileServiceImpl implements FileService {
.method(Method.GET)
.bucket(bucket)
.object(info.getObjectKey())
.build()
);
.build());
urls.add(url);
}
return urls;
@@ -223,7 +244,8 @@ public class FileServiceImpl implements FileService {
@Override
public String uploadImage(MultipartFile image) throws Throwable {
String suffix = Objects.requireNonNull(image.getOriginalFilename()).substring(image.getOriginalFilename().lastIndexOf("."));
String suffix = Objects.requireNonNull(image.getOriginalFilename())
.substring(image.getOriginalFilename().lastIndexOf("."));
String hash = CommonUtils.calculateFileHash(image);
String objectKey = hash + suffix;
String lowResolutionObjectKey = CommonConstants.LOW_RESOLUTION_PREFIX + hash + suffix;
@@ -300,6 +322,7 @@ public class FileServiceImpl implements FileService {
.object(objectKey)
.build());
}
@Override
public InputStream fetchImageLowRes(String instanceId) throws Throwable {
String objectKey = imageInfoMapper.selectObjectKeyById(instanceId);
@@ -325,18 +348,21 @@ public class FileServiceImpl implements FileService {
.build());
}
}
@Override
public Map<String, Object> getImagesListByOwnerId(ImageInfoVo imageInfoVo) {
HashMap<String, String> map = new HashMap<>();
map.put("ownerId", imageInfoVo.getOwnerId());
@SuppressWarnings("unchecked")
Map<String, Object> resultMap = (Map<String, Object>) PageUtils.pageQuery(imageInfoVo.getCurrent(), imageInfoVo.getPageSize(), ImageInfoMapper.class, "selectListByOwnerId",
Map<String, Object> resultMap = (Map<String, Object>) PageUtils.pageQuery(imageInfoVo.getCurrent(),
imageInfoVo.getPageSize(), ImageInfoMapper.class, "selectListByOwnerId",
map, "list");
return resultMap;
}
/**
* 检查对象是否存在
*
* @param objectKey 对象键
* @return true表示存在false表示不存在
*/

View File

@@ -11,4 +11,6 @@ public class ImageInfoVo extends CommonVo {
private Long size;
private String instanceId;
private String ownerId;
private String thumbnailInstanceId;
private Long duration;
}

View File

@@ -5,8 +5,8 @@
<mapper namespace="com.timeline.file.dao.ImageInfoMapper">
<insert id="insert">
INSERT INTO image_info (instance_id, image_name, object_key, upload_time, size, content_type, user_id)
VALUES (#{instanceId}, #{imageName}, #{objectKey}, #{uploadTime}, #{size}, #{contentType}, #{userId})
INSERT INTO image_info (instance_id, image_name, object_key, upload_time, size, content_type, user_id, thumbnail_instance_id, duration)
VALUES (#{instanceId}, #{imageName}, #{objectKey}, #{uploadTime}, #{size}, #{contentType}, #{userId}, #{thumbnailInstanceId}, #{duration})
</insert>
<select id="findByObjectKey" resultType="com.timeline.file.entity.ImageInfo">

View File

@@ -14,6 +14,9 @@ public class StoryItem {
private String description;
private String location;
private LocalDateTime storyItemTime;
private String videoUrl;
private Long duration;
private String thumbnailUrl;
private String createId;
private String updateId;
private LocalDateTime createTime;

View File

@@ -5,8 +5,8 @@
<mapper namespace="com.timeline.story.dao.StoryItemMapper">
<insert id="insert">
INSERT INTO story_item (instance_id, master_item_id, description, location, title, create_id, story_instance_id, is_delete, story_item_time, update_id)
VALUES (#{instanceId}, #{masterItemId}, #{description}, #{location}, #{title},#{createId}, #{storyInstanceId}, #{isDelete}, #{storyItemTime}, #{updateId})
INSERT INTO story_item (instance_id, master_item_id, description, location, title, create_id, story_instance_id, is_delete, story_item_time, update_id, video_url, duration, thumbnail_url)
VALUES (#{instanceId}, #{masterItemId}, #{description}, #{location}, #{title},#{createId}, #{storyInstanceId}, #{isDelete}, #{storyItemTime}, #{updateId}, #{videoUrl}, #{duration}, #{thumbnailUrl})
</insert>
<update id="update">
@@ -16,7 +16,10 @@
title = #{title},
create_id = #{createId},
update_time = NOW(),
update_id = #{updateId}
update_id = #{updateId},
video_url = #{videoUrl},
duration = #{duration},
thumbnail_url = #{thumbnailUrl}
WHERE instance_id = #{instanceId}
</update>
@@ -48,6 +51,9 @@
si.master_item_id,
si.is_delete,
si.story_item_time as story_item_time,
si.video_url,
si.duration,
si.thumbnail_url,
si.create_id AS create_id,
si.create_time AS create_time,
u1.username AS create_name,