85b2da3db5
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>
111 lines
3.3 KiB
TypeScript
111 lines
3.3 KiB
TypeScript
// packages/contracts/src/realtime.ts
|
||
// WS 实时消息契约(P2,技术实施方案 §7):OpenAPI 不覆盖 WS 帧,本文件单独
|
||
// 维护,与 openapi.yaml 生成类型同属契约单一事实来源。消息一律为以 type 判别
|
||
// 的联合;新增/修改消息类型须同步 __tests__/realtime.test.ts 的穷尽断言。
|
||
|
||
/** 心跳间隔(毫秒):客户端周期发送 ping、服务端回 pong;超时无心跳的连接由服务端踢除。 */
|
||
export const HEARTBEAT_MS = 20000;
|
||
|
||
/**
|
||
* ov 入口 WS 网关挂载路径:与 HTTP 同端口(ov 8081),升级请求按本路径路由
|
||
* (services/api realtime 网关装饰器与 e2e/viewer 客户端共用,单一来源)。
|
||
*/
|
||
export const REALTIME_WS_PATH = '/v1/realtime';
|
||
|
||
// ── server → client ──
|
||
|
||
/** 照片发布:副本应用后向相册房间广播(ov 侧,下同)。 */
|
||
export interface PhotoPublishedMessage {
|
||
type: 'photo.published';
|
||
photoId: string;
|
||
albumId: string;
|
||
publishedAt: string; // ISO UTC
|
||
entityVersion: number;
|
||
}
|
||
|
||
/** 照片隐藏。 */
|
||
export interface PhotoHiddenMessage {
|
||
type: 'photo.hidden';
|
||
photoId: string;
|
||
albumId: string;
|
||
entityVersion: number;
|
||
}
|
||
|
||
/** 照片删除(软删除的公开语义等同隐藏,客户端行为一致:即刻移出列表)。 */
|
||
export interface PhotoDeletedMessage {
|
||
type: 'photo.deleted';
|
||
photoId: string;
|
||
albumId: string;
|
||
entityVersion: number;
|
||
}
|
||
|
||
/** 点赞合并广播:短窗内同照片批次合并为一次 burst;originBatchIds 供客户端过滤自身批次。 */
|
||
export interface LikesBurstMessage {
|
||
type: 'likes.burst';
|
||
photoId: string;
|
||
albumId: string;
|
||
delta: number;
|
||
total: number; // 服务端权威累计
|
||
countVersion: number;
|
||
burstId: string;
|
||
originBatchIds: string[]; // 合并进本 burst 的批次 ID(含己则抑制自身动画)
|
||
}
|
||
|
||
/** 相册浏览数更新。 */
|
||
export interface StatsUpdatedMessage {
|
||
type: 'stats.updated';
|
||
albumId: string;
|
||
viewCount: number;
|
||
countVersion: number;
|
||
}
|
||
|
||
/** 心跳应答:回显客户端 ping 携带的 ts。 */
|
||
export interface PongMessage {
|
||
type: 'pong';
|
||
ts: number;
|
||
}
|
||
|
||
/**
|
||
* 错误帧:客户端消息无法处理时的服务端应答(如 subscribe 的相册不存在/
|
||
* 不公开)。连接保持打开——客户端可修正后重发(如改订其它相册或等待恢复)。
|
||
*/
|
||
export interface RealtimeErrorMessage {
|
||
type: 'error';
|
||
/** 契约 ErrorCodes 域码(subscribe 校验失败 → ALBUM_NOT_PUBLIC 等)。 */
|
||
code: string;
|
||
/** 可读说明(可选,不承诺稳定性,客户端只应消费 code)。 */
|
||
message?: string;
|
||
}
|
||
|
||
export type ServerMessage =
|
||
| PhotoPublishedMessage
|
||
| PhotoHiddenMessage
|
||
| PhotoDeletedMessage
|
||
| LikesBurstMessage
|
||
| StatsUpdatedMessage
|
||
| PongMessage
|
||
| RealtimeErrorMessage;
|
||
|
||
// ── client → server ──
|
||
|
||
/** 订阅相册房间;服务端校验相册公开后加入。 */
|
||
export interface SubscribeMessage {
|
||
type: 'subscribe';
|
||
albumToken: string;
|
||
sinceVersion?: number; // 缺省从订阅时刻起推送,不补播历史
|
||
}
|
||
|
||
/** 退订相册房间。 */
|
||
export interface UnsubscribeMessage {
|
||
type: 'unsubscribe';
|
||
albumToken: string;
|
||
}
|
||
|
||
/** 心跳;服务端原样回 pong。 */
|
||
export interface PingMessage {
|
||
type: 'ping';
|
||
ts: number; // epoch 毫秒
|
||
}
|
||
|
||
export type ClientMessage = SubscribeMessage | UnsubscribeMessage | PingMessage;
|