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
+8 -5
View File
@@ -1202,12 +1202,14 @@ paths:
加载成功后上报一次(列表/图片/WS 等接口调用一律不触发计数)。
- requestId 为上报幂等键(建议 UUID),counted 确认前跨刷新/重试持久复用;
重复 requestId 返回 counted=false 与权威 viewCount,不重复计数。
重复 requestId 幂等重放:counted 回显该 requestId 首次处理的结局
(确认响应丢失后重试仍见 true),viewCount 恒为当前权威值,不重复计数。
- 去重窗口(VISIT_WINDOW_MINUTES,默认 30 分钟,D07)内同一 visitorId
不重复计数;窗口过后再上报按新访次计数。
- 过期 requestId(VISIT_REQUEST_TTL_MINUTES,默认 35 分钟)拒绝重放
(counted=false),不得重新加计。
- 基础限速:visitorId 滑窗,超限 429(RATE_LIMITED);去重本身为主缓解。
(counted=false,无论原结局),不得重新加计。
- 基础限速:visitorId+来源 IP 固定窗(VISIT_WINDOW_REQUESTS,默认
60/窗),超限 429(RATE_LIMITED);去重本身为主缓解。
- 相册非公开/紧急停用 404(ALBUM_NOT_PUBLIC)。
security: []
requestBody:
@@ -2666,8 +2668,9 @@ components:
type: boolean
readOnly: true
description: |
本次是否实际计数:true=新访次;false=去重窗口内重复、重复
requestId 或过期 requestId 重放(均不重复计数)。
本次是否计入:true=新访次,或幂等重放已确认计入的 requestId
(回显原结局);false=去重窗口内重复或过期 requestId 重放
(均不重复计数)。
# ── 内部同步 ──
+4
View File
@@ -28,3 +28,7 @@ export const LIKE_IP_WINDOW_DELTA = 2000;
export const LIKE_BURST_MERGE_MS = 500;
/** 浏览上报 requestId 保留分钟:过期 requestId 拒绝重放(≥ 去重窗口)。 */
export const VISIT_REQUEST_TTL_MINUTES = 35;
/** 浏览上报基础限速:visitorId+IP 固定窗(=去重窗口)内请求数上限,超限 429。 */
export const VISIT_WINDOW_REQUESTS = 60;
/** ov 互动流水清理调度间隔分钟(like_batches/visit_requests/visit_sessions)。 */
export const CLEANUP_INTERVAL_MINUTES = 60;
+8 -5
View File
@@ -554,12 +554,14 @@ export interface paths {
* 加载成功后上报一次(列表/图片/WS 等接口调用一律不触发计数)。
*
* - requestId 为上报幂等键(建议 UUID),counted 确认前跨刷新/重试持久复用;
* 重复 requestId 返回 counted=false 与权威 viewCount,不重复计数。
* 重复 requestId 幂等重放:counted 回显该 requestId 首次处理的结局
* (确认响应丢失后重试仍见 true),viewCount 恒为当前权威值,不重复计数。
* - 去重窗口(VISIT_WINDOW_MINUTES,默认 30 分钟,D07)内同一 visitorId
* 不重复计数;窗口过后再上报按新访次计数。
* - 过期 requestId(VISIT_REQUEST_TTL_MINUTES,默认 35 分钟)拒绝重放
* (counted=false),不得重新加计。
* - 基础限速:visitorId 滑窗,超限 429(RATE_LIMITED);去重本身为主缓解。
* (counted=false,无论原结局),不得重新加计。
* - 基础限速:visitorId+来源 IP 固定窗(VISIT_WINDOW_REQUESTS,默认
* 60/窗),超限 429(RATE_LIMITED);去重本身为主缓解。
* - 相册非公开/紧急停用 404(ALBUM_NOT_PUBLIC)。
*/
post: operations["reportAlbumVisit"];
@@ -1334,8 +1336,9 @@ export interface components {
*/
readonly viewCount: number;
/**
* @description 本次是否实际计数:true=新访次;false=去重窗口内重复、重复
* requestId 或过期 requestId 重放(均不重复计数)。
* @description 本次是否计入:true=新访次,或幂等重放已确认计入的 requestId
* (回显原结局);false=去重窗口内重复或过期 requestId 重放
* (均不重复计数)。
*/
readonly counted: boolean;
};