feat(#1714): prepare_direct_upload 去重 + 预建 PROCESSING asset 占位 #1730

Merged
auto-approve-bot merged 2 commits from feature/prepare-dedup-1714 into develop 2026-09-06 12:31:38 +08:00
Owner

背景

当前 direct 两段式上传的 prepare 只签名 OSS 表单、不做去重(去重只在 complete),导致重复文件每次都完整上传一遍 OSS 才在 complete 阶段被跳过,浪费带宽。

改动

schema(apps/api/app/schemas/upload.py

  • DirectUploadPrepareRequest 增加 client_upload_id(客户端幂等 token)
  • DirectUploadPrepareResponse 扩展 3 个向后兼容字段:duplicated: bool=Falseskip_transfer: bool=Falseasset_id: str=""

prepare_direct_upload(apps/api/app/api/routes/upload.py

  • 生成 OSS 签名前,若 file_hashclient_upload_id 非空,先调 _find_duplicate_asset 去重(复用现有 3 级查找:client_upload_id → file_hash → 同名兜底)
  • 命中:直接返回 duplicated=True, skip_transfer=True, asset_id=existing.id,upload_url/fields 为空——前端识别后跳过 OSS 直传,直接走 complete
  • 未命中:正常签名 OSS,并立即调 _create_pending_asset 预建一条 PROCESSING 占位 asset(写入 file_hash/client_upload_id/storage_key/filename/size),占住 file_hash 闸门,响应带 asset_id;预建失败 try/except 降级不阻塞签名

_create_pending_asset 改 find-or-create

  • 先按 client_upload_id、再按 file_hash 查现有记录;命中则复用并补齐缺失字段(触发 update),未命中才新建——避免 pre-create + complete 重复建两条

_find_duplicate_asset 兜底规则细化

  • READY/ERROR 稳定素材:总命中(避免重复创建)
  • PROCESSING/UPLOADING 占位:仅当占位 hash 与请求 hash 一致、或占位无 hash(旧客户端 complete 占位)时命中;占位有 hash 且与请求不同 → 跳过(同文件名不同内容的新上传不能误杀)

测试

12 个新单测(tests/unit/test_prepare_dedup_1714.py):

  • 首次上传无 hash:duplicated=false、无 asset_id
  • 首次上传带 hash:duplicated=false + asset_id 非空(PROCESSING 占位落库)
  • 二次同 hash:duplicated=true, skip_transfer=true
  • 同 client_upload_id 重试:直接跳过
  • file_hash 空:走老逻辑
  • 同名兜底不命中 PROCESSING 占位 / 命中 READY 稳定记录
  • PROCESSING 占位 hash 不同跳过 / 相同命中
  • find-or-create 复用现有记录 / 无匹配新建 / update 异常吞掉
  • 预建失败降级:签名仍正常返回

全量 14400 passed(5 个已知 TTS 沙箱失败与本次无关),diff coverage 85%+。

前端配合

后端 prepare 响应已带 skip_transfer + asset_id,前端识别后跳过 OSS 直传、直接调 complete(前端工程师跟进)。

## 背景 当前 direct 两段式上传的 prepare 只签名 OSS 表单、不做去重(去重只在 complete),导致重复文件每次都完整上传一遍 OSS 才在 complete 阶段被跳过,浪费带宽。 ## 改动 ### schema(`apps/api/app/schemas/upload.py`) - `DirectUploadPrepareRequest` 增加 `client_upload_id`(客户端幂等 token) - `DirectUploadPrepareResponse` 扩展 3 个向后兼容字段:`duplicated: bool=False`、`skip_transfer: bool=False`、`asset_id: str=""` ### prepare_direct_upload(`apps/api/app/api/routes/upload.py`) - 生成 OSS 签名前,若 `file_hash` 或 `client_upload_id` 非空,先调 `_find_duplicate_asset` 去重(复用现有 3 级查找:client_upload_id → file_hash → 同名兜底) - **命中**:直接返回 `duplicated=True, skip_transfer=True, asset_id=existing.id`,upload_url/fields 为空——前端识别后跳过 OSS 直传,直接走 complete - **未命中**:正常签名 OSS,并立即调 `_create_pending_asset` 预建一条 PROCESSING 占位 asset(写入 file_hash/client_upload_id/storage_key/filename/size),占住 file_hash 闸门,响应带 `asset_id`;预建失败 try/except 降级不阻塞签名 ### _create_pending_asset 改 find-or-create - 先按 client_upload_id、再按 file_hash 查现有记录;命中则复用并补齐缺失字段(触发 update),未命中才新建——避免 pre-create + complete 重复建两条 ### _find_duplicate_asset 兜底规则细化 - READY/ERROR 稳定素材:总命中(避免重复创建) - PROCESSING/UPLOADING 占位:仅当占位 hash 与请求 hash 一致、或占位无 hash(旧客户端 complete 占位)时命中;占位有 hash 且与请求不同 → 跳过(同文件名不同内容的新上传不能误杀) ## 测试 12 个新单测(`tests/unit/test_prepare_dedup_1714.py`): - 首次上传无 hash:duplicated=false、无 asset_id - 首次上传带 hash:duplicated=false + asset_id 非空(PROCESSING 占位落库) - 二次同 hash:duplicated=true, skip_transfer=true - 同 client_upload_id 重试:直接跳过 - file_hash 空:走老逻辑 - 同名兜底不命中 PROCESSING 占位 / 命中 READY 稳定记录 - PROCESSING 占位 hash 不同跳过 / 相同命中 - find-or-create 复用现有记录 / 无匹配新建 / update 异常吞掉 - 预建失败降级:签名仍正常返回 全量 14400 passed(5 个已知 TTS 沙箱失败与本次无关),diff coverage 85%+。 ## 前端配合 后端 prepare 响应已带 `skip_transfer` + `asset_id`,前端识别后跳过 OSS 直传、直接调 complete(前端工程师跟进)。
xiaoxia added 2 commits 2026-09-06 12:23:58 +08:00
- schema: DirectUploadPrepareRequest 加 client_upload_id;
  DirectUploadPrepareResponse 加 duplicated/skip_transfer/asset_id(向后兼容)
- _create_pending_asset: find-or-create 模式,先按 client_upload_id/file_hash
  查现有记录,命中则复用并补齐字段,未命中才新建
- prepare_direct_upload: 签名 OSS 前按 file_hash/client_upload_id 去重,
  命中直接返回 duplicated=true+skip_transfer=true;未命中正常签名并预建
  PROCESSING 占位 asset(占位失败不阻塞签名)
- _find_duplicate_asset 兜底规则细化:READY/ERROR 总命中;PROCESSING/
  UPLOADING 仅当 hash 一致或占位无 hash 时命中,避免同文件名不同内容冲突
- 8 个新单测覆盖:首次带 hash、重复命中、client_upload_id 重试、
  文件名兜底不命中 PROCESSING、兜底命中 READY、find-or-create 复用
test(#1714): 补 prepare 去重边界分支——预建失败降级、update异常吞掉、PROCESSING hash不同跳过/相同命中
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 30s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 30s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m35s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m48s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m53s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 2m20s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m5s
AI Code Review / AI Code Review (pull_request) Successful in 4m11s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 5m28s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 6m49s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 4s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 4m31s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 9s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 17s
9088be360f

🚀 预览环境已部署

项目 详情
PR号 #1730
预览链接 https://pr-1730.preview.xiaoxiajianji.com
API环境 staging

💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。

🔄 每次提交新代码后预览环境会自动更新。

🗑️ PR 关闭或合并后,预览环境会自动清理。

🚀 **预览环境已部署** | 项目 | 详情 | |------|------| | PR号 | #1730 | | 预览链接 | [https://pr-1730.preview.xiaoxiajianji.com](https://pr-1730.preview.xiaoxiajianji.com) | | API环境 | staging | > 💡 预览环境使用 staging API 数据,请勿在预览环境中操作重要数据。 > > 🔄 每次提交新代码后预览环境会自动更新。 > > 🗑️ PR 关闭或合并后,预览环境会自动清理。
Collaborator

代码审查结果 - PR #1730

⚠️ 问题(2个需要修改)

  1. apps/api/app/api/routes/upload.py 第193行:兜底去重逻辑存在误判风险。当 existing_hash 为空(旧占位)而 file_hash 不为空时,代码进入 else 分支判定为命中。这会导致不同内容(同名但 hash 不同)的新上传被错误关联到旧的空 hash 占位记录上,造成数据混乱或去重失效。
  2. apps/api/app/api/routes/upload.py 第248行_create_pending_asset 中字段补齐失败时异常被静默吞掉(pass)。如果补齐 file_hash 等 ID 关键字段失败,会导致内存对象与数据库记录不一致。后续流程(如 complete)若依赖 DB 中的这些字段进行校验,将导致业务逻辑错误。

💡 建议(1个可选)

  1. apps/api/app/api/routes/upload.py 第393行:日志记录建议增加堆栈信息。当前 logger.warning("预建 asset 占位失败,降级走 old flow: %s", error) 仅打印异常对象,建议使用 exc_info=Trueexc_info=error 以便排查预建失败的根本原因。

格式检查通过 | 逻辑审查需修改 | 性能良好


🤖 由 AI 代码审查机器人自动生成 | 2026-09-06 04:28:09 | 模型:

## 代码审查结果 - PR #1730 ### ⚠️ 问题(2个需要修改) 1. **apps/api/app/api/routes/upload.py 第193行**:兜底去重逻辑存在误判风险。当 `existing_hash` 为空(旧占位)而 `file_hash` 不为空时,代码进入 `else` 分支判定为命中。这会导致不同内容(同名但 hash 不同)的新上传被错误关联到旧的空 hash 占位记录上,造成数据混乱或去重失效。 2. **apps/api/app/api/routes/upload.py 第248行**:`_create_pending_asset` 中字段补齐失败时异常被静默吞掉(`pass`)。如果补齐 `file_hash` 等 ID 关键字段失败,会导致内存对象与数据库记录不一致。后续流程(如 complete)若依赖 DB 中的这些字段进行校验,将导致业务逻辑错误。 ### 💡 建议(1个可选) 1. **apps/api/app/api/routes/upload.py 第393行**:日志记录建议增加堆栈信息。当前 `logger.warning("预建 asset 占位失败,降级走 old flow: %s", error)` 仅打印异常对象,建议使用 `exc_info=True` 或 `exc_info=error` 以便排查预建失败的根本原因。 --- ✅ 格式检查通过 | ❌ 逻辑审查需修改 | ✅ 性能良好 --- <sub>🤖 由 AI 代码审查机器人自动生成 | 2026-09-06 04:28:09 | 模型: </sub> <!-- AI_CODE_REVIEW_AUTO_COMMENT -->
auto-approve-bot merged commit 70dde8cbfb into develop 2026-09-06 12:31:38 +08:00
auto-approve-bot deleted branch feature/prepare-dedup-1714 2026-09-06 12:31:39 +08:00

🗑️ 预览环境已清理

PR #1730 已关闭或合并,对应的预览环境已被清理。

如有需要,可以重新打开 PR 来重新生成预览环境。

🗑️ **预览环境已清理** PR #1730 已关闭或合并,对应的预览环境已被清理。 > 如有需要,可以重新打开 PR 来重新生成预览环境。
Sign in to join this conversation.