diff --git a/.env.production b/.env.production
index ef983024..9b52a6cc 100644
--- a/.env.production
+++ b/.env.production
@@ -16,6 +16,15 @@ NODE_ENV=production
# Mock 配置(生产环境禁用 Mock)
REACT_APP_ENABLE_MOCK=false
+# 🔧 调试模式(生产环境临时调试用)
+# 开启后会在全局暴露 window.__DEBUG__ 调试 API
+# ⚠️ 警告: 调试模式会记录所有 API 请求/响应,调试完成后请立即关闭!
+# 使用方法:
+# 1. 设置为 true 并重新构建
+# 2. 在浏览器控制台使用 window.__DEBUG__.help() 查看命令
+# 3. 调试完成后设置为 false 并重新构建
+REACT_APP_ENABLE_DEBUG=false
+
# 后端 API 地址(生产环境)
REACT_APP_API_URL=http://49.232.185.254:5001
@@ -40,3 +49,18 @@ TSC_COMPILE_ON_ERROR=true
IMAGE_INLINE_SIZE_LIMIT=10000
# Node.js 内存限制(适用于大型项目)
NODE_OPTIONS=--max_old_space_size=4096
+
+# ========================================
+# Bytedesk 客服系统配置
+# ========================================
+# Bytedesk 服务器地址
+REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
+
+# 组织 ID(从管理后台获取)
+REACT_APP_BYTEDESK_ORG=df_org_uid
+
+# 工作组 ID(从管理后台获取)
+REACT_APP_BYTEDESK_SID=df_wg_aftersales
+
+# 客服类型(2=人工客服, 1=机器人)
+REACT_APP_BYTEDESK_TYPE=2
diff --git a/BYTEDESK_INTEGRATION_FILES.txt b/BYTEDESK_INTEGRATION_FILES.txt
new file mode 100644
index 00000000..9088fcc9
--- /dev/null
+++ b/BYTEDESK_INTEGRATION_FILES.txt
@@ -0,0 +1,49 @@
+# Bytedesk 客服系统集成文件
+
+以下文件和目录属于客服系统集成功能,未提交到当前分支:
+
+## 1. Dify 机器人控制逻辑
+**位置**: public/index.html
+**状态**: 已存入 stash
+**Stash ID**: stash@{0}
+**说明**: 根据路径控制 Dify 机器人显示(已设置为完全不显示,只使用 Bytedesk 客服)
+
+## 2. Bytedesk 集成代码
+**位置**: src/bytedesk-integration/
+**状态**: 未跟踪文件(需要手动管理)
+**内容**:
+ - .env.bytedesk.example - Bytedesk 环境变量配置示例
+ - App.jsx.example - 集成 Bytedesk 的示例代码
+ - components/ - Bytedesk 相关组件
+ - config/ - Bytedesk 配置文件
+ - 前端工程师集成手册.md - 详细集成文档
+
+## 恢复方法
+
+### 恢复 public/index.html 的改动:
+```bash
+git stash apply stash@{0}
+```
+
+### 使用 Bytedesk 集成代码:
+```bash
+# 查看集成手册
+cat src/bytedesk-integration/前端工程师集成手册.md
+
+# 复制示例配置
+cp src/bytedesk-integration/.env.bytedesk.example .env.bytedesk
+cp src/bytedesk-integration/App.jsx.example src/App.jsx
+```
+
+## 注意事项
+
+⚠️ **重要提示:**
+- `src/bytedesk-integration/` 目录中的文件是未跟踪的(untracked)
+- 如果需要提交客服功能,需要先添加到 git:
+ ```bash
+ git add src/bytedesk-integration/
+ git commit -m "feat: 集成 Bytedesk 客服系统"
+ ```
+
+- 当前分支(feature_bugfix/251110_event)专注于非客服功能
+- 建议在单独的分支中开发客服功能
diff --git a/craco.config.js b/craco.config.js
index 9072329a..be5e0264 100644
--- a/craco.config.js
+++ b/craco.config.js
@@ -110,6 +110,9 @@ module.exports = {
...webpackConfig.resolve,
alias: {
...webpackConfig.resolve.alias,
+ // 强制 'debug' 模块解析到 node_modules(避免与 src/devtools/ 冲突)
+ 'debug': path.resolve(__dirname, 'node_modules/debug'),
+
// 根目录别名
'@': path.resolve(__dirname, 'src'),
@@ -119,6 +122,7 @@ module.exports = {
'@constants': path.resolve(__dirname, 'src/constants'),
'@contexts': path.resolve(__dirname, 'src/contexts'),
'@data': path.resolve(__dirname, 'src/data'),
+ '@devtools': path.resolve(__dirname, 'src/devtools'),
'@hooks': path.resolve(__dirname, 'src/hooks'),
'@layouts': path.resolve(__dirname, 'src/layouts'),
'@lib': path.resolve(__dirname, 'src/lib'),
@@ -263,6 +267,34 @@ module.exports = {
logLevel: 'debug',
pathRewrite: { '^/concept-api': '' },
},
+ '/bytedesk-api': {
+ target: 'http://43.143.189.195',
+ changeOrigin: true,
+ secure: false,
+ logLevel: 'debug',
+ pathRewrite: { '^/bytedesk-api': '' },
+ },
+ '/chat': {
+ target: 'http://43.143.189.195',
+ changeOrigin: true,
+ secure: false,
+ logLevel: 'debug',
+ // 不需要pathRewrite,保留/chat路径
+ },
+ '/config': {
+ target: 'http://43.143.189.195',
+ changeOrigin: true,
+ secure: false,
+ logLevel: 'debug',
+ // 不需要pathRewrite,保留/config路径
+ },
+ '/visitor': {
+ target: 'http://43.143.189.195',
+ changeOrigin: true,
+ secure: false,
+ logLevel: 'debug',
+ // 不需要pathRewrite,保留/visitor路径
+ },
},
}),
},
diff --git a/docs/DARK_MODE_TEST.md b/docs/DARK_MODE_TEST.md
index 965b0a2c..97a73720 100644
--- a/docs/DARK_MODE_TEST.md
+++ b/docs/DARK_MODE_TEST.md
@@ -48,16 +48,18 @@ npm start
### 3. 触发通知
-**Mock 模式**(默认):
-- 等待 60 秒,会自动推送 1-2 条通知
-- 或在控制台执行:
+**测试通知**:
+- 使用调试 API 发送测试通知:
```javascript
- import { mockSocketService } from './services/mockSocketService.js';
- mockSocketService.sendTestNotification();
- ```
+ // 方式1: 使用调试工具(推荐)
+ window.__DEBUG__.notification.forceNotification({
+ title: '测试通知',
+ body: '验证暗色模式下的通知样式'
+ });
-**Real 模式**:
-- 创建测试事件(运行后端测试脚本)
+ // 方式2: 等待后端真实推送
+ // 确保已连接后端,等待真实事件推送
+ ```
### 4. 验证效果
@@ -139,61 +141,46 @@ npm start
### 手动触发各类型通知
-```javascript
-// 引入服务
-import { mockSocketService } from './services/mockSocketService.js';
-import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from './constants/notificationTypes.js';
+> **注意**: Mock Socket 已移除,请使用调试工具或真实后端测试。
-// 测试公告通知(蓝色)
-mockSocketService.sendTestNotification({
- type: NOTIFICATION_TYPES.ANNOUNCEMENT,
- priority: PRIORITY_LEVELS.IMPORTANT,
+```javascript
+// 使用调试工具测试不同类型的通知
+// 确保已开启调试模式:REACT_APP_ENABLE_DEBUG=true
+
+// 测试公告通知
+window.__DEBUG__.notification.forceNotification({
title: '测试公告通知',
- content: '这是暗色模式下的蓝色通知',
- timestamp: Date.now(),
+ body: '这是暗色模式下的蓝色通知',
+ tag: 'test_announcement',
autoClose: 0,
});
// 测试股票上涨(红色)
-mockSocketService.sendTestNotification({
- type: NOTIFICATION_TYPES.STOCK_ALERT,
- priority: PRIORITY_LEVELS.URGENT,
- title: '测试股票上涨',
- content: '宁德时代 +5.2%',
- extra: { priceChange: '+5.2%' },
- timestamp: Date.now(),
- autoClose: 0,
+window.__DEBUG__.notification.forceNotification({
+ title: '🔴 测试股票上涨',
+ body: '宁德时代 +5.2%',
+ tag: 'test_stock_up',
});
// 测试股票下跌(绿色)
-mockSocketService.sendTestNotification({
- type: NOTIFICATION_TYPES.STOCK_ALERT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- title: '测试股票下跌',
- content: '比亚迪 -3.8%',
- extra: { priceChange: '-3.8%' },
- timestamp: Date.now(),
- autoClose: 0,
+window.__DEBUG__.notification.forceNotification({
+ title: '🟢 测试股票下跌',
+ body: '比亚迪 -3.8%',
+ tag: 'test_stock_down',
});
// 测试事件动向(橙色)
-mockSocketService.sendTestNotification({
- type: NOTIFICATION_TYPES.EVENT_ALERT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- title: '测试事件动向',
- content: '央行宣布降准',
- timestamp: Date.now(),
- autoClose: 0,
+window.__DEBUG__.notification.forceNotification({
+ title: '🟠 测试事件动向',
+ body: '央行宣布降准',
+ tag: 'test_event',
});
// 测试分析报告(紫色)
-mockSocketService.sendTestNotification({
- type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
- priority: PRIORITY_LEVELS.NORMAL,
- title: '测试分析报告',
- content: '医药行业深度报告',
- timestamp: Date.now(),
- autoClose: 0,
+window.__DEBUG__.notification.forceNotification({
+ title: '🟣 测试分析报告',
+ body: '医药行业深度报告',
+ tag: 'test_report',
});
```
diff --git a/docs/MESSAGE_PUSH_INTEGRATION_TEST.md b/docs/MESSAGE_PUSH_INTEGRATION_TEST.md
index a368b484..6d1e7510 100644
--- a/docs/MESSAGE_PUSH_INTEGRATION_TEST.md
+++ b/docs/MESSAGE_PUSH_INTEGRATION_TEST.md
@@ -330,13 +330,14 @@ if (Notification.permission === 'granted') {
### 关键文件
-- `src/services/mockSocketService.js` - Mock Socket 服务
-- `src/services/socketService.js` - 真实 Socket.IO 服务
-- `src/services/socket/index.js` - 统一导出
-- `src/contexts/NotificationContext.js` - 通知上下文(含适配器)
+- `src/services/socketService.js` - Socket.IO 服务
+- `src/services/socket/index.js` - Socket 服务导出
+- `src/contexts/NotificationContext.js` - 通知上下文
- `src/hooks/useEventNotifications.js` - React Hook
- `src/views/Community/components/EventList.js` - 事件列表集成
+> **注意**: `mockSocketService.js` 已移除(2025-01-10),现仅使用真实 Socket 连接。
+
### 数据流
```
diff --git a/docs/NOTIFICATION_SYSTEM.md b/docs/NOTIFICATION_SYSTEM.md
index b042e458..0fc20c6a 100644
--- a/docs/NOTIFICATION_SYSTEM.md
+++ b/docs/NOTIFICATION_SYSTEM.md
@@ -1,8 +1,10 @@
# 实时消息推送系统 - 完整技术文档
> **版本**: v2.11.0
-> **更新日期**: 2025-01-07
+> **更新日期**: 2025-01-10
> **文档类型**: 快速入门 + 完整技术规格
+>
+> ⚠️ **重要更新**: Mock Socket 已移除(2025-01-10),文档中关于 `mockSocketService` 的内容仅供历史参考。
---
diff --git a/public/service-worker.js b/public/service-worker.js
index 6dd051da..8570fbd1 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -2,10 +2,10 @@
/**
* Service Worker for Browser Notifications
* 主要功能:支持浏览器通知的稳定运行
+ *
+ * 注意:此 Service Worker 仅用于通知功能,不拦截任何 HTTP 请求
*/
-const CACHE_NAME = 'valuefrontier-v1';
-
// Service Worker 安装事件
self.addEventListener('install', (event) => {
console.log('[Service Worker] Installing...');
@@ -56,18 +56,6 @@ self.addEventListener('notificationclose', (event) => {
console.log('[Service Worker] Notification closed:', event.notification.tag);
});
-// Fetch 事件 - 基础的网络优先策略
-self.addEventListener('fetch', (event) => {
- // 对于通知相关的资源,使用网络优先策略
- event.respondWith(
- fetch(event.request)
- .catch(() => {
- // 网络失败时,尝试从缓存获取
- return caches.match(event.request);
- })
- );
-});
-
// 推送消息事件(预留,用于未来的 Push API 集成)
self.addEventListener('push', (event) => {
console.log('[Service Worker] Push message received:', event);
diff --git a/src/components/NotificationTestTool/index.js b/src/components/NotificationTestTool/index.js
index 51f09b64..b70f2bc5 100644
--- a/src/components/NotificationTestTool/index.js
+++ b/src/components/NotificationTestTool/index.js
@@ -26,7 +26,6 @@ import {
} from '@chakra-ui/react';
import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp, MdCampaign, MdTrendingUp, MdArticle, MdAssessment, MdWarning } from 'react-icons/md';
import { useNotification } from '../../contexts/NotificationContext';
-import { SOCKET_TYPE } from '../../services/socket';
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../../constants/notificationTypes';
const NotificationTestTool = () => {
@@ -295,7 +294,7 @@ const NotificationTestTool = () => {
{isConnected ? 'Connected' : 'Disconnected'}
- {SOCKET_TYPE}
+ REAL
浏览器: {getPermissionLabel()}
diff --git a/src/contexts/AuthContext.js b/src/contexts/AuthContext.js
index 89703509..b1ae1879 100755
--- a/src/contexts/AuthContext.js
+++ b/src/contexts/AuthContext.js
@@ -58,7 +58,9 @@ export const AuthProvider = ({ children }) => {
// 创建超时控制器
const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
+ const timeoutId = setTimeout(() => {
+ controller.abort(new Error('Session check timeout after 5 seconds'));
+ }, 5000); // 5秒超时
const response = await fetch(`/api/auth/session`, {
method: 'GET',
@@ -96,8 +98,18 @@ export const AuthProvider = ({ children }) => {
setIsAuthenticated((prev) => prev === false ? prev : false);
}
} catch (error) {
- logger.error('AuthContext', 'checkSession', error);
- // 网络错误或超时,设置为未登录状态
+ // ✅ 区分AbortError和真实错误
+ if (error.name === 'AbortError') {
+ logger.debug('AuthContext', 'Session check aborted', {
+ reason: error.message || 'Request cancelled',
+ isTimeout: error.message?.includes('timeout')
+ });
+ // AbortError不改变登录状态(保持原状态)
+ return;
+ }
+
+ // 只有真实错误才标记为未登录
+ logger.error('AuthContext', 'checkSession failed', error);
setUser((prev) => prev === null ? prev : null);
setIsAuthenticated((prev) => prev === false ? prev : false);
} finally {
@@ -108,7 +120,16 @@ export const AuthProvider = ({ children }) => {
// ⚡ 初始化时检查Session - 并行执行,不阻塞页面渲染
useEffect(() => {
+ const controller = new AbortController();
+
+ // 传递signal给checkSession(需要修改checkSession签名)
+ // 暂时使用原有方式,但添加cleanup防止组件卸载时的内存泄漏
checkSession(); // 直接调用,与页面渲染并行
+
+ // ✅ Cleanup: 组件卸载时abort可能正在进行的请求
+ return () => {
+ controller.abort(new Error('AuthProvider unmounted'));
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
diff --git a/src/contexts/NotificationContext.js b/src/contexts/NotificationContext.js
index 518ec9c5..e2e9d55e 100644
--- a/src/contexts/NotificationContext.js
+++ b/src/contexts/NotificationContext.js
@@ -2,20 +2,15 @@
/**
* 通知上下文 - 管理实时消息推送和通知显示
*
- * 环境说明:
- * - 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 模式连接生产环境
+ * 使用真实 Socket.IO 连接到后端服务器
+ * 连接地址配置在环境变量中 (REACT_APP_API_URL)
*/
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
import { useToast, Box, HStack, Text, Button, CloseButton, VStack, Icon } from '@chakra-ui/react';
import { BellIcon } from '@chakra-ui/icons';
import { logger } from '../utils/logger';
-import socket, { SOCKET_TYPE } from '../services/socket';
+import socket from '../services/socket';
import notificationSound from '../assets/sounds/notification.wav';
import { browserNotificationService } from '../services/browserNotificationService';
import { notificationMetricsService } from '../services/notificationMetricsService';
@@ -62,6 +57,7 @@ export const NotificationProvider = ({ children }) => {
const reconnectedTimerRef = useRef(null); // 用于自动消失 RECONNECTED 状态
const processedEventIds = useRef(new Set()); // 用于Socket层去重,记录已处理的事件ID
const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID,避免内存泄漏
+ const notificationTimers = useRef(new Map()); // 跟踪所有通知的自动关闭定时器
// ⚡ 使用权限引导管理 Hook
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
@@ -71,9 +67,20 @@ export const NotificationProvider = ({ children }) => {
try {
audioRef.current = new Audio(notificationSound);
audioRef.current.volume = 0.5;
+ logger.info('NotificationContext', 'Audio initialized');
} catch (error) {
logger.error('NotificationContext', 'Audio initialization failed', error);
}
+
+ // 清理函数:释放音频资源
+ return () => {
+ if (audioRef.current) {
+ audioRef.current.pause();
+ audioRef.current.src = '';
+ audioRef.current = null;
+ logger.info('NotificationContext', 'Audio resources cleaned up');
+ }
+ };
}, []);
/**
@@ -104,6 +111,13 @@ export const NotificationProvider = ({ children }) => {
const removeNotification = useCallback((id, wasClicked = false) => {
logger.info('NotificationContext', 'Removing notification', { id, wasClicked });
+ // 清理对应的定时器
+ if (notificationTimers.current.has(id)) {
+ clearTimeout(notificationTimers.current.get(id));
+ notificationTimers.current.delete(id);
+ logger.info('NotificationContext', 'Cleared auto-close timer', { id });
+ }
+
// 监控埋点:追踪关闭(非点击的情况)
setNotifications(prev => {
const notification = prev.find(n => n.id === id);
@@ -119,6 +133,14 @@ export const NotificationProvider = ({ children }) => {
*/
const clearAllNotifications = useCallback(() => {
logger.info('NotificationContext', 'Clearing all notifications');
+
+ // 清理所有定时器
+ notificationTimers.current.forEach((timerId, id) => {
+ clearTimeout(timerId);
+ logger.info('NotificationContext', 'Cleared timer during clear all', { id });
+ });
+ notificationTimers.current.clear();
+
setNotifications([]);
}, []);
@@ -446,9 +468,16 @@ export const NotificationProvider = ({ children }) => {
// 自动关闭
if (newNotification.autoClose && newNotification.autoClose > 0) {
- setTimeout(() => {
+ const timerId = setTimeout(() => {
removeNotification(newNotification.id);
}, newNotification.autoClose);
+
+ // 将定时器ID保存到Map中
+ notificationTimers.current.set(newNotification.id, timerId);
+ logger.info('NotificationContext', 'Set auto-close timer', {
+ id: newNotification.id,
+ delay: newNotification.autoClose
+ });
}
}, [playNotificationSound, removeNotification]);
@@ -548,34 +577,11 @@ export const NotificationProvider = ({ children }) => {
const isPageHidden = document.hidden; // 页面是否在后台
- // ========== 原分发策略(按优先级区分)- 已废弃 ==========
- // 策略 1: 紧急通知 - 双重保障(浏览器 + 网页)
- // if (priority === PRIORITY_LEVELS.URGENT) {
- // logger.info('NotificationContext', 'Urgent notification: sending browser + web');
- // // 总是发送浏览器通知
- // sendBrowserNotification(newNotification);
- // // 如果在前台,也显示网页通知
- // if (!isPageHidden) {
- // addWebNotification(newNotification);
- // }
- // }
- // 策略 2: 重要通知 - 智能切换(后台=浏览器,前台=网页)
- // else if (priority === PRIORITY_LEVELS.IMPORTANT) {
- // if (isPageHidden) {
- // logger.info('NotificationContext', 'Important notification (background): sending browser');
- // sendBrowserNotification(newNotification);
- // } else {
- // logger.info('NotificationContext', 'Important notification (foreground): sending web');
- // addWebNotification(newNotification);
- // }
- // }
- // 策略 3: 普通通知 - 仅网页通知
- // else {
- // logger.info('NotificationContext', 'Normal notification: sending web only');
- // addWebNotification(newNotification);
- // }
-
- // ========== 新分发策略(仅区分前后台) ==========
+ // ========== 通知分发策略(区分前后台) ==========
+ // 策略: 根据页面可见性智能分发通知
+ // - 页面在后台: 发送浏览器通知(系统级提醒)
+ // - 页面在前台: 发送网页通知(页面内 Toast)
+ // 注: 不再区分优先级,统一使用前后台策略
if (isPageHidden) {
// 页面在后台:发送浏览器通知
logger.info('NotificationContext', 'Page hidden: sending browser notification');
@@ -592,7 +598,7 @@ export const NotificationProvider = ({ children }) => {
// 连接到 Socket 服务
useEffect(() => {
logger.info('NotificationContext', 'Initializing socket connection...');
- console.log(`%c[NotificationContext] Initializing socket (type: ${SOCKET_TYPE})`, 'color: #673AB7; font-weight: bold;');
+ console.log('%c[NotificationContext] Initializing socket connection', 'color: #673AB7; font-weight: bold;');
// ✅ 第一步: 注册所有事件监听器
console.log('%c[NotificationContext] Step 1: Registering event listeners...', 'color: #673AB7;');
@@ -624,30 +630,22 @@ export const NotificationProvider = ({ children }) => {
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
}
- // 如果使用 mock,可以启动定期推送
- if (SOCKET_TYPE === 'MOCK') {
- // 启动模拟推送:使用配置的间隔和数量
- const { interval, maxBatch } = NOTIFICATION_CONFIG.mockPush;
- socket.startMockPush(interval, maxBatch);
- logger.info('NotificationContext', 'Mock push started', { interval, maxBatch });
- } else {
- // ✅ 真实模式下,订阅事件推送
- console.log('%c[NotificationContext] 🔔 订阅事件推送...', 'color: #FF9800; font-weight: bold;');
+ // 订阅事件推送
+ console.log('%c[NotificationContext] 🔔 订阅事件推送...', 'color: #FF9800; font-weight: bold;');
- if (socket.subscribeToEvents) {
- socket.subscribeToEvents({
- eventType: 'all',
- importance: 'all',
- onSubscribed: (data) => {
- console.log('%c[NotificationContext] ✅ 订阅成功!', 'color: #4CAF50; font-weight: bold;');
- console.log('[NotificationContext] 订阅确认:', data);
- logger.info('NotificationContext', 'Events subscribed', data);
- },
- // ⚠️ 不需要 onNewEvent 回调,因为 NotificationContext 已经通过 socket.on('new_event') 监听
- });
- } else {
- console.warn('[NotificationContext] ⚠️ socket.subscribeToEvents 方法不可用');
- }
+ if (socket.subscribeToEvents) {
+ socket.subscribeToEvents({
+ eventType: 'all',
+ importance: 'all',
+ onSubscribed: (data) => {
+ console.log('%c[NotificationContext] ✅ 订阅成功!', 'color: #4CAF50; font-weight: bold;');
+ console.log('[NotificationContext] 订阅确认:', data);
+ logger.info('NotificationContext', 'Events subscribed', data);
+ },
+ // ⚠️ 不需要 onNewEvent 回调,因为 NotificationContext 已经通过 socket.on('new_event') 监听
+ });
+ } else {
+ console.error('[NotificationContext] ❌ socket.subscribeToEvents 方法不可用');
}
});
@@ -662,10 +660,10 @@ export const NotificationProvider = ({ children }) => {
logger.error('NotificationContext', 'Socket connect_error', error);
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
- // 获取重连次数(Real 和 Mock 都支持)
+ // 获取重连次数
const attempts = socket.getReconnectAttempts?.() || 0;
setReconnectAttempt(attempts);
- logger.info('NotificationContext', 'Reconnection attempt', { attempts, socketType: SOCKET_TYPE });
+ logger.info('NotificationContext', 'Reconnection attempt', { attempts });
});
// 监听重连失败
@@ -696,7 +694,18 @@ export const NotificationProvider = ({ children }) => {
logger.info('NotificationContext', 'Received new event', data);
// ========== Socket层去重检查 ==========
- const eventId = data.id || `${data.type}_${data.publishTime}`;
+ // 生成更健壮的事件ID
+ const eventId = data.id ||
+ `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+ // 如果缺少原始ID,记录警告
+ if (!data.id) {
+ logger.warn('NotificationContext', 'Event missing ID, generated fallback', {
+ eventId,
+ eventType: data.type,
+ title: data.title
+ });
+ }
if (processedEventIds.current.has(eventId)) {
logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId });
@@ -752,11 +761,19 @@ export const NotificationProvider = ({ children }) => {
return () => {
logger.info('NotificationContext', 'Cleaning up socket connection');
- // 如果是 mock service,停止推送
- if (SOCKET_TYPE === 'MOCK') {
- socket.stopMockPush();
+ // 清理 reconnected 状态定时器
+ if (reconnectedTimerRef.current) {
+ clearTimeout(reconnectedTimerRef.current);
+ reconnectedTimerRef.current = null;
}
+ // 清理所有通知的自动关闭定时器
+ notificationTimers.current.forEach((timerId, id) => {
+ clearTimeout(timerId);
+ logger.info('NotificationContext', 'Cleared timer during cleanup', { id });
+ });
+ notificationTimers.current.clear();
+
socket.off('connect');
socket.off('disconnect');
socket.off('connect_error');
@@ -776,11 +793,7 @@ export const NotificationProvider = ({ children }) => {
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible' && !isConnected && connectionStatus === CONNECTION_STATUS.FAILED) {
logger.info('NotificationContext', 'Tab refocused, attempting auto-reconnect');
- if (SOCKET_TYPE === 'REAL') {
- socket.reconnect?.();
- } else {
- socket.connect();
- }
+ socket.reconnect?.();
}
};
@@ -806,11 +819,7 @@ export const NotificationProvider = ({ children }) => {
isClosable: true,
});
- if (SOCKET_TYPE === 'REAL') {
- socket.reconnect?.();
- } else {
- socket.connect();
- }
+ socket.reconnect?.();
}
};
@@ -842,14 +851,51 @@ export const NotificationProvider = ({ children }) => {
const retryConnection = useCallback(() => {
logger.info('NotificationContext', 'Manual reconnection triggered');
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
-
- if (SOCKET_TYPE === 'REAL') {
- socket.reconnect?.();
- } else {
- socket.connect();
- }
+ socket.reconnect?.();
}, []);
+ /**
+ * 同步浏览器通知权限状态
+ * 场景:
+ * 1. 用户在其他标签页授权后返回
+ * 2. 用户在浏览器设置中修改权限
+ * 3. 页面长时间打开后权限状态变化
+ */
+ useEffect(() => {
+ const checkPermission = () => {
+ const current = browserNotificationService.getPermissionStatus();
+ if (current !== browserPermission) {
+ logger.info('NotificationContext', 'Browser permission changed', {
+ old: browserPermission,
+ new: current
+ });
+ setBrowserPermission(current);
+
+ // 如果权限被授予,显示成功提示
+ if (current === 'granted' && browserPermission !== 'granted') {
+ toast({
+ title: '桌面通知已开启',
+ description: '您现在可以在后台接收重要通知',
+ status: 'success',
+ duration: 3000,
+ isClosable: true,
+ });
+ }
+ }
+ };
+
+ // 页面聚焦时检查
+ window.addEventListener('focus', checkPermission);
+
+ // 定期检查(可选,用于捕获浏览器设置中的变化)
+ const intervalId = setInterval(checkPermission, 30000); // 每30秒检查一次
+
+ return () => {
+ window.removeEventListener('focus', checkPermission);
+ clearInterval(intervalId);
+ };
+ }, [browserPermission, toast]);
+
const value = {
notifications,
isConnected,
diff --git a/src/devtools/apiDebugger.js b/src/devtools/apiDebugger.js
new file mode 100644
index 00000000..2256ae39
--- /dev/null
+++ b/src/devtools/apiDebugger.js
@@ -0,0 +1,253 @@
+// src/debug/apiDebugger.js
+/**
+ * API 调试工具
+ * 生产环境临时调试使用,后期可整体删除 src/debug/ 目录
+ */
+
+import axios from 'axios';
+import { getApiBase } from '@utils/apiConfig';
+
+class ApiDebugger {
+ constructor() {
+ this.requestLog = [];
+ this.maxLogSize = 100;
+ this.isLogging = true;
+ }
+
+ /**
+ * 初始化 Axios 拦截器
+ */
+ init() {
+ // 请求拦截器
+ axios.interceptors.request.use(
+ (config) => {
+ if (this.isLogging) {
+ const logEntry = {
+ type: 'request',
+ timestamp: new Date().toISOString(),
+ method: config.method.toUpperCase(),
+ url: config.url,
+ baseURL: config.baseURL,
+ fullURL: this._getFullURL(config),
+ headers: config.headers,
+ data: config.data,
+ params: config.params,
+ };
+
+ this._addLog(logEntry);
+
+ console.log(
+ `%c[API Request] ${logEntry.method} ${logEntry.fullURL}`,
+ 'color: #2196F3; font-weight: bold;',
+ {
+ headers: config.headers,
+ data: config.data,
+ params: config.params,
+ }
+ );
+ }
+ return config;
+ },
+ (error) => {
+ console.error('[API Request Error]', error);
+ return Promise.reject(error);
+ }
+ );
+
+ // 响应拦截器
+ axios.interceptors.response.use(
+ (response) => {
+ if (this.isLogging) {
+ const logEntry = {
+ type: 'response',
+ timestamp: new Date().toISOString(),
+ method: response.config.method.toUpperCase(),
+ url: response.config.url,
+ fullURL: this._getFullURL(response.config),
+ status: response.status,
+ statusText: response.statusText,
+ headers: response.headers,
+ data: response.data,
+ };
+
+ this._addLog(logEntry);
+
+ console.log(
+ `%c[API Response] ${logEntry.method} ${logEntry.fullURL} - ${logEntry.status}`,
+ 'color: #4CAF50; font-weight: bold;',
+ {
+ status: response.status,
+ data: response.data,
+ headers: response.headers,
+ }
+ );
+ }
+ return response;
+ },
+ (error) => {
+ if (this.isLogging) {
+ const logEntry = {
+ type: 'error',
+ timestamp: new Date().toISOString(),
+ method: error.config?.method?.toUpperCase() || 'UNKNOWN',
+ url: error.config?.url || 'UNKNOWN',
+ fullURL: error.config ? this._getFullURL(error.config) : 'UNKNOWN',
+ status: error.response?.status,
+ statusText: error.response?.statusText,
+ message: error.message,
+ data: error.response?.data,
+ };
+
+ this._addLog(logEntry);
+
+ console.error(
+ `%c[API Error] ${logEntry.method} ${logEntry.fullURL}`,
+ 'color: #F44336; font-weight: bold;',
+ {
+ status: error.response?.status,
+ message: error.message,
+ data: error.response?.data,
+ }
+ );
+ }
+ return Promise.reject(error);
+ }
+ );
+
+ console.log('%c[API Debugger] Initialized', 'color: #FF9800; font-weight: bold;');
+ }
+
+ /**
+ * 获取完整 URL
+ */
+ _getFullURL(config) {
+ const baseURL = config.baseURL || '';
+ const url = config.url || '';
+ const fullURL = baseURL + url;
+
+ // 添加查询参数
+ if (config.params) {
+ const params = new URLSearchParams(config.params).toString();
+ return params ? `${fullURL}?${params}` : fullURL;
+ }
+
+ return fullURL;
+ }
+
+ /**
+ * 添加日志
+ */
+ _addLog(entry) {
+ this.requestLog.unshift(entry);
+ if (this.requestLog.length > this.maxLogSize) {
+ this.requestLog = this.requestLog.slice(0, this.maxLogSize);
+ }
+ }
+
+ /**
+ * 获取所有日志
+ */
+ getLogs(type = 'all') {
+ if (type === 'all') {
+ return this.requestLog;
+ }
+ return this.requestLog.filter((log) => log.type === type);
+ }
+
+ /**
+ * 清空日志
+ */
+ clearLogs() {
+ this.requestLog = [];
+ console.log('[API Debugger] Logs cleared');
+ }
+
+ /**
+ * 导出日志为 JSON
+ */
+ exportLogs() {
+ const blob = new Blob([JSON.stringify(this.requestLog, null, 2)], {
+ type: 'application/json',
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `api-logs-${Date.now()}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ console.log('[API Debugger] Logs exported');
+ }
+
+ /**
+ * 打印日志统计
+ */
+ printStats() {
+ const stats = {
+ total: this.requestLog.length,
+ requests: this.requestLog.filter((log) => log.type === 'request').length,
+ responses: this.requestLog.filter((log) => log.type === 'response').length,
+ errors: this.requestLog.filter((log) => log.type === 'error').length,
+ };
+
+ console.table(stats);
+ return stats;
+ }
+
+ /**
+ * 手动发送 API 请求(测试用)
+ */
+ async testRequest(method, endpoint, data = null, config = {}) {
+ const apiBase = getApiBase();
+ const url = `${apiBase}${endpoint}`;
+
+ console.log(`[API Debugger] Testing ${method.toUpperCase()} ${url}`);
+
+ try {
+ const response = await axios({
+ method,
+ url,
+ data,
+ ...config,
+ });
+
+ console.log('[API Debugger] Test succeeded:', response.data);
+ return response.data;
+ } catch (error) {
+ console.error('[API Debugger] Test failed:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * 开启/关闭日志记录
+ */
+ toggleLogging(enabled) {
+ this.isLogging = enabled;
+ console.log(`[API Debugger] Logging ${enabled ? 'enabled' : 'disabled'}`);
+ }
+
+ /**
+ * 获取最近的错误
+ */
+ getRecentErrors(count = 10) {
+ return this.requestLog.filter((log) => log.type === 'error').slice(0, count);
+ }
+
+ /**
+ * 按 URL 过滤日志
+ */
+ getLogsByURL(urlPattern) {
+ return this.requestLog.filter((log) => log.url && log.url.includes(urlPattern));
+ }
+
+ /**
+ * 按状态码过滤日志
+ */
+ getLogsByStatus(status) {
+ return this.requestLog.filter((log) => log.status === status);
+ }
+}
+
+// 导出单例
+export const apiDebugger = new ApiDebugger();
+export default apiDebugger;
diff --git a/src/devtools/index.js b/src/devtools/index.js
new file mode 100644
index 00000000..bbd88bbe
--- /dev/null
+++ b/src/devtools/index.js
@@ -0,0 +1,268 @@
+// src/debug/index.js
+/**
+ * 调试工具统一入口
+ *
+ * 使用方法:
+ * 1. 开启调试: 在 .env.production 中设置 REACT_APP_ENABLE_DEBUG=true
+ * 2. 使用控制台命令: window.__DEBUG__.api.getLogs()
+ * 3. 后期移除: 删除整个 src/debug/ 目录 + 从 src/index.js 移除导入
+ *
+ * 全局 API:
+ * - window.__DEBUG__ - 调试 API 主对象
+ * - window.__DEBUG__.api - API 调试工具
+ * - window.__DEBUG__.notification - 通知调试工具
+ * - window.__DEBUG__.socket - Socket 调试工具
+ * - window.__DEBUG__.help() - 显示帮助信息
+ * - window.__DEBUG__.exportAll() - 导出所有日志
+ */
+
+import { apiDebugger } from './apiDebugger';
+import { notificationDebugger } from './notificationDebugger';
+import { socketDebugger } from './socketDebugger';
+
+class DebugToolkit {
+ constructor() {
+ this.api = apiDebugger;
+ this.notification = notificationDebugger;
+ this.socket = socketDebugger;
+ }
+
+ /**
+ * 初始化所有调试工具
+ */
+ init() {
+ console.log(
+ '%c╔════════════════════════════════════════════════════════════════╗',
+ 'color: #FF9800; font-weight: bold;'
+ );
+ console.log(
+ '%c║ 🔧 调试模式已启用 (Debug Mode Enabled) ║',
+ 'color: #FF9800; font-weight: bold;'
+ );
+ console.log(
+ '%c╚════════════════════════════════════════════════════════════════╝',
+ 'color: #FF9800; font-weight: bold;'
+ );
+ console.log('');
+
+ // 初始化各个调试工具
+ this.api.init();
+ this.notification.init();
+ this.socket.init();
+
+ // 暴露到全局
+ window.__DEBUG__ = this;
+
+ // 打印帮助信息
+ this._printWelcome();
+ }
+
+ /**
+ * 打印欢迎信息
+ */
+ _printWelcome() {
+ console.log('%c📚 调试工具使用指南:', 'color: #2196F3; font-weight: bold; font-size: 14px;');
+ console.log('');
+ console.log('%c1️⃣ API 调试:', 'color: #2196F3; font-weight: bold;');
+ console.log(' __DEBUG__.api.getLogs() - 获取所有 API 日志');
+ console.log(' __DEBUG__.api.getRecentErrors() - 获取最近的错误');
+ console.log(' __DEBUG__.api.exportLogs() - 导出 API 日志');
+ console.log(' __DEBUG__.api.testRequest(method, endpoint, data) - 测试 API 请求');
+ console.log('');
+ console.log('%c2️⃣ 通知调试:', 'color: #9C27B0; font-weight: bold;');
+ console.log(' __DEBUG__.notification.getLogs() - 获取所有通知日志');
+ console.log(' __DEBUG__.notification.forceNotification() - 发送测试通知');
+ console.log(' __DEBUG__.notification.checkPermission() - 检查通知权限');
+ console.log(' __DEBUG__.notification.exportLogs() - 导出通知日志');
+ console.log('');
+ console.log('%c3️⃣ Socket 调试:', 'color: #00BCD4; font-weight: bold;');
+ console.log(' __DEBUG__.socket.getLogs() - 获取所有 Socket 日志');
+ console.log(' __DEBUG__.socket.getStatus() - 获取连接状态');
+ console.log(' __DEBUG__.socket.reconnect() - 手动重连');
+ console.log(' __DEBUG__.socket.exportLogs() - 导出 Socket 日志');
+ console.log('');
+ console.log('%c4️⃣ 通用命令:', 'color: #4CAF50; font-weight: bold;');
+ console.log(' __DEBUG__.help() - 显示帮助信息');
+ console.log(' __DEBUG__.exportAll() - 导出所有日志');
+ console.log(' __DEBUG__.printStats() - 打印所有统计信息');
+ console.log(' __DEBUG__.clearAll() - 清空所有日志');
+ console.log('');
+ console.log(
+ '%c⚠️ 警告: 调试模式会记录所有 API 请求和响应,请勿在生产环境长期开启!',
+ 'color: #F44336; font-weight: bold;'
+ );
+ console.log('');
+ }
+
+ /**
+ * 显示帮助信息
+ */
+ help() {
+ this._printWelcome();
+ }
+
+ /**
+ * 导出所有日志
+ */
+ exportAll() {
+ console.log('[Debug Toolkit] Exporting all logs...');
+
+ const allLogs = {
+ timestamp: new Date().toISOString(),
+ api: this.api.getLogs(),
+ notification: this.notification.getLogs(),
+ socket: this.socket.getLogs(),
+ };
+
+ const blob = new Blob([JSON.stringify(allLogs, null, 2)], {
+ type: 'application/json',
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `debug-all-logs-${Date.now()}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+
+ console.log('[Debug Toolkit] ✅ All logs exported');
+ }
+
+ /**
+ * 打印所有统计信息
+ */
+ printStats() {
+ console.log('\n%c=== 📊 调试统计信息 ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
+ console.log('\n%c[API 统计]', 'color: #2196F3; font-weight: bold;');
+ const apiStats = this.api.printStats();
+
+ console.log('\n%c[通知统计]', 'color: #9C27B0; font-weight: bold;');
+ const notificationStats = this.notification.printStats();
+
+ console.log('\n%c[Socket 统计]', 'color: #00BCD4; font-weight: bold;');
+ const socketStats = this.socket.printStats();
+
+ return {
+ api: apiStats,
+ notification: notificationStats,
+ socket: socketStats,
+ };
+ }
+
+ /**
+ * 清空所有日志
+ */
+ clearAll() {
+ console.log('[Debug Toolkit] Clearing all logs...');
+ this.api.clearLogs();
+ this.notification.clearLogs();
+ this.socket.clearLogs();
+ console.log('[Debug Toolkit] ✅ All logs cleared');
+ }
+
+ /**
+ * 快速诊断(检查所有系统状态)
+ */
+ diagnose() {
+ console.log('\n%c=== 🔍 系统诊断 ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
+
+ // 1. Socket 状态
+ console.log('\n%c[1/3] Socket 状态', 'color: #00BCD4; font-weight: bold;');
+ const socketStatus = this.socket.getStatus();
+
+ // 2. 通知权限
+ console.log('\n%c[2/3] 通知权限', 'color: #9C27B0; font-weight: bold;');
+ const notificationStatus = this.notification.checkPermission();
+
+ // 3. API 错误
+ console.log('\n%c[3/3] 最近的 API 错误', 'color: #F44336; font-weight: bold;');
+ const recentErrors = this.api.getRecentErrors(5);
+ if (recentErrors.length > 0) {
+ console.table(
+ recentErrors.map((err) => ({
+ 时间: err.timestamp,
+ 方法: err.method,
+ URL: err.url,
+ 状态码: err.status,
+ 错误信息: err.message,
+ }))
+ );
+ } else {
+ console.log('✅ 没有 API 错误');
+ }
+
+ // 4. 汇总报告
+ const report = {
+ timestamp: new Date().toISOString(),
+ socket: socketStatus,
+ notification: notificationStatus,
+ apiErrors: recentErrors.length,
+ };
+
+ console.log('\n%c=== 诊断报告 ===', 'color: #4CAF50; font-weight: bold;');
+ console.table(report);
+
+ return report;
+ }
+
+ /**
+ * 性能监控
+ */
+ performance() {
+ console.log('\n%c=== ⚡ 性能监控 ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
+
+ // 计算 API 平均响应时间
+ const apiLogs = this.api.getLogs();
+ const responseTimes = [];
+
+ for (let i = 0; i < apiLogs.length - 1; i++) {
+ const log = apiLogs[i];
+ const prevLog = apiLogs[i + 1];
+
+ if (
+ log.type === 'response' &&
+ prevLog.type === 'request' &&
+ log.url === prevLog.url
+ ) {
+ const responseTime =
+ new Date(log.timestamp).getTime() - new Date(prevLog.timestamp).getTime();
+ responseTimes.push({
+ url: log.url,
+ method: log.method,
+ time: responseTime,
+ });
+ }
+ }
+
+ if (responseTimes.length > 0) {
+ const avgTime =
+ responseTimes.reduce((sum, item) => sum + item.time, 0) / responseTimes.length;
+ const maxTime = Math.max(...responseTimes.map((item) => item.time));
+ const minTime = Math.min(...responseTimes.map((item) => item.time));
+
+ console.log('API 响应时间统计:');
+ console.table({
+ 平均响应时间: `${avgTime.toFixed(2)}ms`,
+ 最快响应: `${minTime}ms`,
+ 最慢响应: `${maxTime}ms`,
+ 请求总数: responseTimes.length,
+ });
+
+ // 显示最慢的 5 个请求
+ console.log('\n最慢的 5 个请求:');
+ const slowest = responseTimes.sort((a, b) => b.time - a.time).slice(0, 5);
+ console.table(
+ slowest.map((item) => ({
+ 方法: item.method,
+ URL: item.url,
+ 响应时间: `${item.time}ms`,
+ }))
+ );
+ } else {
+ console.log('暂无性能数据');
+ }
+ }
+}
+
+// 导出单例
+export const debugToolkit = new DebugToolkit();
+export default debugToolkit;
diff --git a/src/devtools/notificationDebugger.js b/src/devtools/notificationDebugger.js
new file mode 100644
index 00000000..f2929a22
--- /dev/null
+++ b/src/devtools/notificationDebugger.js
@@ -0,0 +1,166 @@
+// src/debug/notificationDebugger.js
+/**
+ * 通知系统调试工具
+ * 扩展现有的 window.__NOTIFY_DEBUG__,添加更多生产环境调试能力
+ */
+
+import { browserNotificationService } from '@services/browserNotificationService';
+
+class NotificationDebugger {
+ constructor() {
+ this.eventLog = [];
+ this.maxLogSize = 100;
+ }
+
+ /**
+ * 初始化调试工具
+ */
+ init() {
+ console.log('%c[Notification Debugger] Initialized', 'color: #FF9800; font-weight: bold;');
+ }
+
+ /**
+ * 记录通知事件
+ */
+ logEvent(eventType, data) {
+ const logEntry = {
+ type: eventType,
+ timestamp: new Date().toISOString(),
+ data,
+ };
+
+ this.eventLog.unshift(logEntry);
+ if (this.eventLog.length > this.maxLogSize) {
+ this.eventLog = this.eventLog.slice(0, this.maxLogSize);
+ }
+
+ console.log(
+ `%c[Notification Event] ${eventType}`,
+ 'color: #9C27B0; font-weight: bold;',
+ data
+ );
+ }
+
+ /**
+ * 获取所有事件日志
+ */
+ getLogs() {
+ return this.eventLog;
+ }
+
+ /**
+ * 清空日志
+ */
+ clearLogs() {
+ this.eventLog = [];
+ console.log('[Notification Debugger] Logs cleared');
+ }
+
+ /**
+ * 导出日志
+ */
+ exportLogs() {
+ const blob = new Blob([JSON.stringify(this.eventLog, null, 2)], {
+ type: 'application/json',
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `notification-logs-${Date.now()}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ console.log('[Notification Debugger] Logs exported');
+ }
+
+ /**
+ * 强制发送浏览器通知(测试用)
+ */
+ forceNotification(options = {}) {
+ const defaultOptions = {
+ title: '🧪 测试通知',
+ body: `测试时间: ${new Date().toLocaleString()}`,
+ tag: `test_${Date.now()}`,
+ requireInteraction: false,
+ autoClose: 5000,
+ };
+
+ const finalOptions = { ...defaultOptions, ...options };
+
+ console.log('[Notification Debugger] Sending test notification:', finalOptions);
+
+ const notification = browserNotificationService.sendNotification(finalOptions);
+
+ if (notification) {
+ console.log('[Notification Debugger] ✅ Notification sent successfully');
+ } else {
+ console.error('[Notification Debugger] ❌ Failed to send notification');
+ }
+
+ return notification;
+ }
+
+ /**
+ * 检查通知权限状态
+ */
+ checkPermission() {
+ const permission = browserNotificationService.getPermissionStatus();
+ const isSupported = browserNotificationService.isSupported();
+
+ const status = {
+ supported: isSupported,
+ permission,
+ canSend: isSupported && permission === 'granted',
+ };
+
+ console.table(status);
+ return status;
+ }
+
+ /**
+ * 请求通知权限
+ */
+ async requestPermission() {
+ console.log('[Notification Debugger] Requesting notification permission...');
+ const result = await browserNotificationService.requestPermission();
+ console.log(`[Notification Debugger] Permission result: ${result}`);
+ return result;
+ }
+
+ /**
+ * 打印事件统计
+ */
+ printStats() {
+ const stats = {
+ total: this.eventLog.length,
+ byType: {},
+ };
+
+ this.eventLog.forEach((log) => {
+ stats.byType[log.type] = (stats.byType[log.type] || 0) + 1;
+ });
+
+ console.log('=== Notification Stats ===');
+ console.table(stats.byType);
+ console.log(`Total events: ${stats.total}`);
+
+ return stats;
+ }
+
+ /**
+ * 按类型过滤日志
+ */
+ getLogsByType(eventType) {
+ return this.eventLog.filter((log) => log.type === eventType);
+ }
+
+ /**
+ * 获取最近的事件
+ */
+ getRecentEvents(count = 10) {
+ return this.eventLog.slice(0, count);
+ }
+}
+
+// 导出单例
+export const notificationDebugger = new NotificationDebugger();
+export default notificationDebugger;
diff --git a/src/devtools/socketDebugger.js b/src/devtools/socketDebugger.js
new file mode 100644
index 00000000..3312ad68
--- /dev/null
+++ b/src/devtools/socketDebugger.js
@@ -0,0 +1,194 @@
+// src/debug/socketDebugger.js
+/**
+ * Socket 调试工具
+ * 扩展现有的 window.__SOCKET_DEBUG__,添加更多生产环境调试能力
+ */
+
+import { socket } from '@services/socket';
+
+class SocketDebugger {
+ constructor() {
+ this.eventLog = [];
+ this.maxLogSize = 100;
+ }
+
+ /**
+ * 初始化调试工具
+ */
+ init() {
+ // 监听所有 Socket 事件
+ this._attachEventListeners();
+ console.log('%c[Socket Debugger] Initialized', 'color: #FF9800; font-weight: bold;');
+ }
+
+ /**
+ * 附加事件监听器
+ */
+ _attachEventListeners() {
+ const events = [
+ 'connect',
+ 'disconnect',
+ 'connect_error',
+ 'reconnect',
+ 'reconnect_failed',
+ 'new_event',
+ 'system_notification',
+ ];
+
+ events.forEach((event) => {
+ socket.on(event, (data) => {
+ this.logEvent(event, data);
+ });
+ });
+ }
+
+ /**
+ * 记录 Socket 事件
+ */
+ logEvent(eventType, data) {
+ const logEntry = {
+ type: eventType,
+ timestamp: new Date().toISOString(),
+ data,
+ };
+
+ this.eventLog.unshift(logEntry);
+ if (this.eventLog.length > this.maxLogSize) {
+ this.eventLog = this.eventLog.slice(0, this.maxLogSize);
+ }
+
+ console.log(
+ `%c[Socket Event] ${eventType}`,
+ 'color: #00BCD4; font-weight: bold;',
+ data
+ );
+ }
+
+ /**
+ * 获取所有事件日志
+ */
+ getLogs() {
+ return this.eventLog;
+ }
+
+ /**
+ * 清空日志
+ */
+ clearLogs() {
+ this.eventLog = [];
+ console.log('[Socket Debugger] Logs cleared');
+ }
+
+ /**
+ * 导出日志
+ */
+ exportLogs() {
+ const blob = new Blob([JSON.stringify(this.eventLog, null, 2)], {
+ type: 'application/json',
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `socket-logs-${Date.now()}.json`;
+ a.click();
+ URL.revokeObjectURL(url);
+ console.log('[Socket Debugger] Logs exported');
+ }
+
+ /**
+ * 获取连接状态
+ */
+ getStatus() {
+ const status = {
+ connected: socket.connected || false,
+ type: window.SOCKET_TYPE || 'UNKNOWN',
+ reconnectAttempts: socket.getReconnectAttempts?.() || 0,
+ maxReconnectAttempts: socket.getMaxReconnectAttempts?.() || Infinity,
+ };
+
+ console.table(status);
+ return status;
+ }
+
+ /**
+ * 手动触发连接
+ */
+ connect() {
+ console.log('[Socket Debugger] Manually connecting...');
+ socket.connect();
+ }
+
+ /**
+ * 手动断开连接
+ */
+ disconnect() {
+ console.log('[Socket Debugger] Manually disconnecting...');
+ socket.disconnect();
+ }
+
+ /**
+ * 手动重连
+ */
+ reconnect() {
+ console.log('[Socket Debugger] Manually reconnecting...');
+ socket.disconnect();
+ setTimeout(() => {
+ socket.connect();
+ }, 1000);
+ }
+
+ /**
+ * 发送测试事件
+ */
+ emitTest(eventName, data = {}) {
+ console.log(`[Socket Debugger] Emitting test event: ${eventName}`, data);
+ socket.emit(eventName, data);
+ }
+
+ /**
+ * 打印事件统计
+ */
+ printStats() {
+ const stats = {
+ total: this.eventLog.length,
+ byType: {},
+ };
+
+ this.eventLog.forEach((log) => {
+ stats.byType[log.type] = (stats.byType[log.type] || 0) + 1;
+ });
+
+ console.log('=== Socket Stats ===');
+ console.table(stats.byType);
+ console.log(`Total events: ${stats.total}`);
+
+ return stats;
+ }
+
+ /**
+ * 按类型过滤日志
+ */
+ getLogsByType(eventType) {
+ return this.eventLog.filter((log) => log.type === eventType);
+ }
+
+ /**
+ * 获取最近的事件
+ */
+ getRecentEvents(count = 10) {
+ return this.eventLog.slice(0, count);
+ }
+
+ /**
+ * 获取错误事件
+ */
+ getErrors() {
+ return this.eventLog.filter(
+ (log) => log.type === 'connect_error' || log.type === 'reconnect_failed'
+ );
+ }
+}
+
+// 导出单例
+export const socketDebugger = new SocketDebugger();
+export default socketDebugger;
diff --git a/src/index.js b/src/index.js
index c4350120..d2c327b0 100755
--- a/src/index.js
+++ b/src/index.js
@@ -13,6 +13,19 @@ import App from './App';
import { browserNotificationService } from './services/browserNotificationService';
window.browserNotificationService = browserNotificationService;
+// 🔧 条件导入调试工具(生产环境可选)
+// 开启方式: 在 .env 文件中设置 REACT_APP_ENABLE_DEBUG=true
+// 移除方式: 删除此段代码 + 删除 src/devtools/ 目录
+if (process.env.REACT_APP_ENABLE_DEBUG === 'true') {
+ import('./devtools').then(({ debugToolkit }) => {
+ debugToolkit.init();
+ console.log(
+ '%c✅ 调试工具已加载!使用 window.__DEBUG__.help() 查看命令',
+ 'color: #4CAF50; font-weight: bold; font-size: 14px;'
+ );
+ });
+}
+
// 注册 Service Worker(用于支持浏览器通知)
function registerServiceWorker() {
// ⚠️ Mock 模式下跳过 Service Worker 注册(避免与 MSW 冲突)
diff --git a/src/services/mockSocketService.js b/src/services/mockSocketService.js
deleted file mode 100644
index 9e69bc94..00000000
--- a/src/services/mockSocketService.js
+++ /dev/null
@@ -1,916 +0,0 @@
-// src/services/mockSocketService.js
-/**
- * Mock Socket 服务 - 用于开发环境模拟实时推送
- * 模拟金融资讯、事件动向、分析报告等实时消息推送
- */
-
-import { logger } from '../utils/logger';
-import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../constants/notificationTypes';
-
-// 模拟金融资讯数据
-const mockFinancialNews = [
- // ========== 公告通知 ==========
- {
- type: NOTIFICATION_TYPES.ANNOUNCEMENT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- title: '贵州茅台发布2024年度财报公告',
- content: '2024年度营收同比增长15.2%,净利润创历史新高,董事会建议每10股派息180元',
- publishTime: new Date('2024-03-28T15:30:00').getTime(),
- pushTime: Date.now(),
- isAIGenerated: false,
- clickable: true,
- link: '/event-detail/ann001',
- extra: {
- announcementType: '财报',
- companyCode: '600519',
- companyName: '贵州茅台',
- },
- autoClose: 10000,
- },
- {
- type: NOTIFICATION_TYPES.ANNOUNCEMENT,
- priority: PRIORITY_LEVELS.URGENT,
- title: '宁德时代发布重大资产重组公告',
- content: '公司拟收购某新能源材料公司100%股权,交易金额约120亿元,预计增厚业绩20%',
- publishTime: new Date('2024-03-28T09:00:00').getTime(),
- pushTime: Date.now(),
- isAIGenerated: false,
- clickable: true,
- link: '/event-detail/ann002',
- extra: {
- announcementType: '重组',
- companyCode: '300750',
- companyName: '宁德时代',
- },
- autoClose: 12000,
- },
- {
- type: NOTIFICATION_TYPES.ANNOUNCEMENT,
- priority: PRIORITY_LEVELS.NORMAL,
- title: '中国平安发布分红派息公告',
- content: '2023年度利润分配方案:每10股派发现金红利23.0元(含税),分红率达30.5%',
- publishTime: new Date('2024-03-27T16:00:00').getTime(),
- pushTime: Date.now(),
- isAIGenerated: false,
- clickable: true,
- link: '/event-detail/ann003',
- extra: {
- announcementType: '分红',
- companyCode: '601318',
- companyName: '中国平安',
- },
- autoClose: 10000,
- },
-
- // ========== 股票动向 ==========
- {
- type: NOTIFICATION_TYPES.STOCK_ALERT,
- priority: PRIORITY_LEVELS.URGENT,
- title: '您关注的股票触发预警',
- content: '宁德时代(300750) 当前价格 ¥245.50,盘中涨幅达 +5.2%,已触达您设置的目标价位',
- publishTime: Date.now(),
- pushTime: Date.now(),
- isAIGenerated: false,
- clickable: true,
- link: '/stock-overview?code=300750',
- extra: {
- stockCode: '300750',
- stockName: '宁德时代',
- priceChange: '+5.2%',
- currentPrice: '245.50',
- triggerType: '目标价',
- },
- autoClose: 10000,
- },
- {
- type: NOTIFICATION_TYPES.STOCK_ALERT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- title: '您关注的股票异常波动',
- content: '比亚迪(002594) 5分钟内跌幅达 -3.8%,当前价格 ¥198.20,建议关注',
- publishTime: Date.now(),
- pushTime: Date.now(),
- isAIGenerated: false,
- clickable: true,
- link: '/stock-overview?code=002594',
- extra: {
- stockCode: '002594',
- stockName: '比亚迪',
- priceChange: '-3.8%',
- currentPrice: '198.20',
- triggerType: '异常波动',
- },
- autoClose: 10000,
- },
- {
- type: NOTIFICATION_TYPES.STOCK_ALERT,
- priority: PRIORITY_LEVELS.NORMAL,
- title: '持仓股票表现',
- content: '隆基绿能(601012) 今日表现优异,涨幅 +4.5%,您当前持仓浮盈 +¥8,200',
- publishTime: Date.now(),
- pushTime: Date.now(),
- isAIGenerated: false,
- clickable: true,
- link: '/trading-simulation',
- extra: {
- stockCode: '601012',
- stockName: '隆基绿能',
- priceChange: '+4.5%',
- profit: '+8200',
- },
- autoClose: 8000,
- },
-
- // ========== 事件动向 ==========
- {
- type: NOTIFICATION_TYPES.EVENT_ALERT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- title: '央行宣布降准0.5个百分点',
- content: '中国人民银行宣布下调金融机构存款准备金率0.5个百分点,释放长期资金约1万亿元,利好股市',
- publishTime: new Date('2024-03-28T09:00:00').getTime(),
- pushTime: Date.now(),
- isAIGenerated: false,
- clickable: true,
- link: '/event-detail/evt001',
- extra: {
- eventId: 'evt001',
- relatedStocks: 12,
- impactLevel: '重大利好',
- sectors: ['银行', '地产', '基建'],
- },
- autoClose: 12000,
- },
- {
- type: NOTIFICATION_TYPES.EVENT_ALERT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- title: '新能源汽车补贴政策延期',
- content: '财政部宣布新能源汽车购置补贴政策延长至2024年底,涉及比亚迪、理想汽车等5家龙头企业',
- publishTime: new Date('2024-03-28T10:30:00').getTime(),
- pushTime: Date.now(),
- isAIGenerated: false,
- clickable: true,
- link: '/event-detail/evt002',
- extra: {
- eventId: 'evt002',
- relatedStocks: 5,
- impactLevel: '重大利好',
- sectors: ['新能源汽车'],
- },
- autoClose: 12000,
- },
- {
- type: NOTIFICATION_TYPES.EVENT_ALERT,
- priority: PRIORITY_LEVELS.NORMAL,
- title: '芯片产业扶持政策出台',
- content: '工信部发布《半导体产业发展指导意见》,未来三年投入500亿专项资金支持芯片研发',
- publishTime: new Date('2024-03-27T14:00:00').getTime(),
- pushTime: Date.now(),
- isAIGenerated: false,
- clickable: true,
- link: '/event-detail/evt003',
- extra: {
- eventId: 'evt003',
- relatedStocks: 8,
- impactLevel: '中长期利好',
- sectors: ['半导体', '芯片设计'],
- },
- autoClose: 10000,
- },
-
- // ========== 预测通知 ==========
- {
- type: NOTIFICATION_TYPES.EVENT_ALERT,
- priority: PRIORITY_LEVELS.NORMAL,
- title: '【预测】央行可能宣布降准政策',
- content: '基于最新宏观数据分析,预计央行将在本周宣布降准0.5个百分点,释放长期资金',
- publishTime: Date.now(),
- pushTime: Date.now(),
- isAIGenerated: true,
- clickable: false, // ❌ 不可点击
- link: null,
- extra: {
- isPrediction: true,
- statusHint: '详细报告生成中...',
- relatedPredictionId: 'pred_001',
- },
- autoClose: 15000,
- },
- {
- type: NOTIFICATION_TYPES.EVENT_ALERT,
- priority: PRIORITY_LEVELS.NORMAL,
- title: '【预测】新能源补贴政策或将延期',
- content: '根据政策趋势分析,财政部可能宣布新能源汽车购置补贴政策延长至2025年底',
- publishTime: Date.now(),
- pushTime: Date.now(),
- isAIGenerated: true,
- clickable: false, // ❌ 不可点击
- link: null,
- extra: {
- isPrediction: true,
- statusHint: '详细报告生成中...',
- relatedPredictionId: 'pred_002',
- },
- autoClose: 15000,
- },
-
- // ========== 分析报告 ==========
- {
- type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- title: '医药行业深度报告:创新药迎来政策拐点',
- content: 'CXO板块持续受益于全球创新药研发外包需求,建议关注药明康德、凯莱英等龙头企业',
- publishTime: new Date('2024-03-28T08:00:00').getTime(),
- pushTime: Date.now(),
- author: {
- name: '李明',
- organization: '中信证券',
- },
- isAIGenerated: false,
- clickable: true,
- link: '/forecast-report?id=rpt001',
- extra: {
- reportType: '行业研报',
- industry: '医药',
- rating: '强烈推荐',
- },
- autoClose: 12000,
- },
- {
- type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
- priority: PRIORITY_LEVELS.IMPORTANT,
- title: 'AI产业链投资机会分析',
- content: '随着大模型应用加速落地,算力、数据、应用三大方向均存在投资机会,重点关注海光信息、寒武纪',
- publishTime: new Date('2024-03-28T07:30:00').getTime(),
- pushTime: Date.now(),
- author: {
- name: '王芳',
- organization: '招商证券',
- },
- isAIGenerated: true,
- clickable: true,
- link: '/forecast-report?id=rpt002',
- extra: {
- reportType: '策略报告',
- industry: '人工智能',
- rating: '推荐',
- },
- autoClose: 12000,
- },
- {
- type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
- priority: PRIORITY_LEVELS.NORMAL,
- title: '比亚迪:新能源汽车龙头业绩持续超预期',
- content: '2024年销量目标400万辆,海外市场拓展顺利,维持"买入"评级,目标价280元',
- publishTime: new Date('2024-03-27T09:00:00').getTime(),
- pushTime: Date.now(),
- author: {
- name: '张伟',
- organization: '国泰君安',
- },
- isAIGenerated: false,
- clickable: true,
- link: '/forecast-report?id=rpt003',
- extra: {
- reportType: '公司研报',
- industry: '新能源汽车',
- rating: '买入',
- targetPrice: '280',
- },
- autoClose: 10000,
- },
- {
- type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
- priority: PRIORITY_LEVELS.NORMAL,
- title: '2024年A股市场展望:结构性行情延续',
- content: 'AI应用、高端制造、自主可控三大主线贯穿全年,建议关注科技成长板块配置机会',
- publishTime: new Date('2024-03-26T16:00:00').getTime(),
- pushTime: Date.now(),
- author: {
- name: 'AI分析师',
- organization: '价值前沿',
- },
- isAIGenerated: true,
- clickable: true,
- link: '/forecast-report?id=rpt004',
- extra: {
- reportType: '策略报告',
- industry: '市场策略',
- rating: '谨慎乐观',
- },
- autoClose: 10000,
- },
-];
-
-class MockSocketService {
- constructor() {
- this.connected = false;
- this.connecting = false; // 新增:正在连接标志,防止重复连接
- this.listeners = new Map();
- this.intervals = [];
- this.messageQueue = [];
- this.reconnectAttempts = 0;
- this.customReconnectTimer = null;
- this.failConnection = false; // 是否模拟连接失败
- this.pushPaused = 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];
- }
-
- /**
- * 连接到 mock socket
- */
- connect() {
- // ✅ 防止重复连接
- if (this.connected) {
- logger.warn('mockSocketService', 'Already connected');
- console.log('%c[Mock Socket] Already connected, skipping', 'color: #FF9800; font-weight: bold;');
- return;
- }
-
- if (this.connecting) {
- logger.warn('mockSocketService', 'Connection in progress');
- console.log('%c[Mock Socket] Connection already in progress, skipping', 'color: #FF9800; font-weight: bold;');
- return;
- }
-
- this.connecting = true; // 标记为连接中
- logger.info('mockSocketService', 'Connecting to mock socket service...');
- console.log('%c[Mock Socket] 🔌 Connecting...', 'color: #2196F3; font-weight: bold;');
-
- // 模拟连接延迟
- setTimeout(() => {
- // 检查是否应该模拟连接失败
- if (this.failConnection) {
- this.connecting = false; // 清除连接中标志
- logger.warn('mockSocketService', 'Simulated connection failure');
- console.log('%c[Mock Socket] ❌ Connection failed (simulated)', 'color: #F44336; font-weight: bold;');
-
- // 触发连接错误事件
- this.emit('connect_error', {
- message: 'Mock connection error for testing',
- timestamp: Date.now(),
- });
-
- // 安排下次重连(会继续失败,直到 failConnection 被清除)
- this.scheduleReconnection();
- return;
- }
-
- // 正常连接成功
- this.connected = true;
- this.connecting = false; // 清除连接中标志
- this.reconnectAttempts = 0;
-
- // 清除自定义重连定时器
- if (this.customReconnectTimer) {
- clearTimeout(this.customReconnectTimer);
- this.customReconnectTimer = null;
- }
-
- logger.info('mockSocketService', 'Mock socket connected successfully');
- console.log('%c[Mock Socket] ✅ Connected successfully!', 'color: #4CAF50; font-weight: bold; font-size: 14px;');
- console.log(`%c[Mock Socket] Status: connected=${this.connected}, connecting=${this.connecting}`, 'color: #4CAF50;');
-
- // ✅ 使用 setTimeout(0) 确保监听器已注册后再触发事件
- setTimeout(() => {
- console.log('%c[Mock Socket] Emitting connect event...', 'color: #9C27B0;');
- this.emit('connect', { timestamp: Date.now() });
- console.log('%c[Mock Socket] Connect event emitted', 'color: #9C27B0;');
- }, 0);
-
- // 在连接后3秒发送欢迎消息
- setTimeout(() => {
- 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(triggerReconnect = false) {
- if (!this.connected) {
- return;
- }
-
- logger.info('mockSocketService', 'Disconnecting from mock socket service...');
-
- // 清除所有定时器
- this.intervals.forEach(interval => clearInterval(interval));
- this.intervals = [];
- this.pushPaused = false; // 重置暂停状态
-
- const wasConnected = this.connected;
- this.connected = false;
- 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;
-
- // 不立即重连,等待自动重连或手动重连
- }
-
- /**
- * 监听事件
- * @param {string} event - 事件名称
- * @param {Function} callback - 回调函数
- */
- on(event, callback) {
- if (!this.listeners.has(event)) {
- this.listeners.set(event, []);
- }
- this.listeners.get(event).push(callback);
-
- logger.info('mockSocketService', `Event listener added: ${event}`);
- }
-
- /**
- * 移除事件监听
- * @param {string} event - 事件名称
- * @param {Function} callback - 回调函数
- */
- off(event, callback) {
- if (!this.listeners.has(event)) {
- return;
- }
-
- const callbacks = this.listeners.get(event);
- const index = callbacks.indexOf(callback);
-
- if (index !== -1) {
- callbacks.splice(index, 1);
- logger.info('mockSocketService', `Event listener removed: ${event}`);
- }
-
- // 如果没有监听器了,删除该事件
- if (callbacks.length === 0) {
- this.listeners.delete(event);
- }
- }
-
- /**
- * 触发事件
- * @param {string} event - 事件名称
- * @param {*} data - 事件数据
- */
- emit(event, data) {
- if (!this.listeners.has(event)) {
- return;
- }
-
- const callbacks = this.listeners.get(event);
- callbacks.forEach(callback => {
- try {
- callback(data);
- } catch (error) {
- logger.error('mockSocketService', 'emit', error, { event, data });
- }
- });
- }
-
- /**
- * 启动模拟消息推送
- * @param {number} interval - 推送间隔(毫秒)
- * @param {number} burstCount - 每次推送的消息数量(1-3条)
- */
- startMockPush(interval = 15000, burstCount = 1) {
- if (!this.connected) {
- logger.warn('mockSocketService', 'Cannot start mock push: not connected');
- return;
- }
-
- logger.info('mockSocketService', `Starting mock push: interval=${interval}ms, burst=${burstCount}`);
-
- const pushInterval = setInterval(() => {
- // 检查是否暂停推送
- if (this.pushPaused) {
- logger.info('mockSocketService', '⏸️ Mock push is paused, skipping this cycle...');
- return;
- }
-
- // 随机选择 1-burstCount 条消息
- const count = Math.floor(Math.random() * burstCount) + 1;
-
- for (let i = 0; i < count; i++) {
- // 从模拟数据中随机选择一条
- const randomIndex = Math.floor(Math.random() * mockFinancialNews.length);
- const alert = {
- ...mockFinancialNews[randomIndex],
- timestamp: Date.now(),
- id: `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
- };
-
- // 延迟发送(模拟层叠效果)
- setTimeout(() => {
- this.emit('new_event', alert);
- logger.info('mockSocketService', 'Mock notification sent', alert);
- }, i * 500); // 每条消息间隔500ms
- }
- }, interval);
-
- this.intervals.push(pushInterval);
- }
-
- /**
- * 停止模拟推送
- */
- stopMockPush() {
- this.intervals.forEach(interval => clearInterval(interval));
- this.intervals = [];
- this.pushPaused = false; // 重置暂停状态
- logger.info('mockSocketService', 'Mock push stopped');
- }
-
- /**
- * 暂停自动推送(保持连接和定时器运行)
- */
- pausePush() {
- this.pushPaused = true;
- logger.info('mockSocketService', '⏸️ Mock push paused (connection and intervals maintained)');
- }
-
- /**
- * 恢复自动推送
- */
- resumePush() {
- this.pushPaused = false;
- logger.info('mockSocketService', '▶️ Mock push resumed');
- }
-
- /**
- * 查询推送暂停状态
- * @returns {boolean} 是否已暂停
- */
- isPushPaused() {
- return this.pushPaused;
- }
-
- /**
- * 手动触发一条测试消息
- * @param {object} customData - 自定义消息数据(可选)
- */
- sendTestNotification(customData = null) {
- // 如果传入自定义数据,直接使用(向后兼容)
- if (customData) {
- this.emit('new_event', customData);
- logger.info('mockSocketService', 'Custom test notification sent', customData);
- return;
- }
-
- // 默认发送新格式的测试通知(符合当前通知系统规范)
- const notification = {
- type: 'announcement', // 公告通知类型
- priority: 'important', // 重要优先级(30秒自动关闭)
- title: '🧪 测试通知',
- content: '这是一条手动触发的测试消息,用于验证通知系统是否正常工作',
- publishTime: Date.now(),
- pushTime: Date.now(),
- id: `test_${Date.now()}`,
- clickable: false,
- };
-
- this.emit('new_event', notification);
- logger.info('mockSocketService', 'Test notification sent', notification);
- }
-
- /**
- * 获取连接状态
- */
- isConnected() {
- return this.connected;
- }
-
- /**
- * 获取当前重连尝试次数
- */
- getReconnectAttempts() {
- return this.reconnectAttempts;
- }
-
- /**
- * 获取最大重连次数(Mock 模式无限重试)
- */
- getMaxReconnectAttempts() {
- return Infinity;
- }
-
- /**
- * 订阅事件推送(Mock 实现)
- * @param {object} options - 订阅选项
- * @param {string} options.eventType - 事件类型 ('all' | 'policy' | 'market' | 'tech' | ...)
- * @param {string} options.importance - 重要性 ('all' | 'S' | 'A' | 'B' | 'C')
- * @param {Function} options.onNewEvent - 收到新事件时的回调函数
- * @param {Function} options.onSubscribed - 订阅成功的回调函数(可选)
- */
- subscribeToEvents(options = {}) {
- const {
- eventType = 'all',
- importance = 'all',
- onNewEvent,
- onSubscribed,
- } = options;
-
- logger.info('mockSocketService', 'Subscribing to events', { eventType, importance });
-
- // Mock: 立即触发订阅成功回调
- if (onSubscribed) {
- setTimeout(() => {
- onSubscribed({
- success: true,
- event_type: eventType,
- importance: importance,
- message: 'Mock subscription confirmed'
- });
- }, 100);
- }
-
- // Mock: 如果提供了 onNewEvent 回调,监听 'new_event' 事件
- if (onNewEvent) {
- // 先移除之前的监听器(避免重复)
- this.off('new_event', onNewEvent);
- // 添加新的监听器
- this.on('new_event', onNewEvent);
- logger.info('mockSocketService', 'Event listener registered for new_event');
- }
- }
-
- /**
- * 取消订阅事件推送(Mock 实现)
- * @param {object} options - 取消订阅选项
- * @param {string} options.eventType - 事件类型
- * @param {Function} options.onUnsubscribed - 取消订阅成功的回调函数(可选)
- */
- unsubscribeFromEvents(options = {}) {
- const {
- eventType = 'all',
- onUnsubscribed,
- } = options;
-
- logger.info('mockSocketService', 'Unsubscribing from events', { eventType });
-
- // Mock: 移除 new_event 监听器
- this.off('new_event');
-
- // Mock: 立即触发取消订阅成功回调
- if (onUnsubscribed) {
- setTimeout(() => {
- onUnsubscribed({
- success: true,
- event_type: eventType,
- message: 'Mock unsubscription confirmed'
- });
- }, 100);
- }
- }
-
- /**
- * 快捷方法:订阅所有类型的事件(Mock 实现)
- * @param {Function} onNewEvent - 收到新事件时的回调函数
- */
- subscribeToAllEvents(onNewEvent) {
- this.subscribeToEvents({
- eventType: 'all',
- importance: 'all',
- onNewEvent,
- });
- }
-
- /**
- * 快捷方法:订阅指定重要性的事件(Mock 实现)
- * @param {string} importance - 重要性级别 ('S' | 'A' | 'B' | 'C')
- * @param {Function} onNewEvent - 收到新事件时的回调函数
- */
- subscribeToImportantEvents(importance, onNewEvent) {
- this.subscribeToEvents({
- eventType: 'all',
- importance,
- onNewEvent,
- });
- }
-
- /**
- * 快捷方法:订阅指定类型的事件(Mock 实现)
- * @param {string} eventType - 事件类型
- * @param {Function} onNewEvent - 收到新事件时的回调函数
- */
- subscribeToEventType(eventType, onNewEvent) {
- this.subscribeToEvents({
- eventType,
- importance: 'all',
- onNewEvent,
- });
- }
-}
-
-// 导出单例
-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;
- },
-
- // 暂停自动推送(保持连接)
- pausePush: () => {
- mockSocketService.pausePush();
- logger.info('mockSocketService', '⏸️ Auto push paused');
- return true;
- },
-
- // 恢复自动推送
- resumePush: () => {
- mockSocketService.resumePush();
- logger.info('mockSocketService', '▶️ Auto push resumed');
- return true;
- },
-
- // 查看推送暂停状态
- isPushPaused: () => {
- const paused = mockSocketService.isPushPaused();
- logger.info('mockSocketService', `Push status: ${paused ? '⏸️ Paused' : '▶️ Active'}`);
- return paused;
- },
- };
-
- 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() - 查看重连次数');
- logger.info('mockSocketService', ' __mockSocket.pausePush() - ⏸️ 暂停自动推送(保持连接)');
- logger.info('mockSocketService', ' __mockSocket.resumePush() - ▶️ 恢复自动推送');
- logger.info('mockSocketService', ' __mockSocket.isPushPaused() - 查看推送状态');
-}
-
-export default mockSocketService;
diff --git a/src/services/socket/index.js b/src/services/socket/index.js
index 8e7d49d3..cee61978 100644
--- a/src/services/socket/index.js
+++ b/src/services/socket/index.js
@@ -1,364 +1,19 @@
// src/services/socket/index.js
/**
* Socket 服务统一导出
- * 根据环境变量自动选择使用 Mock 或真实 Socket.IO 服务
+ * 使用真实 Socket.IO 服务连接后端
*/
-import { mockSocketService } from '../mockSocketService';
import { socketService } from '../socketService';
-// 判断是否使用 Mock
-const useMock = process.env.REACT_APP_ENABLE_MOCK === 'true' || process.env.REACT_APP_USE_MOCK_SOCKET === 'true';
-
-// 根据环境选择服务
-export const socket = useMock ? mockSocketService : socketService;
-
-// 同时导出两个服务,方便测试和调试
-export { mockSocketService, socketService };
-
-// 导出服务类型标识
-export const SOCKET_TYPE = useMock ? 'MOCK' : 'REAL';
+// 导出 socket 服务
+export const socket = socketService;
+export { socketService };
// 打印当前使用的服务类型
console.log(
- `%c[Socket Service] Using ${SOCKET_TYPE} Socket Service`,
- `color: ${useMock ? '#FF9800' : '#4CAF50'}; font-weight: bold; font-size: 12px;`
+ '%c[Socket Service] Using REAL Socket Service',
+ 'color: #4CAF50; font-weight: bold; font-size: 12px;'
);
-// ========== 暴露调试 API 到全局 ==========
-if (typeof window !== 'undefined') {
- // 暴露 Socket 类型到全局
- window.SOCKET_TYPE = SOCKET_TYPE;
-
- // 暴露调试 API
- window.__SOCKET_DEBUG__ = {
- // 获取当前连接状态
- getStatus: () => {
- const isConnected = socket.connected || false;
- return {
- type: SOCKET_TYPE,
- connected: isConnected,
- reconnectAttempts: socket.getReconnectAttempts?.() || 0,
- maxReconnectAttempts: socket.getMaxReconnectAttempts?.() || Infinity,
- service: useMock ? 'mockSocketService' : 'socketService',
- };
- },
-
- // 手动重连
- reconnect: () => {
- console.log('[Socket Debug] Manual reconnect triggered');
- if (socket.reconnect) {
- socket.reconnect();
- } else {
- socket.disconnect();
- socket.connect();
- }
- },
-
- // 断开连接
- disconnect: () => {
- console.log('[Socket Debug] Manual disconnect triggered');
- socket.disconnect();
- },
-
- // 连接
- connect: () => {
- console.log('[Socket Debug] Manual connect triggered');
- socket.connect();
- },
-
- // 获取服务实例 (仅用于调试)
- getService: () => socket,
-
- // 导出诊断信息
- exportDiagnostics: () => {
- const status = window.__SOCKET_DEBUG__.getStatus();
- const diagnostics = {
- ...status,
- timestamp: new Date().toISOString(),
- userAgent: navigator.userAgent,
- url: window.location.href,
- env: {
- NODE_ENV: process.env.NODE_ENV,
- REACT_APP_ENABLE_MOCK: process.env.REACT_APP_ENABLE_MOCK,
- REACT_APP_USE_MOCK_SOCKET: process.env.REACT_APP_USE_MOCK_SOCKET,
- REACT_APP_API_URL: process.env.REACT_APP_API_URL,
- REACT_APP_ENV: process.env.REACT_APP_ENV,
- },
- };
- console.log('[Socket Diagnostics]', diagnostics);
- return diagnostics;
- },
-
- // 手动订阅事件
- subscribe: (options = {}) => {
- const { eventType = 'all', importance = 'all' } = options;
- console.log(`[Socket Debug] Subscribing to events: type=${eventType}, importance=${importance}`);
-
- if (socket.subscribeToEvents) {
- socket.subscribeToEvents({
- eventType,
- importance,
- onNewEvent: (event) => {
- console.log('[Socket Debug] ✅ New event received:', event);
- },
- onSubscribed: (data) => {
- console.log('[Socket Debug] ✅ Subscription confirmed:', data);
- },
- });
- } else {
- console.error('[Socket Debug] ❌ subscribeToEvents method not available');
- }
- },
-
- // 测试连接质量
- testConnection: () => {
- console.log('[Socket Debug] Testing connection...');
- const start = Date.now();
-
- if (socket.emit) {
- socket.emit('ping', { timestamp: start }, (response) => {
- const latency = Date.now() - start;
- console.log(`[Socket Debug] ✅ Connection OK - Latency: ${latency}ms`, response);
- });
- } else {
- console.error('[Socket Debug] ❌ Cannot test connection - socket.emit not available');
- }
- },
-
- // 检查配置是否正确
- checkConfig: () => {
- const config = {
- socketType: SOCKET_TYPE,
- useMock,
- envVars: {
- REACT_APP_ENABLE_MOCK: process.env.REACT_APP_ENABLE_MOCK,
- REACT_APP_USE_MOCK_SOCKET: process.env.REACT_APP_USE_MOCK_SOCKET,
- NODE_ENV: process.env.NODE_ENV,
- REACT_APP_API_URL: process.env.REACT_APP_API_URL,
- },
- socketMethods: {
- connect: typeof socket.connect,
- disconnect: typeof socket.disconnect,
- on: typeof socket.on,
- emit: typeof socket.emit,
- subscribeToEvents: typeof socket.subscribeToEvents,
- },
- };
-
- console.log('[Socket Debug] Configuration Check:', config);
-
- // 检查潜在问题
- const issues = [];
- if (SOCKET_TYPE === 'MOCK' && process.env.NODE_ENV === 'production') {
- issues.push('⚠️ WARNING: Using MOCK socket in production!');
- }
- if (!socket.subscribeToEvents) {
- issues.push('❌ ERROR: subscribeToEvents method missing');
- }
-
- if (issues.length > 0) {
- console.warn('[Socket Debug] Issues found:', issues);
- } else {
- console.log('[Socket Debug] ✅ No issues found');
- }
-
- return { config, issues };
- },
- };
-
- console.log(
- '%c[Socket Debug] Debug API available at window.__SOCKET_DEBUG__',
- 'color: #2196F3; font-weight: bold;'
- );
- console.log(
- '%cTry: window.__SOCKET_DEBUG__.getStatus()',
- 'color: #2196F3;'
- );
- console.log(
- '%c window.__SOCKET_DEBUG__.checkConfig() - 检查配置',
- 'color: #2196F3;'
- );
- console.log(
- '%c window.__SOCKET_DEBUG__.subscribe() - 手动订阅事件',
- 'color: #2196F3;'
- );
- console.log(
- '%c window.__SOCKET_DEBUG__.testConnection() - 测试连接',
- 'color: #2196F3;'
- );
-
- // ========== 通知系统专用调试 API ==========
- window.__NOTIFY_DEBUG__ = {
- // 完整检查(配置+连接+订阅状态)
- checkAll: () => {
- console.log('\n==========【通知系统诊断】==========');
-
- // 1. 检查 Socket 配置
- const socketCheck = window.__SOCKET_DEBUG__.checkConfig();
- console.log('\n✓ Socket 配置检查完成');
-
- // 2. 检查连接状态
- const status = window.__SOCKET_DEBUG__.getStatus();
- console.log('\n✓ 连接状态:', status.connected ? '✅ 已连接' : '❌ 未连接');
-
- // 3. 检查环境变量
- console.log('\n✓ API Base:', process.env.REACT_APP_API_URL || '(使用相对路径)');
-
- // 4. 检查浏览器通知权限
- const browserPermission = Notification?.permission || 'unsupported';
- console.log('\n✓ 浏览器通知权限:', browserPermission);
-
- // 5. 汇总报告
- const report = {
- timestamp: new Date().toISOString(),
- socket: {
- type: SOCKET_TYPE,
- connected: status.connected,
- reconnectAttempts: status.reconnectAttempts,
- },
- env: socketCheck.config.envVars,
- browserNotification: browserPermission,
- issues: socketCheck.issues,
- };
-
- console.log('\n========== 诊断报告 ==========');
- console.table(report);
-
- if (report.issues.length > 0) {
- console.warn('\n⚠️ 发现问题:', report.issues);
- } else {
- console.log('\n✅ 系统正常,未发现问题');
- }
-
- // 提供修复建议
- if (!status.connected) {
- console.log('\n💡 修复建议:');
- console.log(' 1. 检查网络连接');
- console.log(' 2. 尝试手动重连: __SOCKET_DEBUG__.reconnect()');
- console.log(' 3. 检查后端服务是否运行');
- }
-
- if (browserPermission === 'denied') {
- console.log('\n💡 浏览器通知已被拒绝,请在浏览器设置中允许通知权限');
- }
-
- console.log('\n====================================\n');
-
- return report;
- },
-
- // 手动订阅事件(简化版)
- subscribe: (eventType = 'all', importance = 'all') => {
- console.log(`\n[通知调试] 手动订阅事件: type=${eventType}, importance=${importance}`);
- window.__SOCKET_DEBUG__.subscribe({ eventType, importance });
- },
-
- // 模拟接收通知(用于测试UI)
- testNotify: (type = 'announcement') => {
- console.log('\n[通知调试] 模拟通知:', type);
-
- const mockNotifications = {
- announcement: {
- id: `test_${Date.now()}`,
- type: 'announcement',
- priority: 'important',
- title: '🧪 测试公告通知',
- content: '这是一条测试消息,用于验证通知系统是否正常工作',
- publishTime: Date.now(),
- pushTime: Date.now(),
- },
- stock_alert: {
- id: `test_${Date.now()}`,
- type: 'stock_alert',
- priority: 'urgent',
- title: '🧪 测试股票预警',
- content: '贵州茅台触发价格预警: 1850.00元 (+5.2%)',
- publishTime: Date.now(),
- pushTime: Date.now(),
- },
- event_alert: {
- id: `test_${Date.now()}`,
- type: 'event_alert',
- priority: 'important',
- title: '🧪 测试事件动向',
- content: 'AI大模型新政策发布,影响科技板块',
- publishTime: Date.now(),
- pushTime: Date.now(),
- },
- analysis_report: {
- id: `test_${Date.now()}`,
- type: 'analysis_report',
- priority: 'normal',
- title: '🧪 测试分析报告',
- content: '2024年Q1市场策略报告已发布',
- publishTime: Date.now(),
- pushTime: Date.now(),
- },
- };
-
- const notification = mockNotifications[type] || mockNotifications.announcement;
-
- // 触发 new_event 事件
- if (socket.emit) {
- // 对于真实 Socket,模拟服务端推送(实际上客户端无法这样做,仅用于Mock模式)
- console.warn('⚠️ 真实 Socket 无法模拟服务端推送,请使用 Mock 模式或等待真实推送');
- }
-
- // 直接触发事件监听器(如果是 Mock 模式)
- if (SOCKET_TYPE === 'MOCK' && socket.emit) {
- socket.emit('new_event', notification);
- console.log('✅ 已触发 Mock 通知事件');
- }
-
- console.log('通知数据:', notification);
- return notification;
- },
-
- // 导出完整诊断报告
- exportReport: () => {
- const report = window.__NOTIFY_DEBUG__.checkAll();
-
- // 生成可下载的 JSON
- const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `notification-debug-${Date.now()}.json`;
- a.click();
- URL.revokeObjectURL(url);
-
- console.log('✅ 诊断报告已导出');
- return report;
- },
-
- // 快捷帮助
- help: () => {
- console.log('\n========== 通知系统调试 API ==========');
- console.log('window.__NOTIFY_DEBUG__.checkAll() - 完整诊断检查');
- console.log('window.__NOTIFY_DEBUG__.subscribe() - 手动订阅事件');
- console.log('window.__NOTIFY_DEBUG__.testNotify(type) - 模拟通知 (announcement/stock_alert/event_alert/analysis_report)');
- console.log('window.__NOTIFY_DEBUG__.exportReport() - 导出诊断报告');
- console.log('\n========== Socket 调试 API ==========');
- console.log('window.__SOCKET_DEBUG__.getStatus() - 获取连接状态');
- console.log('window.__SOCKET_DEBUG__.checkConfig() - 检查配置');
- console.log('window.__SOCKET_DEBUG__.reconnect() - 手动重连');
- console.log('====================================\n');
- },
- };
-
- console.log(
- '%c[Notify Debug] Notification Debug API available at window.__NOTIFY_DEBUG__',
- 'color: #FF9800; font-weight: bold;'
- );
- console.log(
- '%cTry: window.__NOTIFY_DEBUG__.checkAll() - 完整诊断',
- 'color: #FF9800;'
- );
- console.log(
- '%c window.__NOTIFY_DEBUG__.help() - 查看所有命令',
- 'color: #FF9800;'
- );
-}
-
export default socket;
diff --git a/src/services/socketService.js b/src/services/socketService.js
index e1cdbd07..3203f923 100644
--- a/src/services/socketService.js
+++ b/src/services/socketService.js
@@ -16,6 +16,7 @@ class SocketService {
this.reconnectAttempts = 0;
this.maxReconnectAttempts = Infinity; // 无限重试
this.customReconnectTimer = null; // 自定义重连定时器
+ this.pendingListeners = []; // 暂存等待注册的事件监听器
}
/**
@@ -50,6 +51,15 @@ class SocketService {
...options,
});
+ // 注册所有暂存的事件监听器
+ if (this.pendingListeners.length > 0) {
+ console.log(`[socketService] 📦 注册 ${this.pendingListeners.length} 个暂存的事件监听器`);
+ this.pendingListeners.forEach(({ event, callback }) => {
+ this.on(event, callback);
+ });
+ this.pendingListeners = []; // 清空暂存队列
+ }
+
// 监听连接成功
this.socket.on('connect', () => {
this.connected = true;
@@ -147,8 +157,10 @@ class SocketService {
*/
on(event, callback) {
if (!this.socket) {
- logger.warn('socketService', 'Cannot listen to event: socket not initialized', { event });
- console.warn(`[socketService] ❌ 无法监听事件 ${event}: Socket 未初始化`);
+ // Socket 未初始化,暂存监听器
+ logger.info('socketService', 'Socket not ready, queuing listener', { event });
+ console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`);
+ this.pendingListeners.push({ event, callback });
return;
}