feat(api): upload duplicate pre-check endpoint (checkUploadDuplicates)

「上传查重前置到入队前」Task 1:POST /v1/uploads/duplicates/check 只读批量
查重预检——用户点上传(确认入队)时刻预判服务端历史重复(跨设备/重装/改名
均可查到),不再等任务在上传中心以 PHOTO_DUPLICATE 失败才暴露。

- 契约纯增量:/uploads/duplicates/check + UploadDuplicateCheck(Item|Request|
  Duplicate|Response) schemas;regen openapi.d.ts。
- 判定谓词与 init ③.5 服务端查重逐字一致:同 albumId + uploaderId=当前用户
  + bytes 相等 + visibility != 'deleted'(filename 刻意不参与),保证「预检
  即真相」;命中条目按请求下标逐项返回(同 bytes 多请求条目各一条)。
- 权限链同 init(canView 404 防枚举 / canUpload 403 / 相册空值兜底 404),
  刻意不校验 uploadEnabled:只读预览,init 真正入队时仍按开关 409 兜底。
- 查询走 idx_photos_dedup:请求条目去重字节集一次 findMany,内存建
  bytes→最早 uploadedAt 映射后按请求下标回射。
- items 1..500(minItems/maxItems + 嵌套逐项校验,越界 400)。
- e2e 追加 describe 7 用例:命中含双条目同 bytes/未命中/uploaderId 隔离/
  deleted 不命中/非成员 404/items 边界 400/未认证 401。

契约 lint ✅ · contracts+api typecheck ✅ · api 单测 56/56 ✅ ·
uploads e2e 28/28 ✅

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-28 18:56:07 +08:00
parent 7d744da56e
commit 805f77be76
6 changed files with 564 additions and 5 deletions
+111
View File
@@ -909,6 +909,56 @@ paths:
'500':
$ref: '#/components/responses/InternalError'
/uploads/duplicates/check:
post:
operationId: checkUploadDuplicates
tags: [uploads]
summary: 上传查重预检(批量,只读)
description: |
批量查重预检:对每个请求条目判定「同相册 + 同上传者(当前用户)+ 同字节数」
的非 deleted 照片是否已存在——判定谓词与 initUpload 的 PHOTO_DUPLICATE
查重逐字一致(含 uploaderId 隔离;filename 刻意不参与)。命中条目按请求
下标逐项返回(两个请求条目共享同一命中 bytes 时各返回一条)。
不校验 uploadEnabled:预检是只读预览,相册关闭上传时仍可查询历史重复
(init 在真正入队时仍按开关 409 兜底)。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UploadDuplicateCheckRequest'
responses:
'200':
description: 查重结果(命中子集;无命中时 duplicates 为空数组)。
content:
application/json:
schema:
$ref: '#/components/schemas/UploadDuplicateCheckResponse'
'400':
description: 参数校验失败(VALIDATION_FAILED):items 为空/超 500 项或条目字段非法。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 无该相册的上传权限(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不可见或不存在(ALBUM_NOT_FOUND,不区分以避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/uploads/{uploadId}/renew:
parameters:
- $ref: '#/components/parameters/UploadId'
@@ -3536,6 +3586,67 @@ components:
items:
$ref: '#/components/schemas/UploadSessionRecord'
UploadDuplicateCheckItem:
type: object
description: 查重预检请求条目(与 initUpload 入参同形;filename 仅元数据)。
required: [filename, bytes]
properties:
filename:
type: string
minLength: 1
maxLength: 255
description: 原始文件名(仅元数据;查重谓词不使用,与 init 查重一致)。
bytes:
type: integer
format: int64
minimum: 1
description: 申明文件字节数(查重键)。
UploadDuplicateCheckRequest:
type: object
description: 批量查重预检请求;单批上限 500 项,客户端按 500 分块顺序调用。
required: [albumId, items]
properties:
albumId:
type: string
description: 目标相册 ID。
items:
type: array
minItems: 1
maxItems: 500
description: 待查重条目(1..500 项)。
items:
$ref: '#/components/schemas/UploadDuplicateCheckItem'
UploadDuplicateCheckDuplicate:
type: object
description: 单条命中结果;每个命中的「请求条目」一条(index 指向请求数组)。
required: [index, bytes, uploadedAt]
properties:
index:
type: integer
minimum: 0
description: 命中条目在请求 items 数组中的下标(0 起)。
bytes:
type: integer
format: int64
description: 命中字节数(与对应请求条目一致)。
uploadedAt:
type: string
format: date-time
description: 服务端既有照片的上传时间(同 bytes 多条命中取最早)。
UploadDuplicateCheckResponse:
type: object
description: 查重预检结果(命中子集,按请求下标升序)。
required: [duplicates]
properties:
duplicates:
type: array
description: 命中子集;无命中为空数组。
items:
$ref: '#/components/schemas/UploadDuplicateCheckDuplicate'
# ── 照片(管理端) ──
PhotoVisibilityUpdateRequest: