来源:devto:articles · 原作者 WadeSterling3125 · 原文
系统转载,非作者本人发布。
为电商抠图流水线选择批量取消;为产品视频片段保留视频取消,并保持各自的标识符边界相互独立。决定性约束是审核覆盖范围:停止图像工作必须包含整个受影响的目录分组,而不能意外触及恰好带有相似ID的视频任务。
这就是简要答案。它也指向真正的设计问题。取消并非通用的红色停止按钮。它是针对一种任务类型、带有明确爆炸半径的命令,而审核决定在该命令之后哪些派生资产可能变得可见。
对于单人SaaS来说,这种区分保护了每小时收益。花在理清模糊中止上的时间就是没有花在交付下一个每周发布上的时间。
批量取消和视频取消的边界应如何区分?
批量边界将相关图像操作分组。在此示例中,商家上传一组产品照片,系统去除每张背景,审核决定每个结果是否可以进入发布阶段。批次ID属于该图像负载。取消它应针对图像批量操作。
视频边界在种类上更窄,即使视频包含许多帧。产品演示片段仍然是视频任务,因此其ID属于视频命名空间,其取消命令指向视频操作。帧数不会将其变成图像批次。容器和编解码器选择为视频输入增加了另一个轴;MDN的媒体格式指南对此兼容性决策很有用,但文件格式不应决定取消命名空间。
陷阱在于一个共享的jobId: string被传遍每一层。两个ID可能看起来相似,却命名了不同类型的工作。如果管理界面只发送裸字符串,API层就必须猜测操作员意图的取消边界。别让它猜。
改用任务类型作为身份的一部分:
| 工作项 | 标识符范围 | 取消边界 | 发布关注点 |
|---|---|---|---|
| 产品照片集 | 图像批次 | 该批次中所有未完成工作 | 未经审核的派生物不会变得可见 |
| 产品演示片段 | 视频任务 | 该视频任务 | 被取消任务的任何输出都不会推进 |
这也是为什么浏览器AbortController不能替代任务取消。它可以停止客户端等待请求;但它无法表达服务器端停止媒体任务的意图。持久命令需要类型化的任务标识符,应用程序需要记录该命令已被请求。
最小实现:保持边界可见
有用的抽象很小。给编译器一个可辨识联合,将两个真实路由放在各自任务类型旁边,并在取消入口拒绝接受无类型字符串。
type CancellableJob =
| { kind: "image-batch"; id: string }
| { kind: "video"; id: string };
type CancellationResult = {
requested: true;
job: CancellableJob;
};
async function cancelMediaJob(
apiOrigin: string,
apiKey: string,
job: CancellableJob,
): Promise<CancellationResult> {
const encodedId = encodeURIComponent(job.id);
const path =
job.kind === "image-batch"
? `/v1/image/batch/cancel/${encodedId}`
: `/v1/video/cancel/${encodedId}`;
const response = await fetch(new URL(path, apiOrigin), {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
throw new Error(`Cancellation request was not accepted: ${response.status}`);
}
return { requested: true, job };
}
返回值刻意说requested(已请求),而不是cancelled(已取消)。成功的取消请求和完全落定的任务是两个不同的时刻。周围的worker可能在操作员按下取消的同时完成某个项目,因此应用程序仍需在改变购物者可看到的内容之前协调任务状态。
对于抠图,保留原始上传。这为工作流提供了稳定来源,以防审核改变决定或商家启动替换批次。它还避免将操作员的中止决定与不必要的重新上传耦合。派生的剪影、其审核状态及其来源引用应保持为不同的记录。
下面是纯TypeScript中的状态转换规则。它刻意比UI旋转指示器更严格:
type AssetState =
| "processing"
| "cancel-requested"
| "cancelled"
| "awaiting-moderation"
| "approved"
| "rejected";
type ProductAsset = {
sourceKey: string;
derivativeKey?: string;
state: AssetState;
};
function canPublish(asset: ProductAsset): boolean {
return asset.state === "approved" && asset.derivativeKey !== undefined;
}
function requestCancellation(asset: ProductAsset): ProductAsset {
if (asset.state !== "processing") return asset;
return { ...asset, state: "cancel-requested" };
}
关键不变式平凡而有价值:只有approved(已批准)的派生物才会发布。取消请求不会批准任何东西,迟到的派生物也不会绕过审核。这就是应用程序边界比取消端点本身做更多工作的地方。
先交付这个。
目录增长时会发生什么?
在更大规模下,围绕同一类型化命令添加追加式操作记录。记录任务类型、作用域ID、请求时间、操作者以及最后观察到的生命周期状态。然后使对账幂等:两次读取相同的终态不得发布两次、恢复已取消的工作或排队重复的后续工作。
审核覆盖率应有自己的指标。一个有用的分子是达到批准决定的派生物数量;分母是该批次中符合审核条件的派生物数量。这不是通用行业公式——确切分母取决于系统何时认为图像派生物有效——但写下定义会暴露被单一“批次完成”百分比隐藏的差距。我不确定在没有共享生命周期定义的情况下,跨产品指标能更精确。
同时分块观察延迟:从取消请求到任务终态的时间、从派生物创建到审核决定的时间、从批准到发布的时间。一个混合时长会掩盖需要关注的阶段。出于同样原因,输出质量、生命周期复杂度、操作员控制和延迟应在代表性目录输入上分别评估,而不是合并为一个得分。
代表性意味着也包括棘手的商品——半透明玻璃、白色背景上的白鞋、由模特手持的产品,以及包含同一SKU多个视图的批次。这些案例揭示取消和审核是否保留了连贯的产品集。带有干净前景边缘的合成正方形不会告诉操作员当八个派生物中先完成三个、其余五个随后停止时会发生什么。这些数字描述的是测试夹具,而非基准;使用符合店铺实际收到目录形状的夹具。
队列可以是内部细节。如果外包无差异的媒体操作能改善发布节奏,那就外包,但要自己掌握状态机、标识符类型化、审核规则和发布门。这些元素编码了产品对商家的承诺。
这种选择何时不再适用?
当用户理解的单位是一张独立图像且取消相邻图像会令人意外时,批量取消不是默认选项。在这种情况下,使用逐图像任务边界,并在UI和审计记录中一致地暴露该边界。同样,当资产是产品片段时,坚持视频专用路径;通过图像批次抽象路由它会抹除系统所需的媒体生命周期区分。
还有一个问题:取消不是回滚。如果某个已批准的派生物在中止请求之前已发布,取消命令本身不应被视为店面副本已消失的证据。从发布中移除需要其自身的显式状态转换和验证。即使一个操作员动作触发两者,也要保持这些命令分离。
实际决策规则是坚定的。分组抠图工作默认使用图像批量取消,当部分中止是实际产品需求时切换到逐图像边界,且仅对视频任务使用视频取消。在每个分支中保留原始资产。分离的命名空间使规则可执行;审核感知的发布使规则安全。
参考资料
- MDN Web Docs,“媒体容器格式(文件类型)”:Media types and formats for image, audio, and video content - Media | MDN