diff --git a/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java b/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java index bccbca5..702650a 100644 --- a/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java +++ b/src/main/java/com/aida/lanecarford/controller/TryOnEffectController.java @@ -27,16 +27,16 @@ public class TryOnEffectController { private final TryOnEffectService tryOnEffectService; - @Operation(summary = "生成试穿效果", description = "根据顾客照片和模特照片生成试穿效果") + @Operation(summary = "生成试穿效果", description = "根据服装,模特照片生成试穿效果,其中styleId是必选,当二次生成时,要带上相关参数,比如顾客照片") @PostMapping("/generate") - public ApiResponse generateTryOnEffect( + public ApiResponse generateTryOnEffect( @Parameter(description = "试穿效果请求参数", required = true) @Valid @RequestBody TryOnEffect tryOnEffectDto) { - String taskId = tryOnEffectService.generateTryOnEffect(tryOnEffectDto); - return ApiResponse.success(taskId); + TryOnResultVo tryOnResultVo = tryOnEffectService.generateTryOnEffect(tryOnEffectDto); + return ApiResponse.success(tryOnResultVo); } - @Operation(summary = "获取收藏的试穿效果", description = "获取指定进店记录的收藏的试穿效果") + @Operation(summary = "获取收藏的试穿效果", description = "对应library页面点击details后的显示,参数为进店记录id") @GetMapping("/favorites/{visitRecordId}") public ApiResponse> getFavoriteTryOnEffects( @Parameter(description = "进店记录ID", required = true) @@ -46,7 +46,7 @@ public class TryOnEffectController { } @GetMapping("/style/{styleId}") - @Operation(summary = "获取某套服装的所有生成结果", description = "获取某套服装的所有生成结果") + @Operation(summary = "获取某套服装的所有生成结果", description = "对应customize your look页面点击finish后的显示") public ApiResponse> getTryOnEffectsByStyleId( @Parameter(description = "服装ID", required = true) @PathVariable Long styleId) { diff --git a/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java b/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java index bd55c49..e056662 100644 --- a/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java +++ b/src/main/java/com/aida/lanecarford/controller/VisitRecordController.java @@ -5,6 +5,7 @@ import com.aida.lanecarford.entity.VisitRecord; import com.aida.lanecarford.service.VisitRecordService; import com.aida.lanecarford.vo.LibraryVo; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; @@ -47,6 +48,7 @@ public class VisitRecordController { } //根据顾客ID查询所有进店记录 @GetMapping("/customer/{customerId}") + @Operation(summary = "根据顾客ID查询所有进店记录",description = "打开libiary页面调用这个接口,参数为当前顾客的id") public ApiResponse> getByCustomerId(@PathVariable Long customerId) { log.info("开始查询顾客ID为{}的进店记录", customerId); diff --git a/src/main/java/com/aida/lanecarford/dto/CustomerPhotoDto.java b/src/main/java/com/aida/lanecarford/dto/CustomerPhotoDto.java index a532a0e..f7d136b 100644 --- a/src/main/java/com/aida/lanecarford/dto/CustomerPhotoDto.java +++ b/src/main/java/com/aida/lanecarford/dto/CustomerPhotoDto.java @@ -1,12 +1,15 @@ package com.aida.lanecarford.dto; import com.aida.lanecarford.entity.CustomerPhoto; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.web.multipart.MultipartFile; @Data +@Schema(description = "顾客照片数据传输对象") public class CustomerPhotoDto extends CustomerPhoto { @NotNull(message = "file.cannot.be.empty") + @Schema(description = "上传的照片文件", required = true, type = "string", format = "binary") private MultipartFile file; -} +} \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java b/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java index 2ed8f96..14a3c4b 100644 --- a/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java +++ b/src/main/java/com/aida/lanecarford/entity/CustomerPhoto.java @@ -1,6 +1,7 @@ package com.aida.lanecarford.entity; import com.baomidou.mybatisplus.annotation.*; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -21,29 +22,34 @@ import java.time.LocalDateTime; @Accessors(chain = true) @EqualsAndHashCode(callSuper = true) @TableName("customer_photos") +@Schema(description = "顾客照片信息") public class CustomerPhoto extends BaseEntity { /** * 顾客ID */ + @Schema(description = "顾客ID", example = "1", required = true) @TableField("customer_id") private Long customerId; /** * 进店记录ID */ + @Schema(description = "进店记录ID", example = "1", required = true) @TableField("visit_record_id") private Long visitRecordId; /** * 照片URL */ + @Schema(description = "照片URL", example = "https://example.com/photo.jpg", required = true) @TableField("photo_url") private String photoUrl; /** * 是否为主照片(0-否,1-是) */ + @Schema(description = "是否为主照片", example = "1", allowableValues = {"0", "1"}) @TableField("is_primary") private Integer isPrimary; diff --git a/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java b/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java index 16f851d..6c00a1f 100644 --- a/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java +++ b/src/main/java/com/aida/lanecarford/entity/TryOnEffect.java @@ -1,6 +1,7 @@ package com.aida.lanecarford.entity; import com.baomidou.mybatisplus.annotation.*; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; @@ -26,78 +27,91 @@ public class TryOnEffect extends BaseEntity { /** * 顾客ID */ + @Schema(description = "顾客ID", example = "1", required = true) @TableField("customer_id") private Long customerId; /** * 进店记录ID */ + @Schema(description = "进店记录ID", example = "1", required = true) @TableField("visit_record_id") private Long visitRecordId; /** - * 风格ID + * 衣服搭配ID,当is_regenerated为0时才会有值 */ + @Schema(description = "衣服搭配ID,当is_regenerated为0时才会有值", example = "1", required = true) @TableField("style_id") private Long styleId; /** - * 顾客照片ID + * 顾客照片ID,当is_regenerated为1时才会有值 */ + @Schema(description = "顾客照片ID,当is_regenerated为1时才会有值", example = "1", required = true) @TableField("customer_photo_id") private Long customerPhotoId; /** * 模特照片ID */ + @Schema(description = "模特照片ID,目前没有这个功能", example = "1", required = true) @TableField("model_photo_id") private Long modelPhotoId; /** * 提示词,当is_regenerated为1时才会有值 */ + @Schema(description = "提示词,当is_regenerated为1时才会有值", example = "1", required = true) @TableField("prompt") private String prompt; /** * 原试穿效果ID,当is_regenerated为1时才会有值 */ + @Schema(description = "原试穿效果ID,当is_regenerated为1时才会有值", example = "1", required = true) @TableField("original_try_on_id") private Long originalTryOnId; /** * 是否由生成结果重新生成(0-否,1-是) */ + @Schema(description = "是否由生成结果重新生成(0-否,1-是)", example = "1", required = true) @TableField("is_regenerated") private Integer isRegenerated; /** * 试穿结果图片URL */ + @Schema(description = "试穿结果图片URL", example = "1", required = false) @TableField("result_image_url") private String resultImageUrl; /** * 请求ID */ + @Schema(description = "请求ID", example = "1", required = false) @TableField("request_id") private String requestId; /** * 生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败) */ + @Schema(description = "生成状态(pending-等待中,processing-处理中,completed-已完成,failed-失败)", example = "1", required = false) @TableField("generation_status") private String generationStatus; /** * 错误信息 */ + @Schema(description = "错误信息", example = "1", required = false) @TableField("error_message") private String errorMessage; /** - * 是否喜欢的最终造型(0-否,1-是) + * 是否喜欢(0-否,1-是) */ + @Schema(description = "是否喜欢(0-否,1-是)", example = "1", required = false) @TableField("is_favorite") private Integer isFavorite; diff --git a/src/main/java/com/aida/lanecarford/service/ImageCompositionService.java b/src/main/java/com/aida/lanecarford/service/ImageCompositionService.java index e3617b0..c74f69b 100644 --- a/src/main/java/com/aida/lanecarford/service/ImageCompositionService.java +++ b/src/main/java/com/aida/lanecarford/service/ImageCompositionService.java @@ -17,13 +17,4 @@ public interface ImageCompositionService { * @return 合成后图片的MinIO访问URL */ String composeAndUploadImages(List imageUrls); - - /** - * 合成图片并上传到指定存储桶 - * - * @param imageUrls 图片URL列表(1-3张) - * @param bucketName 存储桶名称 - * @return 合成后图片的MinIO访问URL - */ - String composeAndUploadImages(List imageUrls, String bucketName); } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/service/TryOnEffectService.java b/src/main/java/com/aida/lanecarford/service/TryOnEffectService.java index 89c33e7..1eaaec2 100644 --- a/src/main/java/com/aida/lanecarford/service/TryOnEffectService.java +++ b/src/main/java/com/aida/lanecarford/service/TryOnEffectService.java @@ -15,7 +15,7 @@ import java.util.List; */ public interface TryOnEffectService extends IService { - String generateTryOnEffect(@Valid TryOnEffect tryOnEffectDto); + TryOnResultVo generateTryOnEffect(@Valid TryOnEffect tryOnEffectDto); List getFavoriteTryOnEffects(Long visitRecordId); diff --git a/src/main/java/com/aida/lanecarford/service/impl/CustomerPhotoServiceImpl.java b/src/main/java/com/aida/lanecarford/service/impl/CustomerPhotoServiceImpl.java index 02b22c3..2e70766 100644 --- a/src/main/java/com/aida/lanecarford/service/impl/CustomerPhotoServiceImpl.java +++ b/src/main/java/com/aida/lanecarford/service/impl/CustomerPhotoServiceImpl.java @@ -1,5 +1,6 @@ package com.aida.lanecarford.service.impl; +import com.aida.lanecarford.constant.MinioFileConstants; import com.aida.lanecarford.dto.CustomerPhotoDto; import com.aida.lanecarford.entity.CustomerPhoto; import com.aida.lanecarford.mapper.CustomerPhotoMapper; @@ -23,13 +24,16 @@ public class CustomerPhotoServiceImpl extends ServiceImpl imageUrls) { - return composeAndUploadImages(imageUrls, null); - } - - @Override - public String composeAndUploadImages(List imageUrls, String bucketName) { try { - log.info("开始合成并上传图片,图片数量: {}, 存储桶: {}", - imageUrls != null ? imageUrls.size() : 0, bucketName); + log.info("开始合成并上传图片,图片数量: {}", + imageUrls != null ? imageUrls.size() : 0); // 参数验证 if (imageUrls == null || imageUrls.isEmpty()) { @@ -59,19 +63,15 @@ public class ImageCompositionServiceImpl implements ImageCompositionService { // 合成图片 byte[] composedImageBytes = imageCompositionUtil.composeImages(validUrls); - // 生成文件名 - String fileName = imageCompositionUtil.generateComposedFileName(validUrls); + // 使用新的上传方法,返回逻辑URL + String logicalUrl = minioUtil.uploadBytes( + composedImageBytes, + MinioFileConstants.FileType.COMPOSED_IMAGE, + "image/jpeg" + ); - // 上传到MinIO - String uploadedUrl; - if (bucketName != null && !bucketName.trim().isEmpty()) { - uploadedUrl = minioUtil.uploadBytes(composedImageBytes, fileName, "image/jpeg", bucketName); - } else { - uploadedUrl = minioUtil.uploadBytes(composedImageBytes, fileName, "image/jpeg"); - } - - log.info("图片合成并上传成功,访问URL: {}", uploadedUrl); - return uploadedUrl; + log.info("图片合成并上传成功,逻辑URL: {}", logicalUrl); + return logicalUrl; } catch (Exception e) { log.error("图片合成并上传失败: {}", e.getMessage(), e); diff --git a/src/main/java/com/aida/lanecarford/service/impl/TryOnEffectServiceImpl.java b/src/main/java/com/aida/lanecarford/service/impl/TryOnEffectServiceImpl.java index 861797e..914bf2d 100644 --- a/src/main/java/com/aida/lanecarford/service/impl/TryOnEffectServiceImpl.java +++ b/src/main/java/com/aida/lanecarford/service/impl/TryOnEffectServiceImpl.java @@ -3,6 +3,7 @@ package com.aida.lanecarford.service.impl; import cn.hutool.json.JSONObject; import com.aida.lanecarford.common.CommonConstant; import com.aida.lanecarford.common.response.ResultEnum; +import com.aida.lanecarford.constant.MinioFileConstants; import com.aida.lanecarford.entity.CustomerPhoto; import com.aida.lanecarford.entity.ModelPhoto; import com.aida.lanecarford.entity.Style; @@ -53,9 +54,9 @@ public class TryOnEffectServiceImpl extends ServiceImpl imageUrls = new ArrayList<>(); @@ -64,7 +65,7 @@ public class TryOnEffectServiceImpl extends ServiceImpl 1) { log.info("开始合成图片,图片数量: {}", imageUrls.size()); try { - toAIUrl = imageCompositionService.composeAndUploadImages(imageUrls); - log.info("图片合成成功,合成图片URL: {}", toAIUrl); + // 将逻辑URL批量转换为预签名URL,以便图像合成服务可以访问 + List presignedUrls = minioUtil.convertToPresignedUrls(imageUrls, CommonConstant.MINIO_IMAGE_EXPIRE_TIME); + log.debug("批量转换逻辑URL为预签名URL,数量: {}", presignedUrls.size()); + + toAIlogicalUrl = imageCompositionService.composeAndUploadImages(presignedUrls); + + log.info("图片合成成功,合成图片URL: {}", toAIlogicalUrl); } catch (Exception e) { log.error("图片合成失败: {}", e.getMessage(), e); throw new RuntimeException("image error " + e.getMessage(), e); } + } else if (imageUrls.size() == 1) { + toAIlogicalUrl = imageUrls.get(0); } else { log.warn("没有找到有效的图片URL进行合成"); - throw new RuntimeException("image cannot be null"); + throw BusinessException.parameterRequired("image"); } } //调用模型生成试穿效果 - log.info("准备调用第三方AI服务,输入图片URL: {}", toAIUrl); - String AIRreultUrl = AITryOnEffect(prompt, toAIUrl); + log.info("准备调用第三方AI服务,输入图片URL: {}", toAIlogicalUrl); + String AIRreultlogicalUrl = AITryOnEffect(prompt, toAIlogicalUrl); - tryOnEffectDto.setResultImageUrl(AIRreultUrl); + tryOnEffectDto.setResultImageUrl(AIRreultlogicalUrl); this.saveOrUpdate(tryOnEffectDto); + TryOnResultVo tryOnResultVo = new TryOnResultVo(); + tryOnResultVo.setTryOnId(tryOnEffectDto.getId()); - return "taskId"; + tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl(AIRreultlogicalUrl, CommonConstant.MINIO_IMAGE_EXPIRE_TIME)); + + return tryOnResultVo; } + //library页面点击details后的显示 @Override public List getFavoriteTryOnEffects(Long visitRecordId) { List tryOnEffects = this.list(new LambdaQueryWrapper() @@ -138,17 +151,18 @@ public class TryOnEffectServiceImpl extends ServiceImpl styleLambdaQueryWrapper = new LambdaQueryWrapper<>(); styleLambdaQueryWrapper.eq(Style::getId, tryOnEffect.getStyleId()).select(Style::getStyleImageUrl); Style style = styleService.getOne(styleLambdaQueryWrapper); - tryOnResultVo.setStyleUrl(minioUtil.getPresignedUrl( - style.getStyleImageUrl(), + tryOnResultVo.setStyleUrl(minioUtil.convertToPresignedUrl( + style.getStyleImageUrl(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME )); } @@ -160,6 +174,7 @@ public class TryOnEffectServiceImpl extends ServiceImpl getTryOnEffectsByStyleId(Long styleId) { List tryOnEffects = this.list(new LambdaQueryWrapper() @@ -168,19 +183,20 @@ public class TryOnEffectServiceImpl extends ServiceImpl styleLambdaQueryWrapper = new LambdaQueryWrapper<>(); styleLambdaQueryWrapper.eq(Style::getId, tryOnEffect.getStyleId()).select(Style::getStyleImageUrl); Style style = styleService.getOne(styleLambdaQueryWrapper); - tryOnResultVo.setStyleUrl(minioUtil.getPresignedUrl( - style.getStyleImageUrl(), - CommonConstant.MINIO_IMAGE_EXPIRE_TIME - )); + tryOnResultVo.setStyleUrl(minioUtil.convertToPresignedUrl( + style.getStyleImageUrl(), + CommonConstant.MINIO_IMAGE_EXPIRE_TIME + )); } tryOnResultVo.setIsRegenerated(tryOnEffect.getIsRegenerated()); @@ -195,12 +211,12 @@ public class TryOnEffectServiceImpl extends ServiceImpl originalUrls) { - String timestamp = String.valueOf(System.currentTimeMillis()); - String suffix = originalUrls.size() + "_images_composed"; - return String.format("composed_%s_%s.jpg", suffix, timestamp); + return MinioFileConstants.generateComposedImageWithTimestampObjectName(originalUrls.size()); } } \ No newline at end of file diff --git a/src/main/java/com/aida/lanecarford/util/MinioUtil.java b/src/main/java/com/aida/lanecarford/util/MinioUtil.java index e818ea0..faaf8fb 100644 --- a/src/main/java/com/aida/lanecarford/util/MinioUtil.java +++ b/src/main/java/com/aida/lanecarford/util/MinioUtil.java @@ -1,6 +1,7 @@ package com.aida.lanecarford.util; import com.aida.lanecarford.config.MinioConfig; +import com.aida.lanecarford.constant.MinioFileConstants; import com.aida.lanecarford.exception.MinioException; import io.minio.*; import io.minio.errors.*; @@ -9,7 +10,6 @@ import io.minio.messages.Bucket; import io.minio.messages.Item; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; @@ -22,18 +22,20 @@ import java.io.IOException; import java.io.InputStream; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Base64; import java.util.List; -import java.util.UUID; import java.util.concurrent.TimeUnit; /** - * MinIO 工具类 - * 提供文件上传、下载、删除等操作 - * + * MinIO 工具类 - 重构版本 + * 统一命名规范和URL管理 + *

+ * 设计原则: + * 1. 统一使用一个桶(lanecarford) + * 2. 数据库存储逻辑URL(不含桶名的相对路径) + * 3. 对外提供预签名URL用于访问 + * * @author Aida * @since 2024-01-01 */ @@ -43,14 +45,12 @@ import java.util.concurrent.TimeUnit; public class MinioUtil { private final MinioClient minioClient; - private final MinioConfig minioConfig; + // ==================== 基础桶操作 ==================== + /** * 检查存储桶是否存在 - * - * @param bucketName 存储桶名称 - * @return 是否存在 */ public boolean bucketExists(String bucketName) { try { @@ -63,8 +63,6 @@ public class MinioUtil { /** * 创建存储桶 - * - * @param bucketName 存储桶名称 */ public void createBucket(String bucketName) { try { @@ -80,8 +78,6 @@ public class MinioUtil { /** * 获取所有存储桶 - * - * @return 存储桶列表 */ public List getAllBuckets() { try { @@ -92,62 +88,39 @@ public class MinioUtil { } } - /** - * 删除存储桶 - * - * @param bucketName 存储桶名称 - */ - public void removeBucket(String bucketName) { - try { - minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build()); - log.info("删除存储桶成功: {}", bucketName); - } catch (Exception e) { - log.error("删除存储桶失败: {}", e.getMessage(), e); - throw new MinioException("删除存储桶失败", e); - } - } + // ==================== 文件上传操作 ==================== /** - * 上传文件 - * - * @param file 文件 - * @return 文件访问URL + * 上传MultipartFile文件 + * + * @param file 文件 + * @param fileType 文件类型 + * @return 逻辑URL(不含桶名) */ - public String uploadFile(MultipartFile file) { - return uploadFile(file, minioConfig.getBucketName()); - } - - /** - * 上传文件到指定存储桶 - * - * @param file 文件 - * @param bucketName 存储桶名称 - * @return 文件访问URL - */ - public String uploadFile(MultipartFile file, String bucketName) { + public String uploadFile(MultipartFile file, MinioFileConstants.FileType fileType) { if (file == null || file.isEmpty()) { throw new MinioException("文件不能为空"); } try { - // 确保存储桶存在 - createBucket(bucketName); + // 确保默认桶存在 + ensureDefaultBucketExists(); + + // 生成对象名称 + String objectName = MinioFileConstants.generateObjectNameByType(fileType); - // 生成文件名 - String fileName = generateFileName(file.getOriginalFilename()); - // 上传文件 minioClient.putObject( PutObjectArgs.builder() - .bucket(bucketName) - .object(fileName) + .bucket(minioConfig.getBucketName()) + .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build() ); - log.info("文件上传成功: {}", fileName); - return getFileUrl(bucketName, fileName); + log.info("文件上传成功: {}", objectName); + return objectName; // 返回逻辑URL } catch (Exception e) { log.error("文件上传失败: {}", e.getMessage(), e); throw new MinioException("文件上传失败", e); @@ -156,41 +129,25 @@ public class MinioUtil { /** * 上传字节数组 - * + * * @param bytes 字节数组 - * @param fileName 文件名 + * @param objectName 对象名称(逻辑路径) * @param contentType 内容类型 - * @return 文件访问URL + * @return 逻辑URL(不含桶名) */ - public String uploadBytes(byte[] bytes, String fileName, String contentType) { - return uploadBytes(bytes, fileName, contentType, minioConfig.getBucketName()); - } - - /** - * 上传字节数组到指定存储桶 - * - * @param bytes 字节数组 - * @param fileName 文件名 - * @param contentType 内容类型 - * @param bucketName 存储桶名称 - * @return 文件访问URL - */ - public String uploadBytes(byte[] bytes, String fileName, String contentType, String bucketName) { + public String uploadBytes(byte[] bytes, String objectName, String contentType) { if (bytes == null || bytes.length == 0) { throw new MinioException("文件内容不能为空"); } try { - // 确保存储桶存在 - createBucket(bucketName); - - // 生成文件名:TODO 确定好桶的位置后 - String objectName = generateFileName(fileName); + // 确保默认桶存在 + ensureDefaultBucketExists(); // 上传文件 minioClient.putObject( PutObjectArgs.builder() - .bucket(bucketName) + .bucket(minioConfig.getBucketName()) .object(objectName) .stream(new ByteArrayInputStream(bytes), bytes.length, -1) .contentType(contentType) @@ -198,7 +155,7 @@ public class MinioUtil { ); log.info("字节数组上传成功: {}", objectName); - return getFileUrl(bucketName, objectName); + return objectName; // 返回逻辑URL } catch (Exception e) { log.error("字节数组上传失败: {}", e.getMessage(), e); throw new MinioException("字节数组上传失败", e); @@ -206,36 +163,37 @@ public class MinioUtil { } /** - * 下载文件 - * - * @param fileName 文件名 - * @return 文件输入流 + * 上传字节数组(根据文件类型自动生成对象名) + * + * @param bytes 字节数组 + * @param fileType 文件类型 + * @param contentType 内容类型 + * @return 逻辑URL(不含桶名) */ - public InputStream downloadFile(String fileName) { - return downloadFile(minioConfig.getBucketName(), fileName); + public String uploadBytes(byte[] bytes, MinioFileConstants.FileType fileType, String contentType) { + String objectName = MinioFileConstants.generateObjectNameByType(fileType); + return uploadBytes(bytes, objectName, contentType); } + // ==================== 文件下载操作 ==================== + /** - * 从指定存储桶下载文件 - * - * @param bucketName 存储桶名称 - * @param fileName 文件名 + * 下载文件 + * + * @param objectName 对象名称(逻辑路径) * @return 文件输入流 */ - public InputStream downloadFile(String bucketName, String fileName) { + public InputStream downloadFile(String objectName) { try { - // 确保存储桶存在,如果不存在则创建 - createBucket(bucketName); - // 检查文件是否存在 - if (!doesObjectExist(bucketName, fileName)) { - throw new IOException("File " + fileName + " does not exist in bucket " + bucketName); + if (!doesObjectExist(objectName)) { + throw new IOException("文件不存在: " + objectName); } - + return minioClient.getObject( GetObjectArgs.builder() - .bucket(bucketName) - .object(fileName) + .bucket(minioConfig.getBucketName()) + .object(objectName) .build() ); } catch (Exception e) { @@ -244,59 +202,42 @@ public class MinioUtil { } } - /** - * 删除文件 - * - * @param fileName 文件名 - */ - public void deleteFile(String fileName) { - deleteFile(minioConfig.getBucketName(), fileName); - } + // ==================== 文件删除操作 ==================== /** - * 从指定存储桶删除文件 - * - * @param bucketName 存储桶名称 - * @param fileName 文件名 + * 删除文件 + * + * @param objectName 对象名称(逻辑路径) */ - public void deleteFile(String bucketName, String fileName) { + public void deleteFile(String objectName) { try { minioClient.removeObject( RemoveObjectArgs.builder() - .bucket(bucketName) - .object(fileName) + .bucket(minioConfig.getBucketName()) + .object(objectName) .build() ); - log.info("文件删除成功: {}", fileName); + log.info("文件删除成功: {}", objectName); } catch (Exception e) { log.error("文件删除失败: {}", e.getMessage(), e); throw new MinioException("文件删除失败", e); } } - /** - * 获取文件列表 - * - * @param bucketName 存储桶名称 - * @return 文件列表 - */ - public List listFiles(String bucketName) { - return listFiles(bucketName, null); - } + // ==================== 文件查询操作 ==================== /** * 获取文件列表 - * - * @param bucketName 存储桶名称 - * @param prefix 文件前缀 + * + * @param prefix 文件前缀 * @return 文件列表 */ - public List listFiles(String bucketName, String prefix) { + public List listFiles(String prefix) { List files = new ArrayList<>(); try { Iterable> results = minioClient.listObjects( ListObjectsArgs.builder() - .bucket(bucketName) + .bucket(minioConfig.getBucketName()) .prefix(prefix) .build() ); @@ -314,27 +255,16 @@ public class MinioUtil { /** * 检查文件是否存在 - * - * @param fileName 文件名 + * + * @param objectName 对象名称(逻辑路径) * @return 是否存在 */ - public boolean fileExists(String fileName) { - return fileExists(minioConfig.getBucketName(), fileName); - } - - /** - * 检查文件是否存在 - * - * @param bucketName 存储桶名称 - * @param fileName 文件名 - * @return 是否存在 - */ - public boolean fileExists(String bucketName, String fileName) { + public boolean doesObjectExist(String objectName) { try { minioClient.statObject( StatObjectArgs.builder() - .bucket(bucketName) - .object(fileName) + .bucket(minioConfig.getBucketName()) + .object(objectName) .build() ); return true; @@ -343,32 +273,25 @@ public class MinioUtil { } } - /** - * 获取文件预签名URL(用于临时访问) - * - * @param fileName 文件名 - * @param expires 过期时间(秒) - * @return 预签名URL - */ - public String getPresignedUrl(String fileName, int expires) { - return getPresignedUrl(minioConfig.getBucketName(), fileName, expires); - } + // ==================== URL管理操作 ==================== /** - * 获取文件预签名URL(用于临时访问) - * - * @param bucketName 存储桶名称 - * @param fileName 文件名 + * 获取预签名URL(用于临时访问) + * + * @param objectName 对象名称(逻辑路径) * @param expires 过期时间(秒) * @return 预签名URL */ - public String getPresignedUrl(String bucketName, String fileName, int expires) { + public String getPresignedUrl(String objectName, int expires, String bucketName) { try { + if (bucketName == null){ + bucketName = minioConfig.getBucketName(); + } return minioClient.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName) - .object(fileName) + .object(objectName) .expiry(expires, TimeUnit.SECONDS) .build() ); @@ -379,69 +302,83 @@ public class MinioUtil { } /** - * 获取文件访问URL - * - * @param bucketName 存储桶名称 - * @param fileName 文件名 - * @return 文件访问URL + * 批量获取预签名URL + * + * @param objectNames 对象名称列表(逻辑路径) + * @param expires 过期时间(秒) + * @return 预签名URL列表 */ - public String getFileUrl(String bucketName, String fileName) { - return minioConfig.getEndpoint() + "/" + bucketName + "/" + fileName; + public List getPresignedUrls(List objectNames, int expires) { + List presignedUrls = new ArrayList<>(); + for (String objectName : objectNames) { + if (objectName != null && !objectName.trim().isEmpty()) { + presignedUrls.add(getPresignedUrl(objectName, expires, null)); + } + } + return presignedUrls; } /** - * 生成唯一文件名 - * - * @param originalFilename 原始文件名 - * @return 生成的文件名 + * 判断URL是否为MinIO逻辑URL + * + * @param url URL字符串 + * @return 是否为MinIO逻辑URL */ - private String generateFileName(String originalFilename) { - if (originalFilename == null || originalFilename.trim().isEmpty()) { - throw new MinioException("文件名不能为空"); + public boolean isMinioLogicalUrl(String url) { + if (url == null || url.trim().isEmpty()) { + return false; } - // 获取文件扩展名 - String extension = ""; - int lastDotIndex = originalFilename.lastIndexOf("."); - if (lastDotIndex > 0) { - extension = originalFilename.substring(lastDotIndex); - } - - // 生成唯一文件名 - String uuid = UUID.randomUUID().toString().replace("-", ""); - - return uuid + extension; + // 逻辑URL不包含协议和域名,只是相对路径 + return !url.startsWith("http://") && !url.startsWith("https://") && + (url.contains("/") || url.endsWith(".jpg") || url.endsWith(".png") || url.endsWith(".jpeg")); } + /** + * 将MinIO逻辑URL转换为预签名URL + * + * @param logicalUrl 逻辑URL + * @param expires 过期时间(秒) + * @return 预签名URL,如果不是逻辑URL则返回原URL + */ + public String convertToPresignedUrl(String logicalUrl, int expires) { + if (isMinioLogicalUrl(logicalUrl)) { + return getPresignedUrl(logicalUrl, expires, null); + } + return logicalUrl; + } + + /** + * 批量转换逻辑URL为预签名URL + * + * @param logicalUrls 逻辑URL列表 + * @param expires 过期时间(秒) + * @return 预签名URL列表 + */ + public List convertToPresignedUrls(List logicalUrls, int expires) { + List presignedUrls = new ArrayList<>(); + for (String logicalUrl : logicalUrls) { + presignedUrls.add(convertToPresignedUrl(logicalUrl, expires)); + } + return presignedUrls; + } + + // ==================== 图片处理操作 ==================== + /** * 获取压缩后的图片Base64编码 - * @param path 图片的minio路径 - * @param targetWidth 目标宽度 + * + * @param objectName 对象名称(逻辑路径) + * @param targetWidth 目标宽度 * @param targetHeight 目标高度 * @return 压缩后的图片Base64编码 - * @throws IOException */ - public String getCompressedImageAsBase64(String path, int targetWidth, int targetHeight) throws IOException { - int index = path.indexOf("/"); - String bucketName = path.substring(0, index); - String fileName = path.substring(index + 1); - - // 检查桶是否存在 - boolean found = doesObjectExist(bucketName, fileName); - if (!found) { - throw new IOException("Bucket " + bucketName + " does not exist"); - } - - try (InputStream stream = minioClient.getObject( - GetObjectArgs.builder() - .bucket(bucketName) - .object(fileName) - .build())) { - + public String getCompressedImageAsBase64(String objectName, int targetWidth, int targetHeight) throws IOException { + try (InputStream stream = downloadFile(objectName)) { // 读取原始图片 BufferedImage originalImage = ImageIO.read(stream); if (originalImage == null) { - throw new IOException("无法读取图片: " + path); + throw new IOException("无法读取图片: " + objectName); } // 计算压缩比例,保持宽高比 @@ -470,42 +407,57 @@ public class MinioUtil { // 转换为Base64 ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ImageIO.write(compressedImage, "JPEG", baos); // 使用JPEG格式以减小文件大小 + ImageIO.write(compressedImage, "JPEG", baos); byte[] imageBytes = baos.toByteArray(); - log.info("图片压缩完成: {} -> {}x{} (原始: {}x{})", path, newWidth, newHeight, originalWidth, originalHeight); + log.info("图片压缩完成: {} -> {}x{} (原始: {}x{})", objectName, newWidth, newHeight, originalWidth, originalHeight); return Base64.getEncoder().encodeToString(imageBytes); - } catch (ServerException e) { - throw new RuntimeException(e); - } catch (InsufficientDataException e) { - throw new RuntimeException(e); - } catch (ErrorResponseException e) { - throw new RuntimeException(e); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } catch (InvalidKeyException e) { - throw new RuntimeException(e); - } catch (InvalidResponseException e) { - throw new RuntimeException(e); - } catch (XmlParserException e) { - throw new RuntimeException(e); - } catch (InternalException e) { - throw new RuntimeException(e); + } catch (Exception e) { + log.error("图片压缩失败: {}", e.getMessage(), e); + throw new IOException("图片压缩失败", e); } } - public boolean doesObjectExist(String bucketName, String objectName) { + + // ==================== 私有辅助方法 ==================== + + /** + * 确保默认桶存在 + */ + private void ensureDefaultBucketExists() { + createBucket(minioConfig.getBucketName()); + } + + + /** + * @deprecated 使用 uploadBytes(byte[], String, String) 替代 + */ + @Deprecated + public String uploadBytes(byte[] bytes, String fileName, String contentType, String bucketName) { + log.warn("使用了已废弃的方法,建议使用新的API"); + if (!bucketName.equals(minioConfig.getBucketName())) { + log.warn("指定的桶名 {} 与配置的桶名 {} 不一致,将使用配置的桶名", bucketName, minioConfig.getBucketName()); + } + return uploadBytes(bytes, fileName, contentType); + } + + /** + * @deprecated 使用 getPresignedUrl(String, int) 替代 + */ + @Deprecated + public String getFileUrl(String bucketName, String fileName) { + log.warn("使用了已废弃的方法 getFileUrl,建议使用 getPresignedUrl"); + return minioConfig.getEndpoint() + "/" + bucketName + "/" + fileName; + } + + /** + * 获取文件字节数组 + */ + private byte[] getFileBytes(MultipartFile file) { try { - minioClient.statObject( - StatObjectArgs.builder() - .bucket(bucketName) - .object(objectName) - .build() - ); - return true; - } catch (Exception e) { - // 如果发生异常,说明文件不存在或者出现了其他错误 - return false; + return file.getBytes(); + } catch (IOException e) { + throw new MinioException("读取文件字节失败", e); } } } \ No newline at end of file