/** * WebSocket聊天管理工具类 * 实现了心跳机制、断线重连、消息队列等功能 */ const DEBUG_WS = false; class WebSocketManager { constructor() { // 心跳相关 this.heartbeatTimer = null; this.heartbeatInterval = 30000; // 30秒 // 重连相关 this.reconnectTimer = null; this.reconnectCount = 0; this.maxReconnectCount = 10; this.reconnectDelay = 1000; // 初始1秒 // 消息队列(断线时暂存) this.messageQueue = []; this.maxQueueSize = 100; // 回调函数 this.onMessageCallback = null; this.onConnectCallback = null; this.onDisconnectCallback = null; this.onErrorCallback = null; } /** * 连接WebSocket * @param {Number} userId 用户ID * @param {String} baseUrl WebSocket服务地址 */ connect(userId, baseUrl = 'ws://localhost:8083') { if (this.isConnected) { if (DEBUG_WS) console.log('WebSocket已连接'); return; } this.userId = userId; this.url = `${baseUrl}/ws/chat?userId=${userId}`; if (DEBUG_WS) console.log('正在连接WebSocket...', this.url); this.socket = uni.connectSocket({ url: this.url, success: () => { if (DEBUG_WS) console.log('WebSocket连接请求已发送'); }, fail: (err) => { console.error('WebSocket连接失败', err); this.onError(err); } }); // 监听连接打开 uni.onSocketOpen((res) => { if (DEBUG_WS) console.log('WebSocket连接成功'); this.isConnected = true; this.reconnectCount = 0; // 启动心跳 this.startHeartbeat(); // 发送队列中的消息 this.flushMessageQueue(); // 触发连接回调 if (this.onConnectCallback) { this.onConnectCallback(); } }); // 监听消息接收 uni.onSocketMessage((res) => { try { if (DEBUG_WS) console.log('?? 收到原始WebSocket消息:', res.data); const message = JSON.parse(res.data); if (DEBUG_WS) console.log('?? 解析后的消息对象:', JSON.stringify(message, null, 2)); this.handleMessage(message); } catch (e) { console.error('❌ 解析消息失败', e); console.error(' 原始数据:', res.data); } }); // 监听连接关闭 uni.onSocketClose((res) => { if (DEBUG_WS) console.log('WebSocket连接关闭', res); this.isConnected = false; this.stopHeartbeat(); // 触发断开回调 if (this.onDisconnectCallback) { this.onDisconnectCallback(); } // 尝试重连 this.reconnect(); }); // 监听错误 uni.onSocketError((err) => { console.error('WebSocket错误', err); this.isConnected = false; this.onError(err); }); } /** * 处理接收到的消息 */ handleMessage(message) { if (DEBUG_WS) console.log('收到消息', message); switch (message.type) { case 'pong': // 心跳响应 console.log('收到pong'); break; case 'chat': // 聊天消息 if (this.onMessageCallback) { this.onMessageCallback(message); } // 自动发送ACK确认 this.sendAck(message.messageId); break; case 'ack': // 消息确认 if (DEBUG_WS) { console.log('✅ 收到ACK消息!'); console.log(' messageId:', message.messageId); console.log(' 完整ACK消息:', JSON.stringify(message)); } if (this.onMessageCallback) { console.log(' 正在调用onMessageCallback...'); this.onMessageCallback(message); } else { console.warn(' ⚠️ onMessageCallback未设置!'); } break; case 'recall': // 消息撤回 if (this.onMessageCallback) { this.onMessageCallback(message); } break; case 'typing': // 正在输入 if (this.onMessageCallback) { this.onMessageCallback(message); } break; case 'read': // 已读回执 if (this.onMessageCallback) { this.onMessageCallback(message); } break; case 'online': // 上线通知 if (DEBUG_WS) console.log('用户上线', message.fromUserId); break; case 'error': // 错误消息 console.error('服务器错误', message.errorMessage); uni.showToast({ title: message.errorMessage, icon: 'none' }); break; default: if (DEBUG_WS) console.log('未知消息类型', message.type); } } /** * 发送聊天消息 */ sendMessage(toUserId, content, messageType = 1, extraData = {}) { const message = { type: 'chat', messageId: this.generateMessageId(), fromUserId: this.userId, toUserId: toUserId, messageType: messageType, content: content, timestamp: Date.now(), extraData: extraData }; if (messageType === 2) { // 图片消息 message.mediaUrl = extraData.mediaUrl; message.thumbnailUrl = extraData.thumbnailUrl; } else if (messageType === 3 || messageType === 4) { // 语音或视频消息 message.mediaUrl = extraData.mediaUrl; message.duration = extraData.duration; message.mediaSize = extraData.mediaSize; } this.send(message); return message.messageId; } /** * 发送消息ACK确认 */ sendAck(messageId) { this.send({ type: 'ack', messageId: messageId, timestamp: Date.now() }); } /** * 撤回消息 */ recallMessage(messageId) { this.send({ type: 'recall', messageId: messageId, timestamp: Date.now() }); } /** * 发送正在输入状态 */ sendTyping(toUserId) { this.send({ type: 'typing', toUserId: toUserId, timestamp: Date.now() }); } /** * 发送已读回执 */ sendReadReceipt(toUserId) { this.send({ type: 'read', fromUserId: this.userId, toUserId: toUserId, timestamp: Date.now() }); } /** * 发送数据到WebSocket */ send(data) { console.log('?? 准备发送WebSocket消息'); console.log(' 消息类型:', data.type); console.log(' isConnected:', this.isConnected); console.log(' 完整数据:', JSON.stringify(data, null, 2)); if (!this.isConnected) { console.log('❌ WebSocket未连接,消息加入队列'); // 加入消息队列 if (this.messageQueue.length < this.maxQueueSize) { this.messageQueue.push(data); } return false; } try { const jsonStr = JSON.stringify(data); console.log('?? JSON字符串长度:', jsonStr.length); uni.sendSocketMessage({ data: jsonStr, success: () => { console.log('✅ WebSocket消息已发送到服务器'); console.log(' 消息类型:', data.type); console.log(' messageId:', data.messageId || 'N/A'); }, fail: (err) => { console.error('❌ WebSocket消息发送失败', err); console.error(' 错误详情:', JSON.stringify(err)); // 发送失败,加入队列 if (this.messageQueue.length < this.maxQueueSize) { this.messageQueue.push(data); } } }); return true; } catch (e) { console.error('❌ 发送消息异常', e); console.error(' 异常详情:', e.message); return false; } } /** * 发送队列中的消息 */ flushMessageQueue() { if (this.messageQueue.length === 0) { return; } console.log(`发送队列中的 ${this.messageQueue.length} 条消息`); while (this.messageQueue.length > 0) { const message = this.messageQueue.shift(); this.send(message); } } /** * 启动心跳 */ startHeartbeat() { this.stopHeartbeat(); this.heartbeatTimer = setInterval(() => { if (this.isConnected) { this.send({ type: 'ping', timestamp: Date.now() }); } }, this.heartbeatInterval); } /** * 停止心跳 */ stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } } /** * 断线重连 */ reconnect() { if (this.reconnectCount >= this.maxReconnectCount) { console.log('达到最大重连次数,停止重连'); uni.showToast({ title: '连接失败,请检查网络', icon: 'none' }); return; } // 指数退避算法 const delay = Math.min( this.reconnectDelay * Math.pow(2, this.reconnectCount), 30000 // 最大30秒 ); console.log(`${delay}ms 后进行第 ${this.reconnectCount + 1} 次重连...`); this.reconnectTimer = setTimeout(() => { this.reconnectCount++; this.connect(this.userId, this.url.split('/ws/chat')[0]); }, delay); } /** * 取消重连 */ cancelReconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } } /** * 关闭连接 */ close() { this.cancelReconnect(); this.stopHeartbeat(); if (this.socket) { uni.closeSocket(); this.socket = null; } this.isConnected = false; } /** * 错误处理 */ onError(error) { if (this.onErrorCallback) { this.onErrorCallback(error); } } /** * 生成消息ID */ generateMessageId() { return `${this.userId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * 设置消息回调 */ onMessage(callback) { this.onMessageCallback = callback; } /** * 设置连接回调 */ onConnect(callback) { this.onConnectCallback = callback; } /** * 设置断开回调 */ onDisconnect(callback) { this.onDisconnectCallback = callback; } /** * 设置错误回调 */ onErrorHandler(callback) { this.onErrorCallback = callback; } } // 导出单例 export default new WebSocketManager();