minio整理,一些返回逻辑修改
This commit is contained in:
@@ -27,16 +27,16 @@ public class TryOnEffectController {
|
||||
|
||||
private final TryOnEffectService tryOnEffectService;
|
||||
|
||||
@Operation(summary = "生成试穿效果", description = "根据顾客照片和模特照片生成试穿效果")
|
||||
@Operation(summary = "生成试穿效果", description = "根据服装,模特照片生成试穿效果,其中styleId是必选,当二次生成时,要带上相关参数,比如顾客照片")
|
||||
@PostMapping("/generate")
|
||||
public ApiResponse<String> generateTryOnEffect(
|
||||
public ApiResponse<TryOnResultVo> 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<List<TryOnResultVo>> 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<List<TryOnResultVo>> getTryOnEffectsByStyleId(
|
||||
@Parameter(description = "服装ID", required = true)
|
||||
@PathVariable Long styleId) {
|
||||
|
||||
@@ -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<List<LibraryVo>> getByCustomerId(@PathVariable Long customerId) {
|
||||
log.info("开始查询顾客ID为{}的进店记录", customerId);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -17,13 +17,4 @@ public interface ImageCompositionService {
|
||||
* @return 合成后图片的MinIO访问URL
|
||||
*/
|
||||
String composeAndUploadImages(List<String> imageUrls);
|
||||
|
||||
/**
|
||||
* 合成图片并上传到指定存储桶
|
||||
*
|
||||
* @param imageUrls 图片URL列表(1-3张)
|
||||
* @param bucketName 存储桶名称
|
||||
* @return 合成后图片的MinIO访问URL
|
||||
*/
|
||||
String composeAndUploadImages(List<String> imageUrls, String bucketName);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import java.util.List;
|
||||
*/
|
||||
public interface TryOnEffectService extends IService<TryOnEffect> {
|
||||
|
||||
String generateTryOnEffect(@Valid TryOnEffect tryOnEffectDto);
|
||||
TryOnResultVo generateTryOnEffect(@Valid TryOnEffect tryOnEffectDto);
|
||||
|
||||
List<TryOnResultVo> getFavoriteTryOnEffects(Long visitRecordId);
|
||||
|
||||
|
||||
@@ -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<CustomerPhotoMapper, C
|
||||
|
||||
@Override
|
||||
public CustomerPhoto upload(CustomerPhotoDto customerPhotoDto) {
|
||||
//TODO:设置桶名
|
||||
String url = minioUtil.uploadFile(customerPhotoDto.getFile());
|
||||
|
||||
String logicalUrl = minioUtil.uploadFile(
|
||||
customerPhotoDto.getFile(),
|
||||
MinioFileConstants.FileType.CUSTOMER_PHOTO
|
||||
);
|
||||
|
||||
CustomerPhoto customerPhoto = CopyUtil.copyObject(customerPhotoDto, CustomerPhoto.class);
|
||||
customerPhoto.setPhotoUrl(url);
|
||||
this.save(customerPhoto);
|
||||
|
||||
customerPhoto.setPhotoUrl(logicalUrl);
|
||||
this.save(customerPhoto);
|
||||
|
||||
return customerPhoto;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.aida.lanecarford.service.impl;
|
||||
|
||||
import com.aida.lanecarford.config.MinioConfig;
|
||||
import com.aida.lanecarford.constant.MinioFileConstants;
|
||||
import com.aida.lanecarford.service.ImageCompositionService;
|
||||
import com.aida.lanecarford.util.ImageCompositionUtil;
|
||||
import com.aida.lanecarford.util.MinioUtil;
|
||||
@@ -23,16 +25,18 @@ public class ImageCompositionServiceImpl implements ImageCompositionService {
|
||||
private final ImageCompositionUtil imageCompositionUtil;
|
||||
private final MinioUtil minioUtil;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 合成并上传图片
|
||||
*/
|
||||
@Override
|
||||
public String composeAndUploadImages(List<String> imageUrls) {
|
||||
return composeAndUploadImages(imageUrls, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String composeAndUploadImages(List<String> 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);
|
||||
|
||||
@@ -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<TryOnEffectMapper, TryOn
|
||||
private final MinioUtil minioUtil;
|
||||
|
||||
@Override
|
||||
public String generateTryOnEffect(TryOnEffect tryOnEffectDto) {
|
||||
public TryOnResultVo generateTryOnEffect(TryOnEffect tryOnEffectDto) {
|
||||
Integer isRegenerated = tryOnEffectDto.getIsRegenerated();
|
||||
String toAIUrl = null;
|
||||
String toAIlogicalUrl = null;
|
||||
String prompt = null;
|
||||
// 收集图片URL
|
||||
List<String> imageUrls = new ArrayList<>();
|
||||
@@ -64,7 +65,7 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
Long originalTryOnId = tryOnEffectDto.getOriginalTryOnId();
|
||||
// 验证originalTryOnId不能为空
|
||||
if (originalTryOnId == null) {
|
||||
throw new RuntimeException("originalTryOnId cannot be null");
|
||||
throw BusinessException.parameterRequired("originalTryOnId");
|
||||
}
|
||||
TryOnEffect originalTryOn = this.getById(originalTryOnId);
|
||||
String resultImageUrl = originalTryOn.getResultImageUrl();
|
||||
@@ -83,7 +84,7 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
Long styleId = tryOnEffectDto.getStyleId();
|
||||
// 验证styleId不能为空
|
||||
if (styleId == null) {
|
||||
throw new RuntimeException("styleId cannot be null");
|
||||
throw BusinessException.parameterRequired("styleId");
|
||||
}
|
||||
//根据id查到对应styleurl
|
||||
Style style = styleService.getById(styleId);
|
||||
@@ -91,7 +92,7 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
if (styleImageUrl != null && !styleImageUrl.trim().isEmpty()) {
|
||||
imageUrls.add(styleImageUrl);
|
||||
}
|
||||
|
||||
|
||||
Long modelPhotoId = tryOnEffectDto.getModelPhotoId();
|
||||
if (modelPhotoId != null) {
|
||||
//根据id查到对应modelurl
|
||||
@@ -103,32 +104,44 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
}
|
||||
|
||||
// 合成图片
|
||||
if (!imageUrls.isEmpty()) {
|
||||
if (!imageUrls.isEmpty() && imageUrls.size() > 1) {
|
||||
log.info("开始合成图片,图片数量: {}", imageUrls.size());
|
||||
try {
|
||||
toAIUrl = imageCompositionService.composeAndUploadImages(imageUrls);
|
||||
log.info("图片合成成功,合成图片URL: {}", toAIUrl);
|
||||
// 将逻辑URL批量转换为预签名URL,以便图像合成服务可以访问
|
||||
List<String> 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<TryOnResultVo> getFavoriteTryOnEffects(Long visitRecordId) {
|
||||
List<TryOnEffect> tryOnEffects = this.list(new LambdaQueryWrapper<TryOnEffect>()
|
||||
@@ -138,17 +151,18 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
for (TryOnEffect tryOnEffect : tryOnEffects) {
|
||||
TryOnResultVo tryOnResultVo = new TryOnResultVo();
|
||||
tryOnResultVo.setTryOnId(tryOnEffect.getId());
|
||||
tryOnResultVo.setTryOnUrl(minioUtil.getPresignedUrl(
|
||||
// 使用新的API获取预签名URL,数据库存储的是逻辑URL
|
||||
tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl(
|
||||
tryOnEffect.getResultImageUrl(),
|
||||
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
|
||||
));
|
||||
// 如果是原始效果,则获取对应的style图片
|
||||
if (tryOnEffect.getIsRegenerated()==0){
|
||||
if (tryOnEffect.getIsRegenerated() == 0) {
|
||||
LambdaQueryWrapper<Style> 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<TryOnEffectMapper, TryOn
|
||||
return tryOnResultVos;
|
||||
}
|
||||
|
||||
//目前用于customize your look页面点击finish后的显示
|
||||
@Override
|
||||
public List<TryOnResultVo> getTryOnEffectsByStyleId(Long styleId) {
|
||||
List<TryOnEffect> tryOnEffects = this.list(new LambdaQueryWrapper<TryOnEffect>()
|
||||
@@ -168,19 +183,20 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
for (TryOnEffect tryOnEffect : tryOnEffects) {
|
||||
TryOnResultVo tryOnResultVo = new TryOnResultVo();
|
||||
tryOnResultVo.setTryOnId(tryOnEffect.getId());
|
||||
tryOnResultVo.setTryOnUrl(minioUtil.getPresignedUrl(
|
||||
tryOnEffect.getResultImageUrl(),
|
||||
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
|
||||
));
|
||||
// 使用新的API获取预签名URL,数据库存储的是逻辑URL
|
||||
tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl(
|
||||
tryOnEffect.getResultImageUrl(),
|
||||
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
|
||||
));
|
||||
// 如果是原始效果,则获取对应的style图片
|
||||
if (tryOnEffect.getIsRegenerated()==0){
|
||||
if (tryOnEffect.getIsRegenerated() == 0) {
|
||||
LambdaQueryWrapper<Style> 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<TryOnEffectMapper, TryOn
|
||||
if (tryOnId == null) {
|
||||
throw new BusinessException("TryOn ID is required", "试穿效果ID不能为空", ResultEnum.PARAMETER_ERROR.getCode());
|
||||
}
|
||||
|
||||
|
||||
TryOnEffect tryOnEffect = this.getById(tryOnId);
|
||||
if (tryOnEffect == null) {
|
||||
throw new BusinessException("TryOn effect not found", "试穿效果不存在", ResultEnum.FAIL.getCode());
|
||||
}
|
||||
|
||||
|
||||
// 设置为收藏
|
||||
tryOnEffect.setIsFavorite(1);
|
||||
this.updateById(tryOnEffect);
|
||||
@@ -212,12 +228,12 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
if (tryOnId == null) {
|
||||
throw new BusinessException("TryOn ID is required", "试穿效果ID不能为空", ResultEnum.PARAMETER_ERROR.getCode());
|
||||
}
|
||||
|
||||
|
||||
TryOnEffect tryOnEffect = this.getById(tryOnId);
|
||||
if (tryOnEffect == null) {
|
||||
throw new BusinessException("TryOn effect not found", "试穿效果不存在", ResultEnum.FAIL.getCode());
|
||||
}
|
||||
|
||||
|
||||
// 取消收藏
|
||||
tryOnEffect.setIsFavorite(0);
|
||||
this.updateById(tryOnEffect);
|
||||
@@ -225,50 +241,49 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String AITryOnEffect(String prompt, String url) {
|
||||
log.info("开始执行AITryOnEffect - prompt: {}, url: {}", prompt, url);
|
||||
|
||||
|
||||
// 参数验证
|
||||
if (prompt == null || prompt.trim().isEmpty()) {
|
||||
log.error("参数验证失败 - prompt不能为空");
|
||||
throw new BusinessException("Prompt is required", "prompt不能为空", ResultEnum.PARAMETER_ERROR.getCode());
|
||||
}
|
||||
|
||||
|
||||
if (url == null || url.trim().isEmpty()) {
|
||||
log.error("参数验证失败 - url不能为空");
|
||||
throw new BusinessException("URL is required", "url不能为空", ResultEnum.PARAMETER_ERROR.getCode());
|
||||
}
|
||||
|
||||
|
||||
// 获取图片的base64编码
|
||||
String base64Image = getImageAsBase64(url);
|
||||
|
||||
|
||||
// 调用谷歌API进行图生图
|
||||
String resultImageUrl = callGoogleImageGenerationAPI(prompt, base64Image);
|
||||
|
||||
|
||||
log.info("AITryOnEffect执行成功,结果URL: {}", resultImageUrl);
|
||||
return resultImageUrl;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取图片的base64编码
|
||||
*/
|
||||
private String getImageAsBase64(String imageUrl) {
|
||||
try {
|
||||
// 从MinIO下载图片并转换为Base64
|
||||
InputStream inputStream = minioUtil.downloadFile(imageUrl);
|
||||
byte[] imageBytes = inputStream.readAllBytes();
|
||||
inputStream.close();
|
||||
|
||||
// 转换为Base64编码
|
||||
String base64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
|
||||
return base64;
|
||||
log.info("开始下载图片并转换为Base64: {}", imageUrl);
|
||||
// 使用新的下载方法,imageUrl是逻辑URL
|
||||
try (InputStream inputStream = minioUtil.downloadFile(imageUrl)) {
|
||||
byte[] imageBytes = inputStream.readAllBytes();
|
||||
String base64 = java.util.Base64.getEncoder().encodeToString(imageBytes);
|
||||
log.info("图片转换为Base64成功,长度: {}", base64.length());
|
||||
return base64;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("获取图片base64编码失败: {}", e.getMessage(), e);
|
||||
throw new BusinessException("Image processing failed", "图片处理失败", ResultEnum.ERROR.getCode());
|
||||
log.error("下载图片或转换Base64失败: {}", e.getMessage(), e);
|
||||
throw new BusinessException("Image download failed", "图片下载失败", ResultEnum.ERROR.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 调用谷歌API进行图像生成
|
||||
*/
|
||||
@@ -284,16 +299,16 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
"https://aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:generateContent",
|
||||
projectId, location, model
|
||||
);
|
||||
|
||||
|
||||
// 构建请求体
|
||||
JSONObject requestBody = buildRequestBody(prompt, base64Image);
|
||||
|
||||
|
||||
// 获取访问令牌
|
||||
String accessToken = getGoogleAccessToken();
|
||||
|
||||
|
||||
// 发送HTTP请求
|
||||
String response = sendHttpRequest(endpoint, requestBody.toString(), accessToken);
|
||||
|
||||
|
||||
// 解析响应并提取图片
|
||||
return processGoogleAPIResponse(response);
|
||||
} catch (Exception e) {
|
||||
@@ -301,66 +316,66 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
throw new BusinessException("Google API call failed", "Google API调用失败", ResultEnum.ERROR.getCode());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建谷歌API请求体
|
||||
*/
|
||||
private JSONObject buildRequestBody(String prompt, String base64Image) {
|
||||
JSONObject requestBody = new JSONObject();
|
||||
|
||||
|
||||
// 创建图片部分
|
||||
JSONObject imagePart = new JSONObject();
|
||||
JSONObject inlineData = new JSONObject();
|
||||
inlineData.set("mimeType", "image/png");
|
||||
inlineData.set("data", base64Image.replace("data:image/png;base64,", ""));
|
||||
imagePart.set("inlineData", inlineData);
|
||||
|
||||
|
||||
// 创建文本部分
|
||||
JSONObject textPart = new JSONObject();
|
||||
textPart.set("text", prompt);
|
||||
|
||||
|
||||
// 创建内容对象
|
||||
JSONObject content = new JSONObject();
|
||||
content.set("role", "user");
|
||||
content.set("parts", Arrays.asList(imagePart, textPart));
|
||||
|
||||
|
||||
// 设置contents数组
|
||||
requestBody.set("contents", Arrays.asList(content));
|
||||
|
||||
|
||||
// 设置生成配置
|
||||
JSONObject generationConfig = new JSONObject();
|
||||
generationConfig.set("maxOutputTokens", 8192);
|
||||
generationConfig.set("responseModalities", Arrays.asList("IMAGE"));
|
||||
|
||||
|
||||
JSONObject imageConfig = new JSONObject();
|
||||
imageConfig.set("aspectRatio", "9:16");
|
||||
generationConfig.set("imageConfig", imageConfig);
|
||||
|
||||
|
||||
requestBody.set("generationConfig", generationConfig);
|
||||
|
||||
|
||||
return requestBody;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取谷歌访问令牌
|
||||
*/
|
||||
private String getGoogleAccessToken() throws IOException {
|
||||
try (InputStream inputStream = TryOnEffectServiceImpl.class.getClassLoader()
|
||||
.getResourceAsStream("aida-461108-b4afaabebb84.json")) {
|
||||
|
||||
|
||||
if (inputStream == null) {
|
||||
throw new IOException("Google credentials file not found");
|
||||
}
|
||||
|
||||
|
||||
GoogleCredentials credentials = GoogleCredentials
|
||||
.fromStream(inputStream)
|
||||
.createScoped(Collections.singletonList("https://www.googleapis.com/auth/cloud-platform"));
|
||||
|
||||
|
||||
credentials.refreshIfExpired();
|
||||
return credentials.getAccessToken().getTokenValue();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送HTTP请求
|
||||
*/
|
||||
@@ -374,7 +389,7 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
.connectionPool(connectionPool)
|
||||
.retryOnConnectionFailure(true)
|
||||
.build();
|
||||
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(endpoint)
|
||||
.addHeader("Authorization", "Bearer " + accessToken)
|
||||
@@ -383,15 +398,15 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
.addHeader("Accept", "application/json")
|
||||
.post(RequestBody.create(MediaType.parse("application/json"), jsonBody))
|
||||
.build();
|
||||
|
||||
|
||||
// 实现重试逻辑
|
||||
int maxRetries = 3;
|
||||
int retryDelay = 2000; // 2秒
|
||||
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
log.info("发起HTTP请求 - 尝试次数: {}/{}, URL: {}", attempt, maxRetries, request.url());
|
||||
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
log.warn("Google API响应失败,状态码: {}", response.code());
|
||||
@@ -402,7 +417,7 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
throw new IOException("HTTP error code: " + response.code());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String result = response.body().string();
|
||||
log.info("Google API调用成功");
|
||||
return result;
|
||||
@@ -424,26 +439,26 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
throw new RuntimeException("请求被中断", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
throw new IOException("所有重试都失败了");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理谷歌API响应
|
||||
*/
|
||||
private String processGoogleAPIResponse(String response) {
|
||||
try {
|
||||
com.alibaba.fastjson.JSONObject jsonResponse = JSON.parseObject(response);
|
||||
|
||||
|
||||
JSONArray candidates = jsonResponse.getJSONArray("candidates");
|
||||
if (candidates == null || candidates.isEmpty()) {
|
||||
log.error("Google API响应中没有候选结果");
|
||||
throw new BusinessException("Google API response invalid", "Google API响应无效", ResultEnum.ERROR.getCode());
|
||||
}
|
||||
|
||||
|
||||
com.alibaba.fastjson.JSONObject candidate = candidates.getJSONObject(0);
|
||||
String finishReason = candidate.getString("finishReason");
|
||||
|
||||
|
||||
if (!"STOP".equals(finishReason)) {
|
||||
String finishMessage = candidate.getString("finishMessage");
|
||||
if ("IMAGE_SAFETY".equals(finishReason)) {
|
||||
@@ -453,32 +468,31 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
|
||||
log.error("生成失败: {}", finishMessage);
|
||||
throw new BusinessException("Image generation failed", "图片生成失败", ResultEnum.ERROR.getCode());
|
||||
}
|
||||
|
||||
|
||||
com.alibaba.fastjson.JSONObject contentResult = candidate.getJSONObject("content");
|
||||
JSONArray parts = contentResult.getJSONArray("parts");
|
||||
|
||||
|
||||
// 查找包含图片数据的部分
|
||||
for (int i = 0; i < parts.size(); i++) {
|
||||
com.alibaba.fastjson.JSONObject part = parts.getJSONObject(i);
|
||||
if (part.containsKey("inlineData")) {
|
||||
com.alibaba.fastjson.JSONObject inlineDataResult = part.getJSONObject("inlineData");
|
||||
String base64Data = inlineDataResult.getString("data");
|
||||
|
||||
|
||||
if (base64Data != null && !base64Data.isEmpty()) {
|
||||
// 上传生成的图片到MinIO
|
||||
String fileName = "tryon_result_" + UUID.randomUUID().toString() + ".png";
|
||||
String uploadedUrl = minioUtil.uploadBytes(
|
||||
|
||||
String logicalUrl = minioUtil.uploadBytes(
|
||||
java.util.Base64.getDecoder().decode(base64Data),
|
||||
fileName,
|
||||
MinioFileConstants.FileType.TRY_ON_RESULT,
|
||||
"image/png"
|
||||
);
|
||||
|
||||
log.info("生成的图片已上传到MinIO: {}", uploadedUrl);
|
||||
return uploadedUrl;
|
||||
|
||||
log.info("生成的图片已上传到MinIO,逻辑URL: {}", logicalUrl);
|
||||
return logicalUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
log.error("响应中没有找到有效的图片数据");
|
||||
throw new BusinessException("Image data not found", "未找到图片数据", ResultEnum.ERROR.getCode());
|
||||
} catch (BusinessException e) {
|
||||
|
||||
@@ -73,8 +73,8 @@ public class VisitRecordServiceImpl extends ServiceImpl<VisitRecordMapper, Visit
|
||||
LibraryVo libraryVo = new LibraryVo();
|
||||
// 如果有收藏的试穿效果,设置其图片URL为默认图片
|
||||
if (favoriteEffect != null && favoriteEffect.getResultImageUrl() != null) {
|
||||
libraryVo.setDefaultImageUrl(minioUtil.getPresignedUrl(
|
||||
favoriteEffect.getResultImageUrl(),
|
||||
libraryVo.setDefaultImageUrl(minioUtil.convertToPresignedUrl(
|
||||
favoriteEffect.getResultImageUrl(),
|
||||
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
|
||||
));
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.aida.lanecarford.util;
|
||||
|
||||
import com.aida.lanecarford.constant.MinioFileConstants;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -263,11 +264,9 @@ public class ImageCompositionUtil {
|
||||
* 生成合成图片的文件名
|
||||
*
|
||||
* @param originalUrls 原始图片URL列表
|
||||
* @return 文件名
|
||||
* @return 生成的文件名
|
||||
*/
|
||||
public String generateComposedFileName(List<String> 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());
|
||||
}
|
||||
}
|
||||
@@ -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管理
|
||||
* <p>
|
||||
* 设计原则:
|
||||
* 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<Bucket> 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<String> listFiles(String bucketName) {
|
||||
return listFiles(bucketName, null);
|
||||
}
|
||||
// ==================== 文件查询操作 ====================
|
||||
|
||||
/**
|
||||
* 获取文件列表
|
||||
*
|
||||
* @param bucketName 存储桶名称
|
||||
* @param prefix 文件前缀
|
||||
*
|
||||
* @param prefix 文件前缀
|
||||
* @return 文件列表
|
||||
*/
|
||||
public List<String> listFiles(String bucketName, String prefix) {
|
||||
public List<String> listFiles(String prefix) {
|
||||
List<String> files = new ArrayList<>();
|
||||
try {
|
||||
Iterable<Result<Item>> 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<String> getPresignedUrls(List<String> objectNames, int expires) {
|
||||
List<String> 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<String> convertToPresignedUrls(List<String> logicalUrls, int expires) {
|
||||
List<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user