4.7 阿里云百炼AI扩图接口实现

This commit is contained in:
zhangsan 2025-04-07 17:23:26 +08:00
parent a44466e846
commit e40c4a975b
11 changed files with 486 additions and 13 deletions

View File

@ -0,0 +1,87 @@
package edu.whut.smilepicturebackend.api.aliyunai;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONUtil;
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskRequest;
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskResponse;
import edu.whut.smilepicturebackend.api.aliyunai.model.GetOutPaintingTaskResponse;
import edu.whut.smilepicturebackend.exception.BusinessException;
import edu.whut.smilepicturebackend.exception.ErrorCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class AliYunAiApi {
// 读取配置文件
@Value("${smile-picture.aliyun.apiKey}")
private String apiKey;
// 创建任务地址
public static final String CREATE_OUT_PAINTING_TASK_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/image2image/out-painting";
// 查询任务状态
public static final String GET_OUT_PAINTING_TASK_URL = "https://dashscope.aliyuncs.com/api/v1/tasks/%s";
/**
* 创建任务
*
* @param createOutPaintingTaskRequest
* @return
*/
public CreateOutPaintingTaskResponse createOutPaintingTask(CreateOutPaintingTaskRequest createOutPaintingTaskRequest) {
if (createOutPaintingTaskRequest == null) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "扩图参数为空");
}
// 发送请求
HttpRequest httpRequest = HttpRequest.post(CREATE_OUT_PAINTING_TASK_URL)
.header("Authorization", "Bearer " + apiKey)
// 必须开启异步处理 enable
.header("X-DashScope-Async", "enable")
.header("Content-Type", "application/json")
.body(JSONUtil.toJsonStr(createOutPaintingTaskRequest));
// 处理响应
//这段代码会在 try 块结束时正常返回或抛出异常自动调用 httpResponse.close()从而释放所有底层资源
try (HttpResponse httpResponse = httpRequest.execute()) {
if (!httpResponse.isOk()) {
log.error("请求异常:{}", httpResponse.body());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI 扩图失败");
}
CreateOutPaintingTaskResponse createOutPaintingTaskResponse = JSONUtil.toBean(httpResponse.body(), CreateOutPaintingTaskResponse.class);
if (createOutPaintingTaskResponse.getCode() != null) {
String errorMessage = createOutPaintingTaskResponse.getMessage();
log.error("请求异常:{}", errorMessage);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI 扩图失败," + errorMessage);
}
return createOutPaintingTaskResponse;
}
}
/**
* 查询创建的任务结果
*
* @param taskId
* @return
*/
public GetOutPaintingTaskResponse getOutPaintingTask(String taskId) {
if (StrUtil.isBlank(taskId)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "任务 ID 不能为空");
}
// 处理响应
String url = String.format(GET_OUT_PAINTING_TASK_URL, taskId);
try (HttpResponse httpResponse = HttpRequest.get(url)
.header("Authorization", "Bearer " + apiKey)
.execute()) {
if (!httpResponse.isOk()) {
log.error("请求异常:{}", httpResponse.body());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取任务结果失败");
}
return JSONUtil.toBean(httpResponse.body(), GetOutPaintingTaskResponse.class);
}
}
}

View File

@ -0,0 +1,112 @@
package edu.whut.smilepicturebackend.api.aliyunai.model;
import cn.hutool.core.annotation.Alias;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.io.Serializable;
/**
* 创建扩图任务请求
*/
@Data
public class CreateOutPaintingTaskRequest implements Serializable {
/**
* 模型例如 "image-out-painting"
*/
private String model = "image-out-painting";
/**
* 输入图像信息
*/
private Input input;
/**
* 图像处理参数
*/
private Parameters parameters;
@Data
public static class Input {
/**
* 必选图像 URL
*/
@Alias("image_url") //java类中一般写驼峰式的但是大模型接收的是image_url 所以用@Alias
private String imageUrl;
}
@Data
public static class Parameters implements Serializable {
/**
* 可选逆时针旋转角度默认值 0取值范围 [0, 359]
*/
private Integer angle;
/**
* 可选输出图像的宽高比默认空字符串不设置宽高比
* 可选值["", "1:1", "3:4", "4:3", "9:16", "16:9"]
*/
@Alias("output_ratio")
private String outputRatio;
/**
* 可选图像居中在水平方向上按比例扩展默认值 1.0范围 [1.0, 3.0]
*/
@Alias("x_scale")
@JsonProperty("xScale")
private Float xScale;
/**
* 可选图像居中在垂直方向上按比例扩展默认值 1.0范围 [1.0, 3.0]
*/
@Alias("y_scale")
@JsonProperty("yScale")
private Float yScale;
/**
* 可选在图像上方添加像素默认值 0
*/
@Alias("top_offset")
private Integer topOffset;
/**
* 可选在图像下方添加像素默认值 0
*/
@Alias("bottom_offset")
private Integer bottomOffset;
/**
* 可选在图像左侧添加像素默认值 0
*/
@Alias("left_offset")
private Integer leftOffset;
/**
* 可选在图像右侧添加像素默认值 0
*/
@Alias("right_offset")
private Integer rightOffset;
/**
* 可选开启图像最佳质量模式默认值 false
* 若为 true耗时会成倍增加
*/
@Alias("best_quality")
private Boolean bestQuality;
/**
* 可选限制模型生成的图像文件大小默认值 true
* - 单边长度 <= 10000输出图像文件大小限制为 5MB 以下
* - 单边长度 > 10000输出图像文件大小限制为 10MB 以下
*/
@Alias("limit_image_size")
private Boolean limitImageSize;
/**
* 可选添加 "Generated by AI" 水印默认值 true
*/
@Alias("add_watermark")
private Boolean addWatermark = false;
}
}

View File

@ -0,0 +1,60 @@
package edu.whut.smilepicturebackend.api.aliyunai.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 创建扩图任务响应类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateOutPaintingTaskResponse {
private Output output;
/**
* 表示任务的输出信息
*/
@Data
public static class Output {
/**
* 任务 ID
*/
private String taskId;
/**
* 任务状态
* <ul>
* <li>PENDING排队中</li>
* <li>RUNNING处理中</li>
* <li>SUSPENDED挂起</li>
* <li>SUCCEEDED执行成功</li>
* <li>FAILED执行失败</li>
* <li>UNKNOWN任务不存在或状态未知</li>
* </ul>
*/
private String taskStatus;
}
/**
* 接口错误码
* <p>接口成功请求不会返回该参数</p>
*/
private String code;
/**
* 接口错误信息
* <p>接口成功请求不会返回该参数</p>
*/
private String message;
/**
* 请求唯一标识
* <p>可用于请求明细溯源和问题排查</p>
*/
private String requestId;
}

View File

@ -0,0 +1,111 @@
package edu.whut.smilepicturebackend.api.aliyunai.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 查询扩图任务响应类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GetOutPaintingTaskResponse {
/**
* 请求唯一标识
*/
private String requestId;
/**
* 输出信息
*/
private Output output;
/**
* 表示任务的输出信息
*/
@Data
public static class Output {
/**
* 任务 ID
*/
private String taskId;
/**
* 任务状态
* <ul>
* <li>PENDING排队中</li>
* <li>RUNNING处理中</li>
* <li>SUSPENDED挂起</li>
* <li>SUCCEEDED执行成功</li>
* <li>FAILED执行失败</li>
* <li>UNKNOWN任务不存在或状态未知</li>
* </ul>
*/
private String taskStatus;
/**
* 提交时间
* 格式YYYY-MM-DD HH:mm:ss.SSS
*/
private String submitTime;
/**
* 调度时间
* 格式YYYY-MM-DD HH:mm:ss.SSS
*/
private String scheduledTime;
/**
* 结束时间
* 格式YYYY-MM-DD HH:mm:ss.SSS
*/
private String endTime;
/**
* 输出图像的 URL
*/
private String outputImageUrl;
/**
* 接口错误码
* <p>接口成功请求不会返回该参数</p>
*/
private String code;
/**
* 接口错误信息
* <p>接口成功请求不会返回该参数</p>
*/
private String message;
/**
* 任务指标信息
*/
private TaskMetrics taskMetrics;
}
/**
* 表示任务的统计信息
*/
@Data
public static class TaskMetrics {
/**
* 总任务数
*/
private Integer total;
/**
* 成功任务数
*/
private Integer succeeded;
/**
* 失败任务数
*/
private Integer failed;
}
}

View File

@ -1,7 +1,11 @@
package edu.whut.smilepicturebackend.controller; package edu.whut.smilepicturebackend.controller;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import edu.whut.smilepicturebackend.annotation.AuthCheck; import edu.whut.smilepicturebackend.annotation.AuthCheck;
import edu.whut.smilepicturebackend.api.aliyunai.AliYunAiApi;
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskResponse;
import edu.whut.smilepicturebackend.api.aliyunai.model.GetOutPaintingTaskResponse;
import edu.whut.smilepicturebackend.api.imagesearch.ImageSearchApiFacade; import edu.whut.smilepicturebackend.api.imagesearch.ImageSearchApiFacade;
import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult; import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult;
import edu.whut.smilepicturebackend.common.BaseResponse; import edu.whut.smilepicturebackend.common.BaseResponse;
@ -24,7 +28,6 @@ import edu.whut.smilepicturebackend.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -42,6 +45,7 @@ public class PictureController {
private final UserService userService; private final UserService userService;
private final PictureService pictureService; private final PictureService pictureService;
private final SpaceService spaceService; private final SpaceService spaceService;
private final AliYunAiApi aliYunAiApi;
@ -305,4 +309,32 @@ public class PictureController {
pictureService.editPictureByBatch(pictureEditByBatchRequest, loginUser); pictureService.editPictureByBatch(pictureEditByBatchRequest, loginUser);
return ResultUtils.success(true); return ResultUtils.success(true);
} }
/**
*创建 AI 扩图任务
* @param createPictureOutPaintingTaskRequest
* @param request
* @return
*/
@PostMapping("/out_painting/create_task")
// @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT)
public BaseResponse<CreateOutPaintingTaskResponse> createPictureOutPaintingTask(@RequestBody CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest,
HttpServletRequest request) {
if (createPictureOutPaintingTaskRequest == null || createPictureOutPaintingTaskRequest.getPictureId() == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User loginUser = userService.getLoginUser(request);
CreateOutPaintingTaskResponse response = pictureService.createPictureOutPaintingTask(createPictureOutPaintingTaskRequest, loginUser);
return ResultUtils.success(response);
}
/**
* 查询 AI 扩图任务 前端轮询结果当结果中有SUCCESS那么同时取出该响应中的结果
*/
@GetMapping("/out_painting/get_task")
public BaseResponse<GetOutPaintingTaskResponse> getPictureOutPaintingTask(String taskId) {
ThrowUtils.throwIf(StrUtil.isBlank(taskId), ErrorCode.PARAMS_ERROR);
GetOutPaintingTaskResponse task = aliYunAiApi.getOutPaintingTask(taskId);
return ResultUtils.success(task);
}
} }

View File

@ -58,6 +58,7 @@ public abstract class PictureUploadTemplate {
String uploadPath = String.format("/%s/%s/%s",projectName, uploadPathPrefix, uploadFilename); String uploadPath = String.format("/%s/%s/%s",projectName, uploadPathPrefix, uploadFilename);
File file = null; File file = null;
try { try {
log.info("uploadPath"+uploadPath);
// 3. 创建临时文件获取文件到服务器 // 3. 创建临时文件获取文件到服务器
file = File.createTempFile(uploadPath, null); file = File.createTempFile(uploadPath, null);
// 处理文件来源 // 处理文件来源

View File

@ -9,6 +9,7 @@ import cn.hutool.http.Method;
import edu.whut.smilepicturebackend.exception.BusinessException; import edu.whut.smilepicturebackend.exception.BusinessException;
import edu.whut.smilepicturebackend.exception.ErrorCode; import edu.whut.smilepicturebackend.exception.ErrorCode;
import edu.whut.smilepicturebackend.exception.ThrowUtils; import edu.whut.smilepicturebackend.exception.ThrowUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.File; import java.io.File;
@ -21,6 +22,7 @@ import java.util.List;
* URL 图片上传 * URL 图片上传
*/ */
@Service @Service
@Slf4j
public class UrlPictureUpload extends PictureUploadTemplate { public class UrlPictureUpload extends PictureUploadTemplate {
@Override @Override
@ -46,14 +48,15 @@ public class UrlPictureUpload extends PictureUploadTemplate {
* @return 不含点的扩展名比如 "jpg","png"取不出时返回空串 * @return 不含点的扩展名比如 "jpg","png"取不出时返回空串
*/ */
protected String fetchAndValidateExtension(String fileUrl) { protected String fetchAndValidateExtension(String fileUrl) {
log.info("收到的fileurl:{}",fileUrl);
try (HttpResponse resp = HttpUtil try (HttpResponse resp = HttpUtil
.createRequest(Method.HEAD, fileUrl) .createRequest(Method.HEAD, fileUrl)
.execute()) { .execute()) {
if (resp.getStatus() != HttpStatus.HTTP_OK) { if (resp.getStatus() != HttpStatus.HTTP_OK) {
throw new BusinessException( // 网络或权限问题时直接降级
ErrorCode.OPERATION_ERROR, "文件不存在或不可访问"); log.warn("HEAD 请求未返回 200status = {},将从 URL 中提取后缀", resp.getStatus());
return "";
} }
// 1) Content-Type 验证 & 提取扩展名 // 1) Content-Type 验证 & 提取扩展名
String ct = resp.header("Content-Type"); String ct = resp.header("Content-Type");
String ext = ""; String ext = "";
@ -95,16 +98,21 @@ public class UrlPictureUpload extends PictureUploadTemplate {
@Override @Override
protected String getOriginFilename(Object inputSource) { protected String getOriginFilename(Object inputSource) {
String fileUrl = (String) inputSource; String fileUrl = (String) inputSource;
// 1) HEAD 验证并拿扩展名只这一处会发 HEAD // 先把 query string 去掉
String ext = fetchAndValidateExtension(fileUrl); int qIdx = fileUrl.indexOf('?');
// 2) fallback若服务器没返回类型再从 URL 中简单截取 String cleanUrl = qIdx > 0 ? fileUrl.substring(0, qIdx) : fileUrl;
// 1) 尝试 HEAD 拿扩展名不可用时 ext == ""
String ext = fetchAndValidateExtension(cleanUrl);
// 2) fallback ext 为空就直接返回去掉参数后的 URL
if (StrUtil.isBlank(ext)) { if (StrUtil.isBlank(ext)) {
ext = FileUtil.extName(fileUrl); return cleanUrl;
} else {
// 3) cleanUrl 中拿到 baseName再拼回去
String base = FileUtil.mainName(FileUtil.getName(cleanUrl));
return base + "." + ext;
} }
ThrowUtils.throwIf(ext==null,ErrorCode.PARAMS_ERROR,"不正确的图片格式");
// 3) 拿到 baseName然后拼回去
String base = FileUtil.mainName(FileUtil.getName(fileUrl));
return StrUtil.isNotBlank(ext) ? base + "." + ext : base;
} }
@Override @Override

View File

@ -0,0 +1,24 @@
package edu.whut.smilepicturebackend.model.dto.picture;
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskRequest;
import lombok.Data;
import java.io.Serializable;
/**
* 创建扩图任务请求
*/
@Data
public class CreatePictureOutPaintingTaskRequest implements Serializable {
/**
* 图片 id
*/
private Long pictureId;
/**
* 扩图参数
*/
private CreateOutPaintingTaskRequest.Parameters parameters;
private static final long serialVersionUID = 1L;
}

View File

@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskResponse;
import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult; import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult;
import edu.whut.smilepicturebackend.model.dto.picture.*; import edu.whut.smilepicturebackend.model.dto.picture.*;
import edu.whut.smilepicturebackend.model.entity.Picture; import edu.whut.smilepicturebackend.model.entity.Picture;
@ -144,4 +145,11 @@ public interface PictureService extends IService<Picture> {
*/ */
void editPictureByBatch(PictureEditByBatchRequest pictureEditByBatchRequest, User loginUser); void editPictureByBatch(PictureEditByBatchRequest pictureEditByBatchRequest, User loginUser);
/**
* 创建扩图任务
*
* @param createPictureOutPaintingTaskRequest
* @param loginUser
*/
CreateOutPaintingTaskResponse createPictureOutPaintingTask(CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, User loginUser);
} }

View File

@ -12,6 +12,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import edu.whut.smilepicturebackend.api.aliyunai.AliYunAiApi;
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskRequest;
import edu.whut.smilepicturebackend.api.aliyunai.model.CreateOutPaintingTaskResponse;
import edu.whut.smilepicturebackend.api.imagesearch.ImageSearchApiFacade; import edu.whut.smilepicturebackend.api.imagesearch.ImageSearchApiFacade;
import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult; import edu.whut.smilepicturebackend.api.imagesearch.model.ImageSearchResult;
import edu.whut.smilepicturebackend.exception.BusinessException; import edu.whut.smilepicturebackend.exception.BusinessException;
@ -76,6 +79,8 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
private final CosManager cosManager; private final CosManager cosManager;
private final SpaceService spaceService; private final SpaceService spaceService;
private final TransactionTemplate transactionTemplate; private final TransactionTemplate transactionTemplate;
private final AliYunAiApi aliYunAiApi;
@Override @Override
public void validPicture(Picture picture) { public void validPicture(Picture picture) {
ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR); ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR);
@ -154,6 +159,7 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
// 根据 inputSource 的类型区分上传方式!! // 根据 inputSource 的类型区分上传方式!!
PictureUploadTemplate pictureUploadTemplate = filePictureUpload; PictureUploadTemplate pictureUploadTemplate = filePictureUpload;
if (inputSource instanceof String) { if (inputSource instanceof String) {
log.info("收到 upload/url 请求url = {}", inputSource);
pictureUploadTemplate = urlPictureUpload; pictureUploadTemplate = urlPictureUpload;
} }
//上传到腾讯云COS上 //上传到腾讯云COS上
@ -680,6 +686,26 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "批量编辑失败"); ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR, "批量编辑失败");
} }
@Override
public CreateOutPaintingTaskResponse createPictureOutPaintingTask(CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, User loginUser) {
// 获取图片信息
Long pictureId = createPictureOutPaintingTaskRequest.getPictureId();
Picture picture = this.getById(pictureId);
if (picture == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "图片不存在");
}
// 校验权限已经改为使用注解鉴权
// checkPictureAuth(loginUser, picture);
// 创建扩图任务
CreateOutPaintingTaskRequest createOutPaintingTaskRequest = new CreateOutPaintingTaskRequest();
CreateOutPaintingTaskRequest.Input input = new CreateOutPaintingTaskRequest.Input();
input.setImageUrl(picture.getUrl());
createOutPaintingTaskRequest.setInput(input);
createOutPaintingTaskRequest.setParameters(createPictureOutPaintingTaskRequest.getParameters());
// 创建任务
return aliYunAiApi.createOutPaintingTask(createOutPaintingTaskRequest);
}
/** /**
* nameRule 格式图片-{序号} =>图片-1 图片-2 ... * nameRule 格式图片-{序号} =>图片-1 图片-2 ...
* *

View File

@ -69,3 +69,7 @@ cos:
secretKey: ${smile-picture.cos.client.secretKey} secretKey: ${smile-picture.cos.client.secretKey}
region: ${smile-picture.cos.client.region} region: ${smile-picture.cos.client.region}
bucket: ${smile-picture.cos.client.bucket} bucket: ${smile-picture.cos.client.bucket}
smile-picture:
aliyun:
apiKey: ${smile-picture.aliyun.apiKey}