tim-presence-manager.js 18 KB

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