/** * NIM 在线状态管理器 * 负责订阅和管理用户的在线状态 * * 功能: * 1. 订阅用户在线状态 * 2. 监听在线状态变化事件 * 3. 批量订阅/取消订阅 * 4. 本地缓存在线状态 */ const EventEmitter = require('./event-emitter.js'); class NimPresenceManager extends EventEmitter { constructor() { super(); this.nim = null; this.presenceCache = new Map(); // userId -> { online: boolean, lastUpdateTime: number } this.subscribed = new Set(); // 已订阅的用户ID集合 this.initialized = false; } /** * 初始化在线状态管理器 * @param {Object} nim - NIM 实例 */ init(nim) { if (this.initialized) { console.log('⚠️ NIM 在线状态管理器已初始化'); return; } if (!nim || !nim.V2NIMSubscriptionService) { console.error('❌ NIM 实例或订阅服务未初始化'); return; } this.nim = nim; this.setupEventListeners(); this.initialized = true; console.log('✅ NIM 在线状态管理器初始化成功'); } /** * 设置事件监听器 * 参考官方文档: https://doc.yunxin.163.com/messaging2/guide/DA0MjM3NTk?platform=client */ setupEventListeners() { try { // 使用 .on() 方法注册监听器(与其他服务保持一致) this.nim.V2NIMSubscriptionService.on('onUserStatusChanged', (userStatusList) => { this.handleUserStatusChanged(userStatusList); }); console.log('✅ 在线状态监听器已注册'); } catch (error) { console.error('❌ 注册在线状态监听器失败:', error); } } /** * 处理用户状态变化事件 * @param {Array} userStatusList - 用户状态列表,数组元素为 V2NIMUserStatus 对象 * * V2NIMUserStatus 结构参考 (官方文档): * { * accountId: string, // 用户账号ID (必填) * statusType: number, // 状态类型 (必填): * // 0: 未知 * // 1: 登录 (在线) * // 2: 登出 (离线) * // 3: 断开连接 (离线) * // 10000+: 自定义状态 * clientType: V2NIMLoginClientType, // 终端类型 (必填) * publishTime: number, // 发布时间戳 (必填) * uniqueId: string, // 唯一ID (可选) * extension: string, // 扩展字段 (可选) * serverExtension: string // 服务端扩展 (可选) * } * * 参考: https://doc.yunxin.163.com/messaging2/client-apis/DAxNjk0Mzc?platform=client#V2NIMUserStatus */ handleUserStatusChanged(userStatusList) { if (!Array.isArray(userStatusList)) { return; } userStatusList.forEach(status => { const userId = status.accountId; if (!userId) { console.warn('⚠️ 用户状态缺少 accountId'); return; } // 转换 statusType 为数字(兼容字符串或数字) const statusTypeNum = typeof status.statusType === 'number' ? status.statusType : parseInt(status.statusType, 10); // statusType === 1 表示在线 const isOnline = statusTypeNum === 1; // 更新缓存 this.presenceCache.set(userId, { online: isOnline, lastUpdateTime: Date.now(), statusType: statusTypeNum, clientType: status.clientType || null, publishTime: status.publishTime || 0, uniqueId: status.uniqueId || '', extension: status.extension || '', serverExtension: status.serverExtension || '' }); // 触发状态变化事件 this.emit('presence_changed', { userId, isOnline, statusType: statusTypeNum, clientType: status.clientType || null, publishTime: status.publishTime || 0, uniqueId: status.uniqueId || '', extension: status.extension || '', serverExtension: status.serverExtension || '' }); }); } /** * 订阅用户在线状态 * @param {Array} userIds - 用户ID列表 * @param {number} expiry - 订阅有效期(秒),默认 7 天,范围 60~2592000 * @param {boolean} immediateSync - 是否立即同步状态值,默认 true (改为true以便立即获取状态) * @returns {Promise} */ async subscribe(userIds, expiry = 7 * 24 * 3600, immediateSync = true) { if (!this.initialized || !this.nim) { console.error('❌ NIM 在线状态管理器未初始化'); return; } if (!Array.isArray(userIds) || userIds.length === 0) { console.warn('⚠️ 订阅用户列表为空'); return; } // 过滤已订阅的用户 const newUserIds = userIds.filter(id => !this.subscribed.has(id)); if (newUserIds.length === 0) { console.log('📝 所有用户已订阅,无需重复订阅'); return; } // 限制单次最多 100 个用户 if (newUserIds.length > 100) { console.warn(`⚠️ 单次订阅最多 100 个用户,当前 ${newUserIds.length} 个,将分批订阅`); const batches = []; for (let i = 0; i < newUserIds.length; i += 100) { batches.push(newUserIds.slice(i, i + 100)); } for (const batch of batches) { await this.subscribe(batch, expiry, immediateSync); } return; } try { // 调用 NIM SDK 订阅接口 const option = { accountIds: newUserIds, duration: expiry, immediateSync: immediateSync }; const result = await this.nim.V2NIMSubscriptionService.subscribeUserStatus(option); // 如果 immediateSync=true,SDK 可能会立即返回用户状态列表 if (result && Array.isArray(result) && result.length > 0) { this.handleUserStatusChanged(result); } else if (result && result.statusList && Array.isArray(result.statusList)) { this.handleUserStatusChanged(result.statusList); } // 记录已订阅 newUserIds.forEach(id => this.subscribed.add(id)); console.log(`✅ 已订阅 ${newUserIds.length} 个用户的在线状态`); } catch (error) { console.error('❌ 订阅用户在线状态失败:', error); throw error; } } /** * 取消订阅用户在线状态 * @param {Array} userIds - 用户ID列表 * @returns {Promise} */ async unsubscribe(userIds) { if (!this.initialized || !this.nim) { console.error('❌ NIM 在线状态管理器未初始化'); return; } if (!Array.isArray(userIds) || userIds.length === 0) { console.warn('⚠️ 取消订阅用户列表为空'); return; } try { console.log(`📡 取消订阅用户在线状态: ${userIds.length} 个用户`); // 调用 NIM SDK 取消订阅接口 // 参考: https://doc.yunxin.163.com/messaging2/client-apis/zY2MzAxNjQ?platform=client const option = { accountIds: userIds }; await this.nim.V2NIMSubscriptionService.unsubscribeUserStatus(option); // 从已订阅集合中移除 userIds.forEach(id => { this.subscribed.delete(id); this.presenceCache.delete(id); }); console.log(`✅ 成功取消订阅 ${userIds.length} 个用户的在线状态`); } catch (error) { console.error('❌ 取消订阅用户在线状态失败:', error); throw error; } } /** * 批量订阅会话列表中的用户 * @param {Array} conversations - 会话列表 * @returns {Promise} */ async subscribeConversations(conversations) { if (!Array.isArray(conversations) || conversations.length === 0) { return; } // 提取单聊用户ID(过滤群聊) const userIds = conversations .filter(conv => { // 单聊类型判断: type === 'single' 或 type === 0 或 chatType === 0 return conv.type === 'single' || conv.type === 0 || conv.chatType === 0; }) .map(conv => { // 优先使用 targetId,如果没有则从 conversationId 解析 let userId = conv.targetId; if (!userId && conv.conversationId && typeof conv.conversationId === 'string') { // conversationId 格式: accountId|type|targetId const parts = conv.conversationId.split('|'); userId = parts.length === 3 ? parts[2] : null; } return userId; }) .filter(Boolean); if (userIds.length > 0) { // immediateSync=true 确保首次订阅时立即返回在线状态 await this.subscribe(userIds, 7 * 24 * 3600, true); } } /** * 获取用户在线状态(从缓存) * @param {string} userId - 用户ID * @returns {Object|null} - { online: boolean, lastUpdateTime: number } */ getUserPresence(userId) { return this.presenceCache.get(userId) || null; } /** * 批量获取用户在线状态 * @param {Array} userIds - 用户ID列表 * @returns {Object} - { userId: { online: boolean, lastUpdateTime: number } } */ getBatchUserPresence(userIds) { const result = {}; userIds.forEach(userId => { const presence = this.getUserPresence(userId); if (presence) { result[userId] = presence; } }); return result; } /** * 清空缓存 */ clearCache() { this.presenceCache.clear(); this.subscribed.clear(); console.log('🗑️ 已清空在线状态缓存'); } /** * 检查管理器是否已初始化 * @returns {Boolean} 是否已初始化 */ getInitStatus() { return this.initialized && this.nim !== null; } /** * 销毁管理器 * 参考官方文档: https://doc.yunxin.163.com/messaging2/guide/DA0MjM3NTk?platform=client */ destroy() { if (this.nim && this.nim.V2NIMSubscriptionService) { try { // 使用 .off() 方法移除监听器 this.nim.V2NIMSubscriptionService.off('onUserStatusChanged'); console.log('✅ 已移除在线状态监听器'); } catch (error) { console.error('❌ 移除在线状态监听器失败:', error); } } this.clearCache(); this.nim = null; this.initialized = false; console.log('🔚 NIM 在线状态管理器已销毁'); } } // 导出单例 const nimPresenceManager = new NimPresenceManager(); module.exports = nimPresenceManager;