805f77be76
「上传查重前置到入队前」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>
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
// 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);
|
||
}
|
||
}
|