diff --git a/NOTIFICATION_SYSTEM.md b/NOTIFICATION_SYSTEM.md index a3f348e1..5e369759 100644 --- a/NOTIFICATION_SYSTEM.md +++ b/NOTIFICATION_SYSTEM.md @@ -1,6 +1,132 @@ # 实时消息推送系统使用指南 -## 🆕 最新更新 (v2.10.0 - 点击加载反馈) +## 🆕 最新更新 (v2.11.0 - Socket 连接优化) + +### 优化内容 + +#### 1. **指数退避策略** 🔄 +- ✅ **智能重连间隔**:从"激进"改为"渐进"策略 + - Real Socket: 第1次 1分钟 → 第2次 2分钟 → 第3次+ 4分钟 + - Mock Socket: 第1次 10秒 → 第2次 20秒 → 第3次+ 40秒(缩短10倍便于测试) +- ✅ **无限重试**:不再在5次后停止,持续使用指数退避重连 +- ✅ **自定义退避逻辑**:禁用 Socket.IO 默认重连机制,实现更可控的重连策略 + +#### 2. **连接状态横幅优化** 📍 +- ✅ **降低侵入性**:zIndex 从 10000 → 1050,高度 py={3} → py={2},半透明背景(opacity 0.95) +- ✅ **手动关闭**:所有状态(DISCONNECTED/RECONNECTING/FAILED)都可手动关闭 +- ✅ **状态持久化**:用户关闭后保存到 localStorage,重连成功后自动清除 +- ✅ **自动消失**:重连成功显示"✓ 已重新连接" 2秒后自动消失 +- ✅ **无限次数显示**:支持 Infinity 最大重连次数,显示"尝试重连中 (第 N 次)" + +#### 3. **Mock 模式测试功能** 🧪 +- ✅ **断线重连模拟**:`__mockSocket.simulateDisconnection()` - 模拟意外断线,自动重连 +- ✅ **连接状态查询**:`__mockSocket.isConnected()` - 查看当前连接状态 +- ✅ **手动重连**:`__mockSocket.reconnect()` - 立即触发重连 +- ✅ **重连次数查询**:`__mockSocket.getAttempts()` - 查看当前重连次数 +- ✅ **控制台提示**:开发模式启动时自动显示可用测试函数 + +#### 4. **环境说明** 🌍 +- ✅ **清晰注释**:在 NotificationContext.js 添加详细的环境说明 + - `SOCKET_TYPE === 'REAL'`:使用真实 Socket.IO,连接 wss://valuefrontier.cn(生产环境) + - `SOCKET_TYPE === 'MOCK'`:使用模拟服务(开发环境),用于本地测试 +- ✅ **环境切换**:设置 `REACT_APP_ENABLE_MOCK=true` 或 `REACT_APP_USE_MOCK_SOCKET=true` 切换到 MOCK 模式 + +### 测试方法 + +#### Mock 模式下测试断线重连: + +1. **启用 Mock 模式**: + ```bash + # .env 文件 + REACT_APP_ENABLE_MOCK=true + ``` + +2. **场景1:模拟断线(自动重连成功)** + + 打开控制台,运行以下命令: + + ```javascript + // 查看可用测试函数 + console.log(window.__mockSocket); + + // 模拟断线(自动重连) + __mockSocket.simulateDisconnection(); + + // 观察重连过程: + // - 连接状态横幅会出现("正在重新连接") + // - 重连次数递增(第1次 10s → 第2次 20s → 第3次 40s) + // - 重连成功后显示"✓ 已重新连接" 2秒后自动消失 + + // 查看连接状态 + __mockSocket.isConnected(); // true/false + + // 查看重连次数 + __mockSocket.getAttempts(); // 0, 1, 2, 3... + + // 手动重连(立即重置重连次数) + __mockSocket.reconnect(); + ``` + +3. **场景2:模拟持续连接失败** 🆕 + + 打开控制台,运行以下命令: + + ```javascript + // 模拟持续连接失败 + __mockSocket.simulateConnectionFailure(); + + // 观察效果: + // - 连接状态横幅出现:"正在重新连接 (第 1 次)" + // - 10秒后:"正在重新连接 (第 2 次)" + // - 20秒后:"正在重新连接 (第 3 次)" + // - 40秒后:"正在重新连接 (第 4 次)" + // - 继续以 40秒间隔重试... (第 5/6/7... 次) + + // 测试手动关闭横幅 + // 点击横幅右侧的 ✕ 按钮 → 横幅消失 + + // 查看重连次数(后台仍在重连) + __mockSocket.getAttempts(); // 递增中... + + // 允许下次重连成功 + __mockSocket.allowReconnection(); + + // 观察效果: + // - 如果横幅已关闭:下次重连成功时会重新出现 "✓ 已重新连接",2秒后消失 + // - 如果横幅未关闭:直接显示 "✓ 已重新连接",2秒后消失 + + // 也可以手动立即重连 + __mockSocket.reconnect(); // 立即成功(如果已调用 allowReconnection) + ``` + +4. **测试手动关闭**: + - 模拟断线后,点击状态横幅右侧的 ✕ 按钮 + - 横幅消失,保存到 localStorage + - 重连成功后,横幅会重新出现 2秒("✓ 已重新连接")然后自动消失 + +### 测试函数速查表 + +| 函数 | 说明 | 使用场景 | +|------|------|----------| +| `__mockSocket.simulateDisconnection()` | 模拟断线,自动重连成功 | 测试正常重连流程 | +| `__mockSocket.simulateConnectionFailure()` | 模拟持续连接失败 | 测试重连失败、指数退避 | +| `__mockSocket.allowReconnection()` | 允许下次重连成功 | 配合 `simulateConnectionFailure()` 使用 | +| `__mockSocket.isConnected()` | 查看当前连接状态 | 调试连接状态 | +| `__mockSocket.reconnect()` | 手动立即重连 | 测试"立即重试"按钮 | +| `__mockSocket.getAttempts()` | 查看当前重连次数 | 验证指数退避逻辑 | + +### 技术细节 + +- **文件修改**: + - `src/services/socketService.js` - 指数退避逻辑,无限重试 + - `src/services/mockSocketService.js` - 模拟断线重连,测试函数 + - `src/components/ConnectionStatusBar/index.js` - UI 优化,手动关闭 + - `src/App.js` - dismissed 状态管理,localStorage 持久化 + - `src/contexts/NotificationContext.js` - 重连成功检测,maxReconnectAttempts 导出 + +--- + +## v2.10.0 更新回顾 - ✅ **按钮加载态**:点击"查看详情"后按钮显示 loading spinner,文字变为"跳转中..."(蓝色) - ✅ **防重复点击**:加载状态时禁用再次点击,cursor 变为 wait,避免误操作 @@ -11,7 +137,7 @@ --- -### v2.9.0 更新回顾 +## v2.9.0 更新回顾 - ✅ **头部简化**:移除 AI 和预测标签,只保留优先级标签(紧急/重要),避免换行拥挤 - ✅ **底部补充**:AI 和预测标识移到底部元数据区,使用 xs size 小徽章,信息不丢失 diff --git a/src/App.js b/src/App.js index 54f7dfe3..94833388 100755 --- a/src/App.js +++ b/src/App.js @@ -60,20 +60,42 @@ import { logger } from "utils/logger"; * 需要在 NotificationProvider 内部使用,所以单独提取 */ function ConnectionStatusBarWrapper() { - const { connectionStatus, reconnectAttempt, retryConnection } = useNotification(); + const { connectionStatus, reconnectAttempt, maxReconnectAttempts, retryConnection } = useNotification(); + const [isDismissed, setIsDismissed] = React.useState(false); + + // 监听连接状态变化 + React.useEffect(() => { + // 重连成功后,清除 dismissed 状态 + if (connectionStatus === 'connected' && isDismissed) { + setIsDismissed(false); + // 从 localStorage 清除 dismissed 标记 + localStorage.removeItem('connection_status_dismissed'); + } + + // 从 localStorage 恢复 dismissed 状态 + if (connectionStatus !== 'connected' && !isDismissed) { + const dismissed = localStorage.getItem('connection_status_dismissed'); + if (dismissed === 'true') { + setIsDismissed(true); + } + } + }, [connectionStatus, isDismissed]); const handleClose = () => { - // 关闭状态条(可选,当前不实现) - // 用户可以通过刷新页面来重新显示 + // 用户手动关闭,保存到 localStorage + setIsDismissed(true); + localStorage.setItem('connection_status_dismissed', 'true'); + logger.info('App', 'Connection status bar dismissed by user'); }; return ( ); } diff --git a/src/components/ConnectionStatusBar/index.js b/src/components/ConnectionStatusBar/index.js index b609aaec..738a0368 100644 --- a/src/components/ConnectionStatusBar/index.js +++ b/src/components/ConnectionStatusBar/index.js @@ -27,6 +27,7 @@ export const CONNECTION_STATUS = { DISCONNECTED: 'disconnected', // 已断开 RECONNECTING: 'reconnecting', // 重连中 FAILED: 'failed', // 连接失败 + RECONNECTED: 'reconnected', // 重连成功(显示2秒后自动消失) }; /** @@ -38,9 +39,10 @@ const ConnectionStatusBar = ({ maxReconnectAttempts = 5, onRetry, onClose, + isDismissed = false, // 用户是否手动关闭 }) => { - // 仅在非正常状态时显示 - const shouldShow = status !== CONNECTION_STATUS.CONNECTED; + // 显示条件:非正常状态 且 用户未手动关闭 + const shouldShow = status !== CONNECTION_STATUS.CONNECTED && !isDismissed; // 状态配置 const statusConfig = { @@ -52,13 +54,20 @@ const ConnectionStatusBar = ({ [CONNECTION_STATUS.RECONNECTING]: { status: 'warning', title: '正在重新连接', - description: `尝试重连中 (第 ${reconnectAttempt}/${maxReconnectAttempts} 次)`, + description: maxReconnectAttempts === Infinity + ? `尝试重连中 (第 ${reconnectAttempt} 次)` + : `尝试重连中 (第 ${reconnectAttempt}/${maxReconnectAttempts} 次)`, }, [CONNECTION_STATUS.FAILED]: { status: 'error', title: '连接失败', description: '无法连接到服务器,请检查网络连接', }, + [CONNECTION_STATUS.RECONNECTED]: { + status: 'success', + title: '已重新连接', + description: '连接已恢复', + }, }; const config = statusConfig[status] || statusConfig[CONNECTION_STATUS.DISCONNECTED]; @@ -68,10 +77,12 @@ const ConnectionStatusBar = ({ { warning: 'orange.50', error: 'red.50', + success: 'green.50', }[config.status], { - warning: 'orange.900', - error: 'red.900', + warning: 'rgba(251, 146, 60, 0.15)', // orange with transparency + error: 'rgba(239, 68, 68, 0.15)', // red with transparency + success: 'rgba(34, 197, 94, 0.15)', // green with transparency }[config.status] ); @@ -79,7 +90,7 @@ const ConnectionStatusBar = ({ @@ -116,8 +128,8 @@ const ConnectionStatusBar = ({ )} - {/* 关闭按钮(仅失败状态显示) */} - {status === CONNECTION_STATUS.FAILED && onClose && ( + {/* 关闭按钮(所有非正常状态都显示) */} + {status !== CONNECTION_STATUS.CONNECTED && onClose && ( { }); const [connectionStatus, setConnectionStatus] = useState(CONNECTION_STATUS.CONNECTED); const [reconnectAttempt, setReconnectAttempt] = useState(0); + const [maxReconnectAttempts, setMaxReconnectAttempts] = useState(Infinity); const audioRef = useRef(null); + const reconnectedTimerRef = useRef(null); // 用于自动消失 RECONNECTED 状态 // 初始化音频 useEffect(() => { @@ -412,21 +423,35 @@ export const NotificationProvider = ({ children }) => { // 连接 socket socket.connect(); + // 获取并保存最大重连次数 + const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity; + setMaxReconnectAttempts(maxAttempts); + logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts }); + // 监听连接状态 socket.on('connect', () => { + const wasDisconnected = connectionStatus !== CONNECTION_STATUS.CONNECTED; setIsConnected(true); - setConnectionStatus(CONNECTION_STATUS.CONNECTED); setReconnectAttempt(0); - logger.info('NotificationContext', 'Socket connected'); + logger.info('NotificationContext', 'Socket connected', { wasDisconnected }); - // 显示重连成功提示(如果之前断开过) - if (connectionStatus !== CONNECTION_STATUS.CONNECTED) { - toast({ - title: '已重新连接', - status: 'success', - duration: 2000, - isClosable: true, - }); + // 如果之前断开过,显示 RECONNECTED 状态2秒后自动消失 + if (wasDisconnected) { + setConnectionStatus(CONNECTION_STATUS.RECONNECTED); + logger.info('NotificationContext', 'Reconnected, will auto-dismiss in 2s'); + + // 清除之前的定时器 + if (reconnectedTimerRef.current) { + clearTimeout(reconnectedTimerRef.current); + } + + // 2秒后自动变回 CONNECTED + reconnectedTimerRef.current = setTimeout(() => { + setConnectionStatus(CONNECTION_STATUS.CONNECTED); + logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status'); + }, 2000); + } else { + setConnectionStatus(CONNECTION_STATUS.CONNECTED); } // 如果使用 mock,可以启动定期推送 @@ -449,11 +474,10 @@ export const NotificationProvider = ({ children }) => { logger.error('NotificationContext', 'Socket connect_error', error); setConnectionStatus(CONNECTION_STATUS.RECONNECTING); - // 获取重连次数(仅 Real Socket 有) - if (SOCKET_TYPE === 'REAL') { - const attempts = socket.getReconnectAttempts?.() || 0; - setReconnectAttempt(attempts); - } + // 获取重连次数(Real 和 Mock 都支持) + const attempts = socket.getReconnectAttempts?.() || 0; + setReconnectAttempt(attempts); + logger.info('NotificationContext', 'Reconnection attempt', { attempts, socketType: SOCKET_TYPE }); }); // 监听重连失败 @@ -594,6 +618,7 @@ export const NotificationProvider = ({ children }) => { browserPermission, connectionStatus, reconnectAttempt, + maxReconnectAttempts, addNotification, removeNotification, clearAllNotifications, diff --git a/src/services/mockSocketService.js b/src/services/mockSocketService.js index e3897d76..a41c3d87 100644 --- a/src/services/mockSocketService.js +++ b/src/services/mockSocketService.js @@ -306,6 +306,19 @@ class MockSocketService { this.listeners = new Map(); this.intervals = []; this.messageQueue = []; + this.reconnectAttempts = 0; + this.customReconnectTimer = null; + this.failConnection = false; // 是否模拟连接失败 + } + + /** + * 计算指数退避延迟(Mock 模式使用更短的时间便于测试) + * 第1次: 10秒, 第2次: 20秒, 第3次: 40秒, 第4次及以后: 40秒 + */ + getReconnectionDelay(attempt) { + const delays = [10000, 20000, 40000]; // 10s, 20s, 40s (缩短10倍便于测试) + const index = Math.min(attempt - 1, delays.length - 1); + return delays[index]; } /** @@ -321,7 +334,31 @@ class MockSocketService { // 模拟连接延迟 setTimeout(() => { + // 检查是否应该模拟连接失败 + if (this.failConnection) { + logger.warn('mockSocketService', 'Simulated connection failure'); + + // 触发连接错误事件 + this.emit('connect_error', { + message: 'Mock connection error for testing', + timestamp: Date.now(), + }); + + // 安排下次重连(会继续失败,直到 failConnection 被清除) + this.scheduleReconnection(); + return; + } + + // 正常连接成功 this.connected = true; + this.reconnectAttempts = 0; + + // 清除自定义重连定时器 + if (this.customReconnectTimer) { + clearTimeout(this.customReconnectTimer); + this.customReconnectTimer = null; + } + logger.info('mockSocketService', 'Mock socket connected successfully'); // 触发连接成功事件 @@ -329,22 +366,25 @@ class MockSocketService { // 在连接后3秒发送欢迎消息 setTimeout(() => { - this.emit('new_event', { - type: 'system_notification', - severity: 'info', - title: '连接成功', - message: '实时消息推送服务已启动 (Mock 模式)', - timestamp: Date.now(), - autoClose: 5000, - }); + if (this.connected) { + this.emit('new_event', { + type: 'system_notification', + severity: 'info', + title: '连接成功', + message: '实时消息推送服务已启动 (Mock 模式)', + timestamp: Date.now(), + autoClose: 5000, + }); + } }, 3000); }, 1000); } /** * 断开连接 + * @param {boolean} triggerReconnect - 是否触发自动重连(模拟意外断开) */ - disconnect() { + disconnect(triggerReconnect = false) { if (!this.connected) { return; } @@ -355,8 +395,130 @@ class MockSocketService { this.intervals.forEach(interval => clearInterval(interval)); this.intervals = []; + const wasConnected = this.connected; this.connected = false; - this.emit('disconnect', { timestamp: Date.now() }); + this.emit('disconnect', { + timestamp: Date.now(), + reason: triggerReconnect ? 'transport close' : 'io client disconnect' + }); + + // 如果需要触发重连(模拟意外断开) + if (triggerReconnect && wasConnected) { + this.scheduleReconnection(); + } else { + // 清除重连定时器 + if (this.customReconnectTimer) { + clearTimeout(this.customReconnectTimer); + this.customReconnectTimer = null; + } + this.reconnectAttempts = 0; + } + } + + /** + * 使用指数退避策略安排重连 + */ + scheduleReconnection() { + // 清除之前的定时器 + if (this.customReconnectTimer) { + clearTimeout(this.customReconnectTimer); + } + + this.reconnectAttempts++; + const delay = this.getReconnectionDelay(this.reconnectAttempts); + logger.info('mockSocketService', `Scheduling reconnection in ${delay / 1000}s (attempt ${this.reconnectAttempts})`); + + // 触发 connect_error 事件通知UI + this.emit('connect_error', { + message: 'Mock connection error for testing', + timestamp: Date.now(), + }); + + this.customReconnectTimer = setTimeout(() => { + if (!this.connected) { + logger.info('mockSocketService', 'Attempting reconnection...', { + attempt: this.reconnectAttempts, + }); + this.connect(); + } + }, delay); + } + + /** + * 手动重连 + * @returns {boolean} 是否触发重连 + */ + reconnect() { + if (this.connected) { + logger.info('mockSocketService', 'Already connected, no need to reconnect'); + return false; + } + + logger.info('mockSocketService', 'Manually triggering reconnection...'); + + // 清除自动重连定时器 + if (this.customReconnectTimer) { + clearTimeout(this.customReconnectTimer); + this.customReconnectTimer = null; + } + + // 重置重连计数 + this.reconnectAttempts = 0; + + // 立即触发重连 + this.connect(); + + return true; + } + + /** + * 模拟意外断线(测试用) + * @param {number} duration - 断线持续时间(毫秒),0表示需要手动重连 + */ + simulateDisconnection(duration = 0) { + logger.info('mockSocketService', `Simulating disconnection${duration > 0 ? ` for ${duration}ms` : ' (manual reconnect required)'}...`); + + if (duration > 0) { + // 短暂断线,自动重连 + this.disconnect(true); + } else { + // 需要手动重连 + this.disconnect(false); + } + } + + /** + * 模拟持续连接失败(测试用) + * 连接会一直失败,直到调用 allowReconnection() + */ + simulateConnectionFailure() { + logger.info('mockSocketService', '🚫 Simulating persistent connection failure...'); + logger.info('mockSocketService', 'Connection will keep failing until allowReconnection() is called'); + + // 设置失败标志 + this.failConnection = true; + + // 如果当前已连接,先断开并触发重连(会失败) + if (this.connected) { + this.disconnect(true); + } else { + // 如果未连接,直接触发一次连接尝试(会失败) + this.connect(); + } + } + + /** + * 允许重连成功(测试用) + * 清除连接失败标志,下次重连将会成功 + */ + allowReconnection() { + logger.info('mockSocketService', '✅ Allowing reconnection to succeed...'); + logger.info('mockSocketService', 'Next reconnection attempt will succeed'); + + // 清除失败标志 + this.failConnection = false; + + // 不立即重连,等待自动重连或手动重连 } /** @@ -488,9 +650,74 @@ class MockSocketService { isConnected() { return this.connected; } + + /** + * 获取当前重连尝试次数 + */ + getReconnectAttempts() { + return this.reconnectAttempts; + } + + /** + * 获取最大重连次数(Mock 模式无限重试) + */ + getMaxReconnectAttempts() { + return Infinity; + } } // 导出单例 export const mockSocketService = new MockSocketService(); +// 开发模式下添加全局测试函数 +if (process.env.NODE_ENV === 'development') { + window.__mockSocket = { + // 模拟意外断线(自动重连成功) + simulateDisconnection: () => { + logger.info('mockSocketService', '🔌 Simulating disconnection (will auto-reconnect)...'); + mockSocketService.simulateDisconnection(1); // 触发自动重连 + }, + + // 模拟持续连接失败 + simulateConnectionFailure: () => { + logger.info('mockSocketService', '🚫 Simulating connection failure (will keep retrying)...'); + mockSocketService.simulateConnectionFailure(); + }, + + // 允许重连成功 + allowReconnection: () => { + logger.info('mockSocketService', '✅ Allowing next reconnection to succeed...'); + mockSocketService.allowReconnection(); + }, + + // 获取连接状态 + isConnected: () => { + const connected = mockSocketService.isConnected(); + logger.info('mockSocketService', `Connection status: ${connected ? '✅ Connected' : '❌ Disconnected'}`); + return connected; + }, + + // 手动重连 + reconnect: () => { + logger.info('mockSocketService', '🔄 Manually triggering reconnection...'); + return mockSocketService.reconnect(); + }, + + // 获取重连尝试次数 + getAttempts: () => { + const attempts = mockSocketService.getReconnectAttempts(); + logger.info('mockSocketService', `Current reconnection attempts: ${attempts}`); + return attempts; + }, + }; + + logger.info('mockSocketService', '💡 Mock Socket test functions available:'); + logger.info('mockSocketService', ' __mockSocket.simulateDisconnection() - 模拟断线(自动重连成功)'); + logger.info('mockSocketService', ' __mockSocket.simulateConnectionFailure() - 模拟连接失败(持续失败)'); + logger.info('mockSocketService', ' __mockSocket.allowReconnection() - 允许重连成功'); + logger.info('mockSocketService', ' __mockSocket.isConnected() - 查看连接状态'); + logger.info('mockSocketService', ' __mockSocket.reconnect() - 手动重连'); + logger.info('mockSocketService', ' __mockSocket.getAttempts() - 查看重连次数'); +} + export default mockSocketService; diff --git a/src/services/socketService.js b/src/services/socketService.js index ed0eb5b2..a41218c1 100644 --- a/src/services/socketService.js +++ b/src/services/socketService.js @@ -14,7 +14,18 @@ class SocketService { this.socket = null; this.connected = false; this.reconnectAttempts = 0; - this.maxReconnectAttempts = 5; + this.maxReconnectAttempts = Infinity; // 无限重试 + this.customReconnectTimer = null; // 自定义重连定时器 + } + + /** + * 计算指数退避延迟 + * 第1次: 60秒, 第2次: 120秒, 第3次: 240秒, 第4次及以后: 240秒 + */ + getReconnectionDelay(attempt) { + const delays = [60000, 120000, 240000]; // 1min, 2min, 4min + const index = Math.min(attempt - 1, delays.length - 1); + return delays[index]; } /** @@ -29,13 +40,10 @@ class SocketService { logger.info('socketService', 'Connecting to Socket.IO server...', { url: API_BASE_URL }); - // 创建 socket 连接 + // 创建 socket 连接 - 禁用 Socket.IO 自带的重连机制,使用自定义指数退避 this.socket = io(API_BASE_URL, { transports: ['websocket', 'polling'], - reconnection: true, - reconnectionDelay: 1000, - reconnectionDelayMax: 5000, - reconnectionAttempts: this.maxReconnectAttempts, + reconnection: false, // 禁用自动重连,改用自定义策略 timeout: 20000, autoConnect: true, withCredentials: true, // 允许携带认证信息 @@ -46,6 +54,13 @@ class SocketService { this.socket.on('connect', () => { this.connected = true; this.reconnectAttempts = 0; + + // 清除自定义重连定时器 + if (this.customReconnectTimer) { + clearTimeout(this.customReconnectTimer); + this.customReconnectTimer = null; + } + logger.info('socketService', 'Socket.IO connected successfully', { socketId: this.socket.id, }); @@ -53,8 +68,14 @@ class SocketService { // 监听断开连接 this.socket.on('disconnect', (reason) => { + const wasConnected = this.connected; this.connected = false; logger.warn('socketService', 'Socket.IO disconnected', { reason }); + + // 如果是意外断开(非主动断开),触发自定义重连 + if (wasConnected && reason !== 'io client disconnect') { + this.scheduleReconnection(); + } }); // 监听连接错误 @@ -62,30 +83,33 @@ class SocketService { this.reconnectAttempts++; logger.error('socketService', 'connect_error', error, { attempts: this.reconnectAttempts, - maxAttempts: this.maxReconnectAttempts, }); - if (this.reconnectAttempts >= this.maxReconnectAttempts) { - logger.error('socketService', 'Max reconnection attempts reached'); - this.socket.close(); + // 使用指数退避策略安排下次重连 + this.scheduleReconnection(); + }); + } + + /** + * 使用指数退避策略安排重连 + */ + scheduleReconnection() { + // 清除之前的定时器 + if (this.customReconnectTimer) { + clearTimeout(this.customReconnectTimer); + } + + const delay = this.getReconnectionDelay(this.reconnectAttempts); + logger.info('socketService', `Scheduling reconnection in ${delay / 1000}s (attempt ${this.reconnectAttempts})`); + + this.customReconnectTimer = setTimeout(() => { + if (!this.connected && this.socket) { + logger.info('socketService', 'Attempting reconnection...', { + attempt: this.reconnectAttempts, + }); + this.socket.connect(); } - }); - - // 监听重连尝试 - this.socket.io.on('reconnect_attempt', (attemptNumber) => { - logger.info('socketService', 'Reconnection attempt', { attemptNumber }); - }); - - // 监听重连成功 - this.socket.io.on('reconnect', (attemptNumber) => { - this.reconnectAttempts = 0; - logger.info('socketService', 'Reconnected successfully', { attemptNumber }); - }); - - // 监听重连失败 - this.socket.io.on('reconnect_failed', () => { - logger.error('socketService', 'Reconnection failed after max attempts'); - }); + }, delay); } /** @@ -97,9 +121,17 @@ class SocketService { } logger.info('socketService', 'Disconnecting from Socket.IO server...'); + + // 清除自定义重连定时器 + if (this.customReconnectTimer) { + clearTimeout(this.customReconnectTimer); + this.customReconnectTimer = null; + } + this.socket.disconnect(); this.socket = null; this.connected = false; + this.reconnectAttempts = 0; } /** @@ -204,10 +236,16 @@ class SocketService { logger.info('socketService', 'Manually triggering reconnection...'); + // 清除自动重连定时器 + if (this.customReconnectTimer) { + clearTimeout(this.customReconnectTimer); + this.customReconnectTimer = null; + } + // 重置重连计数 this.reconnectAttempts = 0; - // 触发重连 + // 立即触发重连 this.socket.connect(); return true;