package com.tcm.training.admin.upload; import com.tcm.training.admin.common.ApiResponse; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; 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 org.springframework.util.StringUtils; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import javax.validation.Valid; @Validated @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 long APP_PACKAGE_MAX_SIZE = 500 * 1024 * 1024L; // 500MB private static final Set ALLOWED_TYPES = new HashSet<>( Arrays.asList("image/jpeg", "image/png", "image/gif", "image/webp") ); private static final Set ALLOWED_VIDEO_TYPES = new HashSet<>( Arrays.asList("video/mp4", "video/quicktime", "video/x-msvideo", "video/x-matroska", "video/webm") ); private static final Set ALLOWED_APP_PACKAGE_TYPES = new HashSet<>( Arrays.asList( "application/vnd.android.package-archive", "application/octet-stream", "application/x-itunes-ipa", "application/zip", "application/x-zip-compressed" ) ); private static final String PLATFORM_ANDROID = "ANDROID"; private static final String PLATFORM_IOS = "IOS"; private final ObsUploadService obsUploadService; private final LocalAppPackageStorageService localAppPackageStorageService; private final UploadProgressService uploadProgressService; private final RemoteVideoDurationService remoteVideoDurationService; public UploadController(ObsUploadService obsUploadService, LocalAppPackageStorageService localAppPackageStorageService, UploadProgressService uploadProgressService, RemoteVideoDurationService remoteVideoDurationService) { this.obsUploadService = obsUploadService; this.localAppPackageStorageService = localAppPackageStorageService; this.uploadProgressService = uploadProgressService; this.remoteVideoDurationService = remoteVideoDurationService; } /** * 图片上传 * POST /api/v1/upload/image * @return { url: "访问地址" } */ @PostMapping("/upload/image") public ApiResponse uploadImage( @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 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)); } /** * 远程视频时长识别。 */ @PostMapping("/upload/video/duration") public ApiResponse detectVideoDuration( @Valid @RequestBody VideoDurationDetectRequest request) { return ApiResponse.ok(remoteVideoDurationService.detectDuration(request.getVideoUrl())); } @PostMapping("/upload/app-package") public ApiResponse uploadAppPackage( @RequestParam("file") MultipartFile file, @RequestParam("platform") String platform) throws IOException { String normalizedPlatform = normalizePlatform(platform); validateAppPackage(file, normalizedPlatform); UploadFileResult uploadResult = localAppPackageStorageService.store(file, normalizedPlatform); return ApiResponse.ok(new UploadResult(uploadResult.getUrl(), uploadResult.getFileName())); } @GetMapping("/upload/video/progress") public ApiResponse uploadVideoProgress( @RequestParam("taskId") String taskId) { return ApiResponse.ok(uploadProgressService.get(taskId)); } private UploadResult storeFile(MultipartFile file, long maxSize, Set allowedTypes, String typeMessage, String bizPath, String uploadTaskId) throws IOException { validateFileSize(file, maxSize); String contentType = file.getContentType(); if (contentType == null || !allowedTypes.contains(contentType)) { throw new IllegalArgumentException(typeMessage); } UploadFileResult uploadResult = obsUploadService.upload(file, bizPath, uploadTaskId); return new UploadResult(uploadResult.getUrl(), uploadResult.getFileName()); } private void validateFileSize(MultipartFile file, long maxSize) { if (file == null || file.isEmpty()) { throw new IllegalArgumentException("请选择要上传的文件"); } if (file.getSize() > maxSize) { throw new IllegalArgumentException("文件大小超出限制"); } } private void validateAppPackage(MultipartFile file, String platform) { validateFileSize(file, APP_PACKAGE_MAX_SIZE); String originalName = file.getOriginalFilename(); String extension = ""; if (StringUtils.hasText(originalName) && originalName.contains(".")) { extension = originalName.substring(originalName.lastIndexOf(".") + 1).toLowerCase(); } if (PLATFORM_ANDROID.equals(platform) && !"apk".equals(extension)) { throw new IllegalArgumentException("安卓安装包仅支持 APK 格式"); } if (PLATFORM_IOS.equals(platform) && !"ipa".equals(extension)) { throw new IllegalArgumentException("iOS 安装包仅支持 IPA 格式"); } String contentType = file.getContentType(); if (StringUtils.hasText(contentType) && !ALLOWED_APP_PACKAGE_TYPES.contains(contentType)) { throw new IllegalArgumentException("安装包格式不合法"); } } private String normalizePlatform(String platform) { if (!StringUtils.hasText(platform)) { throw new IllegalArgumentException("平台类型不能为空"); } String normalized = platform.trim().toUpperCase(); if (PLATFORM_ANDROID.equals(normalized) || PLATFORM_IOS.equals(normalized)) { return normalized; } throw new IllegalArgumentException("平台类型不合法"); } 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; } } }