You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

11 KiB

聊天功能实现逻辑分析文档

一、核心问题分析

1.1 userId与managerId混用问题

在当前系统中,存在一个关键问题:当客服进行WebSocket认证时,如果未提供managerId但提供了userId,系统会将userId作为managerId使用。这是一种容错处理,但不是理想的设计,因为:

  • userIdmanagerId分别来自不同的数据源和表
  • userId用于users表(普通用户),managerId用于userlogin.personnel表(客服人员)
  • 两者在数据类型和业务含义上存在本质区别

二、认证流程详解

2.1 WebSocket认证核心逻辑

认证逻辑位于server-mysql.js文件中,主要包括以下部分:

用户认证流程

// 用户认证逻辑
defaultUserType === 'user' || finalUserType.includes('customer')) && userId) {
  // 1. 数据库验证用户ID是否存在
  const [existingUsers] = await sequelize.query(
    'SELECT userId FROM users WHERE userId = ? LIMIT 1',
    { replacements: [userId] }
  );
  
  // 2. 查询用户是否在personnel表中存在
  const [personnelData] = await sequelize.query(
    'SELECT id FROM userlogin.personnel WHERE userId = ? LIMIT 1',
    { replacements: [userId] }
  );
  
  // 3. 设置连接信息
  connection.userId = userId;
  connection.isUser = true;
  connection.userType = 'user';
  onlineUsers.set(userId, ws);
  
  // 4. 发送认证成功消息
  ws.send(JSON.stringify({
    type: 'auth_success',
    payload: { userId, type: 'user' }
  }));
}

客服认证流程(含容错处理)

// 客服认证逻辑 - 包含userId作为managerId的容错处理
else if (finalUserType === 'manager' || finalUserType.includes('customer_service')) {
  let stringManagerId;
  if (managerId) {
    stringManagerId = String(managerId).trim();
  } else if (userId) {
    // 问题点:如果没有提供managerId但提供了userId,尝试使用userId作为managerId
    stringManagerId = String(userId).trim();
    console.log(`⚠️ 客服认证使用userId作为managerId: ${stringManagerId}`);
  } else {
    // 缺少必要的managerId或userId
    ws.send(JSON.stringify({
      type: 'auth_error',
      message: '客服认证失败:缺少必要的managerId或userId'
    }));
    return;
  }
  
  // 验证managerId是否在personnel表中存在
  const [existingManagers] = await sequelize.query(
    'SELECT id FROM userlogin.personnel WHERE id = ? LIMIT 1',
    { replacements: [stringManagerId] }
  );
  
  // 设置连接信息
  connection.managerId = stringManagerId;
  connection.isManager = true;
  connection.userType = 'manager';
  onlineManagers.set(stringManagerId, ws);
}

三、消息处理与存储流程

3.1 会话创建与会话管理

会话创建逻辑由createOrGetConversation函数实现:

async function createOrGetConversation(userId, managerId) {
  // 确保ID类型一致
  userId = validateUserId(userId);
  managerId = validateManagerId(managerId);
  
  // 1. 尝试查找已存在的会话
  const [existingConversations] = await sequelize.query(
    'SELECT * FROM chat_conversations WHERE userId = ? AND managerId = ? LIMIT 1',
    { replacements: [userId, managerId] }
  );
  
  if (existingConversations && existingConversations.length > 0) {
    // 如果会话已结束,重新激活
    if (conversation.status !== 1) {
      await sequelize.query(
        'UPDATE chat_conversations SET status = 1 WHERE conversation_id = ?',
        { replacements: [conversation.conversation_id] }
      );
    }
    return conversation;
  }
  
  // 2. 创建新会话
  const conversationId = crypto.randomUUID();
  await sequelize.query(
    `INSERT INTO chat_conversations 
     (conversation_id, userId, managerId, status, user_online, cs_online, created_at, updated_at)
     VALUES (?, ?, ?, 1, ?, ?, ?, ?)`,
    { 
      replacements: [
        conversationId, 
        userId, 
        managerId,
        onlineUsers.has(userId) ? 1 : 0,
        onlineManagers.has(managerId) ? 1 : 0,
        now,
        now
      ]
    }
  );
  
  return { conversation_id: conversationId, userId, managerId, ... };
}

3.2 消息处理核心逻辑

消息处理由handleChatMessage函数实现,包含以下关键步骤:

  1. 确定发送者和接收者

    • 用户发送:senderId = userId,receiverId = managerId
    • 客服发送:senderId = managerId,receiverId = userId
  2. 会话管理

    • 如果没有提供会话ID,创建新会话
    • 如果会话中userId不匹配,进行修复
  3. 消息存储

    • 调用storeMessage函数将消息存入数据库
  4. 消息转发

    • 将消息转发给在线的接收者

3.3 消息存储实现

async function storeMessage(messageData) {
  const { messageId, conversationId, senderType, senderId, receiverId, 
          contentType, content, fileUrl, fileSize, duration, createdAt } = messageData;
  
  // 参数验证
  if (!messageId || !conversationId || !senderType || !senderId || !receiverId || !content) {
    throw new Error('消息数据不完整,缺少必要字段');
  }
  
  // 确保所有ID都是字符串类型
  const stringSenderId = validateUserId(senderId);
  const stringReceiverId = String(receiverId).trim();
  const stringConversationId = String(conversationId).trim();
  
  // 存储消息到数据库
  const result = await sequelize.query(
    `INSERT INTO chat_messages 
     (message_id, conversation_id, sender_type, sender_id, receiver_id, 
      content_type, content, file_url, file_size, duration, is_read, status, 
      created_at, updated_at)
     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 1, ?, ?)`,
    { 
      replacements: [/* 参数列表 */] 
    }
  );
  
  return { success: true, messageId, affectedRows };
}

四、数据模型关系

4.1 核心数据表结构

  1. users表

    • 存储普通用户信息
    • 主键:userId(字符串类型)
  2. userlogin.personnel表

    • 存储客服人员信息
    • 主键:id(数字类型,作为managerId使用)
  3. chat_conversations表

    • 存储会话信息
    • 字段:conversation_id, userId, managerId, status, last_message, last_message_time等
    • userId和managerId分别关联users表和personnel表
  4. chat_messages表

    • 存储消息信息
    • 字段:message_id, conversation_id, sender_type, sender_id, receiver_id, content等
    • conversation_id关联chat_conversations表

4.2 表关系图

+------------+         +-------------------+         +-------------------+
|  users表   |         | chat_conversations表 |         |  chat_messages表 |
+------------+         +-------------------+         +-------------------+
| userId (PK)|1      N | conversation_id   |1      N | message_id       |
| nickName   |<--------| userId (FK)       |<--------| conversation_id  |
| avatarUrl  |         | managerId (FK)    |         | sender_type      |
+------------+         | status            |         | sender_id        |
                       | last_message      |         | receiver_id      |
                       +-------------------+         | content          |
                              ^                      +-------------------+
                              |
+-------------------+         |
| personnel表       |         |
+-------------------+         |
| id (PK/managerId) |---------+
| name              |
| userId (可选)     |
+-------------------+

五、问题分析与建议修复方案

5.1 为什么会使用userId作为managerId?

通过代码分析,我们发现以下几个原因导致了userId被用作managerId:

  1. 前端认证数据不一致

    • utils/websocket.jsauthenticate函数可以看出,前端可能会发送不完整的认证信息
    • 客服认证本应使用managerId,但在某些情况下可能只提供了userId
  2. 容错处理设计

    • 后端代码添加了容错逻辑,当缺少managerId时尝试使用userId作为替代
    • 这是为了避免因认证失败而导致功能完全不可用
  3. 历史数据类型问题

    • 聊天功能问题分析与解决方案.md可知,系统曾存在userId类型不匹配的问题
    • 这可能导致了数据结构设计上的混乱,进而影响了认证逻辑

5.2 当前实现的问题

  1. 数据源混淆

    • userId和managerId分别来自不同的表,混用会导致数据关联错误
    • 当使用userId作为managerId时,系统会在personnel表中查询,可能无法找到对应的记录
  2. 业务逻辑混乱

    • 客服身份验证应该基于personnel表中的managerId,而不是userId
    • 混用导致身份验证逻辑变得模糊和不可靠
  3. 潜在的数据一致性问题

    • 使用错误的ID可能导致会话创建失败或消息发送到错误的接收者

5.3 建议修复方案

  1. 前端改进

    • 确保客服认证时总是正确提供managerId
    • 从utils/websocket.js中可以看出,前端应该优先使用storedManagerId
  2. 后端逻辑修复

    // 改进后的客服认证逻辑
    else if (finalUserType === 'manager' || finalUserType.includes('customer_service')) {
      // 明确要求提供managerId
      if (!managerId) {
        ws.send(JSON.stringify({
          type: 'auth_error',
          message: '客服认证失败:缺少必要的managerId'
        }));
        return;
      }
    
      const stringManagerId = String(managerId).trim();
    
      // 严格验证managerId是否在personnel表中存在
      const [existingManagers] = await sequelize.query(
        'SELECT id FROM userlogin.personnel WHERE id = ? LIMIT 1',
        { replacements: [stringManagerId] }
      );
    
      if (!existingManagers || existingManagers.length === 0) {
        ws.send(JSON.stringify({
          type: 'auth_error',
          message: `客服认证失败:managerId ${stringManagerId} 不存在`
        }));
        return;
      }
    
      // 设置正确的连接信息
      connection.managerId = stringManagerId;
      connection.isManager = true;
      connection.userType = 'manager';
      onlineManagers.set(stringManagerId, ws);
    }
    
  3. 用户-客服映射机制

    • 建立明确的userId和managerId映射关系
    • 如果需要,可以在personnel表中添加userId字段,实现双向关联
    • 示例查询:
      // 当只有userId时,查询对应的managerId
      const [personnelData] = await sequelize.query(
        'SELECT id FROM userlogin.personnel WHERE userId = ? LIMIT 1',
        { replacements: [userId] }
      );
      if (personnelData && personnelData.length > 0) {
        const managerId = String(personnelData[0].id);
        // 使用查询到的managerId进行认证
      }
      
  4. 数据清理与验证

    • 定期清理因混用ID导致的无效会话和消息
    • 添加数据验证逻辑,确保会话和消息中的ID引用关系正确

六、总结

当前聊天功能实现中,将userId作为managerId进行认证是一种临时的容错处理,虽然能够让系统在某些情况下继续工作,但会导致数据关联错误和业务逻辑混乱。

理想的解决方案是建立清晰的用户和客服身份验证机制,确保前端正确提供必要的认证信息,并在后端严格验证这些信息的有效性,避免不同数据源ID的混用。

通过实施建议的修复方案,可以提高系统的可靠性和数据一致性,确保聊天功能正常工作,消息能够正确存储和传递。