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 名)。
- 方案 A:
- 读:按
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)保证跨分片唯一。
- 会话维度的 逻辑库分片(Shard N):
- 缓存层:热点会话放入 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)清单
- 登录/注册(邮箱 + 密码)+ JWT
- 联系人:搜索/加好友/同意/黑名单
- 单聊/群聊,会话列表
- 文本/表情/图片/文件消息(<100MB)
- 已读/输入中/在线状态
- 消息漫游与搜索(文本)
- Web Push 通知
- 管理后台(封禁/解封/审计查询)
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 初始化脚本 与 服务原型代码结构。
发表回复