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;