minio基于用户租户的数据桶隔离

This commit is contained in:
2025-12-16 15:18:31 +08:00
parent c0c7a0144c
commit 1447eb8105
21 changed files with 1173 additions and 1005 deletions

View File

@@ -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());
}
}
}

View File

@@ -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);
}
}

View File

@@ -140,6 +140,10 @@ public interface IDataFileService {
boolean initSystemDirectory(String company);
default boolean initTenantData(Long tenantId){
return true;
}
/**
* 下载文件

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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()));

View File

@@ -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()));

View File

@@ -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());
});
// 删除数据库记录

View File

@@ -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();
}
}

View File

@@ -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);
}
}