新增:minio分片上传实现;各服务数据库链接池配置优化

This commit is contained in:
yangyang01000846
2025-11-21 16:34:51 +08:00
parent 03c483207c
commit 44e1928113
25 changed files with 514 additions and 91 deletions

View File

@@ -2,9 +2,9 @@ package com.sdm.data.controller;
import com.sdm.common.common.SdmResponse;
import com.sdm.common.entity.req.data.*;
import com.sdm.common.entity.resp.PageDataResp;
import com.sdm.data.model.req.RenameFileReq;
import com.sdm.common.entity.req.system.LaunchApproveReq;
import com.sdm.common.entity.resp.PageDataResp;
import com.sdm.common.entity.resp.data.ChunkUploadMinioFileResp;
import com.sdm.common.entity.resp.data.FileMetadataInfoResp;
import com.sdm.common.feign.inter.data.IDataFeignClient;
import com.sdm.data.model.entity.FileMetadataInfo;
@@ -383,5 +383,18 @@ public class DataFileController implements IDataFeignClient {
return IDataFileService.queryFileMetadataInfo(uuid, uuidOwnType);
}
/**
* 分片上传文件到minio
*
* @param req
* @return SdmResponse
*/
@PostMapping("/chunkUploadToMinio")
@Operation(summary = "文件分片上传到minio")
public SdmResponse<ChunkUploadMinioFileResp> chunkUploadToMinio(ChunkUploadMinioFileReq req) {
return IDataFileService.chunkUploadToMinio(req);
}
}

View File

@@ -2,11 +2,11 @@ package com.sdm.data.service;
import com.sdm.common.common.SdmResponse;
import com.sdm.common.entity.req.data.*;
import com.sdm.common.entity.resp.PageDataResp;
import com.sdm.data.model.entity.FileMetadataInfo;
import com.sdm.data.model.req.RenameFileReq;
import com.sdm.common.entity.req.system.LaunchApproveReq;
import com.sdm.common.entity.resp.PageDataResp;
import com.sdm.common.entity.resp.data.ChunkUploadMinioFileResp;
import com.sdm.common.entity.resp.data.FileMetadataInfoResp;
import com.sdm.data.model.entity.FileMetadataInfo;
import com.sdm.data.model.req.*;
import com.sdm.data.model.resp.KKFileViewURLFromMinioResp;
import jakarta.servlet.http.HttpServletResponse;
@@ -315,4 +315,6 @@ public interface IDataFileService {
SdmResponse<FileMetadataInfoResp> queryFileMetadataInfo(String uuid, String uuidOwnType);
SdmResponse<ChunkUploadMinioFileResp> chunkUploadToMinio(ChunkUploadMinioFileReq req);
}

View File

@@ -130,4 +130,8 @@ public interface IMinioService {
String getMinioPresignedUrl(String objectKey);
String getMinioPresignedUrl(String objectKey, String bucketName);
Boolean chunkUpload(String bucketName, MultipartFile file, String fileName);
Boolean merge(String tempBucketName,String tempFilePath,String mergeBucketName,String fileName);
}

View File

@@ -8,6 +8,7 @@ import com.github.pagehelper.PageInfo;
import com.sdm.common.common.SdmResponse;
import com.sdm.common.common.ThreadLocalContext;
import com.sdm.common.entity.constants.NumberConstants;
import com.sdm.common.entity.constants.PermConstants;
import com.sdm.common.entity.enums.*;
import com.sdm.common.entity.req.data.*;
import com.sdm.common.entity.req.project.SpdmNodeListReq;
@@ -15,6 +16,7 @@ import com.sdm.common.entity.req.system.LaunchApproveReq;
import com.sdm.common.entity.req.system.UserListReq;
import com.sdm.common.entity.req.system.UserQueryReq;
import com.sdm.common.entity.resp.PageDataResp;
import com.sdm.common.entity.resp.data.ChunkUploadMinioFileResp;
import com.sdm.common.entity.resp.data.FileMetadataInfoResp;
import com.sdm.common.entity.resp.project.SimulationNodeResp;
import com.sdm.common.entity.resp.system.CIDUserResp;
@@ -51,6 +53,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
@@ -86,6 +89,13 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
@Value("${fileSystem.minio}")
private String type;
@Value("${fileSystem.chunkBucket:spdm}")
private String chunkBucket;
// 路径待确定 待配置及初始化
@Value("${fileSystem.chunkBasePath:/chunkBase}")
private String chunkBasePath;
@Autowired
private IFileMetadataInfoService fileMetadataInfoService;
@@ -201,6 +211,59 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
return SdmResponse.success(dto);
}
@Override
public SdmResponse<ChunkUploadMinioFileResp> chunkUploadToMinio(ChunkUploadMinioFileReq req) {
ChunkUploadMinioFileResp resp = new ChunkUploadMinioFileResp();
// 基础路径配置
// Long tenantId = ThreadLocalContext.getTenantId();
Long tenantId = 123456l;
Long userId = 9999l;
// Long userId= ThreadLocalContext.getUserId();
// -2. 参数校验
try {
validateReq(req,tenantId,userId);
} catch (Exception e) {
CoreLogger.error("validateReq error{}", e.getMessage());
return buildFailedResponse(resp,e.getMessage(),"",chunkBucket);
}
// -1.确定文件夹
String timestamp = String.valueOf(System.currentTimeMillis());
// 合并目录
String filePath = org.apache.commons.lang3.StringUtils.isNotBlank(req.getFileDirPath())?
req.getFileDirPath():chunkBasePath + "/" + tenantId + "/" + userId +"/" + timestamp + "/";
// 0. 一个文件直接传
if(Objects.equals(req.getChunkTotal(),NumberConstants.ONE)&&
Objects.equals(req.getChunk(),NumberConstants.ONE)){
String finalFileName = filePath + req.getSourceFileName();
Boolean b = minioService.chunkUpload(chunkBucket, req.getFile(), finalFileName);
if(!b){
return buildFailedResponse(resp,"单一文件上传失败",filePath,chunkBucket);
}
return buildSuccessResponse(resp,finalFileName,filePath,chunkBucket);
}
String tempDirPath = filePath +"temp/";
// 1. 保存当前分片到临时目录 1 2 3 4 ....temp
String chunkFileName =tempDirPath+req.getChunk()+ PermConstants.CHUNK_TEMPFILE_SUFFIX;
// 片文件上传到minio
Boolean b = minioService.chunkUpload(chunkBucket, req.getFile(), chunkFileName);
if(!b){
return buildFailedResponse(resp,"chunkUpload第"+req.getChunk()+"次失败",filePath,chunkBucket);
}
// 2. 判断分片是否已全部上传完毕
if (req.getChunk() < req.getChunkTotal()) {
return buildSuccessResponse(resp,"",filePath,chunkBucket);
}
// 3. 全部分片已经上传 => 自动合并
String finalFileName = filePath + req.getSourceFileName();
Boolean merge = minioService.merge(chunkBucket, tempDirPath, chunkBucket, finalFileName);
if(!merge){
return buildFailedResponse(resp,req.getSourceFileName()+"合并分片失败",filePath,chunkBucket);
}
// 4. 合并完成后删除临时目录 todo
return buildSuccessResponse(resp,finalFileName,filePath,chunkBucket);
}
/**
* 校验审批回调请求参数的合法性
* @param req 审批回调请求对象
@@ -1934,4 +1997,44 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService {
}
}
/**
* 参数校验
*/
private void validateReq(ChunkUploadMinioFileReq req, Long tenantId, Long userId) {
// 基础参数校验
Assert.notNull(tenantId, "租户ID不能为空");
Assert.notNull(userId, "用户ID不能为空");
Assert.hasText(req.getSourceFileName(), "原始文件名称不能为空");
Assert.notNull(req.getChunk(), "分片编号不能为空");
Assert.notNull(req.getChunkTotal(), "分片总数不能为空");
Assert.notNull(req.getFile(), "分片文件不能为空");
Assert.isTrue(!req.getFile().isEmpty(), "分片文件不能为空");
// 业务逻辑校验
Assert.isTrue(req.getChunk() >= 1 && req.getChunk() <= req.getChunkTotal(),
"分片编号非法:当前" + req.getChunk() + ",总分片" + req.getChunkTotal());
Assert.isTrue(req.getChunkTotal() >= 1, "分片总数必须大于等于1");
if(req.getChunk() > NumberConstants.ONE) {
Assert.hasText(req.getFileDirPath(), "分片父级目录不允许为空");
}
}
// 包含业务数据的响应体
private SdmResponse<ChunkUploadMinioFileResp> buildSuccessResponse(ChunkUploadMinioFileResp resp, String finalFileName, String fileDirPath,String chunkBucket) {
resp.setBucketName(chunkBucket);
resp.setFilePath(finalFileName);
resp.setFileDirPath(fileDirPath);
// 成功时,错误信息通常为空
resp.setErrMsg("");
return SdmResponse.success(resp);
}
// 构建一个失败的响应对象
private SdmResponse<ChunkUploadMinioFileResp> buildFailedResponse(ChunkUploadMinioFileResp resp, String errMsg, String fileDirPath,String chunkBucket) {
resp.setBucketName(chunkBucket);
resp.setFileDirPath(fileDirPath);
// 如果 errMsg 为 null设置为空字符串 ""
resp.setErrMsg(errMsg != null ? errMsg : "");
return SdmResponse.failed(resp);
}
}

View File

@@ -2,20 +2,16 @@ package com.sdm.data.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.sdm.common.common.ResultCode;
import com.sdm.common.common.SdmResponse;
import com.sdm.common.common.SdmIterator;
import com.sdm.common.common.SdmResponse;
import com.sdm.common.common.ThreadLocalContext;
import com.sdm.common.entity.constants.PermConstants;
import com.sdm.common.entity.data.*;
import com.sdm.common.entity.enums.UserRole;
import com.sdm.common.entity.pojo.system.SysCompany;
import com.sdm.common.entity.pojo.system.SysUserInfo;
import com.sdm.common.entity.req.data.CreateDirReq;
import com.sdm.common.entity.req.data.DelDirReq;
import com.sdm.common.entity.req.data.QueryDirReq;
import com.sdm.common.entity.req.data.UploadFilesReq;
import com.sdm.common.entity.req.data.*;
import com.sdm.common.entity.resp.data.FileMetadataInfoResp;
import com.sdm.data.model.req.RenameFileReq;
import com.sdm.common.service.CommonService;
import com.sdm.common.utils.*;
import com.sdm.data.dao.DataMapper;
@@ -1283,6 +1279,12 @@ public class SystemFileIDataFileServiceImpl implements IDataFileService {
return null;
}
@Override
public SdmResponse chunkUploadToMinio(ChunkUploadMinioFileReq req) {
// 系统盘分片存储暂时不支持
return null;
}
@Override
public void downloadFile(DownloadFileReq req, HttpServletResponse response) {
if (StringUtils.isNotBlank(req.getPath()) && req.getPath().contains("..")) {

View File

@@ -1,5 +1,7 @@
package com.sdm.data.service.minio;
import com.alibaba.fastjson2.JSON;
import com.sdm.common.log.CoreLogger;
import com.sdm.data.config.MinioConfig;
import com.sdm.data.service.IMinioService;
import io.minio.*;
@@ -21,7 +23,9 @@ import java.io.IOException;
import java.io.InputStream;
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.concurrent.TimeUnit;
@@ -423,4 +427,169 @@ public class MinioService implements IMinioService {
}
return presignedUrl;
}
/**
* description: 分片碎文件上传
*
* @param bucketName 桶名称
* @param file 文件
* @param fileName /xx/xx/文件名,数字类型1 2 3 ... .temp
* @author bo
* @date 2023/5/21 13:06
*/
@Override
public Boolean chunkUpload(String bucketName, MultipartFile file, String fileName) {
InputStream inputStream = null;
try {
inputStream = file.getInputStream();
minioClient.putObject(PutObjectArgs.builder()
.bucket(bucketName)
.object(fileName)
.stream(inputStream, file.getSize(), -1)
.build());
return true;
} catch (Exception e) {
CoreLogger.error("chunkUpload error:{}",e.getMessage());
return false;
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (Exception e) {
CoreLogger.error("chunkUpload finally error:{}",e.getMessage());
}
}
}
}
// 分片上传合并
/**
* 讲快文件合并到新桶 块文件必须满足 名字是 0 1 2 3 5....
*
* @param tempBucketName 存块文件的桶
* @param tempFilePath 存块文件的文件夹路径
* @param mergeBucketName 存新文件的桶
* @param fileName 存到新桶中的文件名称
* @return boolean
*/
@Override
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<String> fileNames = new ArrayList<>();
if (!folderList.isEmpty()) {
for (Object value : folderList) {
Map o = (Map) value;
String name = (String) o.get("fileName");
fileNames.add(name);
}
}
if (!fileNames.isEmpty()) {
fileNames.sort((fileName1, fileName2) -> {
int num1 = getFileNumber(fileName1);
int num2 = getFileNumber(fileName2);
return Integer.compare(num1, num2);
});
for (String name : fileNames) {
sourceObjectList.add(ComposeSource.builder().bucket(tempBucketName).object(name).build());
}
}
minioClient.composeObject(
ComposeObjectArgs.builder()
.bucket(mergeBucketName)
.object(fileName)
.sources(sourceObjectList)
.build());
return true;
} catch (Exception e) {
CoreLogger.error("merge error:{},fileName:{}",e.getMessage(),fileName);
return false;
}
}
public List<Object> getFolderList(String bucketName, String tempFilePath) {
List<Object> items = new ArrayList<>();
if(org.apache.commons.lang3.StringUtils.isBlank(tempFilePath)){
CoreLogger.warn("getFolderList tempFilePath null");
return items;
}
try {
// 1. 规范化 prefix去除开头的 /,目录路径末尾补 /(根目录除外)
String prefix = tempFilePath;
// 去除开头的 /
prefix = prefix.replaceAll("^/", "");
// 如果不是空字符串(根目录),且末尾没有 /,则补 /
if (!prefix.isEmpty() && !prefix.endsWith("/")) {
prefix += "/";
}
// 2. 调用 listObjects使用规范化后的 prefix
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(bucketName)
.prefix(prefix) // 关键:使用规范化后的前缀
.recursive(false) // 只查当前前缀的直接子对象(即“当前目录”下的文件)
.build()
);
Iterator<Result<Item>> iterator = results.iterator();
items = new ArrayList<>();
String format = "{'fileName':'%s','fileSize':'%s'}";
while (iterator.hasNext()) {
Item item = iterator.next().get();
// 注意item.objectName() 是完整的对象键(比如 a/b/c/1.txt如果需要相对路径可做截取
items.add(JSON.parse((String.format(format, item.objectName(),
formatFileSize(item.size())))));
}
} catch (Exception e) {
CoreLogger.error("getFolderList error:{},tempFilePath:{}",e.getMessage(),tempFilePath);
}
return items;
}
/**
* description: 格式化文件大小
*
* @param fileS 文件的字节长度
* @author bo
* @date 2023/5/21 11:40
*/
private static String formatFileSize(long fileS) {
DecimalFormat df = new DecimalFormat("#.00");
String fileSizeString = "";
String wrongSize = "0B";
if (fileS == 0) {
return wrongSize;
}
if (fileS < 1024) {
fileSizeString = df.format((double) fileS) + " B";
} else if (fileS < 1048576) {
fileSizeString = df.format((double) fileS / 1024) + " KB";
} else if (fileS < 1073741824) {
fileSizeString = df.format((double) fileS / 1048576) + " MB";
} else {
fileSizeString = df.format((double) fileS / 1073741824) + " GB";
}
return fileSizeString;
}
/**
* 从固定格式的文件名中截取数字。
* 格式必须是:.../数字.temp
*/
private static int getFileNumber(String fileName) {
try {
int lastSlashIndex = fileName.lastIndexOf('/');
int dotIndex = fileName.lastIndexOf('.');
// 截取数字部分的字符串
String numberStr = fileName.substring(lastSlashIndex + 1, dotIndex);
// 转换为整数
return Integer.parseInt(numberStr);
} catch (Exception e) {
CoreLogger.error("getFileNumber error:{},fileName:{}",e.getMessage(),fileName);
throw new RuntimeException("临时文件名格式非数字.temp类型");
}
}
}

View File

@@ -10,11 +10,16 @@ spring:
jdbc-url: jdbc:mysql://192.168.65.161:3306/spdm_baseline?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 450 # 连接池最大连接数(关键!)
minimum-idle: 50 # 最小空闲连接数(与最大一致,避免频繁创建销毁)
idle-timeout: 300000 # 空闲连接超时时间5分钟
max-lifetime: 600000 # 连接最大存活时间10分钟
connection-timeout: 30000 # 获取连接超时时间30秒避免线程阻塞
# 设置连接池能够容纳的最大连接数。建议值CPU核心数 * 2 + 有效磁盘I/O数。一个常见的经验值是 10-20。
maximum-pool-size: 20
# 连接池在空闲时保持的最小连接数。
minimum-idle: 5
# 一个连接在被标记为空闲之前可以保持空闲状态的最长时间(毫秒)。当连接的空闲时间超过此值后,它可能会被连接池 evict驱逐
idle-timeout: 60000 # 1 min
# 一个连接从被创建开始其生命周期的最大时长毫秒。HikariCP的默认值就是30分钟这是一个非常合理的设置。
max-lifetime: 1800000 # 30 minHikari 默认)
# 应用程序尝试从连接池获取一个连接时等待的最长时间毫秒。建议值30-60秒。
connection-timeout: 30000 # 30s
master:
username: root
password: mysql

View File

@@ -10,11 +10,16 @@ spring:
jdbc-url: jdbc:mysql://192.168.65.161:3306/spdm_baseline?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 450 # 连接池最大连接数(关键!)
minimum-idle: 50 # 最小空闲连接数(与最大一致,避免频繁创建销毁)
idle-timeout: 300000 # 空闲连接超时时间5分钟
max-lifetime: 600000 # 连接最大存活时间10分钟
connection-timeout: 30000 # 获取连接超时时间30秒避免线程阻塞
# 设置连接池能够容纳的最大连接数。建议值CPU核心数 * 2 + 有效磁盘I/O数。一个常见的经验值是 10-20。
maximum-pool-size: 20
# 连接池在空闲时保持的最小连接数。
minimum-idle: 5
# 一个连接在被标记为空闲之前可以保持空闲状态的最长时间(毫秒)。当连接的空闲时间超过此值后,它可能会被连接池 evict驱逐
idle-timeout: 60000 # 1 min
# 一个连接从被创建开始其生命周期的最大时长毫秒。HikariCP的默认值就是30分钟这是一个非常合理的设置。
max-lifetime: 1800000 # 30 minHikari 默认)
# 应用程序尝试从连接池获取一个连接时等待的最长时间毫秒。建议值30-60秒。
connection-timeout: 30000 # 30s
master:
username: root
password: mysql

View File

@@ -6,11 +6,16 @@ spring:
name: data
datasource:
hikari:
maximum-pool-size: 450 # 连接池最大连接数(关键!)
minimum-idle: 50 # 最小空闲连接数(与最大一致,避免频繁创建销毁)
idle-timeout: 300000 # 空闲连接超时时间5分钟
max-lifetime: 600000 # 连接最大存活时间10分钟
connection-timeout: 30000 # 获取连接超时时间30秒避免线程阻塞
# 设置连接池能够容纳的最大连接数。建议值CPU核心数 * 2 + 有效磁盘I/O数。一个常见的经验值是 10-20。
maximum-pool-size: 20
# 连接池在空闲时保持的最小连接数。
minimum-idle: 5
# 一个连接在被标记为空闲之前可以保持空闲状态的最长时间(毫秒)。当连接的空闲时间超过此值后,它可能会被连接池 evict驱逐
idle-timeout: 60000 # 1 min
# 一个连接从被创建开始其生命周期的最大时长毫秒。HikariCP的默认值就是30分钟这是一个非常合理的设置。
max-lifetime: 1800000 # 30 minHikari 默认)
# 应用程序尝试从连接池获取一个连接时等待的最长时间毫秒。建议值30-60秒。
connection-timeout: 30000 # 30s
master:
username: root
password: ENC(+QKYnI6gAYu1SbLaZQTkZA==)

View File

@@ -6,11 +6,16 @@ spring:
name: data
datasource:
hikari:
maximum-pool-size: 450 # 连接池最大连接数(关键!)
minimum-idle: 50 # 最小空闲连接数(与最大一致,避免频繁创建销毁)
idle-timeout: 300000 # 空闲连接超时时间5分钟
max-lifetime: 600000 # 连接最大存活时间10分钟
connection-timeout: 30000 # 获取连接超时时间30秒避免线程阻塞
# 设置连接池能够容纳的最大连接数。建议值CPU核心数 * 2 + 有效磁盘I/O数。一个常见的经验值是 10-20。
maximum-pool-size: 20
# 连接池在空闲时保持的最小连接数。
minimum-idle: 5
# 一个连接在被标记为空闲之前可以保持空闲状态的最长时间(毫秒)。当连接的空闲时间超过此值后,它可能会被连接池 evict驱逐
idle-timeout: 60000 # 1 min
# 一个连接从被创建开始其生命周期的最大时长毫秒。HikariCP的默认值就是30分钟这是一个非常合理的设置。
max-lifetime: 1800000 # 30 minHikari 默认)
# 应用程序尝试从连接池获取一个连接时等待的最长时间毫秒。建议值30-60秒。
connection-timeout: 30000 # 30s
master:
username: root
password: ENC(+QKYnI6gAYu1SbLaZQTkZA==)