/** * 基于 WebSocket + 腾讯 IM 的用户在线状态管理器 * 核心思路: * 1. 利用腾讯 IM SDK 感知自身和好友的 IM 连接状态 * 2. 通过 WebSocket 向服务端上报 IM 状态变更 * 3. 通过 WebSocket 接收服务端推送的其他用户状态变更 */ import timManager from './tim-manager.js'; 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(); // 缓存在线状态 // WebSocket服务器地址 this.wsUrl = 'ws://localhost:8083/ws/chat'; // TIM 状态监听器引用(用于清理) this.timStatusListener = null; this.timConnectListener = null; } /** * 初始化 */ async init(userId) { this.userId = userId; console.log('🚀 初始化 tim-presence-manager,用户ID:', userId); // 连接 WebSocket(接收服务端推送的状态变更) this.connectWebSocket(); console.log('✅ tim-presence-manager 初始化完成'); } /** * 建立 WebSocket 连接 */ connectWebSocket() { try { this.ws = uni.connectSocket({ url: `${this.wsUrl}?userId=${this.userId}`, success: () => { console.log('🔌 WebSocket连接请求已发送'); }, fail: (err) => { console.error('❌ WebSocket连接失败:', err); this.scheduleReconnect(); } }); // 监听连接打开 uni.onSocketOpen(() => { console.log('✅ WebSocket已连接'); this.isConnected = true; this.startHeartbeat(); // 连接成功后,立即上报当前 TIM 连接状态 this.reportCurrentTIMStatus(); }); // 监听消息 uni.onSocketMessage((res) => { this.handleMessage(res.data); }); // 监听错误 uni.onSocketError((err) => { console.error('❌ WebSocket错误:', err); this.isConnected = false; this.scheduleReconnect(); }); // 监听关闭 uni.onSocketClose(() => { console.log('🔌 WebSocket已关闭'); this.isConnected = false; this.stopHeartbeat(); this.scheduleReconnect(); }); } catch (error) { console.error('❌ WebSocket连接异常:', error); this.scheduleReconnect(); } } /** * 监听 TIM 连接状态变更 */ listenTIMConnectionStatus() { if (!timManager.tim) { console.warn('⚠️ TIM 未初始化,无法监听连接状态'); return; } const TIM = timManager.tim.TIM; // 监听 IM 连接状态变化 this.timConnectListener = (event) => { console.log('📡 TIM 连接状态变更:', event.data.state); 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); console.log('✅ 已监听 TIM 连接状态变更'); } /** * 监听 TIM 用户状态更新(好友状态) */ listenTIMUserStatus() { if (!timManager.tim) { console.warn('⚠️ TIM 未初始化,无法监听用户状态'); return; } const TIM = timManager.tim.TIM; // 监听用户状态更新事件 this.timStatusListener = (event) => { console.log('=== 👥 收到 TIM 用户状态更新事件 ==='); console.log(' - 事件数据:', JSON.stringify(event.data, null, 2)); const { userStatusList } = event.data; if (userStatusList && userStatusList.length > 0) { console.log(` - 状态变更用户数: ${userStatusList.length}`); userStatusList.forEach(item => { const userId = item.userID; console.log(` - 用户 ${userId}:`); console.log(` 原始状态类型: ${item.statusType}`); console.log(` USER_STATUS_ONLINE: ${TIM.TYPES.USER_STATUS_ONLINE}`); console.log(` USER_STATUS_OFFLINE: ${TIM.TYPES.USER_STATUS_OFFLINE}`); const status = item.statusType === TIM.TYPES.USER_STATUS_ONLINE ? 'online' : 'offline'; console.log(` 判断结果: ${status}`); // 更新本地缓存 this.onlineStatusCache.set(String(userId), status === 'online'); console.log(` 已更新缓存: ${status === 'online'}`); // 通知状态变化 this.notifyStatusChange(String(userId), status); // 也可以通过 WS 上报给服务端,保证服务端状态一致 this.reportFriendStatus(userId, status); }); } else { console.warn(' ⚠️ userStatusList 为空或不存在'); } console.log('=== 状态更新处理完成 ==='); }; timManager.tim.on(TIM.EVENT.USER_STATUS_UPDATED, this.timStatusListener); console.log('✅ 已监听 TIM 用户状态更新事件'); console.log(' - 事件名称:', TIM.EVENT.USER_STATUS_UPDATED); } /** * 上报当前 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() }); console.log(`📤 已上报 IM 状态: ${status}`); } /** * 上报好友状态给服务端 * @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 轮询作为备用方案 console.log('💡 启用 HTTP 轮询备用方案'); this.startHttpPolling(userIdList); return; } console.log('📡 开始订阅用户状态...'); console.log(' - 订阅用户列表:', userIdList); console.log(' - TIM SDK 版本:', timManager.tim.VERSION); try { const result = await timManager.tim.subscribeUserStatus({ userIDList: userIdList.map(id => String(id)) }); console.log('✅ 订阅用户状态成功!'); console.log(' - 完整结果:', JSON.stringify(result, null, 2)); // 订阅成功后,初始化这些用户的状态 if (result.data && result.data.successUserList) { console.log('📋 成功订阅的用户列表:', result.data.successUserList.length, '个'); result.data.successUserList.forEach(user => { console.log(` - 用户 ${user.userID}:`); console.log(` 状态类型: ${user.statusType}`); console.log(` USER_STATUS_ONLINE 常量:`, timManager.tim.TIM.TYPES.USER_STATUS_ONLINE); console.log(` USER_STATUS_OFFLINE 常量:`, timManager.tim.TIM.TYPES.USER_STATUS_OFFLINE); const status = user.statusType === timManager.tim.TIM.TYPES.USER_STATUS_ONLINE ? 'online' : 'offline'; console.log(` 最终判断状态: ${status}`); 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); console.error(' - 错误详情:', error.message); console.error(' - 错误代码:', error.code); // 如果订阅失败,启用 HTTP 轮询备用方案 if (error.code === 70402 || error.code === 70403) { console.log('💡 TIM 在线状态功能未开启,启用 HTTP 轮询备用方案'); this.startHttpPolling(userIdList); } throw error; } } /** * HTTP 轮询备用方案(当 TIM 订阅失败时使用) * @param {Array} userIdList 用户ID列表 */ startHttpPolling(userIdList) { console.log('🔄 启动 HTTP 轮询,间隔 30 秒'); // 立即查询一次 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: 'http://localhost:8083/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; console.log('🛑 已停止 HTTP 轮询'); } } /** * 取消订阅用户状态 * @param {Array} userIdList 用户ID列表 */ async unsubscribeUserStatus(userIdList) { if (!timManager.tim || !timManager.isLogin) { return; } try { await timManager.tim.unsubscribeUserStatus({ userIDList: userIdList.map(id => String(id)) }); console.log('✅ 取消订阅用户状态成功'); } catch (error) { console.error('❌ 取消订阅用户状态失败:', error); } } /** * 处理接收到的消息 */ handleMessage(data) { try { const message = typeof data === 'string' ? JSON.parse(data) : data; switch (message.type) { case 'PONG': // 心跳响应 console.log('💓 收到心跳响应'); 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() }); } }, 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) { console.log(`👤 用户 ${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; } console.log('🔄 将在5秒后重连...'); this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; if (!this.isConnected && this.userId) { console.log('🔄 尝试重连...'); this.connectWebSocket(); } }, this.reconnectInterval); } /** * 断开连接 */ 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();