This commit is contained in:
jiangh277
2025-08-04 16:51:13 +08:00
parent f8fb9b561c
commit eba0eb085e
41 changed files with 451 additions and 73 deletions

View File

@@ -77,6 +77,10 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
</dependency>
</dependencies>
<build>

View File

@@ -1,21 +1,22 @@
package com.timeline.file.controller;
import com.timeline.file.entity.ImageInfo;
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 com.timeline.common.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.cloud.openfeign.SpringQueryMap;
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;
import java.util.Map;
@Slf4j
@RestController
@@ -47,13 +48,13 @@ public class FileController {
fileService.saveFileMetadata(imageInfoVo);
return ResponseEntity.success("上传成功");
}
@PostMapping("/upload-cover")
public ResponseEntity<String> uploadCover(@RequestPart("cover") MultipartFile cover) throws Throwable {
String objectKey = fileService.uploadCover(cover);
@PostMapping("/upload-image")
public ResponseEntity<String> uploadCover(@RequestPart("image") MultipartFile image) throws Throwable {
String objectKey = fileService.uploadCover(image);
return ResponseEntity.success(objectKey);
}
@RequestMapping(value = "/download/cover/{coverInstanceId}", method = RequestMethod.GET, produces = MediaType.IMAGE_JPEG_VALUE)
@RequestMapping(value = "/image/{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);
@@ -100,4 +101,15 @@ public class FileController {
fileService.removeImageFromStoryItem(imageInstanceId, storyItemId);
return ResponseEntity.success("图片已从故事项中移除");
}
@GetMapping("/image/list")
public ResponseEntity<Map> getImagesListByOwnerId(@SpringQueryMap ImageInfoVo imageInfoVo) throws Throwable {
imageInfoVo.setOwnerId("9999");
Map images = fileService.getImagesListByOwnerId(imageInfoVo);
return ResponseEntity.success(images);
}
@DeleteMapping("/image/{imageInstanceId}")
public ResponseEntity<String> deleteImage(@PathVariable String imageInstanceId) throws Throwable {
fileService.deleteImage(imageInstanceId);
return ResponseEntity.success("图片已删除");
}
}

View File

@@ -1,6 +1,6 @@
package com.timeline.file.dao;
import com.timeline.dto.CommonRelationDTO;
import com.timeline.common.dto.CommonRelationDTO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

View File

@@ -0,0 +1,15 @@
package com.timeline.file.dao;
import com.timeline.file.entity.FileHash;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface FileHashMapper {
void insertFileHash(FileHash fileHash);
List<FileHash> getFileHashByFileHash(String fileHash);
List<FileHash> getFileHashByInstanceId(String instanceId);
List<FileHash> getOtherFileHashByInstanceId(String instanceId);
void deleteFileHash(String instanceId);
}

View File

@@ -1,11 +1,18 @@
package com.timeline.file.dao;
import com.timeline.file.entity.ImageInfo;
import com.timeline.file.vo.ImageInfoVo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
import java.util.Map;
@Mapper
public interface ImageInfoMapper {
void insert(ImageInfo imageInfo);
void update(ImageInfo imageInfo);
String selectObjectKeyById(String objectKey);
void delete(String objectKey);
List<ImageInfo> selectListByOwnerId(Map map);
ImageInfo selectByInstanceId(String instanceId);
}

View File

@@ -0,0 +1,18 @@
package com.timeline.file.entity;
import lombok.Data;
@Data
public class FileHash {
private Integer id;
private String instanceId;
private String hashValue;
private Integer isDeleted;
public FileHash(String instanceId, String hashValue) {
this.id = null;
this.instanceId = instanceId;
this.hashValue = hashValue;
this.isDeleted = 0;
}
}

View File

@@ -15,4 +15,5 @@ public class ImageInfo {
private String contentType;
private String userId;
private Integer isDeleted;
private LocalDateTime updateTime;
}

View File

@@ -8,6 +8,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Service
public interface FileService {
@@ -23,4 +24,5 @@ public interface FileService {
ArrayList<String> getAllImageUrls(List<String> images) throws Throwable;
String uploadCover(MultipartFile cover) throws Throwable;
InputStream downloadCover(String coverKey) throws Throwable;
Map getImagesListByOwnerId(ImageInfoVo imageInfoVo);
}

View File

@@ -1,13 +1,18 @@
package com.timeline.file.service.impl;
import com.timeline.exception.CustomException;
import com.timeline.common.constants.CommonConstants;
import com.timeline.common.exception.CustomException;
import com.timeline.common.utils.CommonUtils;
import com.timeline.common.utils.PageUtils;
import com.timeline.file.config.MinioConfig;
import com.timeline.file.dao.CommonRelationMapper;
import com.timeline.file.dao.FileHashMapper;
import com.timeline.file.dao.ImageInfoMapper;
import com.timeline.file.entity.FileHash;
import com.timeline.file.entity.ImageInfo;
import com.timeline.file.service.FileService;
import com.timeline.file.vo.ImageInfoVo;
import com.timeline.utils.IdUtils;
import com.timeline.common.utils.IdUtils;
import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.http.Method;
@@ -18,8 +23,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
@Slf4j
@Service
@@ -32,6 +36,8 @@ public class FileServiceImpl implements FileService {
@Autowired
private CommonRelationMapper commonRelationMapper;
@Autowired
private FileHashMapper fileHashMapper;
@Autowired
public FileServiceImpl(MinioClient minioClient, MinioConfig minioConfig) {
this.minioClient = minioClient;
this.minioConfig = minioConfig;
@@ -96,16 +102,26 @@ public class FileServiceImpl implements FileService {
}
@Override
public void deleteImage(String objectKey) throws Throwable {
public void deleteImage(String instanceId) throws Throwable {
try {
// 删除 MinIO 中的对象
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectKey)
.build());
List<FileHash> otherFileHashByInstanceId = fileHashMapper.getOtherFileHashByInstanceId(instanceId);
ImageInfo imageInfo = imageInfoMapper.selectByInstanceId(instanceId);
if (otherFileHashByInstanceId != null && !otherFileHashByInstanceId.isEmpty()) {
// 删除 MySQL 记录
imageInfoMapper.delete(objectKey);
} else {
// 不存在其他image_info使用则删除 MinIO 中的对象
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(imageInfo.getObjectKey())
.build());
}
// 删除file_hash
fileHashMapper.deleteFileHash(instanceId);
// 删除image_info
imageInfo.setIsDeleted(CommonConstants.DELETED);
imageInfo.setUpdateTime(LocalDateTime.now());
imageInfoMapper.update(imageInfo);
} catch (Exception e) {
log.error("删除图片失败", e);
throw new CustomException(500, "删除图片失败");
@@ -149,25 +165,33 @@ public class FileServiceImpl implements FileService {
}
@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());
public String uploadCover(MultipartFile image) throws Throwable {
String suffix = Objects.requireNonNull(image.getOriginalFilename()).substring(image.getOriginalFilename().lastIndexOf("."));
String hash = CommonUtils.calculateFileHash(image);
String objectKey = hash + suffix;
log.info("上传图片的ObjectKey值为{}", objectKey);
List<FileHash> hashByFileHash = fileHashMapper.getFileHashByFileHash(hash);
// 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.setObjectKey(objectKey);
imageInfo.setImageName(image.getOriginalFilename());
imageInfo.setContentType(image.getContentType());
imageInfo.setSize(image.getSize());
imageInfo.setUserId("9999");
imageInfo.setUploadTime(LocalDateTime.now());
if (hashByFileHash != null && !hashByFileHash.isEmpty()) {
log.info("当前文件已存在不进行minio文件上传");
} else {
// 1. 上传到 MinIO
minioClient.putObject(PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectKey)
.stream(image.getInputStream(), image.getSize(), -1)
.contentType(image.getContentType())
.build());
}
fileHashMapper.insertFileHash(new FileHash(imageInfo.getInstanceId(), hash));
imageInfoMapper.insert(imageInfo);
return imageInfo.getInstanceId();
@@ -182,4 +206,13 @@ public class FileServiceImpl implements FileService {
.object(objectKey)
.build());
}
@Override
public Map getImagesListByOwnerId(ImageInfoVo imageInfoVo) {
HashMap<String, String> map = new HashMap<>();
map.put("ownerId", imageInfoVo.getOwnerId());
Map resultMap = PageUtils.pageQuery(imageInfoVo.getCurrent(), imageInfoVo.getPageSize(), ImageInfoMapper.class, "selectListByOwnerId",
map, "list");
return resultMap;
}
}

View File

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

View File

@@ -13,7 +13,10 @@ minio.bucketName=timeline-test
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.timeline.file.entity
mybatis.configuration.mapUnderscoreToCamelCase=true
server.port=30002
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
spring.servlet.multipart.max-request-size=10MB

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.timeline.file.dao.FileHashMapper">
<insert id="insertFileHash" parameterType="com.timeline.file.entity.FileHash">
INSERT INTO file_hash (instance_id, hash_value, is_deleted)
VALUES (#{instanceId}, #{hashValue}, #{isDeleted})
</insert>
<select id="getFileHashByInstanceId" resultType="com.timeline.file.entity.FileHash">
SELECT * FROM file_hash WHERE instance_id = #{instanceId} AND is_deleted = 0
</select>
<select id="getFileHashByFileHash" resultType="com.timeline.file.entity.FileHash">
SELECT * FROM file_hash WHERE hash_value = #{hashValue} AND is_deleted = 0
</select>
<select id="getOtherFileHashByInstanceId" resultType="com.timeline.file.entity.FileHash">
SELECT * FROM file_hash WHERE instance_id != #{instanceId} AND is_deleted = 0 AND hash_value IN (SELECT hash_value FROM file_hash WHERE instance_id = #{instanceId} AND is_deleted = 0)
</select>
<delete id="deleteFileHash">
UPDATE file_hash SET is_deleted = 1 WHERE instance_id = #{instanceId}
</delete>
</mapper>

View File

@@ -18,6 +18,22 @@
</delete>
<select id="selectObjectKeyById" resultType="string">
SELECT object_key FROM image_info WHERE instance_id = #{instanceId}
SELECT object_key FROM image_info WHERE instance_id = #{instanceId} AND is_deleted = 0
</select>
<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>
<select id="selectByInstanceId" resultType="com.timeline.file.entity.ImageInfo">
SELECT * FROM image_info WHERE instance_id = #{instanceId} AND is_deleted = 0
</select>
<update id="update" parameterType="com.timeline.file.entity.ImageInfo">
UPDATE image_info SET object_key = #{objectKey},
user_id = #{userId},
update_time = #{updateTime},
is_deleted = #{isDeleted}
WHERE instance_id = #{instanceId}
</update>
</mapper>