feat: 修改文件 │

│                                                                                           │
     │ 1. src/services/socketService.js - 指数退避 + 无限重试                                    │
     │ 2. src/components/ConnectionStatusBar/index.js - UI 优化 + 自动消失                       │
     │ 3. src/App.js - handleClose 实现 + dismissed 状态管理                                     │
     │ 4. src/contexts/NotificationContext.js - 添加成功状态检测                                 │
     │ 5. NOTIFICATION_SYSTEM.md - v2.11.0 文档更新
This commit is contained in:
zdl
2025-10-21 18:34:38 +08:00
parent 09c9273190
commit 23188d5690
6 changed files with 518 additions and 68 deletions

View File

@@ -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 (
<ConnectionStatusBar
status={connectionStatus}
reconnectAttempt={reconnectAttempt}
maxReconnectAttempts={5}
maxReconnectAttempts={maxReconnectAttempts}
onRetry={retryConnection}
onClose={handleClose}
isDismissed={isDismissed}
/>
);
}

View File

@@ -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 = ({
<Slide
direction="top"
in={shouldShow}
style={{ zIndex: 10000 }}
style={{ zIndex: 1050 }} // 降低 zIndex避免遮挡 modal
>
<Alert
status={config.status}
@@ -87,8 +98,9 @@ const ConnectionStatusBar = ({
bg={bg}
borderBottom="1px solid"
borderColor={useColorModeValue('gray.200', 'gray.700')}
py={3}
py={2} // 减小高度,更紧凑
px={{ base: 4, md: 6 }}
opacity={0.95} // 半透明
>
<AlertIcon />
<Box flex="1">
@@ -116,8 +128,8 @@ const ConnectionStatusBar = ({
</Button>
)}
{/* 关闭按钮(仅失败状态显示) */}
{status === CONNECTION_STATUS.FAILED && onClose && (
{/* 关闭按钮(所有非正常状态显示) */}
{status !== CONNECTION_STATUS.CONNECTED && onClose && (
<CloseButton
onClick={onClose}
size="sm"

View File

@@ -1,6 +1,14 @@
// src/contexts/NotificationContext.js
/**
* 通知上下文 - 管理实时消息推送和通知显示
*
* 环境说明:
* - SOCKET_TYPE === 'REAL': 使用真实 Socket.IO 连接(生产环境),连接到 wss://valuefrontier.cn
* - SOCKET_TYPE === 'MOCK': 使用模拟 Socket 服务(开发环境),用于本地测试
*
* 环境切换:
* - 设置 REACT_APP_ENABLE_MOCK=true 或 REACT_APP_USE_MOCK_SOCKET=true 使用 MOCK 模式
* - 否则使用 REAL 模式连接生产环境
*/
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
@@ -19,6 +27,7 @@ const CONNECTION_STATUS = {
DISCONNECTED: 'disconnected',
RECONNECTING: 'reconnecting',
FAILED: 'failed',
RECONNECTED: 'reconnected', // 重连成功显示2秒后自动变回 CONNECTED
};
// 创建通知上下文
@@ -46,7 +55,9 @@ export const NotificationProvider = ({ children }) => {
});
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,

View File

@@ -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;

View File

@@ -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;