tim-presence-manager.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679
  1. /**
  2. * TIM在线状态管理器 + WebSocket聊天管理器
  3. * 统一管理WebSocket连接、心跳、在线状态、聊天消息
  4. */
  5. import timManager from './tim-manager.js';
  6. import TIM from 'tim-wx-sdk';
  7. class TIMPresenceManager {
  8. constructor() {
  9. this.ws = null;
  10. this.heartbeatTimer = null;
  11. this.reconnectTimer = null;
  12. this.heartbeatInterval = 30000; // 30秒心跳
  13. this.reconnectInterval = 5000; // 5秒重连
  14. this.isConnected = false;
  15. this.userId = null;
  16. this.statusCallbacks = new Map(); // 存储状态变化回调
  17. this.onlineStatusCache = new Map(); // 缓存在线状态
  18. this.wsUrl = 'ws://localhost:8083/ws/chat';
  19. // TIM 状态监听器引用(用于清理)
  20. this.timStatusListener = null;
  21. this.timConnectListener = null;
  22. // 聊天消息相关
  23. this.messageQueue = []; // 消息队列(断线时暂存)
  24. this.maxQueueSize = 100;
  25. this.onMessageCallback = null; // 消息回调
  26. this.onConnectCallback = null; // 连接回调
  27. this.onDisconnectCallback = null; // 断开回调
  28. this.onErrorCallback = null; // 错误回调
  29. this.reconnectCount = 0;
  30. this.maxReconnectCount = 10;
  31. }
  32. /**
  33. * 初始化
  34. */
  35. async init(userId) {
  36. this.userId = userId;
  37. console.log('🚀 ========== 初始化 tim-presence-manager ==========');
  38. console.log('🚀 用户ID:', userId);
  39. console.log('🚀 WebSocket URL:', this.wsUrl);
  40. // 连接 WebSocket(接收服务端推送的状态变更)
  41. console.log('🚀 开始连接 WebSocket...');
  42. this.connectWebSocket();
  43. console.log('✅ tim-presence-manager 初始化完成(WebSocket连接中...)');
  44. console.log('================================================');
  45. }
  46. /**
  47. * 建立 WebSocket 连接
  48. */
  49. connectWebSocket() {
  50. try {
  51. const wsUrl = `${this.wsUrl}?userId=${this.userId}`;
  52. console.log('🔌 准备连接 WebSocket:', wsUrl);
  53. console.log(' 当前用户ID:', this.userId);
  54. console.log(' WebSocket URL:', wsUrl);
  55. // 先关闭旧连接
  56. if (this.ws) {
  57. try {
  58. uni.closeSocket();
  59. } catch (e) {
  60. console.warn('关闭旧连接失败:', e);
  61. }
  62. }
  63. this.ws = uni.connectSocket({
  64. url: wsUrl,
  65. success: () => {
  66. console.log('✅ WebSocket连接请求已发送成功');
  67. },
  68. fail: (err) => {
  69. console.error('❌ WebSocket连接请求发送失败:', err);
  70. console.error(' 错误详情:', JSON.stringify(err));
  71. this.scheduleReconnect();
  72. }
  73. });
  74. // 使用 SocketTask 对象的监听器(推荐方式)
  75. this.ws.onOpen((res) => {
  76. console.log('🎉 ========== WebSocket 连接成功 ==========');
  77. console.log(' 响应数据:', res);
  78. console.log(' 用户ID:', this.userId);
  79. console.log(' 连接URL:', wsUrl);
  80. console.log('==========================================');
  81. this.isConnected = true;
  82. this.reconnectCount = 0; // 重置重连计数
  83. // 启动心跳
  84. this.startHeartbeat();
  85. // 发送队列中的消息
  86. this.flushMessageQueue();
  87. // 连接成功后,立即上报当前 TIM 连接状态
  88. this.reportCurrentTIMStatus();
  89. // 触发连接回调
  90. if (this.onConnectCallback) {
  91. this.onConnectCallback();
  92. }
  93. });
  94. // 监听消息
  95. this.ws.onMessage((res) => {
  96. console.log('📨 收到WebSocket消息:', res.data);
  97. this.handleMessage(res.data);
  98. });
  99. // 监听错误
  100. this.ws.onError((err) => {
  101. console.error('❌ ========== WebSocket 错误 ==========');
  102. console.error(' 错误信息:', err);
  103. console.error(' 错误详情:', JSON.stringify(err));
  104. console.error(' 用户ID:', this.userId);
  105. console.error(' 连接URL:', wsUrl);
  106. console.error('======================================');
  107. this.isConnected = false;
  108. // 触发错误回调
  109. if (this.onErrorCallback) {
  110. this.onErrorCallback(err);
  111. }
  112. this.scheduleReconnect();
  113. });
  114. // 监听关闭
  115. this.ws.onClose((res) => {
  116. console.log('🔌 ========== WebSocket 连接关闭 ==========');
  117. console.log(' 关闭信息:', res);
  118. console.log(' 关闭码:', res?.code);
  119. console.log(' 关闭原因:', res?.reason);
  120. console.log(' 用户ID:', this.userId);
  121. console.log('=========================================');
  122. this.isConnected = false;
  123. this.stopHeartbeat();
  124. // 触发断开回调
  125. if (this.onDisconnectCallback) {
  126. this.onDisconnectCallback();
  127. }
  128. this.scheduleReconnect();
  129. });
  130. } catch (error) {
  131. console.error('❌ WebSocket连接异常:', error);
  132. this.scheduleReconnect();
  133. }
  134. }
  135. /**
  136. * 监听 TIM 连接状态变更
  137. */
  138. listenTIMConnectionStatus() {
  139. if (!timManager.tim || !timManager.tim.TIM) {
  140. console.warn('⚠️ TIM 未初始化或 TIM 对象不完整,无法监听连接状态');
  141. return;
  142. }
  143. const TIM = timManager.tim.TIM;
  144. // 监听 IM 连接状态变化
  145. this.timConnectListener = (event) => {
  146. console.log('📡 TIM 连接状态变更:', event.data.state);
  147. let imStatus = 'offline';
  148. // 映射 TIM 状态到业务状态
  149. switch (event.data.state) {
  150. case TIM.TYPES.NET_STATE_CONNECTED:
  151. imStatus = 'online';
  152. break;
  153. case TIM.TYPES.NET_STATE_DISCONNECTED:
  154. case TIM.TYPES.NET_STATE_CONNECTING:
  155. imStatus = 'offline';
  156. break;
  157. }
  158. // 通过 WebSocket 上报状态给服务端
  159. this.reportIMStatus(imStatus);
  160. };
  161. timManager.tim.on(TIM.EVENT.NET_STATE_CHANGE, this.timConnectListener);
  162. console.log('✅ 已监听 TIM 连接状态变更');
  163. }
  164. /**
  165. * 监听 TIM 用户状态更新(好友状态)
  166. */
  167. listenTIMUserStatus() {
  168. if (!timManager.tim || !timManager.tim.TIM) {
  169. console.warn('⚠️ TIM 未初始化或 TIM 对象不完整,无法监听用户状态');
  170. return;
  171. }
  172. const TIM = timManager.tim.TIM;
  173. // 监听用户状态更新事件
  174. this.timStatusListener = (event) => {
  175. console.log('=== 👥 收到 TIM 用户状态更新事件 ===');
  176. console.log(' - 事件数据:', JSON.stringify(event.data, null, 2));
  177. const { userStatusList } = event.data;
  178. if (userStatusList && userStatusList.length > 0) {
  179. console.log(` - 状态变更用户数: ${userStatusList.length}`);
  180. userStatusList.forEach(item => {
  181. const userId = item.userID;
  182. console.log(` - 用户 ${userId}:`);
  183. console.log(` 原始状态类型: ${item.statusType}`);
  184. console.log(` USER_STATUS_ONLINE: ${TIM.TYPES.USER_STATUS_ONLINE}`);
  185. console.log(` USER_STATUS_OFFLINE: ${TIM.TYPES.USER_STATUS_OFFLINE}`);
  186. const status = item.statusType === TIM.TYPES.USER_STATUS_ONLINE ? 'online' : 'offline';
  187. console.log(` 判断结果: ${status}`);
  188. // 更新本地缓存
  189. this.onlineStatusCache.set(String(userId), status === 'online');
  190. console.log(` 已更新缓存: ${status === 'online'}`);
  191. // 通知状态变化
  192. this.notifyStatusChange(String(userId), status);
  193. // 也可以通过 WS 上报给服务端,保证服务端状态一致
  194. this.reportFriendStatus(userId, status);
  195. });
  196. } else {
  197. console.warn(' ⚠️ userStatusList 为空或不存在');
  198. }
  199. console.log('=== 状态更新处理完成 ===');
  200. };
  201. timManager.tim.on(TIM.EVENT.USER_STATUS_UPDATED, this.timStatusListener);
  202. console.log('✅ 已监听 TIM 用户状态更新事件');
  203. console.log(' - 事件名称:', TIM.EVENT.USER_STATUS_UPDATED);
  204. }
  205. /**
  206. * 上报当前 TIM 连接状态
  207. */
  208. reportCurrentTIMStatus() {
  209. if (!timManager.isLogin) {
  210. this.reportIMStatus('offline');
  211. return;
  212. }
  213. // TIM 已登录,上报在线状态
  214. this.reportIMStatus('online');
  215. }
  216. /**
  217. * 上报 IM 状态给服务端
  218. * @param {String} status 状态:online/offline
  219. */
  220. reportIMStatus(status) {
  221. if (!this.isConnected) {
  222. console.warn('⚠️ WebSocket 未连接,无法上报 IM 状态');
  223. return;
  224. }
  225. this.sendMessage({
  226. type: 'imStatusReport',
  227. userId: this.userId,
  228. status: status,
  229. device: 'mobile', // 设备类型
  230. timestamp: Date.now()
  231. });
  232. console.log(`📤 已上报 IM 状态: ${status}`);
  233. }
  234. /**
  235. * 上报好友状态给服务端
  236. * @param {String} friendUserId 好友用户ID
  237. * @param {String} status 状态:online/offline
  238. */
  239. reportFriendStatus(friendUserId, status) {
  240. if (!this.isConnected) {
  241. return;
  242. }
  243. this.sendMessage({
  244. type: 'friendStatusReport',
  245. userId: friendUserId,
  246. status: status,
  247. timestamp: Date.now()
  248. });
  249. }
  250. /**
  251. * 订阅用户状态(使用 TIM 原生能力)
  252. * @param {Array<String>} userIdList 用户ID列表
  253. */
  254. async subscribeUserStatus(userIdList) {
  255. if (!timManager.tim || !timManager.isLogin) {
  256. console.warn('⚠️ TIM 未登录,无法订阅用户状态');
  257. console.warn(' - timManager.tim:', !!timManager.tim);
  258. console.warn(' - timManager.isLogin:', timManager.isLogin);
  259. // 使用 HTTP 轮询作为备用方案
  260. console.log('💡 启用 HTTP 轮询备用方案');
  261. this.startHttpPolling(userIdList);
  262. return;
  263. }
  264. console.log('📡 开始订阅用户状态...');
  265. console.log(' - 订阅用户列表:', userIdList);
  266. console.log(' - TIM SDK 版本:', timManager.tim.VERSION);
  267. try {
  268. const result = await timManager.tim.subscribeUserStatus({
  269. userIDList: userIdList.map(id => String(id))
  270. });
  271. console.log('✅ 订阅用户状态成功!');
  272. console.log(' - 完整结果:', JSON.stringify(result, null, 2));
  273. // 订阅成功后,初始化这些用户的状态
  274. if (result.data && result.data.successUserList) {
  275. console.log('📋 成功订阅的用户列表:', result.data.successUserList.length, '个');
  276. result.data.successUserList.forEach(user => {
  277. console.log(` - 用户 ${user.userID}:`);
  278. console.log(` 状态类型: ${user.statusType}`);
  279. console.log(` USER_STATUS_ONLINE 常量:`, timManager.tim.TIM.TYPES.USER_STATUS_ONLINE);
  280. console.log(` USER_STATUS_OFFLINE 常量:`, timManager.tim.TIM.TYPES.USER_STATUS_OFFLINE);
  281. const status = user.statusType === timManager.tim.TIM.TYPES.USER_STATUS_ONLINE ? 'online' : 'offline';
  282. console.log(` 最终判断状态: ${status}`);
  283. this.onlineStatusCache.set(String(user.userID), status === 'online');
  284. this.notifyStatusChange(String(user.userID), status);
  285. });
  286. }
  287. // 检查失败列表
  288. if (result.data && result.data.failureUserList && result.data.failureUserList.length > 0) {
  289. console.warn('⚠️ 部分用户订阅失败:', result.data.failureUserList);
  290. }
  291. return result;
  292. } catch (error) {
  293. console.error('❌ 订阅用户状态失败:', error);
  294. console.error(' - 错误详情:', error.message);
  295. console.error(' - 错误代码:', error.code);
  296. // 如果订阅失败,启用 HTTP 轮询备用方案
  297. if (error.code === 70402 || error.code === 70403) {
  298. console.log('💡 TIM 在线状态功能未开启,启用 HTTP 轮询备用方案');
  299. this.startHttpPolling(userIdList);
  300. }
  301. throw error;
  302. }
  303. }
  304. /**
  305. * HTTP 轮询备用方案(当 TIM 订阅失败时使用)
  306. * @param {Array<String>} userIdList 用户ID列表
  307. */
  308. startHttpPolling(userIdList) {
  309. console.log('🔄 启动 HTTP 轮询,间隔 30 秒');
  310. // 立即查询一次
  311. this.pollUserStatus(userIdList);
  312. // 每 30 秒轮询一次
  313. this.pollingTimer = setInterval(() => {
  314. this.pollUserStatus(userIdList);
  315. }, 30000);
  316. }
  317. /**
  318. * 轮询用户状态
  319. */
  320. async pollUserStatus(userIdList) {
  321. for (const userId of userIdList) {
  322. try {
  323. const [err, res] = await uni.request({
  324. url: 'http://localhost:8083/api/online/checkStatus',
  325. method: 'GET',
  326. data: { userId }
  327. });
  328. if (!err && res.data && res.data.code === 200) {
  329. const isOnline = res.data.data.online || false;
  330. const oldStatus = this.onlineStatusCache.get(String(userId));
  331. // 只有状态变化时才通知
  332. if (oldStatus !== isOnline) {
  333. this.onlineStatusCache.set(String(userId), isOnline);
  334. this.notifyStatusChange(String(userId), isOnline ? 'online' : 'offline');
  335. }
  336. }
  337. } catch (error) {
  338. console.error(`查询用户 ${userId} 状态失败:`, error);
  339. }
  340. }
  341. }
  342. /**
  343. * 停止 HTTP 轮询
  344. */
  345. stopHttpPolling() {
  346. if (this.pollingTimer) {
  347. clearInterval(this.pollingTimer);
  348. this.pollingTimer = null;
  349. console.log('🛑 已停止 HTTP 轮询');
  350. }
  351. }
  352. /**
  353. * 取消订阅用户状态
  354. * @param {Array<String>} userIdList 用户ID列表
  355. */
  356. async unsubscribeUserStatus(userIdList) {
  357. if (!timManager.tim || !timManager.isLogin) {
  358. return;
  359. }
  360. try {
  361. await timManager.tim.unsubscribeUserStatus({
  362. userIDList: userIdList.map(id => String(id))
  363. });
  364. console.log('✅ 取消订阅用户状态成功');
  365. } catch (error) {
  366. console.error('❌ 取消订阅用户状态失败:', error);
  367. }
  368. }
  369. /**
  370. * 处理接收到的消息
  371. */
  372. handleMessage(data) {
  373. try {
  374. const message = typeof data === 'string' ? JSON.parse(data) : data;
  375. switch (message.type) {
  376. case 'pong': // 改为小写,匹配后端
  377. // 心跳响应
  378. console.log('💓 收到心跳响应');
  379. break;
  380. case 'ONLINE':
  381. case 'STATUS_UPDATE':
  382. // 用户上线/状态更新通知
  383. if (message.userId || message.fromUserId) {
  384. const userId = String(message.userId || message.fromUserId);
  385. const isOnline = message.online !== undefined ? message.online : (message.type === 'ONLINE');
  386. this.onlineStatusCache.set(userId, isOnline);
  387. this.notifyStatusChange(userId, isOnline ? 'online' : 'offline');
  388. }
  389. break;
  390. case 'OFFLINE':
  391. // 用户离线通知
  392. if (message.userId || message.fromUserId) {
  393. const userId = String(message.userId || message.fromUserId);
  394. this.onlineStatusCache.set(userId, false);
  395. this.notifyStatusChange(userId, 'offline');
  396. }
  397. break;
  398. }
  399. } catch (error) {
  400. console.error('❌ 消息解析失败:', error);
  401. }
  402. }
  403. /**
  404. * 启动心跳
  405. */
  406. startHeartbeat() {
  407. this.stopHeartbeat();
  408. this.heartbeatTimer = setInterval(() => {
  409. if (this.isConnected) {
  410. console.log('💓 ========== 发送心跳PING ==========');
  411. console.log('💓 用户ID:', this.userId);
  412. console.log('💓 WebSocket连接状态:', this.isConnected);
  413. console.log('💓 发送消息:', {
  414. type: 'ping',
  415. fromUserId: this.userId,
  416. timestamp: Date.now()
  417. });
  418. this.sendMessage({
  419. type: 'ping', // 改为小写,匹配后端
  420. fromUserId: this.userId,
  421. timestamp: Date.now()
  422. });
  423. console.log('💓 ====================================');
  424. } else {
  425. console.warn('⚠️ WebSocket未连接,跳过心跳');
  426. }
  427. }, this.heartbeatInterval);
  428. }
  429. /**
  430. * 停止心跳
  431. */
  432. stopHeartbeat() {
  433. if (this.heartbeatTimer) {
  434. clearInterval(this.heartbeatTimer);
  435. this.heartbeatTimer = null;
  436. }
  437. }
  438. /**
  439. * 发送消息
  440. */
  441. sendMessage(data) {
  442. if (!this.isConnected) {
  443. console.warn('⚠️ WebSocket未连接,无法发送消息');
  444. return;
  445. }
  446. try {
  447. uni.sendSocketMessage({
  448. data: JSON.stringify(data),
  449. fail: (err) => {
  450. console.error('❌ 发送消息失败:', err);
  451. }
  452. });
  453. } catch (error) {
  454. console.error('❌ 发送消息异常:', error);
  455. }
  456. }
  457. /**
  458. * 监听用户状态变化
  459. * @param {String} targetUserId 目标用户ID
  460. * @param {Function} callback 回调函数 (status) => {}
  461. */
  462. onStatusChange(targetUserId, callback) {
  463. if (!this.statusCallbacks.has(targetUserId)) {
  464. this.statusCallbacks.set(targetUserId, []);
  465. }
  466. this.statusCallbacks.get(targetUserId).push(callback);
  467. }
  468. /**
  469. * 移除状态监听
  470. * @param {String} targetUserId 目标用户ID
  471. * @param {Function} callback 回调函数
  472. */
  473. offStatusChange(targetUserId, callback) {
  474. if (!this.statusCallbacks.has(targetUserId)) {
  475. return;
  476. }
  477. const callbacks = this.statusCallbacks.get(targetUserId);
  478. const index = callbacks.indexOf(callback);
  479. if (index > -1) {
  480. callbacks.splice(index, 1);
  481. }
  482. if (callbacks.length === 0) {
  483. this.statusCallbacks.delete(targetUserId);
  484. }
  485. }
  486. /**
  487. * 通知状态变化
  488. */
  489. notifyStatusChange(userId, status) {
  490. console.log(`👤 用户 ${userId} 状态变更: ${status}`);
  491. const callbacks = this.statusCallbacks.get(userId);
  492. if (callbacks && callbacks.length > 0) {
  493. callbacks.forEach(callback => {
  494. try {
  495. callback(status);
  496. } catch (error) {
  497. console.error('❌ 状态回调执行失败:', error);
  498. }
  499. });
  500. }
  501. }
  502. /**
  503. * 获取缓存的在线状态
  504. * @param {String} userId 目标用户ID
  505. * @return {Boolean|null} 在线状态,null表示未知
  506. */
  507. getCachedStatus(userId) {
  508. return this.onlineStatusCache.get(String(userId)) || null;
  509. }
  510. /**
  511. * 计划重连
  512. */
  513. scheduleReconnect() {
  514. if (this.reconnectTimer) {
  515. return;
  516. }
  517. console.log('🔄 将在5秒后重连...');
  518. this.reconnectTimer = setTimeout(() => {
  519. this.reconnectTimer = null;
  520. if (!this.isConnected && this.userId) {
  521. console.log('🔄 尝试重连...');
  522. this.connectWebSocket();
  523. }
  524. }, this.reconnectInterval);
  525. }
  526. /**
  527. * 发送队列中的消息
  528. */
  529. flushMessageQueue() {
  530. if (this.messageQueue.length === 0) {
  531. return;
  532. }
  533. console.log(`📤 发送队列中的 ${this.messageQueue.length} 条消息`);
  534. while (this.messageQueue.length > 0) {
  535. const message = this.messageQueue.shift();
  536. this.sendMessage(message);
  537. }
  538. }
  539. /**
  540. * 断开连接
  541. */
  542. disconnect() {
  543. this.stopHeartbeat();
  544. this.stopHttpPolling(); // 停止 HTTP 轮询
  545. if (this.reconnectTimer) {
  546. clearTimeout(this.reconnectTimer);
  547. this.reconnectTimer = null;
  548. }
  549. // 移除 TIM 事件监听
  550. if (timManager.tim) {
  551. const TIM = timManager.tim.TIM;
  552. if (this.timConnectListener) {
  553. timManager.tim.off(TIM.EVENT.NET_STATE_CHANGE, this.timConnectListener);
  554. }
  555. if (this.timStatusListener) {
  556. timManager.tim.off(TIM.EVENT.USER_STATUS_UPDATED, this.timStatusListener);
  557. }
  558. }
  559. if (this.isConnected) {
  560. uni.closeSocket();
  561. this.isConnected = false;
  562. }
  563. this.statusCallbacks.clear();
  564. this.userId = null;
  565. }
  566. /**
  567. * 获取连接状态
  568. */
  569. getConnectionStatus() {
  570. return this.isConnected;
  571. }
  572. }
  573. // 导出单例
  574. export default new TIMPresenceManager();