实时团队邀请的架构与数据流

54 阅读5分钟

第二篇:实时团队邀请的架构与数据流

本篇目标:从业务正确性出发设计实时链路,理解为什么“消息是提醒,REST 才是账本”。

上一篇 · 返回目录 · 下一篇

1. 原始问题不是“缺一个 WebSocket”

Alice 提交 Bob 的邮箱,API 写入成员关系并向 Alice 返回成功,但 Bob 没有发请求,因此页面仍是旧列表。需求实际包含三件事:数据库可靠记录成员关系;Bob 在线时尽快知道;Bob 的页面最终与数据库一致。三件事不能全交给一条实时消息。

2. 三层职责

  • PostgreSQL 保存业务事实,当前邀请使用单条 INSERT 的原子性,并由外键和唯一约束兜底;
  • Socket.IO 发送 team.membership.created 变化提醒,优化感知速度;
  • REST 读取权威快照,Bob 收到事件后重新 GET /api/teams。
flowchart LR
    Alice[Alice 浏览器] -->|POST 邀请| API[NestJS API]
    API -->|保存成员关系| DB[(PostgreSQL)]
    API -->|成功后发布| RT[Socket.IO]
    RT -->|用户私有房间| Bob[Bob 浏览器]
    Bob -->|GET /api/teams| API
    API -->|权威快照| Bob

3. 完整时序

sequenceDiagram
    participant Alice as Alice 浏览器
    participant API as NestJS API
    participant DB as PostgreSQL
    participant Socket as Socket.IO
    participant Bob as Bob 浏览器
    Bob->>Socket: 登录后连接并加入 user:bobId
    Alice->>API: POST /api/teams/:teamId/members
    API->>DB: 检查权限、用户、已有成员
    API->>DB: 保存新成员关系
    DB-->>API: 保存成功
    API->>Socket: 发布 team.membership.created
    Socket-->>Bob: 推送事件
    API-->>Alice: 返回成员摘要
    Bob->>Bob: 显示通知
    Bob->>API: GET /api/teams
    API-->>Bob: 最新团队快照

必须先提交数据库再发事件。若顺序相反,Bob 回源可能查不到成员;保存失败时还会收到错误的成功通知。

这里的“先提交”是指 repository.save 对单条成员记录的写入成功;当前 addTeamMember 没有把前置查询、INSERT 和通知包在一个显式业务事务中。数据库唯一约束负责处理检查与写入之间的竞争窗口。

4. 先画出数据模型,避免把连接当成员关系

系统里至少有四种不同对象:

  • User:注册账号;
  • Team:团队;
  • TeamMember:用户与团队之间的持久关系,包含 role;
  • Socket:某个浏览器当前建立的临时连接。

一个 User 可以有多条 TeamMember,也可以同时有多个 Socket。关闭浏览器只会删除连接,不会删除成员关系;退出团队只会改变数据库关系,不代表必须注销账号。把这四个对象分清,很多设计错误会自然消失。

erDiagram
    USER ||--o{ TEAM_MEMBER : joins
    TEAM ||--o{ TEAM_MEMBER : contains
    USER ||--o{ SOCKET_CONNECTION : opens

5. 为什么事件名使用过去式

team.membership.created 表示“成员关系已经创建”,而不是一个待执行命令。只有新记录保存成功才发布。目标用户原本就是成员时,接口可以幂等返回已有记录,但不能再次发布 created。

合理的 payload 包含 eventId、occurredAt、team 摘要和 membership 摘要。eventId 用于去重,其他字段足以生成可读通知;完整团队与项目树仍由 REST 返回。

6. 在线、离线和重连

  • Bob 在线:事件到达,立即展示通知并刷新列表;
  • Bob 离线:当前 best-effort 方案不保存离线 toast,但下次登录的 GET /api/teams 会恢复业务状态;
  • Bob 重连:主动同步团队快照,补偿断线期间可能漏掉的事件。

7. 失败发生在不同位置时怎样处理

失败点数据库是否已有成员是否应该发布用户最终怎样恢复
权限或参数校验失败否否Alice 看到明确 HTTP 错误
INSERT 失败否否不显示成功,修复数据库问题
INSERT 成功、emit 同步异常是尝试过但未成功Bob 下次 GET 恢复,不能回滚成员
Bob 离线、空房间 emit是服务端已 best-effort 调用Bob 下次登录 GET 恢复
Bob 收到事件、GET 失败是已发送页面提示同步失败,可重试或刷新
事件重复到达是已发送eventId 去重,只需一次回源

这张表说明错误处理也必须服从“数据库事实优先”。实时层失败不应把成功写入变回失败,前端回源失败也不应伪装成用户没有加入团队。

8. 为什么不直接把 payload push 进数组

Socket.IO 会保持当前连接内已送达 packet 的顺序,但连接中断时事件可能缺失,payload 也不包含页面所需的所有字段。直接把 payload 当完整状态,无法修复漏失,也可能因业务层重复发送而重复插入。当前 eventId 去重属于防御性保护;若未来引入重试、Outbox 或多个消息来源,它会更重要。页面可以立即展示 toast,团队卡片则在 REST 返回后更新,这是有意的职责分离。

9. 当前一致性边界

当前方案保证数据库成功后才尝试发布、重复邀请不重复写入、已有成员不重复通知、漏掉消息仍能 REST 恢复。它不保证 emit 后浏览器一定收到、不保存离线通知,也不处理跨 API 实例房间共享和数据库与消息的原子提交。

若要求每条通知必达且可追踪,需要 Outbox、消息队列、消费确认和持久化通知中心;作品 Demo 没有盲目引入这些复杂度。

10. 常见错误设计

  1. 让邀请者浏览器直接向 Bob 发消息:浏览器不应决定他人权限;
  2. 写库失败仍发送成功事件:事件必须位于成功分支;
  3. Bob 离线就不能邀请:在线状态不应阻止业务事实写入;
  4. 只在首次 connect 拉数据:重连后必须补一次快照。

11. 本篇小结

实时邀请不是用 Socket.IO 替代 REST,而是在 REST 业务链路外增加低延迟提醒:先写数据库,后发事件;事件让界面回源,REST 返回最终状态。

下一篇:NestJS + Socket.IO 鉴权、用户房间与幂等邀请