Jelajahi Sumber

用户在线状态{成功}

mazhenhang 1 bulan lalu
induk
melakukan
1f615aa907

+ 46 - 6
LiangZhiYUMao/App.vue

@@ -8,15 +8,27 @@ export default {
 		
 		// 初始化TIM(全局)
 		this.initGlobalTIM();
+		
+		// 延迟挂载全局方法(等待 App 实例创建完成)
+		setTimeout(() => {
+			const app = getApp();
+			if (app) {
+				app.globalData.initGlobalTIM = this.initGlobalTIM.bind(this);
+				app.globalData.initPresenceManager = this.initPresenceManager.bind(this);
+				console.log('✅ 全局方法挂载成功');
+			}
+		}, 100);
 	},
 	
 	onShow: function () {
 		console.log('=== App显示 ===');
+		console.log('   当前时间:', new Date().toLocaleTimeString());
 		
 		// 延迟检查TIM连接状态,避免在登录过程中重复调用
 		setTimeout(() => {
+			console.log('⏰ 开始执行 checkTIMConnection...');
 			this.checkTIMConnection();
-		}, 1000);
+		}, 500);  // 缩短延迟时间到 500ms
 	},
 	
 	onHide: function () {
@@ -106,14 +118,42 @@ export default {
 		},
 		
 		/**
-		 * 检查TIM连接状态
+		 * 检查TIM连接状态和WebSocket连接状态
 		 */
 		checkTIMConnection() {
-			if (timManager.isLogin) {
-				console.log('✅ TIM连接正常');
-			} else {
-				console.log('⚠️ TIM未连接,尝试重新初始化');
+			console.log('🔍 ========== 开始检查连接状态 ==========');
+			
+			// 获取当前登录用户ID
+			const userId = uni.getStorageSync('userId');
+			console.log('   当前用户ID:', userId);
+			
+			if (!userId) {
+				console.log('⚠️ 未登录,跳过连接检查');
+				console.log('=========================================');
+				return;
+			}
+			
+			// 检查 TIM 是否登录
+			console.log('   TIM 登录状态:', timManager.isLogin);
+			console.log('   TIM 用户ID:', timManager.userId);
+			
+			if (!timManager.isLogin) {
+				console.log('⚠️ TIM未登录,尝试重新初始化');
+				console.log('=========================================');
 				this.initGlobalTIM();
+				return;
+			}
+			
+			// 检查 WebSocket 是否连接
+			console.log('   WebSocket 连接状态:', timPresenceManager.isConnected);
+			
+			if (!timPresenceManager.isConnected) {
+				console.log('⚠️ WebSocket未连接,尝试重新初始化在线状态管理器');
+				console.log('=========================================');
+				this.initPresenceManager(userId);
+			} else {
+				console.log('✅ TIM和WebSocket连接正常');
+				console.log('=========================================');
 			}
 		}
 	},

+ 257 - 139
LiangZhiYUMao/pages/message/chat.vue

@@ -664,6 +664,9 @@ export default {
         // 转换为我们的消息格式
         this.messages = messageList.map(msg => this.convertMessage(msg));
         
+        // 🔥 获取会话对象,用 peerReadTime 更新已读状态
+        await this.updateMessageReadStatusByConversation();
+        
         // 滚动到底部
         this.$nextTick(() => {
           this.scrollToBottom();
@@ -701,6 +704,9 @@ export default {
         const newMessages = messageList.map(msg => this.convertMessage(msg));
         this.messages = [...newMessages, ...this.messages];
         
+        // 🔥 更新已读状态
+        await this.updateMessageReadStatusByConversation();
+        
       } catch (error) {
         console.error('❌ 加载更多消息失败:', error);
       } finally {
@@ -858,6 +864,60 @@ export default {
         isPeerRead: isPeerRead // 对方是否已读(只对自己发送的消息有意义)
       };
     },
+    
+    /**
+     * 根据会话的 peerReadTime 更新消息的已读状态
+     * 这是解决"退出重进后消息变未读"问题的关键方法
+     */
+    async updateMessageReadStatusByConversation() {
+      try {
+        console.log('🔄 开始根据会话 peerReadTime 更新已读状态...');
+        
+        // 获取当前会话对象
+        const conversationRes = await timManager.tim.getConversationProfile(this.conversationID);
+        
+        if (!conversationRes || !conversationRes.data || !conversationRes.data.conversation) {
+          console.warn('⚠️ 无法获取会话对象');
+          return;
+        }
+        
+        const conversation = conversationRes.data.conversation;
+        const peerReadTime = conversation.peerReadTime; // 对方最后阅读时间(秒级时间戳)
+        
+        console.log('   会话ID:', this.conversationID);
+        console.log('   对方最后阅读时间:', peerReadTime, new Date(peerReadTime * 1000).toLocaleString());
+        
+        if (!peerReadTime || peerReadTime === 0) {
+          console.log('   对方尚未阅读任何消息');
+          return;
+        }
+        
+        // 更新消息列表中的已读状态
+        let updatedCount = 0;
+        this.messages.forEach((msg, index) => {
+          // 只处理自己发送的消息
+          if (msg.fromUserId === this.userId) {
+            // 如果消息的发送时间 <= 对方最后阅读时间,说明对方已读
+            const msgTime = Math.floor(msg.sendTime.getTime() / 1000); // 转换为秒级时间戳
+            
+            if (msgTime <= peerReadTime) {
+              // 只更新未标记为已读的消息
+              if (!msg.isPeerRead) {
+                this.$set(this.messages[index], 'isPeerRead', true);
+                updatedCount++;
+                console.log(`   ✅ 消息 ${msg.messageId} 标记为已读 (发送时间: ${new Date(msgTime * 1000).toLocaleString()})`);
+              }
+            }
+          }
+        });
+        
+        console.log(`✅ 根据 peerReadTime 更新了 ${updatedCount} 条消息为已读状态`);
+        
+      } catch (error) {
+        console.error('❌ 更新消息已读状态失败:', error);
+      }
+    },
+    
     /**
      * 检查对方是否拉黑自己
      */
@@ -914,35 +974,55 @@ export default {
       console.log('✅ TIM 对象已就绪,准备监听 MESSAGE_READ_BY_PEER 事件');
       
       // 监听消息已读回执事件
-      const handleMessageReadByPeer = (event) => {
+      const handleMessageReadByPeer = async (event) => {
         console.log('=== 📖 收到已读回执事件 ===');
         console.log('   - 完整事件对象:', event);
         console.log('   - 事件数据:', JSON.stringify(event.data));
         
         // event.data 包含已读的消息列表
         if (event.data && Array.isArray(event.data)) {
-          event.data.forEach(item => {
+          for (const item of event.data) {
             console.log('   - 处理会话:', item.conversationID, '当前会话:', this.conversationID);
             
             // 只处理当前会话的消息
             if (item.conversationID === this.conversationID) {
               console.log('✅ 对方已阅读当前会话的消息');
               
-              let updatedCount = 0;
-              
-              // 更新本地消息列表中所有未读消息的状态
-              this.messages.forEach((msg, index) => {
-                // 只更新自己发送的且未被标记为已读的消息
-                if (msg.fromUserId === this.userId && !msg.isPeerRead && msg.sendStatus !== 4) {
-                  this.$set(this.messages[index], 'isPeerRead', true);
-                  updatedCount++;
-                  console.log(`   - 消息 ${msg.messageId} 已标记为已读`);
+              // 🔥 关键修复:使用 peerReadTime 精确判断哪些消息被读了
+              try {
+                const conversationRes = await timManager.tim.getConversationProfile(this.conversationID);
+                
+                if (conversationRes && conversationRes.data && conversationRes.data.conversation) {
+                  const peerReadTime = conversationRes.data.conversation.peerReadTime;
+                  console.log('   - 对方最后阅读时间:', peerReadTime, new Date(peerReadTime * 1000).toLocaleString());
+                  
+                  if (peerReadTime && peerReadTime > 0) {
+                    let updatedCount = 0;
+                    
+                    // 只更新发送时间 <= peerReadTime 的消息
+                    this.messages.forEach((msg, index) => {
+                      if (msg.fromUserId === this.userId && !msg.isPeerRead && msg.sendStatus !== 4) {
+                        const msgTime = Math.floor(msg.sendTime.getTime() / 1000);
+                        
+                        // 只有消息发送时间 <= 对方阅读时间,才标记为已读
+                        if (msgTime <= peerReadTime) {
+                          this.$set(this.messages[index], 'isPeerRead', true);
+                          updatedCount++;
+                          console.log(`   - 消息 ${msg.messageId} 已标记为已读 (发送时间: ${new Date(msgTime * 1000).toLocaleString()})`);
+                        } else {
+                          console.log(`   - 消息 ${msg.messageId} 保持未读 (发送时间: ${new Date(msgTime * 1000).toLocaleString()} > 阅读时间)`);
+                        }
+                      }
+                    });
+                    
+                    console.log(`✅ 共更新 ${updatedCount} 条消息为已读状态`);
+                  }
                 }
-              });
-              
-              console.log(`✅ 共更新 ${updatedCount} 条消息为已读状态`);
+              } catch (error) {
+                console.error('❌ 获取会话 peerReadTime 失败:', error);
+              }
             }
-          });
+          }
         }
       };
       
@@ -1238,6 +1318,17 @@ export default {
           return '[视频]';
         case 'TIMFileElem':
           return '[文件]';
+        case 'TIMCustomElem':
+          // 处理自定义消息(我们的语音消息)
+          try {
+            const customData = JSON.parse(timMessage.payload.data);
+            if (customData.type === 'voice') {
+              return '[语音]';
+            }
+          } catch (e) {
+            console.error('解析自定义消息内容失败:', e);
+          }
+          return '[未知消息]';
         default:
           return '[未知消息]';
       }
@@ -1552,141 +1643,122 @@ export default {
       this.voiceTouchStartY = e.touches[0].clientY;
       this.voiceCanceling = false;
       
-      // 检查消息发送限制
-      if (this.hasMessageLimit && this.remainingCount <= 0) {
+      // 检查是否被拉黑
+      if (this.isBlockedByTarget) {
         uni.showToast({
-          title: '今日消息发送次数已用完',
+          title: '对方已将你拉黑',
           icon: 'none'
         });
         return;
       }
       
-      // 初始化录音管理器
-      if (!this.recorderManager) {
-        console.log('📱 初始化录音管理器');
-        this.recorderManager = uni.getRecorderManager();
-        
-        // 录音开始
-        this.recorderManager.onStart(() => {
-          console.log('✅ 录音开始回调触发');
-          this.isRecording = true;
-          this.showVoiceRecording = true;
-          this.voiceStartTime = Date.now();
-          this.voiceRecordingTime = 0;
-          this.voiceVolume = 0.3;
-          console.log('   - isRecording:', this.isRecording);
-          console.log('   - showVoiceRecording:', this.showVoiceRecording);
-          
-          // 启动计时器
-          let volumeDirection = 1;
-          let currentVolume = 0.3;
-          this.voiceRecordingTimer = setInterval(() => {
-            this.voiceRecordingTime = Math.floor((Date.now() - this.voiceStartTime) / 1000);
-            
-            // 检查是否达到60秒,自动停止录音
-            if (this.voiceRecordingTime >= 60) {
-              console.log('⏰ 录音时长达到60秒,自动停止');
-              clearInterval(this.voiceRecordingTimer);
-              this.voiceRecordingTimer = null;
-              this.voiceCanceling = false; // 确保不是取消状态
-              if (this.recorderManager) {
-                this.recorderManager.stop();
-              }
-              return;
-            }
-            
-            // 模拟更真实的音量波动
-            if (Math.random() > 0.2) {
-              currentVolume += volumeDirection * (0.1 + Math.random() * 0.2);
-              if (currentVolume > 1.0) {
-                currentVolume = 1.0;
-                volumeDirection = -1;
-              } else if (currentVolume < 0.4) {
-                currentVolume = 0.4;
-                volumeDirection = 1;
-              }
-              this.voiceVolume = currentVolume;
-            } else {
-              this.voiceVolume = Math.max(0.2, this.voiceVolume * 0.8);
-            }
-          }, 100);
-        });
+      // 🔥 每次都重新获取录音管理器(确保状态干净)
+      console.log('📱 获取录音管理器');
+      this.recorderManager = uni.getRecorderManager();
+      
+      // 🔥 每次录音前都重新注册回调(确保回调有效)
+      console.log('📝 注册录音回调');
+      
+      // 录音开始回调(可能延迟或不触发,所以状态已在 start() 后立即设置)
+      this.recorderManager.onStart(() => {
+        console.log('✅ 录音开始回调触发(延迟触发)');
+      });
+      
+      // 录音结束回调
+      this.recorderManager.onStop((res) => {
+        console.log('🎤 录音结束:', res, ', voiceCanceling:', this.voiceCanceling);
+        this.isRecording = false;
+        this.showVoiceRecording = false;
         
-        // 录音结束
-        this.recorderManager.onStop((res) => {
-          console.log('🎤 录音结束:', res, ', voiceCanceling:', this.voiceCanceling);
-          this.isRecording = false;
-          this.showVoiceRecording = false;
-          
-          // 清除计时器
-          if (this.voiceRecordingTimer) {
-            clearInterval(this.voiceRecordingTimer);
-            this.voiceRecordingTimer = null;
-          }
-          
-          // 如果是取消状态,不发送
-          if (this.voiceCanceling) {
-            console.log('❌ 录音已取消,不发送');
-            this.voiceCanceling = false;
-            return;
-          }
-          
-          this.voiceTempPath = res.tempFilePath;
-          this.voiceDuration = Math.floor(res.duration / 1000); // 转换为秒
-          
-          console.log('   - 文件路径:', this.voiceTempPath);
-          console.log('   - 时长:', this.voiceDuration, '秒');
-          
-          // 时长验证
-          if (this.voiceDuration < 1) {
-            uni.showToast({
-              title: '录音时间太短',
-              icon: 'none'
-            });
-            return;
-          }
-          
-          if (this.voiceDuration > 60) {
-            uni.showToast({
-              title: '录音时间不能超过60秒',
-              icon: 'none'
-            });
-            return;
-          }
-          
-          // 发送语音消息
-          this.sendVoiceMessage();
-        });
+        // 清除计时器
+        if (this.voiceRecordingTimer) {
+          clearInterval(this.voiceRecordingTimer);
+          this.voiceRecordingTimer = null;
+        }
         
-        // 录音错误
-        this.recorderManager.onError((err) => {
-          console.error('❌ 录音错误:', err);
-          
-          // 立即清理录音状态和界面
-          this.isRecording = false;
-          this.showVoiceRecording = false;
-          this.voiceCanceling = true;
-          
-          // 清除计时器
-          if (this.voiceRecordingTimer) {
-            clearInterval(this.voiceRecordingTimer);
-            this.voiceRecordingTimer = null;
-          }
-          
-          // 如果是权限错误,不显示任何提示(用户会看到系统权限弹窗)
-          // 其他错误才显示提示
-          if (err.errCode !== 'authorize' && err.errMsg && !err.errMsg.includes('authorize')) {
-            uni.showToast({
-              title: '录音失败',
-              icon: 'none'
+        // 如果是取消状态,不发送
+        if (this.voiceCanceling) {
+          console.log('❌ 录音已取消,不发送');
+          this.voiceCanceling = false;
+          return;
+        }
+        
+        this.voiceTempPath = res.tempFilePath;
+        this.voiceDuration = Math.floor(res.duration / 1000); // 转换为秒
+        
+        console.log('   - 文件路径:', this.voiceTempPath);
+        console.log('   - 时长:', this.voiceDuration, '秒');
+        
+        // 时长验证
+        if (this.voiceDuration < 1) {
+          uni.showToast({
+            title: '录音时间太短',
+            icon: 'none'
+          });
+          return;
+        }
+        
+        if (this.voiceDuration > 60) {
+          uni.showToast({
+            title: '录音时间不能超过60秒',
+            icon: 'none'
+          });
+          return;
+        }
+        
+        // 发送语音消息
+        this.sendVoiceMessage();
+      });
+      
+      // 录音错误回调
+      this.recorderManager.onError((err) => {
+        console.error('❌ 录音错误:', err);
+        
+        // 立即清理录音状态和界面
+        this.isRecording = false;
+        this.showVoiceRecording = false;
+        this.voiceCanceling = true;
+        
+        // 清除计时器
+        if (this.voiceRecordingTimer) {
+          clearInterval(this.voiceRecordingTimer);
+          this.voiceRecordingTimer = null;
+        }
+        
+        // 如果是权限错误,不显示任何提示(用户会看到系统权限弹窗)
+        // 其他错误才显示提示
+        if (err.errCode !== 'authorize' && err.errMsg && !err.errMsg.includes('authorize')) {
+          uni.showToast({
+            title: '录音失败',
+            icon: 'none'
+          });
+        }
+        
+        console.log('✅ 已清理录音状态,隐藏录音界面');
+      });
+      
+      // 🔥 先检查录音权限
+      console.log('🔐 检查录音权限...');
+      uni.getSetting({
+        success: (res) => {
+          console.log('📋 权限设置:', res.authSetting);
+          if (res.authSetting['scope.record'] === false) {
+            console.warn('⚠️ 录音权限被拒绝');
+            uni.showModal({
+              title: '需要录音权限',
+              content: '请在设置中开启录音权限',
+              success: (modalRes) => {
+                if (modalRes.confirm) {
+                  uni.openSetting();
+                }
+              }
             });
+            return;
           }
-          
-          console.log('✅ 已清理录音状态,隐藏录音界面');
-        });
-      }
+        }
+      });
       
-      // 开始录音(等待 onStart 回调后才显示界面)
+      // 开始录音
       console.log('🎙️ 调用 recorderManager.start()');
       try {
         this.recorderManager.start({
@@ -1696,6 +1768,52 @@ export default {
           encodeBitRate: 48000
         });
         console.log('✅ recorderManager.start() 调用成功');
+        
+        // 🔥 立即显示录音界面(不等待 onStart 回调)
+        // 因为微信小程序的 onStart 可能延迟或不触发
+        this.isRecording = true;
+        this.showVoiceRecording = true;
+        this.voiceStartTime = Date.now();
+        this.voiceRecordingTime = 0;
+        this.voiceVolume = 0.3;
+        console.log('🎬 立即显示录音界面');
+        console.log('   - isRecording:', this.isRecording);
+        console.log('   - showVoiceRecording:', this.showVoiceRecording);
+        
+        // 启动计时器
+        let volumeDirection = 1;
+        let currentVolume = 0.3;
+        this.voiceRecordingTimer = setInterval(() => {
+          this.voiceRecordingTime = Math.floor((Date.now() - this.voiceStartTime) / 1000);
+          
+          // 检查是否达到60秒,自动停止录音
+          if (this.voiceRecordingTime >= 60) {
+            console.log('⏰ 录音时长达到60秒,自动停止');
+            clearInterval(this.voiceRecordingTimer);
+            this.voiceRecordingTimer = null;
+            this.voiceCanceling = false;
+            if (this.recorderManager) {
+              this.recorderManager.stop();
+            }
+            return;
+          }
+          
+          // 模拟音量波动
+          if (Math.random() > 0.2) {
+            currentVolume += volumeDirection * (0.1 + Math.random() * 0.2);
+            if (currentVolume > 1.0) {
+              currentVolume = 1.0;
+              volumeDirection = -1;
+            } else if (currentVolume < 0.4) {
+              currentVolume = 0.4;
+              volumeDirection = 1;
+            }
+            this.voiceVolume = currentVolume;
+          } else {
+            this.voiceVolume = Math.max(0.2, this.voiceVolume * 0.8);
+          }
+        }, 100);
+        
       } catch (err) {
         console.error('❌ recorderManager.start() 调用失败:', err);
         this.isRecording = false;

+ 45 - 0
LiangZhiYUMao/pages/page3/page3.vue

@@ -105,6 +105,51 @@ export default {
             uni.hideLoading()
             uni.showToast({title: '登录成功', icon: 'success'})
 
+            // ✅ 初始化 TIM 和 WebSocket
+            console.log('🚀 登录成功,开始初始化 TIM 和 WebSocket...')
+            
+            // 延迟一下,确保全局方法已挂载
+            await new Promise(resolve => setTimeout(resolve, 200))
+            
+            try {
+              const app = getApp()
+              if (app && app.globalData && app.globalData.initGlobalTIM) {
+                await app.globalData.initGlobalTIM()
+                console.log('✅ TIM 和 WebSocket 初始化完成')
+              } else {
+                console.warn('⚠️ 全局 initGlobalTIM 方法不存在,尝试直接导入')
+                // 如果全局方法不存在,直接导入 tim-manager
+                const timManager = require('@/utils/tim-manager.js').default
+                const timPresenceManager = require('@/utils/tim-presence-manager.js').default
+                
+                // 初始化 TIM
+                if (!timManager.isLogin) {
+                  const SDKAppID = 1600109674
+                  if (!timManager.tim) {
+                    timManager.init(SDKAppID)
+                    console.log('✅ TIM SDK 初始化完成')
+                  }
+                  
+                  const res = await uni.request({
+                    url: `http://localhost:8083/api/im/getUserSig?userId=${user.userId}`,
+                    method: 'GET'
+                  })
+                  
+                  if (res[1].data.code === 200) {
+                    const userSig = res[1].data.data.userSig
+                    await timManager.login(String(user.userId), userSig)
+                    console.log('✅ TIM 登录成功')
+                    
+                    // 初始化 WebSocket
+                    await timPresenceManager.init(String(user.userId))
+                    console.log('✅ WebSocket 初始化完成')
+                  }
+                }
+              }
+            } catch (error) {
+              console.error('❌ 初始化 TIM 失败:', error)
+            }
+
             // ✅ 跳转首页
             setTimeout(() => {
               const target = this.redirectUrl || '/pages/index/index'

+ 32 - 9
LiangZhiYUMao/utils/tim-presence-manager.js

@@ -20,7 +20,7 @@ class TIMPresenceManager {
     this.statusCallbacks = new Map(); // 存储状态变化回调
     this.onlineStatusCache = new Map(); // 缓存在线状态
     
-    // WebSocket服务器地址
+    // WebSocket服务器地址(通过网关)
     this.wsUrl = 'ws://localhost:8083/ws/chat';
     
     // TIM 状态监听器引用(用于清理)
@@ -47,20 +47,31 @@ class TIMPresenceManager {
    */
   connectWebSocket() {
     try {
+      const wsUrl = `${this.wsUrl}?userId=${this.userId}`;
+      console.log('🔌 准备连接 WebSocket:', wsUrl);
+      console.log('   当前用户ID:', this.userId);
+      console.log('   WebSocket URL:', wsUrl);
+      
       this.ws = uni.connectSocket({
-        url: `${this.wsUrl}?userId=${this.userId}`,
+        url: wsUrl,
         success: () => {
-          console.log('🔌 WebSocket连接请求已发送');
+          console.log('✅ WebSocket连接请求已发送成功');
         },
         fail: (err) => {
-          console.error('❌ WebSocket连接失败:', err);
+          console.error('❌ WebSocket连接请求发送失败:', err);
+          console.error('   错误详情:', JSON.stringify(err));
           this.scheduleReconnect();
         }
       });
       
       // 监听连接打开
-      uni.onSocketOpen(() => {
-        console.log('✅ WebSocket已连接');
+      uni.onSocketOpen((res) => {
+        console.log('🎉 ========== WebSocket 连接成功 ==========');
+        console.log('   响应数据:', res);
+        console.log('   用户ID:', this.userId);
+        console.log('   连接URL:', wsUrl);
+        console.log('==========================================');
+        
         this.isConnected = true;
         this.startHeartbeat();
         
@@ -75,14 +86,26 @@ class TIMPresenceManager {
       
       // 监听错误
       uni.onSocketError((err) => {
-        console.error('❌ WebSocket错误:', err);
+        console.error('❌ ========== WebSocket 错误 ==========');
+        console.error('   错误信息:', err);
+        console.error('   错误详情:', JSON.stringify(err));
+        console.error('   用户ID:', this.userId);
+        console.error('   连接URL:', wsUrl);
+        console.error('======================================');
+        
         this.isConnected = false;
         this.scheduleReconnect();
       });
       
       // 监听关闭
-      uni.onSocketClose(() => {
-        console.log('🔌 WebSocket已关闭');
+      uni.onSocketClose((res) => {
+        console.log('🔌 ========== WebSocket 连接关闭 ==========');
+        console.log('   关闭信息:', res);
+        console.log('   关闭码:', res?.code);
+        console.log('   关闭原因:', res?.reason);
+        console.log('   用户ID:', this.userId);
+        console.log('=========================================');
+        
         this.isConnected = false;
         this.stopHeartbeat();
         this.scheduleReconnect();

+ 37 - 8
service/websocket/src/main/java/com/zhentao/service/ChatMessageService.java

@@ -13,6 +13,7 @@ import com.zhentao.handler.ChatWebSocketHandler;
 import com.zhentao.repository.ChatConversationMapper;
 import com.zhentao.repository.ChatMessageMapper;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.DuplicateKeyException;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.web.socket.TextMessage;
@@ -454,14 +455,26 @@ public class ChatMessageService {
     /**
      * 保存聊天消息(用于TIM消息同步)
      * 三重存储:MySQL + MongoDB
+     * @return true=成功插入新消息,false=消息已存在(幂等性)
      */
-    public void saveChatMessage(ChatMessage chatMessage) {
-        // 1. 保存到MySQL
-        chatMessageMapper.insert(chatMessage);
-        System.out.println("✅ 消息已保存到MySQL: " + chatMessage.getMessageId());
-        
-        // 2. 同时保存到MongoDB(异步,失败不影响主流程)
-        saveToMongoDB(chatMessage);
+    public boolean saveChatMessage(ChatMessage chatMessage) {
+        try {
+            // 1. 保存到MySQL
+            chatMessageMapper.insert(chatMessage);
+            System.out.println("✅ 消息已保存到MySQL: " + chatMessage.getMessageId());
+            
+            // 2. 同时保存到MongoDB(异步,失败不影响主流程)
+            saveToMongoDB(chatMessage);
+            
+            return true; // 成功插入新消息
+        } catch (DuplicateKeyException e) {
+            // 消息已存在,忽略重复插入(幂等性保证)
+            System.out.println("⚠️ 消息已存在,跳过保存: " + chatMessage.getMessageId());
+            return false; // 消息已存在
+        } catch (Exception e) {
+            System.err.println("❌ 保存消息失败: " + chatMessage.getMessageId());
+            throw e;
+        }
     }
     
     /**
@@ -564,7 +577,23 @@ public class ChatMessageService {
         
         // 保存或更新
         if (conversation.getId() == null) {
-            conversationMapper.insert(conversation);
+            try {
+                conversationMapper.insert(conversation);
+            } catch (DuplicateKeyException e) {
+                // 会话已存在(并发情况),重新查询并更新
+                System.out.println("⚠️ 会话已存在,重新查询并更新: " + userId + " -> " + targetUserId);
+                ChatConversation existingConversation = conversationMapper.selectByUserIdAndTargetUserId(userId, targetUserId);
+                if (existingConversation != null) {
+                    existingConversation.setLastMessage(conversation.getLastMessage());
+                    existingConversation.setLastMessageType(conversation.getLastMessageType());
+                    existingConversation.setLastMessageTime(conversation.getLastMessageTime());
+                    existingConversation.setUpdateTime(conversation.getUpdateTime());
+                    if (incrementUnread) {
+                        existingConversation.setUnreadCount(existingConversation.getUnreadCount() + 1);
+                    }
+                    conversationMapper.updateById(existingConversation);
+                }
+            }
         } else {
             conversationMapper.updateById(conversation);
         }