refactor(image): 重构图片旋转和恢复逻辑

- 将 needRotation 标志重命名为 rotationApplied
- 修改条件旋转阶段的执行逻辑,基于实际旋转角度判断
- 实现通用的图片恢复旋转功能,支持90/180/270度恢复
- 添加恢复旋转角度计算方法 getRestoreAngle
- 更新水印阶段的旋转状态检查逻辑
- 完善单元测试覆盖各种旋转场景
- 优化日志记录和错误处理流程
This commit is contained in:
2025-11-26 20:15:02 +08:00
parent 40d5874560
commit 1945639f90
23 changed files with 147 additions and 79 deletions

View File

@@ -13,6 +13,7 @@ Image Pipeline 是一个通用的、可扩展的图片处理管线框架,用
- **配置驱动**: 支持通过外部配置控制 Stage 的启用/禁用 - **配置驱动**: 支持通过外部配置控制 Stage 的启用/禁用
- **类型安全**: 使用泛型和枚举确保类型安全 - **类型安全**: 使用泛型和枚举确保类型安全
- **解耦设计**: Context 独立于业务模型,支持多种使用场景 - **解耦设计**: Context 独立于业务模型,支持多种使用场景
- **自动清理**: 无论成功或失败都会在 finally 中兜底调用 `context.cleanup()`
## 包结构 ## 包结构
@@ -207,6 +208,8 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
| ImageEnhanceStage | 图像增强(超分) | SUPPORT | 可配置 | | ImageEnhanceStage | 图像增强(超分) | SUPPORT | 可配置 |
| ImageQualityCheckStage | 图像质量检测 | SUPPORT | 仅普通照片 | | ImageQualityCheckStage | 图像质量检测 | SUPPORT | 仅普通照片 |
> **提示**:`ImageEnhanceStage` 的默认构造函数会尝试从 `BCE_IMAGE_APP_ID/BCE_IMAGE_API_KEY/BCE_IMAGE_SECRET_KEY` 环境变量读取百度云凭据;若未配置则自动跳过执行。
### 存储 Stage ### 存储 Stage
| Stage | 职责 | Optional Mode | 执行条件 | | Stage | 职责 | Optional Mode | 执行条件 |
@@ -214,6 +217,12 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
| UploadStage | 上传图片到存储服务 | FORCE_ON | 总是执行 | | UploadStage | 上传图片到存储服务 | FORCE_ON | 总是执行 |
| SourcePhotoUpdateStage | 更新源图片记录 | UNSUPPORT | 总是执行 | | SourcePhotoUpdateStage | 更新源图片记录 | UNSUPPORT | 总是执行 |
### 辅助 Stage
| Stage | 职责 | Optional Mode | 执行条件 |
|-------|------|---------------|---------|
| NoOpStage | 调试/占位 Stage,不做任何处理 | UNSUPPORT | 仅用于保持流程完整或调试 |
## 典型管线示例 ## 典型管线示例
### 1. 打印照片处理管线 ### 1. 打印照片处理管线
@@ -553,7 +562,7 @@ A: 返回 `StageResult.failed()`,管线会立即终止。如果希望继续执
### Q: 临时文件什么时候被清理? ### Q: 临时文件什么时候被清理?
A: 由 `CleanupStage` 负责,通常放在管线最后。也可以手动调用 `context.cleanup()` A: 由 `CleanupStage` 负责,通常放在管线最后。也可以手动调用 `context.cleanup()`此外,`Pipeline` 在 finally 中还会再调用一次 `context.cleanup()`,保证失败或异常时也能释放所有临时文件。
### Q: 如何获取最终处理结果? ### Q: 如何获取最终处理结果?

View File

@@ -65,7 +65,7 @@ public abstract class AbstractPipelineStage<C extends PhotoProcessContext> imple
/** /**
* 模板方法:执行Stage后的清理工作 * 模板方法:执行Stage后的清理工作
*/ */
protected void afterExecute(C context, StageResult result) { protected void afterExecute(C context, StageResult<C> result) {
if (result.isSuccess()) { if (result.isSuccess()) {
log.debug("[{}] 执行成功: {}", getName(), result.getMessage()); log.debug("[{}] 执行成功: {}", getName(), result.getMessage());
} else if (result.isSkipped()) { } else if (result.isSkipped()) {
@@ -80,15 +80,15 @@ public abstract class AbstractPipelineStage<C extends PhotoProcessContext> imple
/** /**
* 子类实现具体的处理逻辑 * 子类实现具体的处理逻辑
*/ */
protected abstract StageResult doExecute(C context); protected abstract StageResult<C> doExecute(C context);
/** /**
* 最终执行方法(带钩子) * 最终执行方法(带钩子)
*/ */
@Override @Override
public final StageResult execute(C context) { public final StageResult<C> execute(C context) {
beforeExecute(context); beforeExecute(context);
StageResult result = doExecute(context); StageResult<C> result = doExecute(context);
afterExecute(context, result); afterExecute(context, result);
return result; return result;
} }

View File

@@ -92,6 +92,7 @@ public class PhotoProcessContext {
private File processedFile; private File processedFile;
private boolean isLandscape = true; private boolean isLandscape = true;
private boolean rotationApplied = false; private boolean rotationApplied = false;
private boolean cleaned = false;
/** /**
* 图像需要旋转的角度(用于后续Stage使用) * 图像需要旋转的角度(用于后续Stage使用)
@@ -227,7 +228,11 @@ public class PhotoProcessContext {
* 清理所有临时文件 * 清理所有临时文件
*/ */
public void cleanup() { public void cleanup() {
if (cleaned) {
return;
}
tempFileManager.cleanup(); tempFileManager.cleanup();
cleaned = true;
} }
// ==================== Builder ==================== // ==================== Builder ====================

View File

@@ -52,7 +52,7 @@ public class Pipeline<C extends PhotoProcessContext> {
} }
long stageStartTime = System.currentTimeMillis(); long stageStartTime = System.currentTimeMillis();
StageResult result = stage.execute(context); StageResult<C> result = stage.execute(context);
long stageDuration = System.currentTimeMillis() - stageStartTime; long stageDuration = System.currentTimeMillis() - stageStartTime;
executedCount++; executedCount++;
@@ -60,12 +60,12 @@ public class Pipeline<C extends PhotoProcessContext> {
// 动态添加后续Stage // 动态添加后续Stage
if (result.getNextStages() != null && !result.getNextStages().isEmpty()) { if (result.getNextStages() != null && !result.getNextStages().isEmpty()) {
List<PipelineStage<?>> nextStages = result.getNextStages(); List<PipelineStage<C>> nextStages = result.getNextStages();
log.info("[{}] Stage {} 动态添加了 {} 个后续Stage", name, stageName, nextStages.size()); log.info("[{}] Stage {} 动态添加了 {} 个后续Stage", name, stageName, nextStages.size());
for (int j = 0; j < nextStages.size(); j++) { for (int j = 0; j < nextStages.size(); j++) {
PipelineStage<?> nextStage = nextStages.get(j); PipelineStage<C> nextStage = nextStages.get(j);
stages.add(i + 1 + j, (PipelineStage<C>) nextStage); stages.add(i + 1 + j, nextStage);
log.debug("[{}] - 插入Stage: {} 到位置 {}", name, nextStage.getName(), i + 1 + j); log.debug("[{}] - 插入Stage: {} 到位置 {}", name, nextStage.getName(), i + 1 + j);
} }
} }
@@ -84,10 +84,12 @@ public class Pipeline<C extends PhotoProcessContext> {
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] 管线执行异常", name, e); log.error("[{}] 管线执行异常", name, e);
throw new PipelineException("管线执行失败: " + e.getMessage(), e); throw new PipelineException("管线执行失败: " + e.getMessage(), e);
} finally {
safeCleanup(context);
} }
} }
private void logStageResult(String stageName, StageResult result, long duration) { private void logStageResult(String stageName, StageResult<C> result, long duration) {
String statusIcon = switch (result.getStatus()) { String statusIcon = switch (result.getStatus()) {
case SUCCESS -> ""; case SUCCESS -> "";
case SKIPPED -> ""; case SKIPPED -> "";
@@ -114,4 +116,15 @@ public class Pipeline<C extends PhotoProcessContext> {
public List<String> getStageNames() { public List<String> getStageNames() {
return stages.stream().map(PipelineStage::getName).toList(); return stages.stream().map(PipelineStage::getName).toList();
} }
private void safeCleanup(C context) {
if (context == null) {
return;
}
try {
context.cleanup();
} catch (Exception cleanupError) {
log.warn("[{}] 管线清理失败", name, cleanupError);
}
}
} }

View File

@@ -30,7 +30,7 @@ public interface PipelineStage<C extends PhotoProcessContext> {
* @param context 管线上下文 * @param context 管线上下文
* @return 执行结果 * @return 执行结果
*/ */
StageResult execute(C context); StageResult<C> execute(C context);
/** /**
* 获取Stage的执行优先级(用于排序) * 获取Stage的执行优先级(用于排序)

View File

@@ -11,7 +11,7 @@ import java.util.List;
* Stage执行结果 * Stage执行结果
*/ */
@Getter @Getter
public class StageResult { public class StageResult<C extends PhotoProcessContext> {
public enum Status { public enum Status {
SUCCESS, // 执行成功 SUCCESS, // 执行成功
@@ -23,9 +23,9 @@ public class StageResult {
private final Status status; private final Status status;
private final String message; private final String message;
private final Throwable exception; private final Throwable exception;
private final List<PipelineStage<?>> nextStages; private final List<PipelineStage<C>> nextStages;
private StageResult(Status status, String message, Throwable exception, List<PipelineStage<?>> nextStages) { private StageResult(Status status, String message, Throwable exception, List<PipelineStage<C>> nextStages) {
this.status = status; this.status = status;
this.message = message; this.message = message;
this.exception = exception; this.exception = exception;
@@ -34,47 +34,47 @@ public class StageResult {
: Collections.emptyList(); : Collections.emptyList();
} }
public static StageResult success() { public static <C extends PhotoProcessContext> StageResult<C> success() {
return new StageResult(Status.SUCCESS, null, null, null); return new StageResult<>(Status.SUCCESS, null, null, null);
} }
public static StageResult success(String message) { public static <C extends PhotoProcessContext> StageResult<C> success(String message) {
return new StageResult(Status.SUCCESS, message, null, null); return new StageResult<>(Status.SUCCESS, message, null, null);
} }
/** /**
* 成功执行并动态添加后续Stage * 成功执行并动态添加后续Stage
*/ */
@SafeVarargs @SafeVarargs
public static StageResult successWithNext(String message, PipelineStage<?>... stages) { public static <C extends PhotoProcessContext> StageResult<C> successWithNext(String message, PipelineStage<C>... stages) {
return new StageResult(Status.SUCCESS, message, null, Arrays.asList(stages)); return new StageResult<>(Status.SUCCESS, message, null, Arrays.asList(stages));
} }
/** /**
* 成功执行并动态添加后续Stage列表 * 成功执行并动态添加后续Stage列表
*/ */
public static StageResult successWithNext(String message, List<PipelineStage<?>> stages) { public static <C extends PhotoProcessContext> StageResult<C> successWithNext(String message, List<PipelineStage<C>> stages) {
return new StageResult(Status.SUCCESS, message, null, stages); return new StageResult<>(Status.SUCCESS, message, null, stages);
} }
public static StageResult skipped() { public static <C extends PhotoProcessContext> StageResult<C> skipped() {
return new StageResult(Status.SKIPPED, "条件不满足,跳过执行", null, null); return new StageResult<>(Status.SKIPPED, "条件不满足,跳过执行", null, null);
} }
public static StageResult skipped(String reason) { public static <C extends PhotoProcessContext> StageResult<C> skipped(String reason) {
return new StageResult(Status.SKIPPED, reason, null, null); return new StageResult<>(Status.SKIPPED, reason, null, null);
} }
public static StageResult failed(String message) { public static <C extends PhotoProcessContext> StageResult<C> failed(String message) {
return new StageResult(Status.FAILED, message, null, null); return new StageResult<>(Status.FAILED, message, null, null);
} }
public static StageResult failed(String message, Throwable exception) { public static <C extends PhotoProcessContext> StageResult<C> failed(String message, Throwable exception) {
return new StageResult(Status.FAILED, message, exception, null); return new StageResult<>(Status.FAILED, message, exception, null);
} }
public static StageResult degraded(String message) { public static <C extends PhotoProcessContext> StageResult<C> degraded(String message) {
return new StageResult(Status.DEGRADED, message, null, null); return new StageResult<>(Status.DEGRADED, message, null, null);
} }
public boolean isSuccess() { public boolean isSuccess() {

View File

@@ -31,7 +31,7 @@ public class CleanupStage extends AbstractPipelineStage<PhotoProcessContext> {
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
try { try {
int fileCount = context.getTempFileManager().getTempFileCount(); int fileCount = context.getTempFileManager().getTempFileCount();
context.cleanup(); context.cleanup();

View File

@@ -35,7 +35,7 @@ public class ConditionalRotateStage extends AbstractPipelineStage<PhotoProcessCo
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
try { try {
File currentFile = context.getCurrentFile(); File currentFile = context.getCurrentFile();
if (currentFile == null || !currentFile.exists()) { if (currentFile == null || !currentFile.exists()) {

View File

@@ -31,7 +31,7 @@ public class DownloadStage extends AbstractPipelineStage<PhotoProcessContext> {
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
String url = context.getOriginalUrl(); String url = context.getOriginalUrl();
if (StringUtils.isBlank(url)) { if (StringUtils.isBlank(url)) {
return StageResult.failed("原图URL为空"); return StageResult.failed("原图URL为空");

View File

@@ -38,11 +38,7 @@ public class ImageEnhanceStage extends AbstractPipelineStage<PhotoProcessContext
* 构造函数 - 使用默认配置 * 构造函数 - 使用默认配置
*/ */
public ImageEnhanceStage() { public ImageEnhanceStage() {
this.enhancerConfig = new BceEnhancerConfig(); this(buildConfigFromEnvironment());
this.enhancerConfig.setAppId("119554288");
this.enhancerConfig.setApiKey("OX6QoijgKio3eVtA0PiUVf7f");
this.enhancerConfig.setSecretKey("dYatXReVriPeiktTjUblhfubpcmYfuMk");
this.enhancerConfig.setQps(1.0f);
} }
/** /**
@@ -66,7 +62,7 @@ public class ImageEnhanceStage extends AbstractPipelineStage<PhotoProcessContext
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
// 检查配置是否完整 // 检查配置是否完整
if (!isConfigValid()) { if (!isConfigValid()) {
log.warn("图像增强配置不完整,跳过增强处理。请在ImageEnhanceStage中配置百度云API凭证"); log.warn("图像增强配置不完整,跳过增强处理。请在ImageEnhanceStage中配置百度云API凭证");
@@ -151,6 +147,15 @@ public class ImageEnhanceStage extends AbstractPipelineStage<PhotoProcessContext
return true; return true;
} }
private static BceEnhancerConfig buildConfigFromEnvironment() {
BceEnhancerConfig config = new BceEnhancerConfig();
config.setAppId(System.getenv("BCE_IMAGE_APP_ID"));
config.setApiKey(System.getenv("BCE_IMAGE_API_KEY"));
config.setSecretKey(System.getenv("BCE_IMAGE_SECRET_KEY"));
config.setQps(1.0f);
return config;
}
/** /**
* 保存MultipartFile到本地文件 * 保存MultipartFile到本地文件
*/ */

View File

@@ -36,7 +36,7 @@ public class ImageOrientationStage extends AbstractPipelineStage<PhotoProcessCon
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
try { try {
File currentFile = context.getCurrentFile(); File currentFile = context.getCurrentFile();
if (currentFile == null || !currentFile.exists()) { if (currentFile == null || !currentFile.exists()) {

View File

@@ -50,7 +50,7 @@ public class ImageQualityCheckStage extends AbstractPipelineStage<PhotoProcessCo
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
File currentFile = context.getCurrentFile(); File currentFile = context.getCurrentFile();
if (currentFile == null || !currentFile.exists()) { if (currentFile == null || !currentFile.exists()) {
return StageResult.failed("当前文件不存在"); return StageResult.failed("当前文件不存在");
@@ -107,7 +107,7 @@ public class ImageQualityCheckStage extends AbstractPipelineStage<PhotoProcessCo
* 示例:也可以根据其他条件动态添加不同的Stage * 示例:也可以根据其他条件动态添加不同的Stage
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
private StageResult checkAndAddMultipleStages(PhotoProcessContext context) { private StageResult<PhotoProcessContext> checkAndAddMultipleStages(PhotoProcessContext context) {
// 示例:根据不同条件添加多个Stage // 示例:根据不同条件添加多个Stage
// 假设检测到需要多个增强操作 // 假设检测到需要多个增强操作

View File

@@ -6,7 +6,7 @@ import com.ycwl.basic.image.pipeline.core.StageResult;
public class NoOpStage extends AbstractPipelineStage<PhotoProcessContext> { public class NoOpStage extends AbstractPipelineStage<PhotoProcessContext> {
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
return StageResult.skipped("无操作"); return StageResult.skipped("无操作");
} }

View File

@@ -39,7 +39,7 @@ public class PuzzleBorderStage extends AbstractPipelineStage<PhotoProcessContext
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
try { try {
File currentFile = context.getCurrentFile(); File currentFile = context.getCurrentFile();
if (currentFile == null || !currentFile.exists()) { if (currentFile == null || !currentFile.exists()) {

View File

@@ -36,7 +36,7 @@ public class RestoreOrientationStage extends AbstractPipelineStage<PhotoProcessC
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
try { try {
File currentFile = context.getCurrentFile(); File currentFile = context.getCurrentFile();
if (currentFile == null || !currentFile.exists()) { if (currentFile == null || !currentFile.exists()) {

View File

@@ -21,7 +21,7 @@ import java.io.File;
@Slf4j @Slf4j
@StageConfig( @StageConfig(
stageId = "source_photo_update", stageId = "source_photo_update",
optionalMode = StageOptionalMode.FORCE_ON, optionalMode = StageOptionalMode.UNSUPPORT,
description = "源图片上传和数据库更新", description = "源图片上传和数据库更新",
defaultEnabled = true defaultEnabled = true
) )
@@ -47,7 +47,7 @@ public class SourcePhotoUpdateStage extends AbstractPipelineStage<PhotoProcessCo
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
File fileToUpload = context.getCurrentFile(); File fileToUpload = context.getCurrentFile();
if (fileToUpload == null || !fileToUpload.exists()) { if (fileToUpload == null || !fileToUpload.exists()) {
return StageResult.failed("没有可上传的文件"); return StageResult.failed("没有可上传的文件");

View File

@@ -33,7 +33,7 @@ public class UploadStage extends AbstractPipelineStage<PhotoProcessContext> {
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
File fileToUpload = context.getCurrentFile(); File fileToUpload = context.getCurrentFile();
if (fileToUpload == null || !fileToUpload.exists()) { if (fileToUpload == null || !fileToUpload.exists()) {
return StageResult.failed("没有可上传的文件"); return StageResult.failed("没有可上传的文件");

View File

@@ -55,7 +55,7 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
if (config == null || config.getWatermarkType() == null) { if (config == null || config.getWatermarkType() == null) {
log.info("未配置水印类型,跳过水印处理"); log.info("未配置水印类型,跳过水印处理");
return StageResult.skipped("未配置水印"); return StageResult.skipped("未配置水印");
@@ -75,7 +75,7 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
} }
try { try {
StageResult result = applyWatermark(context, type); StageResult<PhotoProcessContext> result = applyWatermark(context, type);
if (i > 0) { if (i > 0) {
String degradeMsg = String.format("降级: %s -> %s", String degradeMsg = String.format("降级: %s -> %s",
@@ -115,7 +115,7 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
/** /**
* 应用水印 * 应用水印
*/ */
private StageResult applyWatermark(PhotoProcessContext context, ImageWatermarkOperatorEnum type) private StageResult<PhotoProcessContext> applyWatermark(PhotoProcessContext context, ImageWatermarkOperatorEnum type)
throws Exception { throws Exception {
File currentFile = context.getCurrentFile(); File currentFile = context.getCurrentFile();

View File

@@ -34,13 +34,21 @@ public class TempFileManager {
} }
private Path initTempDirectory() { private Path initTempDirectory() {
Path baseDir = Path.of(System.getProperty("java.io.tmpdir"), "photo_process", processId);
try { try {
Path systemTempDir = Files.createTempDirectory("photo_process_"); Files.createDirectories(baseDir);
log.debug("创建临时目录: {}", systemTempDir); log.debug("创建隔离临时目录: {}", baseDir);
return systemTempDir; return baseDir;
} catch (IOException e) { } catch (IOException e) {
log.warn("无法创建系统临时目录,使用当前目录", e); log.warn("无法创建隔离临时目录,尝试使用系统临时目录", e);
return Path.of("."); try {
Path fallback = Files.createTempDirectory("photo_process_" + processId + "_");
log.debug("创建备用临时目录: {}", fallback);
return fallback;
} catch (IOException ex) {
log.warn("无法创建系统临时目录,使用当前目录", ex);
return Path.of(".");
}
} }
} }

View File

@@ -113,7 +113,7 @@ class PipelineBuilderTest {
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
return StageResult.success(); return StageResult.success();
} }
} }

View File

@@ -121,6 +121,20 @@ class PipelineTest {
assertTrue(names.contains("MockStage-stage2")); assertTrue(names.contains("MockStage-stage2"));
} }
@Test
void testPipelineFailureStillCleansTempFiles() {
Pipeline<PhotoProcessContext> pipeline = new PipelineBuilder<PhotoProcessContext>("CleanupGuarantee")
.addStage(new TempFileFailStage())
.build();
PhotoProcessContext context = createTestContext();
boolean success = pipeline.execute(context);
assertFalse(success);
assertEquals(0, context.getTempFileManager().getTempFileCount());
}
/** /**
* 创建测试用的 Context * 创建测试用的 Context
*/ */
@@ -137,9 +151,9 @@ class PipelineTest {
*/ */
private static class MockStage extends AbstractPipelineStage<PhotoProcessContext> { private static class MockStage extends AbstractPipelineStage<PhotoProcessContext> {
private final String id; private final String id;
private final StageResult result; private final StageResult<PhotoProcessContext> result;
MockStage(String id, StageResult result) { MockStage(String id, StageResult<PhotoProcessContext> result) {
this.id = id; this.id = id;
this.result = result; this.result = result;
} }
@@ -150,7 +164,7 @@ class PipelineTest {
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
return result; return result;
} }
} }
@@ -178,8 +192,22 @@ class PipelineTest {
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
return StageResult.success(); return StageResult.success();
} }
} }
private static class TempFileFailStage extends AbstractPipelineStage<PhotoProcessContext> {
@Override
public String getName() {
return "TempFileFailStage";
}
@Override
protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
context.getTempFileManager().createTempFile("fail", ".tmp");
return StageResult.failed("force failure");
}
}
} }

View File

@@ -13,7 +13,7 @@ class StageResultTest {
@Test @Test
void testSuccess() { void testSuccess() {
StageResult result = StageResult.success(); StageResult<PhotoProcessContext> result = StageResult.success();
assertEquals(StageResult.Status.SUCCESS, result.getStatus()); assertEquals(StageResult.Status.SUCCESS, result.getStatus());
assertTrue(result.isSuccess()); assertTrue(result.isSuccess());
@@ -25,7 +25,7 @@ class StageResultTest {
@Test @Test
void testSuccessWithMessage() { void testSuccessWithMessage() {
StageResult result = StageResult.success("处理完成"); StageResult<PhotoProcessContext> result = StageResult.success("处理完成");
assertTrue(result.isSuccess()); assertTrue(result.isSuccess());
assertEquals("处理完成", result.getMessage()); assertEquals("处理完成", result.getMessage());
@@ -36,7 +36,7 @@ class StageResultTest {
MockStage nextStage1 = new MockStage("next1"); MockStage nextStage1 = new MockStage("next1");
MockStage nextStage2 = new MockStage("next2"); MockStage nextStage2 = new MockStage("next2");
StageResult result = StageResult.successWithNext("添加后续Stage", nextStage1, nextStage2); StageResult<PhotoProcessContext> result = StageResult.successWithNext("添加后续Stage", nextStage1, nextStage2);
assertTrue(result.isSuccess()); assertTrue(result.isSuccess());
assertEquals("添加后续Stage", result.getMessage()); assertEquals("添加后续Stage", result.getMessage());
@@ -47,19 +47,19 @@ class StageResultTest {
@Test @Test
void testSuccessWithNextStagesList() { void testSuccessWithNextStagesList() {
List<PipelineStage<?>> stages = List.of( List<PipelineStage<PhotoProcessContext>> stages = List.of(
new MockStage("next1"), new MockStage("next1"),
new MockStage("next2") new MockStage("next2")
); );
StageResult result = StageResult.successWithNext("添加列表", stages); StageResult<PhotoProcessContext> result = StageResult.successWithNext("添加列表", stages);
assertEquals(2, result.getNextStages().size()); assertEquals(2, result.getNextStages().size());
} }
@Test @Test
void testSkipped() { void testSkipped() {
StageResult result = StageResult.skipped(); StageResult<PhotoProcessContext> result = StageResult.skipped();
assertEquals(StageResult.Status.SKIPPED, result.getStatus()); assertEquals(StageResult.Status.SKIPPED, result.getStatus());
assertTrue(result.isSkipped()); assertTrue(result.isSkipped());
@@ -69,7 +69,7 @@ class StageResultTest {
@Test @Test
void testSkippedWithReason() { void testSkippedWithReason() {
StageResult result = StageResult.skipped("图片类型不匹配"); StageResult<PhotoProcessContext> result = StageResult.skipped("图片类型不匹配");
assertTrue(result.isSkipped()); assertTrue(result.isSkipped());
assertEquals("图片类型不匹配", result.getMessage()); assertEquals("图片类型不匹配", result.getMessage());
@@ -77,7 +77,7 @@ class StageResultTest {
@Test @Test
void testFailed() { void testFailed() {
StageResult result = StageResult.failed("下载失败"); StageResult<PhotoProcessContext> result = StageResult.failed("下载失败");
assertEquals(StageResult.Status.FAILED, result.getStatus()); assertEquals(StageResult.Status.FAILED, result.getStatus());
assertTrue(result.isFailed()); assertTrue(result.isFailed());
@@ -88,7 +88,7 @@ class StageResultTest {
@Test @Test
void testFailedWithException() { void testFailedWithException() {
Exception exception = new RuntimeException("网络错误"); Exception exception = new RuntimeException("网络错误");
StageResult result = StageResult.failed("下载失败", exception); StageResult<PhotoProcessContext> result = StageResult.failed("下载失败", exception);
assertTrue(result.isFailed()); assertTrue(result.isFailed());
assertEquals("下载失败", result.getMessage()); assertEquals("下载失败", result.getMessage());
@@ -97,7 +97,7 @@ class StageResultTest {
@Test @Test
void testDegraded() { void testDegraded() {
StageResult result = StageResult.degraded("使用备用方案"); StageResult<PhotoProcessContext> result = StageResult.degraded("使用备用方案");
assertEquals(StageResult.Status.DEGRADED, result.getStatus()); assertEquals(StageResult.Status.DEGRADED, result.getStatus());
assertTrue(result.isDegraded()); assertTrue(result.isDegraded());
@@ -128,9 +128,9 @@ class StageResultTest {
@Test @Test
void testNextStagesImmutable() { void testNextStagesImmutable() {
MockStage stage1 = new MockStage("stage1"); MockStage stage1 = new MockStage("stage1");
StageResult result = StageResult.successWithNext("test", stage1); StageResult<PhotoProcessContext> result = StageResult.successWithNext("test", stage1);
List<PipelineStage<?>> nextStages = result.getNextStages(); List<PipelineStage<PhotoProcessContext>> nextStages = result.getNextStages();
assertThrows(UnsupportedOperationException.class, () -> { assertThrows(UnsupportedOperationException.class, () -> {
nextStages.add(new MockStage("stage2")); nextStages.add(new MockStage("stage2"));
@@ -153,7 +153,7 @@ class StageResultTest {
} }
@Override @Override
protected StageResult doExecute(PhotoProcessContext context) { protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
return StageResult.success(); return StageResult.success();
} }
} }

View File

@@ -33,10 +33,10 @@ class ImageEnhanceStageTest {
BceEnhancerConfig config = stage.getEnhancerConfig(); BceEnhancerConfig config = stage.getEnhancerConfig();
assertNotNull(config); assertNotNull(config);
assertEquals("119554288", config.getAppId());
assertEquals("OX6QoijgKio3eVtA0PiUVf7f", config.getApiKey());
assertEquals("dYatXReVriPeiktTjUblhfubpcmYfuMk", config.getSecretKey());
assertEquals(1.0f, config.getQps()); assertEquals(1.0f, config.getQps());
assertEquals(System.getenv("BCE_IMAGE_APP_ID"), config.getAppId());
assertEquals(System.getenv("BCE_IMAGE_API_KEY"), config.getApiKey());
assertEquals(System.getenv("BCE_IMAGE_SECRET_KEY"), config.getSecretKey());
} }
@Test @Test