# 步骤 09:断开连接 > 实现断开连接流程:主动断开、被动断开、被踢处理、会话清理。 > 对应流程:`docs/流程/07_断开连接.md` > 依赖步骤:08(语音通信) --- ## 一、目标 - [ ] 主动断开流程(Disconnect)— 服务器详情卡中触发 - [ ] 被动断开处理(OnDisconnected)— 网络异常或服务端断开 - [ ] 被踢处理(OnKicked)— 自己被频道踢或服务器踢 - [ ] 会话状态清理 — 停止语音、清除频道/成员/Pending - [ ] 断线重连机制 — 自动重连 + 手动重连 + 放弃 - [ ] 返回主页逻辑 — 清理后导航回服务器配置页 --- ## 二、任务清单 ### 9.1 主动断开 **目标**:实现用户主动断开连接的完整流程,从 UI 触发到会话清理。 **对应流程**:`docs/流程/07_断开连接.md` 时序图 — 用户主动断开分支 **对应 UI 设计**:`docs/UI架构设计.md` 3.1 服务器详情卡 — 断开服务器按钮 **对应 SDK**:`docs/sdk文档-go.md` — `Disconnect() error` **断开时序**: ``` 用户点击 "断开服务器" → 弹出确认对话框 "确定要断开连接吗?" → 确认 → ServerViewModel.disconnect() → 状态改为 Disconnecting(阻止新命令/语音/移动) → TSBridge.disconnect() → Go SDK Disconnect() → 发送 shutdown reason 到服务器 → 服务器关闭会话 → Go SDK 触发 OnDisconnected(nil) → 清理会话状态 → 导航回服务器配置页 ``` **任务**: 1. **断开确认对话框** 在服务器详情卡中,点击"断开服务器"按钮时弹出确认对话框: ```kotlin // ui/components/DisconnectConfirmDialog.kt @Composable fun DisconnectConfirmDialog( onConfirm: () -> Unit, onDismiss: () -> Unit ) { AlertDialog( onDismissRequest = onDismiss, title = { Text("断开连接") }, text = { Text("确定要断开与服务器的连接吗?") }, confirmButton = { TextButton(onClick = onConfirm) { Text("断开", color = MaterialTheme.colorScheme.error) } }, dismissButton = { TextButton(onClick = onDismiss) { Text("取消") } } ) } ``` 2. **ServerViewModel 主动断开方法** ```kotlin // viewmodel/ServerViewModel.kt /** * 主动断开连接(用户触发) * * 对应 docs/流程/07_断开连接.md: * "用户明确要求离开服务器" * "立即阻止新聊天、移动和语音请求" */ fun disconnect() { Log.i(TAG, "User requested disconnect") // 1. 设置断开中状态,阻止新操作 _connectionState.value = ConnectionState.Disconnecting // 2. 停止语音活动 voiceViewModel.stopVoice() // 3. 调用 SDK 断开 try { TSBridge.disconnect() } catch (e: Exception) { Log.e(TAG, "Disconnect error", e) } // 4. 清理会话状态 clearSession() // 5. 更新连接状态 _connectionState.value = ConnectionState.Disconnected // 6. 导航回主页(由 UI 层观察 connectionState 变化后执行) } ``` 3. **服务器详情卡集成** ```kotlin // ui/components/ServerDetailCard.kt @Composable fun ServerDetailCard( serverViewModel: ServerViewModel, onDismiss: () -> Unit ) { val serverInfo by serverViewModel.serverInfo.collectAsState() var showDisconnectDialog by remember { mutableStateOf(false) } Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { // 标题栏 Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { Text("服务器详情", style = MaterialTheme.typography.titleMedium) IconButton(onClick = onDismiss) { Icon(Icons.Default.Close, contentDescription = "关闭") } } Spacer(Modifier.height(16.dp)) // 服务器信息 InfoRow("服务器名", serverInfo.name) InfoRow("服务器地址", serverInfo.address) InfoRow("在线人数", "${serverInfo.clientsOnline} / ${serverInfo.maxClients}") Spacer(Modifier.height(24.dp)) // 断开按钮 Button( onClick = { showDisconnectDialog = true }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error ), modifier = Modifier.fillMaxWidth() ) { Text("断开服务器") } } // 确认对话框 if (showDisconnectDialog) { DisconnectConfirmDialog( onConfirm = { showDisconnectDialog = false serverViewModel.disconnect() onDismiss() // 关闭卡片 }, onDismiss = { showDisconnectDialog = false } ) } } ``` ### 9.2 被动断开处理 **目标**:处理网络异常或服务端主动断开的情况,显示重连 UI 并支持自动重连。 **对应流程**:`docs/流程/07_断开连接.md` 时序图 — 网络或服务器异常分支 **对应 UI 设计**:`docs/UI架构设计.md` 5.4 断线重连 **对应 SDK**:`docs/sdk文档-go.md` — `OnDisconnected(fn func(error))` **TSBridge 回调**:`TSBridge.Callbacks.onDisconnected(message: String)` **被动断开时序**: ``` 网络异常 / 服务端关闭 → Go SDK 触发 OnDisconnected(error) → TSBridge.onDisconnected(message) → ServerViewModel.handleDisconnected(message) → 停止语音活动 → 设置连接状态为 DisconnectedWithError → 显示重连横幅 → 启动自动重连计时器 ``` **重连策略**: | 尝试次数 | 间隔 | 说明 | |----------|------|------| | 第 1 次 | 2 秒 | 快速重试 | | 第 2 次 | 4 秒 | 递增 | | 第 3 次 | 8 秒 | 递增 | | 第 4 次 | 16 秒 | 递增 | | 第 5 次 | 30 秒 | 最后一次 | | 超过 5 次 | 停止 | 显示放弃提示 | **任务**: 1. **连接状态密封类** ```kotlin // data/Models.kt /** * 连接状态机 * * 对应 docs/流程/07_断开连接.md 状态树: * Disconnecting — 正在主动或被动结束会话 * Terminated — 当前会话资源已清理 * * 状态转换: * Idle → Connecting:用户点击连接 * Connecting → Connected:握手成功 * Connected → Disconnecting:主动断开 / 网络异常 / 被踢 * Connected → Reconnecting:网络异常(自动重连) * Disconnecting → Disconnected:断开完成 * Reconnecting → Connected:重连成功 * Reconnecting → Disconnected:重连失败 / 用户放弃 * Disconnected → Idle:返回主页 */ sealed class ConnectionState { /** 空闲 — 未连接 */ object Idle : ConnectionState() /** 连接中 — 正在握手 */ object Connecting : ConnectionState() /** 已连接 — 正常会话中 */ object Connected : ConnectionState() /** 断开中 — 正在主动断开 */ object Disconnecting : ConnectionState() /** 重连中 — 网络异常后自动重连 */ data class Reconnecting( val attempt: Int, val maxAttempts: Int = 5, val reason: String = "" ) : ConnectionState() /** 已断开 — 会话结束 */ data class Disconnected( val reason: String = "", val wasKicked: Boolean = false ) : ConnectionState() } ``` 2. **ServerViewModel 被动断开处理** ```kotlin // viewmodel/ServerViewModel.kt companion object { private const val TAG = "ServerViewModel" private const val MAX_RECONNECT_ATTEMPTS = 5 private val RECONNECT_DELAYS = longArrayOf(2000, 4000, 8000, 16000, 30000) } // ── 重连状态 ── private var reconnectAttempt = 0 private var reconnectJob: Job? = null private var lastConnectParams: ConnectParams? = null /** * 处理被动断开(由 TSBridge 回调触发) * * 对应 docs/流程/07_断开连接.md: * "OnDisconnected(error) — 携带异常断开原因" * "停止全部依赖连接的操作" */ fun handleDisconnected(message: String) { Log.w(TAG, "handleDisconnected: $message") // 如果是主动断开,不触发重连 if (_connectionState.value is ConnectionState.Disconnecting) { return } // 1. 停止语音 voiceViewModel.onDisconnected() // 2. 保存断开原因 _lastDisconnectReason.value = message // 3. 启动自动重连 if (lastConnectParams != null) { startReconnect(message) } else { // 无连接参数,直接标记断开 _connectionState.value = ConnectionState.Disconnected( reason = message, wasKicked = false ) } } /** * 启动自动重连 * * 对应 docs/UI架构设计.md 5.4: * "自动重连:最多尝试 5 次,间隔递增(2s → 4s → 8s → 16s → 30s)" */ private fun startReconnect(reason: String) { reconnectAttempt = 0 _connectionState.value = ConnectionState.Reconnecting( attempt = 0, reason = reason ) reconnectJob = viewModelScope.launch { while (reconnectAttempt < MAX_RECONNECT_ATTEMPTS && _connectionState.value is ConnectionState.Reconnecting ) { val delayMs = RECONNECT_DELAYS[reconnectAttempt] Log.i(TAG, "Reconnect attempt ${reconnectAttempt + 1}/$MAX_RECONNECT_ATTEMPTS in ${delayMs}ms") delay(delayMs) reconnectAttempt++ _connectionState.value = ConnectionState.Reconnecting( attempt = reconnectAttempt, reason = reason ) val params = lastConnectParams ?: break val error = TSBridge.connect( host = params.host, nickname = params.nickname, password = params.password, defaultChannel = params.defaultChannel, defaultChannelPassword = params.defaultChannelPassword, callbacks = createCallbacks() ) if (error.isEmpty()) { Log.i(TAG, "Reconnect succeeded on attempt $reconnectAttempt") // 重连成功由 onConnected 回调处理 return@launch } Log.w(TAG, "Reconnect attempt $reconnectAttempt failed: $error") } // 重连失败 Log.w(TAG, "All reconnect attempts failed") _connectionState.value = ConnectionState.Disconnected( reason = reason, wasKicked = false ) } } /** * 手动重连(用户点击"手动重连"按钮) * * 对应 docs/UI架构设计.md 5.4: * "手动重连:用户主动触发立即重连" */ fun manualReconnect() { reconnectJob?.cancel() reconnectAttempt = 0 val params = lastConnectParams ?: return _connectionState.value = ConnectionState.Connecting viewModelScope.launch { val error = TSBridge.connect( host = params.host, nickname = params.nickname, password = params.password, defaultChannel = params.defaultChannel, defaultChannelPassword = params.defaultChannelPassword, callbacks = createCallbacks() ) if (error.isNotEmpty()) { _connectionState.value = ConnectionState.Disconnected( reason = error, wasKicked = false ) } // 成功由 onConnected 回调处理 } } /** * 放弃重连(用户点击"放弃"按钮) * * 对应 docs/UI架构设计.md 5.4: * "放弃:停止重连 → 显示完整断线提示 → 返回主页" */ fun abandonReconnect() { reconnectJob?.cancel() reconnectJob = null reconnectAttempt = 0 clearSession() _connectionState.value = ConnectionState.Disconnected( reason = _lastDisconnectReason.value, wasKicked = false ) } ``` 3. **重连横幅 UI** ```kotlin // ui/components/ReconnectBanner.kt @Composable fun ReconnectBanner( reconnectState: ConnectionState.Reconnecting, onManualReconnect: () -> Unit, onAbandon: () -> Unit ) { Surface( modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.errorContainer, tonalElevation = 4.dp ) { Column( modifier = Modifier.padding(12.dp) ) { Row( verticalAlignment = Alignment.CenterVertically ) { Icon( imageVector = Icons.Default.Warning, contentDescription = null, tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(20.dp) ) Spacer(Modifier.width(8.dp)) Text( text = "连接已断开", style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onErrorContainer ) } Spacer(Modifier.height(4.dp)) Text( text = "正在尝试重连... (${reconnectState.attempt}/${reconnectState.maxAttempts})", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onErrorContainer ) if (reconnectState.reason.isNotEmpty()) { Text( text = reconnectState.reason, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.7f) ) } Spacer(Modifier.height(8.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End ) { TextButton(onClick = onManualReconnect) { Text("手动重连") } TextButton(onClick = onAbandon) { Text("放弃", color = MaterialTheme.colorScheme.error) } } } } } ``` ### 9.3 被踢处理 **目标**:处理自己被踢出频道或服务器的情况,显示全屏提示并清理会话。 **对应流程**:`docs/流程/07_断开连接.md` 时序图 — 自己被踢分支 **对应 UI 设计**:`docs/UI架构设计.md` 5.3 被踢处理 **对应 SDK**:`docs/sdk文档-go.md` — `OnKicked(fn func(string))`,`ClientLeftViewEvent.ReasonID` **TSBridge 回调**:`TSBridge.Callbacks.onKicked(reason: String)` **被踢时序**: ``` 服务器发送 notifyclientleftview (reasonid=4 或 5) → Go SDK 识别为自己被踢 → Go SDK 触发 OnKicked(reason) → TSBridge.onKicked(reason) → ServerViewModel.handleKicked(reason) → 立即停止语音 → 清理会话状态 → 设置连接状态为 Disconnected(wasKicked=true) → 显示被踢全屏提示 ``` **ReasonID 说明**: | ReasonID | 含义 | 说明 | |----------|------|------| | 4 | 频道踢出 | 被踢出当前频道,但仍在服务器上 | | 5 | 服务器踢出 | 被踢出整个服务器 | > 注意:Go SDK 的 `OnKicked` 已从 `notifyclientleftview` 中识别出自己被踢,上层无需再次判断。 **任务**: 1. **ServerViewModel 被踢处理** ```kotlin // viewmodel/ServerViewModel.kt /** * 处理被踢事件(由 TSBridge 回调触发) * * 对应 docs/流程/07_断开连接.md: * "OnKicked(reason) — SDK 将自己被踢转换为独立事件" * "必须与普通成员离开区分" * * 对应 docs/UI架构设计.md 5.3: * "全屏覆盖提示,不可通过点击外部关闭" */ fun handleKicked(reason: String) { Log.w(TAG, "handleKicked: $reason") // 1. 立即停止语音(断开或被踢必须立即停止) voiceViewModel.stopVoice() // 2. 停止重连(如果正在进行) reconnectJob?.cancel() reconnectJob = null // 3. 清理会话状态 clearSession() // 4. 设置被踢状态 _connectionState.value = ConnectionState.Disconnected( reason = reason, wasKicked = true ) // 5. 记录被踢原因供 UI 显示 _kickReason.value = reason Log.i(TAG, "Kicked from server, session cleared") } ``` 2. **被踢全屏提示 UI** ```kotlin // ui/screens/KickedScreen.kt @Composable fun KickedScreen( reason: String, onReconnect: () -> Unit, onBackToHome: () -> Unit ) { // 全屏覆盖,不可通过点击外部关闭 Box( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.surface) .systemBarsPadding(), contentAlignment = Alignment.Center ) { Column( modifier = Modifier.padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally ) { // 图标 Icon( imageVector = Icons.Default.PersonOff, contentDescription = null, modifier = Modifier.size(64.dp), tint = MaterialTheme.colorScheme.error ) Spacer(Modifier.height(24.dp)) // 标题 Text( text = "你已被踢出", style = MaterialTheme.typography.headlineMedium, color = MaterialTheme.colorScheme.onSurface ) Spacer(Modifier.height(12.dp)) // 原因 if (reason.isNotEmpty()) { Text( text = "原因:$reason", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) } else { Text( text = "你已被服务器管理员踢出", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) } Spacer(Modifier.height(32.dp)) // 重新连接按钮 Button( onClick = onReconnect, modifier = Modifier.fillMaxWidth() ) { Text("重新连接") } Spacer(Modifier.height(12.dp)) // 返回主页按钮 OutlinedButton( onClick = onBackToHome, modifier = Modifier.fillMaxWidth() ) { Text("返回主页") } } } } ``` ### 9.4 会话清理逻辑 **目标**:统一清理当前会话的所有临时状态,防止旧数据污染下次连接。 **对应流程**:`docs/流程/07_断开连接.md` 状态树 — Terminated 分支: - 清理命令请求(取消未完成的业务操作) - 清理语音与传输(停止语音和文件连接) - 清理会话状态(移除频道成员等临时事实) **关键原则**: - 断开时统一清理当前会话资源 - 防止旧成员、频道和 Pending 污染下一次连接 - 清理顺序:先停止活跃操作 → 再清除数据 **任务**: 1. **Repository 会话清理** ```kotlin // data/Repository.kt /** * 清理当前会话的所有临时状态 * * 对应 docs/流程/07_断开连接.md: * "清理频道、成员和 Pending — 防止旧会话事实污染下一次连接" * * 清理顺序: * 1. 清除频道列表和频道树 * 2. 清除客户端列表 * 3. 清除当前频道/客户端状态 * 4. 清除消息历史 * 5. 清除待处理操作 */ fun clearSession() { Log.i(TAG, "clearSession: clearing all session data") // 频道数据 _channels.value = emptyList() _channelTree.value = emptyList() _currentChannelId.value = 0L _currentChannel.value = null // 客户端数据 _clients.value = emptyList() _currentClientId.value = 0 // 消息数据 _messages.value = emptyMap() _unreadCounts.value = emptyMap() // 服务器信息 _serverInfo.value = ServerInfo() // 连接状态 _isConnected.value = false Log.d(TAG, "clearSession: all session data cleared") } ``` 2. **ServerViewModel 清理入口** ```kotlin // viewmodel/ServerViewModel.kt /** * 清理会话(统一入口) * * 对应 docs/流程/07_断开连接.md 统一实现原则: * "断开时统一清理当前会话资源" */ private fun clearSession() { // 1. 停止语音 voiceViewModel.stopVoice() // 2. 清除 Repository 数据 repository.clearSession() // 3. 清除聊天 ViewModel 状态 chatViewModel.clearMessages() // 4. 清除频道 ViewModel 状态 channelViewModel.clearChannels() // 5. 清除连接参数(可选:保留用于重连) // lastConnectParams 保留,供重连使用 Log.i(TAG, "Session cleared") } ``` ### 9.5 断开 UI 反馈 **目标**:整合所有断开场景的 UI 反馈,确保用户在每种情况下都有清晰的视觉提示。 **对应 UI 设计**:`docs/UI架构设计.md` 5.1 连接状态、5.3 被踢处理、5.4 断线重连 **场景汇总**: | 场景 | 触发 | UI 行为 | |------|------|---------| | 主动断开 | 用户点击"断开服务器" | 确认对话框 → 断开 → 返回主页 | | 网络异常 | OnDisconnected(error) | 顶部重连横幅 → 自动重连 | | 被踢出 | OnKicked(reason) | 全屏被踢提示 → 重新连接/返回主页 | | 重连成功 | onConnected(重连后) | 横幅消失 → 全量同步 → 恢复正常 | | 重连失败 | 超过最大重试次数 | 显示放弃提示 → 返回主页 | **任务**: 1. **主页面断开状态观察** 在主 Activity 或 NavGraph 中观察连接状态,处理导航: ```kotlin // MainActivity.kt 或 NavGraph.kt @Composable fun AppNavigation(serverViewModel: ServerViewModel) { val connectionState by serverViewModel.connectionState.collectAsState() val navController = rememberNavController() // 观察连接状态变化,处理导航 LaunchedEffect(connectionState) { when (connectionState) { is ConnectionState.Disconnected -> { val state = connectionState as ConnectionState.Disconnected if (state.wasKicked) { // 被踢 → 导航到被踢页面 navController.navigate("kicked") { popUpTo("channelList") { inclusive = true } } } else { // 普通断开 → 返回主页 navController.navigate("serverConfig") { popUpTo("channelList") { inclusive = true } } } } is ConnectionState.Connected -> { // 连接/重连成功 → 导航到频道列表 navController.navigate("channelList") { popUpTo("serverConfig") { inclusive = true } } } else -> { /* 其他状态不处理导航 */ } } } NavHost(navController, startDestination = "serverConfig") { composable("serverConfig") { ServerConfigScreen(serverViewModel) } composable("channelList") { // 重连横幅 val state = connectionState if (state is ConnectionState.Reconnecting) { ReconnectBanner( reconnectState = state, onManualReconnect = { serverViewModel.manualReconnect() }, onAbandon = { serverViewModel.abandonReconnect() } ) } ChannelListScreen(...) } composable("kicked") { val state = connectionState as? ConnectionState.Disconnected KickedScreen( reason = serverViewModel.kickReason.collectAsState().value, onReconnect = { serverViewModel.manualReconnect() }, onBackToHome = { serverViewModel.abandonReconnect() navController.navigate("serverConfig") { popUpTo("kicked") { inclusive = true } } } ) } } } ``` 2. **频道列表页断开状态指示** 对应 `docs/UI架构设计.md` 2.2 头部右侧连接状态指示: ```kotlin // ui/components/ConnectionStatusIndicator.kt @Composable fun ConnectionStatusIndicator( connectionState: ConnectionState, modifier: Modifier = Modifier ) { val (color, text) = when (connectionState) { is ConnectionState.Connected -> Color(0xFF4CAF50) to "已连接" is ConnectionState.Connecting -> Color(0xFFFF9800) to "连接中" is ConnectionState.Disconnecting -> Color(0xFFFF9800) to "断开中" is ConnectionState.Reconnecting -> Color(0xFFFF9800) to "重连中" is ConnectionState.Disconnected -> Color(0xFFF44336) to "已断开" is ConnectionState.Idle -> Color.Gray to "未连接" } Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .size(8.dp) .background(color, CircleShape) ) Spacer(Modifier.width(4.dp)) Text( text = text, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } ``` --- ## 三、状态与数据流 ### 3.1 连接状态机 ``` ┌──────────────────────────────────────────────────┐ │ │ ▼ │ ┌─────────┐ │ │ Idle │◄───────────────────────────────────────────┤ └────┬────┘ │ │ 点击连接 │ ▼ │ ┌─────────────┐ │ │ Connecting │ │ └──┬────────┬─┘ │ │ │ │ 成功 │ │ 失败 │ ▼ ▼ │ ┌──────────────┐ ┌──────────────┐ │ │ Connected │ │ Disconnected│ │ └──────┬───────┘ └──────────────┘ │ │ │ │ 主动断开 / 网络异常 / 被踢 │ ▼ │ ┌──────────────────────────────────┐ │ │ Disconnecting │ │ │ (主动断开时直接进入此状态) │ │ └──────┬───────────────────────────┘ │ │ │ │ 网络异常且有重连参数 │ ▼ │ ┌──────────────┐ │ │ Reconnecting │──── 重连成功 ───→ Connected │ │ (attempt/N) │ │ └──────┬───────┘ │ │ │ │ 超过最大次数 / 用户放弃 │ ▼ │ ┌──────────────┐ │ │ Disconnected │──── 返回主页 ───→ Idle │ │ (wasKicked?) │ │ └──────────────┘ │ │ │ │ 被踢 → 用户点击重新连接 │ └──────────── Connecting ─────────────────────────────────┘ ``` ### 3.2 主动断开数据流 ``` UI (服务器详情卡) ServerViewModel TSBridge Go SDK 服务器 │ │ │ │ │ │ 点击"断开服务器" │ │ │ │ ├───────────────────→│ │ │ │ │ │ 确认对话框 │ │ │ │◄───────────────────┤ │ │ │ │ 确认 │ │ │ │ ├───────────────────→│ │ │ │ │ │ disconnect() │ │ │ │ ├───────────────────→│ │ │ │ │ │ disconnect() │ │ │ │ ├───────────────→│ │ │ │ │ │ shutdown │ │ │ │ ├─────────────→│ │ │ │ │ │ │ │ │ │ 连接关闭 │ │ │ │ │◄─────────────┤ │ │ │ onDisconnected │ │ │ │ │◄───────────────┤ │ │ │ clearSession() │ │ │ │ ├───────────────────→│ (Repository) │ │ │ │ 状态 → Disconnected│ │ │ │ 导航回主页 │ │ │ │ │◄───────────────────┤ │ │ │ ``` ### 3.3 被动断开与重连数据流 ``` 网络/服务器 Go SDK TSBridge ServerViewModel UI │ │ │ │ │ │ 网络中断 │ │ │ │ │ ──────── X ──────│ │ │ │ │ │ onDisconnected │ │ │ │ ├───────────────→│ │ │ │ │ │ onDisconnected │ │ │ │ ├───────────────→│ │ │ │ │ │ stopVoice() │ │ │ │ │ startReconnect() │ │ │ │ │ 状态→Reconnecting│ │ │ │ ├─────────────────→│ │ │ │ │ │ 显示横幅 │ │ │ │ │ │ │ │ │ delay(2s) │ │ │ │ │ connect() │ │ │ ├────────────────┤ │ │ │◄───────────────┤ │ │ │ │ Connect │ │ │ │◄─────────────────┤ │ │ │ │ │ │ │ │ │ 连接成功 │ │ │ │ │─────────────────→│ │ │ │ │ │ onConnected │ │ │ │ ├───────────────→│ │ │ │ │ │ onConnected │ │ │ │ ├───────────────→│ │ │ │ │ │ 全量同步 │ │ │ │ │ 状态→Connected │ │ │ │ ├─────────────────→│ │ │ │ │ │ 横幅消失 ``` ### 3.4 被踢数据流 ``` 服务器 Go SDK TSBridge ServerViewModel UI │ │ │ │ │ │ notifyclient │ │ │ │ │ leftview │ │ │ │ │ (reasonid=5) │ │ │ │ ├─────────────────→│ │ │ │ │ │ 识别为自己被踢 │ │ │ │ │ onKicked │ │ │ │ ├───────────────→│ │ │ │ │ │ onKicked │ │ │ │ ├───────────────→│ │ │ │ │ │ stopVoice() │ │ │ │ │ clearSession() │ │ │ │ │ 状态→Disconnected│ │ │ │ │ (wasKicked=true) │ │ │ │ ├─────────────────→│ │ │ │ │ │ 全屏提示 │ │ │ │ │ │ │ │ │ 用户选择: │ │ │ │ │ 重新连接/返回主页 │ ``` ### 3.5 断开前置依赖矩阵 对应 `docs/流程/07_断开连接.md` 事件依赖: | 操作或事件 | 必须依赖 | 建议依赖 | 依赖失败时的处理 | | --- | --- | --- | --- | | `Disconnect`(主动) | 已连接 | 无 | 未连接时忽略 | | `OnDisconnected`(被动) | 已注册处理器 | 保存断开原因 | 停止全部依赖连接的操作 | | `OnKicked`(被踢) | 已注册处理器 | 区分 reasonid 4 和 5 | 清理会话且不伪装为普通成员离开 | | 会话清理 | 断开事件触发 | 按顺序清理 | 部分清理失败不影响其他清理 | --- ## 四、与其他步骤的集成 ### 4.1 与语音通信集成(步骤 08) - 主动断开时调用 `voiceViewModel.stopVoice()` - 被动断开时调用 `voiceViewModel.onDisconnected()` - 被踢时调用 `voiceViewModel.stopVoice()` - 重连成功后调用 `voiceViewModel.onReconnected()` 对应 `docs/流程/05_语音通信.md`: - "断开或被踢必须立即停止" - "OnDisconnected(error) → StopCapture → 状态改为 blocked" - "重连并同步前禁止恢复发送" → 重连成功后恢复 Idle ### 4.2 与频道列表页集成(步骤 05) - 断开后频道树清空 - 断开后当前频道栏隐藏 - 重连成功后重新加载频道树 ### 4.3 与聊天页集成(步骤 07) - 断开后消息列表清空 - 断开后消息输入框禁用 - 重连成功后重新加载消息历史 ### 4.4 与状态同步集成(步骤 10) - 重连成功后执行全量同步(`docs/流程/08_状态同步.md` ⑥) - 全量同步完成后恢复正常使用 ### 4.5 与服务器配置页集成(步骤 03) - 断开后导航回服务器配置页 - 保留上次连接参数(地址、昵称、密码)用于重连 - 被踢后"重新连接"使用相同参数 --- ## 五、TSBridge 回调注册 **目标**:在 ServerViewModel 中注册 OnDisconnected 和 OnKicked 回调。 **对应实现**:`android/app/src/main/java/com/tsmobile/app/TSBridge.kt` — Callbacks 接口 **已有回调定义**: ```kotlin // TSBridge.kt 中已有: interface Callbacks { fun onConnected() fun onDisconnected(message: String) // ← 被动断开 fun onTextMessage(msg: TextMsg) fun onClientEnter(client: Client) fun onClientLeave(id: Int, reasonMsg: String) fun onClientMoved(id: Int, targetChannelID: String) fun onKicked(reason: String) // ← 被踢 fun onVoiceData(clientID: Int, data: ByteArray, codec: Int) } ``` **任务**: ```kotlin // viewmodel/ServerViewModel.kt /** * 创建回调对象(供 TSBridge.connect 和重连使用) */ private fun createCallbacks(): TSBridge.Callbacks { return object : TSBridge.Callbacks { override fun onConnected() { Log.i(TAG, "onConnected") handleConnected() } override fun onDisconnected(message: String) { Log.w(TAG, "onDisconnected: $message") handleDisconnected(message) } override fun onTextMessage(msg: TextMsg) { chatViewModel.handleTextMessage(msg) } override fun onClientEnter(client: Client) { channelViewModel.handleClientEnter(client) } override fun onClientLeave(id: Int, reasonMsg: String) { channelViewModel.handleClientLeave(id, reasonMsg) } override fun onClientMoved(id: Int, targetChannelID: String) { channelViewModel.handleClientMoved(id, targetChannelID) } override fun onKicked(reason: String) { Log.w(TAG, "onKicked: $reason") handleKicked(reason) } override fun onVoiceData(clientID: Int, data: ByteArray, codec: Int) { voiceViewModel.handleVoiceData(clientID, data, codec) } } } ``` --- ## 六、验收标准 ### 功能验收 - [ ] **主动断开** - 服务器详情卡中有"断开服务器"按钮 - 点击后弹出确认对话框 - 确认后执行断开,返回服务器配置页 - 断开过程中语音停止、会话清理 - [ ] **被动断开** - 网络异常时自动检测断开 - 顶部显示重连横幅(含断开原因) - 自动重连最多 5 次,间隔递增 - 重连成功后横幅消失,恢复正常 - [ ] **被踢处理** - 被踢后显示全屏提示(含踢出原因) - 提供"重新连接"和"返回主页"两个选项 - 被踢后语音立即停止、会话清理 - [ ] **重连机制** - 自动重连:2s → 4s → 8s → 16s → 30s - 手动重连:立即尝试连接 - 放弃重连:停止重连,返回主页 - 重连成功后执行全量同步 - [ ] **会话清理** - 断开后频道列表清空 - 断开后客户端列表清空 - 断开后消息历史清空 - 断开后语音停止 - 无旧数据污染新连接 - [ ] **状态指示** - 头部连接状态指示正确显示 - 未连接时显示灰色"未连接" - 连接中显示橙色"连接中" - 已连接显示绿色"已连接" - 重连中显示橙色"重连中" ### 错误处理验收 | 错误场景 | 预期行为 | |----------|----------| | 主动断开时 SDK 报错 | 忽略错误,强制清理会话 | | 重连时密码错误 | 停止重连,显示错误,返回主页 | | 重连时服务器满 | 继续重试直到最大次数 | | 重连时网络仍不可用 | 继续重试直到最大次数 | | 被踢后重连再次被踢 | 显示被踢提示,不自动重连 | | 清理会话时部分失败 | 继续清理其他部分,记录日志 | ### 测试用例 | 场景 | 操作 | 预期结果 | |------|------|----------| | 主动断开 | 服务器详情卡 → 断开服务器 → 确认 | 断开,返回主页 | | 主动断开取消 | 服务器详情卡 → 断开服务器 → 取消 | 不断开,卡片关闭 | | 网络异常 | 断开网络 | 显示重连横幅,自动重连 | | 重连成功 | 网络恢复 | 重连成功,横幅消失,全量同步 | | 重连失败 | 持续断网 5 次 | 停止重连,显示放弃提示 | | 手动重连 | 点击"手动重连" | 立即尝试连接 | | 放弃重连 | 点击"放弃" | 停止重连,返回主页 | | 被频道踢 | 被管理员踢出频道 | 全屏提示,可重连或返回 | | 被服务器踢 | 被管理员踢出服务器 | 全屏提示,可重连或返回 | | 被踢重连 | 被踢后点击"重新连接" | 使用相同参数重新连接 | | 被踢返回 | 被踢后点击"返回主页" | 返回服务器配置页 | | 语音停止 | 发言中断开连接 | 语音立即停止 | | 数据清理 | 断开后重新连接 | 新连接无旧数据 | | 状态指示 | 观察头部状态变化 | 颜色和文字正确 | --- ## 七、参考文档 - `docs/流程/07_断开连接.md` - 时序图、状态树、事件依赖 - `docs/UI架构设计.md` - 3.1 服务器详情卡、5.3 被踢处理、5.4 断线重连 - `docs/sdk文档-go.md` - 1. 连接管理(Disconnect)、2. 事件注册(OnDisconnected、OnKicked) - `docs/implementation/02_Bridge层实现.md` - TSBridge.disconnect、Callbacks 接口 - `docs/implementation/08_语音通信.md` - VoiceViewModel.stopVoice、onDisconnected - `android/app/src/main/java/com/tsmobile/app/TSBridge.kt` - Bridge 层实现