WebQQ 2.0(Web 版即时通讯)——设计文档与数据库全解析(2025)

目标:在现代 Web 技术栈上复刻「WebQQ 2.0」的核心体验,支持单聊、群聊、文件与图片传输、离线消息、消息漫游、搜索、好友/群管理、状态与通知,并具备可水平扩展的云原生后端。


1. 范围与定位

  • 核心体验
    • 实时聊天(单聊/群聊)、富文本、表情包、图片/文件、语音(可选 WebRTC)。
    • 设备多端登录(Web/PWA/桌面容器),消息漫游与多端同步。
    • 好友体系(申请/同意/黑名单)、群体系(公告、禁言、成员角色)。
    • 在线/离线/忙碌(Presence)与输入中/已读(Typing/Read)。
    • 全局搜索(联系人、群、消息内容)。
  • 非目标(首版):端到端加密(E2EE)默认不开;音视频会议与直播;开放平台。

2. 主要角色与使用场景

  • 普通用户:添加好友、创建群、发送消息、跨设备同步。
  • 群管理员/所有者:群成员管理、公告、全员禁言、拉黑。
  • 运营/客服(内控):风控与申诉(需留审计痕迹,受合规控制)。

3. 总体架构

┌─────────────── Client (Web/PWA/Desktop) ───────────────┐
│ React + TS + Service Worker + IndexedDB + WebSocket     │
└───────────────↑───────────────↑───────────────↑─────────┘
                 │               │               │
         REST/HTTPS (OpenAPI)   WebSocket       Upload
                 │               │               │
        ┌────────┴───────┐   ┌──┴─────────┐   ┌──┴─────────┐
        │  API Gateway   │   │  WS Gateway │   │  Media CDN │
        └──────┬─────────┘   └──────┬──────┘   └──────┬──────┘
               │                   │                  │
      ┌────────┴────────┐   ┌──────┴────────┐        │
      │ Auth Service    │   │ IM Service    │        │
      │ User/Profile    │   │ Presence/Typing│       │
      │ Contacts/Blocks │   │ Delivery/Receipt│      │
      └──┬──────────────┘   └──────┬─────────┘        │
         │                         │                  │
     ┌───┴────┐   ┌───────────┐    │      ┌──────────┴─────────┐
     │ RDBMS  │   │ Redis     │<───┘      │  Object Storage(S3) │
     │(Postgres)   (sessions, presence)   │  + Virus Scan       │
     └───┬────┘   └───────────┘           └──────────┬──────────┘
         │                 ┌──────────────────────────┘
         │                 │
     ┌───┴────┐   ┌───────┴─────────┐
     │ Kafka  │←→ │  Search Index   │ (Elasticsearch/OpenSearch)
     └────────┘   └──────────────────┘
  • API Gateway:鉴权、速率限制、日志;面向 REST/GraphQL。
  • WS Gateway:与客户端保持长连接,负责连接分发、心跳、粘性路由。
  • IM Service:会话、消息、投递、回执、已读、群管理。
  • Auth/User/Contacts:登录注册、资料、好友、黑名单。
  • Presence/Typing:Redis 发布/订阅 + 过期策略。
  • Media:直传对象存储,后台异步生成缩略图、做病毒扫描、鉴黄(可选)。
  • Search:异步索引消息文本、群公告、用户昵称等。

4. 关键设计抉择

  • 消息投递语义:客户端到服务端 at-least-once;服务端去重(幂等键 client_msg_id)。
  • 顺序保证:以 会话维度(conversation_id) 的单调递增序列号 seq 保序;不同会话间不保证全局顺序。
  • ID 生成:Snowflake/Twitter-64(时间 + 机房 + 节点 + 自增)。
  • 多端同步:服务端保存消息(漫游),客户端维护 per-conversation 的 last_ack_seq
  • 离线推送:浏览器推送(Web Push)、邮件/短信(可选)。
  • 搜索一致性:主存放在 Postgres;消息异步写入 Kafka → 索引。

5. 客户端离线与缓存

  • IndexedDB:存储最近 N 天会话与媒体缩略图;启动时与服务端进行 增量同步(基于 seq)。
  • Service Worker:接收 Web Push、离线页面、消息草稿缓存。
  • 端态合并:本地未发送消息与服务端状态合并,靠 client_msg_id 去重。

6. REST/WS 协议(节选)

6.1 认证

  • POST /v1/auth/login → { token, refresh_token, devices[] }
  • WebSocket 连接参数:wss://ws.example.com?token=JWT

6.2 会话(Conversations)

  • POST /v1/conversations (单聊/群聊创建)
  • GET /v1/conversations?after=<cursor>&limit=50
  • GET /v1/conversations/{id}

6.3 消息(Messages)

  • REST 发送(备选):POST /v1/messages
  • WebSocket 发送:
{
  "type": "msg/send",
  "conversation_id": "snowflake",
  "client_msg_id": "uuid4",
  "payload": {"kind":"text","text":"hello"},
  "mentions": ["uid1","uid2"],
  "quoted_msg_id": "optional"
}
  • 服务端下行:
{
  "type": "msg/notify",
  "conversation_id": "...",
  "msg_id": "snowflake",
  "seq": 1024,
  "sender": "u123",
  "ts": 1734839020000,
  "payload": {"kind":"image","url":"https://cdn/...","w":800,"h":600}
}

6.4 回执与已读

  • 上行:{"type":"receipt/ack","conversation_id":"...","msg_id":"..."}
  • 已读:{"type":"read/upsert","conversation_id":"...","seq":1024}(表示读到 1024)

6.5 Presence & Typing

  • 上行:{"type":"presence/set","status":"online|away|dnd|offline"}
  • Typing:{"type":"typing","conversation_id":"...","on":true}(10s 过期)

7. 数据库模型(关系库:PostgreSQL)

7.1 概念实体

  • users 用户
  • devices 设备(多端信息)
  • contacts 好友关系(双向)与 friend_requests
  • blocks 拉黑
  • conversations 会话(单聊/群聊)
  • participants 会话参与者(角色、已读游标)
  • messages 消息(Append-Only)
  • message_attachments 附件/媒体元数据
  • reactions 表情回应
  • read_receipts(可合并到 participants 的 last_read_seq
  • groups/group_members(也可用 conversations + participants 表达)
  • audit_logs 审计(内控)

7.2 关系与主键(ER 文字示意)

users (user_id PK) 1—* devices
users 1—* friend_requests  *—1 users (to_user)
users *—* users via contacts (composite PK: min(user_id), max(user_id))
conversations (conv_id PK) 1—* participants (conv_id, user_id PK)
conversations 1—* messages (msg_id PK, conv_id FK, seq UNIQUE per conv)
messages 1—* message_attachments
messages 1—* reactions
participants: last_read_seq, mute, role

7.3 DDL(简化)

CREATE EXTENSION IF NOT EXISTS pgcrypto; -- for gen_random_uuid

-- 用户与设备
CREATE TABLE users (
  user_id      BIGINT PRIMARY KEY,
  username     VARCHAR(32) UNIQUE NOT NULL,
  display_name VARCHAR(64) NOT NULL,
  avatar_url   TEXT,
  email        CITEXT UNIQUE,
  phone        VARCHAR(32) UNIQUE,
  password_hash TEXT NOT NULL,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  status       SMALLINT NOT NULL DEFAULT 1 -- 1 normal, 0 banned
);

CREATE TABLE devices (
  device_id    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id      BIGINT NOT NULL REFERENCES users(user_id),
  platform     VARCHAR(16) NOT NULL, -- web/pwa/desktop
  push_token   TEXT,
  last_ip      INET,
  last_seen_at TIMESTAMPTZ,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- 好友与黑名单
CREATE TABLE friend_requests (
  req_id     BIGINT PRIMARY KEY,
  from_user  BIGINT NOT NULL REFERENCES users(user_id),
  to_user    BIGINT NOT NULL REFERENCES users(user_id),
  remark     TEXT,
  status     SMALLINT NOT NULL DEFAULT 0, -- 0 pending, 1 accept, 2 reject
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (from_user, to_user)
);

CREATE TABLE contacts (
  user_a BIGINT NOT NULL REFERENCES users(user_id),
  user_b BIGINT NOT NULL REFERENCES users(user_id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (user_a, user_b),
  CHECK (user_a < user_b)
);

CREATE TABLE blocks (
  blocker BIGINT NOT NULL REFERENCES users(user_id),
  blocked BIGINT NOT NULL REFERENCES users(user_id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (blocker, blocked)
);

-- 会话与成员
CREATE TABLE conversations (
  conv_id     BIGINT PRIMARY KEY,
  kind        SMALLINT NOT NULL, -- 1 direct, 2 group
  title       VARCHAR(128),
  owner_id    BIGINT REFERENCES users(user_id),
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE participants (
  conv_id   BIGINT NOT NULL REFERENCES conversations(conv_id) ON DELETE CASCADE,
  user_id   BIGINT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
  role      SMALLINT NOT NULL DEFAULT 0, -- 0 member, 1 admin, 2 owner
  mute      BOOLEAN NOT NULL DEFAULT false,
  last_read_seq BIGINT NOT NULL DEFAULT 0,
  joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (conv_id, user_id)
);

-- 消息(按会话分区)
CREATE TABLE messages (
  msg_id        BIGINT PRIMARY KEY,
  conv_id       BIGINT NOT NULL REFERENCES conversations(conv_id) ON DELETE CASCADE,
  seq           BIGINT NOT NULL, -- per-conversation sequence
  sender_id     BIGINT NOT NULL REFERENCES users(user_id),
  client_msg_id UUID NOT NULL,
  kind          SMALLINT NOT NULL, -- 1 text,2 image,3 file,4 system
  content       JSONB NOT NULL,    -- {text:"..."} or {url:"...", w:...,h:...}
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now(),
  edited_at     TIMESTAMPTZ,
  deleted       BOOLEAN NOT NULL DEFAULT false,
  UNIQUE (conv_id, seq),
  UNIQUE (conv_id, sender_id, client_msg_id)
) PARTITION BY HASH (conv_id);

-- 建 8 个分区示例
CREATE TABLE messages_p0 PARTITION OF messages FOR VALUES WITH (MODULUS 8, REMAINDER 0);
CREATE TABLE messages_p1 PARTITION OF messages FOR VALUES WITH (MODULUS 8, REMAINDER 1);
-- ... p2..p7 省略

CREATE TABLE message_attachments (
  id        BIGINT PRIMARY KEY,
  msg_id    BIGINT NOT NULL REFERENCES messages(msg_id) ON DELETE CASCADE,
  url       TEXT NOT NULL,
  size      BIGINT,
  mime      VARCHAR(64),
  width     INT,
  height    INT
);

CREATE TABLE reactions (
  msg_id   BIGINT NOT NULL REFERENCES messages(msg_id) ON DELETE CASCADE,
  user_id  BIGINT NOT NULL REFERENCES users(user_id),
  emoji    VARCHAR(32) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (msg_id, user_id, emoji)
);

CREATE TABLE audit_logs (
  id BIGINT PRIMARY KEY,
  actor BIGINT,
  action VARCHAR(64),
  target VARCHAR(64),
  payload JSONB,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

7.4 索引建议

CREATE INDEX idx_messages_conv_seq ON messages USING btree (conv_id, seq);
CREATE INDEX idx_messages_sender_time ON messages (sender_id, created_at);
CREATE INDEX idx_participants_user ON participants (user_id);
CREATE INDEX idx_friend_requests_to ON friend_requests (to_user, status);

7.5 读写路径与一致性

  • 写入messages 采用按 conv_id 哈希分区;seq 使用「查询当前最大 + 1」会有竞争,改为:
    • 方案 A:participants 维护每个会话的 next_seq(行级锁);
    • 方案 B:Postgres SEQUENCE per-conversation(可在创建会话时创建一个 sequence 名)。
  • :按 conv_id + seq > last_read_seq 拉取增量;分页基于 seq 或 msg_id 游标。

8. 消息流转(序列图)

8.1 发送文本消息

Client → WS Gateway: msg/send(conversation_id, client_msg_id, payload)
WS Gateway → IM Service: route by conversation_id
IM Service: idempotency check(client_msg_id); assign seq; persist(messages)
IM Service → Search Sink: Kafka produce(index doc)
IM Service → WS Gateway: msg/notify to all participants
WS Gateway → Online Participants: deliver
Client → WS Gateway: receipt/ack(msg_id)
IM Service: update last_delivery(optional)

8.2 离线同步

Client → REST: GET /v1/messages?conv_id=...&after_seq=S&limit=50
Server → Client: list(messages seq>S)
Client: update local IndexedDB; send read/upsert(seq=max)

9. 扩展性与分片

  • 水平扩展:WS Gateway 无状态(粘性在 conv_id);IM Service 可按 conv_id 一致性哈希到不同分片。
  • 数据库分片
    • 首期:单集群 + 分区表;
    • 成长期:
      • 会话维度的 逻辑库分片(Shard N):conv_id % N
      • 全局 ID 服务(Snowflake)保证跨分片唯一。
  • 缓存层:热点会话放入 Redis;成员列表、会话元数据 TTL 30s。

10. 媒体与安全

  • 对象存储直传:前端向后端请求带签名的上传 URL(Pre-signed URL),上传后回调。
  • 内容安全:病毒扫描(clamav)、鉴黄/涉政/涉暴模型(可选);违规自动撤回并记审计。
  • 防滥用:速率限制(IP、用户、会话维度);验证码;反刷策略;黑名单优先级匹配。
  • 隐私合规
    • 可导出/删除个人数据(GDPR/CCPA);
    • 数据最小化与加密(静态加密 KMS、传输 TLS);
    • 审计留痕。

11. 端到端加密(可选设计)

  • 密钥:Double Ratchet/Olm;每设备持有设备公钥;服务器只转发密文与密钥包。
  • 落地
    • 单聊:每对设备建立会话密钥;群聊:Sender Keys(群共享一次性会话密钥)。
    • 服务器仅存密文;搜索需改为 本地索引 或基于加密索引(复杂)。
  • 权衡:E2EE 将影响审计与功能(撤回、搜索、风控)。

12. 搜索与索引

  • 数据源:消息写入成功后异步写 Kafka;Search 服务消费 → 写 ES/OpenSearch。
  • 索引结构(示例):
{ "msg_id":"...","conv_id":"...","sender":"u1","ts":1734839,
  "text":"hello world","mentions":["u2"],"type":"text" }
  • 查询:按用户可见范围过滤(基于 participants 进行 ACL 过滤)。

13. 监控与可观测性

  • 指标:WS 连接数、投递延迟 P50/P95、消息写入 QPS、Kafka lag、错误率。
  • 日志:结构化 JSON;追踪 trace_id(从客户端透传)。
  • 告警:队列拥塞、分片失衡、数据库分区膨胀、失败率激增。

14. 迁移与灰度

  • Feature flag 控滚动发布;
  • 双写期:老索引 + 新索引;
  • 会话级灰度:按 conv_id % 100 < 5 引流到新分片。

15. 性能预算(首版目标)

  • 并发连接:单 WS 节点 50k(内核参数 + epoll);
  • 消息延迟:端到端 < 200ms(同区域);
  • 持久化:Postgres 单分区 5k msg/s;通过 8 分区线性扩容。

16. 最小可行版本(MVP)清单

  1. 登录/注册(邮箱 + 密码)+ JWT
  2. 联系人:搜索/加好友/同意/黑名单
  3. 单聊/群聊,会话列表
  4. 文本/表情/图片/文件消息(<100MB)
  5. 已读/输入中/在线状态
  6. 消息漫游与搜索(文本)
  7. Web Push 通知
  8. 管理后台(封禁/解封/审计查询)

17. 风险与对策

  • 热点群:写入放大与风暴——分片内再分区(队列分桶)、批量推送。
  • 超大附件:限速与断点续传、清理过期副本、CDN 缓存策略。
  • 滥用与攻击:登录与消息层限流、IP 黑名单、验证码与行为识别、WAF。
  • 合规:数据主权(区域化部署)、内容审查策略、用户申诉通道。

18. API 示例(OpenAPI 片段)

openapi: 3.0.3
info:
  title: WebQQ 2.0 API
  version: 1.0.0
paths:
  /v1/auth/login:
    post:
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                username: {type: string}
                password: {type: string}
      responses:
        '200': {description: OK}
  /v1/users/search:
    get:
      parameters:
        - in: query
          name: q
          schema: {type: string}
      responses:
        '200': {description: OK}

19. 开发计划(里程碑)

  • M1(2 周):用户体系 + WS 骨架 + 单聊消息存储/下发
  • M2(3 周):群聊、已读、输入中、图片/文件上传
  • M3(2 周):搜索接入(Kafka → OpenSearch)、Web Push
  • M4(2 周):管理后台 + 审计 + 监控
  • M5(持续):性能优化、分片、容灾演练

附:测试用例要点

  • 幂等:重复提交 client_msg_id 不产生新消息
  • 顺序:同会话多并发发送,seq 单调不减
  • 同步:断网后重连增量拉取正确
  • 安全:被拉黑双方不可互发,新消息不下发
  • 上传:超大文件断点续传与校验

以上设计既可用单体实现(Spring Boot + Postgres + Redis),也能平滑演进到微服务与多分片。若需要,我可以基于本文档输出 SQL 初始化脚本 与 服务原型代码结构