tim-presence-manager.js 16 KB

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