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

View File

@@ -65,7 +65,7 @@ public abstract class AbstractPipelineStage<C extends PhotoProcessContext> imple
/**
* 模板方法:执行Stage后的清理工作
*/
protected void afterExecute(C context, StageResult result) {
protected void afterExecute(C context, StageResult<C> result) {
if (result.isSuccess()) {
log.debug("[{}] 执行成功: {}", getName(), result.getMessage());
} 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
public final StageResult execute(C context) {
public final StageResult<C> execute(C context) {
beforeExecute(context);
StageResult result = doExecute(context);
StageResult<C> result = doExecute(context);
afterExecute(context, result);
return result;
}

View File

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

View File

@@ -52,7 +52,7 @@ public class Pipeline<C extends PhotoProcessContext> {
}
long stageStartTime = System.currentTimeMillis();
StageResult result = stage.execute(context);
StageResult<C> result = stage.execute(context);
long stageDuration = System.currentTimeMillis() - stageStartTime;
executedCount++;
@@ -60,12 +60,12 @@ public class Pipeline<C extends PhotoProcessContext> {
// 动态添加后续Stage
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());
for (int j = 0; j < nextStages.size(); j++) {
PipelineStage<?> nextStage = nextStages.get(j);
stages.add(i + 1 + j, (PipelineStage<C>) nextStage);
PipelineStage<C> nextStage = nextStages.get(j);
stages.add(i + 1 + j, nextStage);
log.debug("[{}] - 插入Stage: {} 到位置 {}", name, nextStage.getName(), i + 1 + j);
}
}
@@ -84,10 +84,12 @@ public class Pipeline<C extends PhotoProcessContext> {
} catch (Exception e) {
log.error("[{}] 管线执行异常", name, 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()) {
case SUCCESS -> "";
case SKIPPED -> "";
@@ -114,4 +116,15 @@ public class Pipeline<C extends PhotoProcessContext> {
public List<String> getStageNames() {
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 管线上下文
* @return 执行结果
*/
StageResult execute(C context);
StageResult<C> execute(C context);
/**
* 获取Stage的执行优先级(用于排序)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -55,7 +55,7 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
}
@Override
protected StageResult doExecute(PhotoProcessContext context) {
protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
if (config == null || config.getWatermarkType() == null) {
log.info("未配置水印类型,跳过水印处理");
return StageResult.skipped("未配置水印");
@@ -75,7 +75,7 @@ public class WatermarkStage extends AbstractPipelineStage<PhotoProcessContext> {
}
try {
StageResult result = applyWatermark(context, type);
StageResult<PhotoProcessContext> result = applyWatermark(context, type);
if (i > 0) {
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 {
File currentFile = context.getCurrentFile();

View File

@@ -34,13 +34,21 @@ public class TempFileManager {
}
private Path initTempDirectory() {
Path baseDir = Path.of(System.getProperty("java.io.tmpdir"), "photo_process", processId);
try {
Path systemTempDir = Files.createTempDirectory("photo_process_");
log.debug("创建临时目录: {}", systemTempDir);
return systemTempDir;
Files.createDirectories(baseDir);
log.debug("创建隔离临时目录: {}", baseDir);
return baseDir;
} catch (IOException e) {
log.warn("无法创建系统临时目录,使用当前目录", e);
return Path.of(".");
log.warn("无法创建隔离临时目录,尝试使用系统临时目录", e);
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
protected StageResult doExecute(PhotoProcessContext context) {
protected StageResult<PhotoProcessContext> doExecute(PhotoProcessContext context) {
return StageResult.success();
}
}

View File

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

View File

@@ -33,10 +33,10 @@ class ImageEnhanceStageTest {
BceEnhancerConfig config = stage.getEnhancerConfig();
assertNotNull(config);
assertEquals("119554288", config.getAppId());
assertEquals("OX6QoijgKio3eVtA0PiUVf7f", config.getApiKey());
assertEquals("dYatXReVriPeiktTjUblhfubpcmYfuMk", config.getSecretKey());
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