多租户更改

This commit is contained in:
richjohnrambo
2026-08-11 15:59:57 +08:00
parent 6c800de67d
commit c2076721fc
453 changed files with 102349 additions and 47 deletions
+749
View File
@@ -0,0 +1,749 @@
# Tinode 话题系统详解
> Tinode 的核心设计理念:**一切皆话题 (Topic-Oriented)**
## 概述
在 Tinode 中,私聊、群聊、个人信息、搜索等功能都通过"话题 (Topic)"来实现。话题是一个隔离的通信通道,不同话题之间互不干扰。
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Tinode 话题架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【通信类话题】- 可以发送消息 │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ p2p 私聊 │ │ grp 群聊 │ │ slf 笔记 │ │
│ │ p2pXXX │ │ grpXXX │ │ slfXXX │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
│ 【系统类话题】- 特殊用途 │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ me 个人 │ │ fnd 搜索 │ │ sys 系统 │ │
│ │ usrXXX │ │ fnd │ │ sys │ │
│ └───────────────┘ └───────────────┘ └───────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 话题类型
### 类型定义
```go
// server/store/types/types.go
type TopicCat int
const (
TopicCatMe TopicCat = iota // 'me' 话题 - 用户个人信息
TopicCatFnd // 'fnd' 话题 - 用户搜索
TopicCatP2P // 'p2p' 话题 - 私聊 (点对点)
TopicCatGrp // 'grp' 话题 - 群聊
TopicCatSys // 'sys' 话题 - 系统话题
TopicCatSlf // 'slf' 话题 - 自我话题 (笔记/收藏)
)
```
### 命名规则
```go
func GetTopicCat(name string) TopicCat {
switch name[:3] {
case "usr": return TopicCatMe // usrABC123 → me 话题
case "p2p": return TopicCatP2P // p2pABC... → 私聊
case "grp": return TopicCatGrp // grpABC... → 群聊
case "chn": return TopicCatGrp // chnABC... → 频道 (也是群聊)
case "fnd": return TopicCatFnd // fnd → 搜索话题
case "sys": return TopicCatSys // sys → 系统话题
case "slf": return TopicCatSlf // slfABC... → 自我话题
}
}
```
---
## 六种话题详解
### 1. P2P 私聊话题 (`p2p`)
**命名格式**: `p2p` + 两用户ID排序拼接
**示例**: Alice (usrA) 和 Bob (usrB) 的私聊话题名为 `p2pusrAusrB`
```
用户 Alice (usrA) 想给用户 Bob (usrB) 发消息
┌─────────┐ ┌─────────┐
│ Alice │ │ Bob │
│ usrA │ │ usrB │
└────┬────┘ └────┬────┘
│ │
│ 订阅 p2pusrAusrB │ 订阅 p2pusrAusrB
│ │
│ ┌───────────────────────┴────────────────┐
│ │ │
│ │ P2P Topic: p2pusrAusrB │
│ │ │
│ │ perUser: │
│ │ usrA → {modeWant, modeGiven} │
│ │ usrB → {modeWant, modeGiven} │
│ │ │
│ │ messages: [msg1, msg2, ...] │
│ │ │
│ └────────────────────────────────────────┘
│ │
└──────── 发送消息 ───────────────┘
↓
广播给所有订阅者
(Alice 和 Bob 都能收到)
```
**关键特性**:
| 特性 | 说明 |
|------|------|
| 自动创建 | 当 Alice 给 Bob 发第一条消息时自动创建 |
| 双向订阅 | 两个用户都是订阅者,权限对等 |
| 自动删除 | 当两个用户都取消订阅时,话题自动删除 |
| 消息存储 | 存储两个人的聊天记录 |
| 无管理员 | 没有管理员概念,两人权限相同 |
**创建流程**:
```
1. Alice 发送订阅请求 {sub: {topic: "usrB"}}
(注意:订阅的是 Bob 的用户ID,不是 p2p 话题名)
2. 服务器检测到这是 P2P 订阅
- 解析出目标用户: usrB
- 生成话题名: p2pusrAusrB (排序后)
3. 检查话题是否存在
- 不存在 → 创建新话题
- 存在 → 直接订阅
4. 通知 Bob 有新的订阅请求
- 如果 Bob 在线,推送 {pres: {what: "knock"}}
- Bob 可以选择订阅或忽略
5. Alice 发送消息 → 广播给 Alice 和 Bob
```
---
### 2. GRP 群聊话题 (`grp` / `chn`)
**命名格式**: `grp` 或 `chn` + 唯一ID
**示例**: `grpTechGroup123`, `chnAnnouncements456`
```
群组: "技术交流群"
话题名: grpTechGroup123
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Alice │ │ Bob │ │ Carol │ │ Dave │
│ (创建者) │ │ (管理员) │ │ (成员) │ │ (成员) │
└────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘
│ │ │ │
│ │ │ │
└────────────┴────────────┴────────────┘
│
▼
┌───────────────────────────────────────┐
│ GRP Topic: grpTechGroup123 │
│ │
│ owner: usrA (Alice) │
│ │
│ perUser: │
│ usrA → {owner, admin, write} │
│ usrB → {admin, write} │
│ usrC → {write} │
│ usrD → {write} │
│ │
│ messages: [msg1, msg2, ...] │
│ │
│ tags: ["技术", "编程", "Go"] │
│ │
│ public: { │
│ "name": "技术交流群", │
│ "avatar": "group.jpg" │
│ } │
└───────────────────────────────────────┘
```
**关键特性**:
| 特性 | 说明 |
|------|------|
| 手动创建 | 需要显式创建,设置群名、头像等 |
| 多人订阅 | 可以有很多成员,无上限 |
| 权限管理 | 拥有者 > 管理员 > 普通成员 > 访客 |
| 持久化 | 不会因为用户离开而删除 |
| 可搜索 | 通过 tags 可以被搜索到 |
| 邀请机制 | 可以邀请他人加入 |
**权限层级**:
```
┌─────────────────────────────────────────────────────┐
│ 群组权限层级 │
├─────────────────────────────────────────────────────┤
│ │
│ Owner (拥有者) │
│ ├── 可以删除群组 │
│ ├── 可以转让拥有者 │
│ ├── 可以任命/撤销管理员 │
│ └── 拥有所有权限 │
│ │
│ Admin (管理员) │
│ ├── 可以修改群信息 │
│ ├── 可以踢出成员 │
│ ├── 可以邀请新成员 │
│ └── 可以设置成员权限 │
│ │
│ Member (普通成员) │
│ ├── 可以发送消息 │
│ ├── 可以查看群信息 │
│ └── 可以主动退出 │
│ │
│ Guest (访客) │
│ ├── 只能接收消息 │
│ └── 不能发送 │
│ │
└─────────────────────────────────────────────────────┘
```
**频道模式 (chn)**:
`chn` 前缀表示频道,是群聊的一种特殊形式:
```
┌─────────────────────────────────────────────────────┐
│ 频道 vs 群聊 │
├─────────────────────────────────────────────────────┤
│ │
│ 群聊 (grp) │
│ ├── 所有成员都能发言 │
│ ├── 消息对所有人可见 │
│ └── 适合团队协作 │
│ │
│ 频道 (chn) │
│ ├── 只有管理员能发言 │
│ ├── 类似微信公众号 │
│ ├── 订阅者只能接收 │
│ └── 适合公告、广播 │
│ │
└─────────────────────────────────────────────────────┘
```
---
### 3. ME 个人话题 (`usr`)
**命名格式**: `usr` + 用户ID (就是用户自己的ID)
**示例**: 用户 Alice 的 ID 是 `usrABC123`,她的 me 话题名也是 `usrABC123`
```
用户: Alice
话题名: usrABC123 (用户自己的 ID)
┌─────────────────────────────────────────────────────────────┐
│ ME Topic: usrABC123 │
│ │
│ 这是用户的"影子"话题,不存储消息,只管理: │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ perSubs (联系人列表) │ │
│ │ │ │
│ │ p2pusrAusrB → {unread: 5, lastSeen: ...} │ │
│ │ grpTechGroup → {unread: 12, lastSeen: ...} │ │
│ │ p2pusrAusrC → {unread: 0, lastSeen: ...} │ │
│ │ │ │
│ │ 这些是 Alice 订阅的所有话题的引用 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 功能: │
│ - 管理联系人列表 │
│ - 推送在线状态 (pres: {what: "online"/"offline"}) │
│ - 设备管理 (多设备登录) │
│ - 收到新消息的推送 │
└─────────────────────────────────────────────────────────────┘
```
**关键特性**:
| 特性 | 说明 |
|------|------|
| 临时话题 | 不存储在 topics 表中,是用户 ID 的引用 |
| 不能发消息 | 只用于管理,不能发送聊天消息 |
| 联系人管理 | 管理用户订阅的所有话题 |
| 在线状态 | 推送 "online" / "offline" 状态变化 |
| 设备信息 | 管理多设备登录 |
| 单用户 | 只有用户自己订阅 |
**联系人列表结构**:
```javascript
// 用户订阅 me 话题后,会收到联系人列表
{
"ctrl": {
"params": {
"sub": [
{
"topic": "p2pusrAusrB",
"unread": 5,
"lastSeen": "2024-01-15T10:30:00Z"
},
{
"topic": "grpTechGroup",
"unread": 12,
"lastSeen": "2024-01-15T09:00:00Z"
}
]
}
}
}
```
---
### 4. FND 搜索话题 (`fnd`)
**命名格式**: 固定为 `fnd`
```
用户 Alice 想搜索其他用户
Alice 发送请求:
{
"sub": {
"topic": "fnd",
"set": {
"desc": {
"public": {"tags": ["Go", "程序员"]}
}
},
"get": {
"what": "desc",
"desc": {"limit": 20}
}
}
}
│
▼
┌─────────────────────────────────────────────────────────────┐
│ FND Topic: fnd │
│ │
│ 根据用户设置的搜索条件,返回匹配的用户列表 │
│ │
│ 搜索逻辑: │
│ 1. Alice 设置 tags: ["Go", "程序员"] │
│ 2. 服务器搜索所有包含这些 tags 的用户 │
│ 3. 返回匹配的用户列表 │
│ │
│ 返回: │
│ - usrB (Bob) - tags: ["Go", "后端"] │
│ - usrC (Carol) - tags: ["程序员", "Java"] │
│ - usrD (Dave) - tags: ["Go", "架构师"] │
└─────────────────────────────────────────────────────────────┘
```
**关键特性**:
| 特性 | 说明 |
|------|------|
| 搜索专用 | 用于搜索其他用户 |
| 基于标签 | 通过 tags 进行搜索匹配 |
| 单用户 | 只有搜索者自己订阅 |
| 不持久化 | 搜索结果不存储 |
**搜索请求示例**:
```javascript
// 订阅 fnd 话题并设置搜索条件
{
"sub": {
"id": "search123",
"topic": "fnd",
"set": {
"desc": {
"public": {
"tags": ["Go", "后端", "架构师"]
}
}
},
"get": {
"what": "desc"
}
}
}
// 服务器返回匹配的用户
{
"pres": {
"topic": "fnd",
"what": "acs"
},
"desc": {
"topic": "usrB",
"public": {
"fn": "Bob",
"tags": ["Go", "后端"]
}
}
}
```
---
### 5. SLF 自我话题 (`slf`)
**命名格式**: `slf` + 唯一ID
**示例**: `slfNotes123`
```
用户: Alice
话题名: slfNotes123
┌─────────────────────────────────────────────────────────────┐
│ SLF Topic: slfNotes123 │
│ │
│ 这是"给自己发消息"的话题 │
│ │
│ 用途: │
│ - 草稿箱 │
│ - 收藏的消息 │
│ - 个人备忘录 │
│ - 跨设备同步的笔记 │
│ │
│ 特点: │
│ - 只有自己一个订阅者 │
│ - 可以发消息给自己 │
│ - 消息只有自己能看到 │
└─────────────────────────────────────────────────────────────┘
```
**关键特性**:
| 特性 | 说明 |
|------|------|
| 个人笔记 | 给自己发消息 |
| 跨设备同步 | 手机、电脑都能看到 |
| 收藏功能 | 可以收藏其他话题的消息 |
| 私密 | 只有自己能看到 |
**使用场景**:
```javascript
// 保存一条笔记
{
"pub": {
"id": "note123",
"topic": "slfNotes123",
"content": "明天开会,记得带资料"
}
}
// 收藏一条消息到笔记
// 1. 先复制消息内容
// 2. 发送到 slf 话题
{
"pub": {
"id": "note456",
"topic": "slfNotes123",
"content": {
"from": "grpTechGroup",
"text": "重要的技术讨论记录..."
}
}
}
```
---
### 6. SYS 系统话题 (`sys`)
**命名格式**: 固定为 `sys`
```
系统全局广播
┌─────────────────────────────────────────────────────────────┐
│ SYS Topic: sys │
│ │
│ 用于系统级通知 │
│ │
│ 用途: │
│ - 系统公告 │
│ - 维护通知 │
│ - 版本更新 │
│ - 紧急通知 │
│ │
│ 特点: │
│ - 只有 root 用户可以发送 │
│ - 广播给所有用户 │
│ - 强制推送 │
└─────────────────────────────────────────────────────────────┘
```
**关键特性**:
| 特性 | 说明 |
|------|------|
| 系统广播 | 发送系统级通知 |
| Root 专用 | 只有 root 用户可以发送 |
| 全员推送 | 广播给所有用户 |
| 不可取消 | 用户不能取消订阅 |
---
## 话题类型对比
| 话题类型 | 命名前缀 | 能发消息 | 持久化 | 订阅人数 | 用途 |
|---------|---------|---------|--------|---------|------|
| **P2P** | `p2p` | ✅ | ✅ | 2人 | 私聊 |
| **GRP** | `grp`/`chn` | ✅ | ✅ | 多人 | 群聊/频道 |
| **SLF** | `slf` | ✅ | ✅ | 1人 | 笔记/收藏 |
| **ME** | `usr` | ❌ | ❌ | 1人 | 个人信息/联系人 |
| **FND** | `fnd` | ❌ | ❌ | 1人 | 搜索用户 |
| **SYS** | `sys` | ✅ | ✅ | 所有人 | 系统广播 |
---
## 私聊 vs 群聊
### 核心差异
```
私聊 (P2P):
┌─────────────────────────────────────────────────────┐
│ p2pusrAusrB │
│ │
│ • 订阅者固定为2人 (usrA + usrB) │
│ • 任一人发消息,另一人必收 │
│ • 最后一人取消订阅 → 话题删除 │
│ • 没有管理员概念,权限对等 │
│ • 订阅即创建,无需手动创建 │
│ • 适合私密对话 │
└─────────────────────────────────────────────────────┘
群聊 (GRP):
┌─────────────────────────────────────────────────────┐
│ grpTechGroup123 │
│ │
│ • 订阅者可以是任意多人 │
│ • 有创建者、管理员、普通成员之分 │
│ • 创建者离开不会删除话题 │
│ • 可以踢人、禁言、设置权限 │
│ • 需要显式创建 │
│ • 适合团队协作、社区交流 │
└─────────────────────────────────────────────────────┘
```
### 创建流程对比
**私聊创建**:
```
1. Alice 发送: {sub: {topic: "usrB"}}
2. 服务器:
- 解析目标: usrB
- 生成话题名: p2pusrAusrB
- 检查是否存在
- 不存在则创建
3. Alice 订阅成功
4. Bob 收到通知: {pres: {what: "knock"}}
5. Bob 订阅: {sub: {topic: "usrA"}}
6. 双方可以发消息
```
**群聊创建**:
```
1. Alice 发送: {
acc: {
"user": "new", // 创建新话题
"scheme": "basic",
"secret": "群密码",
"desc": {
"public": {
"fn": "技术交流群",
"note": "讨论技术问题"
},
"defacs": {
"auth": "JRWS", // 默认权限
"anon": "N"
}
}
}
}
2. 服务器:
- 创建群话题: grpXXX
- Alice 自动成为拥有者
- 初始化群信息
3. Alice 可以邀请他人加入
```
---
## 话题核心数据结构
```go
// server/topic.go
type Topic struct {
// 话题名称
name string
// 话题类型
cat types.TopicCat
// 创建/更新时间
created time.Time
updated time.Time
// 最后消息ID
lastID int
// 拥有者
owner types.Uid
// 默认访问权限
accessAuth types.AccessMode // 认证用户默认权限
accessAnon types.AccessMode // 匿名用户默认权限
// 发现标签
tags []string
// 扩展数据
aux map[string]any
// 公开数据 (群名、头像等)
public any
trusted any
// 订阅者数据
perUser map[types.Uid]perUserData
// 会话列表
sessions map[*Session]perSessionData
// 消息通道
clientMsg chan *ClientComMessage
serverMsg chan *ServerComMessage
}
```
---
## 订阅者数据结构
```go
// 每个用户在话题中的数据
type perUserData struct {
// 权限
modeWant types.AccessMode // 用户想要的权限
modeGiven types.AccessMode // 话题给予的权限
// 实际权限 = modeWant & modeGiven
// 状态
deleted bool
// 私有数据
private any
}
```
---
## 访问权限说明
权限用字符串表示,如 `"JRWS"`:
| 权限 | 含义 | 说明 |
|------|------|------|
| J | Join | 加入话题 |
| R | Read | 读取消息 |
| W | Write | 发送消息 |
| P | Presence | 接收在线状态 |
| A | Admin | 管理权限 |
| S | Share | 分享权限 |
| D | Delete | 删除权限 |
| O | Owner | 拥有者权限 |
**权限组合示例**:
- `"JRWP"` - 普通成员:可以加群、读消息、发消息、看在线状态
- `"JRWS"` - 可分享成员:额外可以分享群链接
- `"JRWPA"` - 管理员:额外可以管理成员
- `"JRWPAO"` - 拥有者:完全控制
---
## 消息流转
### 发布消息流程
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 消息发布流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ① 客户端发送 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ {pub: {topic: "grpTechGroup", content: "Hello!"}} │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ② Session.dispatch() → Session.publish() │
│ │ │
│ ▼ │
│ ③ Hub 路由到 Topic │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ hub.topicGet("grpTechGroup") → topic │ │
│ │ topic.clientMsg <- msg │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ④ Topic 处理消息 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 检查发送者权限 (W) │ │
│ │ 2. 生成消息ID (lastID++) │ │
│ │ 3. 存储到数据库 │ │
│ │ 4. 准备广播 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ⑤ 广播给所有订阅者 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ for session := range topic.sessions { │ │
│ │ session.send <- {data: {topic: "grpTechGroup", content: ...}} │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ⑥ 订阅者的 writeLoop 发送 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ws.WriteMessage(TextMessage, JSON) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 总结
Tinode 的话题系统是其核心架构:
1. **统一模型**: 私聊、群聊、个人信息都通过话题实现
2. **命名规则**: 通过前缀区分类型 (`p2p`, `grp`, `usr` 等)
3. **权限控制**: 细粒度的访问控制 (J/R/W/P/A/S/D/O)
4. **发布订阅**: 消息通过话题广播给所有订阅者
理解话题系统是理解 Tinode 整体架构的关键。