
AI 或媒体任务显示“已完成”以后用户真正关心的往往才刚开始下载的是哪一版结果文件是否真的能打开预览和下载是否对应同一个内容结果被覆盖后还能不能查回旧版如果交付环节只保存一个output.mp4路径任务再稳定最后一步仍然容易失去可信度。本文使用脱敏任务PT-20261009-001输入资产和配置快照已经固定Worker 生成一个临时结果经过媒体探测和业务校验后提交为正式产物。示例中的文件、摘要和任务号均为教学数据不包含真实素材、内部存储地址、账户信息或生产发布流程。环境边界Java 17、Spring Boot 风格服务层、Python 3.11 Worker、MySQL 8.x。对象存储、文件系统或 NAS 都可以承载产物重点在于版本、访问控制和验收证据的统一语义。目录为什么文件生成了还不等于交付完成固定案例一份结果需要哪些交付证据产物版本正式结果不能被原地覆盖预览与下载便利入口不能绕过验收和权限验收记录怎样证明结果可用且来自这次任务服务层实现临时产物原子提交为正式版本预期输出与自动测试SQL 验证如何发现孤儿结果和版本混乱异常边界与上线验收小结和延伸阅读一、为什么文件生成了还不等于交付完成Worker 在临时目录生成一个非空文件只能说明某一步有输出。它还可能没有音频流、媒体时长为零、写入中断或只是一次失败尝试遗留的旧文件。更常见的问题是新一次任务直接覆盖output.mp4用户下载到的内容与页面显示的任务、配置和完成时间不再对应。交付完成应同时满足四件事产物通过当前任务的验收它被登记为正式版本当前用户有权获取它任务记录、产物摘要和验收结论能够相互核对。下载链接只是这四件事成立后的访问方式不应成为系统的事实来源。图1结果文件、任务状态、验收记录和下载权限必须一起成立才能称为可交付。二、固定案例一份结果需要哪些交付证据任务PT-20261009-001成功后交付result-v1.mp4同时生成一份delivery-summary.json。用户不需要看到完整 Worker 日志但系统要保存能回答“这是什么、从哪来、能不能用”的关键事实证据教学示例用途产物版本v1区分重跑或人工确认后产生的新结果对象键deliveries/PT-20261009-001/v1/result.mp4指向不可随意覆盖的正式对象内容摘要和大小sha25691ab...、82MB识别下载内容并发现传输或覆盖问题媒体摘要H.264、1080P、90.04 秒、有音频证明基本可播放条件验收结论ACCEPTED说明已通过自动检查是否还需人工抽检来源关联输入摘要、配置版本、尝试号允许回查这份结果如何产生结果摘要只保存必要元数据不保存原视频内容、模型内部输出或敏感日志。对于文本、图片、报表等其他工具表中的媒体摘要可替换为页数、像素尺寸、行数、格式校验等领域事实交付逻辑仍然相同。图2交付证据让下载的文件能够被解释、回查和重新验证。三、产物版本正式结果不能被原地覆盖用户点击“重新处理”不一定意味着旧结果错误也可能只是选择了新的预设、修复了输入或采用了新版本配置。因此正式产物应使用版本号或不可变标识保存而不是覆盖同一对象键。当前可下载版本可以由任务记录显式指向例如current_delivery_id历史版本保留、归档或清理则由独立的留存策略决定。CREATETABLEtool_delivery(idBIGINTPRIMARYKEYAUTO_INCREMENT,job_idBIGINTNOTNULL,version_noINTNOTNULL,object_keyVARCHAR(255)NOTNULL,sha256CHAR(64)NOTNULL,byte_sizeBIGINTNOTNULL,delivery_statusVARCHAR(24)NOTNULL,acceptance_json JSONNOTNULL,source_attempt_noINTNOTNULL,created_atDATETIMENOTNULL,UNIQUEKEYuk_job_delivery_version(job_id,version_no),UNIQUEKEYuk_delivery_object_key(object_key),CONSTRAINTck_delivery_statusCHECK(delivery_statusIN(ACCEPTED,REVOKED,EXPIRED)),CONSTRAINTfk_delivery_jobFOREIGNKEY(job_id)REFERENCEStool_job(id));version_no不等于文件名后缀的装饰它是任务范围内的唯一序号。ACCEPTED也不表示永久可下载权限撤销、留存到期或发现验收缺陷时可以将版本标为REVOKED或EXPIRED但不应把它悄悄替换成另一份文件。图3版本让“当前可用”和“历史曾交付”同时可解释避免一个路径承载所有含义。四、预览与下载便利入口不能绕过验收和权限预览适合帮助用户快速确认内容下载适合取得完整交付文件但二者不能直接暴露存储对象的永久地址。服务端应根据任务归属、产物状态和有效期生成受控访问响应无权限、非ACCEPTED、已撤销或已过期的版本都不应生成预览或下载地址。操作前置条件返回内容不应发生的事列出结果有任务查看权限版本、摘要、验收状态把内部对象键返回给页面预览当前版本已验收且格式可预览短时预览地址或代理流预览未完成临时文件下载当前用户有下载权限且版本有效短时下载地址或受控流用固定公开链接绕过权限查看历史有审计权限或本人任务权限历史版本与状态将已撤销版本伪装为当前结果预览成功并不能替代验收浏览器能播放一个片段不代表文件时长、音频流、完整性都符合约定。相反验收通过也不保证所有浏览器都能内嵌预览因此预览失败应有“下载后检查”的明确降级路径而不是把任务改成失败。五、验收记录怎样证明结果可用且来自这次任务验收应先定义规则再记录结论。媒体案例至少检查输出存在、大小非零、可读取流信息、时长落在合理区间、视频流存在且要求保留音频时音频流存在。规则版本也应保存以便日后说明“当时按什么标准通过”。{ruleVersion:delivery-media-v1,checkedAt:2026-10-09T10:42:18,checks:{fileNonEmpty:true,videoStreamPresent:true,audioStreamPresent:true,durationSeconds:90.04,durationWithinTolerance:true},decision:ACCEPTED}自动验收解决的是基础完整性不替代人工抽检。字幕是否遮挡、画面是否符合业务预期、报表数据是否符合业务口径都可能需要抽样复核。应把人工结论作为追加的验收记录而不是修改原来的自动检查结果。图4验收不是一句“成功”而是一组可回查的检查结果和清晰的适用边界。六、服务层实现临时产物原子提交为正式版本Worker 完成处理后先把临时对象交给验收服务通过后服务层在短事务内创建交付版本、写验收摘要并更新任务当前交付引用。复制或移动大文件本身不应持有数据库事务先让存储层完成可核对的正式对象写入再用事务登记其事实若登记失败则将该对象标记为待清理不能直接把它暴露给用户。TransactionalpublicDeliveryViewcommitAcceptedResult(LongjobId,TemporaryArtifacttemp,AcceptanceResultacceptance){acceptance.requireAccepted();ToolJobjobjobRepository.requireRunning(jobId);intnextVersiondeliveryRepository.nextVersionForUpdate(jobId);StoredObjectstoredobjectStore.promote(temp,deliveries/job.getJobNo()/vnextVersion);ToolDeliverydeliveryToolDelivery.accepted(jobId,nextVersion,stored.key(),stored.sha256(),stored.size(),acceptance.toJson(),job.currentAttemptNo());deliveryRepository.insert(delivery);jobRepository.markReadyWithDelivery(jobId,delivery.getId());returnDeliveryView.from(delivery);}未验收不提升、未登记不开放、任务终态与当前交付引用一起更新。若状态已被取消或被其他尝试接管正式对象应进入待清理清单不能留下可猜测、可下载的孤儿文件。七、预期输出与自动测试用户查询成功任务时应得到交付视图而不是原始路径{jobNo:PT-20261009-001,status:READY,delivery:{version:1,status:ACCEPTED,previewAvailable:true,downloadAvailable:true,sha256:91ab...}}测试要确认未验收产物无法成为当前版本以及迟到的尝试无法覆盖已交付结果TestvoidrejectedArtifactMustNotCreateDelivery(){assertThatThrownBy(()-deliveryService.commitAcceptedResult(jobId,tempArtifact(),AcceptanceResult.rejected())).hasMessage(DELIVERY_NOT_ACCEPTED);assertThat(deliveryRepository.countByJob(jobId)).isZero();}TestvoidacceptedDeliveryCannotBeOverwrittenByLateAttempt(){readyJobWithDelivery(1);assertThatThrownBy(()-deliveryService.commitAcceptedResult(jobId,lateTempArtifact(),AcceptanceResult.accepted())).hasMessage(JOB_NOT_RUNNING);}还应测试授权无任务权限的用户无法获得预览或下载地址已撤销版本不在常规结果列表出现预览地址过期后需要重新经服务端授权。八、SQL 验证如何发现孤儿结果和版本混乱-- 预期结果0 行。READY 任务必须指向一份已验收交付版本。SELECTj.job_noFROMtool_job jLEFTJOINtool_delivery dONd.idj.current_delivery_idANDd.delivery_statusACCEPTEDWHEREj.statusREADYANDd.idISNULL;-- 预期结果0 组。同一任务的版本号不能重复。SELECTjob_id,version_no,COUNT(*)AStotalFROMtool_deliveryGROUPBYjob_id,version_noHAVINGCOUNT(*)1;-- 预期结果0 行。未验收版本不能被当前任务引用。SELECTj.job_no,d.delivery_statusFROMtool_job jJOINtool_delivery dONd.idj.current_delivery_idWHEREd.delivery_statusACCEPTED;对象存储中“有对象、数据库没有登记”的情况要用存储清单与数据库交叉比对后进入待清理队列。清理也应延迟执行给事务恢复和人工排查留出时间。九、异常边界与上线验收场景系统应做什么不应做什么临时文件生成先验收再提升为正式对象直接开放下载再次处理同一任务创建新版本或新任务并明确关系原地覆盖旧版本预览不兼容给出下载后的检查路径把任务标为失败权限撤销或留存过期停止新访问并保留审计状态用公开对象地址继续暴露文件登记事务失败对已写对象标记待清理并告警让孤儿对象成为下载入口上线验收应准备正常样本、故意缺音频样本和一次重跑正常样本通过后可预览下载缺音频样本不能成为正式版本重跑产生v2而v1仍可按留存策略查询。最后用无权限账户访问入口确认不能获取对象键或文件内容。图5结果交付不是文件搬运而是让每一份可下载内容都能证明自己从哪里来、为何可用、谁可以获取。十、小结和延伸阅读可靠交付把“生成文件”变成“可解释结果”正式版本不可静默覆盖预览和下载受权限与状态约束验收结论有规则和时间证据任务能回查输入、配置和执行尝试。这样用户下载到的不只是一个文件而是一份有来源、有版本、有边界的交付物。下一篇将讨论参数很多时如何通过预设、表单校验、危险参数控制和配置审计让普通用户能正确提交任务而不把工具变成任意命令入口。参考资料Spring Framework事务管理参考MySQL 8.4CREATE TABLE 与约束FFmpegffprobe 文档