/** * TIM在线状态管理器 + WebSocket聊天管理器 * 统一管理WebSocket连接、心跳、在线状态、聊天消息 */ import timManager from './tim-manager.js'; import TIM from 'tim-wx-sdk'; // WebSocket/TIM 调试日志开关 const DEBUG_WS = false; class TIMPresenceManager { constructor() { this.ws = null; this.heartbeatTimer = null; this.reconnectTimer = null; this.heartbeatInterval = 30000; // 30秒心跳 this.reconnectInterval = 5000; // 5秒重连 this.isConnected = false; this.userId = null; this.statusCallbacks = new Map(); // 存储状态变化回调 this.onlineStatusCache = new Map(); // 缓存在线状态 this.wsUrl = 'wss://ws.zhongruanke.cn/ws/chat'; // TIM 状态监听器引用(用于清理) this.timStatusListener = null; this.timConnectListener = null; // 聊天消息相关 this.messageQueue = []; // 消息队列(断线时暂存) this.maxQueueSize = 100; this.onMessageCallback = null; // 消息回调 this.onConnectCallback = null; // 连接回调 this.onDisconnectCallback = null; // 断开回调 this.onErrorCallback = null; // 错误回调 this.reconnectCount = 0; this.maxReconnectCount = 10; } /** * 初始化 */ async init(userId) { this.userId = userId; this.connectWebSocket(); } /** * 建立 WebSocket 连接 */ connectWebSocket() { try { const wsUrl = `${this.wsUrl}?userId=${this.userId}`; // 先关闭旧连接 if (this.ws) { try { uni.closeSocket(); } catch (e) { console.warn('关闭旧连接失败:', e); } } this.ws = uni.connectSocket({ url: wsUrl, success: () => { }, fail: (err) => { console.error('❌ WebSocket连接请求发送失败:', err); console.error(' 错误详情:', JSON.stringify(err)); this.scheduleReconnect(); } }); // 使用 SocketTask 对象的监听器(推荐方式) this.ws.onOpen((res) => { this.isConnected = true; this.reconnectCount = 0; // 重置重连计数 // 启动心跳 this.startHeartbeat(); // 发送队列中的消息 this.flushMessageQueue(); // 连接成功后,立即上报当前 TIM 连接状态 this.reportCurrentTIMStatus(); // 触发连接回调 if (this.onConnectCallback) { this.onConnectCallback(); } }); // 监听消息 this.ws.onMessage((res) => { this.handleMessage(res.data); }); // 监听错误 this.ws.onError((err) => { this.isConnected = false; // 触发错误回调 if (this.onErrorCallback) { this.onErrorCallback(err); } this.scheduleReconnect(); }); // 监听关闭 this.ws.onClose((res) => { this.isConnected = false; this.stopHeartbeat(); // 触发断开回调 if (this.onDisconnectCallback) { this.onDisconnectCallback(); } this.scheduleReconnect(); }); } catch (error) { console.error('❌ WebSocket连接异常:', error); this.scheduleReconnect(); } } /** * 监听 TIM 连接状态变更 */ listenTIMConnectionStatus() { if (!timManager.tim || !timManager.tim.TIM) { console.warn('⚠️ TIM 未初始化或 TIM 对象不完整,无法监听连接状态'); return; } const TIM = timManager.tim.TIM; // 监听 IM 连接状态变化 this.timConnectListener = (event) => { let imStatus = 'offline'; // 映射 TIM 状态到业务状态 switch (event.data.state) { case TIM.TYPES.NET_STATE_CONNECTED: imStatus = 'online'; break; case TIM.TYPES.NET_STATE_DISCONNECTED: case TIM.TYPES.NET_STATE_CONNECTING: imStatus = 'offline'; break; } // 通过 WebSocket 上报状态给服务端 this.reportIMStatus(imStatus); }; timManager.tim.on(TIM.EVENT.NET_STATE_CHANGE, this.timConnectListener); } /** * 监听 TIM 用户状态更新(好友状态) */ listenTIMUserStatus() { if (!timManager.tim || !timManager.tim.TIM) { console.warn('⚠️ TIM 未初始化或 TIM 对象不完整,无法监听用户状态'); return; } const TIM = timManager.tim.TIM; // 监听用户状态更新事件 this.timStatusListener = (event) => { const { userStatusList } = event.data; if (userStatusList && userStatusList.length > 0) { userStatusList.forEach(item => { const userId = item.userID; const status = item.statusType === TIM.TYPES.USER_STATUS_ONLINE ? 'online' : 'offline'; // 更新本地缓存 this.onlineStatusCache.set(String(userId), status === 'online'); // 通知状态变化 this.notifyStatusChange(String(userId), status); // 也可以通过 WS 上报给服务端,保证服务端状态一致 this.reportFriendStatus(userId, status); }); } else { console.warn(' ⚠️ userStatusList 为空或不存在'); } }; timManager.tim.on(TIM.EVENT.USER_STATUS_UPDATED, this.timStatusListener); } /** * 上报当前 TIM 连接状态 */ reportCurrentTIMStatus() { if (!timManager.isLogin) { this.reportIMStatus('offline'); return; } // TIM 已登录,上报在线状态 this.reportIMStatus('online'); } /** * 上报 IM 状态给服务端 * @param {String} status 状态:online/offline */ reportIMStatus(status) { if (!this.isConnected) { console.warn('⚠️ WebSocket 未连接,无法上报 IM 状态'); return; } this.sendMessage({ type: 'imStatusReport', userId: this.userId, status: status, device: 'mobile', // 设备类型 timestamp: Date.now() }); } /** * 上报好友状态给服务端 * @param {String} friendUserId 好友用户ID * @param {String} status 状态:online/offline */ reportFriendStatus(friendUserId, status) { if (!this.isConnected) { return; } this.sendMessage({ type: 'friendStatusReport', userId: friendUserId, status: status, timestamp: Date.now() }); } /** * 订阅用户状态(使用 TIM 原生能力) * @param {Array} userIdList 用户ID列表 */ async subscribeUserStatus(userIdList) { if (!timManager.tim || !timManager.isLogin) { console.warn('⚠️ TIM 未登录,无法订阅用户状态'); console.warn(' - timManager.tim:', !!timManager.tim); console.warn(' - timManager.isLogin:', timManager.isLogin); // 使用 HTTP 轮询作为备用方案 this.startHttpPolling(userIdList); return; } try { const result = await timManager.tim.subscribeUserStatus({ userIDList: userIdList.map(id => String(id)) }); // 订阅成功后,初始化这些用户的状态 if (result.data && result.data.successUserList) { result.data.successUserList.forEach(user => { const status = user.statusType === timManager.tim.TIM.TYPES.USER_STATUS_ONLINE ? 'online' : 'offline'; this.onlineStatusCache.set(String(user.userID), status === 'online'); this.notifyStatusChange(String(user.userID), status); }); } // 检查失败列表 if (result.data && result.data.failureUserList && result.data.failureUserList.length > 0) { console.warn('⚠️ 部分用户订阅失败:', result.data.failureUserList); } return result; } catch (error) { console.error(' - 错误代码:', error.code); // 如果订阅失败,启用 HTTP 轮询备用方案 if (error.code === 70402 || error.code === 70403) { this.startHttpPolling(userIdList); } throw error; } } /** * HTTP 轮询备用方案(当 TIM 订阅失败时使用) * @param {Array} userIdList 用户ID列表 */ startHttpPolling(userIdList) { // 立即查询一次 this.pollUserStatus(userIdList); // 每 30 秒轮询一次 this.pollingTimer = setInterval(() => { this.pollUserStatus(userIdList); }, 30000); } /** * 轮询用户状态 */ async pollUserStatus(userIdList) { for (const userId of userIdList) { try { const [err, res] = await uni.request({ url: 'https://api.zhongruanke.cn/api/online/checkStatus', method: 'GET', data: { userId } }); if (!err && res.data && res.data.code === 200) { const isOnline = res.data.data.online || false; const oldStatus = this.onlineStatusCache.get(String(userId)); // 只有状态变化时才通知 if (oldStatus !== isOnline) { this.onlineStatusCache.set(String(userId), isOnline); this.notifyStatusChange(String(userId), isOnline ? 'online' : 'offline'); } } } catch (error) { console.error(`查询用户 ${userId} 状态失败:`, error); } } } /** * 停止 HTTP 轮询 */ stopHttpPolling() { if (this.pollingTimer) { clearInterval(this.pollingTimer); this.pollingTimer = null; } } /** * 取消订阅用户状态 * @param {Array} userIdList 用户ID列表 */ async unsubscribeUserStatus(userIdList) { if (!timManager.tim || !timManager.isLogin) { return; } try { await timManager.tim.unsubscribeUserStatus({ userIDList: userIdList.map(id => String(id)) }); } catch (error) { console.error('❌ 取消订阅用户状态失败:', error); } } /** * 处理接收到的消息 */ handleMessage(data) { try { const message = typeof data === 'string' ? JSON.parse(data) : data; switch (message.type) { case 'pong': // 改为小写,匹配后端 // 心跳响应 break; case 'ONLINE': case 'STATUS_UPDATE': // 用户上线/状态更新通知 if (message.userId || message.fromUserId) { const userId = String(message.userId || message.fromUserId); const isOnline = message.online !== undefined ? message.online : (message.type === 'ONLINE'); this.onlineStatusCache.set(userId, isOnline); this.notifyStatusChange(userId, isOnline ? 'online' : 'offline'); } break; case 'OFFLINE': // 用户离线通知 if (message.userId || message.fromUserId) { const userId = String(message.userId || message.fromUserId); this.onlineStatusCache.set(userId, false); this.notifyStatusChange(userId, 'offline'); } break; } } catch (error) { console.error('❌ 消息解析失败:', error); } } /** * 启动心跳 */ startHeartbeat() { this.stopHeartbeat(); this.heartbeatTimer = setInterval(() => { if (this.isConnected) { this.sendMessage({ type: 'ping', // 改为小写,匹配后端 fromUserId: this.userId, timestamp: Date.now() }); } else { console.warn('⚠️ WebSocket未连接,跳过心跳'); } }, this.heartbeatInterval); } /** * 停止心跳 */ stopHeartbeat() { if (this.heartbeatTimer) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } } /** * 发送消息 */ sendMessage(data) { if (!this.isConnected) { console.warn('⚠️ WebSocket未连接,无法发送消息'); return; } try { uni.sendSocketMessage({ data: JSON.stringify(data), fail: (err) => { console.error('❌ 发送消息失败:', err); } }); } catch (error) { console.error('❌ 发送消息异常:', error); } } /** * 监听用户状态变化 * @param {String} targetUserId 目标用户ID * @param {Function} callback 回调函数 (status) => {} */ onStatusChange(targetUserId, callback) { if (!this.statusCallbacks.has(targetUserId)) { this.statusCallbacks.set(targetUserId, []); } this.statusCallbacks.get(targetUserId).push(callback); } /** * 移除状态监听 * @param {String} targetUserId 目标用户ID * @param {Function} callback 回调函数 */ offStatusChange(targetUserId, callback) { if (!this.statusCallbacks.has(targetUserId)) { return; } const callbacks = this.statusCallbacks.get(targetUserId); const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } if (callbacks.length === 0) { this.statusCallbacks.delete(targetUserId); } } /** * 通知状态变化 */ notifyStatusChange(userId, status) { const callbacks = this.statusCallbacks.get(userId); if (callbacks && callbacks.length > 0) { callbacks.forEach(callback => { try { callback(status); } catch (error) { console.error('❌ 状态回调执行失败:', error); } }); } } /** * 获取缓存的在线状态 * @param {String} userId 目标用户ID * @return {Boolean|null} 在线状态,null表示未知 */ getCachedStatus(userId) { return this.onlineStatusCache.get(String(userId)) || null; } /** * 计划重连 */ scheduleReconnect() { if (this.reconnectTimer) { return; } this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; if (!this.isConnected && this.userId) { this.connectWebSocket(); } }, this.reconnectInterval); } /** * 发送队列中的消息 */ flushMessageQueue() { if (this.messageQueue.length === 0) { return; } while (this.messageQueue.length > 0) { const message = this.messageQueue.shift(); this.sendMessage(message); } } /** * 断开连接 */ disconnect() { this.stopHeartbeat(); this.stopHttpPolling(); // 停止 HTTP 轮询 if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } // 移除 TIM 事件监听 if (timManager.tim) { const TIM = timManager.tim.TIM; if (this.timConnectListener) { timManager.tim.off(TIM.EVENT.NET_STATE_CHANGE, this.timConnectListener); } if (this.timStatusListener) { timManager.tim.off(TIM.EVENT.USER_STATUS_UPDATED, this.timStatusListener); } } if (this.isConnected) { uni.closeSocket(); this.isConnected = false; } this.statusCallbacks.clear(); this.userId = null; } /** * 获取连接状态 */ getConnectionStatus() { return this.isConnected; } } // 导出单例 export default new TIMPresenceManager();