feat: 添加消息推送能力,添加新闻催化分析页的合规提示

This commit is contained in:
zdl
2025-10-21 10:59:52 +08:00
parent 300c0a18a6
commit ced6dbc559
15 changed files with 1800 additions and 125 deletions

View File

@@ -0,0 +1,254 @@
// src/services/mockSocketService.js
/**
* Mock Socket 服务 - 用于开发环境模拟实时推送
* 模拟交易提醒、系统通知等实时消息推送
*/
import { logger } from '../utils/logger';
// 模拟交易提醒数据
const mockTradeAlerts = [
{
type: 'trade_alert',
severity: 'success',
title: '买入成功',
message: '您的订单已成功执行:买入 贵州茅台(600519) 100股成交价 ¥1,850.00',
timestamp: Date.now(),
autoClose: 8000,
},
{
type: 'trade_alert',
severity: 'warning',
title: '价格预警',
message: '您关注的股票 比亚迪(002594) 当前价格 ¥245.50,已触达预设价格',
timestamp: Date.now(),
autoClose: 10000,
},
{
type: 'trade_alert',
severity: 'info',
title: '持仓提醒',
message: '您持有的 宁德时代(300750) 今日涨幅达 5.2%,当前盈利 +¥12,350',
timestamp: Date.now(),
autoClose: 8000,
},
{
type: 'trade_alert',
severity: 'error',
title: '委托失败',
message: '卖出订单失败:五粮液(000858) 当前处于停牌状态,无法交易',
timestamp: Date.now(),
autoClose: 12000,
},
{
type: 'system_notification',
severity: 'info',
title: '系统公告',
message: '市场将于15:00收盘请注意及时调整持仓',
timestamp: Date.now(),
autoClose: 10000,
},
{
type: 'trade_alert',
severity: 'success',
title: '分红到账',
message: '您持有的 中国平安(601318) 分红已到账,金额 ¥560.00',
timestamp: Date.now(),
autoClose: 8000,
},
];
class MockSocketService {
constructor() {
this.connected = false;
this.listeners = new Map();
this.intervals = [];
this.messageQueue = [];
}
/**
* 连接到 mock socket
*/
connect() {
if (this.connected) {
logger.warn('mockSocketService', 'Already connected');
return;
}
logger.info('mockSocketService', 'Connecting to mock socket service...');
// 模拟连接延迟
setTimeout(() => {
this.connected = true;
logger.info('mockSocketService', 'Mock socket connected successfully');
// 触发连接成功事件
this.emit('connect', { timestamp: Date.now() });
// 在连接后3秒发送欢迎消息
setTimeout(() => {
this.emit('trade_notification', {
type: 'system_notification',
severity: 'info',
title: '连接成功',
message: '实时消息推送服务已启动 (Mock 模式)',
timestamp: Date.now(),
autoClose: 5000,
});
}, 3000);
}, 1000);
}
/**
* 断开连接
*/
disconnect() {
if (!this.connected) {
return;
}
logger.info('mockSocketService', 'Disconnecting from mock socket service...');
// 清除所有定时器
this.intervals.forEach(interval => clearInterval(interval));
this.intervals = [];
this.connected = false;
this.emit('disconnect', { timestamp: Date.now() });
}
/**
* 监听事件
* @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(() => {
// 随机选择 1-burstCount 条消息
const count = Math.floor(Math.random() * burstCount) + 1;
for (let i = 0; i < count; i++) {
// 从模拟数据中随机选择一条
const randomIndex = Math.floor(Math.random() * mockTradeAlerts.length);
const alert = {
...mockTradeAlerts[randomIndex],
timestamp: Date.now(),
id: `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
};
// 延迟发送(模拟层叠效果)
setTimeout(() => {
this.emit('trade_notification', 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 = [];
logger.info('mockSocketService', 'Mock push stopped');
}
/**
* 手动触发一条测试消息
* @param {object} customData - 自定义消息数据(可选)
*/
sendTestNotification(customData = null) {
const notification = customData || {
type: 'trade_alert',
severity: 'info',
title: '测试消息',
message: '这是一条手动触发的测试消息',
timestamp: Date.now(),
autoClose: 5000,
id: `test_${Date.now()}`,
};
this.emit('trade_notification', notification);
logger.info('mockSocketService', 'Test notification sent', notification);
}
/**
* 获取连接状态
*/
isConnected() {
return this.connected;
}
}
// 导出单例
export const mockSocketService = new MockSocketService();
export default mockSocketService;

View File

@@ -0,0 +1,28 @@
// src/services/socket/index.js
/**
* Socket 服务统一导出
* 根据环境变量自动选择使用 Mock 或真实 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';
// 打印当前使用的服务类型
console.log(
`%c[Socket Service] Using ${SOCKET_TYPE} Socket Service`,
`color: ${useMock ? '#FF9800' : '#4CAF50'}; font-weight: bold; font-size: 12px;`
);
export default socket;

View File

@@ -0,0 +1,194 @@
// src/services/socketService.js
/**
* 真实 Socket.IO 服务 - 用于生产环境连接真实后端
*/
import { io } from 'socket.io-client';
import { logger } from '../utils/logger';
import { getApiBase } from '../utils/apiConfig';
const API_BASE_URL = getApiBase();
class SocketService {
constructor() {
this.socket = null;
this.connected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}
/**
* 连接到 Socket.IO 服务器
* @param {object} options - 连接选项
*/
connect(options = {}) {
if (this.socket && this.connected) {
logger.warn('socketService', 'Already connected');
return;
}
logger.info('socketService', 'Connecting to Socket.IO server...', { url: API_BASE_URL });
// 创建 socket 连接
this.socket = io(API_BASE_URL, {
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: this.maxReconnectAttempts,
timeout: 20000,
autoConnect: true,
withCredentials: true, // 允许携带认证信息
...options,
});
// 监听连接成功
this.socket.on('connect', () => {
this.connected = true;
this.reconnectAttempts = 0;
logger.info('socketService', 'Socket.IO connected successfully', {
socketId: this.socket.id,
});
});
// 监听断开连接
this.socket.on('disconnect', (reason) => {
this.connected = false;
logger.warn('socketService', 'Socket.IO disconnected', { reason });
});
// 监听连接错误
this.socket.on('connect_error', (error) => {
this.reconnectAttempts++;
logger.error('socketService', 'connect_error', error, {
attempts: this.reconnectAttempts,
maxAttempts: this.maxReconnectAttempts,
});
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
logger.error('socketService', 'Max reconnection attempts reached');
this.socket.close();
}
});
// 监听重连尝试
this.socket.io.on('reconnect_attempt', (attemptNumber) => {
logger.info('socketService', 'Reconnection attempt', { attemptNumber });
});
// 监听重连成功
this.socket.io.on('reconnect', (attemptNumber) => {
this.reconnectAttempts = 0;
logger.info('socketService', 'Reconnected successfully', { attemptNumber });
});
// 监听重连失败
this.socket.io.on('reconnect_failed', () => {
logger.error('socketService', 'Reconnection failed after max attempts');
});
}
/**
* 断开连接
*/
disconnect() {
if (!this.socket) {
return;
}
logger.info('socketService', 'Disconnecting from Socket.IO server...');
this.socket.disconnect();
this.socket = null;
this.connected = false;
}
/**
* 监听事件
* @param {string} event - 事件名称
* @param {Function} callback - 回调函数
*/
on(event, callback) {
if (!this.socket) {
logger.warn('socketService', 'Cannot listen to event: socket not initialized', { event });
return;
}
this.socket.on(event, callback);
logger.info('socketService', `Event listener added: ${event}`);
}
/**
* 移除事件监听
* @param {string} event - 事件名称
* @param {Function} callback - 回调函数(可选)
*/
off(event, callback) {
if (!this.socket) {
return;
}
if (callback) {
this.socket.off(event, callback);
} else {
this.socket.off(event);
}
logger.info('socketService', `Event listener removed: ${event}`);
}
/**
* 发送消息到服务器
* @param {string} event - 事件名称
* @param {*} data - 发送的数据
* @param {Function} callback - 确认回调(可选)
*/
emit(event, data, callback) {
if (!this.socket || !this.connected) {
logger.warn('socketService', 'Cannot emit: socket not connected', { event, data });
return;
}
if (callback) {
this.socket.emit(event, data, callback);
} else {
this.socket.emit(event, data);
}
logger.info('socketService', `Event emitted: ${event}`, data);
}
/**
* 加入房间
* @param {string} room - 房间名称
*/
joinRoom(room) {
this.emit('join_room', { room });
}
/**
* 离开房间
* @param {string} room - 房间名称
*/
leaveRoom(room) {
this.emit('leave_room', { room });
}
/**
* 获取连接状态
*/
isConnected() {
return this.connected;
}
/**
* 获取 Socket ID
*/
getSocketId() {
return this.socket?.id || null;
}
}
// 导出单例
export const socketService = new SocketService();
export default socketService;