package com.designhub.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.designhub.entity.FileUpload; import com.designhub.entity.S3Config; import com.designhub.mapper.FileUploadMapper; import com.designhub.mapper.S3ConfigMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.core.sync.RequestBody; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.S3Configuration; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.URI; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.UUID; import net.coobird.thumbnailator.Thumbnails; /** * S3文件存储服务 */ @Service public class S3Service { private static final Logger log = LoggerFactory.getLogger(S3Service.class); @Autowired private S3ConfigMapper s3ConfigMapper; @Autowired private FileUploadMapper fileUploadMapper; /** * 上传文件到S3 * * @param file 文件 * @param uploaderId 上传者ID * @param uploaderName 上传者姓名 * @param relatedType 关联类型 * @param relatedId 关联ID * @return 文件URL */ public String uploadFile(MultipartFile file, Long uploaderId, String uploaderName, String relatedType, Long relatedId) throws IOException { // 获取激活的S3配置 S3Config config = s3ConfigMapper.getActiveConfig(); if (config == null) { throw new RuntimeException("S3配置未激活,请先配置S3存储"); } // 生成唯一文件名 String originalFilename = file.getOriginalFilename(); String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".")); String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8); String storedName = timestamp + "_" + uuid + fileExtension; // 构建文件路径 String filePath = config.getFilePath() + storedName; try { // 创建S3客户端 S3Client s3Client = createS3Client(config); // 上传文件 PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(config.getBucket()) .key(filePath) .contentType(file.getContentType()) .build(); s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(file.getInputStream(), file.getSize())); // 生成文件URL String fileUrl = config.getPublicDomain() + "/" + filePath; // 保存文件上传记录 FileUpload fileUpload = new FileUpload(); fileUpload.setOriginalName(originalFilename); fileUpload.setStoredName(storedName); fileUpload.setFilePath(filePath); fileUpload.setFileUrl(fileUrl); fileUpload.setFileSize(file.getSize()); fileUpload.setFileType(file.getContentType()); fileUpload.setUploaderId(uploaderId); fileUpload.setUploaderName(uploaderName); fileUpload.setRelatedType(relatedType); fileUpload.setRelatedId(relatedId); fileUploadMapper.insert(fileUpload); log.info("文件上传成功: {} -> {}", originalFilename, fileUrl); return fileUrl; } catch (Exception e) { log.error("文件上传失败: {}", originalFilename, e); throw new RuntimeException("文件上传失败: " + e.getMessage()); } } /** * 创建S3客户端 */ private S3Client createS3Client(S3Config config) { AwsBasicCredentials credentials = AwsBasicCredentials.create( config.getAccessKeyId(), config.getSecretAccessKey() ); S3Configuration s3Config = S3Configuration.builder() .pathStyleAccessEnabled(config.getEnablePathStyle()) .build(); return S3Client.builder() .region(Region.of(config.getRegion())) .credentialsProvider(StaticCredentialsProvider.create(credentials)) .endpointOverride(URI.create(config.getEndpoint())) .serviceConfiguration(s3Config) .build(); } /** * 获取文件上传记录 */ public FileUpload getFileUploadById(Long id) { return fileUploadMapper.selectById(id); } /** * 根据关联信息获取文件列表 */ public java.util.List getFilesByRelated(String relatedType, Long relatedId) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(FileUpload::getRelatedType, relatedType) .eq(FileUpload::getRelatedId, relatedId) .orderByDesc(FileUpload::getCreateTime); return fileUploadMapper.selectList(wrapper); } /** * 上传预览图(压缩到最大宽度200px,保持比例) * * @param file 图片文件 * @param uploaderId 上传者ID * @param uploaderName 上传者姓名 * @return 压缩后图片的URL */ public String uploadPreviewImage(MultipartFile file, Long uploaderId, String uploaderName) throws IOException { // 获取激活的S3配置 S3Config config = s3ConfigMapper.getActiveConfig(); if (config == null) { throw new RuntimeException("S3配置未激活,请先配置S3存储"); } // 验证是否为图片 String contentType = file.getContentType(); if (contentType == null || !contentType.startsWith("image/")) { throw new RuntimeException("只能上传图片文件"); } // 生成唯一文件名 String originalFilename = file.getOriginalFilename(); String fileExtension = originalFilename.substring(originalFilename.lastIndexOf(".")); String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8); String storedName = "preview_" + timestamp + "_" + uuid + fileExtension; // 构建文件路径(预览图专用目录) String filePath = config.getFilePath() + "previews/" + storedName; try { // 压缩图片到最大宽度200px,保持比例 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); Thumbnails.of(file.getInputStream()) .width(200) // 最大宽度200像素 .keepAspectRatio(true) // 保持比例 .outputQuality(0.85) // 图片质量 .toOutputStream(outputStream); byte[] compressedBytes = outputStream.toByteArray(); // 创建S3客户端 S3Client s3Client = createS3Client(config); // 上传压缩后的图片 PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(config.getBucket()) .key(filePath) .contentType(contentType) .build(); s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(new ByteArrayInputStream(compressedBytes), compressedBytes.length)); // 生成文件URL String fileUrl = config.getPublicDomain() + "/" + filePath; // 保存文件上传记录 FileUpload fileUpload = new FileUpload(); fileUpload.setOriginalName(originalFilename); fileUpload.setStoredName(storedName); fileUpload.setFilePath(filePath); fileUpload.setFileUrl(fileUrl); fileUpload.setFileSize((long) compressedBytes.length); fileUpload.setFileType(contentType); fileUpload.setUploaderId(uploaderId); fileUpload.setUploaderName(uploaderName); fileUpload.setRelatedType("preview_image"); fileUploadMapper.insert(fileUpload); log.info("预览图上传成功: {} -> {} (压缩后大小: {} bytes)", originalFilename, fileUrl, compressedBytes.length); return fileUrl; } catch (Exception e) { log.error("预览图上传失败: {}", originalFilename, e); throw new RuntimeException("预览图上传失败: " + e.getMessage()); } } }