backend/src/main/java/com/tcm/training/admin/upload/UploadController.java
@@ -1,37 +1,38 @@
package com.tcm.training.admin.upload;
import com.tcm.training.admin.common.ApiResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
@RestController
@RequestMapping({"/api/v1", "/api"})
public class UploadController {
    private static final long MAX_SIZE = 5 * 1024 * 1024L; // 5MB
    private static final long VIDEO_MAX_SIZE = 200 * 1024 * 1024L; // 200MB
    private static final Set<String> ALLOWED_TYPES = new HashSet<>(
            Arrays.asList("image/jpeg", "image/png", "image/gif", "image/webp")
    );
    private static final Set<String> ALLOWED_VIDEO_TYPES = new HashSet<>(
            Arrays.asList("video/mp4", "video/quicktime", "video/x-msvideo", "video/x-matroska", "video/webm")
    );
    @Value("${app.upload.dir:uploads}")
    private String uploadDir;
    private final ObsUploadService obsUploadService;
    private final UploadProgressService uploadProgressService;
    @Value("${app.upload.path-prefix:/uploads}")
    private String pathPrefix;
    public UploadController(ObsUploadService obsUploadService, UploadProgressService uploadProgressService) {
        this.obsUploadService = obsUploadService;
        this.uploadProgressService = uploadProgressService;
    }
    /**
     * 图片上传
@@ -40,67 +41,62 @@
     */
    @PostMapping("/upload/image")
    public ApiResponse<UploadResult> uploadImage(
            @RequestParam("file") MultipartFile file,
            HttpServletRequest request) throws IOException {
            @RequestParam("file") MultipartFile file) throws IOException {
        return ApiResponse.ok(storeFile(file, MAX_SIZE, ALLOWED_TYPES, "仅支持 JPG、PNG、GIF、WebP 格式", "image", null));
    }
        // 校验文件非空
    @PostMapping("/upload/video")
    public ApiResponse<UploadResult> uploadVideo(
            @RequestParam("file") MultipartFile file,
            @RequestParam(value = "uploadTaskId", required = false) String uploadTaskId) throws IOException {
        return ApiResponse.ok(storeFile(file, VIDEO_MAX_SIZE, ALLOWED_VIDEO_TYPES, "仅支持 MP4、MOV、AVI、MKV、WebM 格式", "video", uploadTaskId));
    }
    @GetMapping("/upload/video/progress")
    public ApiResponse<UploadProgressInfo> uploadVideoProgress(
            @RequestParam("taskId") String taskId) {
        return ApiResponse.ok(uploadProgressService.get(taskId));
    }
    private UploadResult storeFile(MultipartFile file,
                                   long maxSize,
                                   Set<String> allowedTypes,
                                   String typeMessage,
                                   String bizPath,
                                   String uploadTaskId) throws IOException {
        if (file == null || file.isEmpty()) {
            throw new IllegalArgumentException("请选择要上传的文件");
        }
        // 校验文件大小
        if (file.getSize() > MAX_SIZE) {
            throw new IllegalArgumentException("文件大小不能超过 5MB");
        if (file.getSize() > maxSize) {
            throw new IllegalArgumentException("文件大小超出限制");
        }
        // 校验文件类型
        String contentType = file.getContentType();
        if (contentType == null || !ALLOWED_TYPES.contains(contentType)) {
            throw new IllegalArgumentException("仅支持 JPG、PNG、GIF、WebP 格式");
        if (contentType == null || !allowedTypes.contains(contentType)) {
            throw new IllegalArgumentException(typeMessage);
        }
        // 生成子目录(按日期)和唯一文件名
        String dateDir = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
        String originalName = file.getOriginalFilename();
        String ext = "";
        if (originalName != null && originalName.contains(".")) {
            ext = originalName.substring(originalName.lastIndexOf("."));
        }
        String fileName = UUID.randomUUID().toString().replace("-", "") + ext;
        // 创建目录并保存文件(强制绝对路径,规避 transferTo 临时文件问题)
        File dir = new File(uploadDir, dateDir).getAbsoluteFile();
        if (!dir.exists() && !dir.mkdirs()) {
            throw new IOException("创建上传目录失败: " + dir.getAbsolutePath());
        }
        File dest = new File(dir, fileName);
        // 用 InputStream 手动拷贝,避免 Spring transferTo 依赖临时文件的问题
        try (java.io.InputStream in = file.getInputStream();
             java.io.OutputStream out = new java.io.FileOutputStream(dest)) {
            byte[] buf = new byte[8192];
            int n;
            while ((n = in.read(buf)) != -1) {
                out.write(buf, 0, n);
            }
        }
        // 构造访问 URL
        String baseUrl = request.getScheme() + "://" + request.getServerName()
                + ":" + request.getServerPort();
        String url = baseUrl + pathPrefix + "/" + dateDir + "/" + fileName;
        return ApiResponse.ok(new UploadResult(url));
        UploadFileResult uploadResult = obsUploadService.upload(file, bizPath, uploadTaskId);
        return new UploadResult(uploadResult.getUrl(), uploadResult.getFileName());
    }
    public static class UploadResult {
        private final String url;
        private final String fileName;
        public UploadResult(String url) {
            this(url, null);
        }
        public UploadResult(String url, String fileName) {
            this.url = url;
            this.fileName = fileName;
        }
        public String getUrl() {
            return url;
        }
        public String getFileName() {
            return fileName;
        }
    }
}