图像生成服务
大约 3 分钟
第十九章:图像生成服务
19.1 API设计:同步还是异步
图像生成通常具备三个特点:
- 慢:生成可能需要几秒到几十秒
- 贵:成本比纯文本更高
- 大:结果需要存储与分发(对象存储 + CDN)
因此建议默认使用异步 API:
- POST
/images/jobs:提交任务,返回jobId - GET
/images/jobs/{id}:查询状态/结果 URL
19.2 图像处理:请求、结果、失败
建议的任务状态:
- PENDING → RUNNING → SUCCEEDED
- 失败进入 FAILED,并保留错误原因(脱敏)
19.3 存储管理:对象存储 + CDN(思路)
生产环境建议:
- 把生成结果落到对象存储(OSS/S3/MinIO)
- 用 CDN 分发 URL
- 结果表存:prompt 摘要、模型、尺寸、耗时、文件 hash、存储 key
本教程为了可复制运行,示例用内存存储与“模拟 URL”表示流程。
19.4 最小实现(可复制运行)
19.4.1 Job 模型
package com.example.images.domain;
public enum ImageJobStatus {
PENDING,
RUNNING,
SUCCEEDED,
FAILED
}package com.example.images.domain;
import java.time.Instant;
import java.util.Optional;
public record ImageJob(
String id,
String prompt,
ImageJobStatus status,
Optional<String> imageUrl,
Optional<String> error,
Instant createdAt
) {
}19.4.2 仓库与 Runner
package com.example.images.repo;
import com.example.images.domain.ImageJob;
import com.example.images.domain.ImageJobStatus;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Repository;
@Repository
public class InMemoryImageJobRepository {
private final ConcurrentHashMap<String, ImageJob> store = new ConcurrentHashMap<>();
public ImageJob create(String prompt) {
String id = UUID.randomUUID().toString();
ImageJob job = new ImageJob(id, prompt, ImageJobStatus.PENDING, Optional.empty(), Optional.empty(), Instant.now());
store.put(id, job);
return job;
}
public Optional<ImageJob> find(String id) {
return Optional.ofNullable(store.get(id));
}
public void update(ImageJob job) {
store.put(job.id(), job);
}
}package com.example.images.service;
import com.example.images.domain.ImageJob;
import com.example.images.domain.ImageJobStatus;
import com.example.images.repo.InMemoryImageJobRepository;
import java.util.Optional;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class ImageJobRunner {
private final InMemoryImageJobRepository repo;
public ImageJobRunner(InMemoryImageJobRepository repo) {
this.repo = repo;
}
@Async
public void run(String jobId) {
ImageJob job = repo.find(jobId).orElseThrow();
repo.update(new ImageJob(job.id(), job.prompt(), ImageJobStatus.RUNNING, Optional.empty(), Optional.empty(), job.createdAt()));
try {
String url = "https://example.local/images/" + job.id() + ".png";
repo.update(new ImageJob(job.id(), job.prompt(), ImageJobStatus.SUCCEEDED, Optional.of(url), Optional.empty(), job.createdAt()));
} catch (Exception e) {
repo.update(new ImageJob(job.id(), job.prompt(), ImageJobStatus.FAILED, Optional.empty(), Optional.of(e.getMessage()), job.createdAt()));
}
}
}真实对接时,你会把 url 换成供应商返回的图片地址,或先下载到对象存储再生成 CDN URL。
19.4.3 API:提交与查询
package com.example.images.api;
import com.example.images.domain.ImageJob;
import com.example.images.repo.InMemoryImageJobRepository;
import com.example.images.service.ImageJobRunner;
import java.util.Map;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ImageJobController {
private final InMemoryImageJobRepository repo;
private final ImageJobRunner runner;
public ImageJobController(InMemoryImageJobRepository repo, ImageJobRunner runner) {
this.repo = repo;
this.runner = runner;
}
@PostMapping("/images/jobs")
public ApiResponse<Map<String, String>> create(@RequestBody CreateImageJobRequest request) {
ImageJob job = repo.create(request.prompt());
runner.run(job.id());
return ApiResponse.ok(Map.of("jobId", job.id()));
}
@GetMapping("/images/jobs/{id}")
public ApiResponse<ImageJob> get(@PathVariable String id) {
return repo.find(id)
.map(ApiResponse::ok)
.orElseGet(() -> ApiResponse.fail("job not found"));
}
public record CreateImageJobRequest(String prompt) {
}
}19.5 性能优化:批处理、限流、重试
- 对图像生成做单独的配额与限流(比文本更贵)
- 失败重试要更谨慎:可能产生重复成本
- 对同 prompt 的结果可做缓存(注意版权与合规)
