← 完整项目实验室 · 第 4 篇

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

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

学完这一篇,你能做到

设计任务与配额状态,并验证失败回补、素材归属和重复请求。

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

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

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

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

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

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

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

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

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

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

为队列和执行器设置边界

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

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

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

协调 R2 对象与数据库记录

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

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

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

离线检查配额预留与回补

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

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 输出代替它们。

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

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

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

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

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

资料来源与延伸阅读

资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。