| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690 |
- /**
- * TIM在线状态管理器 + WebSocket聊天管理器
- * 统一管理WebSocket连接、心跳、在线状态、聊天消息
- */
- import timManager from './tim-manager.js';
- import TIM from 'tim-wx-sdk';
- // WebSocket/TIM 调试日志开关
- const DEBUG_WS = false;
- 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(); // 缓存在线状态
- this.wsUrl = 'ws://localhost:8083/ws/chat';
-
- // TIM 状态监听器引用(用于清理)
- this.timStatusListener = null;
- this.timConnectListener = null;
-
- // 聊天消息相关
- this.messageQueue = []; // 消息队列(断线时暂存)
- this.maxQueueSize = 100;
- this.onMessageCallback = null; // 消息回调
- this.onConnectCallback = null; // 连接回调
- this.onDisconnectCallback = null; // 断开回调
- this.onErrorCallback = null; // 错误回调
- this.reconnectCount = 0;
- this.maxReconnectCount = 10;
- }
-
- /**
- * 初始化
- */
- async init(userId) {
- this.userId = userId;
-
- if (DEBUG_WS) {
- console.log('?? ========== 初始化 tim-presence-manager ==========');
- console.log('?? 用户ID:', userId);
- console.log('?? WebSocket URL:', this.wsUrl);
- }
-
- // 连接 WebSocket(接收服务端推送的状态变更)
- if (DEBUG_WS) console.log('?? 开始连接 WebSocket...');
- this.connectWebSocket();
-
- console.log('✅ tim-presence-manager 初始化完成(WebSocket连接中...)');
- console.log('================================================');
- }
-
- /**
- * 建立 WebSocket 连接
- */
- connectWebSocket() {
- try {
- const wsUrl = `${this.wsUrl}?userId=${this.userId}`;
- if (DEBUG_WS) {
- console.log('?? 准备连接 WebSocket:', wsUrl);
- console.log(' 当前用户ID:', this.userId);
- console.log(' WebSocket URL:', wsUrl);
- }
-
- // 先关闭旧连接
- if (this.ws) {
- try {
- uni.closeSocket();
- } catch (e) {
- console.warn('关闭旧连接失败:', e);
- }
- }
-
- this.ws = uni.connectSocket({
- url: wsUrl,
- success: () => {
- if (DEBUG_WS) console.log('✅ WebSocket连接请求已发送成功');
- },
- fail: (err) => {
- console.error('❌ WebSocket连接请求发送失败:', err);
- console.error(' 错误详情:', JSON.stringify(err));
- this.scheduleReconnect();
- }
- });
-
- // 使用 SocketTask 对象的监听器(推荐方式)
- this.ws.onOpen((res) => {
- if (DEBUG_WS) {
- console.log('?? ========== WebSocket 连接成功 ==========');
- console.log(' 响应数据:', res);
- console.log(' 用户ID:', this.userId);
- console.log(' 连接URL:', wsUrl);
- console.log('==========================================');
- }
-
- this.isConnected = true;
- this.reconnectCount = 0; // 重置重连计数
-
- // 启动心跳
- this.startHeartbeat();
-
- // 发送队列中的消息
- this.flushMessageQueue();
-
- // 连接成功后,立即上报当前 TIM 连接状态
- this.reportCurrentTIMStatus();
-
- // 触发连接回调
- if (this.onConnectCallback) {
- this.onConnectCallback();
- }
- });
-
- // 监听消息
- this.ws.onMessage((res) => {
- if (DEBUG_WS) console.log('?? 收到WebSocket消息:', res.data);
- this.handleMessage(res.data);
- });
-
- // 监听错误
- this.ws.onError((err) => {
- console.error('❌ ========== WebSocket 错误 ==========');
- console.error(' 错误信息:', err);
- console.error(' 错误详情:', JSON.stringify(err));
- console.error(' 用户ID:', this.userId);
- console.error(' 连接URL:', wsUrl);
- console.error('======================================');
-
- this.isConnected = false;
-
- // 触发错误回调
- if (this.onErrorCallback) {
- this.onErrorCallback(err);
- }
-
- this.scheduleReconnect();
- });
-
- // 监听关闭
- this.ws.onClose((res) => {
- if (DEBUG_WS) {
- console.log('?? ========== WebSocket 连接关闭 ==========');
- console.log(' 关闭信息:', res);
- console.log(' 关闭码:', res?.code);
- console.log(' 关闭原因:', res?.reason);
- console.log(' 用户ID:', this.userId);
- console.log('=========================================');
- }
-
- this.isConnected = false;
- this.stopHeartbeat();
-
- // 触发断开回调
- if (this.onDisconnectCallback) {
- this.onDisconnectCallback();
- }
-
- this.scheduleReconnect();
- });
-
- } catch (error) {
- console.error('❌ WebSocket连接异常:', error);
- this.scheduleReconnect();
- }
- }
-
- /**
- * 监听 TIM 连接状态变更
- */
- listenTIMConnectionStatus() {
- if (!timManager.tim || !timManager.tim.TIM) {
- console.warn('⚠️ TIM 未初始化或 TIM 对象不完整,无法监听连接状态');
- return;
- }
-
- const TIM = timManager.tim.TIM;
-
- // 监听 IM 连接状态变化
- this.timConnectListener = (event) => {
- if (DEBUG_WS) 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);
- if (DEBUG_WS) console.log('✅ 已监听 TIM 连接状态变更');
- }
-
- /**
- * 监听 TIM 用户状态更新(好友状态)
- */
- listenTIMUserStatus() {
- if (!timManager.tim || !timManager.tim.TIM) {
- console.warn('⚠️ TIM 未初始化或 TIM 对象不完整,无法监听用户状态');
- return;
- }
-
- const TIM = timManager.tim.TIM;
-
- // 监听用户状态更新事件
- this.timStatusListener = (event) => {
- console.log('=== ?? 收到 TIM 用户状态更新事件 ===');
- console.log(' - 事件数据:', JSON.stringify(event.data, null, 2));
-
- const { userStatusList } = event.data;
-
- if (userStatusList && userStatusList.length > 0) {
- console.log(` - 状态变更用户数: ${userStatusList.length}`);
-
- userStatusList.forEach(item => {
- const userId = item.userID;
- console.log(` - 用户 ${userId}:`);
- console.log(` 原始状态类型: ${item.statusType}`);
- console.log(` USER_STATUS_ONLINE: ${TIM.TYPES.USER_STATUS_ONLINE}`);
- console.log(` USER_STATUS_OFFLINE: ${TIM.TYPES.USER_STATUS_OFFLINE}`);
-
- const status = item.statusType === TIM.TYPES.USER_STATUS_ONLINE ? 'online' : 'offline';
- console.log(` 判断结果: ${status}`);
-
- // 更新本地缓存
- this.onlineStatusCache.set(String(userId), status === 'online');
- console.log(` 已更新缓存: ${status === 'online'}`);
-
- // 通知状态变化
- this.notifyStatusChange(String(userId), status);
-
- // 也可以通过 WS 上报给服务端,保证服务端状态一致
- this.reportFriendStatus(userId, status);
- });
- } else {
- console.warn(' ⚠️ userStatusList 为空或不存在');
- }
- console.log('=== 状态更新处理完成 ===');
- };
-
- timManager.tim.on(TIM.EVENT.USER_STATUS_UPDATED, this.timStatusListener);
- console.log('✅ 已监听 TIM 用户状态更新事件');
- console.log(' - 事件名称:', TIM.EVENT.USER_STATUS_UPDATED);
- }
-
- /**
- * 上报当前 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 未登录,无法订阅用户状态');
- console.warn(' - timManager.tim:', !!timManager.tim);
- console.warn(' - timManager.isLogin:', timManager.isLogin);
-
- // 使用 HTTP 轮询作为备用方案
- console.log('?? 启用 HTTP 轮询备用方案');
- this.startHttpPolling(userIdList);
- return;
- }
-
- console.log('?? 开始订阅用户状态...');
- console.log(' - 订阅用户列表:', userIdList);
- console.log(' - TIM SDK 版本:', timManager.tim.VERSION);
-
- try {
- const result = await timManager.tim.subscribeUserStatus({
- userIDList: userIdList.map(id => String(id))
- });
-
- console.log('✅ 订阅用户状态成功!');
- console.log(' - 完整结果:', JSON.stringify(result, null, 2));
-
- // 订阅成功后,初始化这些用户的状态
- if (result.data && result.data.successUserList) {
- console.log('?? 成功订阅的用户列表:', result.data.successUserList.length, '个');
-
- result.data.successUserList.forEach(user => {
- console.log(` - 用户 ${user.userID}:`);
- console.log(` 状态类型: ${user.statusType}`);
- console.log(` USER_STATUS_ONLINE 常量:`, timManager.tim.TIM.TYPES.USER_STATUS_ONLINE);
- console.log(` USER_STATUS_OFFLINE 常量:`, timManager.tim.TIM.TYPES.USER_STATUS_OFFLINE);
-
- const status = user.statusType === timManager.tim.TIM.TYPES.USER_STATUS_ONLINE ? 'online' : 'offline';
- console.log(` 最终判断状态: ${status}`);
-
- this.onlineStatusCache.set(String(user.userID), status === 'online');
- this.notifyStatusChange(String(user.userID), status);
- });
- }
-
- // 检查失败列表
- if (result.data && result.data.failureUserList && result.data.failureUserList.length > 0) {
- console.warn('⚠️ 部分用户订阅失败:', result.data.failureUserList);
- }
-
- return result;
- } catch (error) {
- console.error('❌ 订阅用户状态失败:', error);
- console.error(' - 错误详情:', error.message);
- console.error(' - 错误代码:', error.code);
-
- // 如果订阅失败,启用 HTTP 轮询备用方案
- if (error.code === 70402 || error.code === 70403) {
- console.log('?? TIM 在线状态功能未开启,启用 HTTP 轮询备用方案');
- this.startHttpPolling(userIdList);
- }
-
- throw error;
- }
- }
-
- /**
- * HTTP 轮询备用方案(当 TIM 订阅失败时使用)
- * @param {Array<String>} userIdList 用户ID列表
- */
- startHttpPolling(userIdList) {
- console.log('?? 启动 HTTP 轮询,间隔 30 秒');
-
- // 立即查询一次
- this.pollUserStatus(userIdList);
-
- // 每 30 秒轮询一次
- this.pollingTimer = setInterval(() => {
- this.pollUserStatus(userIdList);
- }, 30000);
- }
-
- /**
- * 轮询用户状态
- */
- async pollUserStatus(userIdList) {
- for (const userId of userIdList) {
- try {
- const [err, res] = await uni.request({
- url: 'http://localhost:8083/api/online/checkStatus',
- method: 'GET',
- data: { userId }
- });
-
- if (!err && res.data && res.data.code === 200) {
- const isOnline = res.data.data.online || false;
- const oldStatus = this.onlineStatusCache.get(String(userId));
-
- // 只有状态变化时才通知
- if (oldStatus !== isOnline) {
- this.onlineStatusCache.set(String(userId), isOnline);
- this.notifyStatusChange(String(userId), isOnline ? 'online' : 'offline');
- }
- }
- } catch (error) {
- console.error(`查询用户 ${userId} 状态失败:`, error);
- }
- }
- }
-
- /**
- * 停止 HTTP 轮询
- */
- stopHttpPolling() {
- if (this.pollingTimer) {
- clearInterval(this.pollingTimer);
- this.pollingTimer = null;
- console.log('?? 已停止 HTTP 轮询');
- }
- }
-
- /**
- * 取消订阅用户状态
- * @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) {
- console.log('?? ========== 发送心跳PING ==========');
- console.log('?? 用户ID:', this.userId);
- console.log('?? WebSocket连接状态:', this.isConnected);
- console.log('?? 发送消息:', {
- type: 'ping',
- fromUserId: this.userId,
- timestamp: Date.now()
- });
-
- this.sendMessage({
- type: 'ping', // 改为小写,匹配后端
- fromUserId: this.userId,
- timestamp: Date.now()
- });
-
- console.log('?? ====================================');
- } else {
- console.warn('⚠️ WebSocket未连接,跳过心跳');
- }
- }, 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);
- }
-
- /**
- * 发送队列中的消息
- */
- flushMessageQueue() {
- if (this.messageQueue.length === 0) {
- return;
- }
-
- console.log(`?? 发送队列中的 ${this.messageQueue.length} 条消息`);
-
- while (this.messageQueue.length > 0) {
- const message = this.messageQueue.shift();
- this.sendMessage(message);
- }
- }
-
- /**
- * 断开连接
- */
- disconnect() {
- this.stopHeartbeat();
- this.stopHttpPolling(); // 停止 HTTP 轮询
-
- 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();
|