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.

333 lines
11 KiB

# 聊天功能实现逻辑分析文档
## 一、核心问题分析
### 1.1 userId与managerId混用问题
在当前系统中,存在一个关键问题:**当客服进行WebSocket认证时,如果未提供`managerId`但提供了`userId`,系统会将`userId`作为`managerId`使用**。这是一种容错处理,但不是理想的设计,因为:
- `userId`和`managerId`分别来自不同的数据源和表
- `userId`用于users表(普通用户),`managerId`用于userlogin.personnel表(客服人员)
- 两者在数据类型和业务含义上存在本质区别
## 二、认证流程详解
### 2.1 WebSocket认证核心逻辑
认证逻辑位于server-mysql.js文件中,主要包括以下部分:
#### 用户认证流程
```javascript
// 用户认证逻辑
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' }
}));
}
```
#### 客服认证流程(含容错处理)
```javascript
// 客服认证逻辑 - 包含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`函数实现:
```javascript
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 消息存储实现
```javascript
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.js`的`authenticate`函数可以看出,前端可能会发送不完整的认证信息
- 客服认证本应使用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. **后端逻辑修复**
```javascript
// 改进后的客服认证逻辑
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字段,实现双向关联
- 示例查询:
```javascript
// 当只有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的混用。
通过实施建议的修复方案,可以提高系统的可靠性和数据一致性,确保聊天功能正常工作,消息能够正确存储和传递。