// services/api/src/sync/dto.ts —— 内部同步端点请求 DTO(P2 Task 3 // bootstrap/reconcile;Task 4 增 digest;Task 9 增 stat-snapshots)。 // 契约权威:openapi.yaml [internal](syncBootstrap/syncReconcile/syncDigest/ // syncStatSnapshots)。经全局 ValidationPipe(whitelist)剥离未知字段;校验 // 失败 400 VALIDATION_FAILED。 import { Type } from 'class-transformer'; import { ArrayMaxSize, ArrayMinSize, IsDateString, IsIn, IsInt, IsOptional, IsString, Max, Min, ValidateIf, ValidateNested, } from 'class-validator'; /** 契约 SyncEntityType(slot-state 预留——与 openapi enum 同步)。 */ export const SYNC_ENTITY_TYPES = [ 'album', 'category', 'photo', 'asset', 'slot-state', ] as const; /** 契约 BootstrapRequest:albumId 缺省 = 全部相册。 */ export class BootstrapRequestDto { @IsOptional() @IsString() albumId?: string; } /** 契约 ReconcileRequest:必填 entityType/sinceVersion/limit。 */ export class ReconcileRequestDto { @IsIn(SYNC_ENTITY_TYPES) entityType!: (typeof SYNC_ENTITY_TYPES)[number]; @IsOptional() @IsString() albumId?: string; @IsInt() @Min(0) sinceVersion!: number; @IsInt() @Min(1) @Max(1000) limit!: number; } /** 契约 DigestRequest(Task 4):albumId 缺省 = 全部相册。 */ export class DigestRequestDto { @IsOptional() @IsString() albumId?: string; } /** 契约 SyncStatKind(与 openapi enum 同步)。 */ export const SYNC_STAT_KINDS = ['likes', 'views'] as const; /** * 契约 StatSnapshot(Task 9 ov→cn 统计快照单元)。kind=likes 时 photoId 必填 * (ValidateIf 条件校验,违反 → 400);kind=views 时 photoId 即使携带也仅作 * 透传字段,接收端按 albumId 应用(相册级快照无照片位)。 */ export class StatSnapshotDto { // 仅 kind=likes 时校验(条件为 true 时 undefined 也过不了 IsString → 400, // 即契约「likes 时 photoId 必填」);kind=views 时跳过、接收端忽略该字段。 @ValidateIf((o: StatSnapshotDto) => o.kind === 'likes') @IsString() photoId?: string; @IsString() albumId!: string; @IsIn(SYNC_STAT_KINDS) kind!: (typeof SYNC_STAT_KINDS)[number]; @IsInt() @Min(0) total!: number; @IsInt() @Min(1) statVersion!: number; @IsString() source!: string; @IsDateString() confirmedAt!: string; } /** 契约 StatSnapshotRequest(Task 9):批次上限对齐 SYNC_BATCH_SIZE 量级。 */ export class StatSnapshotRequestDto { @ArrayMinSize(1) @ArrayMaxSize(100) @ValidateNested({ each: true }) @Type(() => StatSnapshotDto) snapshots!: StatSnapshotDto[]; }