Files
photo-live/docs/plans/2026-09-18-P2-双地域仿真与互动.md
T
浅念 3d6e9ce175 feat(ov): ov schema, entry skeleton and compose profile
P2 Task 2 ov 数据层:
- compose ov profile:mysql-ov(mysql:8.4,127.0.0.1:3309,库级
  utf8mb4_0900_bin)+ redis-ov(6381),cn 侧默认 up 不受影响
  (3307/3308 已被其它本机项目占用,端口决策 3309)
- @photo/db-ov:9 表 schema(albums_ov/photos_ov 副本、sync_inbox/
  sync_state 同步账本(水位行同表标志位)、like_batches/album_stats/
  visit_sessions/visit_requests 互动域、outbox_stat_events 回传队列);
  迁移 20260918060102_init_ov 走固化校对手决流程,information_schema
  复核全部业务表/列 utf8mb4_0900_bin(_prisma_migrations 同 cn 先例
  保持 unicode_ci)
- generator 独立 output(包内 generated/):pnpm 将两包 @prisma/client
  提升为同一 store 实例,默认 .prisma/client 输出会被双 schema 互相
  覆盖,ov 客户端独立产出经 src/index.ts 转出口
- ov 入口骨架:main-ov.ts(PORT_OV 默认 8081)+ app-ov.module.ts
  (仅管线骨架 + /v1/health,feature modules 后续任务装配);logger
  装配选项抽取至 shared/logger-options.ts 供 cn/ov 双入口共用(行为
  不变,P1 套件守护)
- test/reset-ov.ts:ov 库 truncate-if-exists 隔离工具(无 MinIO 清理)
- 根脚本:db:generate 双库齐跑,新增 db-ov:migrate/db-ov:generate/dev:ov

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

15 KiB
Raw Blame History

P2 双地域仿真与互动 实施计划

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 交付海外观众服务(本地双实例仿真):cn→ov 版本化内容同步(outbox/inbox、墓碑、补漏对账、高优先级下线)、海外紧急停用入口、点赞批次幂等 + WebSocket 实时气泡、浏览统计窗口去重、统计快照回传;viewer 切换 ov-api 并获得点赞/气泡/热门排序/浏览上报能力。

Architecture: ov 侧 = services/api 新增 ov-app 部署入口(:8081)+ ov Prisma schema(独立 MySQL photo_ov)+ WebSocket 网关;services/sync 承担 cn outbox 发送 / ov inbox 接收 / 双向对账与统计回传。全链路本地 compose 仿真(MySQL×2/Redis×2 以 profiles 启用 ov 侧)。对齐总纲 P2 与技术实施方案 §2A/§7-§8。

Tech Stack: 沿用 P1 全套;新增 ws(NestJS ws adapter)、内部同步接口服务间鉴权(HMAC 签名 + 时间戳,env 密钥)。

对应验收(仿真级): A09 A10 A14 A21 A26 A27;A25 部分(本地直读语义)。

依赖基线: tag p1(b5145d6)。

决策初值登记(D07/D11,按建议值先行,验收前可调):

  • D07:浏览去重窗口 30 分钟(env VISIT_WINDOW_MINUTES=30);主办字段不纳入 P2。
  • D11:outbox 推送间隔 2s/批 100 条、高优先级即时;补漏对账每 5 分钟;统计回传每 60s 增量快照;中断恢复=水位重放。全部 env 可调。

Task 1: ov 契约与决策初值

Files: Modify packages/contracts/openapi.yaml(+gen)、src/constants.ts、.env.example

  • 契约新增:
    • 公开互动端点(ov 域,路径前缀不变,部署域名区分):POST /public/albums/{token}/photos/{photoId}/likes(匿名+限速;body {sessionId, batchId, delta 1..20} → 200 {total, countVersion, batchAccepted};重复 batchId → 幂等返回已确认结果);POST /public/albums/{token}/visits(body {visitorId, requestId} → 200 {viewCount, counted});likes 上限/限速语义写入 description
    • WS 事件契约(components 不适用——单独 packages/contracts/src/realtime.ts):server→client 消息 photo.published / photo.hidden / photo.deleted / likes.burst{photoId, albumId, delta, total, countVersion, burstId, originBatchIds[]} / stats.updated{albumId, viewCount};client→server subscribe{albumToken, sinceVersion?} / unsubscribe;心跳 ping/pong 20s
    • 内部同步接口(tag internal,security: internalSyncKey 描述):POST /internal/sync/content-batches(数组:{eventId, schemaVersion, entityType: album|category|photo|asset|slot-state(预留), entityId, albumId, entityVersion, tombstone?, priority, payload});POST /internal/sync/stat-snapshots(cn←ov 方向:数组 {photoId?, albumId, kind: likes|views, total, statVersion, source, confirmedAt});对账 POST /internal/sync/reconcile{entityType, sinceVersion, limit} → 缺失清单;全量快照 POST /internal/sync/bootstrap{albumId?}→{snapshotEventIds[]}(经对账通道分批拉取)
  • constants:SYNC 批量/间隔、VISIT_WINDOW_MINUTES、REALTIME 心跳等 env 键名与默认值入 .env.example
  • gen + lint + 全回归绿;Commit feat(contracts): p2 realtime, likes, visits and internal sync contracts

Task 2: ov 数据层

Files: Create packages/db-ov/(mirror db-cn 模式:package/schema/src);services/api ov 入口骨架 src/main-ov.ts + app-ov.module.ts;compose ov 侧 profile(mysql-ov 3309、redis-ov 6381,127.0.0.1 绑定);test/reset-ov.ts(api 包共用,按连接串区分)

  • ov schema:albums_ov(cn 副本:publicToken/状态/开关/翻译 JSON/分类 JSON/banner 对象键/版本)、photos_ov(photoId/albumId/分类/宽高/objectKeys/likeTotal/countVersion/visibility/版本/publishedAt)、sync_inbox(eventId 唯一、entityType/entityId/entityVersion、tombstone、payload、receivedAt/appliedAt)、sync_state(entityType+entityId → appliedVersion,水位表 per source)、like_batches(photoId+sessionId+batchId 唯一、delta、createdAt + 索引/清理窗口)、album_stats(albumId 主键 viewCount/updatedAt/statVersion)、visit_sessions(albumId+visitorId 唯一、lastCountedAt)、visit_requests(requestId 唯一、counted、createdAt)、outbox_stat_events(ov→cn 回传队列)
  • 迁移走固化校对手决流程;14→N 表全 bin 校对验证;健康检查 + /health
  • Commit feat(ov): ov schema, entry skeleton and compose profile

Task 3: cn 事件补全 + sync 发送器(cn→ov)

Files: Create services/sync/(@photo/sync:sender + scheduler);Modify services/api/src/albums|photos|public(事件补全)、packages/db-cn/prisma/schema.prisma(albums/categories/album_assets 增 entityVersion 列——迁移走校对手决流程)

  • cn 事件补全(评审缺口 1):albums 服务在 创建/更新(含开关、下线 status、translations 替换)时写 outbox album.updated(下线与 publicEnabled=false → 高优先级);categories 增改删 → category.*(删除=tombstone);assets complete 生效 → asset.updated;各表 entityVersion 条件自增(同 photos 模式:更新事务内 +1);payload 携带 ov 所需展示字段(translations JSON、开关、banner 对象键、分类翻译)
  • 首次全量快照:POST /internal/sync/bootstrap{albumId?}——对指定相册(或全部)生成当前快照事件序列(album→categories→assets→photos 含版本),ov 冷启动或新建副本时经对账通道拉取(§2A"首次全量快照携带水位")
  • outbox 发送器:轮询 outbox_events(未 delivered)按优先级(高优先级=相册下线/照片隐藏删除 tombstone 类,即发)批量 HMAC 签名推送 content-batches → ov 返回 {acceptedEventIds, appliedWatermarks} → 回写 deliveredAt;失败退避重试,绝不丢失(deliveredAt 为空即重发);eventId 幂等由 ov 保证
  • 集成测试:cn 发布照片→sender→ov inbox applied→photos_ov 出现;相册更新(开关/翻译)→ albums_ov 副本同步;重复推送→幂等;乱序版本→旧版本忽略;墓碑→副本删除且不被旧事件复活(A26 核心);bootstrap 冷启动建全套副本
  • Commit feat(sync): cn event coverage, bootstrap and outbox sender

Task 4: inbox 接收与对账(ov 侧)

Files: services/api/src/sync-inbox/(ov module);sync 服务补漏调度

  • inbox 端点(internal 鉴权):事务内 eventId 唯一插入→entityVersion 比较(sync_state)→应用或忽略→依赖缺失(相册未到而照片先到)暂存不 applied、待依赖事件到达后补应用(或对账补取);墓碑优先;applied 后应答
  • 对账:定时(5min)按 sync_state 水位向 cn 请求 reconcile 清单→拉缺失→重放;同步状态表供管理查询(延迟、积压——P4a 展示,本阶段出 API/DB)
  • 集成测试:断续中断(停 sender)→恢复后补齐;隐藏优先于发布乱序到达(先 photo.hidden 后 photo.published 旧版本→保持隐藏)
  • Commit feat(sync): ov inbox with versioning, tombstones and reconciliation

Task 5: 海外紧急停用入口

  • ov 侧 POST /internal/emergency/disable{albumToken|albumId, reason}(internal 鉴权):ov DB 直接置 albums_ov.emergency_disabled=true(独立于同步版本——不被任何后续同步事件解除,解除仅经显式 enable 端点);公开 API/WS 检查该标志;cn 侧不感知(文档化);审计写 ov 侧日志文件(无 cn audit——登记偏差)
  • 测试:停用→ov 公开 404→cn 继续同步新照片(ov 保持屏蔽)→显式启用恢复
  • Commit feat(ov): emergency disable channel independent of sync

Task 6: ov 公开读 API

  • ov-app 实现 P1 同形公开三端点(album/photos 列表/单图)读 photos_ov 副本 + 最新/热门排序(sort=latest|hot,hot=likeTotal 排序短期快照——30s 缓冲快照表或内存缓存)+ banner 签名 URL(ov 侧持有 MinIO/OSS 只读凭证——本地同 MinIO)
  • viewer 切换:API base env VITE_API_BASE(dev 默认 ov :8081);全 viewer e2e 迁移跑 ov 端(P1 套件保持绿=契约同形证明)
  • Commit feat(ov): public read api on replicas with hot sorting

Task 7: 点赞与实时(ov)

  • likes 端点:事务(like_batches 唯一插入→photos_ov.likeTotal 原子+delta、countVersion++→outbox_stat_events + 实时 burst 事件入队);重复 batchId 幂等返回;基础限速(sessionId+IP 滑窗,Redis ov);delta 越界 400;照片/相册非公开 404
  • WS 网关(NestJS ws adapter on ov):subscribe 校验相册公开→加入相册房间;广播 likes.burst(短窗 500ms 合并、保留 originBatchIds 供自身过滤)、photo.*、stats.updated;心跳 20s 踢死链;断线客户端降级轮询(viewer 侧 Task 10)
  • (契约含 POST /internal/sync/bootstrap——Task 1 已列)集成测试:A09(连点累计、重复请求不加计)、A10(两相册隔离:房间不串)、A21 部分(合并广播 batch 过滤在后端侧验证 originBatchIds 正确性)
  • Commit feat(ov): idempotent batched likes and realtime broadcast

Task 8: 浏览统计(ov)+ 去重数据清理

  • visits 端点:事务(visit_requests requestId 唯一→visit_sessions 锁行/upsert→窗口判断(VISIT_WINDOW_MINUTES)→counted 则 album_stats.viewCount++、statVersion++→应答 {viewCount, counted});并发首访唯一(唯一约束+重试);过期 requestId 拒绝;GET album 返回 viewCount(真实值,替换 P1 占位 null)
  • 上报排除规则落地:接口仅显式调用(列表/图片/WS 皆不触发——测试断言);基础限速(sessionId 滑窗——去重本身为 P2 主缓解,登记)
  • 清理调度(评审缺口 2):ov 定时任务清理 like_batches/visit_requests/visit_sessions 超窗口数据(LIKE_BATCH_WINDOW 默认 24h、VISIT_REQUEST_TTL 同窗口;累计总数长期保留);过期批次拒绝重放:likes 端点遇窗口外 batchId → 拒绝并返回当前权威 total(counted=false 语义),不得重新加计(测试:清理后重放旧 batchId 不加计)
  • 集成测试:A14 仿真级(窗口内刷新不重复、跨 requestId 重试幂等、新窗口再计、并发首访只加一次、过期重放拒绝)
  • Commit feat(ov): windowed dedup visit counting with retention cleanup

Task 9: 统计回传(ov→cn)

  • cn 副本列迁移(走校对手决流程):albums.view_count_replica + albums.stats_synced_at(photos.like_count 已存在作副本位)
  • ov 定时(60s)聚合 outbox_stat_events→stat-snapshots(HMAC)→cn 接收端(internal)事务替换 cn 侧 photos.like_count 副本/album view 副本(仅更高 statVersion 替换,绝不相加)+ 回执确认→ov 清理队列;中断重发幂等(statVersion 比较)
  • cn 管理列表(listAlbumPhotos/albums)展示副本计数 + statsSyncedAt(A27:不重复累计、最终一致、展示同步时间)
  • 集成测试:A27 仿真级(重复回传不加计、乱序快照旧版本忽略、最终一致断言)
  • Commit feat(sync): stat snapshot return path with version replacement

Task 10: viewer 互动化

  • 点赞 UI:大图/列表点击→本地动画+计数即时、批次收集(500ms 或 5 张一批)提交 {sessionId(会话随机), batchId, delta}→确认校准(total 权威);失败退避重试同 batchId;自身气泡过滤(originBatchIds 含己 batchId→抑制己部动画,他人照常)
  • 气泡:WS 连接(断线降级 8s 轮询列表页首校准 + likes 总数校准;不补播历史气泡);气泡含被赞照片小缩略图+点击定位;页面不可见暂停动画(visibilitychange);"减少动画"设置(localStorage,隐藏气泡仅保计数跳动)
  • 浏览上报:visitorId 跨标签共享(localStorage+BroadcastChannel 协调写、读回一致;不可用降级页面级随机)、页面可见且 album 加载成功后一次性 {visitorId, requestId(uuid 持久到 counted 确认为止)}、确认后更新显示
  • 热门/最新排序切换 UI(排序=快照语义:切换重新拉列表,当前浏览序列不变——对齐技术方案 §7)
  • Playwright:点赞累计+刷新校准、自身气泡不重复(同页点赞看他人模拟广播)、断线重连计数校准(断 WS→续)、排序切换、隐藏照片气泡/列表即时消失
  • Commit feat(viewer): likes, bubbles, visit reporting and hot sorting

Task 11: 全链路集成测试(同步+互动)

  • 场景套件(集成级,cn+ov+sync+worker 全起):①上传→发布→同步→ov 可见→点赞→回传→cn 副本一致(A25 本地直读语义+A27)②隐藏→高优先级同步→ov 消失+WS 推送隐藏③sender 停机 30s(积压)→恢复→追平+对账无缺④墓碑后旧事件重放不复活(A26)⑤紧急停用→ov 屏蔽→cn 无感⑥并发点赞/浏览压力小样(50 并发)幂等
  • Commit test(sync): cross-region simulation integration suite

Task 12: 回归门 + P2 验收记录

  • 全量回归绿(P1 套件不降级 + 新增);记录数字
  • docs/plans/progress/P2-验收记录.md(A09/A10/A14/A21/A26/A27 仿真级证据、D07/D11 实测建议值、偏差登记、P4a/P5 移交:真实地域部署/CDN/管理页同步状态展示/清理任务生产化)
  • 总纲 P2 行进度注记;git tag p2;Commit docs: p2 acceptance record

执行注意

  • 沿用 P1 全部工程约定(校对手决、reset 隔离、turbo concurrency=1、maxWorkers=1、契约先行+gen、Co-Authored-By trailer、偏差登记)。
  • ov 入口与 cn 入口同一 services/api 代码库:模块按部署角色组织(sync-inbox/likes/visits/ws 仅 ov 装配;auth/albums/uploads 仅 cn 装配)——app-ov.module.ts 显式挑选。
  • 双数据库连接:PrismaService(cn) 已全局;新增 OvPrismaService 独立 provider(非 global,按模块注入);测试 reset 区分两库。env 键名明确:DATABASE_URL_OV(mysql://...3307/photo_ov)、REDIS_URL_OV(redis://...6381)。
  • 偏差预登记(评审建议):ov 侧"确认资源可访问再设可见"的 head 探测在本地仿真不做(同 MinIO 必然可达),真实 OSS/CDN 探测随 G1/P5;cn 侧相册浏览数副本落新列 albums.view_count_replica(Task 9 迁移)。
  • 内部接口 HMAC:X-Sync-Signature: hmacsha256(body, SYNC_INTERNAL_KEY) + X-Sync-Timestamp(±300s 防重放);key 仅 env。
  • WS 与 HTTP 同端口(ov 8081);dev:sync / dev:ov 根脚本补齐。
  • 性能口径(P5 压测才硬性):本阶段并发小样仅验语义正确性,不做容量结论。