minio基于用户租户的数据桶隔离
This commit is contained in:
@@ -538,4 +538,20 @@ public class DataFileController implements IDataFeignClient {
|
||||
return ResponseEntity.ok("SUCCESS:" + targetFile.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 供 System 服务调用:新租户初始化
|
||||
*/
|
||||
@PostMapping("/initNewTenant")
|
||||
public SdmResponse initNewTenant(@RequestParam Long tenantId) {
|
||||
if (tenantId == null) {
|
||||
return SdmResponse.failed("TenantId 不能为空");
|
||||
}
|
||||
try {
|
||||
boolean success = IDataFileService.initTenantData(tenantId);
|
||||
return success ? SdmResponse.success() : SdmResponse.failed("初始化失败");
|
||||
} catch (Exception e) {
|
||||
return SdmResponse.failed("初始化异常: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,27 +19,12 @@ import javax.annotation.Resource;
|
||||
@Component
|
||||
public class InitSystemDirectory {
|
||||
|
||||
@Resource
|
||||
private SpringUtils springUtils;
|
||||
|
||||
@Autowired
|
||||
private IDataFileService IDataFileService;
|
||||
|
||||
@Resource
|
||||
private SystemMapper systemMapper;
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct() {
|
||||
/* List<SysCompany> companyList = systemMapper.getCompanyList(0);
|
||||
for (SysCompany company : companyList) {
|
||||
log.info(company.getCompany() + "初始化系统文件...");
|
||||
boolean result = IDataService.initSystemDirectory(company.getCompany());
|
||||
if (result) {
|
||||
systemMapper.updateCompanyInitDir(company.getCompany(), 1);
|
||||
}
|
||||
log.info(company.getCompany() + "初始化系统文件完成!");
|
||||
}*/
|
||||
|
||||
IDataFileService.initSystemDirectory(null);
|
||||
}
|
||||
}
|
||||
@@ -140,6 +140,10 @@ public interface IDataFileService {
|
||||
boolean initSystemDirectory(String company);
|
||||
|
||||
|
||||
default boolean initTenantData(Long tenantId){
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
|
||||
@@ -8,28 +8,22 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public interface IMinioService {
|
||||
/**
|
||||
* 获取MinIO的存储桶名称
|
||||
*
|
||||
* @return 存储桶名称
|
||||
*/
|
||||
public String getBucketName();
|
||||
|
||||
|
||||
Set<String> preloadBucketCache();
|
||||
|
||||
void createBucketIfAbsent(String bucketName);
|
||||
|
||||
/**
|
||||
* 获取MinIO的保密业务存储桶名称
|
||||
*
|
||||
* @return 保密业务存储桶名称
|
||||
*/
|
||||
public String getSecretBusinessBucketName();
|
||||
String getSecretBusinessBucketName();
|
||||
|
||||
/**
|
||||
* 创建目录
|
||||
*
|
||||
* @param objectKey 绝对路径目录名
|
||||
*/
|
||||
public void createDirectoryByObjectKey(String objectKey);
|
||||
String getCurrentTenantBucketName();
|
||||
|
||||
/**
|
||||
* 创建目录
|
||||
@@ -37,52 +31,31 @@ public interface IMinioService {
|
||||
* @param objectKey 绝对路径目录名
|
||||
* @param bucketName 桶名称
|
||||
*/
|
||||
public void createDirectoryByObjectKey(String objectKey, String bucketName);
|
||||
void createDirectoryByObjectKey(String objectKey, String bucketName);
|
||||
|
||||
/**
|
||||
* 递归删除指定目录下的所有对象。
|
||||
*
|
||||
* @param directoryName 目录名称(objectKey)
|
||||
* @param bucketName 桶名称
|
||||
*/
|
||||
public void deleteDirectoryRecursively(String directoryName);
|
||||
|
||||
public void deleteDirectoryRecursively2(String directoryName,String bucketName);
|
||||
void deleteDirectoryRecursively(String directoryName, String bucketName);
|
||||
|
||||
/**
|
||||
* 递归获取目录下的所有对象。
|
||||
*
|
||||
* @param directoryName 目录名称(objectKey)
|
||||
* @param bucketName
|
||||
*/
|
||||
public Iterable<Result<Item>> listObjects(String directoryName);
|
||||
/**
|
||||
* 递归删除指定目录下的所有对象。
|
||||
*
|
||||
* @param directoryName 目录名称(objectKey)
|
||||
* @param bucketName 桶名称
|
||||
*/
|
||||
public void deleteDirectoryRecursively(String directoryName, String bucketName);
|
||||
|
||||
/**
|
||||
* 从MinIO删除文件
|
||||
* @param minioObjectKey 文件名(objectKey)
|
||||
*/
|
||||
public void deleteFile(String minioObjectKey);
|
||||
Iterable<Result<Item>> listObjects(String directoryName, String bucketName);
|
||||
|
||||
/**
|
||||
* 从MinIO删除文件
|
||||
* @param minioObjectKey 文件名(objectKey)
|
||||
* @param bucketName 桶名称
|
||||
*/
|
||||
public void deleteFile(String minioObjectKey, String bucketName);
|
||||
void deleteFile(String minioObjectKey, String bucketName);
|
||||
|
||||
/**
|
||||
* 上传文件到MinIO
|
||||
* @param file 文件对象
|
||||
* @param objectName 文件名(objectKey)
|
||||
* @param tags 文件标签
|
||||
*/
|
||||
public void uploadFile(MultipartFile file, String objectName, Map<String, String> tags);
|
||||
|
||||
/**
|
||||
* 上传文件到MinIO
|
||||
* @param file 文件对象
|
||||
@@ -90,16 +63,7 @@ public interface IMinioService {
|
||||
* @param tags 文件标签
|
||||
* @param bucketName 桶名称
|
||||
*/
|
||||
public void uploadFile(MultipartFile file, String objectName, Map<String, String> tags, String bucketName);
|
||||
|
||||
/**
|
||||
* 重命名MinIO中的文件
|
||||
*
|
||||
* @param oldObjectName 原文件名(objectKey)
|
||||
* @param newObjectName 新文件名(objectKey)
|
||||
* @throws Exception 异常信息
|
||||
*/
|
||||
public void renameFile(String oldObjectName, String newObjectName);
|
||||
void uploadFile(MultipartFile file, String objectName, Map<String, String> tags, String bucketName);
|
||||
|
||||
/**
|
||||
* 重命名MinIO中的文件
|
||||
@@ -109,21 +73,9 @@ public interface IMinioService {
|
||||
* @param bucketName 桶名称
|
||||
* @throws Exception 异常信息
|
||||
*/
|
||||
public void renameFile(String oldObjectName, String newObjectName, String bucketName);
|
||||
void renameFile(String oldObjectName, String newObjectName, String bucketName);
|
||||
|
||||
|
||||
/**
|
||||
* 从MinIO下载文件
|
||||
* @param objectName 文件名(objectKey)
|
||||
*/
|
||||
byte[] downloadFile(String objectName) throws Exception;
|
||||
|
||||
/**
|
||||
* 从MinIO下载文件到response
|
||||
* @param objectName 文件名(objectKey)
|
||||
*/
|
||||
void downloadFile(String objectName, HttpServletResponse response,String encodedFileName,Boolean preview,String contentType) throws Exception;
|
||||
|
||||
/**
|
||||
* 从MinIO下载文件
|
||||
* @param objectName 文件名(objectKey)
|
||||
@@ -131,24 +83,24 @@ public interface IMinioService {
|
||||
*/
|
||||
byte[] downloadFile(String objectName, String bucketName) throws Exception;
|
||||
|
||||
InputStream getMinioInputStream(String objectName);
|
||||
/**
|
||||
* 从MinIO下载文件到response
|
||||
* @param objectName 文件名(objectKey)
|
||||
*/
|
||||
void downloadFile(String objectName, String bucketName, HttpServletResponse response,String encodedFileName,Boolean preview, String contentType) throws Exception;
|
||||
|
||||
InputStream getMinioInputStream(String objectName, String bucketName);
|
||||
|
||||
/**
|
||||
* 根据标签查询文件
|
||||
*/
|
||||
List<String> queryFilesByTags(Map<String, String> tags) throws Exception;
|
||||
|
||||
/**
|
||||
* 根据标签查询文件
|
||||
*/
|
||||
List<String> queryFilesByTags(Map<String, String> tags, String bucketName) throws Exception;
|
||||
|
||||
String getMinioPresignedUrl(String objectKey);
|
||||
String getMinioPresignedUrl(String objectKey, String bucketName);
|
||||
|
||||
Boolean chunkUpload(String bucketName, MultipartFile file, String fileName,Map<String, String> tags);
|
||||
|
||||
Boolean merge(String tempBucketName,String tempFilePath,String mergeBucketName,String fileName);
|
||||
|
||||
|
||||
}
|
||||
@@ -102,7 +102,7 @@ public class DataAnalysisServiceImpl implements IDataAnalysisService {
|
||||
for (Long fileId : fileIds) {
|
||||
FileMetadataInfo csvFile = fileMetadataInfoService.getById(fileId);
|
||||
String objectKey = csvFile.getObjectKey();
|
||||
byte[] fileBytes = MinIOService.downloadFile(objectKey);
|
||||
byte[] fileBytes = MinIOService.downloadFile(objectKey,csvFile.getBucketName());
|
||||
|
||||
// 将字节数组转换为 BufferedReader 以便读取 CSV 内容
|
||||
ByteArrayInputStream bis = new ByteArrayInputStream(fileBytes);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@ public class DeleteApproveStrategy implements ApproveStrategy {
|
||||
// 审批通过
|
||||
if (NumberConstants.TWO == status) {
|
||||
// 删除MinIO文件
|
||||
minioService.deleteFile(metadata.getObjectKey());
|
||||
minioService.deleteFile(metadata.getObjectKey(),metadata.getBucketName() );
|
||||
// 删除数据库记录
|
||||
service.removeById(metadata.getId());
|
||||
fileStorageService.remove(new LambdaQueryWrapper<FileStorage>().eq(FileStorage::getFileId, metadata.getId()));
|
||||
|
||||
@@ -53,7 +53,7 @@ public class ModifyFileApproveStrategy implements ApproveStrategy {
|
||||
// 审批不通过
|
||||
if (NumberConstants.THREE == status) {
|
||||
// 删除MinIO中修改的文件
|
||||
minioService.deleteFile(metadata.getObjectKey());
|
||||
minioService.deleteFile(metadata.getObjectKey(), metadata.getBucketName());
|
||||
// 删除修改产生的新记录
|
||||
service.removeById(metadata.getId());
|
||||
fileMetadataExtensionService.remove(new LambdaQueryWrapper<FileMetadataExtension>().eq(FileMetadataExtension::getTFilemetaId, metadata.getId()));
|
||||
|
||||
@@ -43,7 +43,7 @@ public class UploadApproveStrategy implements ApproveStrategy {
|
||||
// 删除MinIO文件
|
||||
List<Long>removeIds = new ArrayList<>();
|
||||
approveMetadataInfos.forEach(metadata -> {
|
||||
minioService.deleteFile(metadata.getObjectKey());
|
||||
minioService.deleteFile(metadata.getObjectKey(), metadata.getBucketName());
|
||||
removeIds.add(metadata.getId());
|
||||
});
|
||||
// 删除数据库记录
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sdm.data.service.minio;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.sdm.common.common.ThreadLocalContext;
|
||||
import com.sdm.common.entity.constants.NumberConstants;
|
||||
import com.sdm.common.log.CoreLogger;
|
||||
import com.sdm.data.config.MinioConfig;
|
||||
@@ -31,10 +32,8 @@ import java.nio.channels.WritableByteChannel;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -47,6 +46,10 @@ public class MinioService implements IMinioService {
|
||||
// 可配置缓冲区大小) 16KB
|
||||
private static final int DEFAULT_BUFFER_SIZE = 16384;
|
||||
|
||||
// 1. 本地缓存:记录已经确认存在的桶,避免每次上传都请求 MinIO 网络
|
||||
// 使用 ConcurrentHashMap.newKeySet() 保证高并发下的线程安全
|
||||
private final Set<String> existingBucketsCache = ConcurrentHashMap.newKeySet();
|
||||
|
||||
@Autowired
|
||||
public MinioService(MinioClient minioClient, MinioConfig minioConfig) {
|
||||
this.minioClient = minioClient;
|
||||
@@ -58,13 +61,6 @@ public class MinioService implements IMinioService {
|
||||
*/
|
||||
@PostConstruct
|
||||
public void initializeBucket() {
|
||||
try {
|
||||
createBucketIfNotExists(minioConfig.getSpdmBucket());
|
||||
log.info("桶{}初始化完成", minioConfig.getSpdmBucket());
|
||||
} catch (Exception e) {
|
||||
log.error("桶{}初始化失败", minioConfig.getSpdmBucket(), e);
|
||||
}
|
||||
|
||||
try {
|
||||
createBucketIfNotExists(minioConfig.getSecretBusinessBucket());
|
||||
log.info("桶{}初始化完成", minioConfig.getSecretBusinessBucket());
|
||||
@@ -75,11 +71,97 @@ public class MinioService implements IMinioService {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 【初始化专用】从 MinIO 拉取现有桶列表(缓存预热)
|
||||
* 该方法由 Initializer 调用,只消耗一次网络请求
|
||||
*/
|
||||
public Set<String> preloadBucketCache() {
|
||||
log.info("正在从 MinIO 拉取现有桶列表进行缓存预热...");
|
||||
Set<String> currentMinioBuckets = new HashSet<>();
|
||||
try {
|
||||
List<Bucket> buckets = minioClient.listBuckets();
|
||||
String baseName = minioConfig.getSpdmBucket();
|
||||
|
||||
public String getBucketName() {
|
||||
return minioConfig.getSpdmBucket();
|
||||
for (Bucket bucket : buckets) {
|
||||
// 只要是以 spdm 开头的,都认为是系统的业务桶,加入缓存
|
||||
if (bucket.name().startsWith(baseName)) {
|
||||
existingBucketsCache.add(bucket.name());
|
||||
currentMinioBuckets.add(bucket.name());
|
||||
}
|
||||
}
|
||||
log.info("MinIO 桶缓存预热完成,共加载 {} 个桶。", existingBucketsCache.size());
|
||||
} catch (Exception e) {
|
||||
log.error("预热缓存失败(不影响核心功能,将降级为惰性检查)", e);
|
||||
}
|
||||
return currentMinioBuckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* 【初始化专用】创建指定桶(如果缓存里没有)
|
||||
* 供初始化器批量补齐使用
|
||||
*/
|
||||
public void createBucketIfAbsent(String bucketName) {
|
||||
if (existingBucketsCache.contains(bucketName)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
|
||||
if (!found) {
|
||||
log.info("系统初始化:创建缺失的租户桶 -> {}", bucketName);
|
||||
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
|
||||
// TODO: 如果需要,可以在这里设置生命周期策略 (Lifecycle Config)
|
||||
}
|
||||
existingBucketsCache.add(bucketName);
|
||||
} catch (Exception e) {
|
||||
log.error("创建桶失败: {}", bucketName, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 【核心方法】获取当前租户的桶名(带 缓存+懒加载+双重锁 保障)
|
||||
* 供业务层上传时调用
|
||||
*/
|
||||
public String getCurrentTenantBucketName() {
|
||||
Long tenantId = ThreadLocalContext.getTenantId();
|
||||
|
||||
if (tenantId == null) {
|
||||
log.error("严重错误:尝试进行 MinIO 操作但缺失租户上下文!");
|
||||
throw new RuntimeException("系统内部错误:租户上下文缺失,操作被拒绝。");
|
||||
}
|
||||
|
||||
// 2. 拼接目标桶名,例如: spdm-1001
|
||||
String bucketName = minioConfig.getSpdmBucket() + "-" + tenantId;
|
||||
|
||||
// 3. 第一层保障:查本地缓存 (性能极高,无网络IO)
|
||||
if (existingBucketsCache.contains(bucketName)) {
|
||||
return bucketName;
|
||||
}
|
||||
|
||||
// 4. 第二层保障:惰性创建 (防止初始化失败或运行时新增租户)
|
||||
// 使用 intern() 锁住字符串,防止同一个租户并发创建,但不阻塞其他租户
|
||||
synchronized (bucketName.intern()) {
|
||||
// 双重检查
|
||||
if (existingBucketsCache.contains(bucketName)) {
|
||||
return bucketName;
|
||||
}
|
||||
try {
|
||||
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
|
||||
if (!found) {
|
||||
log.info("运行时检测到新租户桶不存在,正在创建: {}", bucketName);
|
||||
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
|
||||
}
|
||||
existingBucketsCache.add(bucketName);
|
||||
} catch (Exception e) {
|
||||
log.error("运行时初始化租户桶失败: {}", bucketName, e);
|
||||
throw new RuntimeException("文件存储初始化失败,请联系管理员", e);
|
||||
}
|
||||
}
|
||||
return bucketName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public String getSecretBusinessBucketName() {
|
||||
return minioConfig.getSecretBusinessBucket();
|
||||
}
|
||||
@@ -99,7 +181,7 @@ public class MinioService implements IMinioService {
|
||||
// 使用空内容创建目录对象
|
||||
ObjectWriteResponse objectWriteResponse = minioClient.putObject(
|
||||
PutObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.object(objectKey)
|
||||
.stream(new ByteArrayInputStream(new byte[0]), 0, -1)
|
||||
.build());
|
||||
@@ -108,14 +190,6 @@ public class MinioService implements IMinioService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建目录(默认使用spdmBucket)
|
||||
*
|
||||
* @param objectKey 绝对路径目录名
|
||||
*/
|
||||
public void createDirectoryByObjectKey(String objectKey) {
|
||||
createDirectoryByObjectKey(objectKey, minioConfig.getSpdmBucket());
|
||||
}
|
||||
|
||||
private static String dealDirPath(String dirPath) {
|
||||
if (dirPath.startsWith("/")) {
|
||||
@@ -143,7 +217,7 @@ public class MinioService implements IMinioService {
|
||||
// 列出目录下的所有对象
|
||||
Iterable<Result<Item>> results = minioClient.listObjects(
|
||||
ListObjectsArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.prefix(directoryName)
|
||||
.recursive(true)
|
||||
.build());
|
||||
@@ -163,7 +237,7 @@ public class MinioService implements IMinioService {
|
||||
|
||||
Iterable<Result<DeleteError>> deleteErrors = minioClient.removeObjects(
|
||||
RemoveObjectsArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.objects(deleteObjects)
|
||||
.build());
|
||||
|
||||
@@ -179,31 +253,19 @@ public class MinioService implements IMinioService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归删除指定目录下的所有对象(默认使用spdmBucket)。
|
||||
*
|
||||
* @param directoryName 目录名称
|
||||
*/
|
||||
public void deleteDirectoryRecursively(String directoryName) {
|
||||
deleteDirectoryRecursively(directoryName, minioConfig.getSpdmBucket());
|
||||
}
|
||||
|
||||
public void deleteDirectoryRecursively2(String directoryName,String bucketName) {
|
||||
deleteDirectoryRecursively(directoryName, bucketName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Result<Item>> listObjects(String directoryName) {
|
||||
public Iterable<Result<Item>> listObjects(String directoryName, String bucketName) {
|
||||
// 列出目录下的所有对象
|
||||
return minioClient.listObjects(
|
||||
ListObjectsArgs.builder()
|
||||
.bucket(minioConfig.getSpdmBucket())
|
||||
.bucket(getBucketName(bucketName))
|
||||
.prefix(directoryName)
|
||||
.recursive(true)
|
||||
.build());
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从MinIO删除文件
|
||||
* @param minioObjectKey 文件名
|
||||
@@ -215,7 +277,7 @@ public class MinioService implements IMinioService {
|
||||
minioObjectKey = dealDirPath(minioObjectKey);
|
||||
minioClient.removeObject(
|
||||
RemoveObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.object(minioObjectKey)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
@@ -223,13 +285,6 @@ public class MinioService implements IMinioService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MinIO删除文件(默认使用spdmBucket)
|
||||
*/
|
||||
public void deleteFile(String minioObjectKey) {
|
||||
deleteFile(minioObjectKey, minioConfig.getSpdmBucket());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到MinIO
|
||||
* @param file 文件对象
|
||||
@@ -240,7 +295,7 @@ public class MinioService implements IMinioService {
|
||||
public void uploadFile(MultipartFile file, String objectName, Map<String, String> tags, String bucketName) {
|
||||
try {
|
||||
// 检查存储桶是否存在,不存在则创建(为了向后兼容,仍然保留此处检查)
|
||||
createBucketIfNotExists(bucketName);
|
||||
createBucketIfNotExists(getBucketName(bucketName));
|
||||
|
||||
// 转换标签为MinIO格式
|
||||
Map<String, String> tagMap = tags != null ? tags : Map.of();
|
||||
@@ -249,7 +304,7 @@ public class MinioService implements IMinioService {
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
minioClient.putObject(
|
||||
PutObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.object(objectName)
|
||||
.stream(inputStream, file.getSize(), -1)
|
||||
.contentType(file.getContentType())
|
||||
@@ -262,13 +317,6 @@ public class MinioService implements IMinioService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到MinIO(默认使用spdmBucket)
|
||||
*/
|
||||
public void uploadFile(MultipartFile file, String objectName, Map<String, String> tags) {
|
||||
uploadFile(file, objectName, tags, minioConfig.getSpdmBucket());
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名MinIO中的文件
|
||||
*
|
||||
@@ -279,6 +327,7 @@ public class MinioService implements IMinioService {
|
||||
*/
|
||||
public void renameFile(String oldObjectName, String newObjectName, String bucketName) {
|
||||
try {
|
||||
bucketName =getBucketName(bucketName);
|
||||
// 参数校验
|
||||
if (!StringUtils.hasText(oldObjectName)) {
|
||||
throw new IllegalArgumentException("原文件名不能为空");
|
||||
@@ -319,17 +368,6 @@ public class MinioService implements IMinioService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名MinIO中的文件(默认使用spdmBucket)
|
||||
*
|
||||
* @param oldObjectName 原文件名
|
||||
* @param newObjectName 新文件名
|
||||
* @throws Exception 异常信息
|
||||
*/
|
||||
public void renameFile(String oldObjectName, String newObjectName) {
|
||||
renameFile(oldObjectName, newObjectName, minioConfig.getSpdmBucket());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从MinIO下载文件
|
||||
@@ -340,38 +378,26 @@ public class MinioService implements IMinioService {
|
||||
|
||||
try (InputStream stream = minioClient.getObject(
|
||||
GetObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.object(objectName)
|
||||
.build())) {
|
||||
return IOUtils.toByteArray(stream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MinIO下载文件(默认使用spdmBucket)
|
||||
*/
|
||||
public byte[] downloadFile(String objectName) throws Exception {
|
||||
return downloadFile(objectName, minioConfig.getSpdmBucket());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void downloadFile(String objectName, HttpServletResponse response,String encodedFileName,Boolean preview,String contentType) throws Exception {
|
||||
downloadFile(objectName, minioConfig.getSpdmBucket(),response,encodedFileName,preview,contentType);
|
||||
}
|
||||
|
||||
// 流式响应的返回
|
||||
public void downloadFile(String objectName, String bucketName, HttpServletResponse response,String encodedFileName,Boolean preview, String contentType) throws Exception {
|
||||
// 1. 获取文件元数据(大小)
|
||||
StatObjectResponse stat = minioClient.statObject(
|
||||
StatObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.object(objectName)
|
||||
.build());
|
||||
long fileSize = stat.size();
|
||||
// 1. 获取MinIO的流式数据(使用try-with-resources确保资源释放)
|
||||
try (InputStream stream = minioClient.getObject(
|
||||
GetObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.object(objectName)
|
||||
.build());
|
||||
ReadableByteChannel inChannel = Channels.newChannel(stream);
|
||||
@@ -402,16 +428,11 @@ public class MinioService implements IMinioService {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public InputStream getMinioInputStream(String objectName) {
|
||||
return getMinioInputStream(objectName, minioConfig.getSpdmBucket());
|
||||
}
|
||||
|
||||
public InputStream getMinioInputStream(String objectName, String bucketName) {
|
||||
try {
|
||||
return minioClient.getObject(
|
||||
GetObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.object(objectName)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
@@ -429,7 +450,7 @@ public class MinioService implements IMinioService {
|
||||
List<String> result = new ArrayList<>();
|
||||
Iterable<Result<Item>> results = minioClient.listObjects(
|
||||
ListObjectsArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.build());
|
||||
|
||||
for (Result<Item> itemResult : results) {
|
||||
@@ -441,12 +462,6 @@ public class MinioService implements IMinioService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标签查询文件(默认使用spdmBucket)
|
||||
*/
|
||||
public List<String> queryFilesByTags(Map<String, String> tags) throws Exception {
|
||||
return queryFilesByTags(tags, minioConfig.getSpdmBucket());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件标签是否匹配查询条件
|
||||
@@ -556,18 +571,12 @@ public class MinioService implements IMinioService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public String getMinioPresignedUrl(String objectKey) {
|
||||
return getMinioPresignedUrl(objectKey, minioConfig.getSpdmBucket());
|
||||
}
|
||||
|
||||
public String getMinioPresignedUrl(String objectKey, String bucketName) {
|
||||
String presignedUrl = null;
|
||||
try {
|
||||
// 生成预签名URL
|
||||
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(objectKey).expiry(3600, TimeUnit.SECONDS).build();
|
||||
GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(getBucketName(bucketName)).object(objectKey).expiry(3600, TimeUnit.SECONDS).build();
|
||||
presignedUrl = minioClient.getPresignedObjectUrl(args);
|
||||
} catch (Exception e) {
|
||||
log.error("获取文件预览URL失败: " + objectKey, e);
|
||||
@@ -591,7 +600,7 @@ public class MinioService implements IMinioService {
|
||||
try {
|
||||
inputStream = file.getInputStream();
|
||||
minioClient.putObject(PutObjectArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.object(fileName)
|
||||
.stream(inputStream, file.getSize(), -1)
|
||||
.build());
|
||||
@@ -624,7 +633,7 @@ public class MinioService implements IMinioService {
|
||||
public Boolean merge(String tempBucketName,String tempFilePath,String mergeBucketName,String fileName) {
|
||||
try {
|
||||
List<ComposeSource> sourceObjectList = new ArrayList<ComposeSource>();
|
||||
List<Object> folderList = getFolderList(tempBucketName,tempFilePath);
|
||||
List<Object> folderList = getFolderList(getBucketName(tempBucketName),tempFilePath);
|
||||
List<String> fileNames = new ArrayList<>();
|
||||
if (!folderList.isEmpty()) {
|
||||
for (Object value : folderList) {
|
||||
@@ -640,12 +649,12 @@ public class MinioService implements IMinioService {
|
||||
return Integer.compare(num1, num2);
|
||||
});
|
||||
for (String name : fileNames) {
|
||||
sourceObjectList.add(ComposeSource.builder().bucket(tempBucketName).object(name).build());
|
||||
sourceObjectList.add(ComposeSource.builder().bucket(getBucketName(tempBucketName)).object(name).build());
|
||||
}
|
||||
}
|
||||
minioClient.composeObject(
|
||||
ComposeObjectArgs.builder()
|
||||
.bucket(mergeBucketName)
|
||||
.bucket(getBucketName(mergeBucketName))
|
||||
.object(fileName)
|
||||
.sources(sourceObjectList)
|
||||
.build());
|
||||
@@ -674,7 +683,7 @@ public class MinioService implements IMinioService {
|
||||
// 2. 调用 listObjects,使用规范化后的 prefix
|
||||
Iterable<Result<Item>> results = minioClient.listObjects(
|
||||
ListObjectsArgs.builder()
|
||||
.bucket(bucketName)
|
||||
.bucket(getBucketName(bucketName))
|
||||
.prefix(prefix) // 关键:使用规范化后的前缀
|
||||
.recursive(false) // 只查当前前缀的直接子对象(即“当前目录”下的文件)
|
||||
.build()
|
||||
@@ -739,5 +748,10 @@ public class MinioService implements IMinioService {
|
||||
}
|
||||
|
||||
|
||||
private String getBucketName(String bucketName) {
|
||||
return StringUtils.hasText(bucketName) ? bucketName : getCurrentTenantBucketName();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.sdm.data.service.minio;
|
||||
|
||||
import com.sdm.common.common.SdmResponse;
|
||||
import com.sdm.common.entity.req.data.TenantListReq;
|
||||
import com.sdm.common.entity.resp.PageDataResp;
|
||||
import com.sdm.common.entity.resp.system.TenantResp;
|
||||
import com.sdm.common.feign.inter.system.ISysTenantFeignClient;
|
||||
import com.sdm.common.feign.inter.system.ISysUserFeignClient;
|
||||
import com.sdm.data.config.MinioConfig;
|
||||
import com.sdm.data.service.IDataFileService;
|
||||
import com.sdm.data.service.IMinioService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class MinioTenantInitializer implements CommandLineRunner {
|
||||
@Autowired
|
||||
private IMinioService minioService;
|
||||
@Autowired
|
||||
private MinioConfig minioConfig;
|
||||
@Autowired
|
||||
private ISysTenantFeignClient sysTenantFeignClient;
|
||||
|
||||
@Autowired
|
||||
private IDataFileService dataFileService;
|
||||
|
||||
// 配置重试策略
|
||||
private static final int MAX_RETRIES = 12; // 最多重试12次
|
||||
private static final int RETRY_DELAY_SECONDS = 5; // 每次间隔5秒,共覆盖60秒启动延迟
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
// 【关键】使用异步线程执行,绝对不要阻塞 Data 服务的主线程启动
|
||||
CompletableFuture.runAsync(this::initBuckets);
|
||||
}
|
||||
|
||||
private void initBuckets() {
|
||||
log.info(">>> [MinIO Bucket Init] 开始异步初始化租户桶...");
|
||||
|
||||
List<Long> allTenantIds = null;
|
||||
int attempt = 0;
|
||||
|
||||
// 1. 循环重试获取租户信息 (解决 Feign 调用失败问题)
|
||||
while (attempt < MAX_RETRIES) {
|
||||
try {
|
||||
// 调用 Feign 接口 (建议 System 服务提供一个只查 ID 的轻量接口)
|
||||
TenantListReq req = new TenantListReq();
|
||||
req.setCurrent(1);
|
||||
req.setSize(1000);
|
||||
SdmResponse<PageDataResp<List<TenantResp>>> response = sysTenantFeignClient.listTenant(req);
|
||||
log.info(">>> [MinIO Bucket Init] 获取租户列表成功,共有 {} 个租户", response.getData().getTotal());
|
||||
|
||||
if (response != null && response.isSuccess() && response.getData() != null && response.getData().getData() != null) {
|
||||
allTenantIds = response.getData().getData().stream()
|
||||
.map(TenantResp::getTenantId)
|
||||
.toList();
|
||||
log.info(">>> [MinIO Bucket Init] 成功连接 System 服务,获取到 {} 个租户", allTenantIds.size());
|
||||
break;
|
||||
} else {
|
||||
log.warn(">>> [MinIO Bucket Init] System 服务返回数据为空或异常");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
attempt++;
|
||||
log.warn(">>> [MinIO Bucket Init] 连接 System 服务失败 (第 {}/{} 次),{} 秒后重试...",
|
||||
attempt, MAX_RETRIES, RETRY_DELAY_SECONDS);
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(RETRY_DELAY_SECONDS);
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果 System 服务彻底挂了,放弃初始化,依靠 MinioService 的运行时懒加载兜底
|
||||
if (allTenantIds == null) {
|
||||
log.error(">>> [MinIO Bucket Init] 放弃初始化。系统将依赖 '运行时懒加载' 机制自动创建缺失的桶。");
|
||||
// 这里仅仅是预热失败,不抛异常,不影响 Data 服务运行
|
||||
return;
|
||||
}
|
||||
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
// 3. 预热缓存并获取 MinIO 现有桶
|
||||
Set<String> existingBuckets = minioService.preloadBucketCache();
|
||||
|
||||
// 4. 计算预期应该存在的桶 (spdm-1001, spdm-1002...)
|
||||
String baseBucket = minioConfig.getSpdmBucket();
|
||||
Set<String> requiredBuckets = allTenantIds.stream()
|
||||
.map(id -> baseBucket + "-" + id)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// 5. 求差集:需要创建 = 理论存在 - 实际存在
|
||||
requiredBuckets.removeAll(existingBuckets);
|
||||
|
||||
// 6. 补齐缺失 (使用并行流加快速度)
|
||||
if (!requiredBuckets.isEmpty()) {
|
||||
log.info(">>> [MinIO Bucket Init] 发现 {} 个缺失的租户桶,开始补齐...", requiredBuckets.size());
|
||||
requiredBuckets.parallelStream().forEach(bucketName -> {
|
||||
minioService.createBucketIfAbsent(bucketName);
|
||||
});
|
||||
} else {
|
||||
log.info(">>> [MinIO Bucket Init] 桶状态一致,无需补齐。");
|
||||
}
|
||||
|
||||
// 7. 确保每个租户的 DB 和 MinIO 目录结构都已初始化
|
||||
log.info(">>> [Tenant Init] 开始检查 {} 个租户的基础目录结构...", allTenantIds.size());
|
||||
|
||||
// 使用并行流加速(如果有几千个租户,串行会很慢)
|
||||
allTenantIds.parallelStream().forEach(tenantId -> {
|
||||
try {
|
||||
dataFileService.initTenantData(tenantId);
|
||||
} catch (Exception e) {
|
||||
log.error(">>> [Tenant Init] 租户 {} 初始化失败", tenantId, e);
|
||||
}
|
||||
});
|
||||
|
||||
log.info(">>> [MinIO Bucket Init] 初始化流程结束,耗时: {} ms", System.currentTimeMillis() - start);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user