diff --git a/.env.production b/.env.production
index ef983024..81f10c55 100644
--- a/.env.production
+++ b/.env.production
@@ -16,6 +16,15 @@ NODE_ENV=production
# Mock 配置(生产环境禁用 Mock)
REACT_APP_ENABLE_MOCK=false
+# 🔧 调试模式(生产环境临时调试用)
+# 开启后会在全局暴露 window.__DEBUG__ 和 window.__TEST_NOTIFICATION__ 调试 API
+# ⚠️ 警告: 调试模式会记录所有 API 请求/响应,调试完成后请立即关闭!
+# 使用方法:
+# 1. 设置为 true 并重新构建
+# 2. 在浏览器控制台使用 window.__DEBUG__.help() 查看命令
+# 3. 调试完成后设置为 false 并重新构建
+REACT_APP_ENABLE_DEBUG=true
+
# 后端 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/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..3db0d127 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...');
@@ -35,7 +35,7 @@ self.addEventListener('notificationclick', (event) => {
.then((windowClients) => {
// 查找是否已有打开的窗口
for (let client of windowClients) {
- if (client.url.includes(window.location.origin) && 'focus' in client) {
+ if (client.url.includes(self.location.origin) && 'focus' in client) {
// 聚焦现有窗口并导航到目标页面
return client.focus().then(client => {
return client.navigate(urlToOpen);
@@ -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..8254a29d 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,12 @@ 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()); // 跟踪所有通知的自动关闭定时器
+
+ // ⚡ 方案2: 使用 Ref 存储最新的回调函数引用(避免闭包陷阱)
+ const addNotificationRef = useRef(null);
+ const adaptEventToNotificationRef = useRef(null);
+ const isFirstConnect = useRef(true); // 标记是否首次连接
// ⚡ 使用权限引导管理 Hook
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
@@ -71,9 +72,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 +116,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 +138,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 +473,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 +582,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');
@@ -589,26 +600,42 @@ export const NotificationProvider = ({ children }) => {
return newNotification.id;
}, [notifications, toast, sendBrowserNotification, addWebNotification, browserPermission, hasRequestedPermission, requestBrowserPermission]);
- // 连接到 Socket 服务
+ /**
+ * ✅ 方案2: 同步最新的回调函数到 Ref
+ * 确保 Socket 监听器始终使用最新的函数引用(避免闭包陷阱)
+ */
+ useEffect(() => {
+ addNotificationRef.current = addNotification;
+ console.log('[NotificationContext] 📝 已更新 addNotificationRef');
+ }, [addNotification]);
+
+ useEffect(() => {
+ adaptEventToNotificationRef.current = adaptEventToNotification;
+ console.log('[NotificationContext] 📝 已更新 adaptEventToNotificationRef');
+ }, [adaptEventToNotification]);
+
+
+ // ========== 连接到 Socket 服务(⚡ 方案2: 只执行一次) ==========
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] 🚀 初始化 Socket 连接(方案2:只注册一次)', 'color: #673AB7; font-weight: bold;');
- // ✅ 第一步: 注册所有事件监听器
- console.log('%c[NotificationContext] Step 1: Registering event listeners...', 'color: #673AB7;');
-
- // 监听连接状态
+ // ========== 监听连接成功(首次连接 + 重连) ==========
socket.on('connect', () => {
- const wasDisconnected = connectionStatus !== CONNECTION_STATUS.CONNECTED;
setIsConnected(true);
setReconnectAttempt(0);
- logger.info('NotificationContext', 'Socket connected', { wasDisconnected });
- console.log('%c[NotificationContext] ✅ Received connect event, updating state to connected', 'color: #4CAF50; font-weight: bold;');
- // 如果之前断开过,显示 RECONNECTED 状态2秒后自动消失
- if (wasDisconnected) {
+ // 判断是首次连接还是重连
+ if (isFirstConnect.current) {
+ console.log('%c[NotificationContext] ✅ 首次连接成功', 'color: #4CAF50; font-weight: bold;');
+ console.log('[NotificationContext] Socket ID:', socket.getSocketId?.());
+ setConnectionStatus(CONNECTION_STATUS.CONNECTED);
+ isFirstConnect.current = false;
+ logger.info('NotificationContext', 'Socket connected (first time)');
+ } else {
+ console.log('%c[NotificationContext] 🔄 重连成功!', 'color: #FF9800; font-weight: bold;');
setConnectionStatus(CONNECTION_STATUS.RECONNECTED);
- logger.info('NotificationContext', 'Reconnected, will auto-dismiss in 2s');
+ logger.info('NotificationContext', 'Socket reconnected');
// 清除之前的定时器
if (reconnectedTimerRef.current) {
@@ -620,69 +647,61 @@ export const NotificationProvider = ({ children }) => {
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
logger.info('NotificationContext', 'Auto-dismissed RECONNECTED status');
}, 2000);
- } else {
- 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);
+ },
+ });
+ } else {
+ console.error('[NotificationContext] ❌ socket.subscribeToEvents 方法不可用');
}
});
+ // ========== 监听断开连接 ==========
socket.on('disconnect', (reason) => {
setIsConnected(false);
setConnectionStatus(CONNECTION_STATUS.DISCONNECTED);
logger.warn('NotificationContext', 'Socket disconnected', { reason });
+ console.log('%c[NotificationContext] ⚠️ Socket 已断开', 'color: #FF5722;', { reason });
});
- // 监听连接错误
+ // ========== 监听连接错误 ==========
socket.on('connect_error', (error) => {
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 });
+ console.log(`%c[NotificationContext] 🔄 重连中... (第 ${attempts} 次尝试)`, 'color: #FF9800;');
});
- // 监听重连失败
+ // ========== 监听重连失败 ==========
socket.on('reconnect_failed', () => {
logger.error('NotificationContext', 'Socket reconnect_failed');
setConnectionStatus(CONNECTION_STATUS.FAILED);
+ console.error('[NotificationContext] ❌ 重连失败');
toast({
title: '连接失败',
description: '无法连接到服务器,请检查网络连接',
status: 'error',
- duration: null, // 不自动关闭
+ duration: null,
isClosable: true,
});
});
- // 监听新事件推送(统一事件名)
+ // ========== 监听新事件推送(⚡ 只注册一次,使用 ref 访问最新函数) ==========
socket.on('new_event', (data) => {
console.log('\n%c════════════════════════════════════════', 'color: #FF9800; font-weight: bold;');
console.log('%c[NotificationContext] 📨 收到 new_event 事件!', 'color: #FF9800; font-weight: bold;');
@@ -695,77 +714,114 @@ export const NotificationProvider = ({ children }) => {
logger.info('NotificationContext', 'Received new event', data);
+ // ⚠️ 防御性检查:确保 ref 已初始化
+ if (!addNotificationRef.current || !adaptEventToNotificationRef.current) {
+ console.error('%c[NotificationContext] ❌ Ref 未初始化,跳过处理', 'color: #F44336; font-weight: bold;');
+ logger.error('NotificationContext', 'Refs not initialized', {
+ addNotificationRef: !!addNotificationRef.current,
+ adaptEventToNotificationRef: !!adaptEventToNotificationRef.current,
+ });
+ return;
+ }
+
// ========== Socket层去重检查 ==========
- const eventId = data.id || `${data.type}_${data.publishTime}`;
+ const eventId = data.id || `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+ 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 });
console.warn('[NotificationContext] ⚠️ 重复事件,已忽略:', eventId);
console.log('%c════════════════════════════════════════\n', 'color: #FF9800; font-weight: bold;');
- return; // 重复事件,直接忽略
+ return;
}
- // 记录已处理的事件ID
processedEventIds.current.add(eventId);
console.log('[NotificationContext] ✓ 事件已记录,防止重复处理');
- // 限制Set大小,避免内存泄漏
+ // 限制 Set 大小,避免内存泄漏
if (processedEventIds.current.size > MAX_PROCESSED_IDS) {
const idsArray = Array.from(processedEventIds.current);
processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS));
logger.debug('NotificationContext', 'Cleaned up old processed event IDs', {
- kept: MAX_PROCESSED_IDS
+ kept: MAX_PROCESSED_IDS,
});
}
// ========== Socket层去重检查结束 ==========
- // 使用适配器转换事件格式
+ // ✅ 使用 ref.current 访问最新的适配器函数(避免闭包陷阱)
console.log('[NotificationContext] 正在转换事件格式...');
- const notification = adaptEventToNotification(data);
+ const notification = adaptEventToNotificationRef.current(data);
console.log('[NotificationContext] 转换后的通知对象:', notification);
+ // ✅ 使用 ref.current 访问最新的 addNotification 函数
console.log('[NotificationContext] 准备添加通知到队列...');
- addNotification(notification);
+ addNotificationRef.current(notification);
console.log('[NotificationContext] ✅ 通知已添加到队列');
console.log('%c════════════════════════════════════════\n', 'color: #FF9800; font-weight: bold;');
});
- // 保留系统通知监听(兼容性)
+ // ========== 监听系统通知(兼容性) ==========
socket.on('system_notification', (data) => {
logger.info('NotificationContext', 'Received system notification', data);
- addNotification(data);
+ console.log('[NotificationContext] 📢 收到系统通知:', data);
+
+ if (addNotificationRef.current) {
+ addNotificationRef.current(data);
+ } else {
+ console.error('[NotificationContext] ❌ addNotificationRef 未初始化');
+ }
});
- console.log('%c[NotificationContext] ✅ All event listeners registered', 'color: #4CAF50; font-weight: bold;');
+ console.log('%c[NotificationContext] ✅ 所有监听器已注册(只注册一次)', 'color: #4CAF50; font-weight: bold;');
- // ✅ 第二步: 获取最大重连次数
+ // ========== 获取最大重连次数 ==========
const maxAttempts = socket.getMaxReconnectAttempts?.() || Infinity;
setMaxReconnectAttempts(maxAttempts);
logger.info('NotificationContext', 'Max reconnect attempts', { maxAttempts });
- // ✅ 第三步: 调用 socket.connect()
- console.log('%c[NotificationContext] Step 2: Calling socket.connect()...', 'color: #673AB7; font-weight: bold;');
+ // ========== 启动连接 ==========
+ console.log('%c[NotificationContext] 🔌 调用 socket.connect()...', 'color: #673AB7; font-weight: bold;');
socket.connect();
- console.log('%c[NotificationContext] socket.connect() completed', 'color: #673AB7;');
- // 清理函数
+ // ========== 清理函数(组件卸载时) ==========
return () => {
logger.info('NotificationContext', 'Cleaning up socket connection');
+ console.log('%c[NotificationContext] 🧹 清理 Socket 连接', 'color: #9E9E9E;');
- // 如果是 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');
socket.off('reconnect_failed');
socket.off('new_event');
socket.off('system_notification');
+
+ // 断开连接
socket.disconnect();
+
+ console.log('%c[NotificationContext] ✅ 清理完成', 'color: #4CAF50;');
};
- }, []); // ✅ 空依赖数组,确保只执行一次,避免 React 严格模式重复执行
+ }, []); // ⚠️ 空依赖数组,确保只执行一次
// ==================== 智能自动重试 ====================
@@ -776,11 +832,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 +858,7 @@ export const NotificationProvider = ({ children }) => {
isClosable: true,
});
- if (SOCKET_TYPE === 'REAL') {
- socket.reconnect?.();
- } else {
- socket.connect();
- }
+ socket.reconnect?.();
}
};
@@ -842,14 +890,137 @@ 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]);
+
+ // 🔧 开发环境调试:暴露方法到 window
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'development' || process.env.REACT_APP_ENABLE_DEBUG === 'true') {
+ if (typeof window !== 'undefined') {
+ window.__TEST_NOTIFICATION__ = {
+ // 手动触发网页通知
+ testWebNotification: (type = 'event_alert', priority = 'normal') => {
+ console.log('%c[Debug] 手动触发网页通知', 'color: #FF9800; font-weight: bold;');
+
+ const testData = {
+ id: `test_${Date.now()}`,
+ type: type,
+ priority: priority,
+ title: '🧪 测试网页通知',
+ content: `这是一条测试${type === 'announcement' ? '公告' : type === 'stock_alert' ? '股票' : type === 'event_alert' ? '事件' : '分析'}通知 (优先级: ${priority})`,
+ timestamp: Date.now(),
+ clickable: true,
+ link: '/home',
+ };
+
+ console.log('测试数据:', testData);
+ addNotification(testData);
+ console.log('✅ 通知已添加到队列');
+ },
+
+ // 测试所有类型
+ testAllTypes: () => {
+ console.log('%c[Debug] 测试所有通知类型', 'color: #FF9800; font-weight: bold;');
+ const types = ['announcement', 'stock_alert', 'event_alert', 'analysis_report'];
+ types.forEach((type, i) => {
+ setTimeout(() => {
+ window.__TEST_NOTIFICATION__.testWebNotification(type, 'normal');
+ }, i * 2000); // 每 2 秒一个
+ });
+ },
+
+ // 测试所有优先级
+ testAllPriorities: () => {
+ console.log('%c[Debug] 测试所有优先级', 'color: #FF9800; font-weight: bold;');
+ const priorities = ['normal', 'important', 'urgent'];
+ priorities.forEach((priority, i) => {
+ setTimeout(() => {
+ window.__TEST_NOTIFICATION__.testWebNotification('event_alert', priority);
+ }, i * 2000);
+ });
+ },
+
+ // 帮助
+ help: () => {
+ console.log('\n%c=== 网页通知测试 API ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
+ console.log('\n%c基础用法:', 'color: #2196F3; font-weight: bold;');
+ console.log(' window.__TEST_NOTIFICATION__.testWebNotification(type, priority)');
+ console.log('\n%c参数说明:', 'color: #2196F3; font-weight: bold;');
+ console.log(' type (通知类型):');
+ console.log(' - "announcement" 公告通知(蓝色)');
+ console.log(' - "stock_alert" 股票动向(红色/绿色)');
+ console.log(' - "event_alert" 事件动向(橙色)');
+ console.log(' - "analysis_report" 分析报告(紫色)');
+ console.log('\n priority (优先级):');
+ console.log(' - "normal" 普通(15秒自动关闭)');
+ console.log(' - "important" 重要(30秒自动关闭)');
+ console.log(' - "urgent" 紧急(不自动关闭)');
+ console.log('\n%c示例:', 'color: #4CAF50; font-weight: bold;');
+ console.log(' // 测试紧急事件通知');
+ console.log(' window.__TEST_NOTIFICATION__.testWebNotification("event_alert", "urgent")');
+ console.log('\n // 测试所有类型');
+ console.log(' window.__TEST_NOTIFICATION__.testAllTypes()');
+ console.log('\n // 测试所有优先级');
+ console.log(' window.__TEST_NOTIFICATION__.testAllPriorities()');
+ console.log('\n');
+ }
+ };
+
+ console.log('[NotificationContext] 🔧 调试 API 已加载: window.__TEST_NOTIFICATION__');
+ console.log('[NotificationContext] 💡 使用 window.__TEST_NOTIFICATION__.help() 查看帮助');
+ }
+ }
+
+ // 清理函数
+ return () => {
+ if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
+ delete window.__TEST_NOTIFICATION__;
+ }
+ };
+ }, [addNotification]); // 依赖 addNotification 函数
+
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..cb672545
--- /dev/null
+++ b/src/devtools/index.js
@@ -0,0 +1,271 @@
+// 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.testWebNotification(type, priority) - 测试网页通知 🆕');
+ console.log(' __DEBUG__.notification.testAllNotificationTypes() - 测试所有类型 🆕');
+ console.log(' __DEBUG__.notification.testAllNotificationPriorities() - 测试所有优先级 🆕');
+ 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..0ed4867a
--- /dev/null
+++ b/src/devtools/notificationDebugger.js
@@ -0,0 +1,204 @@
+// 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);
+ }
+
+ /**
+ * 测试网页通知(需要 window.__TEST_NOTIFICATION__ 可用)
+ */
+ testWebNotification(type = 'event_alert', priority = 'normal') {
+ if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
+ console.log('[Notification Debugger] 调用测试 API');
+ window.__TEST_NOTIFICATION__.testWebNotification(type, priority);
+ } else {
+ console.error('[Notification Debugger] ❌ window.__TEST_NOTIFICATION__ 不可用');
+ console.error('💡 请确保:');
+ console.error(' 1. REACT_APP_ENABLE_DEBUG=true');
+ console.error(' 2. NotificationContext 已加载');
+ console.error(' 3. 页面已刷新');
+ }
+ }
+
+ /**
+ * 测试所有通知类型
+ */
+ testAllNotificationTypes() {
+ if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
+ window.__TEST_NOTIFICATION__.testAllTypes();
+ } else {
+ console.error('[Notification Debugger] ❌ window.__TEST_NOTIFICATION__ 不可用');
+ }
+ }
+
+ /**
+ * 测试所有优先级
+ */
+ testAllNotificationPriorities() {
+ if (typeof window !== 'undefined' && window.__TEST_NOTIFICATION__) {
+ window.__TEST_NOTIFICATION__.testAllPriorities();
+ } else {
+ console.error('[Notification Debugger] ❌ window.__TEST_NOTIFICATION__ 不可用');
+ }
+ }
+}
+
+// 导出单例
+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..510b097c 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 冲突)
@@ -31,24 +44,64 @@ function registerServiceWorker() {
navigator.serviceWorker
.register('/service-worker.js')
.then((registration) => {
- console.log('[App] Service Worker registered successfully:', registration.scope);
+ console.log('[App] ✅ Service Worker 注册成功');
+ console.log('[App] Scope:', registration.scope);
- // 监听更新
+ // 检查当前激活状态
+ if (navigator.serviceWorker.controller) {
+ console.log('[App] ✅ Service Worker 已激活并控制页面');
+ } else {
+ console.log('[App] ⏳ Service Worker 已注册,等待激活...');
+ console.log('[App] 💡 刷新页面以激活 Service Worker');
+
+ // 监听 controller 变化(Service Worker 激活后触发)
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
+ console.log('[App] ✅ Service Worker 控制器已更新');
+ });
+ }
+
+ // 监听 Service Worker 更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
- console.log('[App] Service Worker update found');
+ console.log('[App] 🔄 发现 Service Worker 更新');
if (newWorker) {
newWorker.addEventListener('statechange', () => {
+ console.log(`[App] Service Worker 状态: ${newWorker.state}`);
if (newWorker.state === 'activated') {
- console.log('[App] Service Worker activated');
+ console.log('[App] ✅ Service Worker 已激活');
+
+ // 如果有旧的 Service Worker 在控制页面,提示用户刷新
+ if (navigator.serviceWorker.controller) {
+ console.log('[App] 💡 Service Worker 已更新,建议刷新页面');
+ }
}
});
}
});
})
.catch((error) => {
- console.error('[App] Service Worker registration failed:', error);
+ console.error('[App] ❌ Service Worker 注册失败');
+ console.error('[App] 错误类型:', error.name);
+ console.error('[App] 错误信息:', error.message);
+ console.error('[App] 完整错误:', error);
+
+ // 额外检查:验证文件是否可访问
+ fetch('/service-worker.js', { method: 'HEAD' })
+ .then(response => {
+ if (response.ok) {
+ console.error('[App] Service Worker 文件存在但注册失败');
+ console.error('[App] 💡 可能的原因:');
+ console.error('[App] 1. Service Worker 文件有语法错误');
+ console.error('[App] 2. 浏览器不支持某些 Service Worker 特性');
+ console.error('[App] 3. HTTPS 证书问题(Service Worker 需要 HTTPS)');
+ } else {
+ console.error('[App] Service Worker 文件不存在(HTTP', response.status, ')');
+ }
+ })
+ .catch(fetchError => {
+ console.error('[App] 无法访问 Service Worker 文件:', fetchError.message);
+ });
});
});
} else {
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..11586e78 100644
--- a/src/services/socket/index.js
+++ b/src/services/socket/index.js
@@ -1,364 +1,34 @@
// 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';
+// 导出 socket 服务
+export const socket = socketService;
+export { socketService };
-// 根据环境选择服务
-export const socket = useMock ? mockSocketService : socketService;
+// ⚡ 新增:暴露 Socket 实例到 window(用于调试和验证)
+if (typeof window !== 'undefined') {
+ window.socket = socketService;
+ window.socketService = socketService;
-// 同时导出两个服务,方便测试和调试
-export { mockSocketService, socketService };
-
-// 导出服务类型标识
-export const SOCKET_TYPE = useMock ? 'MOCK' : 'REAL';
+ console.log(
+ '%c[Socket Service] ✅ Socket instance exposed to window',
+ 'color: #4CAF50; font-weight: bold; font-size: 14px;'
+ );
+ console.log(' 📍 window.socket:', window.socket);
+ console.log(' 📍 window.socketService:', window.socketService);
+ console.log(' 📍 Socket.IO instance:', window.socket?.socket);
+ console.log(' 📍 Connection status:', window.socket?.connected ? '✅ Connected' : '❌ Disconnected');
+}
// 打印当前使用的服务类型
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..2e8d22cb 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,23 @@ class SocketService {
...options,
});
+ // 注册所有暂存的事件监听器(保留 pendingListeners,不清空)
+ if (this.pendingListeners.length > 0) {
+ console.log(`[socketService] 📦 注册 ${this.pendingListeners.length} 个暂存的事件监听器`);
+ this.pendingListeners.forEach(({ event, callback }) => {
+ // 直接在 Socket.IO 实例上注册(避免递归调用 this.on())
+ const wrappedCallback = (...args) => {
+ console.log(`%c[socketService] 🔔 收到原始事件: ${event}`, 'color: #2196F3; font-weight: bold;');
+ console.log(`[socketService] 事件数据 (${event}):`, ...args);
+ callback(...args);
+ };
+
+ this.socket.on(event, wrappedCallback);
+ console.log(`[socketService] ✓ 已注册事件监听器: ${event}`);
+ });
+ // ⚠️ 重要:不清空 pendingListeners,保留用于重连
+ }
+
// 监听连接成功
this.socket.on('connect', () => {
this.connected = true;
@@ -147,8 +165,18 @@ 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 未初始化,暂存监听器(检查是否已存在,避免重复)
+ const exists = this.pendingListeners.some(
+ (listener) => listener.event === event && listener.callback === callback
+ );
+
+ if (!exists) {
+ logger.info('socketService', 'Socket not ready, queuing listener', { event });
+ console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`);
+ this.pendingListeners.push({ event, callback });
+ } else {
+ console.log(`[socketService] ⚠️ 监听器已存在,跳过: ${event}`);
+ }
return;
}