This commit is contained in:
jiangh277
2025-07-22 23:00:39 +08:00
commit f8fb9b561c
59 changed files with 2456 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
package com.timeline.file;
import com.timeline.file.config.MinioConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan({"com.timeline", "com.timeline.file"})
@EnableConfigurationProperties(MinioConfig.class)
public class TimelineFileServiceApplication {
public static void main(String[] args) {
SpringApplication.run(TimelineFileServiceApplication.class, args);
}
}

View File

@@ -0,0 +1,25 @@
package com.timeline.file.config;
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioConfig {
private String endpoint;
private String accessKey;
private String secretKey;
private String bucketName;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}

View File

@@ -0,0 +1,103 @@
package com.timeline.file.controller;
import com.timeline.file.service.FileService;
import com.timeline.file.service.impl.FileServiceImpl;
import com.timeline.file.vo.ImageInfoVo;
import com.timeline.response.ResponseEntity;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/file")
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();
return "create default bucket success";
}
@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);
}
@PostMapping("/uploaded")
public ResponseEntity<String> uploaded(@RequestBody ImageInfoVo imageInfoVo) throws Throwable {
fileService.saveFileMetadata(imageInfoVo);
return ResponseEntity.success("上传成功");
}
@PostMapping("/upload-cover")
public ResponseEntity<String> uploadCover(@RequestPart("cover") MultipartFile cover) throws Throwable {
String objectKey = fileService.uploadCover(cover);
return ResponseEntity.success(objectKey);
}
@RequestMapping(value = "/download/cover/{coverInstanceId}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
public void downloadCover(@PathVariable String coverInstanceId, HttpServletResponse response) throws Throwable {
log.info("downloadCover");
InputStream inputStream = fileService.downloadCover(coverInstanceId);
response.setContentType("image/jpeg");
IOUtils.copy(inputStream, response.getOutputStream());
}
/**
* 上传图片后绑定到某个 StoryItem
*/
@PostMapping("/upload-story-item-image")
public ResponseEntity<String> uploadStoryItemImage(
@RequestParam String storyItemId,
@RequestBody ImageInfoVo imageInfoVo) throws Throwable {
// 1. 存储图片元数据
fileService.saveFileMetadata(imageInfoVo);
// 2. 关联图片和 StoryItem
fileService.associateImageWithStoryItem(imageInfoVo.getInstanceId(), storyItemId, "9999");
return ResponseEntity.success("上传并绑定成功");
}
/**
* 获取某个 StoryItem 的所有图片链接
*/
@GetMapping("/story-item/images/{storyItemId}")
public ResponseEntity<List<String>> getStoryItemImages(@PathVariable String storyItemId) throws Throwable {
List<String> imageIds = fileService.getStoryItemImages(storyItemId);
ArrayList<String> urls = fileService.getAllImageUrls(imageIds);
return ResponseEntity.success(urls);
}
/**
* 从 StoryItem 移除一个图片
*/
@DeleteMapping("/story-item/image/remove")
public ResponseEntity<String> removeImageFromStoryItem(
@RequestParam String imageInstanceId,
@RequestParam String storyItemId) throws Throwable {
fileService.removeImageFromStoryItem(imageInstanceId, storyItemId);
return ResponseEntity.success("图片已从故事项中移除");
}
}

View File

@@ -0,0 +1,17 @@
package com.timeline.file.dao;
import com.timeline.dto.CommonRelationDTO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface CommonRelationMapper {
void insertImageStoryItemRelation(String imageInstanceId, String storyItemId, String userId);
void insertRelation(CommonRelationDTO relationData);
List<String> getImagesByStoryItemId(String storyItemId);
List<String> getStoryItemsByImageInstanceId(String imageInstanceId);
void deleteImageStoryItemRelation(String imageInstanceId, String storyItemId);
}

View File

@@ -0,0 +1,11 @@
package com.timeline.file.dao;
import com.timeline.file.entity.ImageInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ImageInfoMapper {
void insert(ImageInfo imageInfo);
String selectObjectKeyById(String objectKey);
void delete(String objectKey);
}

View File

@@ -0,0 +1,18 @@
package com.timeline.file.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class ImageInfo {
private Long imageId;
private String instanceId;
private String imageName;
private String objectKey;
private LocalDateTime uploadTime;
private Long size;
private String contentType;
private String userId;
private Integer isDeleted;
}

View File

@@ -0,0 +1,26 @@
package com.timeline.file.service;
import com.timeline.file.entity.ImageInfo;
import com.timeline.file.vo.ImageInfoVo;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
@Service
public interface FileService {
void createBucketIfNotExist() throws Throwable;
String generateUploadUrl(String fileName) throws Throwable;
String generateDownloadUrl(String fileName) throws Throwable;
void 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 uploadCover(MultipartFile cover) throws Throwable;
InputStream downloadCover(String coverKey) throws Throwable;
}

View File

@@ -0,0 +1,185 @@
package com.timeline.file.service.impl;
import com.timeline.exception.CustomException;
import com.timeline.file.config.MinioConfig;
import com.timeline.file.dao.CommonRelationMapper;
import com.timeline.file.dao.ImageInfoMapper;
import com.timeline.file.entity.ImageInfo;
import com.timeline.file.service.FileService;
import com.timeline.file.vo.ImageInfoVo;
import com.timeline.utils.IdUtils;
import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Service
public class FileServiceImpl implements FileService {
private final MinioClient minioClient;
private final MinioConfig minioConfig;
@Autowired
private ImageInfoMapper imageInfoMapper;
@Autowired
private CommonRelationMapper commonRelationMapper;
@Autowired
public FileServiceImpl(MinioClient minioClient, MinioConfig minioConfig) {
this.minioClient = minioClient;
this.minioConfig = minioConfig;
}
@Override
public void createBucketIfNotExist() throws Throwable {
try {
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());
}
} catch (MinioException e) {
log.error("MinIO 操作失败:", e);
throw new CustomException(500, "创建bucket失败");
}
}
@Override
public String generateUploadUrl(String fileName) throws Throwable {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.PUT)
.bucket(minioConfig.getBucketName())
.object(fileName).build()
);
}
@Override
public String generateDownloadUrl(String fileName) throws Throwable {
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket(minioConfig.getBucketName())
.object(fileName).build()
);
}
@Override
public void saveFileMetadata(ImageInfoVo imageInfoVo) {
try {
ImageInfo imageInfo = new ImageInfo();
imageInfo.setInstanceId(IdUtils.randomUuidUpper());
imageInfo.setObjectKey(imageInfoVo.getObjectKey());
imageInfo.setImageName(imageInfoVo.getImageName());
imageInfo.setContentType(imageInfoVo.getContentType());
imageInfo.setSize(imageInfoVo.getSize());
imageInfo.setUploadTime(LocalDateTime.now());
imageInfo.setUserId("9999");
imageInfoMapper.insert(imageInfo);
} catch (Exception e) {
log.error("保存图片元数据失败", e);
throw new CustomException(500, "保存图片信息失败");
}
}
@Override
public List<ImageInfo> listAllImages() {
return List.of();
}
@Override
public void deleteImage(String objectKey) throws Throwable {
try {
// 删除 MinIO 中的对象
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectKey)
.build());
// 删除 MySQL 记录
imageInfoMapper.delete(objectKey);
} catch (Exception e) {
log.error("删除图片失败", e);
throw new CustomException(500, "删除图片失败");
}
}
@Override
public void associateImageWithStoryItem(String imageInstanceId, String storyItemId, String userId) {
try {
commonRelationMapper.insertImageStoryItemRelation(imageInstanceId, storyItemId, userId);
} catch (Exception e) {
log.error("关联图片和故事项失败", e);
throw new CustomException(500, "关联图片和故事项失败");
}
}
@Override
public List<String> getStoryItemImages(String storyItemId) {
return commonRelationMapper.getImagesByStoryItemId(storyItemId);
}
@Override
public void removeImageFromStoryItem(String imageInstanceId, String storyItemId) {
try {
commonRelationMapper.deleteImageStoryItemRelation(imageInstanceId, storyItemId);
} catch (Exception e) {
log.error("从故事项移除图片失败", e);
throw new CustomException(500, "从故事项移除图片失败");
}
}
@Override
public ArrayList<String> getAllImageUrls(List<String > imageIds) throws Throwable {
ArrayList<String> urls = new ArrayList<>();
for (String imageInstanceId : imageIds) {
String objectKey = imageInfoMapper.selectObjectKeyById(imageInstanceId);
String url = this.generateDownloadUrl(objectKey);
urls.add(url);
}
return urls;
}
@Override
public String uploadCover(MultipartFile cover) throws Throwable {
String fileName = "cover-" + IdUtils.randomUuidUpper() + ".jpg";
// 1. 上传到 MinIO
minioClient.putObject(PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(fileName)
.stream(cover.getInputStream(), cover.getSize(), -1)
.contentType(cover.getContentType())
.build());
// 2. 保存元数据到 MySQL
ImageInfo imageInfo = new ImageInfo();
imageInfo.setInstanceId(IdUtils.randomUuidUpper());
imageInfo.setObjectKey(fileName);
imageInfo.setImageName(fileName);
imageInfo.setContentType(cover.getContentType());
imageInfo.setSize(cover.getSize());
imageInfo.setUserId("9999");
imageInfo.setUploadTime(LocalDateTime.now());
imageInfoMapper.insert(imageInfo);
return imageInfo.getInstanceId();
}
@Override
public InputStream downloadCover(String coverInstanceId) throws Throwable {
log.info("获取");
String objectKey = imageInfoMapper.selectObjectKeyById(coverInstanceId);
return minioClient.getObject(GetObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectKey)
.build());
}
}

View File

@@ -0,0 +1,12 @@
package com.timeline.file.vo;
import lombok.Data;
@Data
public class ImageInfoVo {
private String objectKey;
private String imageName;
private String contentType;
private Long size;
private String instanceId;
}

View File

@@ -0,0 +1,9 @@
package com.timeline.file.vo;
import lombok.Data;
@Data
public class StoryItemImageRelationVo {
private String imageInstanceId;
private String storyItemId;
}