feat(ov): windowed dedup visit counting with retention cleanup

Task 8 (P2): POST /public/albums/{token}/visits with visit_sessions
window dedup (VISIT_WINDOW_MINUTES), requestId idempotent replay
(echoes stored outcome; expired beyond VISIT_REQUEST_TTL_MINUTES
rejected), album_stats viewCount/statVersion authority + outbox_stat_events
views snapshots, WS stats.updated broadcast, visitorId+IP fixed-window
rate limit (VISIT_WINDOW_REQUESTS), and ov CleanupModule scheduler
(setInterval, CLEANUP_INTERVAL_MINUTES) deleting like_batches /
visit_requests / visit_sessions past their windows — totals never
touched. Contract prose aligned to replay semantics; constants
VISIT_WINDOW_REQUESTS / CLEANUP_INTERVAL_MINUTES added.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 18:14:01 +08:00
parent 85b2da3db5
commit 1f2a0c658f
14 changed files with 1398 additions and 25 deletions
+19 -5
View File
@@ -1,8 +1,9 @@
// services/api/src/interactions/dto.ts —— 点赞批次上报请求体(P2 Task 7)。
// 校验边界与契约 LikeBatchRequest 逐字段对齐(sessionId/batchId 1..64、
// delta 1..20 整数);越界一律 400 VALIDATION_FAILED(全局 ValidationPipe
// whitelist 剥离未声明字段)。delta 必须是 JSON number 整数(管道未开
// transform,字符串数字不静默转换——公开匿名端点从严)。
// services/api/src/interactions/dto.ts —— 互动上报请求体(P2 Task 7 点赞批次 /
// Task 8 浏览上报)。校验边界与契约 LikeBatchRequest / VisitReportRequest
// 逐字段对齐(sessionId/batchId 1..64、delta 1..20 整数;visitorId/requestId
// 8..128);越界一律 400 VALIDATION_FAILED(全局 ValidationPipe whitelist
// 剥离未声明字段)。delta 必须是 JSON number 整数(管道未开 transform,
// 字符串数字不静默转换——公开匿名端点从严)。
import { IsInt, IsString, Length, Max, Min } from 'class-validator';
export class LikeBatchDto {
@@ -22,3 +23,16 @@ export class LikeBatchDto {
@Max(20)
delta!: number;
}
/** 浏览上报请求体(P2 Task 8;契约 VisitReportRequest)。 */
export class VisitReportDto {
/** 访客标识(客户端持久化并在标签页间共享;去重窗口与限速的键)。 */
@IsString()
@Length(8, 128)
visitorId!: string;
/** 上报幂等键:counted 确认前跨刷新/重试持久复用(建议 UUID)。 */
@IsString()
@Length(8, 128)
requestId!: string;
}