# 项目：教学插图任务与素材库

原文: https://yishto.com/learn/project-labs/lab-illustration-job-queue/

作者: [Yishto](https://yishto.com/about/)

把插图请求设计为有归属和预算的任务，协调队列、配额账本、R2 素材与人工审核，检查取消和重复执行。

首次发布: 2026-11-10
内容更新: 2026-11-10

教学插图项目把一次生图请求变成有预算、归属和审核记录的任务。用户提交插图说明，系统接受任务、执行生成、保存候选素材，再由用户决定采用哪一张。生成成功不等于素材已经批准，也不等于所有失败都可以无成本重试。

第一版只支持一种插图用途与一个模型适配器。例子使用合成任务和内部配额，不代表真实定价、支付或供应商退款规则。先跑通请求到素材的路径，再增加购买配额、批量任务和公开展示。

## 明确任务与素材的不同身份

任务保存用户、输入版本、目标格式、预算与状态。尝试记录某次执行的供应商引用、耗时和错误。素材记录对象位置、所属用户、来源任务、尺寸、内容类型和审核状态；一个任务可能产生多个候选素材。

界面可以有等待、运行、待审核、批准、失败和取消状态。状态必须对应真实行为：收到供应商结果后还要验证和存储，确认素材可读取后才能提供下载。取消请求不代表供应商已经停止或没有发生费用。

上传说明或参考图时限制大小、格式与用途。用户应有权使用输入素材；模型结果还需要检查实际内容是否适合教学。公开素材库读取明确批准的版本，不默认把所有候选图公开。

## 配额账本围绕逻辑任务计账

接受请求时，在数据库事务中创建唯一逻辑任务并预留内部配额。相同用户重试同一操作键，返回已有任务；相同键对应不同输入，应返回冲突。任务键、请求载荷摘要和用户范围一起定义去重契约。

账本区分预留、结算与回补，记录原因和关联任务。失败是否回补配额是产品政策，不是供应商是否退款的证明。网络超时后供应商可能已经完成，所以不能只看到异常就反复提交新的付费生成请求。

成功结算与失败回补应由条件状态迁移决定，只执行一次。用不同服务分别读余额再写余额会发生竞争，需要事务、唯一约束及对应并发控制。前端显示余额不能成为接受任务的权威依据。

## 为队列和执行器设置边界

队列将请求接受与生成完成分开。提交响应返回任务身份和可查询状态，页面不必保持连接直到图像完成。事务中的 outbox 可以保存待发送的任务意图，发送器仍需处理重试，消费者仍需去重。

执行器限制并发、输入大小、等待时间和尝试次数。临时错误可以按预算重试，永久输入错误进入失败分支。不同供应商可能直接返回图像或返回异步任务引用，适配器应把它们转为一致的业务状态，而不是假设 API 都有同样的流程。

工作租约可以帮助协调执行者，但过期后旧执行者仍可能返回结果。接收结果时检查当前尝试与状态，防止旧结果覆盖新状态。外部生成能否去重取决于供应商接口，不能由内部队列承诺外部效果恰好一次。

## 协调 R2 对象与数据库记录

使用受控对象键保存生成结果，数据库记录归属和状态。对象上传与数据库提交不是同一个事务；可以先建立暂存状态、上传并验证，再确认素材记录，定期对账和清理未被确认的对象。

R2 的对象读写一致性不等于数据库记录自动同步，也不等于 CDN 不会保留旧缓存。不要把写入对象成功直接当成用户有权下载。下载入口先检查素材归属、审核与访问政策。

预签名 URL 为特定对象和操作提供限时访问，持有者在条件有效时可使用它。签名链接不执行应用的成员检查，也不撤回已经下载的内容。生成链接前完成授权，避免把凭据、链接和私人插图说明写入公共日志。

## 离线检查配额预留与回补

下面约定接受一个任务预留一单位内部配额，失败时回补一次。任务已存在不能再次预留，已失败不能再次回补。

```python
balance = 3
jobs = {}
def submit(job_id):
    global balance
    if job_id in jobs or balance < 1:
        return False
    balance -= 1
    jobs[job_id] = "reserved"
    return True
def fail(job_id):
    global balance
    if jobs.get(job_id) != "reserved":
        return False
    jobs[job_id] = "failed"
    balance += 1
    return True
assert submit("J-1") and not submit("J-1")
assert balance == 2
assert fail("J-1") and not fail("J-1")
assert balance == 3
print(balance, jobs)
```

输出为 `3 {'J-1': 'failed'}`。这是单进程合成状态模型，没有真实账本、支付、队列、R2 或并发。它也没有成功结算函数，完整服务还需实现成功、取消、未知外部结果及对账分支。

## 从固定结果实现完整界面

先用固定素材和可控错误实现提交、任务查询与归属检查，再连接存储。页面展示任务阶段、失败原因和重试含义，允许查看已完成的候选结果。图片加载失败与生成任务失败是不同问题。

接入供应商前，验证素材确认与配额迁移。然后增加模型适配器、超时和拒绝情况，记录每次尝试。最后接上审核、下载与取消，真实平台检查分别记录，不能用上面的 Python 输出代替它们。

需要购买配额时，先在支付测试环境验证签名事件、重复通知和权限更新。浏览器支付成功页面不直接增加余额，测试记录也不代表已经发生实际收入。

素材确认时检查文件签名和实际可解码格式，不能只相信供应商返回的扩展名。限制像素尺寸与解码资源，生成缩略图后仍保留原素材的归属和任务引用。公开列表与私有下载分别授权，缓存策略按可见性制定；用户将素材转为私有后，已有公开缓存的处理需要单独核实。素材删除与任务记录留存也分别定义，删除对象不应让配额账本失去解释依据。

## 练习：在上传与确认之间失败

模拟相同任务键重复提交、余额不足、生成超时、对象上传后数据库确认失败和重复回补。验收余额迁移只有一次，未确认对象不被列为可用素材，归属错误的请求不能下载。

再模拟取消后旧执行者返回成功。结果不能无条件覆盖取消状态；系统应按预先政策核实或保存为隔离候选，并处理内部配额与外部成本。为每种分支写出用户反馈、实际状态和对账动作。

## 资料来源与延伸阅读

- [Image generation APIs](https://developers.openai.com/api/docs/guides/image-generation)
- [Object storage concepts](https://developers.cloudflare.com/r2/)
- [Synchronized queue classes](https://docs.python.org/3/library/queue.html)
- [Test payments and integrations](https://docs.stripe.com/testing)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [请求太多时：任务队列、缓存与背压](https://yishto.com/learn/service-data/jobs-caches-and-backpressure/)
- [图像、语音与视频进入应用后要补什么](https://yishto.com/learn/ai-foundations/multimodal-image-and-speech/)
