浏览代码

用户在线状态2

mazhenhang 1 月之前
父节点
当前提交
e6e65e4da5

+ 20 - 33
LiangZhiYUMao/App.vue

@@ -1,14 +1,11 @@
 <script>
-import presenceManager from '@/utils/presence-manager.js';
+import timPresenceManager from '@/utils/tim-presence-manager.js';
 import timManager from '@/utils/tim-manager.js';
 
 export default {
 	onLaunch: function () {
 		console.log('=== App启动 ===');
 		
-		// 初始化全局在线状态
-		this.initGlobalPresence();
-		
 		// 初始化TIM(全局)
 		this.initGlobalTIM();
 	},
@@ -16,9 +13,6 @@ export default {
 	onShow: function () {
 		console.log('=== App显示 ===');
 		
-		// App从后台切换到前台时,重新连接
-		this.initGlobalPresence();
-		
 		// 延迟检查TIM连接状态,避免在登录过程中重复调用
 		setTimeout(() => {
 			this.checkTIMConnection();
@@ -31,32 +25,6 @@ export default {
 	},
 	
 	methods: {
-		/**
-		 * 初始化全局在线状态
-		 */
-		initGlobalPresence() {
-			try {
-				// 获取当前登录用户ID
-				const userInfo = uni.getStorageSync('userInfo');
-				const userId = uni.getStorageSync('userId') || userInfo?.userId || userInfo?.id;
-				
-				if (userId) {
-					console.log('🌐 初始化全局在线状态,用户ID:', userId);
-					
-					// 如果未连接,则连接
-					if (!presenceManager.getConnectionStatus()) {
-						presenceManager.connect(String(userId));
-						console.log('✅ 全局在线状态WebSocket已连接');
-					} else {
-						console.log('✅ 全局在线状态WebSocket已存在');
-					}
-				} else {
-					console.log('⚠️ 未登录,跳过在线状态初始化');
-				}
-			} catch (error) {
-				console.error('❌ 初始化全局在线状态失败:', error);
-			}
-		},
 		
 		/**
 		 * 初始化全局TIM
@@ -110,6 +78,9 @@ export default {
 				// 登录TIM
 				await timManager.login(String(userId), userSig);
 				console.log('✅ 全局TIM登录成功');
+				
+				// TIM 登录成功后,初始化在线状态管理器
+				await this.initPresenceManager(userId);
 			} else {
 				throw new Error('获取UserSig失败');
 			}
@@ -118,6 +89,22 @@ export default {
 			}
 		},
 		
+		/**
+		 * 初始化在线状态管理器
+		 */
+		async initPresenceManager(userId) {
+			try {
+				console.log('🌐 初始化在线状态管理器,用户ID:', userId);
+				
+				// 初始化 TIM + WebSocket 混合状态管理器
+				await timPresenceManager.init(String(userId));
+				
+				console.log('✅ 在线状态管理器初始化成功');
+			} catch (error) {
+				console.error('❌ 初始化在线状态管理器失败:', error);
+			}
+		},
+		
 		/**
 		 * 检查TIM连接状态
 		 */

+ 43 - 14
LiangZhiYUMao/pages/message/index.vue

@@ -398,12 +398,21 @@ export default {
 	uni.$off('blacklistUpdated', this.handleBlacklistUpdate);
 	
 	// 清理在线状态监听
-	const presenceManager = require('@/utils/presence-manager.js').default;
+	const timPresenceManager = require('@/utils/tim-presence-manager.js').default;
+	const userIdList = [];
 	this.conversations.forEach(conv => {
 	  if (conv.onlineStatusCallback) {
-	    presenceManager.offStatusChange(conv.targetUserId, conv.onlineStatusCallback);
+	    timPresenceManager.offStatusChange(conv.targetUserId, conv.onlineStatusCallback);
+	    userIdList.push(conv.targetUserId);
 	  }
 	});
+	
+	// 取消订阅用户状态
+	if (userIdList.length > 0) {
+	  timPresenceManager.unsubscribeUserStatus(userIdList).catch(err => {
+	    console.error('❌ 取消订阅失败:', err);
+	  });
+	}
   },
   
   methods: {
@@ -1221,10 +1230,26 @@ export default {
     /**
      * 初始化在线状态监听
      */
-    initOnlineStatusListener() {
+    async initOnlineStatusListener() {
       console.log('👂 初始化在线状态监听');
       
-      const presenceManager = require('@/utils/presence-manager.js').default;
+      const timPresenceManager = require('@/utils/tim-presence-manager.js').default;
+      
+      // 收集所有需要订阅的用户ID
+      const userIdList = this.conversations.map(conv => conv.targetUserId).filter(id => id);
+      
+      if (userIdList.length === 0) {
+        console.log('⚠️ 没有需要订阅的用户');
+        return;
+      }
+      
+      // 使用 TIM 原生能力订阅用户状态
+      try {
+        await timPresenceManager.subscribeUserStatus(userIdList);
+        console.log('✅ 已订阅用户状态:', userIdList);
+      } catch (error) {
+        console.error('❌ 订阅用户状态失败:', error);
+      }
       
       // 监听所有用户的在线状态变化
       this.conversations.forEach(conv => {
@@ -1243,7 +1268,13 @@ export default {
         conv.onlineStatusCallback = callback;
         
         // 注册监听
-        presenceManager.onStatusChange(conv.targetUserId, callback);
+        timPresenceManager.onStatusChange(conv.targetUserId, callback);
+        
+        // 获取缓存的状态并初始化
+        const cachedStatus = timPresenceManager.getCachedStatus(conv.targetUserId);
+        if (cachedStatus !== null) {
+          this.$set(conv, 'isOnline', cachedStatus);
+        }
       });
     },
     
@@ -1253,17 +1284,15 @@ export default {
     async refreshOnlineStatus() {
       console.log('🔄 刷新在线状态');
       
-      const presenceManager = require('@/utils/presence-manager.js').default;
+      const timPresenceManager = require('@/utils/tim-presence-manager.js').default;
       
-      // 批量查询所有会话用户的在线状态
-      for (const conv of this.conversations) {
-        try {
-          const isOnline = await presenceManager.queryOnlineStatus(conv.targetUserId);
-          this.$set(conv, 'isOnline', isOnline);
-        } catch (error) {
-          console.error(`❌ 查询用户 ${conv.targetUserId} 在线状态失败:`, error);
+      // 从缓存中获取在线状态
+      this.conversations.forEach(conv => {
+        const cachedStatus = timPresenceManager.getCachedStatus(conv.targetUserId);
+        if (cachedStatus !== null) {
+          this.$set(conv, 'isOnline', cachedStatus);
         }
-      }
+      });
       
       console.log('✅ 在线状态刷新完成');
     },

+ 3 - 3
LiangZhiYUMao/pages/mine/index.vue

@@ -1073,9 +1073,9 @@
 						console.log('步骤1: 断开WebSocket连接')
 						// 断开在线状态WebSocket
 						try {
-							const presenceManager = require('@/utils/presence-manager.js').default;
-							if (presenceManager) {
-								presenceManager.disconnect();
+							const timPresenceManager = require('@/utils/tim-presence-manager.js').default;
+							if (timPresenceManager) {
+								timPresenceManager.disconnect();
 								console.log('✅ 在线状态WebSocket已断开');
 							}
 						} catch (error) {

+ 3 - 3
LiangZhiYUMao/pages/settings/index.vue

@@ -174,9 +174,9 @@
 						if (res.confirm) {
 							// 1. 断开WebSocket连接
 							try {
-								const presenceManager = require('@/utils/presence-manager.js').default;
-								if (presenceManager) {
-									presenceManager.disconnect();
+								const timPresenceManager = require('@/utils/tim-presence-manager.js').default;
+								if (timPresenceManager) {
+									timPresenceManager.disconnect();
 									console.log('✅ 在线状态WebSocket已断开');
 								}
 							} catch (error) {

+ 487 - 0
LiangZhiYUMao/utils/tim-presence-manager.js

@@ -0,0 +1,487 @@
+/**
+ * 基于 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;
+  }
+  
+  /**
+   * 初始化并连接
+   * @param {String} userId 当前用户ID
+   */
+  async init(userId) {
+    if (this.isConnected || !userId) {
+      return;
+    }
+    
+    this.userId = userId;
+    
+    // 1. 建立 WebSocket 连接
+    this.connectWebSocket();
+    
+    // 2. 监听 TIM 连接状态变更
+    this.listenTIMConnectionStatus();
+    
+    // 3. 监听 TIM 用户状态更新
+    this.listenTIMUserStatus();
+  }
+  
+  /**
+   * 建立 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 用户状态更新:', event.data);
+      
+      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);
+        });
+      }
+    };
+    
+    timManager.tim.on(TIM.EVENT.USER_STATUS_UPDATED, this.timStatusListener);
+    console.log('✅ 已监听 TIM 用户状态更新');
+  }
+  
+  /**
+   * 上报当前 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<String>} userIdList 用户ID列表
+   */
+  async subscribeUserStatus(userIdList) {
+    if (!timManager.tim || !timManager.isLogin) {
+      console.warn('⚠️ TIM 未登录,无法订阅用户状态');
+      return;
+    }
+    
+    try {
+      const result = await timManager.tim.subscribeUserStatus({
+        userIDList: userIdList.map(id => String(id))
+      });
+      
+      console.log('✅ 订阅用户状态成功:', result);
+      
+      // 订阅成功后,初始化这些用户的状态
+      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);
+        });
+      }
+      
+      return result;
+    } catch (error) {
+      console.error('❌ 订阅用户状态失败:', error);
+      throw error;
+    }
+  }
+  
+  /**
+   * 取消订阅用户状态
+   * @param {Array<String>} 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();
+    
+    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();