319 lines
12 KiB
Markdown
319 lines
12 KiB
Markdown
# 状态同步
|
||||
|
|
|
|||
|
|
> 独立于主流程之外的横切关注点:每当连接建立、事件到达、数据不一致或重新连接时,都需要更新频道、成员和服务器信息。
|
|||
|
|
> 本流程不直接对应某个用户操作,而是被所有用户流程在特定步骤中触发。
|
|||
|
|
> 所有 API 调用通过 `TSBridge`,事件通过 `EventBus` 接收。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、触发时机总览
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
触发状态同步的时机
|
|||
|
|
│
|
|||
|
|
├─ ① 首次同步(连接建立后)
|
|||
|
|
│ 触发源:EventBus Connected 事件
|
|||
|
|
│ 动作:TSBridge.getChannelsJSON() + TSBridge.getClientsJSON() + TSBridge.getClientID()
|
|||
|
|
│ 目标:建立完整的频道基线和成员基线
|
|||
|
|
│
|
|||
|
|
├─ ② 增量同步(事件驱动)
|
|||
|
|
│ 触发源:EventBus ClientEnter / ClientMoved / ClientLeave 事件
|
|||
|
|
│ 动作:debounce 合并后 refreshClientList() 全量刷新
|
|||
|
|
│ 目标:维护实时成员位置和频道人数
|
|||
|
|
│
|
|||
|
|
├─ ③ 补偿同步(数据不一致时)
|
|||
|
|
│ 触发源:事件引用了未知的 clientID 或 channelID
|
|||
|
|
│ 动作:重新调用 TSBridge.getClientsJSON() 或 TSBridge.getChannelsJSON()
|
|||
|
|
│ 目标:用完整列表修复丢失的基线数据
|
|||
|
|
│
|
|||
|
|
├─ ④ 自身状态同步(频道切换后)
|
|||
|
|
│ 触发源:EventBus 收到自己的 ClientMoved 事件
|
|||
|
|
│ 动作:比对 clientID == selfID → 更新自身所在频道
|
|||
|
|
│ 目标:确认当前用户的实际频道位置
|
|||
|
|
│
|
|||
|
|
├─ ⑤ 消息归档同步(收到消息时)
|
|||
|
|
│ 触发源:EventBus TextMessage 事件
|
|||
|
|
│ 动作:按 targetMode + target 归档到正确的会话
|
|||
|
|
│ 目标:维护消息与目标频道的对应关系
|
|||
|
|
│
|
|||
|
|
├─ ⑥ 重连后全量同步(断开重连后)
|
|||
|
|
│ 触发源:重新收到 EventBus Connected 事件
|
|||
|
|
│ 动作:清空旧会话状态 → 重新执行首次同步
|
|||
|
|
│ 目标:确保重连后数据与服务器完全一致
|
|||
|
|
│
|
|||
|
|
└─ ⑦ 同步失败处理
|
|||
|
|
触发源:TSBridge.getChannelsJSON() / TSBridge.getClientsJSON() 请求失败
|
|||
|
|
动作:标记同步失败 → 不进入业务就绪 → 允许重试
|
|||
|
|
目标:防止在不完整数据上执行业务操作
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、同步状态树
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
SYNC[同步状态<br/>建立服务器数据基线]
|
|||
|
|
|
|||
|
|
SYNC --> S0[未同步 Unsynced<br/>已连接但尚无完整数据]
|
|||
|
|
SYNC --> S1[同步中 Syncing<br/>调用 getChannelList 和 getClientList]
|
|||
|
|
SYNC --> S2[已同步 Synchronized<br/>列表基线可供 UI 使用]
|
|||
|
|
SYNC --> S3[同步失败 SyncFailed<br/>列表请求失败或上下文失效]
|
|||
|
|
|
|||
|
|
S0 --> S1
|
|||
|
|
S1 --> S2
|
|||
|
|
S1 --> S3
|
|||
|
|
S3 --> S1
|
|||
|
|
S2 --> S1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、成员状态树
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
MEMBER[成员状态<br/>维护在线用户及频道位置]
|
|||
|
|
|
|||
|
|
MEMBER --> M0[空基线 Empty<br/>尚未执行 ListClients]
|
|||
|
|
MEMBER --> M1[已有基线 Loaded<br/>getClientList 已建立成员表]
|
|||
|
|
MEMBER --> M2[增量更新 Updating<br/>OnClientEnter/Leave/Moved 正在归并]
|
|||
|
|
MEMBER --> M3[不一致 Inconsistent<br/>事件引用未知成员或频道]
|
|||
|
|
|
|||
|
|
M0 --> M1
|
|||
|
|
M1 --> M2
|
|||
|
|
M2 --> M1
|
|||
|
|
M2 --> M3
|
|||
|
|
M3 --> M1
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、成员实体状态树
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
ENTITY[成员实体状态树<br/>描述 ListClients 基线与实时事件如何协同]
|
|||
|
|
|
|||
|
|
ENTITY --> BASE[基线分支<br/>由客户端请求建立完整成员集合]
|
|||
|
|
ENTITY --> DELTA[增量分支<br/>由服务端推送维护实时变化]
|
|||
|
|
ENTITY --> SELF[当前用户分支<br/>通过 ClientID 识别自己的事件]
|
|||
|
|
ENTITY --> REPAIR[修复分支<br/>处理事件缺失或引用不一致]
|
|||
|
|
|
|||
|
|
BASE --> B1[TSBridge.getClientsJSON 请求<br/>获取服务器当前全部在线客户端]
|
|||
|
|
B1 --> B2[按 clientID 建表<br/>使用客户端 ID 去重保存]
|
|||
|
|
B2 --> B3[按 channelID 建索引<br/>派生每个频道的成员列表]
|
|||
|
|
|
|||
|
|
DELTA --> D1[EventBus ClientEnter 事件<br/>debounce 合并后全量刷新]
|
|||
|
|
DELTA --> D2[EventBus ClientMoved 事件<br/>debounce 合并后全量刷新]
|
|||
|
|
DELTA --> D3[EventBus ClientLeave 事件<br/>debounce 合并后全量刷新]
|
|||
|
|
|
|||
|
|
SELF --> S1[TSBridge.getClientID 本地调用<br/>读取服务器分配的自身 ID]
|
|||
|
|
S1 --> S2[事件 clientID 比对<br/>判断移动或离开事件是否属于自己]
|
|||
|
|
S2 --> S3[更新自身频道事实<br/>自己的移动决定实际所在频道]
|
|||
|
|
|
|||
|
|
REPAIR --> R1[检测未知 clientID<br/>移动或离开事件找不到实体]
|
|||
|
|
R1 --> R2[重新调用 TSBridge.getClientsJSON<br/>用完整列表修复成员基线]
|
|||
|
|
REPAIR --> R3[检测未知 channelID<br/>成员引用本地不存在的频道]
|
|||
|
|
R3 --> R4[重新调用 TSBridge.getChannelsJSON<br/>用完整列表修复频道基线]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、时序:首次同步
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant UI as 上层应用<br/>发起同步并更新状态
|
|||
|
|
participant VM as ViewModel<br/>收集 EventBus 事件
|
|||
|
|
participant BRIDGE as TSBridge<br/>调用 gomobile API
|
|||
|
|
participant SDK as Go SDK (gomobile)<br/>发送列表请求
|
|||
|
|
participant TS as TeamSpeak 服务器<br/>返回频道和成员数据
|
|||
|
|
participant STORE as 业务状态仓库<br/>保存基线数据
|
|||
|
|
|
|||
|
|
Note over VM: EventBus Connected 事件触发首次同步
|
|||
|
|
|
|||
|
|
par 请求频道基线
|
|||
|
|
VM->>BRIDGE: TSBridge.getChannelsJSON()
|
|||
|
|
BRIDGE->>SDK: gomobile 调用 GetChannelsJSON
|
|||
|
|
SDK->>TS: channellist
|
|||
|
|
TS-->>SDK: JSON 字符串
|
|||
|
|
SDK-->>BRIDGE: 频道 JSON
|
|||
|
|
and 请求成员基线
|
|||
|
|
VM->>BRIDGE: TSBridge.getClientsJSON()
|
|||
|
|
BRIDGE->>SDK: gomobile 调用 GetClientsJSON
|
|||
|
|
SDK->>TS: clientlist
|
|||
|
|
TS-->>SDK: JSON 字符串
|
|||
|
|
SDK-->>BRIDGE: 成员 JSON
|
|||
|
|
and 读取自身 ID
|
|||
|
|
VM->>BRIDGE: TSBridge.getClientID()
|
|||
|
|
BRIDGE-->>VM: selfClientID
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
alt 全部成功
|
|||
|
|
VM->>STORE: 原子提交频道、成员和自身 ID
|
|||
|
|
STORE-->>UI: 同步完成 → 状态进入 Synchronized
|
|||
|
|
else 任一失败
|
|||
|
|
VM->>STORE: 标记同步失败 → 状态进入 SyncFailed
|
|||
|
|
Note over UI: 允许重试,不进入业务就绪
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、时序:增量同步与补偿同步
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant TS as TeamSpeak 服务器<br/>产生成员变化事件
|
|||
|
|
participant SDK as Go SDK (gomobile)<br/>解析 notify* 通知
|
|||
|
|
participant BRIDGE as TSBridge<br/>JNI 回调转 EventBus
|
|||
|
|
participant EB as EventBus<br/>事件合并与分发
|
|||
|
|
participant VM as ViewModel<br/>debounce 后刷新成员
|
|||
|
|
participant STORE as 成员实体仓库<br/>以 clientID 保存唯一成员
|
|||
|
|
participant UI as 频道树 UI<br/>派生人数和成员预览
|
|||
|
|
|
|||
|
|
Note over TS,UI: ② 增量同步 — 正常事件流
|
|||
|
|
|
|||
|
|
TS-->>SDK: notifycliententerview
|
|||
|
|
SDK->>BRIDGE: OnClientEnter(Client)
|
|||
|
|
BRIDGE->>EB: emit(ClientEnter)
|
|||
|
|
|
|||
|
|
TS-->>SDK: notifyclientmoved
|
|||
|
|
SDK->>BRIDGE: OnClientMoved(id, channelID)
|
|||
|
|
BRIDGE->>EB: emit(ClientMoved)
|
|||
|
|
|
|||
|
|
TS-->>SDK: notifyclientleftview
|
|||
|
|
SDK->>BRIDGE: OnClientLeave(id, reason)
|
|||
|
|
BRIDGE->>EB: emit(ClientLeave)
|
|||
|
|
|
|||
|
|
Note over EB: debounce 300ms 合并
|
|||
|
|
|
|||
|
|
EB->>VM: collect → refreshClientList()
|
|||
|
|
VM->>STORE: TSBridge.getClientsJSON() → 全量刷新
|
|||
|
|
|
|||
|
|
alt 刷新成功
|
|||
|
|
STORE-->>UI: 基线已更新
|
|||
|
|
else 数据不一致
|
|||
|
|
Note over VM: ③ 补偿同步 — 修复丢失的基线
|
|||
|
|
VM->>STORE: TSBridge.getChannelsJSON() → 修复频道基线
|
|||
|
|
STORE-->>UI: 基线已修复
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、时序:重连后全量同步
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant UI as 上层应用<br/>处理重连流程
|
|||
|
|
participant VM as ViewModel<br/>管理清理和重建
|
|||
|
|
participant BRIDGE as TSBridge<br/>管理连接生命周期
|
|||
|
|
participant SDK as Go SDK (gomobile)<br/>重新建立连接
|
|||
|
|
participant STORE as 会话数据仓库<br/>保存会话临时事实
|
|||
|
|
|
|||
|
|
Note over UI: 网络恢复或手动重连
|
|||
|
|
|
|||
|
|
UI->>VM: 检测到需要重连
|
|||
|
|
VM->>VM: 状态改为 disconnecting
|
|||
|
|
VM->>STORE: 清理旧会话数据
|
|||
|
|
STORE-->>VM: 清理完成
|
|||
|
|
|
|||
|
|
Note over VM: 重新执行连接流程
|
|||
|
|
|
|||
|
|
VM->>BRIDGE: TSBridge.connect(...)
|
|||
|
|
BRIDGE->>SDK: gomobile Connect
|
|||
|
|
SDK-->>BRIDGE: OnConnected 回调
|
|||
|
|
BRIDGE->>BRIDGE: EventBus.emit(Connected)
|
|||
|
|
BRIDGE->>VM: EventBus 分发 Connected 事件
|
|||
|
|
|
|||
|
|
Note over VM: 重新执行首次同步
|
|||
|
|
|
|||
|
|
VM->>BRIDGE: getChannelsJSON + getClientsJSON + getClientID
|
|||
|
|
BRIDGE->>SDK: gomobile 调用
|
|||
|
|
SDK-->>BRIDGE: 完整基线数据
|
|||
|
|
VM->>STORE: 原子提交新基线
|
|||
|
|
STORE-->>UI: 同步完成 → 恢复业务就绪
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、同步与主流程的关系
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart TD
|
|||
|
|
subgraph 主流程触发点
|
|||
|
|
A[01 连接服务器<br/>EventBus Connected]
|
|||
|
|
B[02 浏览频道<br/>EventBus ClientEnter/Moved/Leave]
|
|||
|
|
C[03 切换频道<br/>EventBus ClientMoved]
|
|||
|
|
D[04 文本消息<br/>EventBus TextMessage]
|
|||
|
|
E[07 断开连接<br/>重新连接]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
subgraph 状态同步流程
|
|||
|
|
F[① 首次同步]
|
|||
|
|
G[② 增量同步]
|
|||
|
|
H[③ 补偿同步]
|
|||
|
|
I[④ 自身状态同步]
|
|||
|
|
J[⑤ 消息归档同步]
|
|||
|
|
K[⑥ 重连后全量同步]
|
|||
|
|
L[⑦ 同步失败处理]
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
A --> F
|
|||
|
|
B --> G
|
|||
|
|
B --> H
|
|||
|
|
C --> I
|
|||
|
|
D --> J
|
|||
|
|
E --> K
|
|||
|
|
F --> L
|
|||
|
|
K --> F
|
|||
|
|
H --> F
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九、事件依赖
|
|||
|
|
|
|||
|
|
### 同步相关前置依赖矩阵
|
|||
|
|
|
|||
|
|
| 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 |
|
|||
|
|
| --- | --- | --- | --- |
|
|||
|
|
| 首次同步 | EventBus `Connected` 事件 | 同时读取频道、成员和自身 ID | 任一核心请求失败则不标记业务就绪 |
|
|||
|
|
| `getClientsJSON` 增量归并 | 已有频道基线更易校验 | 与 `getChannelsJSON` 同一同步批次 | 未知频道触发频道补偿读取 |
|
|||
|
|
| `ClientEnter` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
|
|||
|
|
| `ClientMoved` 事件 | 已注册 EventBus 事件收集 | 已有成员基线和自身 ID | debounce 后全量刷新 |
|
|||
|
|
| `ClientLeave` 事件 | 已注册 EventBus 事件收集 | 已有成员基线 | debounce 后全量刷新 |
|
|||
|
|
| 提交自己新频道 | 自己的 `ClientMoved` 事件 | 匹配目标频道和请求上下文 | 命令响应不能直接提交频道 |
|
|||
|
|
| `TextMessage` 归档 | `targetMode`、`target`、发送者信息 | 已有频道或私聊实体 | 不得使用当前页面猜测目标 |
|
|||
|
|
|
|||
|
|
### 权威性划分
|
|||
|
|
|
|||
|
|
| 数据 | 推荐权威来源 | 原因 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| 初始频道列表 | `TSBridge.getChannelsJSON()` 响应 | SDK 未列出频道创建、更新、删除事件 |
|
|||
|
|
| 初始在线成员 | `TSBridge.getClientsJSON()` 响应 | 建立完整在线成员基线 |
|
|||
|
|
| 自身客户端 ID | `TSBridge.getClientID()` | SDK 本地缓存服务器分配的 ID |
|
|||
|
|
| 成员实时变化 | EventBus `ClientEnter/Leave/Moved` 事件 | 服务端主动推送实时事实,debounce 后全量刷新 |
|
|||
|
|
| 文本消息 | EventBus `TextMessage` 事件 | 来源是 `notifytextmessage` |
|
|||
|
|
|
|||
|
|
### 缺失事件能力带来的依赖限制
|
|||
|
|
|
|||
|
|
根据 SDK 文档,当前公开事件不包含频道创建、频道更新和频道删除。因此:
|
|||
|
|
|
|||
|
|
1. `TsClient.getChannelList()` 是频道目录的主要权威来源。
|
|||
|
|
2. 不能假设频道列表会依靠 `onClient*` 事件永久保持最新。
|
|||
|
|
3. 发现成员引用未知频道时,应重新调用 `TsClient.getChannelList()`。
|
|||
|
|
4. 如果产品需要实时频道管理,应扩展 SDK 对相应 `notify*` 通知的支持。
|
|||
|
|
|
|||
|
|
### 统一实现原则(同步相关)
|
|||
|
|
|
|||
|
|
- **列表命令建立基线,服务端事件维护增量。** 两者缺一不可。
|
|||
|
|
- **成员表按 ClientID 幂等更新。** debounce 合并后 refreshClientList 全量刷新,天然幂等。
|
|||
|
|
- **当前用户通过 ClientID 识别。** 不依赖昵称或 UI 当前页面判断自己。
|
|||
|
|
- **未知实体引用触发补偿同步。** 不静默忽略移动或离开事件。
|
|||
|
|
- **消息按 TargetMode 和 Target 归档。** 不使用当前页面频道代替真实目标。
|
|||
|
|
- **断开时统一清理当前会话资源。** 防止旧成员、频道和 Pending 污染下一次连接。
|
|||
|
|
- **事件通过 EventBus 分发,不在 JNI 回调中直接更新 UI 状态。** JNI 回调只负责 `emit()`,ViewModel 通过 `collect()` 响应。
|