本部分内容主要来源于鱼皮智能协图云图库部分,并在笔者个人项目学习的基础上进行扩展衍生。由于项目开发文档已经足够详细,因此这里只记录要点。
这部分内容后端较为简单。
一、基础图片编辑
需求分析
在日常的图片管理中,用户经常需要对图片进行简单处理,比如裁剪多余部分、旋转图片、放大缩小尺寸等。
因此,我们首先要引入基础图片编辑功能,帮助用户快速完成以下操作:
- 裁剪:支持按固定比例或自由裁剪
- 旋转:提供顺时针、逆时针旋转功能
这个功能非常适合上传证件照之类的场景。
注意,该功能不需要限制仅在空间内才能使用,公共图库也可以支持。
方案设计
图片编辑功能的实现以前端为主,编辑完成后通过调用现有的图片上传接口,将编辑后的图片保存至平台。
具体业务流程:
- 在图片上传页面,如果用户已上传图片,页面会展示 "编辑图片" 按钮。
- 用户点击 "编辑图片" 后,将打开图片编辑的弹窗组件,支持裁剪、旋转等操作。
- 用户确认编辑后,会调用图片上传接口,将编辑后的新图片保存至平台,同时更新图片信息。
其实还有另一种设计,在用户每次选择本地或 URL 图片时,先不调用后端的图片上传接口,而是自动弹出图片编辑弹窗组件,编辑完后再保存。但这样做就不是 "扩展功能" 而是 "修改已有功能",涉及到的代码改动会更多,感兴趣的同学可以尝试实现。
AI 图片编辑
方案设计
1.由于 AI 绘画任务计算量大且耗时长,所以选择异步调用
同步调用流程如下,好处是客户端可以直接获取到结果,调用更方便:

异步调用流程如下,客户端需要在提交任务后,不断轮询请求,来检查任务是否执行完成:

2.选择前端轮询
1)前端轮询
前端调用后端提交任务后得到任务 ID,然后通过定时器轮询请求查询任务状态接口,直到任务完成或失败。
2)后端轮询
后端通过循环或定时任务检测任务状态,接口保持阻塞,直到任务完成或失败,直接返回结果给前端。
后端开发
新建数据模型类
java
@Data
public class CreateOutPaintingTaskRequest implements Serializable {
private String model = "image-out-painting";
private Input input;
private Parameters parameters;
@Data
public static class Input {
@Alias("image_url")
private String imageUrl;
}
@Data
public static class Parameters implements Serializable {
private Integer angle;
@Alias("output_ratio")
private String outputRatio;
@Alias("x_scale")
@JsonProperty("xScale")
private Float xScale;
@Alias("y_scale")
@JsonProperty("yScale")
private Float yScale;
@Alias("top_offset")
private Integer topOffset;
@Alias("bottom_offset")
private Integer bottomOffset;
@Alias("left_offset")
private Integer leftOffset;
@Alias("right_offset")
private Integer rightOffset;
@Alias("best_quality")
private Boolean bestQuality;
@Alias("limit_image_size")
private Boolean limitImageSize;
@Alias("add_watermark")
private Boolean addWatermark = false;
}
}
前端如果传递参数名 xScale,是无法赋值给 xScale 字段的;但是传递参数名 xscale,就可以赋值。这是因为 SpringMVC 对于第二个字母是大写的参数无法映射(和参数类别无关)
xScale字段 → Lombok 生成getXScale() → Jackson 推断属性名为XScale → 前端传xScale无法匹配,传xscale模糊匹配成功 → 出现你遇到的诡异现象。
原因与JavaBean有关,相关知识详解见这里。
API 调用类,通过 Hutool 的 HTTP 请求工具类来调用阿里云百炼的 API
java
@Slf4j
@Component
public class AliYunAiApi {
@Value("${aliYunAi.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";
public CreateOutPaintingTaskResponse createOutPaintingTask(CreateOutPaintingTaskRequest createOutPaintingTaskRequest) {
if (createOutPaintingTaskRequest == null) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "扩图参数为空");
}
HttpRequest httpRequest = HttpRequest.post(CREATE_OUT_PAINTING_TASK_URL)
.header(Header.AUTHORIZATION, "Bearer " + apiKey)
.header("X-DashScope-Async", "enable")
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue())
.body(JSONUtil.toJsonStr(createOutPaintingTaskRequest));
try (HttpResponse httpResponse = httpRequest.execute()) {
if (!httpResponse.isOk()) {
log.error("请求异常:{}", httpResponse.body());
throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI 扩图失败");
}
CreateOutPaintingTaskResponse response = JSONUtil.toBean(httpResponse.body(), CreateOutPaintingTaskResponse.class);
String errorCode = response.getCode();
if (StrUtil.isNotBlank(errorCode)) {
String errorMessage = response.getMessage();
log.error("AI 扩图失败,errorCode:{}, errorMessage:{}", errorCode, errorMessage);
throw new BusinessException(ErrorCode.OPERATION_ERROR, "AI 扩图接口响应异常");
}
return response;
}
}
public GetOutPaintingTaskResponse getOutPaintingTask(String taskId) {
if (StrUtil.isBlank(taskId)) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "任务 id 不能为空");
}
try (HttpResponse httpResponse = HttpRequest.get(String.format(GET_OUT_PAINTING_TASK_URL, taskId))
.header(Header.AUTHORIZATION, "Bearer " + apiKey)
.execute()) {
if (!httpResponse.isOk()) {
throw new BusinessException(ErrorCode.OPERATION_ERROR, "获取任务失败");
}
return JSONUtil.toBean(httpResponse.body(), GetOutPaintingTaskResponse.class);
}
}
}
要点:
try-with-resources的核心标识是try后的**圆括号(),**括号里面声明的就是需要自动释放的资源。
java
@Override
public CreateOutPaintingTaskResponse createPictureOutPaintingTask(CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest, User loginUser) {
Long pictureId = createPictureOutPaintingTaskRequest.getPictureId();
Picture picture = Optional.ofNullable(this.getById(pictureId))
.orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND_ERROR));
checkPictureAuth(loginUser, picture);
CreateOutPaintingTaskRequest taskRequest = new CreateOutPaintingTaskRequest();
CreateOutPaintingTaskRequest.Input input = new CreateOutPaintingTaskRequest.Input();
input.setImageUrl(picture.getUrl());
taskRequest.setInput(input);
BeanUtil.copyProperties(createPictureOutPaintingTaskRequest, taskRequest);
return aliYunAiApi.createOutPaintingTask(taskRequest);
}
要点:
Picture picture = Optional.ofNullable(this.getById(pictureId)) .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND_ERROR));
Optional.ofNullable(...):把查询结果(可能为null,比如图片 ID 不存在)包装成Optional对象,允许后续优雅处理null值。orElseThrow(...):如果查询结果为null(图片不存在),直接抛出自定义业务异常 (BusinessException),错误码为NOT_FOUND_ERROR(未找到),终止后续流程,避免后续对picture对象操作出现NullPointerException(空指针异常)。
