338 lines
10 KiB
JavaScript
338 lines
10 KiB
JavaScript
|
|
/**
|
|||
|
|
* 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<string>} userIds - 用户ID列表
|
|||
|
|
* @param {number} expiry - 订阅有效期(秒),默认 7 天,范围 60~2592000
|
|||
|
|
* @param {boolean} immediateSync - 是否立即同步状态值,默认 true (改为true以便立即获取状态)
|
|||
|
|
* @returns {Promise<void>}
|
|||
|
|
*/
|
|||
|
|
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<string>} userIds - 用户ID列表
|
|||
|
|
* @returns {Promise<void>}
|
|||
|
|
*/
|
|||
|
|
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<void>}
|
|||
|
|
*/
|
|||
|
|
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<string>} 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;
|