You've already forked FrameTour-BE
feat(image): 实现源图片超分辨率增强流水线
- 引入Pipeline模式重构图片处理流程 - 新增SourcePhotoUpdateStage用于上传并更新源图片URL - 扩展PhotoProcessContext支持超分场景配置 - 增加SOURCE_PHOTO_SUPER_RESOLUTION枚举值 - 修改各Stage判断逻辑适配新的图片类型系统 - 调整SourceService接口支持File类型参数 - 优化超分处理日志记录和异常处理机制
This commit is contained in:
@@ -4,6 +4,13 @@ import cn.hutool.http.HttpUtil;
|
||||
import com.ycwl.basic.annotation.IgnoreToken;
|
||||
import com.ycwl.basic.image.enhancer.adapter.BceImageEnhancer;
|
||||
import com.ycwl.basic.image.enhancer.entity.BceEnhancerConfig;
|
||||
import com.ycwl.basic.image.pipeline.core.Pipeline;
|
||||
import com.ycwl.basic.image.pipeline.core.PipelineBuilder;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.stages.DownloadStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.ImageEnhanceStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.SourcePhotoUpdateStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.CleanupStage;
|
||||
import com.ycwl.basic.mapper.AioDeviceMapper;
|
||||
import com.ycwl.basic.mapper.MemberMapper;
|
||||
import com.ycwl.basic.model.aio.entity.AioDeviceBannerEntity;
|
||||
@@ -41,7 +48,9 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@@ -129,27 +138,39 @@ public class AioDeviceController {
|
||||
redisTemplate.opsForValue().set("aio:faceId:"+resp.getFaceId().toString()+":pass", "1", 1, TimeUnit.DAYS);
|
||||
return;
|
||||
}
|
||||
log.info("超分开始!");
|
||||
log.info("超分开始!共{}张图片待处理", sourcePhotoList.size());
|
||||
|
||||
sourcePhotoList.forEach(photo -> {
|
||||
if (StringUtils.contains(photo.getUrl(), "_q_")) {
|
||||
log.debug("跳过已增强的图片: {}", photo.getUrl());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
File dstFile = new File(photo.getGoodsId()+".jpg");
|
||||
long fileSize = HttpUtil.downloadFile(photo.getUrl(), dstFile);
|
||||
log.info("超分开始:{}", fileSize);
|
||||
BceImageEnhancer enhancer = getEnhancer();
|
||||
MultipartFile enhancedFile = enhancer.enhance(dstFile.getName());
|
||||
log.info("超分结束:{}", photo.getUrl());
|
||||
String url = sourceService.uploadAndUpdateUrl(photo.getGoodsId(), enhancedFile);
|
||||
log.info("上传结束:->{}", url);
|
||||
// 创建超分Pipeline
|
||||
Pipeline<PhotoProcessContext> superResolutionPipeline = createSuperResolutionPipeline(photo.getGoodsId());
|
||||
|
||||
// 使用静态工厂方法创建Context
|
||||
PhotoProcessContext context = PhotoProcessContext.forSuperResolution(
|
||||
photo.getGoodsId(), photo.getUrl(), photo.getScenicId()
|
||||
);
|
||||
|
||||
// 启用图像增强Stage
|
||||
Map<String, Boolean> stageConfig = new HashMap<>();
|
||||
stageConfig.put("image_enhance", true);
|
||||
context.loadStageConfig(null, stageConfig);
|
||||
|
||||
// 执行Pipeline
|
||||
boolean success = superResolutionPipeline.execute(context);
|
||||
|
||||
if (success) {
|
||||
log.info("超分成功: {} -> {}", photo.getUrl(), context.getResultUrl());
|
||||
} else {
|
||||
log.error("超分失败: {}", photo.getGoodsId());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("超分失败:{}", photo.getGoodsId(), e);
|
||||
} finally {
|
||||
File _file = new File(photo.getGoodsId()+".jpg");
|
||||
if (_file.exists()) {
|
||||
_file.delete();
|
||||
}
|
||||
}
|
||||
});
|
||||
redisTemplate.opsForValue().set("aio:faceId:"+sourcePhotoList.getFirst().getFaceId().toString()+":pass", "1", 1, TimeUnit.DAYS);
|
||||
@@ -206,6 +227,28 @@ public class AioDeviceController {
|
||||
return ApiResponse.success(orderService.queryOrder(orderId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建源图片超分辨率增强Pipeline
|
||||
*
|
||||
* @param sourceId 源图片ID
|
||||
* @return 超分Pipeline
|
||||
*/
|
||||
private Pipeline<PhotoProcessContext> createSuperResolutionPipeline(Long sourceId) {
|
||||
// 创建带有百度云配置的ImageEnhanceStage
|
||||
BceEnhancerConfig config = new BceEnhancerConfig();
|
||||
config.setQps(1);
|
||||
config.setAppId("119554288");
|
||||
config.setApiKey("OX6QoijgKio3eVtA0PiUVf7f");
|
||||
config.setSecretKey("dYatXReVriPeiktTjUblhfubpcmYfuMk");
|
||||
|
||||
return new PipelineBuilder<PhotoProcessContext>("SourcePhotoSuperResolutionPipeline")
|
||||
.addStage(new DownloadStage()) // 1. 下载图片
|
||||
.addStage(new ImageEnhanceStage(config)) // 2. 图像增强(超分)
|
||||
.addStage(new SourcePhotoUpdateStage(sourceService, sourceId)) // 3. 上传并更新数据库
|
||||
.addStage(new CleanupStage()) // 4. 清理临时文件
|
||||
.build();
|
||||
}
|
||||
|
||||
private BceImageEnhancer getEnhancer() {
|
||||
BceImageEnhancer enhancer = new BceImageEnhancer();
|
||||
BceEnhancerConfig config = new BceEnhancerConfig();
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ycwl.basic.image.pipeline.annotation;
|
||||
|
||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Stage配置注解
|
||||
* 用于声明Stage的元数据和可选性控制信息
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface StageConfig {
|
||||
|
||||
/**
|
||||
* Stage的唯一标识
|
||||
* 用于外部配置引用该Stage
|
||||
* 例如: "watermark", "download", "upload"
|
||||
*/
|
||||
String stageId();
|
||||
|
||||
/**
|
||||
* 可选性模式
|
||||
* 默认为UNSUPPORT(不支持外部控制)
|
||||
*/
|
||||
StageOptionalMode optionalMode() default StageOptionalMode.UNSUPPORT;
|
||||
|
||||
/**
|
||||
* Stage描述信息
|
||||
* 用于文档和日志说明
|
||||
*/
|
||||
String description() default "";
|
||||
|
||||
/**
|
||||
* 默认是否启用
|
||||
* 仅当optionalMode=SUPPORT时有效
|
||||
* 当外部配置未明确指定时,使用此默认值
|
||||
*/
|
||||
boolean defaultEnabled() default true;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.ycwl.basic.image.pipeline.core;
|
||||
import com.ycwl.basic.image.watermark.entity.WatermarkInfo;
|
||||
import com.ycwl.basic.image.watermark.enums.ImageWatermarkOperatorEnum;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||
import com.ycwl.basic.image.pipeline.enums.PipelineScene;
|
||||
import com.ycwl.basic.integration.common.manager.ScenicConfigManager;
|
||||
import com.ycwl.basic.model.Crop;
|
||||
@@ -15,6 +16,8 @@ import lombok.Setter;
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 图片处理管线上下文
|
||||
@@ -24,10 +27,43 @@ import java.util.Map;
|
||||
@Setter
|
||||
public class PhotoProcessContext {
|
||||
|
||||
private final PrinterOrderItem orderItem;
|
||||
private final TempFileManager tempFileManager;
|
||||
// ==================== 核心字段(构造时必填)====================
|
||||
|
||||
/**
|
||||
* 处理过程唯一标识
|
||||
* 用于 TempFileManager 创建隔离的临时文件目录
|
||||
*/
|
||||
private final String processId;
|
||||
|
||||
/**
|
||||
* 原图 URL
|
||||
*/
|
||||
private final String originalUrl;
|
||||
|
||||
/**
|
||||
* 景区 ID
|
||||
*/
|
||||
private final Long scenicId;
|
||||
|
||||
/**
|
||||
* 临时文件管理器
|
||||
*/
|
||||
private final TempFileManager tempFileManager;
|
||||
|
||||
// ==================== 图片元信息 ====================
|
||||
|
||||
/**
|
||||
* 图片类型
|
||||
*/
|
||||
private ImageType imageType = ImageType.NORMAL_PHOTO;
|
||||
|
||||
/**
|
||||
* 裁剪/旋转信息
|
||||
*/
|
||||
private Crop crop;
|
||||
|
||||
// ==================== 管线配置 ====================
|
||||
|
||||
/**
|
||||
* 景区配置管理器,用于获取景区相关配置
|
||||
*/
|
||||
@@ -50,6 +86,8 @@ public class PhotoProcessContext {
|
||||
*/
|
||||
private Map<String, Boolean> stageEnabledMap = new HashMap<>();
|
||||
|
||||
// ==================== 处理过程状态 ====================
|
||||
|
||||
private File originalFile;
|
||||
private File processedFile;
|
||||
private boolean isLandscape = true;
|
||||
@@ -62,14 +100,78 @@ public class PhotoProcessContext {
|
||||
private String dateFormat;
|
||||
private File qrcodeFile;
|
||||
private Integer offsetLeft;
|
||||
private Crop crop;
|
||||
|
||||
public PhotoProcessContext(PrinterOrderItem orderItem, Long scenicId) {
|
||||
this.orderItem = orderItem;
|
||||
this.scenicId = scenicId;
|
||||
this.tempFileManager = new TempFileManager(orderItem.getId().toString());
|
||||
// ==================== 回调 ====================
|
||||
|
||||
/**
|
||||
* 结果 URL 回调
|
||||
* 用于在 setResultUrl 时通知外部更新相关数据
|
||||
*/
|
||||
private Consumer<String> resultUrlCallback;
|
||||
|
||||
// ==================== 构造函数(私有)====================
|
||||
|
||||
private PhotoProcessContext(Builder builder) {
|
||||
this.processId = builder.processId;
|
||||
this.originalUrl = builder.originalUrl;
|
||||
this.scenicId = builder.scenicId;
|
||||
this.tempFileManager = new TempFileManager(processId);
|
||||
|
||||
this.imageType = builder.imageType;
|
||||
this.crop = builder.crop;
|
||||
this.scene = builder.scene;
|
||||
this.source = builder.source;
|
||||
this.resultUrlCallback = builder.resultUrlCallback;
|
||||
}
|
||||
|
||||
// ==================== 静态工厂方法 ====================
|
||||
|
||||
/**
|
||||
* 从 PrinterOrderItem 创建 Context(打印场景兼容方法)
|
||||
*
|
||||
* @param orderItem 打印订单项
|
||||
* @param scenicId 景区ID
|
||||
* @return PhotoProcessContext
|
||||
*/
|
||||
public static PhotoProcessContext fromPrinterOrderItem(PrinterOrderItem orderItem, Long scenicId) {
|
||||
return PhotoProcessContext.builder()
|
||||
.processId(orderItem.getId().toString())
|
||||
.originalUrl(orderItem.getCropUrl())
|
||||
.scenicId(scenicId)
|
||||
.imageType(ImageType.fromSourceId(orderItem.getSourceId()))
|
||||
.crop(orderItem.getCrop())
|
||||
.resultUrlCallback(url -> orderItem.setCropUrl(url))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 为超分辨率场景创建 Context
|
||||
*
|
||||
* @param itemId 项目ID(用于临时文件隔离)
|
||||
* @param url 原图URL
|
||||
* @param scenicId 景区ID
|
||||
* @return PhotoProcessContext
|
||||
*/
|
||||
public static PhotoProcessContext forSuperResolution(Long itemId, String url, Long scenicId) {
|
||||
return PhotoProcessContext.builder()
|
||||
.processId(itemId.toString())
|
||||
.originalUrl(url)
|
||||
.scenicId(scenicId)
|
||||
.imageType(ImageType.NORMAL_PHOTO)
|
||||
.source(ImageSource.IPC)
|
||||
.scene(PipelineScene.SOURCE_PHOTO_SUPER_RESOLUTION)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Builder
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
// ==================== 业务方法 ====================
|
||||
|
||||
/**
|
||||
* 从景区配置和请求参数中加载Stage开关配置
|
||||
*
|
||||
@@ -94,33 +196,14 @@ public class PhotoProcessContext {
|
||||
return stageEnabledMap.getOrDefault(stageId, defaultEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为拼图类型
|
||||
*/
|
||||
public boolean isPuzzle() {
|
||||
return orderItem.getSourceId() != null && orderItem.getSourceId() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为普通照片
|
||||
*/
|
||||
public boolean isNormalPhoto() {
|
||||
return orderItem.getSourceId() != null && orderItem.getSourceId() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原图URL
|
||||
*/
|
||||
public String getOriginalUrl() {
|
||||
return orderItem.getCropUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置最终处理结果URL
|
||||
*/
|
||||
public void setResultUrl(String url) {
|
||||
this.resultUrl = url;
|
||||
this.orderItem.setCropUrl(url);
|
||||
if (resultUrlCallback != null) {
|
||||
resultUrlCallback.accept(url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,4 +228,72 @@ public class PhotoProcessContext {
|
||||
public void cleanup() {
|
||||
tempFileManager.cleanup();
|
||||
}
|
||||
|
||||
// ==================== Builder ====================
|
||||
|
||||
public static class Builder {
|
||||
private String processId;
|
||||
private String originalUrl;
|
||||
private Long scenicId;
|
||||
private ImageType imageType = ImageType.NORMAL_PHOTO;
|
||||
private Crop crop;
|
||||
private PipelineScene scene;
|
||||
private ImageSource source;
|
||||
private Consumer<String> resultUrlCallback;
|
||||
|
||||
public Builder processId(String processId) {
|
||||
this.processId = processId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder originalUrl(String originalUrl) {
|
||||
this.originalUrl = originalUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder scenicId(Long scenicId) {
|
||||
this.scenicId = scenicId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder imageType(ImageType imageType) {
|
||||
this.imageType = imageType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder crop(Crop crop) {
|
||||
this.crop = crop;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder scene(PipelineScene scene) {
|
||||
this.scene = scene;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder source(ImageSource source) {
|
||||
this.source = source;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder resultUrlCallback(Consumer<String> callback) {
|
||||
this.resultUrlCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
public PhotoProcessContext build() {
|
||||
// 参数校验
|
||||
if (originalUrl == null || originalUrl.isBlank()) {
|
||||
throw new IllegalArgumentException("originalUrl is required");
|
||||
}
|
||||
if (scenicId == null) {
|
||||
throw new IllegalArgumentException("scenicId is required");
|
||||
}
|
||||
// processId 可以自动生成
|
||||
if (processId == null || processId.isBlank()) {
|
||||
processId = UUID.randomUUID().toString();
|
||||
}
|
||||
return new PhotoProcessContext(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.ycwl.basic.image.pipeline.enums;
|
||||
|
||||
/**
|
||||
* 图片类型枚举
|
||||
* 用于区分管线处理的图片类型,替代通过 sourceId 判断的逻辑
|
||||
*/
|
||||
public enum ImageType {
|
||||
|
||||
/**
|
||||
* 普通照片
|
||||
* 对应原 sourceId > 0 的情况(IPC设备拍摄)
|
||||
*/
|
||||
NORMAL_PHOTO("normal", "普通照片"),
|
||||
|
||||
/**
|
||||
* 拼图
|
||||
* 对应原 sourceId == 0 的情况
|
||||
*/
|
||||
PUZZLE("puzzle", "拼图"),
|
||||
|
||||
/**
|
||||
* 手机上传
|
||||
* 对应原 sourceId == null 的情况
|
||||
*/
|
||||
MOBILE_UPLOAD("mobile", "手机上传");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
ImageType(String code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 sourceId 推断图片类型
|
||||
* 用于兼容现有数据结构
|
||||
*
|
||||
* @param sourceId 源ID(null=手机上传, 0=拼图, >0=普通照片)
|
||||
* @return 对应的图片类型
|
||||
*/
|
||||
public static ImageType fromSourceId(Long sourceId) {
|
||||
if (sourceId == null) {
|
||||
return MOBILE_UPLOAD;
|
||||
} else if (sourceId == 0) {
|
||||
return PUZZLE;
|
||||
} else {
|
||||
return NORMAL_PHOTO;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,13 @@ public enum PipelineScene {
|
||||
* 图片增强场景
|
||||
* 包括图片美化、滤镜处理等
|
||||
*/
|
||||
IMAGE_ENHANCE("image_enhance", "图片增强");
|
||||
IMAGE_ENHANCE("image_enhance", "图片增强"),
|
||||
|
||||
/**
|
||||
* 源图片超分辨率增强场景
|
||||
* IPC设备拍摄的源图片进行质量提升
|
||||
*/
|
||||
SOURCE_PHOTO_SUPER_RESOLUTION("source_photo_sr", "源图片超分辨率增强");
|
||||
|
||||
private final String code;
|
||||
private final String description;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ycwl.basic.image.pipeline.enums;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Stage可选性模式枚举
|
||||
* 定义Stage是否支持外部配置控制
|
||||
*/
|
||||
@Getter
|
||||
public enum StageOptionalMode {
|
||||
|
||||
/**
|
||||
* 不支持外部控制
|
||||
* Stage的执行完全由代码中的业务逻辑决定
|
||||
*/
|
||||
UNSUPPORT("不支持外部控制"),
|
||||
|
||||
/**
|
||||
* 支持外部控制
|
||||
* Stage可以通过景区配置或请求参数进行开启/关闭
|
||||
*/
|
||||
SUPPORT("支持外部控制"),
|
||||
|
||||
/**
|
||||
* 强制开启
|
||||
* Stage必须执行,不允许外部配置关闭
|
||||
*/
|
||||
FORCE_ON("强制开启");
|
||||
|
||||
private final String description;
|
||||
|
||||
StageOptionalMode(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.utils.ImageUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -31,7 +32,7 @@ public class ConditionalRotateStage extends AbstractPipelineStage<PhotoProcessCo
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(PhotoProcessContext context) {
|
||||
return context.isNormalPhoto() && !context.isLandscape();
|
||||
return context.getImageType() == ImageType.NORMAL_PHOTO && !context.isLandscape();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -38,7 +39,7 @@ public class DownloadStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||
|
||||
try {
|
||||
String fileExtension = determineFileExtension(context);
|
||||
String filePrefix = context.isPuzzle() ? "puzzle" : "print";
|
||||
String filePrefix = context.getImageType() == ImageType.PUZZLE ? "puzzle" : "print";
|
||||
|
||||
File downloadFile = context.getTempFileManager()
|
||||
.createTempFile(filePrefix, fileExtension);
|
||||
@@ -63,7 +64,7 @@ public class DownloadStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||
}
|
||||
|
||||
private String determineFileExtension(PhotoProcessContext context) {
|
||||
if (context.isPuzzle()) {
|
||||
if (context.getImageType() == ImageType.PUZZLE) {
|
||||
return ".png";
|
||||
}
|
||||
String url = context.getOriginalUrl();
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.utils.ImageUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -30,7 +31,7 @@ public class ImageOrientationStage extends AbstractPipelineStage<PhotoProcessCon
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(PhotoProcessContext context) {
|
||||
return context.isNormalPhoto();
|
||||
return context.getImageType() == ImageType.NORMAL_PHOTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.utils.ImageUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -45,7 +46,7 @@ public class ImageQualityCheckStage extends AbstractPipelineStage<PhotoProcessCo
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(PhotoProcessContext context) {
|
||||
// 仅对普通照片执行质量检测
|
||||
return context.isNormalPhoto();
|
||||
return context.getImageType() == ImageType.NORMAL_PHOTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.utils.ImageUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -34,7 +35,7 @@ public class PuzzleBorderStage extends AbstractPipelineStage<PhotoProcessContext
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(PhotoProcessContext context) {
|
||||
return context.isPuzzle();
|
||||
return context.getImageType() == ImageType.PUZZLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.utils.ImageUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -29,7 +30,7 @@ public class RestoreOrientationStage extends AbstractPipelineStage<PhotoProcessC
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(PhotoProcessContext context) {
|
||||
return context.isNormalPhoto() && context.isNeedRotation();
|
||||
return context.getImageType() == ImageType.NORMAL_PHOTO && context.isNeedRotation();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.ycwl.basic.image.pipeline.stages;
|
||||
|
||||
import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
||||
import com.ycwl.basic.service.pc.SourceService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 源图片上传和更新Stage
|
||||
* 专门用于将增强后的图片上传并更新数据库中的URL
|
||||
*
|
||||
* 与UploadStage的区别:
|
||||
* - UploadStage: 通用上传,仅上传到存储并设置Context.resultUrl
|
||||
* - SourcePhotoUpdateStage: 专门用于源图片,上传+更新数据库source表的URL字段
|
||||
*/
|
||||
@Slf4j
|
||||
@StageConfig(
|
||||
stageId = "source_photo_update",
|
||||
optionalMode = StageOptionalMode.FORCE_ON,
|
||||
description = "源图片上传和数据库更新",
|
||||
defaultEnabled = true
|
||||
)
|
||||
public class SourcePhotoUpdateStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||
|
||||
private final SourceService sourceService;
|
||||
private final Long sourceId;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param sourceService 源图片服务
|
||||
* @param sourceId 源图片ID
|
||||
*/
|
||||
public SourcePhotoUpdateStage(SourceService sourceService, Long sourceId) {
|
||||
this.sourceService = sourceService;
|
||||
this.sourceId = sourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "SourcePhotoUpdateStage";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StageResult doExecute(PhotoProcessContext context) {
|
||||
File fileToUpload = context.getCurrentFile();
|
||||
if (fileToUpload == null || !fileToUpload.exists()) {
|
||||
return StageResult.failed("没有可上传的文件");
|
||||
}
|
||||
|
||||
if (sourceService == null) {
|
||||
return StageResult.failed("SourceService未注入");
|
||||
}
|
||||
|
||||
if (sourceId == null) {
|
||||
return StageResult.failed("SourceId为空");
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
// 调用SourceService上传并更新URL
|
||||
String uploadedUrl = sourceService.uploadAndUpdateUrl(sourceId, fileToUpload);
|
||||
|
||||
// 设置结果URL到Context
|
||||
context.setResultUrl(uploadedUrl);
|
||||
|
||||
log.info("源图片上传并更新成功: sourceId={}, url={}", sourceId, uploadedUrl);
|
||||
|
||||
return StageResult.success("已上传并更新: " + uploadedUrl);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("源图片上传并更新失败: sourceId={}", sourceId, e);
|
||||
return StageResult.failed("上传并更新失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.ycwl.basic.image.pipeline.annotation.StageConfig;
|
||||
import com.ycwl.basic.image.pipeline.core.AbstractPipelineStage;
|
||||
import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.core.StageResult;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||
import com.ycwl.basic.image.pipeline.enums.StageOptionalMode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -37,7 +38,7 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
|
||||
|
||||
@Override
|
||||
protected boolean shouldExecuteByBusinessLogic(PhotoProcessContext context) {
|
||||
return context.isNormalPhoto();
|
||||
return context.getImageType() == ImageType.NORMAL_PHOTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.ycwl.basic.model.pc.source.req.SourceReqQuery;
|
||||
import com.ycwl.basic.model.pc.source.resp.SourceRespVO;
|
||||
import com.ycwl.basic.utils.ApiResponse;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -21,5 +22,5 @@ public interface SourceService {
|
||||
ApiResponse<Integer> update(SourceEntity source);
|
||||
|
||||
ApiResponse cutVideo(Long id);
|
||||
String uploadAndUpdateUrl(Long id, org.springframework.web.multipart.MultipartFile file);
|
||||
String uploadAndUpdateUrl(Long id, File file);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ycwl.basic.storage.StorageFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
@@ -216,14 +217,14 @@ public class SourceServiceImpl implements SourceService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadAndUpdateUrl(Long id, MultipartFile file) {
|
||||
public String uploadAndUpdateUrl(Long id, File file) {
|
||||
SourceRespVO source = sourceMapper.getById(id);
|
||||
if (source == null) {
|
||||
throw new BaseException("该素材不存在");
|
||||
}
|
||||
try {
|
||||
IStorageAdapter adapter = scenicService.getScenicStorageAdapter(source.getScenicId());
|
||||
String uploadedUrl = adapter.uploadFile(file, PHOTO_PATH, id + "_q_.jpg");
|
||||
String uploadedUrl = adapter.uploadFile("image/jpeg", file, PHOTO_PATH, id + "_q_.jpg");
|
||||
|
||||
SourceEntity sourceUpd = new SourceEntity();
|
||||
sourceUpd.setId(id);
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.ycwl.basic.image.pipeline.core.PhotoProcessContext;
|
||||
import com.ycwl.basic.image.pipeline.core.Pipeline;
|
||||
import com.ycwl.basic.image.pipeline.core.PipelineBuilder;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageSource;
|
||||
import com.ycwl.basic.image.pipeline.enums.ImageType;
|
||||
import com.ycwl.basic.image.pipeline.enums.PipelineScene;
|
||||
import com.ycwl.basic.image.pipeline.stages.CleanupStage;
|
||||
import com.ycwl.basic.image.pipeline.stages.ConditionalRotateStage;
|
||||
@@ -776,7 +777,7 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
private String processPhotoWithPipeline(MemberPrintResp item, Long scenicId, File qrCodeFile) {
|
||||
PrinterOrderItem orderItem = PrinterOrderItem.fromMemberPrintResp(item);
|
||||
|
||||
PhotoProcessContext context = new PhotoProcessContext(orderItem, scenicId);
|
||||
PhotoProcessContext context = PhotoProcessContext.fromPrinterOrderItem(orderItem, scenicId);
|
||||
context.setQrcodeFile(qrCodeFile);
|
||||
|
||||
try {
|
||||
@@ -799,13 +800,13 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
context.setSource(ImageSource.UNKNOWN);
|
||||
}
|
||||
|
||||
if (context.isNormalPhoto()) {
|
||||
if (context.getImageType() == ImageType.NORMAL_PHOTO) {
|
||||
prepareNormalPhotoContext(context);
|
||||
} else if (context.isPuzzle()) {
|
||||
} else if (context.getImageType() == ImageType.PUZZLE) {
|
||||
prepareStorageAdapter(context);
|
||||
}
|
||||
|
||||
Pipeline<PhotoProcessContext> pipeline = context.isPuzzle()
|
||||
Pipeline<PhotoProcessContext> pipeline = context.getImageType() == ImageType.PUZZLE
|
||||
? createPuzzlePipeline()
|
||||
: createNormalPhotoPipeline();
|
||||
|
||||
@@ -814,7 +815,7 @@ public class PrinterServiceImpl implements PrinterService {
|
||||
if (success && context.getResultUrl() != null) {
|
||||
log.info("照片处理成功: photoId={}, type={}, url={}",
|
||||
item.getId(),
|
||||
context.isPuzzle() ? "拼图" : "普通照片",
|
||||
context.getImageType() == ImageType.PUZZLE ? "拼图" : "普通照片",
|
||||
context.getResultUrl());
|
||||
return context.getResultUrl();
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user