Files
photo-live/docs/plans/progress/P2-验收记录.md
T
浅念 79eb975096 docs: p2 acceptance record
P2 Task 12: 验收记录(A09/A10/A14/A21 部分/A26/A27 仿真级 + A25 部分
本地直读证据;D07/D11 初值确认——30min 浏览窗口、2s 推送/5min 对账/
60s 回传建议维持待 P5 实测冻结;13 项偏差登记;P4a/P5 移交清单;回归
门实测统计——全仓 448 测试绿)+ 总纲 P2 行进度注记。

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

104 lines
12 KiB
Markdown
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.
# P2 验收记录
- **阶段**:P2 双地域仿真 + 互动(计划 `docs/plans/2026-09-18-P2-双地域仿真与互动.md`,Task 1–12)
- **日期**:2026-09-18
- **基线 commits**:`a4b95ab..HEAD`(P2 计划收尾提交之后至 Task 11 跨地域套件 `452ec12`,共 10 个实施提交,不含本记录提交)
- **执行模式**:子代理驱动 + 双审(每任务先写失败测试→实现→通过→提交;跨任务由子代理接力,主代理与复核代理双重审查)
- **回归门**:`pnpm lint && pnpm typecheck && pnpm test && pnpm e2e && pnpm build` 全绿(数字见文末统计表)
- **基础设施**:photo-live compose 以 profiles 起双地域仿真——mysql-cn(3306)/redis-cn(6379) + mysql-ov(**3309**,宿主 3307 被占,见偏差 #9)/redis-ov(6381) + MinIO;ov 入口与 cn 入口同一 services/api 代码库按部署角色装配(app-ov.module 显式挑选);sync 进程三循环(推送 2s / 对账 5min / 统计回传 60s)
---
## A09 点赞计数(仿真级)
- 后端:`services/api/test/likes-realtime.e2e-spec.ts` **6 用例**——① 连点累计(delta 求和进权威 likeTotal)+ 同 batchId 幂等重试不加计 + outbox_stat_events 权威快照 + 参数/404 校验;② sessionId 滑窗限速超额 429;③ WS ping→pong/幽灵 token error 帧/错误后连接保持;⑥ 隐藏照片后点赞 404(公开面与互动面同一可见性口径);④⑤(房间隔离/合并广播)见下 A10/A21 条目。
- 前端:`apps/viewer/e2e/likes.spec.ts`——大图真实点赞 ×3 → 权威校准 3 → 刷新持久;WS 不可达三败降级轮询校准后重连。
- 跨境一致:cross-region 套件 ①⑥(见 A27)——cn 副本 likeCount 终值 == ov 权威 Σdelta。
## A10 实时隔离(仿真级)
- `likes-realtime.e2e-spec.ts` ④ **房间隔离**:双相册双房间并发订阅,跨册点赞只达本册房间,另一房间零消息(帧级断言)。
- 网关模型:房间 = `album:{albumId}`,subscribe 逐相册校验公开性;广播不排除发送者(过滤依据是 originBatchIds,非连接身份)。
## A14 浏览统计(仿真级)
- `services/api/test/visits.e2e-spec.ts` **11 用例**——① 首访计入/窗口内去重/跨访客计入/GET album 真实 viewCount;② requestId 幂等重试回显原结局;③ 越窗再计;④ 并发同访客两 requestId 恰一个 counted(session 唯一约束仲裁);⑤ 过期 requestId 重放拒绝;⑥ 404/400;⑦ visitorId+IP 固定窗限速(幂等快速通道不耗预算);⑧ **上报排除规则**(读接口/点赞/WS 帧一律不触碰浏览数据);⑨ WS stats.updated 计入即广播、重复不广播;⑩ runCleanupOnce 超窗清理;⑪ 过期批次重放边界(登记偏差 #3 的固化用例)。
- 前端:`apps/viewer/e2e/likes.spec.ts` 浏览上报组——全新上下文 +1、刷新不重复、第二上下文再 +1;visitorId 跨标签共享(localStorage+BroadcastChannel,vitest 覆盖协调写)。
## A21 气泡体验(部分:后端合并 + 前端自身过滤)
- 后端合并验证:`likes-realtime.e2e-spec.ts` ⑤——合并窗内两批 → 恰一条 likes.burst(delta 求和 + **双 originBatchIds**,客户端过滤自身批次的依据);窗参数 LIKE_BURST_MERGE_MS 惰性 env。
- viewer 自身过滤 e2e:`likes.spec.ts` 气泡组——双真实浏览器上下文,他赞出气泡、己赞(originBatchIds 含己 batchId)不出;「减少动画」设置下他赞无气泡、计数照常校准。
- **P4b 移交**:页面不可见暂停动画(visibilitychange)有实现与单测,全端动画体验验收(含 App 端推送通道)随 P3/P4b。
## A26 内容同步(仿真级)
- `services/sync/test/sync.e2e-spec.ts` ④⑨——隐藏后旧 published 以新 eventId 重放被版本比较拒绝;乱序 v7 hidden 先应用、迟到 v6 published 不复活。
- `services/sync/test/reconcile.e2e-spec.ts`——当前态清扫重放对已追平实体判重/判旧;digest 比对触发 bootstrap 重建;「投后即失」漏账追赶。
- `services/sync/test/cross-region.e2e-spec.ts` ④——photo3 隐藏(v3)后伪造其旧 published 事件(v2 载荷、全新 eventId)直推 ov content-batches → applied=0 仍隐藏;**全量 bootstrap 重跑后仍隐藏**(当前态快照不含复活路径)。
- 「不显示缺资源图片」:photos_ov 行缺失即缺席(列表无行、单图 404 PHOTO_NOT_PUBLIC),public-ov spec ⑤ 覆盖。
## A27 统计回传(仿真级)
- `services/sync/test/stat-return.e2e-spec.ts` **7 用例**——① 逐次点赞回传 cn likeCount 被权威快照**替换**(2→5→10,绝非 2+5+10 累加);② 浏览回传 viewCountReplica + **statsSyncedAt 落位**(同步时间展示,cn listAlbums 已暴露);③ 重复回传幂等(清 sentAt 重发 → 同版本全判旧 accepted=0,值分毫不动);④ 乱序快照仅更高 statVersion 替换(版本权威,total 可下调);④b 发送端聚合(同相册 v5/v3 折叠只发 v5、两行全确认);⑤ cn 管理展示;⑥ 新端点守卫/校验。
- 跨境闭环:cross-region ①(两会话点赞 + 两访客浏览 → 一轮回传 → cn 副本一致);⑥(50 并发点赞回传后 cn == ov 权威 Σdelta、30 并发浏览回传后 viewCountReplica=17)。
## A25 海外直读(部分:本地直读语义)
- 仿真级证据:`cross-region.e2e-spec.ts` ①——**观众路径全程只打 ov 入口**(公开相册/列表/单图/点赞/浏览/WS 六类请求一律指向 ovBase;cn 入口仅承载 staff 上传与内部同步动作),列表 likeCount/viewCount 均为 ov 副本本地值,无跨境读。
- **P5 移交**:真实地域部署 + 全球 CDN 图片分发 + 压测容量结论(当前为 compose 仿真,图片仍走本地 MinIO 签名 URL)。
---
## D07 / D11 初值确认(P2 验收前给初值,P5 实测冻结)
| 决策 | 初值(contracts 常量,env 可覆盖) | 结论 |
|------|-----------------------------------|------|
| **D07** 浏览统计窗口 | `VISIT_WINDOW_MINUTES = 30`(requestId 保留 35min、请求限速 60/窗) | **建议维持 30 分钟**——与需求说明书建议值一致;真实流量分布 P5 复核 |
| **D11** 跨境同步参数 | 推送 `SYNC_PUSH_INTERVAL_MS = 2000`(批 100,high 两段优先);对账 `SYNC_RECONCILE_INTERVAL_MS = 300000`;统计回传 `STAT_RETURN_INTERVAL_MS = 60000`;中断策略 = 幂等重试(eventId 受理锚 + entityVersion 单调,失败指数退避 60s 封顶,整批不标记下轮重推) | **建议维持**(2s 推送 / 5min 对账 / 60s 回传)——仿真规模下语义正确性已验,容量与真实 RTT 下的参数调优随 **P5 压测冻结** |
---
## 偏差与发现登记
| # | 事项 | 处理/结论 |
|---|------|-----------|
| 1 | reconcile 返回事件流 | cn `/internal/sync/reconcile` 按 entityType 返回**当前状态事件清单**(合成 eventId `recon:<type>:<id>:<ver>`),非版本水位清单——重放走既有 content-batches 管道即幂等,无需第二条拉取通道;真实部署若需缩量改 per-entity 水位比对(P5 演进项) |
| 2 | digest 形状 | 每相册权威计数 `{categoryCount, publishedPhotoCount..photoCount 区间, assetCount}`——照片取区间口径(从未发布过的 hidden/deleted 照片 ov 合法无行,缺席即撤下),ov 行数落区间内即判一致 |
| 3 | 过期点赞批次接受 + 上限兜底 | like_batches 按 24h 清理后,同 (photoId,sessionId,batchId) 重放会以新批次身份重新加计(永久幂等账本与清理诉求直接冲突,理论边界);缓解 = sessionId 滑窗 500/窗口 + IP 2000/窗口 + 基础限速;visits spec ⑪ 固化该行为 |
| 4 | 紧急停用审计 pino 字段 | 计划原文「写 ov 侧日志文件」落地为 pino 结构化字段 `audit=emergency`(action/albumId/publicToken/reason/already*)——免新增文件 transport 依赖,P5 运维按字段路由聚合到审计存储 |
| 5 | visits 重试语义修订 | requestId 行龄 > 35min → counted=false **无论原结局**(清理后行不存在与「从未提交」数据上不可区分,「窗口外拒绝重放」以清理端为拒绝点);行龄内重试回显原结局(确认响应丢失的闭环依赖) |
| 6 | EventEmitter2 单进程 | ov 实时广播为进程内事件总线(OV_LIKE_BATCH_COMMITTED 等三事件),单实例部署假设;多实例横向扩展需 Redis pub/sub 跨进程扇出(REDIS_URL_OV 已就位,事件名/载荷可原样迁移为频道/消息体,P5 部署项) |
| 7 | hot 排序偏移游标 | 热门序游标为点赞快照 **offset**(30s TTL 缓冲,窗口内冻结、photoId 决胜),与最新序 (publishedAt,photoId) 键集游标不同形——排序为快照语义,切换即重拉(对齐技术方案 §7) |
| 8 | contracts ./realtime 子路径导出 | WS 帧类型/常量新增 `@photo/contracts/realtime` 子入口(package.json exports);主入口同时重导出,前端可深引入收敛产物 |
| 9 | mysql-ov 宿主端口 3309 | 宿主机 3307 已被用户另一项目(yunke-mysql)占用,mysql-ov 发布端口改绑 **3309**(**用户裁决 2026-09-18**,commit `a4b95ab`;compose/env/.env.example 同步) |
| 10 | Prisma client 双输出目录 | db-ov generator 显式 `output = ../generated/client`(db-cn 维持 node_modules 内默认生成)——双 PrismaClient 并存不互踩;生成产物不入库,`pnpm db:generate` 统一生成 |
| 11 | visit requestId 跨相册不复核 | requestId 为 visit_requests 全局主键,重试快速通道按行龄裁决即回显,不复核「该 requestId 原属哪本相册」(幂等键语义 = 单次上报;跨册重用属客户端契约误用,风险面为回显错误 viewCount,不产生加计) |
| 12 | statsSyncedAt 仅 views 驱动 | albums.statsSyncedAt 只由 views 快照应用推进(相册级同步时间);likes 快照替换 photos.likeCount 不触碰相册时间(照片级无相册时间语义,cn 管理列表按相册展示同步时间) |
| 13 | ov 孤儿相册仅告警 | digest 比对发现 ov 有而 cn 无的相册(cn 行已删)仅告警登记——无 cn 快照源不可 bootstrap 重建;清理动作随 P4a 管理页(sync_state/sync_inbox 延迟与积压口径展示) |
## 未尽事项 / 移交
- **P4a**:管理页同步状态展示(相册级 statsSyncedAt 已在 listAlbums 暴露,UI 消费随后台完备);ov 孤儿相册清理入口;批量隐藏;清理任务生产化参数复核(LIKE_BATCH_WINDOW_HOURS / VISIT_REQUEST_TTL_MINUTES / 清理节奏)。
- **P5**:真实地域部署(海外 ECS 已就绪)+ 域名 + 全球 CDN 接入与失效策略;跨地域故障演练(断网/积压/恢复已在仿真覆盖,真实拓扑重演);容量压测与 D07/D11 参数冻结;trust-proxy 配置(点赞/浏览限速当前取 req.ip,经反代需正确取 X-Forwarded-For);EventEmitter2 → Redis pub/sub 多实例扇出;reconcile per-entity 水位比对缩量。
---
## 测试统计汇总(2026-09-18 回归门实测)
| 包 | 单测 suites(files)/tests | e2e suites(tests) |
|----|--------------------------|-------------------|
| @photo/contracts | 3 / **22** | – |
| @photo/db-cn / @photo/db-ov | –(schema 校验随 migrate/生成流程) | – |
| @photo/storage | 1 / 6(真实 MinIO 集成) | – |
| @photo/api-cn | 7 / **51** | **16 / 179**(P1 12/148 基础上 +**likes-realtime 6、visits 11、emergency 7、public-ov 7** = +31) |
| @photo/worker | 1 / 8 | 1 / 7(publish) |
| @photo/sync(P2 新包) | –(passWithNoTests) | **4 / 27**(sync 10、reconcile 4、stat-return 7、**cross-region 6**) |
| @photo/viewer | vitest **16 / 105** | playwright **6 个 spec / 21 用例**(album 5、feed 3、viewer 4、smoke 1、journey 1、**likes 7**) |
| @photo/admin | vitest **4 / 15** | playwright **2 个 spec / 7 用例**(login 5、albums 2) |
| **合计** | **32 / 207** | **29 / 241**(jest 21 suites / 213 用例 + playwright 8 个 spec / 28 用例) |
- 附加门:lint 9/9 任务、typecheck 13/13 任务、build 9/9 任务全绿。
- 总测试数(单测 + e2e):**448**(P1b 出口 324 → +124)。
- 跨地域套件(Task 11):六幕串行剧本 12–16s 稳定通过(双入口进程内起真服务,真实双 MySQL/MinIO/HMAC/WS,限速滑窗键跨 spec 清场)。