// utils/websocket.js // WebSocket连接管理器 class WebSocketManager { constructor() { this.socket = null; this.url = ''; this.isConnected = false; this.isAuthenticated = false; // 新增:认证状态标记 this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectInterval = 3000; // 3秒后重连 this.heartbeatInterval = null; this.heartbeatTime = 30000; // 30秒心跳 this.messageQueue = []; // 未发送的消息队列 this.listeners = {}; // 事件监听器 this.lastHeartbeatTime = 0; // 最后一次心跳响应时间 this.isManualDisconnect = false; // 是否手动断开连接 // 清理可能导致端口错误的存储配置 this._cleanupStorage(); } // 清理可能导致端口错误的存储配置 _cleanupStorage() { try { // 尝试在小程序环境中清理存储 if (typeof wx !== 'undefined' && wx.removeStorageSync) { wx.removeStorageSync('__TEST_MODE__'); wx.removeStorageSync('__TEST_SERVER_IP__'); wx.removeStorageSync('__DEVICE_TYPE__'); console.log('WebSocket: 已清理可能导致端口错误的本地存储配置'); } } catch (e) { console.warn('WebSocket: 清理存储时出错:', e); } } /** * 初始化WebSocket连接 * @param {string} url - WebSocket服务器地址 * @param {object} options - 配置选项 */ connect(url, options = {}) { if (this.socket && this.isConnected) { console.log('WebSocket已经连接'); return; } this.url = url; this.maxReconnectAttempts = options.maxReconnectAttempts || this.maxReconnectAttempts; this.reconnectInterval = options.reconnectInterval || this.reconnectInterval; this.heartbeatTime = options.heartbeatTime || this.heartbeatTime; this.isManualDisconnect = false; // 重置手动断开标志 try { console.log('尝试连接WebSocket:', url); this._trigger('status', { type: 'connecting', message: '正在连接服务器...' }); this.socket = wx.connectSocket({ url: url, success: () => { console.log('WebSocket连接请求已发送'); }, fail: (error) => { console.error('WebSocket连接请求失败:', error); this._trigger('error', error); this._trigger('status', { type: 'error', message: '连接服务器失败' }); this._reconnect(); } }); this._setupEventHandlers(); } catch (error) { console.error('WebSocket初始化失败:', error); this._trigger('error', error); this._trigger('status', { type: 'error', message: '连接异常' }); this._reconnect(); } } /** * 设置WebSocket事件处理器 */ _setupEventHandlers() { if (!this.socket) return; // 连接成功 this.socket.onOpen(() => { console.log('WebSocket连接已打开'); this.isConnected = true; this.isAuthenticated = false; // 重置认证状态 this.reconnectAttempts = 0; this.lastHeartbeatTime = Date.now(); // 记录最后心跳时间 this._trigger('open'); this._trigger('status', { type: 'connected', message: '连接成功' }); // 连接成功后立即进行认证 this.authenticate(); this._startHeartbeat(); }); // 接收消息 this.socket.onMessage((res) => { try { let data = JSON.parse(res.data); // 处理心跳响应 if (data.type === 'pong') { this.lastHeartbeatTime = Date.now(); // 更新心跳时间 return; } // 处理认证响应(兼容auth_response和auth_success两种消息格式) if (data.type === 'auth_response') { if (data.success) { console.log('WebSocket认证成功(auth_response)'); this.isAuthenticated = true; // 触发认证成功事件,并传递用户类型信息 this._trigger('authenticated', { userType: data.userType || 'customer' }); // 认证成功后发送队列中的消息 this._flushMessageQueue(); } else { console.error('WebSocket认证失败:', data.message); this.isAuthenticated = false; this._trigger('authFailed', { message: data.message, userType: data.userType || 'unknown' }); } return; } // 处理auth_success格式的认证成功消息(与后端实际返回格式匹配) if (data.type === 'auth_success') { console.log('WebSocket认证成功(auth_success)'); this.isAuthenticated = true; // 从payload中提取用户类型信息 const userType = data.payload && data.payload.type ? data.payload.type : 'customer'; // 触发认证成功事件 this._trigger('authenticated', { userType: userType }); // 认证成功后发送队列中的消息 - 关键修复! this._flushMessageQueue(); return; } // 处理客服状态更新消息 if (data.type === 'customerServiceStatusUpdate') { console.log('处理客服状态更新:', data); this._trigger('customerServiceStatusUpdate', data); return; } console.log('接收到消息:', data); this._trigger('message', data); } catch (error) { console.error('消息解析失败:', error); this._trigger('error', error); } }); // 连接关闭 this.socket.onClose((res) => { console.log('WebSocket连接已关闭:', res); this.isConnected = false; this._stopHeartbeat(); this._trigger('close', res); this._trigger('status', { type: 'disconnected', message: '连接已关闭' }); // 尝试重连 if (res.code !== 1000 && !this.isManualDisconnect) { // 非正常关闭且不是手动断开 this._reconnect(); } }); // 连接错误 this.socket.onError((error) => { console.error('WebSocket错误:', error); this._trigger('error', error); this._trigger('status', { type: 'error', message: '连接发生错误' }); }); } /** * 发送认证消息 * @param {string} userType - 用户类型,如'user'或'manager' * @param {string} userId - 用户ID或managerId */ authenticate(userType = null, userId = null) { try { // 获取登录用户信息或token const app = getApp(); const globalUserInfo = app.globalData.userInfo || {}; // 如果传入了参数,优先使用传入的参数 let finalUserType = userType || globalUserInfo.userType || globalUserInfo.type || 'customer'; // 构建认证消息 - 严格区分用户类型和认证信息 let authMessage; // 检查是否为客服身份 const storedManagerId = wx.getStorageSync('managerId'); const isManager = finalUserType === 'manager' || storedManagerId; if (isManager) { // 客服认证:必须使用有效的managerId,不允许使用普通userId作为容错 if (!storedManagerId) { console.error('客服认证失败:未找到有效的managerId'); this._trigger('authFailed', { message: '客服认证失败:未找到有效的managerId' }); return; } authMessage = { type: 'auth', managerId: storedManagerId, userType: 'manager', timestamp: Date.now() }; console.log('客服用户认证:', { managerId: storedManagerId, userType: 'manager' }); } else { // 普通用户认证:必须使用users表中的正式userId // 聊天功能必须在用户授权登录后使用,因此必须有有效的userId let finalUserId = null; // 优先级1:使用传入的userId(应该是从服务器获取的正式ID) if (userId) { finalUserId = String(userId); console.log('使用传入的用户ID:', finalUserId); } // 优先级2:从全局用户信息获取(应该包含服务器返回的userId) else if (globalUserInfo && globalUserInfo.userId) { finalUserId = String(globalUserInfo.userId); console.log('从globalData获取用户ID:', finalUserId); } // 优先级3:从本地存储获取(应该存储了服务器返回的userId) else { finalUserId = wx.getStorageSync('userId'); if (finalUserId) { finalUserId = String(finalUserId); console.log('从本地存储获取用户ID:', finalUserId); } } // 验证是否有有效的用户ID if (!finalUserId || finalUserId === 'undefined' || finalUserId === 'null') { console.error('认证失败:未获取到有效的用户ID'); this._trigger('authFailed', { message: '用户未授权登录,请先完成登录', code: 'NO_VALID_USER_ID' }); return; } // 确保使用正确的用户类型 // 根据用户在users表中的类型设置,支持customer、buyer、seller、both let authUserType = finalUserType; if (!authUserType || authUserType === 'user') { authUserType = 'customer'; } console.log('准备认证 - 用户ID:', finalUserId, '用户类型:', authUserType); authMessage = { type: 'auth', userId: finalUserId, userType: authUserType, timestamp: Date.now() }; console.log('普通用户认证:', { userId: finalUserId, userType: authUserType }); } console.log('发送WebSocket认证消息:', authMessage); // 直接发送认证消息,不经过常规消息队列 if (this.isConnected && this.socket) { this.socket.send({ data: JSON.stringify(authMessage), success: () => { console.log('认证消息发送成功'); }, fail: (error) => { console.error('认证消息发送失败:', error); this._trigger('authFailed', { message: '认证消息发送失败' }); // 认证失败后尝试重新认证 setTimeout(() => { this.authenticate(userType, userId); }, 2000); } }); } } catch (error) { console.error('发送认证消息异常:', error); this._trigger('authFailed', { message: '认证处理异常' }); } } /** * 发送消息 * @param {object} data - 要发送的数据 * @returns {boolean} 消息是否已成功放入发送队列(不保证实际发送成功) */ send(data) { // 验证消息格式 if (!data || typeof data !== 'object') { console.error('WebSocket发送消息失败: 消息格式不正确'); return false; } // 为消息添加时间戳 if (!data.timestamp) { data.timestamp = Date.now(); } // 确保消息使用正式用户ID,严格区分sender_id和receiver_id try { const app = getApp(); const globalUserInfo = app.globalData.userInfo || {}; let currentUserId = String(globalUserInfo.userId || wx.getStorageSync('userId') || ''); // 重要:确保使用的是正式用户ID,不允许使用临时ID if (currentUserId.startsWith('temp_') || currentUserId.includes('temp') || currentUserId.includes('test_')) { console.error('严重错误:消息对象中检测到临时或测试用户ID,聊天功能必须使用正式userId'); // 尝试从本地存储获取正式用户ID const userInfo = wx.getStorageSync('userInfo'); if (userInfo && userInfo.userId && !String(userInfo.userId).includes('temp') && !String(userInfo.userId).includes('test_')) { currentUserId = String(userInfo.userId); console.log('已更新为正式用户ID:', currentUserId); } else { console.error('无法获取有效的正式用户ID,消息发送失败'); return false; } } // 确保消息中包含正确的sender_id,并保留userId字段供服务器使用 if (data.userId && !data.sender_id) { console.warn('消息使用了userId字段,应改为使用sender_id字段'); data.sender_id = data.userId; // 关键修复:保留userId字段,因为服务器需要它来处理消息 // delete data.userId; // 不再删除userId字段 } // 如果没有指定sender_id,则设置为当前用户ID if (!data.sender_id) { data.sender_id = currentUserId; } // 确保接收者ID使用receiver_id字段 if (data.targetUserId && !data.receiver_id) { console.warn('消息使用了targetUserId字段,应改为使用receiver_id字段'); data.receiver_id = data.targetUserId; delete data.targetUserId; } // 【修复】确保聊天消息使用正确的数据库字段名 // 数据库使用下划线命名法,前端代码中可能使用驼峰命名法 if (data.receiverId) { console.warn('检测到使用了驼峰命名的receiverId,将转换为下划线命名的receiver_id'); data.receiver_id = data.receiver_id || data.receiverId; // 保留原始字段以保持兼容性 } if (data.senderId) { console.warn('检测到使用了驼峰命名的senderId,将转换为下划线命名的sender_id'); data.sender_id = data.sender_id || data.senderId; // 保留原始字段以保持兼容性 } } catch (e) { console.error('处理消息用户ID时出错:', e); } // 如果是认证消息或连接未建立,直接处理 if (data.type === 'auth' || data.type === 'ping') { // 认证消息和心跳消息不需要等待认证 if (this.isConnected && this.socket) { try { this.socket.send({ data: JSON.stringify(data), success: () => { console.log('特殊消息发送成功:', data); this._trigger('sendSuccess', data); }, fail: (error) => { console.error('特殊消息发送失败:', error); this._trigger('sendError', error); } }); return true; } catch (error) { console.error('发送特殊消息异常:', error); this._trigger('error', error); return false; } } } else if (this.isConnected && this.socket) { // 非特殊消息需要检查认证状态 if (!this.isAuthenticated) { console.log('WebSocket未认证,消息已加入队列等待认证'); this.messageQueue.push(data); // 如果未认证,尝试重新认证 if (!this.isAuthenticated) { this.authenticate(); } return true; } try { this.socket.send({ data: JSON.stringify(data), success: () => { console.log('消息发送成功:', data); this._trigger('sendSuccess', data); }, fail: (error) => { console.error('消息发送失败:', error); // 将失败的消息加入队列 this.messageQueue.push(data); this._trigger('sendError', error); } }); return true; } catch (error) { console.error('发送消息异常:', error); this.messageQueue.push(data); this._trigger('error', error); return false; } } else { // 连接未建立,加入消息队列 console.log('WebSocket未连接,消息已加入队列'); this.messageQueue.push(data); // 尝试重连 if (!this.isConnected) { this._reconnect(); } return true; } } /** * 关闭WebSocket连接 */ close() { if (this.socket) { this._stopHeartbeat(); this.isManualDisconnect = true; // 标记为手动断开 this.socket.close(); this.socket = null; this.isConnected = false; this.isAuthenticated = false; // 重置认证状态 console.log('WebSocket已主动关闭'); this._trigger('status', { type: 'disconnected', message: '连接已断开' }); } } /** * 开始心跳检测 */ _startHeartbeat() { this._stopHeartbeat(); this.heartbeatInterval = setInterval(() => { // 检查是否超过3倍心跳间隔未收到心跳响应 if (Date.now() - this.lastHeartbeatTime > this.heartbeatTime * 3) { console.warn('WebSocket心跳超时,可能已断开连接'); this._stopHeartbeat(); this._reconnect(); return; } if (this.isConnected) { this.send({ type: 'ping', timestamp: Date.now() }); console.log('发送心跳包'); } }, this.heartbeatTime); } /** * 停止心跳检测 */ _stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); this.heartbeatInterval = null; } } /** * 尝试重新连接 */ _reconnect() { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error('WebSocket重连次数已达上限,停止重连'); this._trigger('reconnectFailed'); this._trigger('status', { type: 'error', isWarning: true, message: `已达到最大重连次数(${this.maxReconnectAttempts}次)` }); return; } this.reconnectAttempts++; // 增加重连时间间隔(指数退避) const currentInterval = this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1); console.log(`WebSocket第${this.reconnectAttempts}次重连... 间隔: ${currentInterval}ms`); this._trigger('reconnecting', this.reconnectAttempts); this._trigger('status', { type: 'reconnecting', message: `正在重连(${this.reconnectAttempts}/${this.maxReconnectAttempts})` }); setTimeout(() => { this.connect(this.url); }, currentInterval); } /** * 发送队列中的消息 */ _flushMessageQueue() { if (this.messageQueue.length > 0) { console.log('发送队列中的消息,队列长度:', this.messageQueue.length); // 循环发送队列中的消息,使用小延迟避免消息发送过快 const sendMessage = () => { if (this.messageQueue.length === 0 || !this.isConnected) { return; } const messageData = this.messageQueue.shift(); const message = JSON.stringify(messageData); this.socket.send({ data: message, success: () => { console.log('队列消息发送成功:', messageData); // 继续发送下一条消息,添加小延迟 setTimeout(sendMessage, 50); }, fail: (error) => { console.error('队列消息发送失败:', error); // 发送失败,重新加入队列 this.messageQueue.unshift(messageData); } }); }; // 开始发送队列中的第一条消息 sendMessage(); } } /** * 触发事件 * @param {string} event - 事件名称 * @param {*} data - 事件数据 */ _trigger(event, data = null) { if (this.listeners[event]) { this.listeners[event].forEach(callback => { try { callback(data); } catch (error) { console.error(`事件处理错误 [${event}]:`, error); } }); } } /** * 监听事件 * @param {string} event - 事件名称 * @param {Function} callback - 回调函数 */ on(event, callback) { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(callback); } /** * 移除事件监听 * @param {string} event - 事件名称 * @param {Function} callback - 回调函数,不传则移除所有该事件的监听器 */ off(event, callback) { if (!this.listeners[event]) return; if (callback) { this.listeners[event] = this.listeners[event].filter(cb => cb !== callback); } else { this.listeners[event] = []; } } /** * 获取连接状态 * @returns {boolean} 是否连接 */ getConnectionStatus() { return this.isConnected; } /** * 获取认证状态 * @returns {boolean} 是否已认证 */ getAuthStatus() { return this.isAuthenticated; } /** * 获取重连次数 * @returns {number} 重连次数 */ getReconnectAttempts() { return this.reconnectAttempts; } /** * 清空消息队列 */ clearMessageQueue() { this.messageQueue = []; } } // 消息发送状态管理 - 全局作用域 const messageStatus = new Map(); const MESSAGE_SENDING = 'sending'; const MESSAGE_SENT = 'sent'; const MESSAGE_FAILED = 'failed'; // 消息去重函数 - 防止重复发送 function shouldSendMessage(messageId) { const status = messageStatus.get(messageId); // 如果消息正在发送中,不重复发送 if (status === MESSAGE_SENDING) { console.log(`[WebSocket] 消息 ${messageId} 正在发送中,跳过重复发送`); return false; } // 设置消息状态为发送中 messageStatus.set(messageId, MESSAGE_SENDING); return true; } // 更新消息发送状态 function updateMessageStatus(messageId, status) { messageStatus.set(messageId, status); // 定期清理已完成的消息状态 if (messageStatus.size > 100) { cleanupMessageStatus(); } } // 清理已完成的消息状态 function cleanupMessageStatus() { for (const [messageId, status] of messageStatus.entries()) { if (status === MESSAGE_SENT || status === MESSAGE_FAILED) { messageStatus.delete(messageId); } } } // 创建单例实例 const websocketManager = new WebSocketManager(); // 增强的消息发送函数 function sendEnhancedMessage(messageData) { // 确保消息数据有效 if (!messageData || typeof messageData !== 'object') { console.error('[WebSocket] 无效的消息数据:', messageData); return false; } // 确保消息有唯一ID const actualMessageData = messageData; if (!actualMessageData.messageId) { // 如果是create_conversation消息,使用userId+managerId+timestamp生成临时ID if (actualMessageData.type === 'create_conversation') { actualMessageData.messageId = `create_${actualMessageData.userId}_${actualMessageData.managerId}_${actualMessageData.timestamp}`; } else { // 其他消息类型生成随机ID actualMessageData.messageId = `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } } // 消息去重检查 if (!shouldSendMessage(actualMessageData.messageId)) { return false; } try { // 确保包含必要字段 if (actualMessageData.type === 'chat_message') { if (!actualMessageData.userId || !actualMessageData.managerId) { console.error('[WebSocket] 聊天消息缺少必要的userId或managerId字段'); updateMessageStatus(actualMessageData.messageId, MESSAGE_FAILED); return false; } } // 设置消息状态为发送中 updateMessageStatus(actualMessageData.messageId, 'sending'); // 发送消息 - 使用WebSocketManager实例 const socketManager = websocketManager; if (socketManager.send) { socketManager.send(actualMessageData); console.log(`[WebSocket] 消息 ${actualMessageData.messageId} 发送成功`); } else { console.error('[WebSocket] 无法访问send方法'); updateMessageStatus(actualMessageData.messageId, MESSAGE_FAILED); return false; } // 设置消息发送超时检测 setTimeout(() => { if (messageStatus.get(actualMessageData.messageId) === MESSAGE_SENDING) { console.warn(`[WebSocket] 消息 ${actualMessageData.messageId} 发送超时,可能需要重试`); updateMessageStatus(actualMessageData.messageId, MESSAGE_FAILED); } }, 5000); // 5秒超时 return true; } catch (error) { console.error(`[WebSocket] 发送消息失败: ${error.message}`); updateMessageStatus(actualMessageData.messageId, MESSAGE_FAILED); return false; } } // 修复导出问题,确保正确支持ES6默认导入 // CommonJS导出方式 module.exports = websocketManager; // 添加消息处理相关函数到导出对象 module.exports.sendEnhancedMessage = sendEnhancedMessage; module.exports.updateMessageStatus = updateMessageStatus; module.exports.MESSAGE_SENT = MESSAGE_SENT; module.exports.MESSAGE_FAILED = MESSAGE_FAILED; // ES6模块导出 export default websocketManager; export { sendEnhancedMessage, updateMessageStatus, MESSAGE_SENT, MESSAGE_FAILED };