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
11 KiB
聊天功能实现逻辑分析文档
一、核心问题分析
1.1 userId与managerId混用问题
在当前系统中,存在一个关键问题:当客服进行WebSocket认证时,如果未提供managerId但提供了userId,系统会将userId作为managerId使用。这是一种容错处理,但不是理想的设计,因为:
userId和managerId分别来自不同的数据源和表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函数实现,包含以下关键步骤:
-
确定发送者和接收者:
- 用户发送:senderId = userId,receiverId = managerId
- 客服发送:senderId = managerId,receiverId = userId
-
会话管理:
- 如果没有提供会话ID,创建新会话
- 如果会话中userId不匹配,进行修复
-
消息存储:
- 调用
storeMessage函数将消息存入数据库
- 调用
-
消息转发:
- 将消息转发给在线的接收者
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 核心数据表结构
-
users表:
- 存储普通用户信息
- 主键:userId(字符串类型)
-
userlogin.personnel表:
- 存储客服人员信息
- 主键:id(数字类型,作为managerId使用)
-
chat_conversations表:
- 存储会话信息
- 字段:conversation_id, userId, managerId, status, last_message, last_message_time等
- userId和managerId分别关联users表和personnel表
-
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:
-
前端认证数据不一致:
- 从
utils/websocket.js的authenticate函数可以看出,前端可能会发送不完整的认证信息 - 客服认证本应使用managerId,但在某些情况下可能只提供了userId
- 从
-
容错处理设计:
- 后端代码添加了容错逻辑,当缺少managerId时尝试使用userId作为替代
- 这是为了避免因认证失败而导致功能完全不可用
-
历史数据类型问题:
- 从
聊天功能问题分析与解决方案.md可知,系统曾存在userId类型不匹配的问题 - 这可能导致了数据结构设计上的混乱,进而影响了认证逻辑
- 从
5.2 当前实现的问题
-
数据源混淆:
- userId和managerId分别来自不同的表,混用会导致数据关联错误
- 当使用userId作为managerId时,系统会在personnel表中查询,可能无法找到对应的记录
-
业务逻辑混乱:
- 客服身份验证应该基于personnel表中的managerId,而不是userId
- 混用导致身份验证逻辑变得模糊和不可靠
-
潜在的数据一致性问题:
- 使用错误的ID可能导致会话创建失败或消息发送到错误的接收者
5.3 建议修复方案
-
前端改进:
- 确保客服认证时总是正确提供managerId
- 从utils/websocket.js中可以看出,前端应该优先使用storedManagerId
-
后端逻辑修复:
// 改进后的客服认证逻辑 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); } -
用户-客服映射机制:
- 建立明确的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进行认证 }
-
数据清理与验证:
- 定期清理因混用ID导致的无效会话和消息
- 添加数据验证逻辑,确保会话和消息中的ID引用关系正确
六、总结
当前聊天功能实现中,将userId作为managerId进行认证是一种临时的容错处理,虽然能够让系统在某些情况下继续工作,但会导致数据关联错误和业务逻辑混乱。
理想的解决方案是建立清晰的用户和客服身份验证机制,确保前端正确提供必要的认证信息,并在后端严格验证这些信息的有效性,避免不同数据源ID的混用。
通过实施建议的修复方案,可以提高系统的可靠性和数据一致性,确保聊天功能正常工作,消息能够正确存储和传递。