系统集成 minio

This commit is contained in:
cwchen 2025-10-16 15:59:56 +08:00
parent abd50ba966
commit e37c6c1370
10 changed files with 811 additions and 1 deletions

View File

@ -0,0 +1,44 @@
package com.bonus.web.controller.file;
import com.bonus.common.core.domain.AjaxResult;
import com.bonus.common.domain.ocr.dto.OcrRequest;
import com.bonus.common.domain.ocr.vo.OcrResponse;
import com.bonus.file.service.FileUploadService;
import com.bonus.file.util.FileUtil;
import com.bonus.ocr.service.OcrService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
/**
* @className:FileUploadController
* @author:cwchen
* @date:2025-10-16-15:47
* @version:1.0
* @description:文件上传
*/
@RestController
@RequestMapping("/file")
public class FileUploadController {
@Resource(name = "FileUploadService")
private FileUploadService fileUploadService;
@Value("${uploadSuffix.main_database}")
private String uploadSuffix;
@PostMapping(value = "uploadFile")
private AjaxResult ocrHandler(MultipartFile file) {
// 生成文件路径
String uploadPath = FileUtil.generateDatePath(file, uploadSuffix);
String s = fileUploadService.uploadFile(file,uploadPath);
System.err.println("文件路径" + s);
return AjaxResult.success();
}
}

View File

@ -4,4 +4,7 @@ minio:
endpoint: http://192.168.0.14:9090
access-key: minio
secret-key: bonus@admin123
bucket-name: smart-bid
bucket-name: smart-bid
# 文件上传前缀
uploadSuffix:
main_database: main_database #主体库

View File

@ -181,6 +181,12 @@
<version>4.5.14</version>
</dependency>
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>${minio.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,49 @@
package com.bonus.common.domain.file.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
/**
* @author bonus
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FileDetails {
/**
* 文件或文件夹的路径
*/
private String fileName;
/**
* 相对地址
*/
private String objectKey;
/**
* 文件大小文件夹默认为 0
*/
private long size;
/**
* 最后修改时间文件夹也有此属性
*/
private Date lastModified;
/**
* ETag文件夹一般没有 ETag
*/
private String etag;
/**
* 内容类型文件夹一般没有此信息
*/
private String contentType;
/**
* 是否是文件夹默认值为 false
*/
private boolean isFolder;
}

View File

@ -0,0 +1,34 @@
package com.bonus.common.domain.file.vo;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
/**
* 文件信息
*
* @author bonus
*/
@Data
@Builder
public class SysFile
{
/**
* 文件名称
*/
private String name;
/**
* 文件地址除mongodb 存fileid之外其他均存上传文件的网络路径
*/
private String url;
@Override
public String toString() {
return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
.append("name", getName())
.append("url", getUrl())
.toString();
}
}

View File

@ -0,0 +1,55 @@
package com.bonus.file.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.minio.MinioClient;
/**
* @className:MinioConfig
* @author:cwchen
* @date:2025-10-16-15:04
* @version:1.0
* @description:Minio配置
*/
@Configuration
@Data
public class MinioConfig {
/**
* 服务地址
*/
@Value("${minio.url}")
private String url;
/**
* 服务地址
*/
@Value("${minio.endpoint}")
private String endpoint;
/**
* 用户名
*/
@Value("${minio.access-key}")
private String accessKey;
/**
* 密码
*/
@Value("${minio.secret-key}")
private String secretKey;
/**
* 存储桶名称
*/
@Value("${minio.bucket-name}")
private String bucketName;
@Bean
public MinioClient getMinioClient()
{
return MinioClient.builder().endpoint(endpoint).credentials(accessKey, secretKey).build();
}
}

View File

@ -0,0 +1,73 @@
package com.bonus.file.service;
import com.bonus.common.domain.file.vo.SysFile;
import com.bonus.file.config.MinioConfig;
import com.bonus.file.util.MinioUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
/**
* @className:FileUploadService
* @author:cwchen
* @date:2025-10-16-15:31
* @version:1.0
* @description:文件上传实现类
*/
@Service(value = "FileUploadService")
@Slf4j
public class FileUploadService {
@Resource
public MinioUtil minioUtil;
@Resource
private MinioConfig minioConfig;
/**
* 上传文件
* @param file
* @return
*/
public String uploadFile(MultipartFile file, String path) {
try{
minioUtil.uploadFile(minioConfig.getBucketName(), file,path);
return minioConfig.getBucketName();
}catch (Exception e){
log.error(e.toString(),e);
}
return null;
}
/**
* 删除文件
* @param filePath
*/
public void delFile(String filePath) {
try {
boolean isExist = minioUtil.isObjectExist(minioConfig.getBucketName(), filePath);
if (isExist) {
minioUtil.removeFile(minioConfig.getBucketName(), filePath);
}
} catch (Exception e) {
log.error(e.toString(), e);
}
}
/**
* 上传大文件
* @param file
* @return
*/
public SysFile uploadLargeFile(MultipartFile file, String path) {
try{
return minioUtil.uploadFile(file, path);
}catch (Exception e){
log.error(e.toString(),e);
}
return null;
}
}

View File

@ -0,0 +1,44 @@
package com.bonus.file.util;
import org.springframework.web.multipart.MultipartFile;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
/**
* @className:FileUtil
* @author:cwchen
* @date:2025-10-16-15:56
* @version:1.0
* @description:文件工具类
*/
public class FileUtil {
/**
* 生成日期目录格式的存储路径
*/
public static String generateDatePath(MultipartFile file, String baseDir) {
// 生成日期目录//
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
String datePath = sdf.format(new Date());
// 生成唯一文件名
String originalFilename = file.getOriginalFilename();
String fileExtension = getFileExtension(originalFilename);
String uniqueFileName = UUID.randomUUID().toString() + fileExtension;
// 构建完整路径
return Paths.get(baseDir, datePath, uniqueFileName).toString();
}
/**
* 获取文件扩展名
*/
private static String getFileExtension(String filename) {
if (filename == null || filename.lastIndexOf(".") == -1) {
return "";
}
return filename.substring(filename.lastIndexOf("."));
}
}

View File

@ -0,0 +1,501 @@
package com.bonus.file.util;
import com.bonus.common.domain.file.vo.SysFile;
import io.minio.*;
import com.bonus.file.config.MinioConfig;
import io.minio.errors.MinioException;
import io.minio.http.Method;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* MinioUtil 工具类
* 封装 MinIO 常用操作如文件上传下载删除复制等功能
* @author bonus
*/
@Component
public class MinioUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(MinioUtil.class);
@Resource
private MinioClient minioClient;
@Resource
private MinioConfig minioConfig;
/**
* 分片大小
*/
private static final long PART_SIZE = 5*1024*1024;
/**
* 初始化默认存储桶
* Spring 容器启动后自动调用检查默认存储桶是否存在若不存在则创建
*/
@PostConstruct
public void init() {
try {
if (bucketExists(minioConfig.getBucketName())) {
LOGGER.error("桶已存在: {}", minioConfig.getBucketName());
} else {
createBucket(minioConfig.getBucketName());
}
} catch (Exception e) {
LOGGER.error("创建桶失败: {}",e.getMessage(),e);
throw new RuntimeException("创建桶失败: " + e.getMessage(), e);
}
}
/**
* 检查指定存储桶是否存在
* @param bucketName 存储桶名称
* @return true 表示存在false 表示不存在
* @throws Exception 若检查过程中发生异常
*/
public boolean bucketExists(String bucketName) throws Exception {
return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
}
/**
* 创建指定的存储桶
* @param bucketName 存储桶名称
* @throws Exception 若创建过程中发生异常
*/
@SneakyThrows(Exception.class)
public void createBucket(String bucketName) {
if (bucketExists(bucketName)) {
LOGGER.error("桶已存在: {}", bucketName);
} else {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
LOGGER.error("创建桶成功: {}", bucketName);
}
}
/**
*
* 上传文件到指定存储桶
* @param file MultipartFile 文件对象
* @return 上传后的文件访问 URL
* @throws Exception 若上传过程中发生异常
*/
public SysFile uploadFile(MultipartFile file, String folderPath) throws Exception {
if (file.getSize() < 10 * 1024 * 1024L) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(folderPath)
// -1 表示不限制文件大小
.stream(file.getInputStream(), file.getInputStream().available(), -1)
.contentType(file.getContentType())
.build());
} else {
uploadLargeFile(folderPath, file);
}
return SysFile.builder()
.name(file.getOriginalFilename())
.url(folderPath).build();
}
/**
* 分片上传文件到指定文件夹
* @param file MultipartFile 文件对象
* @param folderPath 目标文件夹路径 "folder/subfolder"
* @throws Exception 若上传过程中发生异常
*/
public void uploadLargeFile(String folderPath,MultipartFile file) throws Exception {
long fileSize = file.getSize();
int partCount = (int) Math.ceil((double) fileSize / PART_SIZE);
List<String> partNames = new ArrayList<>();
// 创建线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
List<CompletableFuture<Void>> futures = new ArrayList<>();
// 上传每个分片
for (int i = 0; i < partCount; i++) {
long offset = i * PART_SIZE;
long currentPartSize = Math.min(PART_SIZE, fileSize - offset);
// 设置分片名称
String partObjectName = folderPath + "part." + i;
partNames.add(partObjectName);
final long partOffset = offset;
final long partSizeFinal = currentPartSize;
final String partName = partObjectName;
// 异步上传每个分片
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try (InputStream inputStream = file.getInputStream()) { // 获取文件的输入流
// 跳过文件前面的部分直到当前分片的起始位置
long skipped = inputStream.skip(partOffset);
if (skipped != partOffset) {
throw new RuntimeException("Could not skip to the correct part offset.");
}
byte[] buffer = new byte[(int) partSizeFinal]; // 创建缓冲区来存储分片数据
int bytesRead = inputStream.read(buffer); // 读取分片数据
if (bytesRead == -1) {
throw new RuntimeException("Error reading the part data.");
}
// 上传分片
try (ByteArrayInputStream stream = new ByteArrayInputStream(buffer)) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(partName)
.stream(stream, stream.available(), -1)
.build());
System.out.println("Uploaded part: " + partName);
}
} catch (Exception e) {
throw new RuntimeException("Error uploading part: " + partName, e);
}
}, executor); // 指定使用线程池执行任务
futures.add(future); // 将任务添加到 futures 列表
}
// 等待所有分片上传完成
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
allOf.join(); // 阻塞等待所有任务完成
// 合并所有分片
List<ComposeSource> sources = new ArrayList<>();
for (String partName : partNames) {
sources.add(ComposeSource.builder().bucket(minioConfig.getBucketName()).object(partName).build());
}
// 将所有分片合并成最终文件
try {
minioClient.composeObject(ComposeObjectArgs.builder()
.bucket(minioConfig.getBucketName())
// 最终文件的路径
.object(folderPath) // 可以自定义最终文件名
.sources(sources)
.build());
System.out.println("Large file uploaded and composed successfully.");
} catch (MinioException e) {
throw new Exception("Error during file composition: " + e.getMessage(), e);
}
// 删除临时分片
for (String partName : partNames) {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioConfig.getBucketName()).object(partName).build());
System.out.println("Removed part: " + partName);
}
System.out.println("Large file uploaded successfully to folder: " + folderPath);
}
/**
* 下载指定文件 InputStream 形式返回
* @param objectName 存储对象名称文件名
* @return 文件的输入流
*/
public InputStream downloadFile(String objectName){
try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build())) {
return inputStream;
} catch (Exception e) {
LOGGER.error("Error downloading file: {}", e.getMessage(), e);
return null;
}
}
/**
* 将多个文件压缩为 ZIP 并上传至 MinIO返回压缩文件的下载链接
* @param bucketName 存储桶名称
* @param objectNames 文件对象列表文件名
* @param zipFileName 压缩后的文件名
* @return 压缩文件的下载 URL
* @throws Exception
*/
public String downloadFilesAsZip(String bucketName, List<String> objectNames, String zipFileName) throws Exception {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ZipOutputStream zos = new ZipOutputStream(baos)) {
// 压缩每个文件
for (String objectName : objectNames) {
try (InputStream inputStream = minioClient.getObject(GetObjectArgs.builder()
.bucket(bucketName)
.object(objectName)
.build())) {
// 添加 ZipEntry
zos.putNextEntry(new ZipEntry(objectName));
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
zos.closeEntry();
}
}
zos.finish();
// 上传 ZIP 文件到 MinIO
try (ByteArrayInputStream zipInputStream = new ByteArrayInputStream(baos.toByteArray())) {
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(zipFileName)
.stream(zipInputStream, zipInputStream.available(), -1)
.contentType("application/zip")
.build());
}
}
// 返回压缩文件的下载 URL
return getFileUrl(bucketName, zipFileName);
}
/**
* 获取文件的临时访问 URL指定过期时间
* @param bucketName 存储桶名称
* @param objectName 存储对象名称文件名
* @param expiryTimeInSeconds URL 的有效时长
* @return 文件的临时访问 URL
* @throws Exception 若生成 URL 过程中发生异常
*/
@SneakyThrows(Exception.class)
public String getFileUrl(String bucketName, String objectName, int expiryTimeInSeconds) {
try{
return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.bucket(bucketName)
.object(objectName)
.expiry(expiryTimeInSeconds)
.method(Method.GET)
.build());
}catch (Exception e){
return "";
}
}
/**
* 获取文件bast64
* @param bucketName
* @param path
* @return
*/
@SneakyThrows(Exception.class)
public String getMinioBast64(String bucketName,String path) {
InputStream inputStream= minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(path).build());
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
byte[] bytes = outputStream.toByteArray();
String bast64= Base64.getEncoder().encodeToString(bytes);
inputStream.close();
outputStream.close();
return bast64;
}
/**
* 删除文件
*
* @param bucketName 存储桶
* @param objectName 文件名称
*/
@SneakyThrows(Exception.class)
public void removeFile(String bucketName, String objectName) {
minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
}
/**
* 上传文件到指定路径
* @param bucketName
* @param file
* @param objectName
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file,String objectName) {
String contentType = file.getContentType();
InputStream inputStream = file.getInputStream();
ObjectWriteResponse object= minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).contentType(contentType).stream(inputStream, inputStream.available(), -1).build());
inputStream.close();
return object;
}
/**
* 图片上传
*
* @param bucketName
* @param imageBase64
* @param
* @return
*/
public ObjectWriteResponse uploadImage(String bucketName, String imageBase64, String path) {
if (!StringUtils.isEmpty(imageBase64)) {
InputStream in = base64ToInputStream(imageBase64);
return uploadFile(bucketName, path, in);
}
return null;
}
public static InputStream base64ToInputStream(String base64) {
ByteArrayInputStream stream = null;
try {
byte[] bytes = Base64.getEncoder().encode(base64.trim().getBytes());
stream = new ByteArrayInputStream(bytes);
} catch (Exception e) {
e.printStackTrace();
}
return stream;
}
/**
* 通过流上传文件
*
* @param bucketName 存储桶
* @param objectName 文件对象
* @param inputStream 文件流
* @return
*/
@SneakyThrows(Exception.class)
public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) {
return minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(inputStream, inputStream.available(), -1).build());
}
/**
* 获取文件的临时访问 URL默认过期时间为 7
* @param bucketName 存储桶名称
* @param objectName 存储对象名称文件名
* @return 文件的临时访问 URL
* @throws Exception 若生成 URL 过程中发生异常
*/
public String getFileUrl(String bucketName, String objectName) {
// 604800 = 7
return getFileUrl(bucketName, objectName, 604800);
}
/**
* 删除指定的对象
* @param objectName 存储对象名称文件名
* @throws Exception 若删除过程中发生异常
*/
public void deleteObject( String objectName) throws Exception {
minioClient.removeObject(RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build());
}
/**
* 判断文件是否存在
*
* @param bucketName
* @param objectName
* @return
*/
public boolean isObjectExist(String bucketName, String objectName) {
boolean exist = true;
try {
minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
} catch (Exception e) {
exist = false;
}
return exist;
}
/**
* 创建文件夹通过上传带有路径的文件
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
public void createFolder(String folderPath) {
try {
if (!folderPath.endsWith("/")) {
folderPath += "/";
}
// 创建一个空文件内容模拟文件夹
// 空文件内容
String emptyFileContent = "";
InputStream emptyFileStream = new ByteArrayInputStream(emptyFileContent.getBytes());
// 上传一个空文件到指定路径从而模拟文件夹创建
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
// 使用文件夹路径 + 文件名
.object(folderPath + "empty.txt")
.stream(emptyFileStream, 0, -1)
.contentType("application/text")
.build()
);
} catch (Exception e) {
// 使用 LOGGER 打印异常日志
LOGGER.error("Error occurred while fetching files from bucket: {}",minioConfig.getBucketName(), e);
}
}
/**
* 删除指定路径的所有对象从而删除整个文件夹及其子文件夹
*
* @param folderPath 文件夹路径例如 "folder1/subfolder/"
*/
public void deleteFolder(String folderPath) {
try {
if (!folderPath.endsWith("/")) {
folderPath += "/";
}
// 列出指定文件夹路径下的所有对象包括子文件夹中的对象
Iterable<Result<Item>> objects = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(minioConfig.getBucketName())
.prefix(folderPath) // 设置路径前缀列出该路径下的所有对象
.recursive(true) // 递归列出所有子文件夹中的对象
.build()
);
// 遍历所有对象并删除它们
for (Result<Item> result : objects) {
Item item = result.get();
String objectName = item.objectName();
// 删除对象
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.object(objectName)
.build()
);
System.out.println("Deleted object: " + objectName);
}
System.out.println("Folder and all subfolders deleted successfully at path: " + folderPath);
} catch (Exception e) {
System.err.println("Error deleting folder and its contents at path: " + folderPath);
e.printStackTrace();
}
}
}

View File

@ -38,6 +38,7 @@
<profiles.active>dev</profiles.active> <!-- 默认值 -->
<!-- 强制使用新的 MySQL 连接器坐标 -->
<mysql.connector-j.version>8.0.33</mysql.connector-j.version>
<minio.version>8.2.2</minio.version>
</properties>
<!-- 依赖声明 -->