diff --git a/pom.xml b/pom.xml index 9c3288c..b151597 100644 --- a/pom.xml +++ b/pom.xml @@ -160,6 +160,20 @@ aspectjweaver + + + io.minio + minio + 8.5.7 + + + + + commons-io + commons-io + 2.11.0 + + diff --git a/src/main/java/com/guwan/backend/config/MinioConfig.java b/src/main/java/com/guwan/backend/config/MinioConfig.java new file mode 100644 index 0000000..fd6cbf7 --- /dev/null +++ b/src/main/java/com/guwan/backend/config/MinioConfig.java @@ -0,0 +1,32 @@ +package com.guwan.backend.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 Bucket bucket; + + @Data + public static class Bucket { + private String files; + private String images; + } + + @Bean + public MinioClient minioClient() { + return MinioClient.builder() + .endpoint(endpoint) + .credentials(accessKey, secretKey) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/guwan/backend/util/MinioUtil.java b/src/main/java/com/guwan/backend/util/MinioUtil.java new file mode 100644 index 0000000..d327681 --- /dev/null +++ b/src/main/java/com/guwan/backend/util/MinioUtil.java @@ -0,0 +1,159 @@ +package com.guwan.backend.util; + +import io.minio.*; +import io.minio.http.Method; +import io.minio.messages.Bucket; +import io.minio.messages.DeleteObject; +import io.minio.messages.Item; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.FilenameUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.*; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MinioUtil { + + private final MinioClient minioClient; + + /** + * 创建存储桶 + */ + public void createBucket(String bucketName) { + try { + boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); + if (!found) { + minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); + } + } catch (Exception e) { + log.error("创建存储桶失败", e); + throw new RuntimeException("创建存储桶失败", e); + } + } + + /** + * 上传文件 + */ + public String uploadFile(String bucketName, MultipartFile file) { + try { + String fileName = generateFileName(file.getOriginalFilename()); + InputStream inputStream = file.getInputStream(); + minioClient.putObject( + PutObjectArgs.builder() + .bucket(bucketName) + .object(fileName) + .stream(inputStream, file.getSize(), -1) + .contentType(file.getContentType()) + .build() + ); + return fileName; + } catch (Exception e) { + log.error("上传文件失败", e); + throw new RuntimeException("上传文件失败", e); + } + } + + /** + * 上传Base64图片 + */ + public String uploadBase64Image(String bucketName, String base64Image, String folder) { + try { + String[] parts = base64Image.split(","); + String imageData = parts[1]; + byte[] bytes = Base64.getDecoder().decode(imageData); + + String fileName = folder + "/" + UUID.randomUUID() + ".jpg"; + ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); + + minioClient.putObject( + PutObjectArgs.builder() + .bucket(bucketName) + .object(fileName) + .stream(inputStream, bytes.length, -1) + .contentType("image/jpeg") + .build() + ); + return fileName; + } catch (Exception e) { + log.error("上传Base64图片失败", e); + throw new RuntimeException("上传Base64图片失败", e); + } + } + + /** + * 删除文件 + */ + public void deleteFile(String bucketName, String fileName) { + try { + minioClient.removeObject( + RemoveObjectArgs.builder() + .bucket(bucketName) + .object(fileName) + .build() + ); + } catch (Exception e) { + log.error("删除文件失败", e); + throw new RuntimeException("删除文件失败", e); + } + } + + /** + * 批量删除文件 + */ + public void deleteFiles(String bucketName, List fileNames) { + try { + List objects = fileNames.stream() + .map(DeleteObject::new) + .toList(); + + Iterable> results = minioClient.removeObjects( + RemoveObjectsArgs.builder() + .bucket(bucketName) + .objects(objects) + .build() + ); + + for (Result result : results) { + DeleteError error = result.get(); + log.error("删除文件失败: {}", error.message()); + } + } catch (Exception e) { + log.error("批量删除文件失败", e); + throw new RuntimeException("批量删除文件失败", e); + } + } + + /** + * 获取文件访问URL + */ + public String getFileUrl(String bucketName, String fileName) { + try { + return minioClient.getPresignedObjectUrl( + GetPresignedObjectUrlArgs.builder() + .method(Method.GET) + .bucket(bucketName) + .object(fileName) + .expiry(7, TimeUnit.DAYS) + .build() + ); + } catch (Exception e) { + log.error("获取文件访问URL失败", e); + throw new RuntimeException("获取文件访问URL失败", e); + } + } + + /** + * 生成文件名 + */ + private String generateFileName(String originalFilename) { + String extension = FilenameUtils.getExtension(originalFilename); + return UUID.randomUUID() + "." + extension; + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1fa9b0e..f4f61f3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -74,9 +74,14 @@ jwt: aliyun: sms: - - - +# MinIO配置 +minio: + endpoint: http://localhost:9000 + accessKey: minioadmin + secretKey: minioadmin + bucket: + files: files # 文件桶 + images: images # 图片桶 # 文件上传配置 file: diff --git a/src/test/java/com/guwan/backend/util/MinioUtilTest.java b/src/test/java/com/guwan/backend/util/MinioUtilTest.java new file mode 100644 index 0000000..9e1cecc --- /dev/null +++ b/src/test/java/com/guwan/backend/util/MinioUtilTest.java @@ -0,0 +1,67 @@ +package com.guwan.backend.util; + +import com.guwan.backend.config.MinioConfig; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockMultipartFile; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +class MinioUtilTest { + + @Autowired + private MinioUtil minioUtil; + + @Autowired + private MinioConfig minioConfig; + + @Test + void testUploadAndDeleteFile() { + // 创建测试文件 + String content = "test content"; + MockMultipartFile file = new MockMultipartFile( + "test.txt", + "test.txt", + "text/plain", + content.getBytes(StandardCharsets.UTF_8) + ); + + // 上传文件 + String fileName = minioUtil.uploadFile(minioConfig.getBucket().getFiles(), file); + assertNotNull(fileName); + + // 获取文件URL + String url = minioUtil.getFileUrl(minioConfig.getBucket().getFiles(), fileName); + assertNotNull(url); + assertTrue(url.contains(fileName)); + + // 删除文件 + minioUtil.deleteFile(minioConfig.getBucket().getFiles(), fileName); + } + + @Test + void testUploadBase64Image() { + // 创建Base64图片 + String base64Image = "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="; + + // 上传图片 + String fileName = minioUtil.uploadBase64Image( + minioConfig.getBucket().getImages(), + base64Image, + "test" + ); + assertNotNull(fileName); + + // 获取图片URL + String url = minioUtil.getFileUrl(minioConfig.getBucket().getImages(), fileName); + assertNotNull(url); + assertTrue(url.contains(fileName)); + + // 删除图片 + minioUtil.deleteFile(minioConfig.getBucket().getImages(), fileName); + } +} \ No newline at end of file