docs(plan): upload duplicate pre-check at enqueue moment — incremental plan
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
# 上传查重前置到「点上传」时刻 实施计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 用户点上传(选完照片确认入队)的那一刻,弹窗提示与服务端历史一致的重复项(跨设备/重装/改名也能查到),而不是等任务在上传中心以 PHOTO_DUPLICATE 失败才暴露。
|
||||||
|
|
||||||
|
**Architecture:** 服务端加一个只读批量查重预检端点(判定谓词与 initUpload 查重**逐字一致**:同 albumId+uploaderId+bytes+非 deleted,保证弹窗即真相);App 选图入队前把「本机 TaskStore 历史 + 服务器历史」合并进**既有**去重弹窗(弹窗/force 透传/409 兜底全部不动)。连拍快门路径不弹窗(不打断拍摄,靠 409 兜底 + 上传中心可见)。
|
||||||
|
|
||||||
|
**背景(为什么):** 现有层 1(本机 checkDuplicates)只查本设备历史——卸载重装/换机/他传的照片查不到 → 不弹窗 → 入队上传 → 服务端 409 落在上传中心(用户 2026-09-28 实测反馈:debug→release 换签名重装后本地历史清空,全落上传中心)。
|
||||||
|
|
||||||
|
**设计要点:**
|
||||||
|
- 端点:`POST /v1/uploads/duplicates/check`,operationId `checkUploadDuplicates`;请求 `{albumId, items:[{filename, bytes}]}`(1..500 项,客户端按 500 分块);响应 `{duplicates:[{index, bytes, uploadedAt}]}`(index 指向请求数组,命中子集)。
|
||||||
|
- 服务端权限链与 initUpload 相同(canViewAlbum 404 兜底 + canUploadToAlbum);查询 `photos.findMany(bytes in [去重字节集], visibility != deleted)` 内存映射回 index。
|
||||||
|
- App:`ApiClient.checkUploadDuplicates` + enqueue.dart 统一入口 `checkDuplicatesUnified`(本机命中 ∪ 服务器命中;服务器调用失败**静默降级**本机-only,不阻塞入队);两页 `_pickFromGallery` 换用统一入口;弹窗/skip/force 逻辑零改动。
|
||||||
|
- 服务器预检按 **bytes** 精确映射回 path(修正现有按 filename 映射的误伤面——同名不同图不再被误标,顺带收紧)。
|
||||||
|
- 契约红线:新端点纯增量,不改既有字段(新契约字段全部 optional 原则)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 契约 + 服务端查重预检端点(TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `packages/contracts/openapi.yaml`(+paths checkUploadDuplicates + schemas)
|
||||||
|
- Regen: `packages/contracts/src/generated/openapi.d.ts`(`pnpm --filter contracts gen`)
|
||||||
|
- Modify: `services/api/src/uploads/dto.ts`(+DuplicateCheckRequestDto)
|
||||||
|
- Modify: `services/api/src/uploads/uploads.controller.ts`(+POST duplicates/check)
|
||||||
|
- Modify: `services/api/src/uploads/uploads.service.ts`(+checkDuplicates)
|
||||||
|
- Test: `services/api/test/uploads.e2e-spec.ts`(追加 describe)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 失败测试**(uploads.e2e-spec 追加):成员 A 已有 bytes=B 的可见照片 → check(B) 命中(index/bytes/uploadedAt);不同 bytes 不命中;他账号上传的同 bytes 不命中(uploaderId 隔离);非成员 404;>500 项 400;空 items 400。
|
||||||
|
- [ ] **Step 2: 跑测试确认失败**(端点不存在 404)
|
||||||
|
- [ ] **Step 3: 契约 + 实现**(谓词与 initUpload ③.5 查重逐字一致;findMany bytes in + visibility not deleted;uploadedAt 取命中行)
|
||||||
|
- [ ] **Step 4: 契约 lint + api typecheck + e2e 全绿**
|
||||||
|
- [ ] **Step 5: Commit** `feat(api): upload duplicate pre-check endpoint (checkUploadDuplicates)`
|
||||||
|
|
||||||
|
### Task 2: App 统一查重入口 + 双页接线(TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/mobile/lib/core/api_client.dart`(+checkUploadDuplicates,500/块顺序分块)
|
||||||
|
- Modify: `apps/mobile/lib/features/uploads/enqueue.dart`(+checkDuplicatesUnified:本机 ∪ 服务器;bytes 映射回 path;服务器失败静默降级)
|
||||||
|
- Modify: `apps/mobile/lib/pages/capture_page.dart`、`album_detail_page.dart`(`_pickFromGallery` 换统一入口)
|
||||||
|
- Test: 上述对应测试(页级 fake apiClient override 命中/不命中/网络失败三态)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 失败测试**(统一入口:仅服务器命中 → 弹窗出现且 forcePaths 正确;服务器异常 → 静默本机-only;本机+服务器并集去重)
|
||||||
|
- [ ] **Step 2: 确认失败 → 实现 → 全绿**(flutter analyze + 全量 test)
|
||||||
|
- [ ] **Step 3: Commit** `feat(app): server-side duplicate pre-check at enqueue moment`
|
||||||
|
|
||||||
|
### Task 3: 部署测试服务器 + 测试包 + 验证清单
|
||||||
|
|
||||||
|
- [ ] 服务端镜像构建部署(git archive → docker build → scp → load → compose up → **restart 两个 web 容器**,见 memory 部署流程)
|
||||||
|
- [ ] 测试服 APK 重打(API_BASE=32005)
|
||||||
|
- [ ] 用户验证:重装 App(清本地历史)→ 重选已传照片 → **点上传即弹重复**(跨设备场景)→「跳过重复」只传非重复 →「全部上传」force 生效 → 上传中心不再出现重复失败
|
||||||
|
|
||||||
|
**风险与回退:** 预检接口失败静默降级(最坏回到现状=409 兜底);revert Task 1/2 各自独立成 commit;服务端查询走 bytes 索引(idx_photos_dedup 已有,90dcc58)。
|
||||||
Reference in New Issue
Block a user