mazhenhang 1 mesiac pred
rodič
commit
a369243f81

+ 23 - 1
LiangZhiYUMao/package-lock.json

@@ -11,7 +11,8 @@
                 "axios": "^1.12.2",
                 "element": "^0.1.4",
                 "plus": "^0.1.0",
-                "tim-wx-sdk": "^2.27.6"
+                "tim-wx-sdk": "^2.27.6",
+                "ws": "^8.18.3"
             }
         },
         "node_modules/asynckit": {
@@ -398,6 +399,27 @@
             "resolved": "https://registry.npmjs.org/tim-wx-sdk/-/tim-wx-sdk-2.27.6.tgz",
             "integrity": "sha512-zB+eRdmigdhEDeqrXC0bLJonUQZzS5uKNPLFtrje503WAnmuxVQjq/n4Zle4FYHG4FiKHKhsrVd0aCYXABlFEg==",
             "license": "ISC"
+        },
+        "node_modules/ws": {
+            "version": "8.18.3",
+            "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+            "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+            "license": "MIT",
+            "engines": {
+                "node": ">=10.0.0"
+            },
+            "peerDependencies": {
+                "bufferutil": "^4.0.1",
+                "utf-8-validate": ">=5.0.2"
+            },
+            "peerDependenciesMeta": {
+                "bufferutil": {
+                    "optional": true
+                },
+                "utf-8-validate": {
+                    "optional": true
+                }
+            }
         }
     }
 }

+ 2 - 1
LiangZhiYUMao/package.json

@@ -17,6 +17,7 @@
         "axios": "^1.12.2",
         "element": "^0.1.4",
         "plus": "^0.1.0",
-        "tim-wx-sdk": "^2.27.6"
+        "tim-wx-sdk": "^2.27.6",
+        "ws": "^8.18.3"
     }
 }

+ 220 - 10
LiangZhiYUMao/pages/message/chat.vue

@@ -124,7 +124,10 @@
                 @click.stop="toggleVoicePlay(msg)">
                 <text class="voice-duration">{{ msg.duration }}''</text>
                 <view class="voice-icon-wrapper" :class="{playing: playingVoiceId === msg.messageId}">
-                  <image class="voice-icon" src="/static/voice-icon.png" mode="aspectFit"></image>
+                  <image 
+                    class="voice-icon" 
+                    :src="msg.fromUserId === userId ? 'http://115.190.125.125:9000/static-images/%E6%88%91%E6%96%B9%E8%AF%AD%E9%9F%B3%E6%B6%88%E6%81%AF' : 'http://115.190.125.125:9000/static-images/%E5%AF%B9%E6%96%B9%E8%AF%AD%E9%9F%B3%E6%B6%88%E6%81%AF'" 
+                    mode="aspectFit"></image>
                 </view>
                 <!-- 暂停后的继续播放按钮 -->
                 <view 
@@ -264,6 +267,7 @@
 <script>
 import timManager from '@/utils/tim-manager.js';
 import TIM from 'tim-wx-sdk';
+import presenceManager from '@/utils/presence-manager.js';
 
 export default {
   data() {
@@ -318,7 +322,9 @@ export default {
 	    voiceVolume: 0.3, // 录音音量(0-1),控制波形高度
 	    playingVoiceId: null, // 当前播放的语音消息ID
 	    pausedVoiceId: null, // 当前暂停的语音消息ID
-	    currentAudioContext: null // 当前音频上下文
+	    currentAudioContext: null, // 当前音频上下文
+	    onlineStatusPollingTimer: null, // 在线状态轮询定时器
+	    voiceRecordCancelled: false // 录音是否已被取消(防止权限允许后自动恢复)
 	};
   },
   
@@ -414,6 +420,12 @@ export default {
     
     // 监听新消息
     this.listenMessages();
+    
+    // 初始化在线状态监听
+    this.initPresence();
+    
+    // 发送已读回执(标记对方发来的消息为已读)
+    this.sendReadReceipt();
   },
   
   onUnload() {
@@ -426,6 +438,9 @@ export default {
       timManager.tim.off(TIM.EVENT.MESSAGE_STATUS_CHANGED, this.handleStatusChange);
     }
     */
+    
+    // 清理在线状态监听
+    this.cleanupPresence();
   },
   
   methods: {
@@ -747,6 +762,64 @@ export default {
       */
     },
     
+    /**
+     * 发送已读回执
+     */
+    sendReadReceipt() {
+      try {
+        console.log('📨 发送已读回执给用户:', this.targetUserId);
+        
+        // 通过WebSocket发送已读回执
+        const message = {
+          type: 'read',  // 已读回执类型
+          fromUserId: parseInt(this.userId),
+          toUserId: parseInt(this.targetUserId),
+          timestamp: Date.now()
+        };
+        
+        // 发送WebSocket消息
+        if (timManager.websocket && timManager.websocket.readyState === 1) {
+          timManager.websocket.send(JSON.stringify(message));
+          console.log('✅ 已读回执发送成功');
+        } else {
+          console.warn('⚠️ WebSocket未连接,无法发送已读回执');
+        }
+      } catch (error) {
+        console.error('❌ 发送已读回执失败:', error);
+      }
+    },
+    
+    /**
+     * 监听已读回执
+     */
+    listenReadReceipt() {
+      // 在WebSocket消息处理中添加已读回执的监听
+      // 这个方法会在initPresence中被调用
+      console.log('👂 开始监听已读回执');
+    },
+    
+    /**
+     * 处理已读回执
+     */
+    handleReadReceipt(data) {
+      try {
+        console.log('📬 收到已读回执:', data);
+        
+        // 更新本地消息状态为已读
+        this.messages.forEach((msg, index) => {
+          // 只更新自己发送给对方的消息
+          if (msg.fromUserId === this.userId && 
+              msg.toUserId === this.targetUserId && 
+              msg.sendStatus === 2) {  // 只更新已送达的消息
+            this.$set(this.messages[index], 'sendStatus', 3);  // 更新为已读
+            console.log(`✅ 消息 ${msg.messageId} 状态更新为已读`);
+          }
+        });
+      } catch (error) {
+        console.error('❌ 处理已读回执失败:', error);
+      }
+    },
+    
     /**
      * 转换消息格式
      */
@@ -1430,6 +1503,121 @@ export default {
       this.inputType = this.inputType === 'text' ? 'voice' : 'text';
     },
     
+    /**
+     * 初始化在线状态监听
+     */
+    async initPresence() {
+      console.log('🔌 初始化在线状态监听');
+      console.log('   - 当前用户ID:', this.userId);
+      console.log('   - 对方用户ID:', this.targetUserId);
+      
+      // 1. 首先通过HTTP API查询对方的在线状态(立即显示)
+      try {
+        const isOnline = await presenceManager.queryOnlineStatus(this.targetUserId);
+        this.isTargetOnline = isOnline;
+        console.log('   - HTTP查询初始在线状态:', this.isTargetOnline);
+      } catch (error) {
+        console.error('❌ HTTP查询在线状态失败:', error);
+        this.isTargetOnline = false;
+      }
+      
+      // 2. 连接WebSocket(如果未连接)
+      if (!presenceManager.getConnectionStatus()) {
+        presenceManager.connect(this.userId);
+        
+        // 等待WebSocket连接建立
+        await this.waitForWebSocketConnection();
+      }
+      
+      // 3. 监听对方用户的在线状态变化
+      this.handleStatusChange = (status) => {
+        console.log(`👤 对方用户 ${this.targetUserId} 状态变更: ${status}`);
+        this.isTargetOnline = (status === 'online');
+        console.log('   - 更新后的在线状态:', this.isTargetOnline);
+      };
+      
+      presenceManager.onStatusChange(this.targetUserId, this.handleStatusChange);
+      console.log('✅ 已订阅对方用户状态');
+      
+      // 4. 监听已读回执(通过WebSocket)
+      if (presenceManager.websocket) {
+        const originalOnMessage = presenceManager.websocket.onmessage;
+        presenceManager.websocket.onmessage = (event) => {
+          // 先调用原有的消息处理
+          if (originalOnMessage) {
+            originalOnMessage.call(presenceManager.websocket, event);
+          }
+          
+          // 处理已读回执
+          try {
+            const data = JSON.parse(event.data);
+            if (data.type === 'read' && data.fromUserId == this.targetUserId) {
+              this.handleReadReceipt(data);
+            }
+          } catch (error) {
+            console.error('❌ 解析WebSocket消息失败:', error);
+          }
+        };
+        console.log('✅ 已监听已读回执');
+      }
+      
+      // 4. 定期轮询在线状态(作为补充,每30秒查询一次)
+      this.onlineStatusPollingTimer = setInterval(async () => {
+        try {
+          const isOnline = await presenceManager.queryOnlineStatus(this.targetUserId);
+          if (this.isTargetOnline !== isOnline) {
+            console.log(`🔄 轮询检测到状态变化: ${this.isTargetOnline} -> ${isOnline}`);
+            this.isTargetOnline = isOnline;
+          }
+        } catch (error) {
+          console.error('❌ 轮询查询在线状态失败:', error);
+        }
+      }, 30000); // 30秒轮询一次
+    },
+    
+    /**
+     * 等待WebSocket连接建立
+     */
+    async waitForWebSocketConnection() {
+      return new Promise((resolve) => {
+        const maxWaitTime = 5000; // 最多等待5秒
+        const startTime = Date.now();
+        
+        const checkConnection = setInterval(() => {
+          if (presenceManager.getConnectionStatus()) {
+            console.log('✅ WebSocket已连接');
+            clearInterval(checkConnection);
+            resolve();
+          } else if (Date.now() - startTime > maxWaitTime) {
+            console.log('⚠️ WebSocket连接超时,继续使用HTTP轮询');
+            clearInterval(checkConnection);
+            resolve();
+          }
+        }, 100);
+      });
+    },
+    
+    /**
+     * 清理在线状态监听
+     */
+    cleanupPresence() {
+      console.log('🔌 清理在线状态监听');
+      
+      // 清除轮询定时器
+      if (this.onlineStatusPollingTimer) {
+        clearInterval(this.onlineStatusPollingTimer);
+        this.onlineStatusPollingTimer = null;
+      }
+      
+      if (this.handleStatusChange) {
+        presenceManager.offStatusChange(this.targetUserId, this.handleStatusChange);
+        this.handleStatusChange = null;
+      }
+      
+      // 注意:不要断开WebSocket连接,因为其他页面可能还在使用
+      // presenceManager.disconnect();
+    },
+    
     /**
      * 开始录音
      */
@@ -1456,7 +1644,18 @@ export default {
         
         // 录音开始
         this.recorderManager.onStart(() => {
-          console.log('✅ 录音开始回调触发');
+          console.log('✅ 录音开始回调触发, voiceRecordCancelled:', this.voiceRecordCancelled);
+          
+          // 关键修复:如果录音已被取消,不再恢复录音状态
+          if (this.voiceRecordCancelled) {
+            console.log('⚠️ 录音已被取消,忽略onStart回调');
+            // 立即停止录音
+            if (this.recorderManager) {
+              this.recorderManager.stop();
+            }
+            return;
+          }
+          
           this.isRecording = true;
           this.showVoiceRecording = true;
           this.voiceStartTime = Date.now();
@@ -1519,6 +1718,7 @@ export default {
       // 立即设置录音状态(不等待onStart回调)
       this.isRecording = true;
       this.showVoiceRecording = true;
+      this.voiceRecordCancelled = false; // 重置取消标志
       this.voiceStartTime = Date.now();
       this.voiceRecordingTime = 0;
       this.voiceVolume = 0.3;
@@ -1630,6 +1830,9 @@ export default {
     cancelVoiceRecord() {
       console.log('❌ 取消录音, voiceCanceling:', this.voiceCanceling);
       
+      // 关键修复:标记录音已被取消,防止onStart回调恢复录音
+      this.voiceRecordCancelled = true;
+      
       // 清除计时器
       if (this.voiceRecordingTimer) {
         clearInterval(this.voiceRecordingTimer);
@@ -1642,15 +1845,22 @@ export default {
         this.voiceCanceling = true;
       }
       
-      if (this.recorderManager && this.isRecording) {
-        this.recorderManager.stop();  // 这会触发onStop回调
-      } else {
-        // 如果没有在录音,直接重置状态
-        this.isRecording = false;
-        this.showVoiceRecording = false;
-        this.voiceCanceling = false;
+      // 立即隐藏录音UI
+      this.isRecording = false;
+      this.showVoiceRecording = false;
+      
+      if (this.recorderManager) {
+        // 尝试停止录音(如果已经开始)
+        try {
+          this.recorderManager.stop();
+        } catch (err) {
+          console.log('⚠️ 停止录音失败(可能还未开始):', err);
+        }
       }
       
+      // 重置取消状态
+      this.voiceCanceling = false;
+      
       uni.showToast({
         title: '已取消录音',
         icon: 'none'

+ 352 - 0
LiangZhiYUMao/utils/presence-manager.js

@@ -0,0 +1,352 @@
+/**
+ * 用户在线状态管理器
+ * 基于WebSocket心跳检测实现
+ */
+
+class PresenceManager {
+  constructor() {
+    this.ws = null;
+    this.heartbeatTimer = null;
+    this.reconnectTimer = null;
+    this.heartbeatInterval = 30000; // 30秒心跳
+    this.reconnectInterval = 5000; // 5秒重连
+    this.isConnected = false;
+    this.userId = null;
+    this.statusCallbacks = new Map(); // 存储状态变化回调
+    this.onlineStatusCache = new Map(); // 缓存在线状态
+    
+    // WebSocket服务器地址(使用实际的聊天WebSocket服务)
+    this.wsUrl = 'ws://localhost:1004/ws/chat';
+    this.httpUrl = 'http://localhost:1004/api/online'; // HTTP API地址
+  }
+  
+  /**
+   * 连接WebSocket
+   * @param {String} userId 当前用户ID
+   */
+  connect(userId) {
+    if (this.isConnected || !userId) {
+      return;
+    }
+    
+    this.userId = userId;
+    
+    try {
+      // uni-app的WebSocket API
+      this.ws = uni.connectSocket({
+        url: `${this.wsUrl}?userId=${userId}`,
+        success: () => {
+          console.log('🔌 WebSocket连接请求已发送');
+        },
+        fail: (err) => {
+          console.error('❌ WebSocket连接失败:', err);
+          this.scheduleReconnect();
+        }
+      });
+      
+      // 监听连接打开
+      uni.onSocketOpen(() => {
+        console.log('✅ WebSocket已连接');
+        this.isConnected = true;
+        this.startHeartbeat();
+      });
+      
+      // 监听消息
+      uni.onSocketMessage((res) => {
+        this.handleMessage(res.data);
+      });
+      
+      // 监听错误
+      uni.onSocketError((err) => {
+        console.error('❌ WebSocket错误:', err);
+        this.isConnected = false;
+        this.scheduleReconnect();
+      });
+      
+      // 监听关闭
+      uni.onSocketClose(() => {
+        console.log('🔌 WebSocket已关闭');
+        this.isConnected = false;
+        this.stopHeartbeat();
+        this.scheduleReconnect();
+      });
+      
+    } catch (error) {
+      console.error('❌ WebSocket连接异常:', error);
+      this.scheduleReconnect();
+    }
+  }
+  
+  /**
+   * 处理接收到的消息
+   */
+  handleMessage(data) {
+    try {
+      const message = typeof data === 'string' ? JSON.parse(data) : data;
+      
+      switch (message.type) {
+        case 'PONG':
+          // 心跳响应
+          console.log('💓 收到心跳响应');
+          break;
+          
+        case 'ONLINE':
+          // 用户上线通知
+          if (message.fromUserId) {
+            this.onlineStatusCache.set(String(message.fromUserId), true);
+            this.notifyStatusChange(String(message.fromUserId), 'online');
+          }
+          break;
+          
+        case 'OFFLINE':
+          // 用户离线通知
+          if (message.fromUserId) {
+            this.onlineStatusCache.set(String(message.fromUserId), false);
+            this.notifyStatusChange(String(message.fromUserId), 'offline');
+          }
+          break;
+          
+        case 'STATUS_UPDATE':
+          // 用户状态更新
+          if (message.userId) {
+            const status = message.online ? 'online' : 'offline';
+            this.onlineStatusCache.set(String(message.userId), message.online);
+            this.notifyStatusChange(String(message.userId), status);
+          }
+          break;
+          
+        default:
+          // 忽略其他消息类型(如聊天消息等)
+          break;
+      }
+    } catch (error) {
+      console.error('❌ 消息解析失败:', error);
+    }
+  }
+  
+  /**
+   * 通过HTTP API查询用户在线状态
+   * @param {String} userId 目标用户ID
+   * @return {Promise<Boolean>} 是否在线
+   */
+  async queryOnlineStatus(userId) {
+    try {
+      const [err, res] = await uni.request({
+        url: `${this.httpUrl}/checkStatus`,
+        method: 'GET',
+        data: {
+          userId: userId
+        }
+      });
+      
+      if (err) {
+        console.error('❌ 查询在线状态失败:', err);
+        return false;
+      }
+      
+      if (res.data && res.data.code === 200) {
+        const isOnline = res.data.data.online || false;
+        this.onlineStatusCache.set(String(userId), isOnline);
+        return isOnline;
+      }
+      
+      return false;
+    } catch (error) {
+      console.error('❌ 查询在线状态异常:', error);
+      return false;
+    }
+  }
+  
+  /**
+   * 获取缓存的在线状态
+   * @param {String} userId 目标用户ID
+   * @return {Boolean|null} 在线状态,null表示未知
+   */
+  getCachedStatus(userId) {
+    return this.onlineStatusCache.get(String(userId)) || null;
+  }
+  
+  /**
+   * 启动心跳
+   */
+  startHeartbeat() {
+    this.stopHeartbeat();
+    
+    this.heartbeatTimer = setInterval(() => {
+      if (this.isConnected) {
+        this.sendMessage({
+          type: 'PING',
+          fromUserId: this.userId,
+          timestamp: Date.now()
+        });
+      }
+    }, this.heartbeatInterval);
+  }
+  
+  /**
+   * 停止心跳
+   */
+  stopHeartbeat() {
+    if (this.heartbeatTimer) {
+      clearInterval(this.heartbeatTimer);
+      this.heartbeatTimer = null;
+    }
+  }
+  
+  /**
+   * 发送消息
+   */
+  sendMessage(data) {
+    if (!this.isConnected) {
+      console.warn('⚠️ WebSocket未连接,无法发送消息');
+      return;
+    }
+    
+    try {
+      uni.sendSocketMessage({
+        data: JSON.stringify(data),
+        fail: (err) => {
+          console.error('❌ 发送消息失败:', err);
+        }
+      });
+    } catch (error) {
+      console.error('❌ 发送消息异常:', error);
+    }
+  }
+  
+  /**
+   * 订阅用户状态
+   * @param {String} targetUserId 目标用户ID
+   */
+  subscribeUser(targetUserId) {
+    if (!this.isConnected || !targetUserId) {
+      return;
+    }
+    
+    this.sendMessage({
+      type: 'SUBSCRIBE',
+      fromUserId: this.userId,
+      toUserId: targetUserId
+    });
+  }
+  
+  /**
+   * 取消订阅用户状态
+   * @param {String} targetUserId 目标用户ID
+   */
+  unsubscribeUser(targetUserId) {
+    if (!this.isConnected || !targetUserId) {
+      return;
+    }
+    
+    this.sendMessage({
+      type: 'UNSUBSCRIBE',
+      fromUserId: this.userId,
+      toUserId: targetUserId
+    });
+  }
+  
+  /**
+   * 监听用户状态变化
+   * @param {String} targetUserId 目标用户ID
+   * @param {Function} callback 回调函数 (status) => {}
+   */
+  onStatusChange(targetUserId, callback) {
+    if (!this.statusCallbacks.has(targetUserId)) {
+      this.statusCallbacks.set(targetUserId, []);
+    }
+    this.statusCallbacks.get(targetUserId).push(callback);
+    
+    // 订阅该用户
+    this.subscribeUser(targetUserId);
+  }
+  
+  /**
+   * 移除状态监听
+   * @param {String} targetUserId 目标用户ID
+   * @param {Function} callback 回调函数
+   */
+  offStatusChange(targetUserId, callback) {
+    if (!this.statusCallbacks.has(targetUserId)) {
+      return;
+    }
+    
+    const callbacks = this.statusCallbacks.get(targetUserId);
+    const index = callbacks.indexOf(callback);
+    if (index > -1) {
+      callbacks.splice(index, 1);
+    }
+    
+    // 如果没有回调了,取消订阅
+    if (callbacks.length === 0) {
+      this.statusCallbacks.delete(targetUserId);
+      this.unsubscribeUser(targetUserId);
+    }
+  }
+  
+  /**
+   * 通知状态变化
+   */
+  notifyStatusChange(userId, status) {
+    console.log(`👤 用户 ${userId} 状态变更: ${status}`);
+    
+    const callbacks = this.statusCallbacks.get(userId);
+    if (callbacks && callbacks.length > 0) {
+      callbacks.forEach(callback => {
+        try {
+          callback(status);
+        } catch (error) {
+          console.error('❌ 状态回调执行失败:', error);
+        }
+      });
+    }
+  }
+  
+  /**
+   * 计划重连
+   */
+  scheduleReconnect() {
+    if (this.reconnectTimer) {
+      return;
+    }
+    
+    console.log('🔄 将在5秒后重连...');
+    this.reconnectTimer = setTimeout(() => {
+      this.reconnectTimer = null;
+      if (!this.isConnected && this.userId) {
+        console.log('🔄 尝试重连...');
+        this.connect(this.userId);
+      }
+    }, this.reconnectInterval);
+  }
+  
+  /**
+   * 断开连接
+   */
+  disconnect() {
+    this.stopHeartbeat();
+    
+    if (this.reconnectTimer) {
+      clearTimeout(this.reconnectTimer);
+      this.reconnectTimer = null;
+    }
+    
+    if (this.isConnected) {
+      uni.closeSocket();
+      this.isConnected = false;
+    }
+    
+    this.statusCallbacks.clear();
+    this.userId = null;
+  }
+  
+  /**
+   * 获取连接状态
+   */
+  getConnectionStatus() {
+    return this.isConnected;
+  }
+}
+
+// 导出单例
+export default new PresenceManager();

+ 82 - 6
service/Essential/src/main/java/com/zhentao/controller/UserController.java

@@ -180,17 +180,38 @@ public class UserController {
             String[] idArray = userIds.split(",");
             java.util.List<java.util.Map<String, Object>> userList = new java.util.ArrayList<>();
             
+            java.util.List<Long> deletedUserIds = new java.util.ArrayList<>();  // 记录需要从IM删除的用户ID
+            
             for (String idStr : idArray) {
                 try {
                     Long userId = Long.parseLong(idStr.trim());
-                    UserInfoVO vo = userService.getUserInfo(userId);
+                    System.out.println("🔍 开始查询用户: " + userId);
                     
-                    if (vo != null) {
+                    try {
+                        UserInfoVO vo = userService.getUserInfo(userId);
+                        
+                        if (vo != null) {
+                            System.out.println("   ✅ 用户存在: " + vo.getNickname() + " (ID: " + userId + ")");
+                            java.util.Map<String, Object> userMap = new java.util.HashMap<>();
+                            userMap.put("userId", vo.getUserId());
+                            userMap.put("nickname", vo.getNickname());
+                            userMap.put("avatarUrl", vo.getAvatar());  // 注意:VO中字段名是avatar
+                            userMap.put("gender", vo.getGender());
+                            userList.add(userMap);
+                        }
+                    } catch (RuntimeException e) {
+                        // 用户不存在时,记录需要删除的用户ID
+                        System.err.println("   ❌ 用户不存在: " + userId);
+                        System.err.println("   ⚠️ 将从IM中删除: " + userId + " - " + e.getMessage());
+                        deletedUserIds.add(userId);
+                        
+                        // 返回默认信息(已注销用户)
                         java.util.Map<String, Object> userMap = new java.util.HashMap<>();
-                        userMap.put("userId", vo.getUserId());
-                        userMap.put("nickname", vo.getNickname());
-                        userMap.put("avatarUrl", vo.getAvatar());  // 注意:VO中字段名是avatar
-                        userMap.put("gender", vo.getGender());
+                        userMap.put("userId", userId);
+                        userMap.put("nickname", "已注销用户");
+                        userMap.put("avatarUrl", "");  // 空头像
+                        userMap.put("gender", 0);
+                        userMap.put("isDeleted", true);  // 标记为已删除
                         userList.add(userMap);
                     }
                 } catch (NumberFormatException e) {
@@ -198,6 +219,15 @@ public class UserController {
                 }
             }
             
+            // 批量删除IM中不存在的用户
+            System.out.println("📊 查询完成,需要删除的用户数量: " + deletedUserIds.size());
+            if (!deletedUserIds.isEmpty()) {
+                System.out.println("🗑️ 准备删除的用户ID列表: " + deletedUserIds);
+                deleteIMAccountsAsync(deletedUserIds);
+            } else {
+                System.out.println("✅ 所有用户都存在,无需删除IM账号");
+            }
+            
             System.out.println("✅ 批量查询成功,返回 " + userList.size() + " 个用户");
             return Result.success(userList);
         } catch (Exception e) {
@@ -207,6 +237,52 @@ public class UserController {
         }
     }
     
+    /**
+     * 异步批量删除IM账号
+     * @param userIds 需要删除的用户ID列表
+     */
+    private void deleteIMAccountsAsync(java.util.List<Long> userIds) {
+        // 使用新线程异步执行,不阻塞主流程
+        new Thread(() -> {
+            try {
+                System.out.println("=== 异步删除IM账号线程启动 ===");
+                System.out.println("🗑️ 开始异步删除IM账号,数量: " + userIds.size());
+                System.out.println("   用户ID列表: " + userIds);
+                
+                // 将用户ID列表转换为逗号分隔的字符串
+                String userIdsStr = userIds.stream()
+                    .map(String::valueOf)
+                    .reduce((a, b) -> a + "," + b)
+                    .orElse("");
+                
+                if (userIdsStr.isEmpty()) {
+                    System.err.println("⚠️ 用户ID列表为空,跳过删除");
+                    return;
+                }
+                
+                System.out.println("   转换后的ID字符串: " + userIdsStr);
+                
+                // 调用WebSocket服务的批量删除IM账号接口
+                String url = "http://localhost:1004/api/im/deleteAccounts?userIds=" + userIdsStr;
+                System.out.println("   调用URL: " + url);
+                
+                org.springframework.web.client.RestTemplate restTemplate = new org.springframework.web.client.RestTemplate();
+                
+                System.out.println("   正在发送DELETE请求...");
+                restTemplate.delete(url);
+                
+                System.out.println("✅ IM账号批量删除成功: " + userIdsStr);
+                System.out.println("=== 异步删除IM账号线程结束 ===");
+                
+            } catch (Exception e) {
+                System.err.println("❌ IM账号批量删除失败: " + e.getMessage());
+                System.err.println("   异常类型: " + e.getClass().getName());
+                e.printStackTrace();
+                // 异步删除失败不影响主流程,只记录日志
+            }
+        }).start();
+    }
+    
     /**
      * 健康检查接口
      * @return 服务状态

+ 112 - 0
service/websocket/src/main/java/com/zhentao/controller/OnlineStatusController.java

@@ -0,0 +1,112 @@
+package com.zhentao.controller;
+
+import com.zhentao.service.OnlineUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 在线状态查询控制器
+ */
+@RestController
+@RequestMapping("/api/online")
+@CrossOrigin(origins = "*")
+public class OnlineStatusController {
+
+    @Autowired
+    private OnlineUserService onlineUserService;
+
+    /**
+     * 检查用户是否在线
+     * @param userId 用户ID
+     * @return 在线状态
+     */
+    @GetMapping("/checkStatus")
+    public Map<String, Object> checkUserOnlineStatus(@RequestParam("userId") Long userId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            boolean isOnline = onlineUserService.isUserOnline(userId);
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("userId", userId);
+            data.put("online", isOnline);
+            data.put("timestamp", System.currentTimeMillis());
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", data);
+            
+            System.out.println("查询用户 " + userId + " 在线状态: " + (isOnline ? "在线" : "离线"));
+        } catch (Exception e) {
+            System.err.println("查询在线状态失败: " + e.getMessage());
+            result.put("code", 500);
+            result.put("message", "查询失败: " + e.getMessage());
+            result.put("data", null);
+        }
+        
+        return result;
+    }
+
+    /**
+     * 批量检查用户在线状态
+     * @param userIds 用户ID列表(逗号分隔)
+     * @return 在线状态列表
+     */
+    @GetMapping("/batchCheckStatus")
+    public Map<String, Object> batchCheckUserOnlineStatus(@RequestParam("userIds") String userIds) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            String[] userIdArray = userIds.split(",");
+            Map<Long, Boolean> statusMap = new HashMap<>();
+            
+            for (String userIdStr : userIdArray) {
+                try {
+                    Long userId = Long.parseLong(userIdStr.trim());
+                    boolean isOnline = onlineUserService.isUserOnline(userId);
+                    statusMap.put(userId, isOnline);
+                } catch (NumberFormatException e) {
+                    System.err.println("无效的用户ID: " + userIdStr);
+                }
+            }
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", statusMap);
+        } catch (Exception e) {
+            System.err.println("批量查询在线状态失败: " + e.getMessage());
+            result.put("code", 500);
+            result.put("message", "查询失败: " + e.getMessage());
+            result.put("data", null);
+        }
+        
+        return result;
+    }
+
+    /**
+     * 获取在线用户总数
+     * @return 在线用户数量
+     */
+    @GetMapping("/count")
+    public Map<String, Object> getOnlineUserCount() {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            long count = onlineUserService.getOnlineUserCount();
+            
+            result.put("code", 200);
+            result.put("message", "查询成功");
+            result.put("data", count);
+        } catch (Exception e) {
+            System.err.println("获取在线用户数量失败: " + e.getMessage());
+            result.put("code", 500);
+            result.put("message", "查询失败: " + e.getMessage());
+            result.put("data", 0);
+        }
+        
+        return result;
+    }
+}

+ 126 - 0
service/websocket/src/main/java/com/zhentao/controller/TIMController.java

@@ -234,4 +234,130 @@ public class TIMController {
         
         return result;
     }
+    
+    /**
+     * 删除单个IM账号
+     * DELETE /api/im/deleteAccount?userId=123
+     * @param userId 用户ID
+     */
+    @DeleteMapping("/deleteAccount")
+    public Map<String, Object> deleteAccount(@RequestParam Long userId) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            if (userId == null) {
+                result.put("code", 400);
+                result.put("message", "userId 不能为空");
+                return result;
+            }
+            
+            // 生成管理员 UserSig(用于调用REST API)
+            String adminUserSig = timUtils.generateUserSig("administrator", 24 * 3600);
+            
+            // 构建请求URL
+            String url = String.format(
+                "https://console.tim.qq.com/v4/im_open_login_svc/account_delete?sdkappid=%d&identifier=administrator&usersig=%s&random=%d&contenttype=json",
+                sdkAppId,
+                adminUserSig,
+                System.currentTimeMillis()
+            );
+            
+            // 构建请求体
+            Map<String, Object> requestBody = new HashMap<>();
+            List<Map<String, String>> deleteItems = new ArrayList<>();
+            Map<String, String> item = new HashMap<>();
+            item.put("UserID", String.valueOf(userId));
+            deleteItems.add(item);
+            requestBody.put("DeleteItem", deleteItems);
+            
+            // 发送请求
+            RestTemplate restTemplate = new RestTemplate();
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
+            
+            @SuppressWarnings("unchecked")
+            Map<String, Object> response = restTemplate.postForObject(url, entity, Map.class);
+            
+            System.out.println("✅ 删除IM账号成功: " + userId);
+            System.out.println("   响应: " + response);
+            
+            result.put("code", 200);
+            result.put("message", "删除IM账号成功");
+            result.put("data", response);
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ 删除IM账号失败: " + e.getMessage());
+            result.put("code", 500);
+            result.put("message", "删除IM账号失败: " + e.getMessage());
+        }
+        
+        return result;
+    }
+    
+    /**
+     * 批量删除IM账号
+     * DELETE /api/im/deleteAccounts?userIds=123,456,789
+     * @param userIds 用户ID列表(逗号分隔)
+     */
+    @DeleteMapping("/deleteAccounts")
+    public Map<String, Object> deleteAccounts(@RequestParam String userIds) {
+        Map<String, Object> result = new HashMap<>();
+        
+        try {
+            String[] ids = userIds.split(",");
+            
+            // 生成管理员 UserSig
+            String adminUserSig = timUtils.generateUserSig("administrator", 24 * 3600);
+            
+            // 构建请求URL
+            String url = String.format(
+                "https://console.tim.qq.com/v4/im_open_login_svc/account_delete?sdkappid=%d&identifier=administrator&usersig=%s&random=%d&contenttype=json",
+                sdkAppId,
+                adminUserSig,
+                System.currentTimeMillis()
+            );
+            
+            // 构建请求体(批量删除)
+            Map<String, Object> requestBody = new HashMap<>();
+            List<Map<String, String>> deleteItems = new ArrayList<>();
+            
+            for (String userId : ids) {
+                Map<String, String> item = new HashMap<>();
+                item.put("UserID", userId.trim());
+                deleteItems.add(item);
+            }
+            
+            requestBody.put("DeleteItem", deleteItems);
+            
+            // 发送请求
+            RestTemplate restTemplate = new RestTemplate();
+            HttpHeaders headers = new HttpHeaders();
+            headers.setContentType(MediaType.APPLICATION_JSON);
+            HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
+            
+            @SuppressWarnings("unchecked")
+            Map<String, Object> response = restTemplate.postForObject(url, entity, Map.class);
+            
+            System.out.println("✅ 批量删除IM账号成功,数量: " + ids.length);
+            System.out.println("   响应: " + response);
+            
+            result.put("code", 200);
+            result.put("message", "批量删除IM账号成功");
+            
+            Map<String, Object> data = new HashMap<>();
+            data.put("total", ids.length);
+            data.put("response", response);
+            result.put("data", data);
+            
+        } catch (Exception e) {
+            e.printStackTrace();
+            System.err.println("❌ 批量删除IM账号失败: " + e.getMessage());
+            result.put("code", 500);
+            result.put("message", "批量删除IM账号失败: " + e.getMessage());
+        }
+        
+        return result;
+    }
 }