tim-manager.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  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. // 使用V2接口,支持红娘消息(TIM用户ID可能是 m_xxx 格式)
  128. const syncData = {
  129. messageId: timMessage.ID,
  130. fromTimUserId: String(timMessage.from), // 保持字符串格式,支持 m_xxx
  131. toTimUserId: String(timMessage.to), // 保持字符串格式,支持 m_xxx
  132. messageType: getMessageType(timMessage),
  133. content: getMessageContent(timMessage),
  134. sendTime: timMessage.time
  135. };
  136. // 如果是语音消息,添加语音信息
  137. if (timMessage.type === 'TIMSoundElem' && timMessage.payload) {
  138. syncData.mediaUrl = timMessage.payload.url || timMessage.payload.remoteAudioUrl;
  139. syncData.duration = timMessage.payload.second || 0;
  140. syncData.mediaSize = timMessage.payload.size || 0;
  141. }
  142. // 如果是自定义消息(我们的语音消息)
  143. if (timMessage.type === 'TIMCustomElem' && timMessage.payload) {
  144. try {
  145. const customData = JSON.parse(timMessage.payload.data);
  146. if (customData.type === 'voice') {
  147. syncData.mediaUrl = customData.url;
  148. syncData.duration = customData.duration;
  149. syncData.mediaSize = customData.size || 0;
  150. }
  151. } catch (e) {
  152. console.error('解析自定义消息失败:', e);
  153. }
  154. }
  155. // 如果是图片消息,添加图片信息
  156. if (timMessage.type === 'TIMImageElem' && timMessage.payload.imageInfoArray) {
  157. const imageInfo = timMessage.payload.imageInfoArray[0];
  158. syncData.mediaUrl = imageInfo.imageUrl;
  159. syncData.thumbnailUrl = imageInfo.imageUrl;
  160. }
  161. // 调用后端同步接口(V2版本,支持红娘消息)
  162. const res = await uni.request({
  163. url: 'http://localhost:8083/api/chat/syncTIMMessageV2',
  164. method: 'POST',
  165. data: syncData,
  166. header: {
  167. 'Content-Type': 'application/json'
  168. }
  169. });
  170. if (res[1].data.code === 200) {
  171. const msgType = res[1].data.type === 'matchmaker' ? '红娘消息' : '用户消息';
  172. console.log(`✅ 接收消息已同步到MySQL (${msgType}):`, timMessage.ID);
  173. }
  174. } catch (error) {
  175. console.error('❌ 同步接收消息失败:', error);
  176. // 同步失败不影响主流程
  177. }
  178. }
  179. /**
  180. * 消息已读回执(对方已读我发送的消息)
  181. */
  182. onMessageReadByPeer(event) {
  183. console.log('=== 📖 [全局] 收到已读回执事件 MESSAGE_READ_BY_PEER ===');
  184. console.log(' - 触发时间:', new Date().toLocaleString());
  185. console.log(' - 事件数据:', JSON.stringify(event.data));
  186. // 触发所有已读回执回调
  187. this.messageReadCallbacks.forEach(callback => {
  188. try {
  189. callback(event);
  190. } catch (error) {
  191. console.error('❌ 已读回执回调执行失败:', error);
  192. }
  193. });
  194. }
  195. /**
  196. * 会话列表更新
  197. */
  198. onConversationListUpdated(event) {
  199. console.log('📋 会话列表更新:', event.data.length, '个');
  200. // 计算总未读数
  201. const conversationList = event.data;
  202. let totalUnread = 0;
  203. conversationList.forEach(conversation => {
  204. totalUnread += conversation.unreadCount || 0;
  205. });
  206. console.log('📊 总未读消息数:', totalUnread);
  207. // 更新到全局状态(Vuex)- 多种方式确保更新成功
  208. try {
  209. // 方式1: 使用 getApp() 获取全局实例
  210. const app = getApp();
  211. if (app && app.$store) {
  212. app.$store.dispatch('updateTotalUnread', totalUnread);
  213. console.log('✅ 已更新全局未读数到 Vuex (getApp):', totalUnread);
  214. } else {
  215. console.warn('⚠️ getApp() 未获取到 $store');
  216. }
  217. // 方式2: 直接导入 store(备用方案)
  218. try {
  219. const store = require('../store/index.js').default;
  220. if (store) {
  221. store.dispatch('updateTotalUnread', totalUnread);
  222. console.log('✅ 已更新全局未读数到 Vuex (require):', totalUnread);
  223. }
  224. } catch (e) {
  225. console.warn('⚠️ require store 失败:', e.message);
  226. }
  227. // 方式3: 触发自定义事件,通知所有页面
  228. uni.$emit('conversationUnreadUpdate', totalUnread);
  229. console.log('✅ 已触发 conversationUnreadUpdate 事件:', totalUnread);
  230. } catch (error) {
  231. console.error('❌ 更新全局未读数失败:', error);
  232. }
  233. }
  234. /**
  235. * 被踢下线
  236. */
  237. onKickedOut(event) {
  238. console.log('❌ 被踢下线:', event.data.type);
  239. uni.showModal({
  240. title: '下线通知',
  241. content: '您的账号在其他设备登录',
  242. showCancel: false
  243. });
  244. }
  245. /**
  246. * 网络状态变化
  247. */
  248. onNetStateChange(event) {
  249. console.log('🌐 网络状态:', event.data.state);
  250. }
  251. /**
  252. * 登录
  253. */
  254. async login(userID, userSig) {
  255. try {
  256. console.log('📱 开始登录 TIM, userID:', userID);
  257. const res = await this.tim.login({
  258. userID: String(userID),
  259. userSig: userSig
  260. });
  261. console.log('✅ TIM 登录成功');
  262. this.userId = userID;
  263. return res;
  264. } catch (error) {
  265. console.error('❌ TIM 登录失败:', error);
  266. throw error;
  267. }
  268. }
  269. /**
  270. * 登出
  271. */
  272. async logout() {
  273. try {
  274. await this.tim.logout();
  275. console.log('✅ TIM 登出成功');
  276. this.isLogin = false;
  277. this.userId = null;
  278. } catch (error) {
  279. console.error('❌ TIM 登出失败:', error);
  280. }
  281. }
  282. /**
  283. * 发送文本消息
  284. */
  285. async sendTextMessage(toUserId, text) {
  286. try {
  287. // 验证参数
  288. if (!toUserId || toUserId === 'undefined' || toUserId === 'null') {
  289. console.error('❌ 接收者ID无效:', toUserId);
  290. throw new Error('接收者ID无效: ' + toUserId);
  291. }
  292. if (!this.userId || this.userId === 'undefined' || this.userId === 'null') {
  293. console.error('❌ 发送者ID无效:', this.userId);
  294. throw new Error('发送者ID未登录或无效');
  295. }
  296. const toUserIdStr = String(toUserId);
  297. console.log('📤 准备发送消息:');
  298. console.log(' - 发送者ID:', this.userId, '(类型:', typeof this.userId, ')');
  299. console.log(' - 接收者ID:', toUserIdStr, '(类型:', typeof toUserIdStr, ')');
  300. console.log(' - 消息内容:', text);
  301. // 创建文本消息
  302. const message = this.tim.createTextMessage({
  303. to: toUserIdStr,
  304. conversationType: TIM.TYPES.CONV_C2C, // 单聊
  305. payload: {
  306. text: text
  307. }
  308. });
  309. console.log('📤 消息已创建,准备发送...');
  310. // 发送消息
  311. const res = await this.tim.sendMessage(message);
  312. console.log('✅ 消息发送成功:', res.data.message);
  313. return res.data.message;
  314. } catch (error) {
  315. console.error('❌ 消息发送失败:', error);
  316. console.error(' - 错误详情:', error.message || error);
  317. console.error(' - 错误代码:', error.code || 'N/A');
  318. throw error;
  319. }
  320. }
  321. /**
  322. * 发送图片消息
  323. */
  324. async sendImageMessage(toUserId, filePath) {
  325. try {
  326. const message = this.tim.createImageMessage({
  327. to: String(toUserId),
  328. conversationType: TIM.TYPES.CONV_C2C,
  329. payload: {
  330. file: filePath
  331. },
  332. onProgress: (percent) => {
  333. console.log('📊 上传进度:', percent);
  334. }
  335. });
  336. const res = await this.tim.sendMessage(message);
  337. console.log('✅ 图片发送成功');
  338. return res.data.message;
  339. } catch (error) {
  340. console.error('❌ 图片发送失败:', error);
  341. throw error;
  342. }
  343. }
  344. /**
  345. * 发送语音消息(通过自定义消息携带MinIO URL)
  346. * @param {String} toUserId 接收者ID
  347. * @param {String} voiceUrl MinIO语音文件URL
  348. * @param {Number} duration 语音时长(秒)
  349. * @param {Number} fileSize 文件大小(字节)
  350. */
  351. async sendVoiceMessage(toUserId, voiceUrl, duration, fileSize) {
  352. try {
  353. console.log('🎤 准备发送语音消息:');
  354. console.log(' - 接收者ID:', toUserId);
  355. console.log(' - 语音URL:', voiceUrl);
  356. console.log(' - 时长:', duration, '秒');
  357. console.log(' - 文件大小:', fileSize, '字节');
  358. // 使用自定义消息发送语音信息
  359. const message = this.tim.createCustomMessage({
  360. to: String(toUserId),
  361. conversationType: TIM.TYPES.CONV_C2C,
  362. payload: {
  363. data: JSON.stringify({
  364. type: 'voice',
  365. url: voiceUrl,
  366. duration: duration,
  367. size: fileSize
  368. }),
  369. description: '[语音]',
  370. extension: voiceUrl
  371. }
  372. });
  373. const res = await this.tim.sendMessage(message);
  374. console.log('✅ 语音发送成功');
  375. return res.data.message;
  376. } catch (error) {
  377. console.error('❌ 语音发送失败:', error);
  378. throw error;
  379. }
  380. }
  381. /**
  382. * 获取会话列表
  383. */
  384. async getConversationList() {
  385. try {
  386. const res = await this.tim.getConversationList();
  387. console.log('📋 会话列表:', res.data.conversationList.length, '个');
  388. return res.data.conversationList;
  389. } catch (error) {
  390. console.error('❌ 获取会话列表失败:', error);
  391. throw error;
  392. }
  393. }
  394. /**
  395. * 获取聊天记录
  396. */
  397. async getMessageList(conversationID, count = 15) {
  398. try {
  399. const res = await this.tim.getMessageList({
  400. conversationID: conversationID,
  401. count: count
  402. });
  403. console.log('💬 聊天记录:', res.data.messageList.length, '条');
  404. return res.data.messageList;
  405. } catch (error) {
  406. console.error('❌ 获取聊天记录失败:', error);
  407. throw error;
  408. }
  409. }
  410. /**
  411. * 将消息设为已读
  412. */
  413. async setMessageRead(conversationID) {
  414. try {
  415. await this.tim.setMessageRead({ conversationID });
  416. console.log('✅ 消息已读');
  417. } catch (error) {
  418. console.error('❌ 设置已读失败:', error);
  419. }
  420. }
  421. /**
  422. * 撤回消息
  423. */
  424. async revokeMessage(message) {
  425. try {
  426. await this.tim.revokeMessage(message);
  427. console.log('✅ 消息已撤回');
  428. } catch (error) {
  429. console.error('❌ 撤回失败:', error);
  430. throw error;
  431. }
  432. }
  433. /**
  434. * 监听消息
  435. */
  436. onMessage(callback) {
  437. this.messageCallbacks.push(callback);
  438. }
  439. /**
  440. * 移除监听
  441. */
  442. offMessage(callback) {
  443. const index = this.messageCallbacks.indexOf(callback);
  444. if (index > -1) {
  445. this.messageCallbacks.splice(index, 1);
  446. }
  447. }
  448. /**
  449. * 监听已读回执
  450. */
  451. onMessageRead(callback) {
  452. this.messageReadCallbacks.push(callback);
  453. console.log('✅ 已注册已读回执回调,当前回调数:', this.messageReadCallbacks.length);
  454. }
  455. /**
  456. * 移除已读回执监听
  457. */
  458. offMessageRead(callback) {
  459. const index = this.messageReadCallbacks.indexOf(callback);
  460. if (index > -1) {
  461. this.messageReadCallbacks.splice(index, 1);
  462. console.log('✅ 已移除已读回执回调,剩余回调数:', this.messageReadCallbacks.length);
  463. }
  464. }
  465. /**
  466. * 获取 TIM 实例
  467. */
  468. getTim() {
  469. return this.tim;
  470. }
  471. /**
  472. * 获取当前登录的 IM 用户ID
  473. */
  474. getCurrentUserId() {
  475. return this.userId ? String(this.userId) : null;
  476. }
  477. }
  478. // 导出单例
  479. const timManager = new TIMManager();
  480. export default timManager;