tyr -on完善接口

This commit is contained in:
litianxiang
2025-10-28 17:23:13 +08:00
parent ec5054d238
commit 9affa65a42
7 changed files with 418 additions and 150 deletions

View File

@@ -1,5 +1,7 @@
package com.aida.lanecarford.config;
import jakarta.validation.constraints.Negative;
import org.checkerframework.checker.units.qual.A;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

View File

@@ -1,6 +1,7 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.constant.MinioFileConstants;
import com.aida.lanecarford.config.MinioConfig;
import com.aida.lanecarford.dto.CustomerPhotoDto;
import com.aida.lanecarford.entity.CustomerPhoto;
import com.aida.lanecarford.mapper.CustomerPhotoMapper;
@@ -21,13 +22,15 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class CustomerPhotoServiceImpl extends ServiceImpl<CustomerPhotoMapper, CustomerPhoto> implements CustomerPhotoService {
private final MinioUtil minioUtil;
private final MinioConfig minioConfig;
@Override
public CustomerPhoto upload(CustomerPhotoDto customerPhotoDto) {
String logicalUrl = minioUtil.uploadFile(
customerPhotoDto.getFile(),
MinioFileConstants.FileType.CUSTOMER_PHOTO
MinioFileConstants.FileType.CUSTOMER_PHOTO,
minioConfig.getBucketName()
);
CustomerPhoto customerPhoto = CopyUtil.copyObject(customerPhotoDto, CustomerPhoto.class);

View File

@@ -28,8 +28,7 @@ import java.util.Objects;
@RequiredArgsConstructor
public class CustomerServiceImpl extends ServiceImpl<CustomerMapper, Customer> implements CustomerService {
@Resource
private VisitRecordService visitRecordService;
private final VisitRecordService visitRecordService;
// 选择顾客登录并添加入店记录
public CustomerCheckInVO customerCheckIn(String name, String email) {

View File

@@ -1,6 +1,7 @@
package com.aida.lanecarford.service.impl;
import com.aida.lanecarford.common.constant.MinioFileConstants;
import com.aida.lanecarford.config.MinioConfig;
import com.aida.lanecarford.service.ImageCompositionService;
import com.aida.lanecarford.util.ImageCompositionUtil;
import com.aida.lanecarford.util.MinioUtil;
@@ -23,6 +24,7 @@ public class ImageCompositionServiceImpl implements ImageCompositionService {
private final ImageCompositionUtil imageCompositionUtil;
private final MinioUtil minioUtil;
private final MinioConfig minioConfig;
@@ -66,7 +68,8 @@ public class ImageCompositionServiceImpl implements ImageCompositionService {
String logicalUrl = minioUtil.uploadBytes(
composedImageBytes,
MinioFileConstants.FileType.COMPOSED_IMAGE,
"image/jpeg"
"image/jpeg",
minioConfig.getBucketName()
);
log.info("图片合成并上传成功逻辑URL: {}", logicalUrl);
@@ -77,4 +80,4 @@ public class ImageCompositionServiceImpl implements ImageCompositionService {
throw new RuntimeException("图片合成并上传失败: " + e.getMessage(), e);
}
}
}
}

View File

@@ -2,19 +2,14 @@ package com.aida.lanecarford.service.impl;
import cn.hutool.json.JSONObject;
import com.aida.lanecarford.common.CommonConstant;
import com.aida.lanecarford.config.MinioConfig;
import com.aida.lanecarford.common.response.ResultEnum;
import com.aida.lanecarford.common.constant.MinioFileConstants;
import com.aida.lanecarford.entity.CustomerPhoto;
import com.aida.lanecarford.entity.ModelPhoto;
import com.aida.lanecarford.entity.Style;
import com.aida.lanecarford.entity.TryOnEffect;
import com.aida.lanecarford.entity.*;
import com.aida.lanecarford.exception.BusinessException;
import com.aida.lanecarford.mapper.CustomerMapper;
import com.aida.lanecarford.mapper.TryOnEffectMapper;
import com.aida.lanecarford.service.CustomerPhotoService;
import com.aida.lanecarford.service.ImageCompositionService;
import com.aida.lanecarford.service.ModelPhotoService;
import com.aida.lanecarford.service.StyleService;
import com.aida.lanecarford.service.TryOnEffectService;
import com.aida.lanecarford.service.*;
import com.aida.lanecarford.util.MinioUtil;
import com.aida.lanecarford.vo.TryOnResultVo;
import com.alibaba.fastjson.JSON;
@@ -25,6 +20,9 @@ import com.google.auth.oauth2.GoogleCredentials;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.io.IOException;
@@ -40,23 +38,29 @@ import java.util.concurrent.TimeUnit;
*
* @author AI Assistant
* @since 2024-01-01
/**
* 试穿效果服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOnEffect> implements TryOnEffectService {
private static final Logger log = LoggerFactory.getLogger(TryOnEffectServiceImpl.class);
private final StyleService styleService;
private final ModelPhotoService modelPhotoService;
private final CustomerPhotoService customerPhotoService;
private final ImageCompositionService imageCompositionService;
private final CustomerMapper customerMapper;
private final MinioUtil minioUtil;
private final MinioConfig minioConfig;
@Override
public TryOnResultVo generateTryOnEffect(TryOnEffect tryOnEffectDto) {
Integer isRegenerated = tryOnEffectDto.getIsRegenerated();
String toAIlogicalUrl = null;
String prompt = null;
Style style = null;
// 收集图片URL
List<String> imageUrls = new ArrayList<>();
if (isRegenerated == 1) {
@@ -86,59 +90,44 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
throw BusinessException.parameterRequired("styleId");
}
//根据id查到对应styleurl
Style style = styleService.getById(styleId);
style = styleService.getById(styleId);
String styleImageUrl = style.getStyleImageUrl();
if (styleImageUrl != null && !styleImageUrl.trim().isEmpty()) {
imageUrls.add(styleImageUrl);
}
Long modelPhotoId = tryOnEffectDto.getModelPhotoId();
if (modelPhotoId != null) {
//根据id查到对应modelurl
ModelPhoto modelPhoto = modelPhotoService.getById(modelPhotoId);
String modelPhotoUrl = modelPhoto.getPhotoUrl();
if (modelPhotoUrl != null && !modelPhotoUrl.trim().isEmpty()) {
imageUrls.add(modelPhotoUrl);
}
}
}
// 合成图片
if (!imageUrls.isEmpty() && imageUrls.size() > 1) {
log.info("开始合成图片,图片数量: {}", imageUrls.size());
try {
// 将逻辑URL批量转换为预签名URL以便图像合成服务可以访问
List<String> presignedUrls = minioUtil.convertToPresignedUrls(imageUrls, CommonConstant.MINIO_IMAGE_EXPIRE_TIME);
log.debug("批量转换逻辑URL为预签名URL数量: {}", presignedUrls.size());
toAIlogicalUrl = imageCompositionService.composeAndUploadImages(presignedUrls);
String aiRreultlogicalUrl = null;
if (tryOnEffectDto.getIsRegenerated() == 0 && imageUrls.size() == 1) {
Customer customer = customerMapper.selectById(tryOnEffectDto.getCustomerId());
prompt = "A full-body, photorealistic professional studio shot of a **young " + customer.getGender() + "**, mid-20s, with **clear facial features and a confident expression**. The model is centered in the frame.They are **standing in a direct, static, full-view pose** on a **clean, light white backdrop** **The entire figure, from head to toe, should be in frame and occupy approximately 80% of the vertical space.**.\n" +
"\n" +
"**CRITICAL COMPOSITION INSTRUCTION:**\n" +
"Generate a single, seamless image of the model with a complete and fully visible head,**wearing ALL distinct items** from the uploaded image. **ALL items MUST be visible and correctly worn in the final outfit.**" +
"\n" +
"**Placement Detail:** Outerwear must be worn on the outside, and if a bag is present, it must be visible.\n" +
"**Quality:** Ultra-high resolution, The figure should fill the frame as much as possible,studio photography quality. Emphasize **realistic fabric textures and sharp print fidelity**.\n" +
"**Negative Constraints (Exclude):** **NO text, NO borders, NO tables, NO multiple models, NO extra items.** **CRITICAL: NO cropping of the head or face.**";
aiRreultlogicalUrl = AITryOnEffect(prompt, imageUrls);
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 BusinessException.parameterRequired("image");
}else if (tryOnEffectDto.getIsRegenerated() == 1 && imageUrls.size() == 1){
//根据提示词修改图像
aiRreultlogicalUrl = AITryOnEffect(prompt, imageUrls);
}else if (tryOnEffectDto.getIsRegenerated() == 1 && imageUrls.size() > 1){
//换脸
aiRreultlogicalUrl = callFaceSwapAPI(imageUrls);
}
//调用模型生成试穿效果
log.info("准备调用第三方AI服务输入图片URL: {}", toAIlogicalUrl);
prompt = "A woman is wearing the outfit (an outerwear, a top, pants, shoes, a handbag), white background, full body, professional portrait photography."+prompt;
// String AIRreultlogicalUrl = AITryOnEffect(prompt, toAIlogicalUrl);
String AIRreultlogicalUrl = "try_on_result/6ecb707a-f541-437a-aef5-9544b5b18164.png";
tryOnEffectDto.setResultImageUrl(AIRreultlogicalUrl);
tryOnEffectDto.setResultImageUrl(aiRreultlogicalUrl);
tryOnEffectDto.setGenerationStatus("completed");
this.saveOrUpdate(tryOnEffectDto);
TryOnResultVo tryOnResultVo = new TryOnResultVo();
tryOnResultVo.setTryOnId(tryOnEffectDto.getId());
tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl(AIRreultlogicalUrl, CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl(aiRreultlogicalUrl, CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
return tryOnResultVo;
}
@@ -164,7 +153,7 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
styleLambdaQueryWrapper.eq(Style::getId, tryOnEffect.getStyleId()).select(Style::getStyleImageUrl);
Style style = styleService.getOne(styleLambdaQueryWrapper);
tryOnResultVo.setStyleUrl(minioUtil.convertToPresignedUrl(
style.getStyleImageUrl(),
style.getStyleImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
));
}
@@ -187,18 +176,18 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
tryOnResultVo.setTryOnId(tryOnEffect.getId());
// 使用新的API获取预签名URL数据库存储的是逻辑URL
tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl(
tryOnEffect.getResultImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
));
tryOnEffect.getResultImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
));
// 如果是原始效果则获取对应的style图片
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.convertToPresignedUrl(
style.getStyleImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
));
style.getStyleImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME
));
}
tryOnResultVo.setIsRegenerated(tryOnEffect.getIsRegenerated());
@@ -243,8 +232,11 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
}
public String AITryOnEffect(String prompt, String url) {
log.info("开始执行AITryOnEffect - prompt: {}, url: {}", prompt, url);
public String AITryOnEffect(String prompt, List<String> urls) {
System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("https.proxyPort", "10809");
// log.info("开始执行AITryOnEffect - prompt: {}, url: {}", prompt, url);
// 参数验证
if (prompt == null || prompt.trim().isEmpty()) {
@@ -252,16 +244,24 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
throw new BusinessException("Prompt is required", "prompt不能为空", ResultEnum.PARAMETER_ERROR.getCode());
}
if (url == null || url.trim().isEmpty()) {
if (urls.isEmpty()) {
log.error("参数验证失败 - url不能为空");
throw new BusinessException("URL is required", "url不能为空", ResultEnum.PARAMETER_ERROR.getCode());
}
ArrayList<String> base64Images = new ArrayList<>();
for (String url : urls) {
if (url == null || url.trim().isEmpty()) {
log.error("参数验证失败 - url不能为空");
throw new BusinessException("URL is required", "url不能为空", ResultEnum.PARAMETER_ERROR.getCode());
}
String base64Image = getImageAsBase64(url);
base64Images.add(base64Image);
}
// 获取图片的base64编码
String base64Image = getImageAsBase64(url);
// 调用谷歌API进行图生图
String resultImageUrl = callGoogleImageGenerationAPI(prompt, base64Image);
String resultImageUrl = callGoogleImageGenerationAPI(prompt, base64Images);
log.info("AITryOnEffect执行成功结果URL: {}", resultImageUrl);
return resultImageUrl;
@@ -289,21 +289,21 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
/**
* 调用谷歌API进行图像生成
*/
private String callGoogleImageGenerationAPI(String prompt, String base64Image) {
private String callGoogleImageGenerationAPI(String prompt, List<String> base64Images) {
try {
System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("https.proxyPort", "10809");
// 谷歌API配置
String projectId = "aida-461108";
String location = "global";
String model = "gemini-2.0-flash-exp"; // 使用适合的模型
String model = "gemini-2.5-flash-image"; // 使用适合的模型
String endpoint = String.format(
"https://aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:generateContent",
projectId, location, model
);
// 构建请求体
JSONObject requestBody = buildRequestBody(prompt, base64Image);
JSONObject requestBody = buildRequestBody(prompt, base64Images);
// 获取访问令牌
String accessToken = getGoogleAccessToken();
@@ -322,15 +322,29 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
/**
* 构建谷歌API请求体
*/
private JSONObject buildRequestBody(String prompt, String base64Image) {
private JSONObject buildRequestBody(String prompt, List<String> base64Images) {
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 imagePart2 = new JSONObject();
if (base64Images.size() == 1) {
JSONObject inlineData = new JSONObject();
inlineData.set("mimeType", "image/png");
inlineData.set("data", base64Images.get(0).replace("data:image/png;base64,", ""));
imagePart.set("inlineData", inlineData);
} else if (base64Images.size() == 2) {
JSONObject inlineData = new JSONObject();
inlineData.set("mimeType", "image/png");
inlineData.set("data", base64Images.get(0));
imagePart.set("inlineData", inlineData);
JSONObject inlineData2 = new JSONObject();
inlineData2.set("mimeType", "image/jpeg");
inlineData2.set("data", base64Images.get(1));
imagePart2.set("inlineData", inlineData2);
}
// 创建文本部分
JSONObject textPart = new JSONObject();
@@ -339,7 +353,11 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
// 创建内容对象
JSONObject content = new JSONObject();
content.set("role", "user");
content.set("parts", Arrays.asList(imagePart, textPart));
if (base64Images.size() == 1) {
content.set("parts", Arrays.asList(imagePart, textPart));
} else if (base64Images.size() == 2) {
content.set("parts", Arrays.asList(imagePart, imagePart2, textPart));
}
// 设置contents数组
requestBody.set("contents", Arrays.asList(content));
@@ -411,6 +429,7 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
log.info("Google API响应 " + response.body());
log.warn("Google API响应失败状态码: {}", response.code());
if (attempt < maxRetries) {
Thread.sleep(retryDelay * attempt);
@@ -486,7 +505,8 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
String logicalUrl = minioUtil.uploadBytes(
java.util.Base64.getDecoder().decode(base64Data),
MinioFileConstants.FileType.TRY_ON_RESULT,
"image/png"
"image/png",
minioConfig.getBucketName()
);
log.info("生成的图片已上传到MinIO逻辑URL: {}", logicalUrl);
@@ -504,4 +524,198 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
throw new BusinessException("Response processing failed", "响应处理失败", ResultEnum.ERROR.getCode());
}
}
/**
* 调用换脸API
* @param imageUrls 图片URL列表第一个为源图片第二个为目标图片
* @return 换脸后的图片URL
*/
private String callFaceSwapAPI(List<String> imageUrls) {
try {
log.info("开始调用换脸API - 源图片: {}, 目标图片: {}", imageUrls.get(0), imageUrls.get(1));
// 获取图片的预签名URL
String inputFaceUrl = imageUrls.get(0);
// 构建输入图片列表(从第二张图片开始作为目标图片列表)
JSONArray inputImageList = new JSONArray();
for (int i = 1; i < imageUrls.size(); i++) {
inputImageList.add(imageUrls.get(i));
}
// 构建请求体
JSONObject requestBody = new JSONObject();
requestBody.put("input_image_list", inputImageList);
requestBody.put("input_face", inputFaceUrl);
requestBody.put("threshold", 0.2);
// 调用换脸API
String response = sendFaceSwapRequest("http://18.167.251.121:10004/api/v1/reface", requestBody.toString());
// 处理响应
return processFaceSwapResponse(response);
} catch (Exception e) {
log.error("换脸API调用失败: {}", e.getMessage(), e);
throw new BusinessException("Face swap API call failed", "换脸API调用失败: " + e.getMessage(), ResultEnum.ERROR.getCode());
}
}
/**
* 发送换脸API请求
*/
private String sendFaceSwapRequest(String endpoint, String jsonBody) throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(180, TimeUnit.SECONDS)
.writeTimeout(180, TimeUnit.SECONDS)
.callTimeout(300, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
Request request = new Request.Builder()
.url(endpoint)
.addHeader("Content-Type", "application/json")
.addHeader("Accept", "application/json")
.addHeader("User-Agent", "LaneCarford-Client/1.0")
.post(RequestBody.create(MediaType.parse("application/json"), jsonBody))
.build();
int maxRetries = 3;
int retryDelay = 3000; // 3秒
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
log.info("发起换脸API请求 - 尝试次数: {}/{}, URL: {}", attempt, maxRetries, request.url());
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
log.warn("换脸API响应失败状态码: {}, 响应: {}", response.code(), response.body() != null ? response.body().string() : "null");
if (attempt < maxRetries) {
Thread.sleep(retryDelay * attempt);
continue;
} else {
throw new IOException("HTTP error code: " + response.code());
}
}
String result = response.body().string();
log.info("换脸API调用成功");
return result;
}
} catch (IOException e) {
log.warn("换脸API网络连接问题 - 尝试: {}/{}, 错误: {}", attempt, maxRetries, e.getMessage());
if (attempt < maxRetries) {
try {
Thread.sleep(retryDelay * attempt);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试被中断", ie);
}
} else {
throw e;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("请求被中断", e);
}
}
throw new IOException("所有重试都失败了");
}
/**
* 处理换脸API响应
*/
private String processFaceSwapResponse(String response) {
try {
log.info("开始处理换脸API响应: {}", response);
JSONObject jsonResponse = new JSONObject(response);
// 检查响应状态
if (jsonResponse.containsKey("success") && jsonResponse.getBool("success")) {
// 成功响应获取结果图片URL
if (jsonResponse.containsKey("result_url")) {
String resultUrl = jsonResponse.getStr("result_url");
log.info("换脸成功结果URL: {}", resultUrl);
// 下载图片并上传到MinIO
return downloadAndUploadToMinio(resultUrl);
} else if (jsonResponse.containsKey("data") && jsonResponse.getJSONObject("data").containsKey("result_url")) {
String resultUrl = jsonResponse.getJSONObject("data").getStr("result_url");
log.info("换脸成功结果URL: {}", resultUrl);
// 下载图片并上传到MinIO
return downloadAndUploadToMinio(resultUrl);
}
}
// 检查是否有错误信息
if (jsonResponse.containsKey("error")) {
String error = jsonResponse.getStr("error");
log.error("换脸API返回错误: {}", error);
throw new BusinessException("Face swap failed", "换脸失败: " + error, ResultEnum.ERROR.getCode());
}
log.error("换脸API响应格式不正确: {}", response);
throw new BusinessException("Invalid response format", "响应格式不正确", ResultEnum.ERROR.getCode());
} catch (BusinessException e) {
throw e;
} catch (Exception e) {
log.error("处理换脸API响应失败: {}", e.getMessage(), e);
throw new BusinessException("Response processing failed", "响应处理失败", ResultEnum.ERROR.getCode());
}
}
/**
* 下载图片并上传到MinIO
*/
private String downloadAndUploadToMinio(String imageUrl) {
try {
log.info("开始下载换脸结果图片: {}", imageUrl);
// 使用现有的下载方法
byte[] imageData = downloadImageFromUrl(imageUrl);
// 生成对象名称
String objectName = MinioFileConstants.TRY_ON_RESULT_DIR + "/" + "faceswap_" + System.currentTimeMillis() + ".jpg";
// 上传到MinIO
String logicalUrl = minioUtil.uploadBytes(imageData, objectName, "image/jpeg", minioConfig.getBucketName());
log.info("换脸结果图片上传成功逻辑URL: {}", logicalUrl);
return logicalUrl;
} catch (Exception e) {
log.error("下载并上传换脸结果图片失败: {}", e.getMessage(), e);
throw new BusinessException("Failed to download and upload result image", "下载并上传结果图片失败", ResultEnum.ERROR.getCode());
}
}
/**
* 从URL下载图片数据
*/
private byte[] downloadImageFromUrl(String imageUrl) throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
Request request = new Request.Builder()
.url(imageUrl)
.addHeader("User-Agent", "LaneCarford-Client/1.0")
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Failed to download image: HTTP " + response.code());
}
return response.body().bytes();
}
}
}

View File

@@ -88,21 +88,24 @@ public class MinioUtil {
// ==================== 文件上传操作 ====================
/**
* 上传MultipartFile文件
* 上传文件到指定桶
*
* @param file 文件
* @param fileType 文件类型
* @return 逻辑URL不含桶名
* @param file 文件
* @param fileType 文件类型
* @param bucketName 桶名
* @return 文件的逻辑路径(包含桶名)
*/
public String uploadFile(MultipartFile file, MinioFileConstants.FileType fileType) {
public String uploadFile(MultipartFile file, MinioFileConstants.FileType fileType, String bucketName) {
if (file == null || file.isEmpty()) {
throw new MinioException("文件不能为空");
}
try {
// 确保默认桶存在
ensureDefaultBucketExists();
// 确保桶存在
createBucket(bucketName);
// 生成对象名称
String objectName = MinioFileConstants.generateObjectNameByType(fileType);
@@ -110,50 +113,53 @@ public class MinioUtil {
// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.bucket(bucketName)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
log.info("文件上传成功: {}", objectName);
return objectName; // 返回逻辑URL
log.info("文件上传成功: {}/{}", bucketName, objectName);
return bucketName + "/" + objectName; // 返回包含桶名的逻辑路径
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e);
throw new MinioException("文件上传失败", e);
}
}
/**
* 上传字节数组
* 上传字节数组到指定桶
*
* @param bytes 字节数组
* @param objectName 对象名称(逻辑路径)
* @param objectName 对象名称
* @param contentType 内容类型
* @return 逻辑URL不含桶名
* @param bucketName 桶名
* @return 文件的逻辑路径(包含桶名)
*/
public String uploadBytes(byte[] bytes, String objectName, String contentType) {
public String uploadBytes(byte[] bytes, String objectName, String contentType, String bucketName) {
if (bytes == null || bytes.length == 0) {
throw new MinioException("文件内容不能为空");
}
try {
// 确保默认桶存在
ensureDefaultBucketExists();
// 确保桶存在
createBucket(bucketName);
// 上传文件
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.bucket(bucketName)
.object(objectName)
.stream(new ByteArrayInputStream(bytes), bytes.length, -1)
.contentType(contentType)
.build()
);
log.info("字节数组上传成功: {}", objectName);
return objectName; // 返回逻辑URL
log.info("字节数组上传成功: {}/{}", bucketName, objectName);
return bucketName + "/" + objectName; // 返回包含桶名的逻辑路径
} catch (Exception e) {
log.error("字节数组上传失败: {}", e.getMessage(), e);
throw new MinioException("字节数组上传失败", e);
@@ -166,11 +172,12 @@ public class MinioUtil {
* @param bytes 字节数组
* @param fileType 文件类型
* @param contentType 内容类型
* @return 逻辑URL不含桶名
* @param bucketName 桶名
* @return 逻辑URL包含桶名
*/
public String uploadBytes(byte[] bytes, MinioFileConstants.FileType fileType, String contentType) {
public String uploadBytes(byte[] bytes, MinioFileConstants.FileType fileType, String contentType, String bucketName) {
String objectName = MinioFileConstants.generateObjectNameByType(fileType);
return uploadBytes(bytes, objectName, contentType);
return uploadBytes(bytes, objectName, contentType, bucketName);
}
// ==================== 文件下载操作 ====================
@@ -178,19 +185,34 @@ public class MinioUtil {
/**
* 下载文件
*
* @param objectName 对象名称(逻辑路径
* @param logicalPath 逻辑路径格式bucketName/folder/filename
* @return 文件输入流
*/
public InputStream downloadFile(String objectName) {
public InputStream downloadFile(String logicalPath) {
// 解析桶名和对象名bucketName/folder/filename
int index = logicalPath.indexOf("/");
if (index <= 0) {
throw new MinioException("逻辑路径格式错误,应包含桶名: " + logicalPath);
}
String bucketName = logicalPath.substring(0, index);
String objectName = logicalPath.substring(index + 1);
// 验证桶名是否存在
try {
// 检查文件是否存在
if (!doesObjectExist(objectName)) {
throw new IOException("文件不存在: " + objectName);
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
throw new MinioException("不存在: " + bucketName);
}
} catch (Exception e) {
log.error("验证桶存在性失败: {}", e.getMessage(), e);
throw new MinioException("验证桶存在性失败", e);
}
try {
return minioClient.getObject(
GetObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.bucket(bucketName)
.object(objectName)
.build()
);
@@ -205,17 +227,37 @@ public class MinioUtil {
/**
* 删除文件
*
* @param objectName 对象名称(逻辑路径
* @param logicalPath 逻辑路径格式bucketName/folder/filename
*/
public void deleteFile(String objectName) {
public void deleteFile(String logicalPath) {
// 解析桶名和对象名bucketName/folder/filename
int index = logicalPath.indexOf("/");
if (index <= 0) {
throw new MinioException("逻辑路径格式错误,应包含桶名: " + logicalPath);
}
String bucketName = logicalPath.substring(0, index);
String objectName = logicalPath.substring(index + 1);
// 验证桶名是否存在
try {
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
throw new MinioException("桶不存在: " + bucketName);
}
} catch (Exception e) {
log.error("验证桶存在性失败: {}", e.getMessage(), e);
throw new MinioException("验证桶存在性失败", e);
}
try {
minioClient.removeObject(
RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.bucket(bucketName)
.object(objectName)
.build()
);
log.info("文件删除成功: {}", objectName);
log.info("文件删除成功: {}/{}", bucketName, objectName);
} catch (Exception e) {
log.error("文件删除失败: {}", e.getMessage(), e);
throw new MinioException("文件删除失败", e);
@@ -224,25 +266,28 @@ public class MinioUtil {
// ==================== 文件查询操作 ====================
/**
* 获取文件列表
* 获取指定桶的文件列表
*
* @param prefix 文件前缀
* @return 文件列表
* @param prefix 文件前缀
* @param bucketName 桶名
* @return 文件列表(包含桶名)
*/
public List<String> listFiles(String prefix) {
public List<String> listFiles(String prefix, String bucketName) {
List<String> files = new ArrayList<>();
try {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder()
.bucket(minioConfig.getBucketName())
.bucket(bucketName)
.prefix(prefix)
.build()
);
for (Result<Item> result : results) {
Item item = result.get();
files.add(item.objectName());
files.add(bucketName + "/" + item.objectName()); // 返回包含桶名的路径
}
} catch (Exception e) {
log.error("获取文件列表失败: {}", e.getMessage(), e);
@@ -254,14 +299,34 @@ public class MinioUtil {
/**
* 检查文件是否存在
*
* @param objectName 对象名称(逻辑路径
* @param logicalPath 逻辑路径格式bucketName/folder/filename
* @return 是否存在
*/
public boolean doesObjectExist(String objectName) {
public boolean doesObjectExist(String logicalPath) {
// 解析桶名和对象名bucketName/folder/filename
int index = logicalPath.indexOf("/");
if (index <= 0) {
throw new MinioException("逻辑路径格式错误,应包含桶名: " + logicalPath);
}
String bucketName = logicalPath.substring(0, index);
String objectName = logicalPath.substring(index + 1);
// 验证桶名是否存在
try {
boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
if (!bucketExists) {
return false; // 桶不存在,文件肯定不存在
}
} catch (Exception e) {
log.error("验证桶存在性失败: {}", e.getMessage(), e);
return false;
}
try {
minioClient.statObject(
StatObjectArgs.builder()
.bucket(minioConfig.getBucketName())
.bucket(bucketName)
.object(objectName)
.build()
);
@@ -278,13 +343,11 @@ public class MinioUtil {
*
* @param objectName 对象名称(逻辑路径)
* @param expires 过期时间(秒)
* @param bucketName 桶名
* @return 预签名URL
*/
public String getPresignedUrl(String objectName, int expires, String bucketName) {
try {
if (bucketName == null){
bucketName = minioConfig.getBucketName();
}
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
@@ -319,15 +382,16 @@ public class MinioUtil {
/**
* 批量获取预签名URL
*
* @param objectNames 对象名称列表(逻辑路径
* @param expires 过期时间(秒)
* @param logicalPaths 逻辑路径列表格式bucketName/folder/filename
* @param expires 过期时间(秒)
* @return 预签名URL列表
*/
public List<String> getPresignedUrls(List<String> objectNames, int expires) {
public List<String> getPresignedUrls(List<String> logicalPaths, int expires) {
List<String> presignedUrls = new ArrayList<>();
for (String objectName : objectNames) {
if (objectName != null && !objectName.trim().isEmpty()) {
presignedUrls.add(getPresignedUrl(objectName, expires, null));
for (String logicalPath : logicalPaths) {
if (logicalPath != null && !logicalPath.trim().isEmpty()) {
// 所有路径都应该包含桶名
presignedUrls.add(getPresignedUrl(logicalPath, expires));
}
}
return presignedUrls;
@@ -352,13 +416,14 @@ public class MinioUtil {
/**
* 将MinIO逻辑URL转换为预签名URL
*
* @param logicalUrl 逻辑URL
* @param logicalUrl 逻辑URL格式bucketName/folder/filename
* @param expires 过期时间(秒)
* @return 预签名URL如果不是逻辑URL则返回原URL
*/
public String convertToPresignedUrl(String logicalUrl, int expires) {
if (isMinioLogicalUrl(logicalUrl)) {
return getPresignedUrl(logicalUrl, expires, null);
// 所有逻辑URL都应该包含桶名
return getPresignedUrl(logicalUrl, expires);
}
return logicalUrl;
}
@@ -383,17 +448,17 @@ public class MinioUtil {
/**
* 获取压缩后的图片Base64编码
*
* @param objectName 对象名称(逻辑路径
* @param logicalPath 逻辑路径(可以包含桶名或不包含
* @param targetWidth 目标宽度
* @param targetHeight 目标高度
* @return 压缩后的图片Base64编码
*/
public String getCompressedImageAsBase64(String objectName, int targetWidth, int targetHeight) throws IOException {
try (InputStream stream = downloadFile(objectName)) {
public String getCompressedImageAsBase64(String logicalPath, int targetWidth, int targetHeight) throws IOException {
try (InputStream stream = downloadFile(logicalPath)) {
// 读取原始图片
BufferedImage originalImage = ImageIO.read(stream);
if (originalImage == null) {
throw new IOException("无法读取图片: " + objectName);
throw new IOException("无法读取图片: " + logicalPath);
}
// 计算压缩比例,保持宽高比
@@ -425,7 +490,7 @@ public class MinioUtil {
ImageIO.write(compressedImage, "JPEG", baos);
byte[] imageBytes = baos.toByteArray();
log.info("图片压缩完成: {} -> {}x{} (原始: {}x{})", objectName, newWidth, newHeight, originalWidth, originalHeight);
log.info("图片压缩完成: {} -> {}x{} (原始: {}x{})", logicalPath, newWidth, newHeight, originalWidth, originalHeight);
return Base64.getEncoder().encodeToString(imageBytes);
} catch (Exception e) {
@@ -436,26 +501,8 @@ public class MinioUtil {
// ==================== 私有辅助方法 ====================
/**
* 确保默认桶存在
*/
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) 替代
*/

View File

@@ -1,6 +1,6 @@
-- Lane Carford AI系统基础架构数据库表结构
-- 创建数据库
CREATE DATABASE IF NOT EXISTS lanecarford CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE DATABASE IF NOT EXISTS lanecrawford CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE lanecrawford;
-- 1. 用户表