GlobalAward上传文件
This commit is contained in:
@@ -41,6 +41,13 @@ public class MinioUtil {
|
||||
@Autowired
|
||||
private MinioClient minioClient;
|
||||
|
||||
/**
|
||||
* 获取MinIO客户端实例
|
||||
*/
|
||||
public MinioClient getMinioClient() {
|
||||
return minioClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* description: 判断bucket是否存在,不存在则创建
|
||||
*
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package com.ai.da.controller;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import com.ai.da.model.dto.*;
|
||||
import com.ai.da.service.GlobalAwardService;
|
||||
import com.ai.da.service.upload.UploadService;
|
||||
import com.ai.da.service.upload.UploadTask;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
@@ -11,30 +16,132 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/global-award")
|
||||
@Api(tags = "全球奖项API", description = "全球奖项大赛管理和文件上传")
|
||||
public class GlobalAwardController {
|
||||
|
||||
@Resource
|
||||
private GlobalAwardService globalAwardService;
|
||||
|
||||
@PostMapping("/uploads/pdf")
|
||||
public Response<String> uploadPdf(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "email", required = false) String email) throws Exception {
|
||||
return Response.success(globalAwardService.uploadPdf(file, email));
|
||||
@Resource
|
||||
private UploadService uploadService;
|
||||
|
||||
// @PostMapping("/uploads/pdf")
|
||||
// public Response<String> uploadPdf(@RequestParam("file") MultipartFile file,
|
||||
// @RequestParam(value = "email", required = false) String email) throws Exception {
|
||||
// return Response.success(globalAwardService.uploadPdf(file, email));
|
||||
// }
|
||||
//
|
||||
// @PostMapping("/uploads/video")
|
||||
// public Response<String> uploadVideo(@RequestParam("file") MultipartFile file,
|
||||
// @RequestParam(value = "email", required = false) String email) throws Exception {
|
||||
// return Response.success(globalAwardService.uploadVideo(file, email));
|
||||
// }
|
||||
|
||||
// ===== 新增分片上传接口 =====
|
||||
|
||||
// ===== PDF分片上传接口 =====
|
||||
|
||||
/** 初始化PDF上传任务 */
|
||||
@PostMapping("/uploads/pdf/init")
|
||||
@ApiOperation(value = "初始化PDF上传", notes = "创建新的PDF上传任务并返回上传参数")
|
||||
public Response<UploadInitResponse> initPdfUpload(@ApiParam(value = "PDF上传初始化请求", required = true) @RequestBody UploadInitRequest request) {
|
||||
UploadTask uploadTask = uploadService.initPdfUpload(request);
|
||||
return Response.success(UploadInitResponse.builder()
|
||||
.uploadId(uploadTask.getUploadId())
|
||||
.chunkSize(uploadTask.getChunkSize())
|
||||
.totalChunks(uploadTask.getTotalChunks())
|
||||
.expiresAt(uploadTask.getExpiresAt())
|
||||
.build());
|
||||
}
|
||||
|
||||
@PostMapping("/uploads/video")
|
||||
public Response<String> uploadVideo(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "email", required = false) String email) throws Exception {
|
||||
return Response.success(globalAwardService.uploadVideo(file, email));
|
||||
/** 上传PDF分片 */
|
||||
@PostMapping("/uploads/pdf/chunk")
|
||||
@ApiOperation(value = "上传PDF分片", notes = "上传PDF文件的单个分片")
|
||||
public Response<UploadChunkResponse> uploadPdfChunk(
|
||||
@ApiParam(value = "PDF文件分片", required = true) @RequestParam("chunk") MultipartFile chunk,
|
||||
@ApiParam(value = "上传任务ID", required = true) @RequestParam("uploadId") String uploadId,
|
||||
@ApiParam(value = "分片索引(从0开始)", required = true) @RequestParam("chunkIndex") int chunkIndex,
|
||||
@ApiParam(value = "分片总数", required = true) @RequestParam("totalChunks") int totalChunks) {
|
||||
|
||||
UploadChunkResponse uploadChunkResponse = uploadService.uploadPdfChunk(uploadId, chunk, chunkIndex, totalChunks);
|
||||
return Response.success(uploadChunkResponse);
|
||||
}
|
||||
|
||||
/** 完成PDF上传 */
|
||||
@PostMapping("/uploads/pdf/complete")
|
||||
@ApiOperation(value = "完成PDF上传", notes = "完成PDF上传并合并所有分片")
|
||||
public Response<UploadCompleteResponse> completePdfUpload(@ApiParam(value = "PDF上传完成请求", required = true) @RequestBody UploadCompleteRequest request) {
|
||||
UploadCompleteResponse uploadCompleteResponse = uploadService.completePdfUpload(
|
||||
request.getUploadId(),
|
||||
request.getFileName(),
|
||||
request.getTotalSize());
|
||||
return Response.success(uploadCompleteResponse);
|
||||
}
|
||||
|
||||
/** 查询PDF上传状态 */
|
||||
@GetMapping("/uploads/pdf/status/{uploadId}")
|
||||
@ApiOperation(value = "查询PDF上传状态", notes = "获取PDF上传任务的当前状态")
|
||||
public Response<UploadStatusResponse> getPdfUploadStatus(@ApiParam(value = "上传任务ID", required = true) @PathVariable String uploadId) {
|
||||
UploadStatusResponse pdfUploadStatus = uploadService.getPdfUploadStatus(uploadId);
|
||||
return Response.success(pdfUploadStatus);
|
||||
}
|
||||
|
||||
// ===== 视频分片上传接口 =====
|
||||
|
||||
/** 初始化视频上传任务 */
|
||||
@PostMapping("/uploads/video/init")
|
||||
@ApiOperation(value = "初始化视频上传", notes = "创建新的视频上传任务并返回上传参数")
|
||||
public Response<UploadInitResponse> initVideoUpload(@ApiParam(value = "视频上传初始化请求", required = true) @RequestBody UploadInitRequest request) {
|
||||
UploadTask uploadTask = uploadService.initVideoUpload(request);
|
||||
return Response.success(UploadInitResponse.builder()
|
||||
.uploadId(uploadTask.getUploadId())
|
||||
.chunkSize(uploadTask.getChunkSize())
|
||||
.totalChunks(uploadTask.getTotalChunks())
|
||||
.expiresAt(uploadTask.getExpiresAt())
|
||||
.build());
|
||||
}
|
||||
|
||||
/** 上传视频分片 */
|
||||
@PostMapping("/uploads/video/chunk")
|
||||
@ApiOperation(value = "上传视频分片", notes = "上传视频文件的单个分片")
|
||||
public Response<UploadChunkResponse> uploadVideoChunk(
|
||||
@ApiParam(value = "视频文件分片", required = true) @RequestParam("chunk") MultipartFile chunk,
|
||||
@ApiParam(value = "上传任务ID", required = true) @RequestParam("uploadId") String uploadId,
|
||||
@ApiParam(value = "分片索引(从0开始)", required = true) @RequestParam("chunkIndex") int chunkIndex,
|
||||
@ApiParam(value = "分片总数", required = true) @RequestParam("totalChunks") int totalChunks) {
|
||||
|
||||
UploadChunkResponse uploadChunkResponse = uploadService.uploadVideoChunk(uploadId, chunk, chunkIndex, totalChunks);
|
||||
return Response.success(uploadChunkResponse);
|
||||
}
|
||||
|
||||
/** 完成视频上传 */
|
||||
@PostMapping("/uploads/video/complete")
|
||||
@ApiOperation(value = "完成视频上传", notes = "完成视频上传并合并所有分片")
|
||||
public Response<UploadCompleteResponse> completeVideoUpload(@ApiParam(value = "视频上传完成请求", required = true) @RequestBody UploadCompleteRequest request) {
|
||||
UploadCompleteResponse uploadCompleteResponse = uploadService.completeVideoUpload(
|
||||
request.getUploadId(),
|
||||
request.getFileName(),
|
||||
request.getTotalSize());
|
||||
return Response.success(uploadCompleteResponse);
|
||||
}
|
||||
|
||||
/** 查询视频上传状态 */
|
||||
@GetMapping("/uploads/video/status/{uploadId}")
|
||||
@ApiOperation(value = "查询视频上传状态", notes = "获取视频上传任务的当前状态")
|
||||
public Response<UploadStatusResponse> getVideoUploadStatus(@ApiParam(value = "上传任务ID", required = true) @PathVariable String uploadId) {
|
||||
UploadStatusResponse videoUploadStatus = uploadService.getVideoUploadStatus(uploadId);
|
||||
return Response.success(videoUploadStatus);
|
||||
}
|
||||
|
||||
@PostMapping("/contestants/save")
|
||||
public Response<Map<String,Object>> submit(@RequestBody ContestantDTO request) {
|
||||
@ApiOperation(value = "保存参赛者信息", notes = "保存或更新参赛者信息及已上传的文件")
|
||||
public Response<Map<String,Object>> submit(@ApiParam(value = "参赛者信息", required = true) @RequestBody ContestantDTO request) {
|
||||
return Response.success(globalAwardService.saveContestant(request));
|
||||
}
|
||||
|
||||
@GetMapping("/contestants/by-email")
|
||||
public Response<ContestantDTO> getContestantByEmail(@RequestParam("email") String email) {
|
||||
@ApiOperation(value = "根据邮箱查询参赛者", notes = "根据邮箱地址获取参赛者信息")
|
||||
public Response<ContestantDTO> getContestantByEmail(@ApiParam(value = "参赛者邮箱地址", required = true) @RequestParam("email") String email) {
|
||||
ContestantDTO dto = globalAwardService.getContestantByEmail(email);
|
||||
return Response.success(dto);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,58 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Contestant request DTO for Global Award
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "参赛者信息", description = "全球奖项大赛参赛者信息数据传输对象")
|
||||
public class ContestantDTO {
|
||||
@ApiModelProperty(value = "邮箱地址", required = true, example = "user@example.com")
|
||||
private String email;
|
||||
|
||||
@ApiModelProperty(value = "名字", required = true, example = "John")
|
||||
private String firstName;
|
||||
|
||||
@ApiModelProperty(value = "姓氏", required = true, example = "Doe")
|
||||
private String lastName;
|
||||
|
||||
@ApiModelProperty(value = "性别", required = true, example = "Male", allowableValues = "Male,Female,Other")
|
||||
private String gender;
|
||||
|
||||
@ApiModelProperty(value = "职业", required = true, example = "Designer")
|
||||
private String occupation;
|
||||
|
||||
@ApiModelProperty(value = "年龄", required = true, example = "25")
|
||||
private Integer age;
|
||||
|
||||
@ApiModelProperty(value = "国家/地区/城市", required = true, example = "China/Shanghai/Shanghai")
|
||||
private String countryRegionCity;
|
||||
|
||||
@ApiModelProperty(value = "电话号码", required = true, example = "+86 138 0000 0000")
|
||||
private String phoneNumber;
|
||||
|
||||
@ApiModelProperty(value = "作品集链接", required = false, example = "https://portfolio.example.com")
|
||||
private String portfolioUrl;
|
||||
|
||||
@ApiModelProperty(value = "设计作品标题", required = true, example = "Modern Office Building Design")
|
||||
private String designTitle;
|
||||
|
||||
@ApiModelProperty(value = "设计作品描述", required = true, example = "A modern office building design featuring sustainable materials...")
|
||||
private String designDescription;
|
||||
|
||||
@ApiModelProperty(value = "PDF文件路径", required = false, example = "contestants/user@example.com/2024/01/design_1234567890.pdf")
|
||||
private String pdfPath;
|
||||
|
||||
@ApiModelProperty(value = "视频文件路径", required = false, example = "contestants/user@example.com/2024/01/video_1234567890.mp4")
|
||||
private String videoPath;
|
||||
|
||||
/**
|
||||
* 是否确认覆盖已存在记录(false 表示发现已有记录时仅返回 existingRecord,不覆盖)
|
||||
*/
|
||||
@ApiModelProperty(value = "是否确认覆盖已存在记录", required = false, example = "false")
|
||||
private Boolean confirm = false;
|
||||
}
|
||||
|
||||
|
||||
33
src/main/java/com/ai/da/model/dto/UploadChunkResponse.java
Normal file
33
src/main/java/com/ai/da/model/dto/UploadChunkResponse.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分片上传响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "分片上传响应", description = "单个文件分片上传成功的响应数据")
|
||||
public class UploadChunkResponse {
|
||||
|
||||
/**
|
||||
* 分片索引
|
||||
*/
|
||||
@ApiModelProperty(value = "分片索引(从0开始)", required = true, example = "0")
|
||||
private Integer chunkIndex;
|
||||
|
||||
/**
|
||||
* 是否上传成功
|
||||
*/
|
||||
@ApiModelProperty(value = "是否上传成功", required = true, example = "true")
|
||||
private Boolean uploaded;
|
||||
|
||||
/**
|
||||
* 分片大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "分片大小(字节)", required = true, example = "1048576")
|
||||
private Long size;
|
||||
}
|
||||
39
src/main/java/com/ai/da/model/dto/UploadCompleteRequest.java
Normal file
39
src/main/java/com/ai/da/model/dto/UploadCompleteRequest.java
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
/**
|
||||
* 完成上传请求DTO
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "完成上传请求", description = "文件上传完成时使用的请求参数")
|
||||
public class UploadCompleteRequest {
|
||||
|
||||
/**
|
||||
* 上传任务ID
|
||||
*/
|
||||
@NotBlank(message = "上传任务ID不能为空")
|
||||
@ApiModelProperty(value = "上传任务唯一标识", required = true, example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
@NotBlank(message = "文件名不能为空")
|
||||
@ApiModelProperty(value = "原始文件名", required = true, example = "design.pdf")
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件总大小(字节)
|
||||
*/
|
||||
@NotNull(message = "文件大小不能为空")
|
||||
@Positive(message = "文件大小必须大于0")
|
||||
@ApiModelProperty(value = "文件总大小(字节)", required = true, example = "10485760")
|
||||
private Long totalSize;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 完成上传响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "完成上传响应", description = "文件上传完成并合并成功的响应数据")
|
||||
public class UploadCompleteResponse {
|
||||
|
||||
/**
|
||||
* 文件在MinIO中的路径
|
||||
*/
|
||||
@ApiModelProperty(value = "文件在MinIO中的存储路径", required = true, example = "contestants/user@example.com/2024/01/design_1234567890.pdf")
|
||||
private String filePath;
|
||||
|
||||
/**
|
||||
* 文件的完整URL
|
||||
*/
|
||||
@ApiModelProperty(value = "文件的完整访问URL", required = true, example = "https://minio.example.com/contestants/user@example.com/2024/01/design_1234567890.pdf")
|
||||
private String fileUrl;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "文件大小(字节)", required = true, example = "10485760")
|
||||
private Long fileSize;
|
||||
}
|
||||
46
src/main/java/com/ai/da/model/dto/UploadInitRequest.java
Normal file
46
src/main/java/com/ai/da/model/dto/UploadInitRequest.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
/**
|
||||
* 初始化上传请求DTO
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "初始化上传请求", description = "文件上传初始化时使用的请求参数")
|
||||
public class UploadInitRequest {
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
@NotBlank(message = "文件名不能为空")
|
||||
@ApiModelProperty(value = "文件名", required = true, example = "design.pdf")
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@NotNull(message = "文件大小不能为空")
|
||||
@Positive(message = "文件大小必须大于0")
|
||||
@ApiModelProperty(value = "文件大小(字节)", required = true, example = "10485760")
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 文件类型(MIME类型)
|
||||
*/
|
||||
@NotBlank(message = "文件类型不能为空")
|
||||
@ApiModelProperty(value = "文件类型(MIME类型)", required = true, example = "application/pdf")
|
||||
private String fileType;
|
||||
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
@ApiModelProperty(value = "用户邮箱", required = false, example = "user@example.com")
|
||||
private String email;
|
||||
}
|
||||
41
src/main/java/com/ai/da/model/dto/UploadInitResponse.java
Normal file
41
src/main/java/com/ai/da/model/dto/UploadInitResponse.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 初始化上传响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "初始化上传响应", description = "文件上传初始化成功的响应数据")
|
||||
public class UploadInitResponse {
|
||||
|
||||
/**
|
||||
* 上传任务ID
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务唯一标识", required = true, example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 分片大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "每个分片的大小(字节)", required = true, example = "1048576")
|
||||
private Integer chunkSize;
|
||||
|
||||
/**
|
||||
* 总分片数
|
||||
*/
|
||||
@ApiModelProperty(value = "文件被分成多少个分片", required = true, example = "10")
|
||||
private Integer totalChunks;
|
||||
|
||||
/**
|
||||
* 任务过期时间
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务过期时间", required = true, example = "2024-01-20T10:30:00")
|
||||
private LocalDateTime expiresAt;
|
||||
}
|
||||
59
src/main/java/com/ai/da/model/dto/UploadStatusResponse.java
Normal file
59
src/main/java/com/ai/da/model/dto/UploadStatusResponse.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 上传状态响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "上传状态响应", description = "查询上传任务当前状态的响应数据")
|
||||
public class UploadStatusResponse {
|
||||
|
||||
/**
|
||||
* 上传任务ID
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务唯一标识", required = true, example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 上传状态
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务状态", required = true, example = "uploading", allowableValues = "initiated,uploading,completed,failed,expired")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 上传进度百分比 (0-100)
|
||||
*/
|
||||
@ApiModelProperty(value = "上传进度百分比(0-100)", required = true, example = "60.0")
|
||||
private Double progress;
|
||||
|
||||
/**
|
||||
* 已上传分片索引集合
|
||||
*/
|
||||
@ApiModelProperty(value = "已上传分片的索引集合", required = true, example = "[0,1,2,3,4]")
|
||||
private Set<Integer> uploadedChunks;
|
||||
|
||||
/**
|
||||
* 总分片数
|
||||
*/
|
||||
@ApiModelProperty(value = "文件被分成多少个分片", required = true, example = "10")
|
||||
private Integer totalChunks;
|
||||
|
||||
/**
|
||||
* 文件总大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "文件总大小(字节)", required = true, example = "10485760")
|
||||
private Long totalSize;
|
||||
|
||||
/**
|
||||
* 已上传大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "已上传的数据大小(字节)", required = true, example = "6291456")
|
||||
private Long uploadedSize;
|
||||
}
|
||||
@@ -55,29 +55,29 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
|
||||
private void validatePdf(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("file is empty");
|
||||
throw new BusinessException("File is empty.");
|
||||
}
|
||||
String ct = file.getContentType();
|
||||
if (ct == null || !ct.toLowerCase().contains("pdf")) {
|
||||
throw new BusinessException("only pdf allowed");
|
||||
throw new BusinessException("Only PDF files are allowed.");
|
||||
}
|
||||
// size limit example 20MB
|
||||
if (file.getSize() > 20L * 1024 * 1024) {
|
||||
throw new BusinessException("pdf too large");
|
||||
throw new BusinessException("PDF file size exceeds the limit.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateVideo(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("file is empty");
|
||||
throw new BusinessException("File is empty.");
|
||||
}
|
||||
String ct = file.getContentType();
|
||||
if (ct == null || !(ct.toLowerCase().contains("mp4") || ct.toLowerCase().contains("video") )) {
|
||||
throw new BusinessException("invalid video type");
|
||||
throw new BusinessException("Invalid video file type.");
|
||||
}
|
||||
// size limit example 100MB
|
||||
if (file.getSize() > 100L * 1024 * 1024) {
|
||||
throw new BusinessException("video too large");
|
||||
throw new BusinessException("Video file size exceeds the limit.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
public Map<String, Object> saveContestant(ContestantDTO request) {
|
||||
Map<String,Object> resp = new HashMap<>();
|
||||
if (request.getEmail() == null) {
|
||||
throw new IllegalArgumentException("email required");
|
||||
throw new BusinessException("Email is required.");
|
||||
}
|
||||
|
||||
QueryWrapper<Contestant> qw = new QueryWrapper<>();
|
||||
@@ -161,7 +161,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
@Override
|
||||
public ContestantDTO getContestantByEmail(String email) {
|
||||
if (email == null) {
|
||||
throw new BusinessException("email required");
|
||||
throw new BusinessException("Email is required.");
|
||||
}
|
||||
QueryWrapper<Contestant> qw = new QueryWrapper<>();
|
||||
qw.eq("email", email);
|
||||
|
||||
92
src/main/java/com/ai/da/service/upload/UploadService.java
Normal file
92
src/main/java/com/ai/da/service/upload/UploadService.java
Normal file
@@ -0,0 +1,92 @@
|
||||
package com.ai.da.service.upload;
|
||||
|
||||
import com.ai.da.model.dto.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 分片上传服务接口
|
||||
* 提供PDF和视频文件的分片上传、断点续传功能
|
||||
*/
|
||||
public interface UploadService {
|
||||
|
||||
// ===== PDF上传相关 =====
|
||||
|
||||
/**
|
||||
* 初始化PDF上传任务
|
||||
* @param request 初始化请求
|
||||
* @return 上传任务信息
|
||||
*/
|
||||
UploadTask initPdfUpload(UploadInitRequest request);
|
||||
|
||||
/**
|
||||
* 上传PDF分片
|
||||
* @param uploadId 上传任务ID
|
||||
* @param chunk 分片文件
|
||||
* @param chunkIndex 分片索引
|
||||
* @param totalChunks 总分片数
|
||||
* @return 分片上传结果
|
||||
*/
|
||||
UploadChunkResponse uploadPdfChunk(String uploadId, MultipartFile chunk,
|
||||
int chunkIndex, int totalChunks);
|
||||
|
||||
/**
|
||||
* 完成PDF上传(异步合并并上传到MinIO)
|
||||
* @param uploadId 上传任务ID
|
||||
* @param fileName 文件名
|
||||
* @param totalSize 文件总大小
|
||||
* @return 完成上传结果
|
||||
*/
|
||||
UploadCompleteResponse completePdfUpload(String uploadId, String fileName, long totalSize);
|
||||
|
||||
/**
|
||||
* 查询PDF上传状态
|
||||
* @param uploadId 上传任务ID
|
||||
* @return 上传状态信息
|
||||
*/
|
||||
UploadStatusResponse getPdfUploadStatus(String uploadId);
|
||||
|
||||
|
||||
// ===== 视频上传相关 =====
|
||||
|
||||
/**
|
||||
* 初始化视频上传任务
|
||||
* @param request 初始化请求
|
||||
* @return 上传任务信息
|
||||
*/
|
||||
UploadTask initVideoUpload(UploadInitRequest request);
|
||||
|
||||
/**
|
||||
* 上传视频分片
|
||||
* @param uploadId 上传任务ID
|
||||
* @param chunk 分片文件
|
||||
* @param chunkIndex 分片索引
|
||||
* @param totalChunks 总分片数
|
||||
* @return 分片上传结果
|
||||
*/
|
||||
UploadChunkResponse uploadVideoChunk(String uploadId, MultipartFile chunk,
|
||||
int chunkIndex, int totalChunks);
|
||||
|
||||
/**
|
||||
* 完成视频上传(异步合并并上传到MinIO)
|
||||
* @param uploadId 上传任务ID
|
||||
* @param fileName 文件名
|
||||
* @param totalSize 文件总大小
|
||||
* @return 完成上传结果
|
||||
*/
|
||||
UploadCompleteResponse completeVideoUpload(String uploadId, String fileName, long totalSize);
|
||||
|
||||
/**
|
||||
* 查询视频上传状态
|
||||
* @param uploadId 上传任务ID
|
||||
* @return 上传状态信息
|
||||
*/
|
||||
UploadStatusResponse getVideoUploadStatus(String uploadId);
|
||||
|
||||
|
||||
// ===== 通用功能 =====
|
||||
|
||||
/**
|
||||
* 清理过期上传任务
|
||||
*/
|
||||
void cleanupExpiredUploads();
|
||||
}
|
||||
106
src/main/java/com/ai/da/service/upload/UploadTask.java
Normal file
106
src/main/java/com/ai/da/service/upload/UploadTask.java
Normal file
@@ -0,0 +1,106 @@
|
||||
package com.ai.da.service.upload;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 上传任务实体类
|
||||
* 用于管理分片上传的状态和元数据
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class UploadTask {
|
||||
|
||||
/**
|
||||
* 上传任务唯一标识
|
||||
*/
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件类型 (pdf/video)
|
||||
*/
|
||||
private String fileType;
|
||||
|
||||
/**
|
||||
* 文件总大小(字节)
|
||||
*/
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 总分片数
|
||||
*/
|
||||
private Integer totalChunks;
|
||||
|
||||
/**
|
||||
* 分片大小(字节)
|
||||
*/
|
||||
private Integer chunkSize;
|
||||
|
||||
/**
|
||||
* 已上传分片索引集合
|
||||
*/
|
||||
private Set<Integer> uploadedChunks;
|
||||
|
||||
/**
|
||||
* 上传状态
|
||||
*/
|
||||
private UploadStatus status;
|
||||
|
||||
/**
|
||||
* 任务创建时间
|
||||
*/
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 任务过期时间
|
||||
*/
|
||||
private LocalDateTime expiresAt;
|
||||
|
||||
/**
|
||||
* 最终文件在MinIO中的路径
|
||||
*/
|
||||
private String finalPath;
|
||||
|
||||
/**
|
||||
* 上传状态枚举
|
||||
*/
|
||||
public enum UploadStatus {
|
||||
/**
|
||||
* 已初始化,等待上传分片
|
||||
*/
|
||||
INITIATED,
|
||||
|
||||
/**
|
||||
* 正在上传分片
|
||||
*/
|
||||
UPLOADING,
|
||||
|
||||
/**
|
||||
* 上传完成
|
||||
*/
|
||||
COMPLETED,
|
||||
|
||||
/**
|
||||
* 上传失败
|
||||
*/
|
||||
FAILED,
|
||||
|
||||
/**
|
||||
* 任务过期
|
||||
*/
|
||||
EXPIRED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
package com.ai.da.service.upload.impl;
|
||||
|
||||
import com.ai.da.common.config.exception.BusinessException;
|
||||
import com.ai.da.common.utils.MinioUtil;
|
||||
import com.ai.da.model.dto.*;
|
||||
import com.ai.da.service.upload.UploadService;
|
||||
import com.ai.da.service.upload.UploadTask;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.minio.PutObjectArgs;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 分片上传服务实现类
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UploadServiceImpl implements UploadService {
|
||||
|
||||
// ===== 配置参数 =====
|
||||
|
||||
@Value("${file.upload.temp.dir:temp/uploads}")
|
||||
private String tempDir;
|
||||
|
||||
// PDF分片大小:1MB
|
||||
@Value("${file.upload.chunk.size.pdf:1048576}")
|
||||
private int pdfChunkSize;
|
||||
|
||||
// 视频分片大小:2MB
|
||||
@Value("${file.upload.chunk.size.video:2097152}")
|
||||
private int videoChunkSize;
|
||||
|
||||
// 文件大小限制
|
||||
@Value("${file.upload.max.size.pdf:20971520}") // PDF: 20MB
|
||||
private long maxPdfSize;
|
||||
|
||||
@Value("${file.upload.max.size.video:104857600}") // 视频: 100MB
|
||||
private long maxVideoSize;
|
||||
|
||||
@Resource
|
||||
private MinioUtil minioUtil;
|
||||
|
||||
@Value("${minio.bucket:contestants}")
|
||||
private String minioBucket;
|
||||
|
||||
// 内存存储上传任务状态
|
||||
private final ConcurrentHashMap<String, UploadTask> uploadTasks = new ConcurrentHashMap<>();
|
||||
|
||||
// JSON序列化工具
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// ===== PDF上传实现 =====
|
||||
|
||||
@Override
|
||||
public UploadTask initPdfUpload(UploadInitRequest request) {
|
||||
// 验证PDF文件
|
||||
validatePdfFile(request);
|
||||
|
||||
// 创建上传任务
|
||||
String uploadId = UUID.randomUUID().toString();
|
||||
int totalChunks = (int) Math.ceil((double) request.getFileSize() / pdfChunkSize);
|
||||
|
||||
UploadTask task = createUploadTask(request, uploadId, totalChunks, pdfChunkSize, "pdf");
|
||||
|
||||
// 创建临时目录并保存任务状态
|
||||
createTempDirectory(uploadId);
|
||||
uploadTasks.put(uploadId, task);
|
||||
saveTaskMetadata(task);
|
||||
|
||||
log.info("PDF上传任务初始化完成: uploadId={}, totalChunks={}, fileSize={}",
|
||||
uploadId, totalChunks, request.getFileSize());
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadChunkResponse uploadPdfChunk(String uploadId, MultipartFile chunk,
|
||||
int chunkIndex, int totalChunks) {
|
||||
// 验证任务状态
|
||||
UploadTask task = validateAndGetTask(uploadId, "pdf");
|
||||
|
||||
// 保存分片到本地
|
||||
saveChunkToLocal(chunk, uploadId, chunkIndex);
|
||||
|
||||
// 更新任务进度
|
||||
updateTaskProgress(task, chunkIndex);
|
||||
|
||||
log.debug("PDF分片上传完成: uploadId={}, chunkIndex={}, size={}",
|
||||
uploadId, chunkIndex, chunk.getSize());
|
||||
|
||||
return UploadChunkResponse.builder()
|
||||
.chunkIndex(chunkIndex)
|
||||
.uploaded(true)
|
||||
.size(chunk.getSize())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public UploadCompleteResponse completePdfUpload(String uploadId, String fileName, long totalSize) {
|
||||
UploadTask task = validateAndGetTask(uploadId, "pdf");
|
||||
|
||||
log.info("开始PDF文件合并: uploadId={}, fileName={}", uploadId, fileName);
|
||||
|
||||
try {
|
||||
// 1. 合并所有分片
|
||||
Path mergedFile = mergeChunks(task);
|
||||
|
||||
// 2. 上传到MinIO
|
||||
String finalPath = uploadToMinio(task, mergedFile, "pdf");
|
||||
|
||||
// 3. 更新任务状态并清理
|
||||
completeTask(task, finalPath);
|
||||
cleanupTempFiles(uploadId);
|
||||
|
||||
log.info("PDF上传完成: uploadId={}, finalPath={}", uploadId, finalPath);
|
||||
|
||||
return buildCompleteResponse(task, finalPath, totalSize);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("PDF上传失败: uploadId={}", uploadId, e);
|
||||
task.setStatus(UploadTask.UploadStatus.FAILED);
|
||||
saveTaskMetadata(task);
|
||||
throw new BusinessException("File merge failed. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadStatusResponse getPdfUploadStatus(String uploadId) {
|
||||
UploadTask task = uploadTasks.get(uploadId);
|
||||
if (task == null) {
|
||||
throw new BusinessException("Upload task not found.");
|
||||
}
|
||||
|
||||
if (!"pdf".equals(task.getFileType())) {
|
||||
throw new BusinessException("Task type mismatch.");
|
||||
}
|
||||
|
||||
// 计算上传进度
|
||||
double progress = task.getTotalChunks() > 0 ?
|
||||
(double) task.getUploadedChunks().size() / task.getTotalChunks() * 100 : 0;
|
||||
|
||||
long uploadedSize = task.getUploadedChunks().size() * task.getChunkSize();
|
||||
|
||||
return UploadStatusResponse.builder()
|
||||
.uploadId(uploadId)
|
||||
.status(task.getStatus().name().toLowerCase())
|
||||
.progress(Math.min(progress, 100.0))
|
||||
.uploadedChunks(new HashSet<>(task.getUploadedChunks()))
|
||||
.totalChunks(task.getTotalChunks())
|
||||
.totalSize(task.getFileSize())
|
||||
.uploadedSize(Math.min(uploadedSize, task.getFileSize()))
|
||||
.build();
|
||||
}
|
||||
|
||||
// ===== 视频上传实现 =====
|
||||
|
||||
@Override
|
||||
public UploadTask initVideoUpload(UploadInitRequest request) {
|
||||
// 验证视频文件
|
||||
validateVideoFile(request);
|
||||
|
||||
// 创建上传任务
|
||||
String uploadId = UUID.randomUUID().toString();
|
||||
int totalChunks = (int) Math.ceil((double) request.getFileSize() / videoChunkSize);
|
||||
|
||||
UploadTask task = createUploadTask(request, uploadId, totalChunks, videoChunkSize, "video");
|
||||
|
||||
// 创建临时目录并保存任务状态
|
||||
createTempDirectory(uploadId);
|
||||
uploadTasks.put(uploadId, task);
|
||||
saveTaskMetadata(task);
|
||||
|
||||
log.info("视频上传任务初始化完成: uploadId={}, totalChunks={}, fileSize={}",
|
||||
uploadId, totalChunks, request.getFileSize());
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadChunkResponse uploadVideoChunk(String uploadId, MultipartFile chunk,
|
||||
int chunkIndex, int totalChunks) {
|
||||
// 验证任务状态
|
||||
UploadTask task = validateAndGetTask(uploadId, "video");
|
||||
|
||||
// 保存分片到本地
|
||||
saveChunkToLocal(chunk, uploadId, chunkIndex);
|
||||
|
||||
// 更新任务进度
|
||||
updateTaskProgress(task, chunkIndex);
|
||||
|
||||
log.debug("视频分片上传完成: uploadId={}, chunkIndex={}, size={}",
|
||||
uploadId, chunkIndex, chunk.getSize());
|
||||
|
||||
return UploadChunkResponse.builder()
|
||||
.chunkIndex(chunkIndex)
|
||||
.uploaded(true)
|
||||
.size(chunk.getSize())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Async
|
||||
public UploadCompleteResponse completeVideoUpload(String uploadId, String fileName, long totalSize) {
|
||||
UploadTask task = validateAndGetTask(uploadId, "video");
|
||||
|
||||
log.info("开始视频文件合并: uploadId={}, fileName={}", uploadId, fileName);
|
||||
|
||||
try {
|
||||
// 1. 合并所有分片
|
||||
Path mergedFile = mergeChunks(task);
|
||||
|
||||
// 2. 上传到MinIO
|
||||
String finalPath = uploadToMinio(task, mergedFile, "video");
|
||||
|
||||
// 3. 更新任务状态并清理
|
||||
completeTask(task, finalPath);
|
||||
cleanupTempFiles(uploadId);
|
||||
|
||||
log.info("视频上传完成: uploadId={}, finalPath={}", uploadId, finalPath);
|
||||
|
||||
return buildCompleteResponse(task, finalPath, totalSize);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("视频上传失败: uploadId={}", uploadId, e);
|
||||
task.setStatus(UploadTask.UploadStatus.FAILED);
|
||||
saveTaskMetadata(task);
|
||||
throw new BusinessException("File merge failed. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadStatusResponse getVideoUploadStatus(String uploadId) {
|
||||
UploadTask task = uploadTasks.get(uploadId);
|
||||
if (task == null) {
|
||||
throw new BusinessException("Upload task not found.");
|
||||
}
|
||||
|
||||
if (!"video".equals(task.getFileType())) {
|
||||
throw new BusinessException("Task type mismatch.");
|
||||
}
|
||||
|
||||
// 计算上传进度
|
||||
double progress = task.getTotalChunks() > 0 ?
|
||||
(double) task.getUploadedChunks().size() / task.getTotalChunks() * 100 : 0;
|
||||
|
||||
long uploadedSize = task.getUploadedChunks().size() * task.getChunkSize();
|
||||
|
||||
return UploadStatusResponse.builder()
|
||||
.uploadId(uploadId)
|
||||
.status(task.getStatus().name().toLowerCase())
|
||||
.progress(Math.min(progress, 100.0))
|
||||
.uploadedChunks(new HashSet<>(task.getUploadedChunks()))
|
||||
.totalChunks(task.getTotalChunks())
|
||||
.totalSize(task.getFileSize())
|
||||
.uploadedSize(Math.min(uploadedSize, task.getFileSize()))
|
||||
.build();
|
||||
}
|
||||
|
||||
// ===== 核心辅助方法 =====
|
||||
|
||||
/**
|
||||
* 验证PDF文件
|
||||
*/
|
||||
private void validatePdfFile(UploadInitRequest request) {
|
||||
if (!"application/pdf".equals(request.getFileType())) {
|
||||
throw new BusinessException("Only PDF files are allowed.");
|
||||
}
|
||||
if (request.getFileSize() > maxPdfSize) {
|
||||
throw new BusinessException("PDF file size cannot exceed " + (maxPdfSize / 1024 / 1024) + "MB.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证视频文件
|
||||
*/
|
||||
private void validateVideoFile(UploadInitRequest request) {
|
||||
if (request.getFileType() == null ||
|
||||
(!request.getFileType().contains("mp4") &&
|
||||
!request.getFileType().contains("video") &&
|
||||
!request.getFileType().contains("avi"))) {
|
||||
throw new BusinessException("Unsupported video format.");
|
||||
}
|
||||
if (request.getFileSize() > maxVideoSize) {
|
||||
throw new BusinessException("Video file size cannot exceed " + (maxVideoSize / 1024 / 1024) + "MB.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建上传任务
|
||||
*/
|
||||
private UploadTask createUploadTask(UploadInitRequest request, String uploadId,
|
||||
int totalChunks, int chunkSize, String fileType) {
|
||||
return UploadTask.builder()
|
||||
.uploadId(uploadId)
|
||||
.fileName(request.getFileName())
|
||||
.fileType(fileType)
|
||||
.fileSize(request.getFileSize())
|
||||
.email(request.getEmail())
|
||||
.totalChunks(totalChunks)
|
||||
.chunkSize(chunkSize)
|
||||
.uploadedChunks(new HashSet<>())
|
||||
.status(UploadTask.UploadStatus.INITIATED)
|
||||
.createdAt(LocalDateTime.now())
|
||||
.expiresAt(LocalDateTime.now().plusHours(24))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建临时目录
|
||||
*/
|
||||
private void createTempDirectory(String uploadId) {
|
||||
try {
|
||||
Path uploadDir = Paths.get(tempDir, uploadId, "chunks");
|
||||
Files.createDirectories(uploadDir);
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("Failed to create temporary directory.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证并获取上传任务
|
||||
*/
|
||||
private UploadTask validateAndGetTask(String uploadId, String expectedType) {
|
||||
UploadTask task = uploadTasks.get(uploadId);
|
||||
if (task == null) {
|
||||
throw new BusinessException("Upload task not found.");
|
||||
}
|
||||
|
||||
if (task.getExpiresAt().isBefore(LocalDateTime.now())) {
|
||||
task.setStatus(UploadTask.UploadStatus.EXPIRED);
|
||||
throw new BusinessException("Upload task has expired.");
|
||||
}
|
||||
|
||||
if (!expectedType.equals(task.getFileType())) {
|
||||
throw new BusinessException("Task type mismatch.");
|
||||
}
|
||||
|
||||
if (task.getStatus() == UploadTask.UploadStatus.COMPLETED ||
|
||||
task.getStatus() == UploadTask.UploadStatus.FAILED) {
|
||||
throw new BusinessException("Task has already been completed or failed.");
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存分片到本地
|
||||
*/
|
||||
private void saveChunkToLocal(MultipartFile chunk, String uploadId, int chunkIndex) {
|
||||
try {
|
||||
Path chunkPath = Paths.get(tempDir, uploadId, "chunks", "chunk_" + chunkIndex);
|
||||
Files.copy(chunk.getInputStream(), chunkPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("Failed to save file chunk.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务进度
|
||||
*/
|
||||
private void updateTaskProgress(UploadTask task, int chunkIndex) {
|
||||
task.getUploadedChunks().add(chunkIndex);
|
||||
task.setStatus(UploadTask.UploadStatus.UPLOADING);
|
||||
saveTaskMetadata(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并分片文件(零拷贝方式)
|
||||
*/
|
||||
private Path mergeChunks(UploadTask task) throws IOException {
|
||||
Path mergedFile = Files.createTempFile("merge_", "_" + task.getFileName());
|
||||
|
||||
try (FileChannel outputChannel = FileChannel.open(mergedFile, StandardOpenOption.WRITE)) {
|
||||
for (int i = 0; i < task.getTotalChunks(); i++) {
|
||||
Path chunkPath = Paths.get(tempDir, task.getUploadId(), "chunks", "chunk_" + i);
|
||||
try (FileChannel inputChannel = FileChannel.open(chunkPath, StandardOpenOption.READ)) {
|
||||
// 零拷贝传输,提升性能
|
||||
inputChannel.transferTo(0, inputChannel.size(), outputChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergedFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到MinIO
|
||||
*/
|
||||
private String uploadToMinio(UploadTask task, Path mergedFile, String fileType) throws Exception {
|
||||
// 生成MinIO路径: contestants/{email}/{date}/{filename}
|
||||
String normalizedEmail = normalizeEmail(task.getEmail());
|
||||
String datePart = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM"));
|
||||
String ext = getFileExtension(task.getFileName());
|
||||
String filename = System.currentTimeMillis() + "_" + UUID.randomUUID().toString() + ext;
|
||||
String relativePath = "contestants/" + normalizedEmail + "/" + datePart + "/" + filename;
|
||||
|
||||
// 上传文件
|
||||
try (FileInputStream fis = new FileInputStream(mergedFile.toFile())) {
|
||||
minioUtil.getMinioClient().putObject(
|
||||
PutObjectArgs.builder()
|
||||
.bucket(minioBucket)
|
||||
.object(relativePath)
|
||||
.stream(fis, mergedFile.toFile().length(), -1)
|
||||
.contentType(getContentType(fileType))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成任务
|
||||
*/
|
||||
private void completeTask(UploadTask task, String finalPath) {
|
||||
task.setStatus(UploadTask.UploadStatus.COMPLETED);
|
||||
task.setFinalPath(finalPath);
|
||||
saveTaskMetadata(task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完成响应
|
||||
*/
|
||||
private UploadCompleteResponse buildCompleteResponse(UploadTask task, String finalPath, long totalSize) {
|
||||
// todo:URL是逻辑url
|
||||
String fileUrl = minioBucket + "/" + finalPath;
|
||||
|
||||
return UploadCompleteResponse.builder()
|
||||
.filePath(finalPath)
|
||||
.fileUrl(fileUrl)
|
||||
.fileSize(totalSize)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存任务元数据
|
||||
*/
|
||||
private void saveTaskMetadata(UploadTask task) {
|
||||
try {
|
||||
Path metadataPath = Paths.get(tempDir, task.getUploadId(), "metadata.json");
|
||||
String json = objectMapper.writeValueAsString(task);
|
||||
Files.writeString(metadataPath, json);
|
||||
} catch (IOException e) {
|
||||
log.warn("保存任务元数据失败: uploadId={}", task.getUploadId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理临时文件
|
||||
*/
|
||||
private void cleanupTempFiles(String uploadId) {
|
||||
try {
|
||||
Path uploadDir = Paths.get(tempDir, uploadId);
|
||||
if (Files.exists(uploadDir)) {
|
||||
Files.walk(uploadDir)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.forEach(path -> {
|
||||
try {
|
||||
Files.delete(path);
|
||||
} catch (IOException e) {
|
||||
log.warn("删除临时文件失败: {}", path);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("清理临时文件失败: uploadId={}", uploadId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期上传任务(每小时执行一次)
|
||||
*/
|
||||
@Scheduled(fixedDelay = 3600000) // 1小时
|
||||
public void cleanupExpiredUploads() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
uploadTasks.entrySet().removeIf(entry -> {
|
||||
UploadTask task = entry.getValue();
|
||||
if (task.getExpiresAt().isBefore(now)) {
|
||||
cleanupTempFiles(task.getUploadId());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 工具方法 =====
|
||||
|
||||
/**
|
||||
* 标准化邮箱地址
|
||||
*/
|
||||
private String normalizeEmail(String email) {
|
||||
if (email == null) {
|
||||
return "anonymous";
|
||||
}
|
||||
return email.replaceAll("[^a-zA-Z0-9]", "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件扩展名
|
||||
*/
|
||||
private String getFileExtension(String fileName) {
|
||||
if (fileName != null && fileName.contains(".")) {
|
||||
return fileName.substring(fileName.lastIndexOf('.'));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件MIME类型
|
||||
*/
|
||||
private String getContentType(String fileType) {
|
||||
switch (fileType.toLowerCase()) {
|
||||
case "pdf":
|
||||
return "application/pdf";
|
||||
case "video":
|
||||
return "video/mp4";
|
||||
default:
|
||||
return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user