tim-presence-manager.js 20 KB

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