fix(#1714): 素材上传去重防重 + complete幂等传递 + 轮询收敛(前端P0) #1717
Reference in New Issue
Block a user
Delete Branch "feat/1714-upload-dedup"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
背景
Issue #1714:同一文件被反复走完整上传链路(prepare→OSS直传→complete),13 个素材建了 69 条记录、转码队列堆 39 个任务。根因诊断见项目文件
asset_duplicate_upload_rootcause_20260905.md。本 PR 为前端 P0 部分(后端 complete 幂等兜底 + worker 转码孤儿修复由后端 PR 处理)。
改动
1. 入队按文件指纹去重(核心)
src/api/assets/uploadDedup.ts纯函数模块:makeFileFingerprint:文件名+大小+lastModified 稳定指纹findDuplicateInQueue:队列查重(error 态可重新激活、在途/已完成均拦截)makeClientUploadId:上传幂等 token(一次逻辑上传一个,重试复用)computeFileHash:SHA-256 内容哈希(≤256MB 全量;大文件抽样头尾各 8MB + 文件大小,避免 2GB 上传前卡死;不支持 crypto.subtle 时降级为空串,FileReader 回退)useAssetUpload.enqueueUploads:同一文件已在 preparing/uploading/ingesting/done 时跳过并轻提示;此前 error 的同指纹项重新激活复用clientUploadId2. complete 携带 file_hash + 幂等 token
prepareDirectUpload/completeDirectUpload请求体新增file_hash、client_upload_id(后端 schema 已支持file_hash,额外字段 Pydantic 忽略不报错,待后端幂等 PR 消费 token)uploadAssetDirect(配音/封面/克隆等非队列链路)自动补算 hash 与 token,去重闸门对所有上传链路生效3. 超时放宽 + complete 失败不盲目重传
4. 入口防重复提交
transferActive)禁用「上传素材」按钮、拦截文件选择/拖拽并提示;重试按钮仅 error 状态可点,tooltip 区分「安全重试(只确认,不重新上传)」5. 轮询收敛
测试
src/test/api/uploadDedup.test.ts(9 个):指纹一致性/区分度、队列查重各状态、幂等 token 唯一性、hash 内容区分与 64 位 hex 格式useAssetUpload.test.tsx新增 2 个集成测试:同文件多次选择不重复入队(1 项/1 次 prepare,完成后重复选择仍不新增,不同文件正常入队)、complete 超时后重试只重发 complete(prepare 1 次、transfer 1 次、complete 2 次)前端职责边界
.gitea/workflows/与 Dockerfile🚀 预览环境已部署
【阻塞级判定】
📊 审查概览
🔴 阻塞级问题(必须修复)
sizeView.setUint32(4, file.size >>> 0)。file.size >>> 0会将数值转换为 32 位无符号整数。对于超过 4GB (2^32 bytes) 的文件,这会导致大小被截断(例如 5GB 变成 1GB),使得哈希输入中的大小字段不正确。这增加了哈希碰撞的概率,导致去重逻辑失效。代码注释中提到“JS 安全整数远小于 2^53,高 4 字节为 0”也是错误的,因为 2^53 远大于 2^32,高 4 字节并不总是 0。sizeView.setBigUint64(0, BigInt(file.size), false)(如果环境支持),或者手动计算高低位:💡 改进建议(不阻塞合并)
[apps/web/src/pages/assets/hooks/useAssetUpload.ts: runUpload & enqueueUploads] 页面刷新后幂等性丢失
handlesRef和itemsRef来实现“complete 阶段失败后的安全重试”。如果用户在上传过程中(特别是 complete 阶段超时后)刷新了页面,handlesRef会被清空。此时用户点击重试,existingHandle为undefined,代码会回退到全量重传(prepare + transfer + complete)。虽然后端有file_hash兜底不会产生重复数据,但会浪费 OSS 流量和时间。建议考虑将clientUploadId以及必要的上下文(如assetId)持久化到sessionStorage,以便在刷新后能恢复状态,继续只发送 complete 请求。[apps/web/src/api/assets/uploadDedup.ts: computeFileHash] 内存峰值风险
computeFileHash会创建一个约 16MB 的Uint8Array(8MB 头 + 8MB 尾 + 8 字节大小)。虽然单次问题不大,但如果用户批量选择多个大文件,且并发计算哈希(取决于调用方的并发控制),可能会导致内存峰值飙升。建议在调用方(如useAssetUpload)中限制哈希计算的并发数,或者观察实际内存表现,必要时改为流式处理(虽然 Web Crypto API 流式支持较复杂)。✅ 良好实践
file_hash和client_upload_id实现端到端去重和幂等,设计思路清晰,能有效解决重复上传和超时重试导致的脏数据问题。useAssetUploadhook 中对complete阶段失败的特殊处理(保留 handle,仅重试 complete)非常符合业务场景,避免了不必要的文件重传。transferActive状态防止重复提交,以及stalled状态提示长时间处理,用户体验考虑周全。✅ 格式检查通过 | ❌ 逻辑审查需修改 | ⚠️ 建议关注性能
🤖 由 AI 代码审查机器人自动生成 | 2026-09-05 08:25:43 | 模型:
CI全绿,自动审批通过。
CI全绿,自动审批通过。
🗑️ 预览环境已清理
PR #1717 已关闭或合并,对应的预览环境已被清理。