findme-miniprogram-frontend/utils/nim-presence-manager.js
2025-12-27 17:16:03 +08:00

337 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;