Files
photo-live/services/api/src/sync/dto.ts
T
浅念 955bea49b5 feat(sync): stat snapshot return path with version replacement
P2 Task 9 统计回传(ov→cn):
- cn 迁移:albums 增 viewCountReplica/statsVersion/statsSyncedAt、
  photos 增 statsVersion(like_count 已有副本位;无 VARCHAR 新列不涉校对手决)
- cn 接收端 POST /v1/internal/sync/stat-snapshots(InternalSyncGuard):
  整批单事务,逐快照 updateMany WHERE statsVersion < statVersion —— 仅更高
  statVersion 以权威 total 整体替换副本(绝不相加),重复/乱序幂等忽略;
  accepted 只数实际应用条数;views 应用同时推进 statsSyncedAt(A27 展示)
- ov 回传发送器 services/sync/stat-return.ts:聚合 outbox_stat_events 未发送
  行按(kind, photoId|albumId)取 MAX(statVersion) 折叠突发 → HMAC 推送 →
  200 后按计数器标记截至该版本全部源行 sentAt;失败留待下轮重发;main.ts
  第三循环 STAT_RETURN_INTERVAL_MS(默认 60s,env 可调)
- 契约 AlbumSummary + statsSyncedAt(string|null readOnly,+gen);listAlbums/
  getAlbum 经 toSummaryResponse 暴露(管理 UI 列留 P4a)
- e2e(services/sync stat-return.e2e-spec,7 例):逐次点赞回传替换式收敛
  (2→5→10 非累加)、浏览窗口去重回传 + statsSyncedAt、重复回传 accepted=0
  值不动(A27 不重复累计)、乱序高版本替换(total 可下调)+ 低版本忽略、
  发送端折叠只发最高版本且被折叠行随批确认、管理列表 likeCount/statsSyncedAt
  展示、新端点 401/400 守卫

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 18:45:43 +08:00

107 lines
2.6 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/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[];
}