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
333 lines
11 KiB
|
3 months ago
|
# 聊天功能实现逻辑分析文档
|
||
|
|
|
||
|
|
## 一、核心问题分析
|
||
|
|
|
||
|
|
### 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的混用。
|
||
|
|
|
||
|
|
通过实施建议的修复方案,可以提高系统的可靠性和数据一致性,确保聊天功能正常工作,消息能够正确存储和传递。
|