findme-miniprogram-frontend/utils/nim-presence-manager.js

338 lines
10 KiB
JavaScript
Raw Permalink Normal View History

2025-12-27 17:16:03 +08:00
/**
* 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=trueSDK 可能会立即返回用户状态列表
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;