tim-manager.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531
  1. import TIM from 'tim-wx-sdk';
  2. class TIMManager {
  3. constructor() {
  4. this.tim = null;
  5. this.isLogin = false;
  6. this.userId = null;
  7. this.messageCallbacks = [];
  8. this.messageReadCallbacks = []; // 🔥 添加已读回执回调列表
  9. }
  10. /**
  11. * 初始化 TIM SDK
  12. */
  13. init(sdkAppID) {
  14. // 创建 SDK 实例
  15. this.tim = TIM.create({
  16. SDKAppID: sdkAppID
  17. });
  18. // 设置日志级别(开发时设为 0,生产设为 1)
  19. this.tim.setLogLevel(0);
  20. // 注册监听事件
  21. this.registerEvents();
  22. console.log('✅ TIM SDK 初始化完成');
  23. }
  24. /**
  25. * 注册事件监听
  26. */
  27. registerEvents() {
  28. // SDK 进入 ready 状态
  29. this.tim.on(TIM.EVENT.SDK_READY, this.onSdkReady.bind(this));
  30. // SDK 未 ready
  31. this.tim.on(TIM.EVENT.SDK_NOT_READY, this.onSdkNotReady.bind(this));
  32. // 收到新消息
  33. this.tim.on(TIM.EVENT.MESSAGE_RECEIVED, this.onMessageReceived.bind(this));
  34. // 会话列表更新
  35. this.tim.on(TIM.EVENT.CONVERSATION_LIST_UPDATED, this.onConversationListUpdated.bind(this));
  36. // 🔥 消息已读回执(对方已读我发送的消息)
  37. this.tim.on(TIM.EVENT.MESSAGE_READ_BY_PEER, this.onMessageReadByPeer.bind(this));
  38. // 被踢下线
  39. this.tim.on(TIM.EVENT.KICKED_OUT, this.onKickedOut.bind(this));
  40. // 网络状态变化
  41. this.tim.on(TIM.EVENT.NET_STATE_CHANGE, this.onNetStateChange.bind(this));
  42. }
  43. /**
  44. * SDK Ready
  45. */
  46. onSdkReady() {
  47. console.log('✅ TIM SDK Ready');
  48. this.isLogin = true;
  49. }
  50. /**
  51. * SDK Not Ready
  52. */
  53. onSdkNotReady() {
  54. console.log('⚠️ TIM SDK Not Ready');
  55. this.isLogin = false;
  56. }
  57. /**
  58. * 收到新消息
  59. */
  60. onMessageReceived(event) {
  61. const messageList = event.data;
  62. console.log('📥 收到新消息:', messageList.length, '条');
  63. // 同步接收到的消息到MySQL(双重保障)
  64. messageList.forEach(message => {
  65. this.syncReceivedMessageToMySQL(message);
  66. });
  67. // 触发回调
  68. this.messageCallbacks.forEach(callback => {
  69. callback(messageList);
  70. });
  71. }
  72. /**
  73. * 同步接收到的消息到MySQL
  74. */
  75. async syncReceivedMessageToMySQL(timMessage) {
  76. try {
  77. // 获取消息类型
  78. const getMessageType = (msg) => {
  79. const typeMap = {
  80. 'TIMTextElem': 1,
  81. 'TIMImageElem': 2,
  82. 'TIMSoundElem': 3,
  83. 'TIMVideoFileElem': 4,
  84. 'TIMFileElem': 5
  85. };
  86. // 处理自定义消息(我们的语音消息)
  87. if (msg.type === 'TIMCustomElem' && msg.payload) {
  88. try {
  89. const customData = JSON.parse(msg.payload.data);
  90. if (customData.type === 'voice') {
  91. return 3; // 语音消息
  92. }
  93. } catch (e) {
  94. console.error('解析自定义消息类型失败:', e);
  95. }
  96. }
  97. return typeMap[msg.type] || 1;
  98. };
  99. // 获取消息内容
  100. const getMessageContent = (msg) => {
  101. switch (msg.type) {
  102. case 'TIMTextElem':
  103. return msg.payload.text || '';
  104. case 'TIMImageElem':
  105. return '[图片]';
  106. case 'TIMSoundElem':
  107. return '[语音]';
  108. case 'TIMVideoFileElem':
  109. return '[视频]';
  110. case 'TIMFileElem':
  111. return '[文件]';
  112. case 'TIMCustomElem':
  113. // 处理自定义消息
  114. try {
  115. const customData = JSON.parse(msg.payload.data);
  116. if (customData.type === 'voice') {
  117. return '[语音]';
  118. }
  119. } catch (e) {
  120. console.error('解析自定义消息内容失败:', e);
  121. }
  122. return '[自定义消息]';
  123. default:
  124. return '[未知消息]';
  125. }
  126. };
  127. const syncData = {
  128. messageId: timMessage.ID,
  129. fromUserId: timMessage.from,
  130. toUserId: timMessage.to,
  131. messageType: getMessageType(timMessage),
  132. content: getMessageContent(timMessage),
  133. sendTime: timMessage.time
  134. };
  135. // 如果是语音消息,添加语音信息
  136. if (timMessage.type === 'TIMSoundElem' && timMessage.payload) {
  137. syncData.mediaUrl = timMessage.payload.url || timMessage.payload.remoteAudioUrl;
  138. syncData.duration = timMessage.payload.second || 0;
  139. syncData.mediaSize = timMessage.payload.size || 0;
  140. }
  141. // 如果是自定义消息(我们的语音消息)
  142. if (timMessage.type === 'TIMCustomElem' && timMessage.payload) {
  143. try {
  144. const customData = JSON.parse(timMessage.payload.data);
  145. if (customData.type === 'voice') {
  146. syncData.mediaUrl = customData.url;
  147. syncData.duration = customData.duration;
  148. syncData.mediaSize = customData.size || 0;
  149. }
  150. } catch (e) {
  151. console.error('解析自定义消息失败:', e);
  152. }
  153. }
  154. // 如果是图片消息,添加图片信息
  155. if (timMessage.type === 'TIMImageElem' && timMessage.payload.imageInfoArray) {
  156. const imageInfo = timMessage.payload.imageInfoArray[0];
  157. syncData.mediaUrl = imageInfo.imageUrl;
  158. syncData.thumbnailUrl = imageInfo.imageUrl;
  159. }
  160. // 调用后端同步接口
  161. const res = await uni.request({
  162. url: 'http://localhost:8083/api/chat/syncTIMMessage',
  163. method: 'POST',
  164. data: syncData,
  165. header: {
  166. 'Content-Type': 'application/json'
  167. }
  168. });
  169. if (res[1].data.code === 200) {
  170. console.log('✅ 接收消息已同步到MySQL:', timMessage.ID);
  171. }
  172. } catch (error) {
  173. console.error('❌ 同步接收消息失败:', error);
  174. // 同步失败不影响主流程
  175. }
  176. }
  177. /**
  178. * 消息已读回执(对方已读我发送的消息)
  179. */
  180. onMessageReadByPeer(event) {
  181. console.log('=== 📖 [全局] 收到已读回执事件 MESSAGE_READ_BY_PEER ===');
  182. console.log(' - 触发时间:', new Date().toLocaleString());
  183. console.log(' - 事件数据:', JSON.stringify(event.data));
  184. // 触发所有已读回执回调
  185. this.messageReadCallbacks.forEach(callback => {
  186. try {
  187. callback(event);
  188. } catch (error) {
  189. console.error('❌ 已读回执回调执行失败:', error);
  190. }
  191. });
  192. }
  193. /**
  194. * 会话列表更新
  195. */
  196. onConversationListUpdated(event) {
  197. console.log('📋 会话列表更新:', event.data.length, '个');
  198. // 计算总未读数
  199. const conversationList = event.data;
  200. let totalUnread = 0;
  201. conversationList.forEach(conversation => {
  202. totalUnread += conversation.unreadCount || 0;
  203. });
  204. console.log('📊 总未读消息数:', totalUnread);
  205. // 更新到全局状态(Vuex)- 多种方式确保更新成功
  206. try {
  207. // 方式1: 使用 getApp() 获取全局实例
  208. const app = getApp();
  209. if (app && app.$store) {
  210. app.$store.dispatch('updateTotalUnread', totalUnread);
  211. console.log('✅ 已更新全局未读数到 Vuex (getApp):', totalUnread);
  212. } else {
  213. console.warn('⚠️ getApp() 未获取到 $store');
  214. }
  215. // 方式2: 直接导入 store(备用方案)
  216. try {
  217. const store = require('../store/index.js').default;
  218. if (store) {
  219. store.dispatch('updateTotalUnread', totalUnread);
  220. console.log('✅ 已更新全局未读数到 Vuex (require):', totalUnread);
  221. }
  222. } catch (e) {
  223. console.warn('⚠️ require store 失败:', e.message);
  224. }
  225. // 方式3: 触发自定义事件,通知所有页面
  226. uni.$emit('conversationUnreadUpdate', totalUnread);
  227. console.log('✅ 已触发 conversationUnreadUpdate 事件:', totalUnread);
  228. } catch (error) {
  229. console.error('❌ 更新全局未读数失败:', error);
  230. }
  231. }
  232. /**
  233. * 被踢下线
  234. */
  235. onKickedOut(event) {
  236. console.log('❌ 被踢下线:', event.data.type);
  237. uni.showModal({
  238. title: '下线通知',
  239. content: '您的账号在其他设备登录',
  240. showCancel: false
  241. });
  242. }
  243. /**
  244. * 网络状态变化
  245. */
  246. onNetStateChange(event) {
  247. console.log('🌐 网络状态:', event.data.state);
  248. }
  249. /**
  250. * 登录
  251. */
  252. async login(userID, userSig) {
  253. try {
  254. console.log('📱 开始登录 TIM, userID:', userID);
  255. const res = await this.tim.login({
  256. userID: String(userID),
  257. userSig: userSig
  258. });
  259. console.log('✅ TIM 登录成功');
  260. this.userId = userID;
  261. return res;
  262. } catch (error) {
  263. console.error('❌ TIM 登录失败:', error);
  264. throw error;
  265. }
  266. }
  267. /**
  268. * 登出
  269. */
  270. async logout() {
  271. try {
  272. await this.tim.logout();
  273. console.log('✅ TIM 登出成功');
  274. this.isLogin = false;
  275. this.userId = null;
  276. } catch (error) {
  277. console.error('❌ TIM 登出失败:', error);
  278. }
  279. }
  280. /**
  281. * 发送文本消息
  282. */
  283. async sendTextMessage(toUserId, text) {
  284. try {
  285. // 验证参数
  286. if (!toUserId || toUserId === 'undefined' || toUserId === 'null') {
  287. console.error('❌ 接收者ID无效:', toUserId);
  288. throw new Error('接收者ID无效: ' + toUserId);
  289. }
  290. if (!this.userId || this.userId === 'undefined' || this.userId === 'null') {
  291. console.error('❌ 发送者ID无效:', this.userId);
  292. throw new Error('发送者ID未登录或无效');
  293. }
  294. const toUserIdStr = String(toUserId);
  295. console.log('📤 准备发送消息:');
  296. console.log(' - 发送者ID:', this.userId, '(类型:', typeof this.userId, ')');
  297. console.log(' - 接收者ID:', toUserIdStr, '(类型:', typeof toUserIdStr, ')');
  298. console.log(' - 消息内容:', text);
  299. // 创建文本消息
  300. const message = this.tim.createTextMessage({
  301. to: toUserIdStr,
  302. conversationType: TIM.TYPES.CONV_C2C, // 单聊
  303. payload: {
  304. text: text
  305. }
  306. });
  307. console.log('📤 消息已创建,准备发送...');
  308. // 发送消息
  309. const res = await this.tim.sendMessage(message);
  310. console.log('✅ 消息发送成功:', res.data.message);
  311. return res.data.message;
  312. } catch (error) {
  313. console.error('❌ 消息发送失败:', error);
  314. console.error(' - 错误详情:', error.message || error);
  315. console.error(' - 错误代码:', error.code || 'N/A');
  316. throw error;
  317. }
  318. }
  319. /**
  320. * 发送图片消息
  321. */
  322. async sendImageMessage(toUserId, filePath) {
  323. try {
  324. const message = this.tim.createImageMessage({
  325. to: String(toUserId),
  326. conversationType: TIM.TYPES.CONV_C2C,
  327. payload: {
  328. file: filePath
  329. },
  330. onProgress: (percent) => {
  331. console.log('📊 上传进度:', percent);
  332. }
  333. });
  334. const res = await this.tim.sendMessage(message);
  335. console.log('✅ 图片发送成功');
  336. return res.data.message;
  337. } catch (error) {
  338. console.error('❌ 图片发送失败:', error);
  339. throw error;
  340. }
  341. }
  342. /**
  343. * 发送语音消息(通过自定义消息携带MinIO URL)
  344. * @param {String} toUserId 接收者ID
  345. * @param {String} voiceUrl MinIO语音文件URL
  346. * @param {Number} duration 语音时长(秒)
  347. * @param {Number} fileSize 文件大小(字节)
  348. */
  349. async sendVoiceMessage(toUserId, voiceUrl, duration, fileSize) {
  350. try {
  351. console.log('🎤 准备发送语音消息:');
  352. console.log(' - 接收者ID:', toUserId);
  353. console.log(' - 语音URL:', voiceUrl);
  354. console.log(' - 时长:', duration, '秒');
  355. console.log(' - 文件大小:', fileSize, '字节');
  356. // 使用自定义消息发送语音信息
  357. const message = this.tim.createCustomMessage({
  358. to: String(toUserId),
  359. conversationType: TIM.TYPES.CONV_C2C,
  360. payload: {
  361. data: JSON.stringify({
  362. type: 'voice',
  363. url: voiceUrl,
  364. duration: duration,
  365. size: fileSize
  366. }),
  367. description: '[语音]',
  368. extension: voiceUrl
  369. }
  370. });
  371. const res = await this.tim.sendMessage(message);
  372. console.log('✅ 语音发送成功');
  373. return res.data.message;
  374. } catch (error) {
  375. console.error('❌ 语音发送失败:', error);
  376. throw error;
  377. }
  378. }
  379. /**
  380. * 获取会话列表
  381. */
  382. async getConversationList() {
  383. try {
  384. const res = await this.tim.getConversationList();
  385. console.log('📋 会话列表:', res.data.conversationList.length, '个');
  386. return res.data.conversationList;
  387. } catch (error) {
  388. console.error('❌ 获取会话列表失败:', error);
  389. throw error;
  390. }
  391. }
  392. /**
  393. * 获取聊天记录
  394. */
  395. async getMessageList(conversationID, count = 15) {
  396. try {
  397. const res = await this.tim.getMessageList({
  398. conversationID: conversationID,
  399. count: count
  400. });
  401. console.log('💬 聊天记录:', res.data.messageList.length, '条');
  402. return res.data.messageList;
  403. } catch (error) {
  404. console.error('❌ 获取聊天记录失败:', error);
  405. throw error;
  406. }
  407. }
  408. /**
  409. * 将消息设为已读
  410. */
  411. async setMessageRead(conversationID) {
  412. try {
  413. await this.tim.setMessageRead({ conversationID });
  414. console.log('✅ 消息已读');
  415. } catch (error) {
  416. console.error('❌ 设置已读失败:', error);
  417. }
  418. }
  419. /**
  420. * 撤回消息
  421. */
  422. async revokeMessage(message) {
  423. try {
  424. await this.tim.revokeMessage(message);
  425. console.log('✅ 消息已撤回');
  426. } catch (error) {
  427. console.error('❌ 撤回失败:', error);
  428. throw error;
  429. }
  430. }
  431. /**
  432. * 监听消息
  433. */
  434. onMessage(callback) {
  435. this.messageCallbacks.push(callback);
  436. }
  437. /**
  438. * 移除监听
  439. */
  440. offMessage(callback) {
  441. const index = this.messageCallbacks.indexOf(callback);
  442. if (index > -1) {
  443. this.messageCallbacks.splice(index, 1);
  444. }
  445. }
  446. /**
  447. * 监听已读回执
  448. */
  449. onMessageRead(callback) {
  450. this.messageReadCallbacks.push(callback);
  451. console.log('✅ 已注册已读回执回调,当前回调数:', this.messageReadCallbacks.length);
  452. }
  453. /**
  454. * 移除已读回执监听
  455. */
  456. offMessageRead(callback) {
  457. const index = this.messageReadCallbacks.indexOf(callback);
  458. if (index > -1) {
  459. this.messageReadCallbacks.splice(index, 1);
  460. console.log('✅ 已移除已读回执回调,剩余回调数:', this.messageReadCallbacks.length);
  461. }
  462. }
  463. }
  464. // 导出单例
  465. const timManager = new TIMManager();
  466. export default timManager;