package com.talentcard.server.upload; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Locale; import java.util.UUID; import com.talentcard.server.common.BusinessException; import com.talentcard.server.common.ErrorCode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @Service public class ImageUploadService { private static final long MAX_IMAGE_SIZE = 10L * 1024 * 1024; private static final long MAX_PDF_SIZE = 20L * 1024 * 1024; private final Path uploadDirectory; @Autowired public ImageUploadService(@Value("${app.upload.directory:uploads}") String uploadDirectory) { this(Path.of(uploadDirectory)); } ImageUploadService(Path uploadDirectory) { this.uploadDirectory = uploadDirectory.toAbsolutePath().normalize(); } public ImageUploadResult upload(MultipartFile file) { if (file == null || file.isEmpty()) { throw new BusinessException(ErrorCode.BAD_REQUEST, "Please select an image"); } if (file.getSize() > MAX_IMAGE_SIZE) { throw new BusinessException(ErrorCode.BAD_REQUEST, "Image size cannot exceed 10 MB"); } String extension = detectImageExtension(file); String filename = UUID.randomUUID() + "." + extension; try { Files.createDirectories(uploadDirectory); Path destination = uploadDirectory.resolve(filename).normalize(); if (!destination.startsWith(uploadDirectory)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "Invalid image filename"); } try (InputStream input = file.getInputStream()) { Files.copy(input, destination, StandardCopyOption.REPLACE_EXISTING); } } catch (IOException exception) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "Failed to save image"); } return new ImageUploadResult("/uploads/" + filename); } public ImageUploadResult uploadPdf(MultipartFile file) { if (file == null || file.isEmpty()) { throw new BusinessException(ErrorCode.BAD_REQUEST, "请选择 PDF 文件"); } if (file.getSize() > MAX_PDF_SIZE) { throw new BusinessException(ErrorCode.BAD_REQUEST, "PDF 文件大小不能超过 20 MB"); } try (InputStream input = file.getInputStream()) { byte[] header = input.readNBytes(5); if (header.length < 5 || header[0] != '%' || header[1] != 'P' || header[2] != 'D' || header[3] != 'F' || header[4] != '-') { throw new BusinessException(ErrorCode.BAD_REQUEST, "仅支持 PDF 文件"); } } catch (IOException exception) { throw new BusinessException(ErrorCode.BAD_REQUEST, "无法读取 PDF 文件"); } String contentType = file.getContentType() == null ? "" : file.getContentType().toLowerCase(Locale.ROOT); if (!contentType.isBlank() && !"application/pdf".equals(contentType) && !"application/octet-stream".equals(contentType)) { throw new BusinessException(ErrorCode.BAD_REQUEST, "仅支持 PDF 文件"); } String filename = UUID.randomUUID() + ".pdf"; try { Files.createDirectories(uploadDirectory); Path destination = uploadDirectory.resolve(filename).normalize(); if (!destination.startsWith(uploadDirectory)) throw new BusinessException(ErrorCode.BAD_REQUEST, "Invalid PDF filename"); Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING); } catch (IOException exception) { throw new BusinessException(ErrorCode.INTERNAL_ERROR, "保存 PDF 文件失败"); } return new ImageUploadResult("/uploads/" + filename); } private String detectImageExtension(MultipartFile file) { byte[] header = new byte[12]; try (InputStream input = file.getInputStream()) { int count = input.readNBytes(header, 0, header.length); if (count >= 3 && (header[0] & 0xff) == 0xff && (header[1] & 0xff) == 0xd8 && (header[2] & 0xff) == 0xff) { return "jpg"; } if (count >= 8 && header[0] == (byte) 0x89 && header[1] == 0x50 && header[2] == 0x4e && header[3] == 0x47 && header[4] == 0x0d && header[5] == 0x0a && header[6] == 0x1a && header[7] == 0x0a) { return "png"; } if (count >= 12 && new String(header, 0, 4).equals("RIFF") && new String(header, 8, 4).equals("WEBP")) { return "webp"; } } catch (IOException exception) { throw new BusinessException(ErrorCode.BAD_REQUEST, "Unable to read image"); } String contentType = file.getContentType(); String type = contentType == null ? "" : contentType.toLowerCase(Locale.ROOT); throw new BusinessException(ErrorCode.BAD_REQUEST, "Only JPG, PNG, and WEBP images are supported" + (type.isBlank() ? "" : " (received " + type + ")")); } }