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; 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.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

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

View File

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

View File

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

View File

@@ -2,19 +2,14 @@ package com.aida.lanecarford.service.impl;
import cn.hutool.json.JSONObject; import cn.hutool.json.JSONObject;
import com.aida.lanecarford.common.CommonConstant; import com.aida.lanecarford.common.CommonConstant;
import com.aida.lanecarford.config.MinioConfig;
import com.aida.lanecarford.common.response.ResultEnum; import com.aida.lanecarford.common.response.ResultEnum;
import com.aida.lanecarford.common.constant.MinioFileConstants; import com.aida.lanecarford.common.constant.MinioFileConstants;
import com.aida.lanecarford.entity.CustomerPhoto; import com.aida.lanecarford.entity.*;
import com.aida.lanecarford.entity.ModelPhoto;
import com.aida.lanecarford.entity.Style;
import com.aida.lanecarford.entity.TryOnEffect;
import com.aida.lanecarford.exception.BusinessException; import com.aida.lanecarford.exception.BusinessException;
import com.aida.lanecarford.mapper.CustomerMapper;
import com.aida.lanecarford.mapper.TryOnEffectMapper; import com.aida.lanecarford.mapper.TryOnEffectMapper;
import com.aida.lanecarford.service.CustomerPhotoService; import com.aida.lanecarford.service.*;
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.util.MinioUtil; import com.aida.lanecarford.util.MinioUtil;
import com.aida.lanecarford.vo.TryOnResultVo; import com.aida.lanecarford.vo.TryOnResultVo;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
@@ -25,6 +20,9 @@ import com.google.auth.oauth2.GoogleCredentials;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import okhttp3.*; import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.IOException; import java.io.IOException;
@@ -40,23 +38,29 @@ import java.util.concurrent.TimeUnit;
* *
* @author AI Assistant * @author AI Assistant
* @since 2024-01-01 * @since 2024-01-01
/**
* 试穿效果服务实现类
*/ */
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOnEffect> implements TryOnEffectService { public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOnEffect> implements TryOnEffectService {
private static final Logger log = LoggerFactory.getLogger(TryOnEffectServiceImpl.class);
private final StyleService styleService; private final StyleService styleService;
private final ModelPhotoService modelPhotoService; private final ModelPhotoService modelPhotoService;
private final CustomerPhotoService customerPhotoService; private final CustomerPhotoService customerPhotoService;
private final ImageCompositionService imageCompositionService; private final ImageCompositionService imageCompositionService;
private final CustomerMapper customerMapper;
private final MinioUtil minioUtil; private final MinioUtil minioUtil;
private final MinioConfig minioConfig;
@Override @Override
public TryOnResultVo generateTryOnEffect(TryOnEffect tryOnEffectDto) { public TryOnResultVo generateTryOnEffect(TryOnEffect tryOnEffectDto) {
Integer isRegenerated = tryOnEffectDto.getIsRegenerated(); Integer isRegenerated = tryOnEffectDto.getIsRegenerated();
String toAIlogicalUrl = null; String toAIlogicalUrl = null;
String prompt = null; String prompt = null;
Style style = null;
// 收集图片URL // 收集图片URL
List<String> imageUrls = new ArrayList<>(); List<String> imageUrls = new ArrayList<>();
if (isRegenerated == 1) { if (isRegenerated == 1) {
@@ -86,59 +90,44 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
throw BusinessException.parameterRequired("styleId"); throw BusinessException.parameterRequired("styleId");
} }
//根据id查到对应styleurl //根据id查到对应styleurl
Style style = styleService.getById(styleId); style = styleService.getById(styleId);
String styleImageUrl = style.getStyleImageUrl(); String styleImageUrl = style.getStyleImageUrl();
if (styleImageUrl != null && !styleImageUrl.trim().isEmpty()) { if (styleImageUrl != null && !styleImageUrl.trim().isEmpty()) {
imageUrls.add(styleImageUrl); 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); }else if (tryOnEffectDto.getIsRegenerated() == 1 && imageUrls.size() == 1){
} catch (Exception e) { //根据提示词修改图像
log.error("图片合成失败: {}", e.getMessage(), e); aiRreultlogicalUrl = AITryOnEffect(prompt, imageUrls);
throw new RuntimeException("image error " + e.getMessage(), e); }else if (tryOnEffectDto.getIsRegenerated() == 1 && imageUrls.size() > 1){
} //换脸
} else if (imageUrls.size() == 1) { aiRreultlogicalUrl = callFaceSwapAPI(imageUrls);
toAIlogicalUrl = imageUrls.get(0);
} else {
log.warn("没有找到有效的图片URL进行合成");
throw BusinessException.parameterRequired("image");
} }
//调用模型生成试穿效果
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"); tryOnEffectDto.setGenerationStatus("completed");
this.saveOrUpdate(tryOnEffectDto); this.saveOrUpdate(tryOnEffectDto);
TryOnResultVo tryOnResultVo = new TryOnResultVo(); TryOnResultVo tryOnResultVo = new TryOnResultVo();
tryOnResultVo.setTryOnId(tryOnEffectDto.getId()); 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; return tryOnResultVo;
} }
@@ -187,18 +176,18 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
tryOnResultVo.setTryOnId(tryOnEffect.getId()); tryOnResultVo.setTryOnId(tryOnEffect.getId());
// 使用新的API获取预签名URL数据库存储的是逻辑URL // 使用新的API获取预签名URL数据库存储的是逻辑URL
tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl( tryOnResultVo.setTryOnUrl(minioUtil.convertToPresignedUrl(
tryOnEffect.getResultImageUrl(), tryOnEffect.getResultImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME CommonConstant.MINIO_IMAGE_EXPIRE_TIME
)); ));
// 如果是原始效果则获取对应的style图片 // 如果是原始效果则获取对应的style图片
if (tryOnEffect.getIsRegenerated() == 0) { if (tryOnEffect.getIsRegenerated() == 0) {
LambdaQueryWrapper<Style> styleLambdaQueryWrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<Style> styleLambdaQueryWrapper = new LambdaQueryWrapper<>();
styleLambdaQueryWrapper.eq(Style::getId, tryOnEffect.getStyleId()).select(Style::getStyleImageUrl); styleLambdaQueryWrapper.eq(Style::getId, tryOnEffect.getStyleId()).select(Style::getStyleImageUrl);
Style style = styleService.getOne(styleLambdaQueryWrapper); Style style = styleService.getOne(styleLambdaQueryWrapper);
tryOnResultVo.setStyleUrl(minioUtil.convertToPresignedUrl( tryOnResultVo.setStyleUrl(minioUtil.convertToPresignedUrl(
style.getStyleImageUrl(), style.getStyleImageUrl(),
CommonConstant.MINIO_IMAGE_EXPIRE_TIME CommonConstant.MINIO_IMAGE_EXPIRE_TIME
)); ));
} }
tryOnResultVo.setIsRegenerated(tryOnEffect.getIsRegenerated()); tryOnResultVo.setIsRegenerated(tryOnEffect.getIsRegenerated());
@@ -243,8 +232,11 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
} }
public String AITryOnEffect(String prompt, String url) { public String AITryOnEffect(String prompt, List<String> urls) {
log.info("开始执行AITryOnEffect - prompt: {}, url: {}", prompt, url); 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()) { 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()); throw new BusinessException("Prompt is required", "prompt不能为空", ResultEnum.PARAMETER_ERROR.getCode());
} }
if (url == null || url.trim().isEmpty()) { if (urls.isEmpty()) {
log.error("参数验证失败 - url不能为空"); log.error("参数验证失败 - url不能为空");
throw new BusinessException("URL is required", "url不能为空", ResultEnum.PARAMETER_ERROR.getCode()); 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编码 // 获取图片的base64编码
String base64Image = getImageAsBase64(url);
// 调用谷歌API进行图生图 // 调用谷歌API进行图生图
String resultImageUrl = callGoogleImageGenerationAPI(prompt, base64Image); String resultImageUrl = callGoogleImageGenerationAPI(prompt, base64Images);
log.info("AITryOnEffect执行成功结果URL: {}", resultImageUrl); log.info("AITryOnEffect执行成功结果URL: {}", resultImageUrl);
return resultImageUrl; return resultImageUrl;
@@ -289,21 +289,21 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
/** /**
* 调用谷歌API进行图像生成 * 调用谷歌API进行图像生成
*/ */
private String callGoogleImageGenerationAPI(String prompt, String base64Image) { private String callGoogleImageGenerationAPI(String prompt, List<String> base64Images) {
try { try {
System.setProperty("https.proxyHost", "127.0.0.1"); System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("https.proxyPort", "10809"); System.setProperty("https.proxyPort", "10809");
// 谷歌API配置 // 谷歌API配置
String projectId = "aida-461108"; String projectId = "aida-461108";
String location = "global"; String location = "global";
String model = "gemini-2.0-flash-exp"; // 使用适合的模型 String model = "gemini-2.5-flash-image"; // 使用适合的模型
String endpoint = String.format( String endpoint = String.format(
"https://aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:generateContent", "https://aiplatform.googleapis.com/v1/projects/%s/locations/%s/publishers/google/models/%s:generateContent",
projectId, location, model projectId, location, model
); );
// 构建请求体 // 构建请求体
JSONObject requestBody = buildRequestBody(prompt, base64Image); JSONObject requestBody = buildRequestBody(prompt, base64Images);
// 获取访问令牌 // 获取访问令牌
String accessToken = getGoogleAccessToken(); String accessToken = getGoogleAccessToken();
@@ -322,15 +322,29 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
/** /**
* 构建谷歌API请求体 * 构建谷歌API请求体
*/ */
private JSONObject buildRequestBody(String prompt, String base64Image) { private JSONObject buildRequestBody(String prompt, List<String> base64Images) {
JSONObject requestBody = new JSONObject(); JSONObject requestBody = new JSONObject();
// 创建图片部分 // 创建图片部分
JSONObject imagePart = new JSONObject(); JSONObject imagePart = new JSONObject();
JSONObject inlineData = new JSONObject(); JSONObject imagePart2 = new JSONObject();
inlineData.set("mimeType", "image/png"); if (base64Images.size() == 1) {
inlineData.set("data", base64Image.replace("data:image/png;base64,", "")); JSONObject inlineData = new JSONObject();
imagePart.set("inlineData", inlineData); 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(); JSONObject textPart = new JSONObject();
@@ -339,7 +353,11 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
// 创建内容对象 // 创建内容对象
JSONObject content = new JSONObject(); JSONObject content = new JSONObject();
content.set("role", "user"); 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数组 // 设置contents数组
requestBody.set("contents", Arrays.asList(content)); requestBody.set("contents", Arrays.asList(content));
@@ -411,6 +429,7 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
try (Response response = client.newCall(request).execute()) { try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) { if (!response.isSuccessful()) {
log.info("Google API响应 " + response.body());
log.warn("Google API响应失败状态码: {}", response.code()); log.warn("Google API响应失败状态码: {}", response.code());
if (attempt < maxRetries) { if (attempt < maxRetries) {
Thread.sleep(retryDelay * attempt); Thread.sleep(retryDelay * attempt);
@@ -486,7 +505,8 @@ public class TryOnEffectServiceImpl extends ServiceImpl<TryOnEffectMapper, TryOn
String logicalUrl = minioUtil.uploadBytes( String logicalUrl = minioUtil.uploadBytes(
java.util.Base64.getDecoder().decode(base64Data), java.util.Base64.getDecoder().decode(base64Data),
MinioFileConstants.FileType.TRY_ON_RESULT, MinioFileConstants.FileType.TRY_ON_RESULT,
"image/png" "image/png",
minioConfig.getBucketName()
); );
log.info("生成的图片已上传到MinIO逻辑URL: {}", logicalUrl); 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()); 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 file 文件
* @param fileType 文件类型 * @param fileType 文件类型
* @return 逻辑URL不含桶名 * @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()) { if (file == null || file.isEmpty()) {
throw new MinioException("文件不能为空"); throw new MinioException("文件不能为空");
} }
try { try {
// 确保默认桶存在 // 确保桶存在
ensureDefaultBucketExists(); createBucket(bucketName);
// 生成对象名称 // 生成对象名称
String objectName = MinioFileConstants.generateObjectNameByType(fileType); String objectName = MinioFileConstants.generateObjectNameByType(fileType);
@@ -110,50 +113,53 @@ public class MinioUtil {
// 上传文件 // 上传文件
minioClient.putObject( minioClient.putObject(
PutObjectArgs.builder() PutObjectArgs.builder()
.bucket(minioConfig.getBucketName()) .bucket(bucketName)
.object(objectName) .object(objectName)
.stream(file.getInputStream(), file.getSize(), -1) .stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType()) .contentType(file.getContentType())
.build() .build()
); );
log.info("文件上传成功: {}", objectName); log.info("文件上传成功: {}/{}", bucketName, objectName);
return objectName; // 返回逻辑URL return bucketName + "/" + objectName; // 返回包含桶名的逻辑路径
} catch (Exception e) { } catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e); log.error("文件上传失败: {}", e.getMessage(), e);
throw new MinioException("文件上传失败", e); throw new MinioException("文件上传失败", e);
} }
} }
/** /**
* 上传字节数组 * 上传字节数组到指定桶
* *
* @param bytes 字节数组 * @param bytes 字节数组
* @param objectName 对象名称(逻辑路径) * @param objectName 对象名称
* @param contentType 内容类型 * @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) { if (bytes == null || bytes.length == 0) {
throw new MinioException("文件内容不能为空"); throw new MinioException("文件内容不能为空");
} }
try { try {
// 确保默认桶存在 // 确保桶存在
ensureDefaultBucketExists(); createBucket(bucketName);
// 上传文件 // 上传文件
minioClient.putObject( minioClient.putObject(
PutObjectArgs.builder() PutObjectArgs.builder()
.bucket(minioConfig.getBucketName()) .bucket(bucketName)
.object(objectName) .object(objectName)
.stream(new ByteArrayInputStream(bytes), bytes.length, -1) .stream(new ByteArrayInputStream(bytes), bytes.length, -1)
.contentType(contentType) .contentType(contentType)
.build() .build()
); );
log.info("字节数组上传成功: {}", objectName); log.info("字节数组上传成功: {}/{}", bucketName, objectName);
return objectName; // 返回逻辑URL return bucketName + "/" + objectName; // 返回包含桶名的逻辑路径
} catch (Exception e) { } catch (Exception e) {
log.error("字节数组上传失败: {}", e.getMessage(), e); log.error("字节数组上传失败: {}", e.getMessage(), e);
throw new MinioException("字节数组上传失败", e); throw new MinioException("字节数组上传失败", e);
@@ -166,11 +172,12 @@ public class MinioUtil {
* @param bytes 字节数组 * @param bytes 字节数组
* @param fileType 文件类型 * @param fileType 文件类型
* @param contentType 内容类型 * @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); 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 文件输入流 * @return 文件输入流
*/ */
public InputStream downloadFile(String objectName) { public InputStream downloadFile(String logicalPath) {
try { // 解析桶名和对象名bucketName/folder/filename
// 检查文件是否存在 int index = logicalPath.indexOf("/");
if (!doesObjectExist(objectName)) { if (index <= 0) {
throw new IOException("文件不存在: " + objectName); 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 {
return minioClient.getObject( return minioClient.getObject(
GetObjectArgs.builder() GetObjectArgs.builder()
.bucket(minioConfig.getBucketName()) .bucket(bucketName)
.object(objectName) .object(objectName)
.build() .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 { try {
minioClient.removeObject( minioClient.removeObject(
RemoveObjectArgs.builder() RemoveObjectArgs.builder()
.bucket(minioConfig.getBucketName()) .bucket(bucketName)
.object(objectName) .object(objectName)
.build() .build()
); );
log.info("文件删除成功: {}", objectName); log.info("文件删除成功: {}/{}", bucketName, objectName);
} catch (Exception e) { } catch (Exception e) {
log.error("文件删除失败: {}", e.getMessage(), e); log.error("文件删除失败: {}", e.getMessage(), e);
throw new MinioException("文件删除失败", e); throw new MinioException("文件删除失败", e);
@@ -224,25 +266,28 @@ public class MinioUtil {
// ==================== 文件查询操作 ==================== // ==================== 文件查询操作 ====================
/** /**
* 获取文件列表 * 获取指定桶的文件列表
* *
* @param prefix 文件前缀 * @param prefix 文件前缀
* @return 文件列表 * @param bucketName 桶名
* @return 文件列表(包含桶名)
*/ */
public List<String> listFiles(String prefix) { public List<String> listFiles(String prefix, String bucketName) {
List<String> files = new ArrayList<>(); List<String> files = new ArrayList<>();
try { try {
Iterable<Result<Item>> results = minioClient.listObjects( Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder() ListObjectsArgs.builder()
.bucket(minioConfig.getBucketName()) .bucket(bucketName)
.prefix(prefix) .prefix(prefix)
.build() .build()
); );
for (Result<Item> result : results) { for (Result<Item> result : results) {
Item item = result.get(); Item item = result.get();
files.add(item.objectName()); files.add(bucketName + "/" + item.objectName()); // 返回包含桶名的路径
} }
} catch (Exception e) { } catch (Exception e) {
log.error("获取文件列表失败: {}", e.getMessage(), e); log.error("获取文件列表失败: {}", e.getMessage(), e);
@@ -254,14 +299,34 @@ public class MinioUtil {
/** /**
* 检查文件是否存在 * 检查文件是否存在
* *
* @param objectName 对象名称(逻辑路径 * @param logicalPath 逻辑路径格式bucketName/folder/filename
* @return 是否存在 * @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 { try {
minioClient.statObject( minioClient.statObject(
StatObjectArgs.builder() StatObjectArgs.builder()
.bucket(minioConfig.getBucketName()) .bucket(bucketName)
.object(objectName) .object(objectName)
.build() .build()
); );
@@ -278,13 +343,11 @@ public class MinioUtil {
* *
* @param objectName 对象名称(逻辑路径) * @param objectName 对象名称(逻辑路径)
* @param expires 过期时间(秒) * @param expires 过期时间(秒)
* @param bucketName 桶名
* @return 预签名URL * @return 预签名URL
*/ */
public String getPresignedUrl(String objectName, int expires, String bucketName) { public String getPresignedUrl(String objectName, int expires, String bucketName) {
try { try {
if (bucketName == null){
bucketName = minioConfig.getBucketName();
}
return minioClient.getPresignedObjectUrl( return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder() GetPresignedObjectUrlArgs.builder()
.method(Method.GET) .method(Method.GET)
@@ -319,15 +382,16 @@ public class MinioUtil {
/** /**
* 批量获取预签名URL * 批量获取预签名URL
* *
* @param objectNames 对象名称列表(逻辑路径 * @param logicalPaths 逻辑路径列表格式bucketName/folder/filename
* @param expires 过期时间(秒) * @param expires 过期时间(秒)
* @return 预签名URL列表 * @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<>(); List<String> presignedUrls = new ArrayList<>();
for (String objectName : objectNames) { for (String logicalPath : logicalPaths) {
if (objectName != null && !objectName.trim().isEmpty()) { if (logicalPath != null && !logicalPath.trim().isEmpty()) {
presignedUrls.add(getPresignedUrl(objectName, expires, null)); // 所有路径都应该包含桶名
presignedUrls.add(getPresignedUrl(logicalPath, expires));
} }
} }
return presignedUrls; return presignedUrls;
@@ -352,13 +416,14 @@ public class MinioUtil {
/** /**
* 将MinIO逻辑URL转换为预签名URL * 将MinIO逻辑URL转换为预签名URL
* *
* @param logicalUrl 逻辑URL * @param logicalUrl 逻辑URL格式bucketName/folder/filename
* @param expires 过期时间(秒) * @param expires 过期时间(秒)
* @return 预签名URL如果不是逻辑URL则返回原URL * @return 预签名URL如果不是逻辑URL则返回原URL
*/ */
public String convertToPresignedUrl(String logicalUrl, int expires) { public String convertToPresignedUrl(String logicalUrl, int expires) {
if (isMinioLogicalUrl(logicalUrl)) { if (isMinioLogicalUrl(logicalUrl)) {
return getPresignedUrl(logicalUrl, expires, null); // 所有逻辑URL都应该包含桶名
return getPresignedUrl(logicalUrl, expires);
} }
return logicalUrl; return logicalUrl;
} }
@@ -383,17 +448,17 @@ public class MinioUtil {
/** /**
* 获取压缩后的图片Base64编码 * 获取压缩后的图片Base64编码
* *
* @param objectName 对象名称(逻辑路径 * @param logicalPath 逻辑路径(可以包含桶名或不包含
* @param targetWidth 目标宽度 * @param targetWidth 目标宽度
* @param targetHeight 目标高度 * @param targetHeight 目标高度
* @return 压缩后的图片Base64编码 * @return 压缩后的图片Base64编码
*/ */
public String getCompressedImageAsBase64(String objectName, int targetWidth, int targetHeight) throws IOException { public String getCompressedImageAsBase64(String logicalPath, int targetWidth, int targetHeight) throws IOException {
try (InputStream stream = downloadFile(objectName)) { try (InputStream stream = downloadFile(logicalPath)) {
// 读取原始图片 // 读取原始图片
BufferedImage originalImage = ImageIO.read(stream); BufferedImage originalImage = ImageIO.read(stream);
if (originalImage == null) { if (originalImage == null) {
throw new IOException("无法读取图片: " + objectName); throw new IOException("无法读取图片: " + logicalPath);
} }
// 计算压缩比例,保持宽高比 // 计算压缩比例,保持宽高比
@@ -425,7 +490,7 @@ public class MinioUtil {
ImageIO.write(compressedImage, "JPEG", baos); ImageIO.write(compressedImage, "JPEG", baos);
byte[] imageBytes = baos.toByteArray(); 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); return Base64.getEncoder().encodeToString(imageBytes);
} catch (Exception e) { } 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) 替代 * @deprecated 使用 getPresignedUrl(String, int) 替代
*/ */

View File

@@ -1,6 +1,6 @@
-- Lane Carford AI系统基础架构数据库表结构 -- 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; USE lanecrawford;
-- 1. 用户表 -- 1. 用户表