Files
photo-live/services/api/src/main-ov.ts
T
浅念 85b2da3db5 feat(ov): idempotent batched likes and realtime broadcast
P2 Task 7(点赞批次端点 + ov 实时网关):
- interactions 模块(仅 ov 装配):POST /public/albums/{token}/photos/
  {photoId}/likes——相册不可见/照片未发布 404、delta 1..20 与
  sessionId/batchId 1..64 校验 400;单事务 like_batches 唯一插入
  (P2002=幂等重试,batchAccepted=true 不重加)→ photos_ov.likeTotal/
  countVersion 原子自增回读 → outbox_stat_events(kind=likes) 兜底;
  Redis ov(REDIS_URL_OV)INCRBY+TTL 会话/IP 双窗口限流(默认
  500/2000,超限 429 RATE_LIMITED),幂等重试先于限流不耗额度
- 过期批次重放边界(24h 清理后的 batchId 重插会再次计入):接受该
  边界——无永久账本可查(保留全量批次行违背清理策略),会话/IP 窗口
  上限即重放天然封顶(同一 sessionId 重放挤占自身额度),偏差已在
  likes.service 头注释登记,Task 8 统计侧按清理后语义处理
- realtime:ContractWsAdapter(契约 {type,...} 帧分发,ws 与 HTTP 同
  端口挂 /v1/realtime)+ RealtimeGateway 相册房间模型——subscribe 逐
  相册校验 isAlbumVisibleOv,失败回 error 帧且连接保持;应用层 ping→
  pong 回显 ts;传输层 HEARTBEAT_MS 周期 ping 踢死连接;点赞提交经
  EventEmitter2(单进程,多实例 Redis pub/sub 登记 P5)触发
  likes.burst 500ms 合并窗(delta 聚合、originBatchIds 供客户端过滤
  自身批次);sync-inbox 应用事务提交后发 OV_PHOTO_APPLIED →
  photo.published/hidden/deleted 按房间广播(stale/duplicate/暂存/
  缺席即撤下不转发)
- 契约:REALTIME_WS_PATH、ServerMessage 增 error 帧(穷尽锁 7 成员
  + 类型测试)、LIKE_SESSION_WINDOW_DELTA/LIKE_IP_WINDOW_DELTA/
  LIKE_BURST_MERGE_MS 常量与 .env.example
- e2e(likes-realtime.e2e-spec.ts,6 用例):A09 三批累加/幂等重试/
  outbox 行、小额度 429、ping/pong+幽灵 token error 帧连接保持、
  A10 双房间隔离(先证非空调订)、合并窗单 burst(delta 求和+双
  originBatchIds)、cn 隐藏→房间收 photo.hidden+隐藏后点赞 404;
  凡进程内起 AppOvModule 的 spec(sync×2/public-ov/emergency)补装
  ContractWsAdapter 与生产 main-ov 一致——api e2e 168(162+6)

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

37 lines
2.0 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/main-ov.ts —— ov-app 部署入口(P2 Task 2 骨架)。与 main-cn
// 同库双入口:同一 services/api 代码库按部署角色装配(见 app-ov.module.ts)。
// 复用 configureApp(全局 v1 前缀 / x-powered-by 隐藏 / CORS)——契约「路径前缀
// 不变,部署域名区分」,ov 观众端(viewer :5175,P2 Task 10 切换 VITE_API_BASE
// 指向本入口)与 cn viewer 共用 CORS_ORIGINS env,无需 ov 专属配置。env 一律
// 来自进程环境(根 dotenv-cli 注入)。
// Task 7 起:WS 网关与 HTTP **同端口**(契约执行注意:WS 与 HTTP 同端口
// 8081)——ContractWsAdapter 按 realtime.ts 契约帧 {type,...} 分发,网关挂载
// /v1/realtime(REALTIME_WS_PATH),upgrade 请求经适配器路由到 noServer 的
// ws Server,普通 HTTP 路由不受影响。
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { type NestExpressApplication } from '@nestjs/platform-express';
import { Logger } from 'nestjs-pino';
import { AppOvModule } from './app-ov.module';
import { ContractWsAdapter } from './realtime/contract-ws.adapter';
import { configureApp } from './shared/configure-app';
async function bootstrap(): Promise<void> {
const app = await NestFactory.create<NestExpressApplication>(AppOvModule, {
bufferLogs: true,
// 内部 sync 端点(content-batches)HMAC 验签需要「原始请求体字节」:
// rawBody 让 body-parser 在 JSON 解析前把 Buffer 挂到 req.rawBody
// (InternalSyncGuard 读取)。发送器对同一字符串只签一次、原样发送。
rawBody: true,
});
app.useLogger(app.get(Logger));
// WS 与 HTTP 同端口:网关(@WebSocketGateway 无 port)挂到本应用 HTTP 服务。
app.useWebSocketAdapter(new ContractWsAdapter(app));
configureApp(app);
const port = Number.parseInt(process.env.PORT_OV ?? '', 10) || 8081;
await app.listen(port);
app.get(Logger).log(`api-ov listening on port ${port}`);
}
void bootstrap();