Files
photo-live/services/api/src/uploads/uploads.controller.ts
T
浅念 805f77be76 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>
2026-09-28 18:56:07 +08:00

94 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// services/api/src/uploads/uploads.controller.ts —— /uploads 端点(Task 18 起;
// Task 19 增补 listUploads/renewUploadCredential/completeUpload;查重预检
// checkUploadDuplicates 为「上传查重前置到入队前」Task 1 增补)。方法名与契约
// operationId 一一对应。控制器级 SessionGuard:端点全需认证。职责:透传认证
// 用户 + 查询参数规整(parseLimit/parseCursor 用 shared/pagination,同契约 bounds)。
// 注意 DTO 必须值导入(非 import type):@Body 参数类型经 emitDecoratorMetadata
// 进入设计期元数据,import type 会令参数在管线中被置为 undefined(Task 18 实测)。
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { userOf } from '../albums/albums.controller';
import { parseCursor, parseLimit } from '../shared/pagination';
import { AuthenticatedRequest, SessionGuard } from '../auth/session.guard';
import { UploadsService } from './uploads.service';
import type {
UploadCompleteResponse,
UploadDuplicateCheckResponse,
UploadInitResponse,
UploadSessionListResponse,
} from './uploads.service';
import { UploadInitRequestDto, DuplicateCheckRequestDto } from './dto';
@Controller('uploads')
@UseGuards(SessionGuard)
export class UploadsController {
constructor(private readonly uploadsService: UploadsService) {}
/** 契约 201(幂等重复调用返回原会话,同 201)。 */
@Post()
initUpload(
@Body() dto: UploadInitRequestDto,
@Req() req: AuthenticatedRequest,
): Promise<UploadInitResponse> {
return this.uploadsService.init(userOf(req), dto);
}
/** 上传历史:当前用户自己的会话,新在前,(createdAt,id) 键集游标。 */
@Get()
listUploads(
@Query('cursor') cursor: string | undefined,
@Query('limit') limit: string | undefined,
@Req() req: AuthenticatedRequest,
): Promise<UploadSessionListResponse> {
return this.uploadsService.list(
userOf(req),
parseCursor(cursor),
parseLimit(limit),
);
}
/**
* 查重预检(只读批量):判定谓词与 init ③.5 服务端查重逐字一致(见 service
* checkDuplicates)。声明于 :uploadId 参数路由之前(静态段路由,无实际冲突,
* 仅保持「具体路径先于参数路径」的可读惯例)。
*/
@Post('duplicates/check')
@HttpCode(HttpStatus.OK)
checkUploadDuplicates(
@Body() dto: DuplicateCheckRequestDto,
@Req() req: AuthenticatedRequest,
): Promise<UploadDuplicateCheckResponse> {
return this.uploadsService.checkDuplicates(userOf(req), dto);
}
/** 续签:仅 pending 会话;返回结构同 initUpload(credential 为新值)。 */
@Post(':uploadId/renew')
@HttpCode(HttpStatus.OK)
renewUploadCredential(
@Param('uploadId') uploadId: string,
@Req() req: AuthenticatedRequest,
): Promise<UploadInitResponse> {
return this.uploadsService.renew(userOf(req), uploadId);
}
/** 完成核验:幂等(重复调用返回相同结果,不重复建 job)。 */
@Post(':uploadId/complete')
@HttpCode(HttpStatus.OK)
completeUpload(
@Param('uploadId') uploadId: string,
@Req() req: AuthenticatedRequest,
): Promise<UploadCompleteResponse> {
return this.uploadsService.complete(userOf(req), uploadId);
}
}