# 状态同步 > 独立于主流程之外的横切关注点:每当连接建立、事件到达、数据不一致或重新连接时,都需要更新频道、成员和服务器信息。 > 本流程不直接对应某个用户操作,而是被所有用户流程在特定步骤中触发。 > 所有 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[同步状态
建立服务器数据基线] SYNC --> S0[未同步 Unsynced
已连接但尚无完整数据] SYNC --> S1[同步中 Syncing
调用 getChannelList 和 getClientList] SYNC --> S2[已同步 Synchronized
列表基线可供 UI 使用] SYNC --> S3[同步失败 SyncFailed
列表请求失败或上下文失效] S0 --> S1 S1 --> S2 S1 --> S3 S3 --> S1 S2 --> S1 ``` --- ## 三、成员状态树 ```mermaid flowchart TD MEMBER[成员状态
维护在线用户及频道位置] MEMBER --> M0[空基线 Empty
尚未执行 ListClients] MEMBER --> M1[已有基线 Loaded
getClientList 已建立成员表] MEMBER --> M2[增量更新 Updating
OnClientEnter/Leave/Moved 正在归并] MEMBER --> M3[不一致 Inconsistent
事件引用未知成员或频道] M0 --> M1 M1 --> M2 M2 --> M1 M2 --> M3 M3 --> M1 ``` --- ## 四、成员实体状态树 ```mermaid flowchart TD ENTITY[成员实体状态树
描述 ListClients 基线与实时事件如何协同] ENTITY --> BASE[基线分支
由客户端请求建立完整成员集合] ENTITY --> DELTA[增量分支
由服务端推送维护实时变化] ENTITY --> SELF[当前用户分支
通过 ClientID 识别自己的事件] ENTITY --> REPAIR[修复分支
处理事件缺失或引用不一致] BASE --> B1[TSBridge.getClientsJSON 请求
获取服务器当前全部在线客户端] B1 --> B2[按 clientID 建表
使用客户端 ID 去重保存] B2 --> B3[按 channelID 建索引
派生每个频道的成员列表] DELTA --> D1[EventBus ClientEnter 事件
debounce 合并后全量刷新] DELTA --> D2[EventBus ClientMoved 事件
debounce 合并后全量刷新] DELTA --> D3[EventBus ClientLeave 事件
debounce 合并后全量刷新] SELF --> S1[TSBridge.getClientID 本地调用
读取服务器分配的自身 ID] S1 --> S2[事件 clientID 比对
判断移动或离开事件是否属于自己] S2 --> S3[更新自身频道事实
自己的移动决定实际所在频道] REPAIR --> R1[检测未知 clientID
移动或离开事件找不到实体] R1 --> R2[重新调用 TSBridge.getClientsJSON
用完整列表修复成员基线] REPAIR --> R3[检测未知 channelID
成员引用本地不存在的频道] R3 --> R4[重新调用 TSBridge.getChannelsJSON
用完整列表修复频道基线] ``` --- ## 五、时序:首次同步 ```mermaid sequenceDiagram participant UI as 上层应用
发起同步并更新状态 participant VM as ViewModel
收集 EventBus 事件 participant BRIDGE as TSBridge
调用 gomobile API participant SDK as Go SDK (gomobile)
发送列表请求 participant TS as TeamSpeak 服务器
返回频道和成员数据 participant STORE as 业务状态仓库
保存基线数据 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 服务器
产生成员变化事件 participant SDK as Go SDK (gomobile)
解析 notify* 通知 participant BRIDGE as TSBridge
JNI 回调转 EventBus participant EB as EventBus
事件合并与分发 participant VM as ViewModel
debounce 后刷新成员 participant STORE as 成员实体仓库
以 clientID 保存唯一成员 participant UI as 频道树 UI
派生人数和成员预览 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 上层应用
处理重连流程 participant VM as ViewModel
管理清理和重建 participant BRIDGE as TSBridge
管理连接生命周期 participant SDK as Go SDK (gomobile)
重新建立连接 participant STORE as 会话数据仓库
保存会话临时事实 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 连接服务器
EventBus Connected] B[02 浏览频道
EventBus ClientEnter/Moved/Leave] C[03 切换频道
EventBus ClientMoved] D[04 文本消息
EventBus TextMessage] E[07 断开连接
重新连接] 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()` 响应。