|
|
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
import org.dromara.common.core.constant.CacheNames;
|
|
|
import org.dromara.common.core.domain.dto.OssDTO;
|
|
|
import org.dromara.common.core.exception.ServiceException;
|
|
|
@@ -23,6 +24,7 @@ import org.dromara.common.oss.core.OssClient;
|
|
|
import org.dromara.common.oss.entity.UploadResult;
|
|
|
import org.dromara.common.oss.enums.AccessPolicyType;
|
|
|
import org.dromara.common.oss.factory.OssFactory;
|
|
|
+import org.dromara.common.redis.utils.RedisUtils;
|
|
|
import org.dromara.system.domain.SysOss;
|
|
|
import org.dromara.system.domain.bo.SysOssBo;
|
|
|
import org.dromara.system.domain.vo.SysOssVo;
|
|
|
@@ -37,22 +39,30 @@ import org.springframework.web.multipart.MultipartFile;
|
|
|
import java.io.File;
|
|
|
import java.io.IOException;
|
|
|
import java.time.Duration;
|
|
|
-import java.util.ArrayList;
|
|
|
-import java.util.Collection;
|
|
|
-import java.util.List;
|
|
|
-import java.util.Map;
|
|
|
+import java.util.*;
|
|
|
|
|
|
/**
|
|
|
* 文件上传 服务层实现
|
|
|
*
|
|
|
* @author Lion Li
|
|
|
*/
|
|
|
+@Slf4j
|
|
|
@RequiredArgsConstructor
|
|
|
@Service
|
|
|
public class SysOssServiceImpl implements ISysOssService, OssService {
|
|
|
|
|
|
private final SysOssMapper baseMapper;
|
|
|
|
|
|
+ // 分片上传阈值:50MB
|
|
|
+ private static final long MULTIPART_UPLOAD_THRESHOLD = 50 * 1024 * 1024;
|
|
|
+
|
|
|
+ // 分片大小:8MB
|
|
|
+ private static final long PART_SIZE = 8 * 1024 * 1024;
|
|
|
+
|
|
|
+ // Redis键前缀
|
|
|
+ private static final String UPLOAD_PARTS_PREFIX = "oss:upload:parts:";
|
|
|
+ private static final String UPLOAD_INFO_PREFIX = "oss:upload:info:";
|
|
|
+
|
|
|
/**
|
|
|
* 查询OSS对象存储列表
|
|
|
*
|
|
|
@@ -181,6 +191,29 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
|
|
storage.download(sysOss.getFileName(), response.getOutputStream(), response::setContentLengthLong);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 生成预签名下载URL
|
|
|
+ *
|
|
|
+ * @param ossId OSS对象ID
|
|
|
+ * @param expirationSeconds 过期时间(秒)
|
|
|
+ * @return 预签名URL
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public String generatePresignedUrl(Long ossId, int expirationSeconds) {
|
|
|
+ SysOssVo sysOss = SpringUtils.getAopProxy(this).getById(ossId);
|
|
|
+ if (ObjectUtil.isNull(sysOss)) {
|
|
|
+ throw new ServiceException("文件数据不存在!");
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ OssClient storage = OssFactory.instance(sysOss.getService());
|
|
|
+ return storage.generatePresignedUrl(sysOss.getFileName(), expirationSeconds);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("生成预签名URL失败,ossId: {}", ossId, e);
|
|
|
+ throw new ServiceException("生成下载链接失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 上传 MultipartFile 到对象存储服务,并保存文件信息到数据库
|
|
|
*
|
|
|
@@ -195,7 +228,8 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
|
|
OssClient storage = OssFactory.instance();
|
|
|
UploadResult uploadResult;
|
|
|
try {
|
|
|
- uploadResult = storage.uploadSuffix(file.getBytes(), suffix, file.getContentType());
|
|
|
+ // 使用原始文件名上传,而不是UUID
|
|
|
+ uploadResult = storage.uploadWithOriginalName(file.getBytes(), originalfileName, file.getContentType());
|
|
|
} catch (IOException e) {
|
|
|
throw new ServiceException(e.getMessage());
|
|
|
}
|
|
|
@@ -214,7 +248,11 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
|
|
String originalfileName = file.getName();
|
|
|
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
|
|
|
OssClient storage = OssFactory.instance();
|
|
|
- UploadResult uploadResult = storage.uploadSuffix(file, suffix);
|
|
|
+
|
|
|
+ // 使用原始文件名上传,而不是UUID
|
|
|
+ String customPath = storage.getPathWithCustomName(originalfileName);
|
|
|
+ UploadResult uploadResult = storage.upload(file.toPath(), customPath, null, FileUtils.getMimeType(suffix));
|
|
|
+
|
|
|
// 保存文件信息
|
|
|
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult);
|
|
|
}
|
|
|
@@ -266,4 +304,252 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
|
|
}
|
|
|
return oss;
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 智能上传文件(根据文件大小自动选择直接上传或分片上传)
|
|
|
+ *
|
|
|
+ * @param file 要上传的文件对象
|
|
|
+ * @return 上传成功后的 SysOssVo 对象,包含文件信息
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public SysOssVo smartUpload(File file) {
|
|
|
+ if (file.length() > MULTIPART_UPLOAD_THRESHOLD) {
|
|
|
+ // 大文件使用分片上传
|
|
|
+ return uploadLargeFile(file);
|
|
|
+ } else {
|
|
|
+ // 小文件使用直接上传
|
|
|
+ return upload(file);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传大文件(分片上传)
|
|
|
+ */
|
|
|
+ private SysOssVo uploadLargeFile(File file) {
|
|
|
+ String originalfileName = file.getName();
|
|
|
+ String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
|
|
|
+
|
|
|
+ // 生成唯一的文件名,避免冲突
|
|
|
+ String fileName = generateUniqueFileName(suffix);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 初始化分片上传
|
|
|
+ String uploadId = initMultipartUpload(fileName, file.length());
|
|
|
+
|
|
|
+ // 计算分片数量
|
|
|
+ long fileSize = file.length();
|
|
|
+ int partCount = (int) Math.ceil((double) fileSize / PART_SIZE);
|
|
|
+
|
|
|
+ List<String> partETags = new ArrayList<>();
|
|
|
+
|
|
|
+ // 上传所有分片
|
|
|
+ try (java.io.FileInputStream fis = new java.io.FileInputStream(file)) {
|
|
|
+ for (int i = 1; i <= partCount; i++) {
|
|
|
+ byte[] partData = new byte[(int) Math.min(PART_SIZE, fileSize - (i - 1) * PART_SIZE)];
|
|
|
+ int bytesRead = fis.read(partData);
|
|
|
+
|
|
|
+ if (bytesRead > 0) {
|
|
|
+ String eTag = uploadPart(uploadId, i, partData);
|
|
|
+ partETags.add(eTag);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 完成分片上传
|
|
|
+ return completeMultipartUpload(uploadId, partETags, fileName, originalfileName);
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new ServiceException("分片上传失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 初始化分片上传
|
|
|
+ *
|
|
|
+ * @param fileName 文件名
|
|
|
+ * @param fileSize 文件大小
|
|
|
+ * @return 上传ID
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public String initMultipartUpload(String fileName, long fileSize) {
|
|
|
+ try {
|
|
|
+ OssClient storage = OssFactory.instance();
|
|
|
+
|
|
|
+ // 调用OSS的初始化分片上传API
|
|
|
+ String uploadId = storage.initMultipartUpload(fileName);
|
|
|
+
|
|
|
+ // 在Redis中存储上传信息
|
|
|
+ Map<String, Object> uploadInfo = new HashMap<>();
|
|
|
+ uploadInfo.put("fileName", fileName);
|
|
|
+ uploadInfo.put("fileSize", String.valueOf(fileSize));
|
|
|
+ uploadInfo.put("createTime", String.valueOf(System.currentTimeMillis()));
|
|
|
+ RedisUtils.setCacheObject(UPLOAD_INFO_PREFIX + uploadId, uploadInfo, Duration.ofHours(24));
|
|
|
+
|
|
|
+ return uploadId;
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new ServiceException("初始化分片上传失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传分片
|
|
|
+ *
|
|
|
+ * @param uploadId 上传ID
|
|
|
+ * @param partNumber 分片序号
|
|
|
+ * @param partData 分片数据
|
|
|
+ * @return 分片ETag
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public String uploadPart(String uploadId, int partNumber, byte[] partData) {
|
|
|
+ try {
|
|
|
+ // 获取上传信息
|
|
|
+ Map<String, Object> uploadInfo = RedisUtils.getCacheObject(UPLOAD_INFO_PREFIX + uploadId);
|
|
|
+ if (uploadInfo == null) {
|
|
|
+ throw new ServiceException("上传会话已过期,请重新开始上传");
|
|
|
+ }
|
|
|
+
|
|
|
+ String fileName = (String) uploadInfo.get("fileName");
|
|
|
+ if (fileName == null) {
|
|
|
+ throw new ServiceException("上传信息不完整,请重新开始上传");
|
|
|
+ }
|
|
|
+
|
|
|
+ OssClient storage = OssFactory.instance();
|
|
|
+
|
|
|
+ // 调用OSS的分片上传API
|
|
|
+ String eTag = storage.uploadPart(fileName, uploadId, partNumber, partData);
|
|
|
+
|
|
|
+ // 在Redis中记录已上传的分片
|
|
|
+ String partsKey = UPLOAD_PARTS_PREFIX + uploadId;
|
|
|
+ RedisUtils.setCacheObject(partsKey + ":" + partNumber, eTag, Duration.ofHours(24));
|
|
|
+
|
|
|
+ return eTag;
|
|
|
+ } catch (ServiceException e) {
|
|
|
+ throw e; // 重新抛出业务异常
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("上传分片失败,uploadId: {}, partNumber: {}, 错误: {}", uploadId, partNumber, e.getMessage(), e);
|
|
|
+ throw new ServiceException("上传分片失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 完成分片上传
|
|
|
+ *
|
|
|
+ * @param uploadId 上传ID
|
|
|
+ * @param partETags 分片ETag列表
|
|
|
+ * @param fileName 文件名
|
|
|
+ * @param originalName 原始文件名
|
|
|
+ * @return 上传成功后的 SysOssVo 对象
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public SysOssVo completeMultipartUpload(String uploadId, List<String> partETags, String fileName, String originalName) {
|
|
|
+ try {
|
|
|
+ // 获取上传信息
|
|
|
+ Map<String, Object> uploadInfo = RedisUtils.getCacheObject(UPLOAD_INFO_PREFIX + uploadId);
|
|
|
+ if (uploadInfo == null) {
|
|
|
+ throw new ServiceException("上传会话已过期,请重新开始上传");
|
|
|
+ }
|
|
|
+
|
|
|
+ String storedFileName = (String) uploadInfo.get("fileName");
|
|
|
+ OssClient storage = OssFactory.instance();
|
|
|
+
|
|
|
+ // 调用OSS的完成分片上传API
|
|
|
+ UploadResult uploadResult = storage.completeMultipartUpload(storedFileName, uploadId, partETags);
|
|
|
+
|
|
|
+ // 保存文件信息到数据库
|
|
|
+ String suffix = StringUtils.substring(originalName, originalName.lastIndexOf("."), originalName.length());
|
|
|
+ SysOssVo result = buildResultEntity(originalName, suffix, storage.getConfigKey(), uploadResult);
|
|
|
+
|
|
|
+ // 清理Redis中的上传信息
|
|
|
+ RedisUtils.deleteObject(UPLOAD_INFO_PREFIX + uploadId);
|
|
|
+ String partsKey = UPLOAD_PARTS_PREFIX + uploadId;
|
|
|
+ // 清理所有分片记录
|
|
|
+ for (int i = 1; i <= partETags.size(); i++) {
|
|
|
+ RedisUtils.deleteObject(partsKey + ":" + i);
|
|
|
+ }
|
|
|
+
|
|
|
+ return result;
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 如果完成失败,取消上传
|
|
|
+ abortMultipartUpload(uploadId);
|
|
|
+ throw new ServiceException("完成分片上传失败: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取消分片上传
|
|
|
+ *
|
|
|
+ * @param uploadId 上传ID
|
|
|
+ * @return 是否成功
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public Boolean abortMultipartUpload(String uploadId) {
|
|
|
+ try {
|
|
|
+ // 获取上传信息
|
|
|
+ Map<String, Object> uploadInfo = RedisUtils.getCacheObject(UPLOAD_INFO_PREFIX + uploadId);
|
|
|
+ if (uploadInfo != null) {
|
|
|
+ String fileName = (String) uploadInfo.get("fileName");
|
|
|
+ OssClient storage = OssFactory.instance();
|
|
|
+
|
|
|
+ // 调用OSS的取消分片上传API
|
|
|
+ storage.abortMultipartUpload(fileName, uploadId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理Redis中的上传信息
|
|
|
+ RedisUtils.deleteObject(UPLOAD_INFO_PREFIX + uploadId);
|
|
|
+ String partsKey = UPLOAD_PARTS_PREFIX + uploadId;
|
|
|
+
|
|
|
+ // 清理所有分片记录
|
|
|
+ Set<String> keys = (Set<String>) RedisUtils.keys(partsKey + ":*");
|
|
|
+ if (keys != null && !keys.isEmpty()) {
|
|
|
+ RedisUtils.deleteObject(keys);
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查已上传的分片
|
|
|
+ *
|
|
|
+ * @param uploadId 上传ID
|
|
|
+ * @return 已上传的分片序号列表
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public List<Integer> checkUploadedParts(String uploadId) {
|
|
|
+ List<Integer> uploadedParts = new ArrayList<>();
|
|
|
+ String partsKey = UPLOAD_PARTS_PREFIX + uploadId;
|
|
|
+
|
|
|
+ // 检查Redis中已上传的分片
|
|
|
+ Set<String> keys = (Set<String>) RedisUtils.keys(partsKey + ":*");
|
|
|
+ if (keys != null) {
|
|
|
+ for (String key : keys) {
|
|
|
+ String partNumberStr = key.substring(key.lastIndexOf(":") + 1);
|
|
|
+ try {
|
|
|
+ uploadedParts.add(Integer.parseInt(partNumberStr));
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ // 忽略无效的分片序号
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return uploadedParts;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成唯一的文件名
|
|
|
+ *
|
|
|
+ * @param suffix 文件后缀
|
|
|
+ * @return 唯一的文件名
|
|
|
+ */
|
|
|
+ private String generateUniqueFileName(String suffix) {
|
|
|
+ // 生成UUID
|
|
|
+ String uuid = UUID.randomUUID().toString();
|
|
|
+ // 生成日期路径
|
|
|
+ String datePath = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy/MM/dd"));
|
|
|
+ // 拼接路径
|
|
|
+ return datePath + "/" + uuid + suffix;
|
|
|
+ }
|
|
|
}
|