chore: 从仓库中移除 doc 目录,后续不再追踪

This commit is contained in:
537yaha
2025-12-02 19:55:54 +08:00
parent 5640fe7ae1
commit c72086dfb6
7 changed files with 0 additions and 3621 deletions
-823
View File
@@ -1,823 +0,0 @@
# 开发日志
> 📋 待实现需求清单请查看:[待实现需求清单.md](./待实现需求清单.md)
## 2025-01-13 12:00:00 UTC
### 完成的工作
1. **实现客服个人资料管理功能**
- **后端**
- User 模型增加字段:`avatar_url`(头像URL)、`nickname`(昵称)、`email`(邮箱)、`created_at``updated_at`
- 创建文件存储服务(`backend/infra/storage.go`):本地存储服务,可扩展为云存储
- 创建个人资料服务(`backend/service/profile_service.go`):提供获取、更新个人资料和上传头像功能
- 创建个人资料控制器(`backend/controller/profile_controller.go`):处理 HTTP 请求
- 新增接口:
- `GET /agent/profile/:user_id`:获取个人资料
- `PUT /agent/profile/:user_id`:更新个人资料(昵称、邮箱)
- `POST /agent/avatar/:user_id`:上传头像(支持 jpg、png、gif,最大 10MB
- 配置静态文件服务:`/uploads` 路径用于访问上传的头像等文件
- **前端**
- 创建个人资料 API 服务(`frontend/features/agent/services/profileApi.ts`
- 创建个人资料 Hook`frontend/features/agent/hooks/useProfile.ts`):管理个人资料状态
- 创建个人资料弹窗组件(`frontend/components/dashboard/ProfileModal.tsx`):
- 显示和编辑昵称、邮箱
- 上传头像(支持预览、上传进度、错误提示)
- 实时更新个人资料
- 更新 DashboardHeader:显示头像和设置按钮,点击打开个人资料弹窗
- 创建头像工具函数(`frontend/utils/avatar.ts`):
- `getAvatarUrl`:拼接完整的头像 URL
- `getAvatarColor`:根据种子值生成头像颜色
- `getAvatarInitial`:获取头像显示文本(首字母)
- 集成个人资料功能到 DashboardShell
- **功能特性**
- 支持头像上传(jpg、png、gif,最大 10MB
- 支持修改昵称和邮箱
- 头像实时预览
- 如果没有上传头像,显示彩色圆形头像(基于用户ID生成颜色)
- 头像显示在 DashboardHeader 中
- **技术要点**
- 文件存储采用可扩展设计,目前使用本地存储,后续可切换为云存储(OSS、S3 等)
- 头像 URL 拼接逻辑:如果后端返回相对路径,前端自动拼接 API_BASE_URL
- 头像上传支持文件类型和大小验证
- 个人资料更新实时刷新 UI
---
## 2025-11-12 19:35:00 UTC
### 完成的工作
1. **修复访客端只有回复消息时才标记客服消息为已读的问题**
- 问题:访客收到客服的消息后,如果访客不回复,消息就一直显示未读;只有访客回复后,客服端才能看到自己的消息变成了已读
- 原因:
- 访客端设置了 `disableAutoScroll={true}`,导致滚动监听被禁用
- 只有在发送消息时(触发自动滚动到底部),才会标记消息为已读
- 如果访客不回复,即使已经在底部附近查看消息,也不会标记为已读
- 解决方案:
- 移除 `disableAutoScroll` 在滚动监听 `useEffect` 中的检查,即使 `disableAutoScroll` 为 true,也应该允许通过滚动来标记消息为已读
- 优化消息列表更新时的已读标记逻辑:即使没有自动滚动,如果用户已经在底部附近,也应该标记为已读
- 这样确保:
- 访客通过滚动到底部查看消息时,会自动标记为已读
- 访客接收到新消息且已经在底部附近时,会自动标记为已读
- 访客发送消息时,也会标记为已读(保持原有行为)
- 实现方式:
- `frontend/components/dashboard/MessageList.tsx`
- 移除 `disableAutoScroll` 在第一个滚动监听 `useEffect` 中的检查
- 优化第二个消息列表更新 `useEffect` 中的已读标记逻辑:如果用户已经在底部附近(`isNearBottom`),即使没有自动滚动,也应该标记为已读
---
## 2025-11-12 19:30:00 UTC
### 完成的工作
1. **修复客服端已读状态有时显示不准确的问题**
- 问题:客服端发送消息后,访客明明已经看过了,但有时候显示已读,有时候还是未读状态
- 原因:
- 当消息已存在时,`handleNewMessage` 直接返回,不会更新消息的已读状态
- 如果 `new_message` 事件在 `messages_read` 事件之后到达,会覆盖已读状态
- `handleMessagesReadBroadcast` 没有检查是否有需要更新的消息,可能进行不必要的状态更新
- 解决方案:
- 优化 `handleNewMessage`:当消息已存在时,更新消息内容(包括已读状态),确保保持最新的已读状态
- 优化 `handleMessagesReadBroadcast`:增加检查是否有需要更新的消息,避免不必要的状态更新
- 这样确保:
- 如果消息已经被标记为已读,即使后续收到 `new_message` 事件,也会保持已读状态
- 如果消息列表中没有需要更新的消息,不会触发不必要的状态更新
- 实现方式:
- `frontend/features/agent/hooks/useMessages.ts`
-`handleNewMessage` 中,当消息已存在时,更新消息内容(包括已读状态)
-`handleMessagesReadBroadcast` 中,增加检查是否有需要更新的消息,避免不必要的状态更新
---
## 2025-11-12 15:25:00 UTC
### 完成的工作
1. **修复客服端已读状态不实时更新的问题**
- 问题:客服端发送消息后,必须手动刷新网页,消息才会变成已读状态
- 原因:
- 后端在广播 `messages_read` 事件时,payload 中没有包含 `conversation_id`
- 前端的 `handleMessagesReadBroadcast` 在更新消息列表时,没有检查 `conversation_id` 是否匹配当前对话
- 解决方案:
- 后端:在 `messages_read` 事件的 payload 中添加 `conversation_id` 字段
- 前端:在 `handleMessagesReadBroadcast` 中,只有当 `conversation_id === conversationId` 时才更新消息列表
- 这样确保只有当前对话的消息才会被更新,避免误更新其他对话的消息
- 实现方式:
- `backend/service/message_service.go`
-`MarkMessagesRead` 方法中,在广播 `messages_read` 事件时,在 payload 中添加 `conversation_id` 字段
- `frontend/features/agent/hooks/useMessages.ts`
-`handleMessagesReadBroadcast` 中,添加 `conversation_id === conversationId` 的检查,只有当匹配时才更新消息列表
---
## 2025-11-12 15:20:00 UTC
### 完成的工作
1. **修复访客端发送消息后不自动滚动的问题**
- 问题:访客端发送完消息后不会自动滚动到底部
- 原因:访客端使用了 `disableAutoScroll={true}`,导致整个滚动逻辑被禁用
- 解决方案:
- 修改 `disableAutoScroll` 的行为:不再完全禁用滚动逻辑
-`disableAutoScroll` 为 true 时,只禁用"收到对方消息时的自动滚动"
- 当最后一条消息是自己发送的时,无论 `disableAutoScroll` 是什么值,都会自动滚动到底部
- 这样确保:
- 查看历史消息时(`disableAutoScroll` 为 true),收到对方消息不会自动滚动
- 自己发送消息后,无论 `disableAutoScroll` 是什么值,都会自动滚动到底部
- 实现方式:
- `frontend/components/dashboard/MessageList.tsx`
- 移除 `disableAutoScroll``useEffect` 开头的早期返回
- 修改滚动判断逻辑:`shouldAutoScroll = hasNewMessage && (isLastMessageFromCurrentUser || (!disableAutoScroll && isNearBottom))`
- 这样确保自己发送的消息总是会触发滚动,而对方发送的消息只有在 `disableAutoScroll` 为 false 且在底部附近时才会滚动
---
## 2025-11-12 15:15:00 UTC
### 完成的工作
1. **优化滚动逻辑,改善查看历史消息的体验**
- 问题:在查看历史消息时,收到对方发送的新消息会自动滚动到底部,打断用户的浏览
- 需求:
- 查看历史消息时,收到对方消息不应该自动滚动到底部
- 自己发送消息后,应该自动滚动到底部
- 解决方案:
- 在消息更新时,通过比较消息ID和数量检查是否有新消息
- 使用 `requestAnimationFrame` 确保 DOM 已更新后再检查位置
- 在 DOM 更新后检查当前位置(距离底部 < 100px 视为在底部附近)
- 滚动逻辑:
1. 如果最后一条消息是自己发送的,无论在哪里都自动滚动到底部
2. 如果最后一条消息是对方发送的,只有在底部附近时才自动滚动到底部(保持底部状态)
3. 如果没有新消息(例如只是消息状态更新),不改变滚动位置
- 这样确保:
- 查看历史消息时(不在底部),收到对方消息不会自动滚动,不会打断浏览
- 自己发送消息后,会自动滚动到底部,可以看到自己发送的消息
- 如果已经在底部查看最新消息,收到对方消息时保持滚动到底部
- 实现方式:
- `frontend/components/dashboard/MessageList.tsx`
- 添加 `lastMessageIdRef``lastMessageCountRef` 来跟踪最后一条消息
- 在消息更新时检查是否有新消息(通过比较消息ID和数量)
- 使用 `requestAnimationFrame` 确保 DOM 已更新后再检查位置和决定是否滚动
- 优化滚动逻辑,区分自己发送的消息和对方发送的消息
-`requestAnimationFrame` 回调中从 `containerRef.current` 重新获取容器,确保使用最新的 DOM 元素
---
## 2025-11-12 15:00:00 UTC
### 完成的工作
1. **修复已读状态逻辑问题**
- 问题1:访客端的已读状态不更新,即使客服已经读取了消息,访客端仍然显示未读,需要刷新页面才能显示已读
- 问题2:客服端的已读逻辑不正确,加载消息时自动标记为已读,但实际上应该在滚动到底部时才标记为已读
- 问题3:访客端收到客服消息时自动标记为已读,但实际上应该在滚动到底部时才标记为已读
- 解决方案:
- 移除加载消息时自动标记为已读的逻辑(客服端和访客端)
- 移除收到新消息时自动标记为已读的逻辑(客服端和访客端)
-`MessageList` 组件中添加滚动检测逻辑,当用户滚动到底部附近(距离底部 < 100px)时,延迟 500ms 后标记未读消息为已读
- 当消息列表更新且自动滚动到底部时,延迟 800ms 后标记未读消息为已读(避免频繁调用,至少间隔 2 秒)
- 修复访客端的 WebSocket `messages_read` 事件处理,确保正确更新已读状态
- 修复客服端的 WebSocket `messages_read` 事件处理,确保只更新客服消息的已读状态(当 `reader_is_agent === false` 时)
- 实现方式:
- `frontend/app/chat/page.tsx`:移除自动标记为已读的逻辑,添加 `onMarkMessagesRead` 回调
- `frontend/app/agent/chat/[conversationId]/page.tsx`:移除自动标记为已读的逻辑,修复 `handleMessagesReadEvent` 函数
- `frontend/features/agent/hooks/useMessages.ts`:移除自动标记为已读的逻辑
- `frontend/components/dashboard/MessageList.tsx`:添加滚动检测逻辑,当滚动到底部时标记消息为已读
- `frontend/components/dashboard/DashboardShell.tsx`:传递 `onMarkMessagesRead` 回调给 `MessageList` 组件
---
## 2025-11-12 14:45:00 UTC
### 完成的工作
1. **优化日志记录,仅保留关键错误**
- 问题:日志过于详细,包含大量正常流程的日志,影响性能和可读性
- 解决方案:移除详细日志,仅保留关键错误和警告日志
- 实现方式:
- 后端:移除正常流程的详细日志(收到发送消息请求、消息创建成功、客户端连接、客户端断开、广播消息等)
- 后端:保留关键错误日志(创建消息失败、WebSocket Hub 为空、广播消息失败、发送消息失败、WebSocket 读取错误、WebSocket 写入错误、发送 ping 失败)
- 后端:保留关键警告日志(客户端断开时未找到、客户端断开时对话不存在、发送消息失败)
- 前端:移除正常流程的详细日志(发送消息、消息发送成功、收到 WebSocket 消息、处理 WebSocket 消息、处理新消息、处理已读事件、添加新消息等)
- 前端:保留关键错误日志(发送消息失败、解析 WebSocket 消息失败、WebSocket 错误、创建 WebSocket 连接失败、WebSocket 重连次数已达上限)
- 前端:移除连接成功的日志(WebSocket 连接成功、WebSocket 连接关闭等)
2. **修复通道关闭问题**
- 问题:通道可能被重复关闭,导致 panic
- 解决方案:在关闭通道前检查通道是否已经关闭
- 实现方式:
-`Hub.unregister` 中,使用 `select` 检查通道是否已经关闭
- 如果通道已经关闭,不再关闭
- 如果通道未关闭,关闭它
3. **优化 WebSocket 连接管理**
- 问题:在开发模式下,大量 WebSocket 连接可能导致问题
- 解决方案:改进断开连接逻辑,确保连接正确关闭
- 实现方式:
-`WSClient.disconnect` 中,设置 `reconnectAttempts = maxReconnectAttempts` 避免重连
- 在关闭连接前,检查连接状态
- 移除断开连接的详细日志
---
## 2025-11-12 14:30:00 UTC
### 完成的工作
1. **添加详细日志用于调试消息广播问题**
- 问题:用户报告访客端发送消息后,客服端没有收到(后续确认消息发送正常)
- 解决方案:添加详细的日志来跟踪消息发送和广播过程,便于未来调试
- 注意:后续优化为仅保留关键错误日志(见 2025-11-12 14:45:00 UTC
### 技术细节
- 修改文件:
- `backend/controller/message_controller.go`:添加消息创建日志
- `backend/service/message_service.go`:添加消息广播日志
- `backend/websocket/hub.go`:添加广播消息日志、修复通道关闭问题
- `backend/websocket/client.go`:添加 WebSocket 发送消息日志、修复通道关闭问题
- `frontend/features/agent/services/messageApi.ts`:添加消息发送日志
- `frontend/app/chat/page.tsx`:添加消息发送和处理日志
- `frontend/features/agent/hooks/useMessages.ts`:添加消息处理日志
- `frontend/lib/websocket.ts`:添加 WebSocket 消息接收日志、优化断开连接逻辑
### 调试步骤
1. **测试消息发送**
- 在访客端发送消息
- 查看浏览器控制台日志:
- 应该看到 `📨 开始发送消息: 对话ID=X, 内容="..."`
- 应该看到 `📤 发送消息: 对话ID=X, 是客服=false, 发送者ID=0, 内容长度=X`
- 应该看到 `✅ 消息发送成功: 对话ID=X`
- 查看后端日志:
- 应该看到 `📨 收到发送消息请求: 对话ID=X, 发送者ID=0, 是客服=false, 内容长度=X`
- 应该看到 `✅ 消息创建成功: 消息ID=X, 对话ID=X, 已广播`
- 应该看到 `📤 准备通过 WebSocket 广播消息: 消息ID=X, 对话ID=X`
- 应该看到 `📤 准备广播消息: 对话ID=X, 类型=new_message`
- 应该看到 `📢 广播消息: 对话ID=X, 类型=new_message, 客户端数=X`
- 应该看到 `📤 WebSocket 消息已发送: 对话ID=X, 类型=new_message, 是访客=false`(客服端)
- 应该看到 `✅ 消息广播完成: 对话ID=X, 成功=X, 失败=0`
2. **测试消息接收**
- 在客服端查看浏览器控制台日志:
- 应该看到 `📨 收到 WebSocket 消息: 对话ID=X, 类型=new_message`
- 应该看到 `📨 处理 WebSocket 消息(客服端): 对话ID=X, 类型=new_message`
- 应该看到 `📨 处理新消息(客服端): {...}`
- 应该看到 `✅ 添加新消息: 消息ID=X, 内容="..."`
3. **如果消息没有发送**
- 检查浏览器控制台是否有错误
- 检查网络请求(Network 标签)是否有 `POST /messages` 请求
- 检查请求状态码(应该是 200
4. **如果消息发送了但没有广播**
- 检查后端日志是否有 `📤 准备通过 WebSocket 广播消息` 日志
- 检查后端日志是否有 `📢 广播消息` 日志
- 检查后端日志是否有 `⚠️ WebSocket Hub 为空` 日志(如果有,说明 Hub 没有正确初始化)
5. **如果消息广播了但没有收到**
- 检查后端日志是否有 `📤 WebSocket 消息已发送` 日志
- 检查后端日志是否有 `⚠️ 发送消息失败` 日志
- 检查前端日志是否有 `📨 收到 WebSocket 消息` 日志
- 检查 WebSocket 连接是否正常(查看浏览器 Network 标签中的 WebSocket 连接)
### 后续优化
- 检查开发模式下大量 WebSocket 连接的问题(可能是 Next.js 热重载导致的)
- 优化连接管理,减少重复连接
- 添加连接数限制,防止连接数过多
---
## 2025-11-12 14:00:00 UTC
### 完成的工作
1. **修复 WebSocket 错误处理问题**
- 问题:WebSocket 连接错误显示为 `{}`,错误信息不够详细
- 解决方案:改进错误处理,提供更详细的错误信息
- 实现方式:
-`onerror` 事件中检查 `readyState``url`,提供详细的错误信息
-`onclose` 事件中获取关闭代码和原因,提供详细的关闭信息
- 只有在非正常关闭时才尝试重连(避免在开发模式下频繁重连)
- 使用 `useRef` 存储回调函数,避免因回调函数变化导致重新连接
- 在连接前检查是否已存在连接,避免重复连接
### 技术细节
- 修改文件:
- `frontend/lib/websocket.ts`:改进错误处理和关闭处理
- `frontend/features/agent/hooks/useWebSocket.ts`:使用 `useRef` 存储回调函数
- `frontend/app/chat/page.tsx`:明确设置 `isVisitor: true`
- 实现原理:
-`onerror` 事件中,检查 `readyState``url`,提供详细错误信息
-`onclose` 事件中,获取关闭代码(`code`)、原因(`reason`)和是否干净关闭(`wasClean`
- 只有在 `!wasClean && code !== 1000` 时才尝试重连
- 使用 `useRef` 存储回调函数,避免因回调函数变化导致 `useEffect` 重新执行
-`connect()` 方法中,检查是否已存在连接,如果存在则先断开
-`npm run lint`frontend,无警告)
### 错误处理改进
- ✅ 提供详细的错误信息(状态、URL等)
- ✅ 提供详细的关闭信息(关闭代码、原因、是否干净关闭)
- ✅ 避免在开发模式下频繁重连
- ✅ 避免因回调函数变化导致重新连接
- ✅ 避免重复连接
### 测试状态
✅ 手动验证:WebSocket 错误处理和关闭处理正常,错误信息详细
---
## 2025-11-12 13:30:00 UTC
### 完成的工作
1. **修复输入框失去焦点问题**
- 问题:在访客端和客服端,发送完一条消息后,输入框失去焦点,需要再次点击输入框才能继续输入
- 解决方案:在 `MessageInput` 组件中添加自动聚焦功能
- 实现方式:
- 使用 `useRef` 引用输入框元素
- 使用 `useEffect` 监听 `sending` 状态变化
-`sending``true` 变为 `false` 时(发送完成),自动聚焦到输入框
- 使用 `setTimeout` 确保 DOM 更新完成后再聚焦
### 技术细节
- 修改文件:
- `frontend/components/dashboard/MessageInput.tsx`:添加自动聚焦功能
- 实现原理:
- 使用 `useRef` 创建输入框引用 `inputRef`
- 使用 `useRef` 记录上一次的 `sending` 状态 `prevSendingRef`
-`useEffect` 中监听 `sending` 状态变化
-`prevSendingRef.current === true && sending === false` 时,说明刚刚发送完成
- 调用 `inputRef.current?.focus()` 聚焦到输入框
- 使用 `setTimeout(..., 0)` 确保 DOM 更新完成后再聚焦
-`npm run lint`frontend,无警告)
### 测试状态
✅ 手动验证:发送消息后,输入框自动聚焦,可以直接继续输入
### 用户体验改进
- ✅ 发送消息后,输入框自动聚焦,无需再次点击
- ✅ 用户可以连续发送多条消息,无需每次点击输入框
- ✅ 提升聊天体验,减少操作步骤
---
## 2025-11-12 13:00:00 UTC
### 完成的工作
1. **更新测试指南文档**
- 添加完整的测试指南,覆盖所有已实现功能
- 添加访客端测试流程(8个测试项)
- 添加客服端测试流程(16个测试项)
- 添加实时通信测试(WebSocket 实时推送、已读状态同步、在线状态更新)
- 添加搜索功能测试(关键词高亮、自动定位)
- 添加访客信息测试(信息收集、联系信息编辑)
- 添加界面交互测试(滚动行为、响应式布局)
- 添加错误处理测试(网络错误、数据验证)
- 添加性能测试(加载性能、实时性能)
- 添加兼容性测试(浏览器兼容、数据持久化)
- 添加快速测试流程(核心功能验证,约30分钟)
- 添加高级测试场景(8个场景:多访客、并发、长时间连接、网络中断、大量消息、搜索性能、并发编辑、多访客状态)
- 添加调试技巧(浏览器开发者工具、控制台、网络请求、后端日志)
- 添加常见问题(10个常见问题及解决方案)
- 添加完整测试检查清单(8个测试类别,100+ 测试项)
- 添加测试结果记录模板
### 技术细节
- 修改文件:
- `doc/测试指南.md`:完整重写,添加所有已实现功能的测试指南
- 文档结构:
- 一、准备工作(数据库配置、快速开始)
- 二、启动后端
- 三、启动前端
- 四、访客端测试流程(8个测试项)
- 五、客服端测试流程(16个测试项)
- 六、调试技巧
- 七、常见问题(10个常见问题)
- 八、完整测试检查清单(8个测试类别)
- 九、快速测试流程(核心功能验证)
- 十、高级测试场景(8个场景)
- 十一、测试结果记录
### 测试覆盖
- ✅ 基础功能测试(访客端、客服端)
- ✅ 实时通信测试(WebSocket 实时推送、已读状态同步、在线状态更新)
- ✅ 搜索功能测试(关键词高亮、自动定位)
- ✅ 访客信息测试(信息收集、联系信息编辑)
- ✅ 界面交互测试(滚动行为、响应式布局)
- ✅ 错误处理测试(网络错误、数据验证)
- ✅ 性能测试(加载性能、实时性能)
- ✅ 兼容性测试(浏览器兼容、数据持久化)
- ✅ 高级测试场景(多访客、并发、长时间连接、网络中断、大量消息、搜索性能、并发编辑、多访客状态)
### 后续优化
- 添加自动化测试(E2E 测试)
- 添加性能测试报告
- 添加测试覆盖率报告
---
## 2025-11-12 12:00:00 UTC
### 完成的工作
1. **对话状态管理(在线/离线)实时更新**
- 后端:WebSocket 连接建立时标记访客在线,断开时标记离线
- 后端:通过 WebSocket 推送 `visitor_status_update` 事件到客服端
- 后端:在 `ConversationService` 中添加 `UpdateVisitorOnlineStatus``UpdateLastSeenAt` 方法
- 后端:在 `Hub` 中添加回调机制,在客户端连接/断开时调用回调函数
- 后端:在 `Client` 中添加 `isVisitor` 字段,区分访客和客服
- 前端:WebSocket 客户端添加 `isVisitor` 参数,默认值为 `true`
- 前端:客服端 WebSocket 连接设置 `isVisitor=false`
- 前端:客服端接收 `visitor_status_update` 事件,刷新对话详情
- 前端:在对话列表中显示在线/离线图标(绿色圆点表示在线)
### 技术细节
- 修改文件:
- 后端:`backend/service/conversation_service.go``backend/websocket/hub.go``backend/websocket/client.go``backend/websocket/handler.go``backend/main.go`
- 前端:`frontend/lib/websocket.ts``frontend/features/agent/hooks/useWebSocket.ts``frontend/features/agent/hooks/useMessages.ts``frontend/features/agent/types.ts``frontend/components/dashboard/ConversationListItem.tsx`
-`npm run lint`frontend,无警告)
-`gofmt`backend,无错误)
### 实现原理
- 当访客连接 WebSocket 时,后端会调用 `UpdateVisitorOnlineStatus(conversationID, true)` 更新在线状态
- 当访客断开 WebSocket 时,后端会检查该对话是否还有其他访客连接,如果没有,则调用 `UpdateVisitorOnlineStatus(conversationID, false)` 更新离线状态
- 后端通过 WebSocket 广播 `visitor_status_update` 事件到该对话的所有客户端(包括客服)
- 客服端在收到 `visitor_status_update` 事件时,刷新当前对话详情,更新在线状态
- 对话列表中显示在线/离线图标(基于 `status === "open"` 判断)
### 测试状态
✅ 手动验证:访客连接/断开 WebSocket 时,客服端实时更新在线状态
### 后续优化
- 实现心跳机制(定期更新 `last_seen_at`
- 根据 `last_seen_at` 判断是否在线(例如,如果 `last_seen_at` 在最近 60 秒内,则认为在线)
-`ConversationSummary` 中添加 `last_seen_at` 字段,以便在对话列表中显示最后活跃时间
- 定期轮询对话列表,更新所有对话的状态
---
## 2025-11-12 11:10:00 UTC
### 完成的工作
1. **修复已读状态同步问题**
- 访客端:修复 `handleMessagesReadEvent` 未判断 `reader_is_agent`,导致客服读取访客消息后,访客端无法更新已读状态
- 客服端:修复 `handleMessagesReadBroadcast` 未判断 `reader_is_agent`,导致访客读取客服消息后,客服端无法更新已读状态
- 访客端:只有当 `reader_is_agent === true` 时,才更新访客消息(`sender_is_agent === false`)的已读状态
- 客服端:只有当 `reader_is_agent === false` 时,才更新客服消息(`sender_is_agent === true`)的已读状态
### 技术细节
- 修改文件:`frontend/app/chat/page.tsx``frontend/features/agent/hooks/useMessages.ts`
-`npm run lint`frontend,无警告)
### 问题原因
- 后端通过 WebSocket 推送 `messages_read` 事件时,会包含 `reader_is_agent` 字段,表示读取者是客服还是访客
- 前端在接收 `messages_read` 事件时,没有判断 `reader_is_agent`,导致错误地更新了消息的已读状态
- 对于访客端:只有当客服读取了访客的消息(`reader_is_agent === true`)时,才应该更新访客消息的已读状态
- 对于客服端:只有当访客读取了客服的消息(`reader_is_agent === false`)时,才应该更新客服消息的已读状态
### 测试状态
✅ 手动验证:访客发送消息后,客服查看消息,访客端显示双对勾(已读状态)
---
## 2025-11-11 08:00:00 UTC
### 完成的工作
1. **访客联系信息编辑闭环**
- 后端新增 `PUT /conversations/:id/contact` 接口,`ConversationService.UpdateConversationContact` 落库邮箱/电话/备注
- 前端 `VisitorDetailPanel` 增加弹窗编辑,支持新增、修改、清空邮箱/电话/备注并即时刷新
2. **服务与 Hook 扩展**
- `conversationApi.updateConversationContact` 封装更新接口,统一返回结构
- `useMessages` 暴露 `updateContactInfo``DashboardShell``VisitorDetailPanel` 通过钩子完成联动
### 技术细节
- 新增/修改文件:`backend/controller/conversation_controller.go``backend/service/conversation_service.go``backend/router/router.go``backend/service/types.go`
- 前端涉及文件:`features/agent/services/conversationApi.ts``features/agent/hooks/useMessages.ts``components/dashboard/VisitorDetailPanel.tsx`
-`npm run lint`frontend
### 测试状态
✅ 手动验证:客服工作台编辑邮箱/电话/备注,数据保存后右栏即时更新
---
## 2025-11-10 07:30:00 UTC
### 完成的工作
1. **客服工作台前端架构拆分**
- `app/agent/dashboard/page.tsx` 只保留页面入口,改由 `DashboardShell` 负责布局编排
- 新增 `components/dashboard/*`,将导航栏、会话列表、消息区、访客详情拆分为独立组件
- 新增 `features/agent/hooks``features/agent/services`,分别承载状态逻辑与 API 调用
- 新增 `utils/format.ts``utils/highlight.tsx``utils/storage.ts`,统一时间格式、关键词高亮与本地存储操作
2. **会话/消息状态管理优化**
- `useAuth` 统一处理本地登录信息与退出逻辑
- `useConversations` 负责对话列表、搜索、防抖与排序
- `useMessages` + `useWebSocket` 负责消息拉取、已读回执、WebSocket 广播与高亮定位
3. **TypeScript 类型补全**
- `features/agent/types.ts` 汇总会话、消息、用户等公共类型
- `lib/websocket.ts``useWebSocket``useMessages` 改用强类型定义,消除 `any`
4. **旧版客服聊天页迁移**
- `/agent/chat/[conversationId]` 复用统一的消息组件、输入框与 WebSocket 逻辑
- 接入服务层 API 与 Hook,移除旧版冗余状态/样式代码
- 支持快速返回工作台,交互体验与四栏布局保持一致
5. **访客聊天页重构**
- `/chat` 页面改用统一的 `MessageList``MessageInput` 组件和消息服务
- 对话初始化、WebSocket、已读回执与客服端共享实现,减少重复逻辑
- 保留访客视角的气泡样式与默认提示,UI/状态与客服端保持一致
### 技术细节
- 新增目录:`components/dashboard/``features/agent/hooks/``features/agent/services/``utils/`
- 复用工具函数:消息预览截断、时间格式化、关键词高亮、localStorage 操作
- ESLint`npm run lint` 通过
### 测试状态
`npm run lint`frontend,无警告)
### 下一步计划
- 继续补充自动化测试(组件级、hook 级单元测试)
-`services` 层补充基础错误处理与重试策略
- 为文件上传、AI 客服等后续功能预留组件与 Hook 模板
## 2025-11-10 05:00:00 UTC
### 完成的工作
1. **访客信息采集落地**
- 后端 `InitConversation` 接口接收并保存网站、来源、浏览器、系统、语言、IP 等信息
- 访客前端自动采集页面 URL、Referrer、User-Agent、语言信息并随初始化请求提交
- 对话表新增字段,支持持久化访客技术信息及联系信息占位
- 会话 `last_seen_at` 字段初始化,便于后续在线状态展示
2. **系统消息写入与展示**
- 消息表新增 `message_type` 字段,区分普通消息与系统消息
- 新对话自动生成系统消息(访问入口、来源页面)
- 客服工作台中,系统消息以灰色气泡居中展示,支持关键词高亮与定位
3. **客服工作台访客详情完善**
- 新增 `GET /conversations/:id` 接口返回完整访客信息
- 右侧访客详情面板展示网站、来源、浏览器、系统、语言、IP、最后活跃等数据
- 联系信息区域展示实际数据(暂无信息时提供提示),保留后续编辑入口
- 聊天头部显示更准确的 last seen 信息
4. **搜索体验优化**
- 搜索匹配的系统消息支持高亮与自动定位
- WebSocket 收到新消息时自动刷新会话详情,保持访客信息实时
5. **消息已读/未读状态(基础版)**
- 数据库新增 `is_read` / `read_at` 字段,支持已读记录
- 新增 `PUT /messages/read` 接口及 WebSocket `messages_read` 事件,同步状态
- 客服端/访客端聊天气泡显示单/双对勾,已读回执实时可见
- 对话列表同步显示最后一条消息的已读状态
### 技术细节
- MySQL 表结构:`conversations` 新增多项访客字段,`messages` 新增 `message_type`
- 后端:新增 `ConversationDetailRes``GetConversationDetail`,并统一时间格式输出
- 前端:新增访客详情状态管理、系统消息渲染分支、技术信息展示组件
- WebSocket:收到新消息后同步刷新会话详情,确保右侧数据与消息流一致
### 测试状态
✅ 通过手动测试:访问 `/chat` 生成新对话,确认数据库记录访客信息与系统消息
✅ 通过客服工作台验证:系统消息样式正常,访客详情与数据库数据一致
✅ 搜索“关键词”后点击结果,可自动定位系统消息并高亮
### 下一步计划
- 实现客服端联系信息的手动添加/编辑
- 基于 `last_seen_at` 和 WebSocket 心跳完成在线状态实时更新
- 继续扩展系统消息(如客服加入/离开、对话状态变化)
- 访客位置字段对接外部服务(基于 IP 定位)
## 2025-01-16 18:00:00 UTC
### 完成的工作
1. **客服工作台四栏布局实现**
- 实现最左侧导航菜单栏(固定宽度 64px,浅灰色背景)
- 实现左侧对话列表栏(固定宽度 320px,显示所有对话)
- 实现中间聊天内容栏(自适应宽度,集成完整聊天功能)
- 实现右侧访客详情栏(固定宽度 320px,显示联系信息和技术信息)
- 统一顶部栏高度(h-16),确保三栏对齐
2. **中间栏聊天功能集成**
- 集成消息显示功能(客服消息在右,访客消息在左)
- 集成消息发送功能(支持实时发送)
- 集成 WebSocket 实时通信(自动接收新消息)
- 实现自动滚动到底部(新消息自动可见)
- 实现智能时间格式化(今天显示时间,更早显示日期+时间)
- 实现消息加载状态和发送状态
3. **右侧栏访客详情实现**
- 显示访客头像(基于 visitor_id 生成颜色)
- 显示在线/离线状态(基于对话状态)
- 显示联系信息(邮箱、电话、备注,支持添加按钮)
- 显示技术信息(网站、来源、位置等,占位待实现)
- 实现刷新按钮(可刷新消息)
4. **UI 优化**
- 移除右侧栏重复的基础信息(对话ID、状态等已在左侧显示)
- 统一联系信息的交互方式(邮箱、电话、备注都有"+ Add"按钮)
- 优化导航栏颜色(与聊天内容背景一致)
- 优化对话列表显示(头像、ID、状态、时间)
### 技术细节
- 使用 React 状态管理多个对话和消息
- 使用 `useEffect` 实现对话切换时自动加载消息
- 使用 WebSocket 实现实时消息推送
- 使用 `useRef` 实现自动滚动功能
- 使用 Tailwind CSS 实现响应式布局
- 对话切换时自动清空消息列表并重新加载
### 用户体验
- 无需跳转页面,在同一页面内切换对话
- 实时接收新消息,无需手动刷新
- 消息发送后立即显示,体验流畅
- 界面布局清晰,信息层次分明
- 右侧栏专注于显示左侧看不到的详细信息
### 测试状态
✅ 功能测试通过:四栏布局、对话切换、消息发送、实时通信均正常工作
### 下一步计划
- 实现会话搜索功能(左侧栏搜索框)
- 实现客服个人资料管理(头像上传、信息修改)
- 实现访客信息自动收集(技术信息、来源页面等)
- 实现对话状态管理(在线/离线状态实时更新)
## 2025-01-15 21:00:00 UTC
### 完成的工作
1. **WebSocket 实时通信功能**
- 实现后端 WebSocket Hub 管理器:管理所有客户端连接,按对话ID分组
- 实现 WebSocket 客户端处理:处理连接、心跳检测、消息发送
- 实现 WebSocket 路由处理:升级 HTTP 连接为 WebSocket 连接
- 集成消息推送:消息创建后自动通过 WebSocket 推送给所有相关客户端
- 实现前端 WebSocket 客户端:封装连接、自动重连、消息接收
- 访客端和客服端都支持实时消息推送
2. **技术实现**
- 后端使用 `gorilla/websocket`
- 前端使用原生 WebSocket API
- 实现心跳检测(Ping/Pong)保持连接活跃
- 实现自动重连机制(最多 5 次)
- 消息去重,避免重复显示
3. **文档更新**
- 创建 `doc/WebSocket学习笔记.md`:详细解释 WebSocket 工作原理
- 包含前后端代码示例和完整流程说明
### 技术细节
- WebSocket 路由:`/ws?conversation_id=<对话ID>`
- 消息格式:`{ type: "new_message", conversation_id: number, data: Message }`
- 连接升级:HTTP 连接升级为 WebSocket 连接(101 状态码)
- 心跳间隔:每 54 秒发送一次 Ping
- 重连策略:断开后等待 3 秒,最多重试 5 次
### 用户体验改进
- ✅ 访客发送消息后,客服立即看到(无需刷新)
- ✅ 客服发送消息后,访客立即看到(无需刷新)
- ✅ 真正的实时双向通信
- ✅ 连接断开后自动重连
### 测试状态
✅ 功能实现完成,待测试
## 2025-01-15 20:00:00 UTC
### 完成的工作
1. **客服端功能完整实现**
- 实现客服登录页面(`/`):使用默认管理员账号(admin/admin123)登录
- 实现对话列表页面(`/agent/conversations`):显示所有未关闭的对话,支持点击进入聊天
- 实现客服聊天页面(`/agent/chat/[conversationId]`):客服可以查看和回复访客消息
- 实现登录状态检查:未登录自动跳转到登录页
- 实现退出登录功能:清除登录状态并跳转
2. **消息显示优化**
- 客服端消息布局:客服消息在右侧(蓝色气泡),访客消息在左侧(白色气泡)
- 从客服视角优化 UI,符合客服使用习惯
3. **后端功能完善**
- 实现默认管理员账号自动创建(首次启动时创建 admin/admin123
- 实现对话列表查询接口(`GET /conversations`):返回所有未关闭的对话
- 实现创建客服账号接口(`POST /admin/users`):管理员可以创建新的客服/管理员账号
- 实现退出登录接口(`POST /logout`):用于前端清除登录状态
4. **文档更新**
- 更新测试指南:添加完整的客服端测试流程
- 更新 CHANGELOG:记录客服端开发过程
### 技术细节
- 使用 `localStorage` 存储客服登录信息(`agent_user_id``agent_username``agent_role`
- 使用 Next.js 路由保护:检查登录状态,未登录自动跳转
- 客服聊天页面复用访客聊天页面的核心逻辑,调整消息显示位置
- 后端使用 `initDefaultAdmin` 函数在首次启动时自动创建默认管理员
### 用户体验
- 客服登录流程简单:使用默认账号即可登录
- 对话列表清晰:显示对话状态、访客ID、时间等信息
- 客服聊天界面直观:客服消息在右,访客消息在左
- 登录状态管理完善:未登录自动跳转,退出登录清除状态
### 测试状态
✅ 功能测试通过:登录、对话列表、客服聊天、退出登录均正常工作
### 下一步计划
- 添加实时消息推送(WebSocket)
- 添加对话状态管理(关闭对话、重新打开等)
- 优化 UI 和用户体验
## 2025-01-15 18:00:00 UTC
### 完成的工作
1. **访客聊天页面完整实现**
- 实现消息发送功能(`POST /messages`
- 实现消息拉取功能(`GET /messages`
- 实现消息列表展示(区分访客和客服消息)
- 实现自动滚动到底部(新消息自动可见)
- 实现时间格式化显示(今天显示时间,更早显示日期+时间)
- 优化 UI:删除多余标签,位置已能区分发送者
2. **代码优化和注释**
- 为所有代码添加详细的中文注释,解释每行代码的作用
- 使用通俗易懂的类比(如"数据盒子"、"书签"等)帮助理解
- 优化代码结构,确保逻辑清晰
3. **文档完善**
- 创建 `doc/前端学习笔记.md`:核心概念解释、英文单词记忆、常见错误
- 创建 `doc/测试指南.md`:完整的测试步骤和问题排查指南
- 创建 `doc/系统角色说明.md`:解释访客和客服的区别
- 修复测试指南中的 localStorage 说明(同一浏览器标签页共享)
4. **Bug修复**
- 修复 `.env` 文件 UTF-8 BOM 编码问题(godotenv 不支持 BOM
- 添加后端 `.env` 文件加载的详细调试信息
- 修复代码结构问题(消息列表和输入框位置错误)
### 技术细节
- 使用 `useState` 管理消息列表、输入框、加载状态
- 使用 `useEffect` 实现自动拉取消息和自动滚动
- 使用 `useRef` 实现自动滚动到底部的功能
- 使用 `localStorage` 持久化访客ID(同一浏览器标签页共享)
### 用户体验
- 访客无需登录,直接访问 `/chat` 即可使用
- 发送消息后自动滚动到底部,无需手动滚动
- 时间显示智能优化:今天只显示时间,更早显示日期+时间
- UI 简洁:位置已能区分发送者,无需额外标签
### 测试状态
✅ 功能测试通过:消息发送、接收、显示、自动滚动均正常工作
### 下一步计划
- 开发客服端功能(客服登录、对话列表、客服聊天页面)
- 或优化现有功能(消息状态、错误重试、UI 优化等)
## 2025-01-15 10:00:00 UTC
### 完成的工作
1. 前端配置化改进
- 创建 `frontend/lib/config.ts` 统一管理 API 地址配置
- 使用环境变量 `NEXT_PUBLIC_API_BASE_URL` 配置后端地址
- 移除前端所有硬编码 API 地址
- 支持通过 `.env.local` 配置不同环境的后端地址
2. 更新文档
- README 添加前端环境变量配置说明
- 补充部署环境切换的使用说明
### 意义
- 本地开发:无需配置,默认使用 `http://127.0.0.1:8080`
- 生产部署:只需修改 `.env.local` 中的 `NEXT_PUBLIC_API_BASE_URL` 为实际域名
- 好处:类比后端数据库配置,前端 API 配置也可通过环境变量灵活切换
## 2025-10-30 12:00:00 UTC
### 完成的工作
1. 统一后端接口路径与方法
- 新增规范路由:`POST /conversation/init``POST /messages``GET /messages`
- 删除旧路由:`/initconversation``/createmessage``/listmessage`
2. 数据库配置改为环境变量
-`.env` 读取 `DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME`
- 移除硬编码 DSN,提升安全性与可配置性
3. 更新 README
- 补充接口文档与后端环境变量示例,增加 `.env` 使用说明
### 风险与注意
- 该变更为不向后兼容的变更,请确保前端使用新路由
- 需要在 `backend/.env` 正确配置数据库连接参数
## 2024-12-19 15:30:00 UTC
### 完成的工作
1. **完善对话初始化功能**
- 修复了 `InitConversation` 函数中不完整的代码逻辑
- 添加了完整的错误处理和响应返回
- 实现了查找现有对话或创建新对话的逻辑
2. **创建项目文档**
- 编写了详细的 README.md 文件
- 包含项目结构、功能说明、API接口文档
- 添加了对话初始化逻辑的详细解释
3. **代码优化**
- 统一了变量命名(req 替代 in)
- 改进了错误提示信息
- 添加了详细的中文注释
### 技术细节
- 对话初始化逻辑:先查找访客的现有开放对话,如果没有则创建新对话
- 使用 GORM 进行数据库操作
- 实现了完整的错误处理机制
### 下一步计划
- 完善发送消息功能
- 实现拉取消息功能
- 添加前端界面
-487
View File
@@ -1,487 +0,0 @@
# WebSocket 学习笔记(通俗版)
## 一、什么是 WebSocket
### 1. 类比理解
**HTTP 就像"打电话"**
- 你打电话 → 对方接听 → 你说完挂断
- 每次都要重新拨号才能通话
- 不能一直保持连接
**WebSocket 就像"对讲机"**
- 你按住按钮说话 → 对方立即听到
- 连接一直保持,随时可以说话
- 不需要每次都"拨号"
### 2. 为什么需要 WebSocket
**传统 HTTP 的问题**
- 访客发送消息 → 后端收到 → 但客服不知道有新消息
- 客服必须手动刷新页面才能看到新消息
- 无法实时推送消息
**WebSocket 的解决方案**
- 访客发送消息 → 后端收到 → 后端立即"喊话"给所有连接的客户端
- 客服不需要刷新,消息自动显示
- 真正的实时通信!
## 二、WebSocket 工作原理(整体流程)
### 1. 连接建立过程
```
1. 前端:我要连接 WebSocket(就像敲门)
ws = new WebSocket("ws://127.0.0.1:8080/ws?conversation_id=123")
2. 后端:检查请求,同意连接(就像开门)
→ 升级 HTTP 连接为 WebSocket 连接
3. 前端:连接成功!(可以开始"对话"了)
ws.onopen = () => { console.log("连接成功") }
```
**关键点**
- 第一次连接还是用 HTTP(就像敲门)
- 后端"升级"这个连接为 WebSocket(就像开门让你进来)
- 之后就可以双向通信了
### 2. 消息发送和接收
```
前端发送消息:
前端 → [HTTP POST] → 后端 → 保存到数据库 → 通过 WebSocket 广播
后端推送消息:
后端 → [WebSocket] → 所有连接的客户端 → 自动显示新消息
```
## 三、后端 WebSocket 工作原理
### 1. Hub(中心管理器)
**类比**:就像"会议室管理员"
```go
// backend/websocket/hub.go
type Hub struct {
// 每个对话ID对应哪些客户端连接
// 就像:会议室1有哪些人在里面
conversations map[uint]map[*Client]bool
// 注册新客户端(有人要加入)
register chan *Client
// 注销客户端(有人要离开)
unregister chan *Client
// 广播消息(有新消息要告诉所有人)
broadcast chan *Message
}
```
**工作流程**
```
1. 有人连接 → 发到 register 通道
2. Hub 收到 → 把这个客户端加到对应对话的列表里
3. 有新消息 → 发到 broadcast 通道
4. Hub 收到 → 找到这个对话的所有客户端 → 发送消息给每个人
```
### 2. Client(客户端连接)
**类比**:就像"会议室里的一个人"
```go
// backend/websocket/client.go
type Client struct {
hub *Hub // 属于哪个 Hub
conn *websocket.Conn // WebSocket 连接
send chan *Message // 发送消息的通道
conversationID uint // 属于哪个对话
}
```
**两个重要方法**
- `ReadPump()`:从客户端读取消息(比如心跳包)
- `WritePump()`:向客户端发送消息(比如新消息推送)
### 3. Handler(处理函数)
**类比**:就像"门卫",检查谁要进来
```go
// backend/websocket/handler.go
func HandleWebSocket(hub *Hub) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. 从 URL 获取对话ID
conversationID := c.Query("conversation_id")
// 2. 升级 HTTP 连接为 WebSocket 连接
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
// 3. 创建客户端
client := NewClient(hub, conn, conversationID)
// 4. 注册到 Hub
hub.register <- client
// 5. 启动读写循环
go client.WritePump()
go client.ReadPump()
}
}
```
**关键步骤**
1. `upgrader.Upgrade()`:把 HTTP 连接"升级"成 WebSocket 连接
2. `hub.register <- client`:告诉 Hub "我来了"
3. `go client.WritePump()`:启动一个 goroutine(后台线程)来发送消息
4. `go client.ReadPump()`:启动一个 goroutine 来接收消息
### 4. 消息推送流程
```go
// backend/service/service.go
func CreateMessage(db *gorm.DB, hub BroadcastHub) {
// 1. 创建消息
msg := models.Message{...}
db.Create(&msg)
// 2. 通过 WebSocket 推送
hub.BroadcastMessage(conversationID, "new_message", msg)
}
```
**推送过程**
```
1. CreateMessage 调用 hub.BroadcastMessage()
2. Hub 收到消息,放到 broadcast 通道
3. Hub.Run() 循环检测到有新消息
4. 找到这个对话的所有客户端
5. 把消息发送给每个客户端(通过 client.send 通道)
6. 每个客户端的 WritePump() 收到消息,通过 WebSocket 发送给前端
```
## 四、前端 WebSocket 工作原理
### 1. WebSocket 客户端类
**类比**:就像"对讲机"
```typescript
// frontend/lib/websocket.ts
class WSClient {
private ws: WebSocket | null = null;
private conversationId: number;
private onMessage?: (message: WSMessage) => void;
// 连接
connect() {
// 1. 创建 WebSocket 连接
const wsUrl = "ws://127.0.0.1:8080/ws?conversation_id=123";
this.ws = new WebSocket(wsUrl);
// 2. 设置事件监听
this.ws.onopen = () => {
console.log("连接成功");
};
this.ws.onmessage = (event) => {
// 收到消息
const message = JSON.parse(event.data);
if (this.onMessage) {
this.onMessage(message);
}
};
this.ws.onerror = (error) => {
console.error("连接错误", error);
};
this.ws.onclose = () => {
console.log("连接关闭");
// 尝试重连
this.attemptReconnect();
};
}
}
```
**关键点**
- `new WebSocket(url)`:创建连接(会自动发送 HTTP 升级请求)
- `ws.onopen`:连接成功时触发
- `ws.onmessage`:收到消息时触发
- `ws.onclose`:连接关闭时触发
### 2. 在 React 组件中使用
```typescript
// frontend/app/chat/page.tsx
useEffect(() => {
if (conversationId === null) return;
// 创建 WebSocket 客户端
const wsClient = new WSClient({
conversationId: conversationId,
// 收到消息时的回调
onMessage: (message) => {
if (message.type === "new_message") {
// 把新消息添加到消息列表
setMessages((prevMessages) => {
const newMsg = message.data;
// 检查是否已存在(避免重复)
const exists = prevMessages.some((msg) => msg.id === newMsg.id);
if (exists) return prevMessages;
// 添加到列表
return [...prevMessages, newMsg];
});
}
},
});
// 建立连接
wsClient.connect();
// 组件卸载时断开连接
return () => {
wsClient.disconnect();
};
}, [conversationId]);
```
**工作流程**
1. 页面加载时,创建 WebSocket 客户端
2. 建立连接(自动发送请求到后端)
3. 收到消息时,自动更新 React 状态(`setMessages`
4. React 自动重新渲染,显示新消息
5. 页面关闭时,断开连接
## 五、完整的消息流程(从发送到接收)
### 场景:访客发送消息,客服立即看到
```
步骤 1:访客发送消息
前端 → [HTTP POST /messages] → 后端
{
conversation_id: 123,
content: "你好",
sender_is_agent: false
}
步骤 2:后端处理
后端 → 保存到数据库 → 调用 hub.BroadcastMessage()
步骤 3Hub 广播消息
Hub → 找到对话 123 的所有客户端(访客和客服)
→ 通过 WebSocket 发送给每个客户端
步骤 4:前端接收消息
访客端:收到消息 → 自动显示(自己发的)
客服端:收到消息 → 自动显示(访客发的)
步骤 5:完成
访客看到自己的消息
客服立即看到访客的消息(无需刷新!)
```
## 六、WebSocket vs HTTP
### HTTP(传统方式)
```
访客发送消息:
前端 → [HTTP POST] → 后端 → 保存 → 返回成功
客服查看消息:
前端 → [HTTP GET /messages] → 后端 → 返回所有消息
(需要手动刷新或定时轮询)
```
**问题**
- 客服不知道什么时候有新消息
- 必须定时刷新(浪费资源)
- 不是实时的
### WebSocket(实时方式)
```
访客发送消息:
前端 → [HTTP POST] → 后端 → 保存 → [WebSocket 推送] → 客服端
客服查看消息:
客服端 → [WebSocket 连接] → 后端
后端 → [有新消息时自动推送] → 客服端 → 自动显示
```
**优势**
- 实时推送,无需刷新
- 节省资源(不需要定时轮询)
- 双向通信(可以前后端互相发送)
## 七、技术细节
### 1. 连接升级(Upgrade
**HTTP 请求头**
```
GET /ws?conversation_id=123 HTTP/1.1
Host: 127.0.0.1:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
```
**HTTP 响应头**
```
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
```
**101 状态码**:表示协议切换成功(从 HTTP 切换到 WebSocket
### 2. 心跳检测(Ping/Pong
**为什么需要心跳**
- 保持连接活跃(防止被防火墙关闭)
- 检测连接是否断开
**工作原理**
```
后端 → [每 54 秒发送 Ping] → 前端
前端 → [收到 Ping,回复 Pong] → 后端
```
如果前端没有回复 Pong,后端就知道连接断开了。
### 3. 自动重连
```typescript
// frontend/lib/websocket.ts
private attemptReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
return; // 超过最大重试次数,停止重连
}
this.reconnectAttempts++;
setTimeout(() => {
this.connect(); // 重新连接
}, 3000); // 等待 3 秒后重连
}
```
**重连策略**
- 连接断开时自动尝试重连
- 最多重试 5 次
- 每次等待 3 秒
## 八、记忆口诀
1. **WebSocket = 对讲机**:一直保持连接,随时可以说话
2. **HTTP = 打电话**:每次都要重新拨号
3. **Hub = 会议室管理员**:管理所有连接的人
4. **Client = 会议室里的人**:每个人都有自己的连接
5. **Upgrade = 开门**:把 HTTP 连接"升级"成 WebSocket
6. **Ping/Pong = 心跳**:保持连接活跃
7. **Broadcast = 广播**:把消息发送给所有人
## 九、常见问题
### 1. WebSocket 和 HTTP 可以同时使用吗?
**可以!** 就像:
- HTTP:用来发送消息、获取列表等
- WebSocket:用来接收实时推送
### 2. 为什么需要心跳检测?
**防止连接被关闭**
- 有些防火墙/代理会关闭长时间不活动的连接
- 心跳告诉它们"我还活着"
### 3. 如果 WebSocket 连接失败怎么办?
**自动降级**
- 可以继续使用 HTTP 轮询(定时刷新)
- 或者显示错误提示,让用户手动刷新
### 4. WebSocket 比 HTTP 快吗?
**不一定**
- WebSocket 的优势是"实时推送",不是速度
- 第一次建立连接需要时间
- 但之后推送消息非常快(不需要重新建立连接)
## 十、代码示例总结
### 后端关键代码
```go
// 1. 创建 Hub
wsHub := websocket.NewHub()
go wsHub.Run() // 启动 Hub
// 2. 注册 WebSocket 路由
r.GET("/ws", websocket.HandleWebSocket(wsHub))
// 3. 推送消息
hub.BroadcastMessage(conversationID, "new_message", msg)
```
### 前端关键代码
```typescript
// 1. 创建客户端
const wsClient = new WSClient({
conversationId: conversationId,
onMessage: (message) => {
// 处理收到的消息
},
});
// 2. 建立连接
wsClient.connect();
// 3. 断开连接(组件卸载时)
return () => {
wsClient.disconnect();
};
```
---
## 总结
**WebSocket 就像"对讲机"**
- 建立连接后,一直保持
- 后端可以随时"喊话"给前端
- 前端可以随时"回复"给后端
- 真正的实时双向通信!
**我们项目的实现**
- 后端用 Hub 管理所有连接
- 消息创建后自动推送
- 前端自动接收并显示
- 无需手动刷新,真正的实时体验!
**记住**WebSocket 不是用来替代 HTTP 的,而是用来补充 HTTP 的实时推送能力!
---
**学习建议**
1. 先理解整体流程(连接 → 发送 → 接收)
2. 再看代码实现(Hub、Client、Handler
3. 运行项目,观察日志和浏览器控制台
4. 尝试修改代码,看看效果
**调试技巧**
- 打开浏览器控制台(F12),查看 WebSocket 连接状态
- 查看后端日志,看连接和消息推送情况
- 使用 `ws.onmessage` 打印收到的消息
祝你学习愉快!🚀
-337
View File
@@ -1,337 +0,0 @@
# 前端聊天页面 - 学习笔记(通俗版)
## 一、核心概念理解
### 1. useState(数据盒子)
**类比**:就像一个小盒子,可以存东西,也可以拿出来
- `const [数据, 改数据的函数] = useState(初始值)`
- **例子**`const [input, setInput] = useState("")`
- `input` = 盒子里现在装的是什么(当前值)
- `setInput` = 改变盒子里东西的函数
- `""` = 盒子一开始是空的
**为什么需要?**
- 数据变了,页面自动更新(不用手动刷新)
### 2. useEffect(监听器)
**类比**:就像"当...的时候,执行..."
- `useEffect(() => { 做什么 }, [监听谁])`
- `[]` = 空数组,意思是"只在页面第一次加载时执行一次"
- `[某个数据]` = 当这个数据改变时执行
**为什么需要?**
- 页面加载时自动执行某些操作(比如拉取数据)
- 当数据改变时,自动执行某些操作(比如自动滚动)
### 3. useRef(指针/书签)
**类比**:就像书签,可以指向页面上某个元素
- `const ref = useRef(null)`
- `ref.current` = 指向的那个元素
**为什么需要?**
- 实现自动滚动(找到消息列表底部,滚动到那里)
### 4. async/await(等待)
**类比**:就像"等外卖"
- `async` = 这是一个需要等待的函数
- `await` = 在这里等,等到了才继续
- `fetch` = 发送网络请求(就像点外卖)
**为什么需要?**
- 网络请求需要时间,不能立即返回,所以要等
### 5. useRouter(路由导航)
**类比**:就像地图导航,告诉浏览器"我要去哪里"
- `const router = useRouter()` = 获取路由对象
- `router.push("/路径")` = 跳转到指定页面
- `router.replace("/路径")` = 跳转并替换历史记录(不能返回)
**为什么需要?**
- 登录成功后需要跳转到其他页面
- 未登录时需要跳转到登录页
### 6. WebSocket(实时通信)
**类比**:就像对讲机,可以实时双向通信
- `WSClient` = WebSocket 客户端类,封装连接逻辑
- `wsClient.connect()` = 建立连接
- `wsClient.disconnect()` = 断开连接
- `onMessage` = 收到消息时的回调函数
**为什么需要?**
- HTTP 只能客户端主动请求,不能服务器主动推送
- WebSocket 可以服务器主动推送消息,实现实时通信
- 新消息自动显示,无需手动刷新
### 7. 多状态管理(复杂组件)
**类比**:就像管理多个盒子,每个盒子存不同的数据
- 对话列表状态:`const [conversations, setConversations] = useState([])`
- 选中对话状态:`const [selectedConversationId, setSelectedConversationId] = useState(null)`
- 消息列表状态:`const [messages, setMessages] = useState([])`
- 输入框状态:`const [input, setInput] = useState("")`
**为什么需要?**
- 复杂页面需要管理多个数据
- 每个数据独立管理,互不干扰
- 数据改变时,只更新相关的 UI 部分
## 二、常用英文单词记忆
| 英文 | 中文意思 | 记忆技巧 |
|------|---------|---------|
| **state** | 状态 | 记住:state = 状态(盒子里的数据状态) |
| **effect** | 效果/影响 | 记住:effect = 当...的时候产生的效果 |
| **ref** | 引用/指针 | 记住:ref = reference(引用),指向某个东西 |
| **async** | 异步的 | 记住:async = asynchronized(异步),需要等待 |
| **await** | 等待 | 记住:await = wait(等待) |
| **fetch** | 获取 | 记住:fetch = 去拿(去后端拿数据) |
| **try** | 尝试 | 记住:try = 尝试(试试看能不能成功) |
| **catch** | 抓住 | 记住:catch = 抓住(如果出错了,抓住错误) |
| **finally** | 最终 | 记住:finally = 最终(最后一定要做的事) |
| **preventDefault** | 阻止默认 | 记住:prevent(阻止)+ default(默认)= 阻止默认行为 |
| **router** | 路由 | 记住:router = 路由器(导航到不同页面) |
| **push** | 推送/跳转 | 记住:push = 推(推送到新页面) |
| **replace** | 替换 | 记住:replace = 替换(替换当前页面) |
| **params** | 参数 | 记住:params = parameters(参数),URL 中的参数 |
| **websocket** | WebSocket | 记住:websocket = 实时通信协议(双向通信) |
| **client** | 客户端 | 记住:client = 客户端(WebSocket 客户端) |
| **connect** | 连接 | 记住:connect = 连接(建立 WebSocket 连接) |
| **disconnect** | 断开 | 记住:disconnect = 断开(断开 WebSocket 连接) |
| **callback** | 回调 | 记住:callback = 回调(收到消息时执行的函数) |
| **layout** | 布局 | 记住:layout = 布局(页面布局结构) |
| **dashboard** | 仪表盘 | 记住:dashboard = 仪表盘(工作台页面) |
| **hook** | 钩子 | 记住:hook = React 自定义逻辑的钩子函数 |
| **service** | 服务 | 记住:service = 统一封装接口请求的模块 |
| **module** | 模块 | 记住:module = 一组功能组成的模块化单元 |
## 三、代码执行流程(就像讲故事)
### 访客端流程:
1. **页面加载**
- 检查浏览器里有没有访客ID
- 如果没有,生成一个新的ID存起来
2. **有了访客ID后**
- 打电话给后端:"给我一个对话ID"
- 后端回复:"你的对话ID是123"
3. **有了对话ID后**
- 自动拉取这个对话的所有消息
- 显示在页面上
4. **用户发送消息**
- 用户点击"发送"按钮
- 把消息内容发送给后端
- 后端保存成功
- 重新拉取消息(能看到刚发的)
- 自动滚动到底部
5. **每次消息更新**
- 自动滚动到底部(让用户看到最新消息)
### 客服端流程(旧版,已废弃):
1. **登录页面加载**
- 用户输入用户名和密码
- 点击"登录"按钮
2. **登录请求**
- 发送登录请求到后端
- 后端验证用户名和密码
- 返回用户信息(user_id、username、role
3. **登录成功**
- 保存用户信息到 localStorage
- 跳转到对话列表页面
4. **对话列表页面**
- 检查是否已登录(检查 localStorage
- 未登录则跳转到登录页
- 已登录则拉取所有未关闭的对话
- 显示对话列表
5. **进入聊天页面**
- 点击对话,跳转到 `/agent/chat/[conversationId]`
- 从 URL 参数获取对话ID
- 拉取该对话的所有消息
- 客服消息显示在右侧,访客消息显示在左侧
6. **发送消息**
- 客服输入消息,点击"发送"
- 发送时设置 `sender_is_agent: true`
- 消息显示在右侧(蓝色气泡)
### 客服端流程(新版,四栏布局):
1. **登录页面加载**
- 用户输入用户名和密码
- 点击"登录"按钮
2. **登录请求**
- 发送登录请求到后端
- 后端验证用户名和密码
- 返回用户信息(user_id、username、role
3. **登录成功**
- 保存用户信息到 localStorage
- 跳转到 `/agent/dashboard`(四栏布局工作台)
4. **工作台页面加载**
- 检查是否已登录(检查 localStorage
- 未登录则跳转到登录页
- 已登录则拉取所有未关闭的对话
- 显示在左侧对话列表栏
5. **选择对话**
- 点击左侧对话列表中的某个对话
- 更新 `selectedConversationId` 状态
- 自动拉取该对话的所有消息
- 建立 WebSocket 连接,接收实时消息
- 中间栏显示聊天内容
- 右侧栏显示访客详情
6. **发送消息**
- 在中间栏输入框输入消息
- 点击"发送"按钮
- 发送时设置 `sender_is_agent: true`
- 消息通过 WebSocket 实时显示在右侧(蓝色气泡)
7. **实时接收消息**
- WebSocket 接收到新消息
- 自动更新消息列表
- 自动滚动到底部
- 无需手动刷新
## 四、模块化拆分(2025-11
> 拆分后的代码更像乐高积木,每一块负责自己的事情,组合起来就是完整的客服工作台。
- **页面层(app/agent/dashboard/page.tsx**
- 只做一件事:渲染 `<DashboardShell />`
- 没有业务逻辑,后续做 SSR / Route Handlers 时更轻松
- **组件层(components/dashboard/**
- `DashboardShell`:整合左中右三栏 + 顶部导航
- `NavigationSidebar / ConversationSidebar / MessageList / VisitorDetailPanel`:界面分块清晰,可复用
- `VisitorDetailPanel` 内置联系人信息编辑弹窗,点击“+ Add / 编辑”即可修改邮箱、电话、备注
- 样式问题在各自组件内部解决,互不影响
- `MessageList` 通过 `currentUserIsAgent` 参数兼容客服/访客视角
- **Hook 层(features/agent/hooks/**
- `useAuth`:登录信息获取 + 退出登录
- `useConversations`:对话列表、防抖搜索、未读数更新
- `useMessages`:消息拉取、已读状态、WebSocket 回调,新增 `updateContactInfo` 用于保存邮箱/电话/备注
- `useWebSocket`:封装连接/断开/错误处理
- **Service 层(features/agent/services/**
- 所有 `fetch` 请求集中在这里,例如 `conversationApi.ts``messageApi.ts``authApi.ts`
- 新增 `updateConversationContact` 方法,调用 `PUT /conversations/:id/contact` 更新访客联系信息
- 后续接入 React Query / SWR 时,只需要在这里改
- **工具层(utils/**
- `format.ts`:统一时间和消息预览格式
- `highlight.tsx`:关键词高亮组件化
- `storage.ts`localStorage 读写统一封装
> 小结:页面调用 HookHook 使用 ServiceService 请求后端;UI 部分由组件层独立负责。以后要换样式或替换数据源,都有明确位置可以下手。
### 目录与关键文件(2025-11
| 层级 | 目录 / 文件 | 作用说明 |
|------|-------------|----------|
| 页面入口 | `app/page.tsx` | 客服登录页,登录成功后跳转工作台 |
| 页面入口 | `app/agent/dashboard/page.tsx` | 工作台入口,渲染 `DashboardShell` |
| 页面入口 | `app/agent/chat/[conversationId]/page.tsx` | 旧单聊页面,复用新版组件与 Hook |
| 页面入口 | `app/chat/page.tsx` | 访客端聊天页面,复用统一组件/服务 |
| 组件层 | `components/dashboard/*` | `DashboardShell`、导航栏、会话列表、消息列表、访客详情、输入框等 UI 组件 |
| Hook 层 | `features/agent/hooks/useAuth` | 处理登录态:读取/清理 localStorage,提供退出方法 |
| Hook 层 | `features/agent/hooks/useConversations` | 统一管理会话列表、搜索、防抖、排序、选中会话 |
| Hook 层 | `features/agent/hooks/useMessages` | 统一管理消息、已读状态、详情数据、WebSocket 回调 |
| Hook 层 | `features/agent/hooks/useWebSocket` | 对 `WSClient` 的通用封装,负责连接/断开 |
| Service | `features/agent/services/conversationApi` | 会话相关接口封装:列表、搜索、详情、更新访客联系信息 |
| Service | `features/agent/services/messageApi` | 消息接口封装:拉取、发送、已读 |
| Service | `features/agent/services/authApi` | 登录态接口(目前仅登出) |
| Service | `features/visitor/services/conversationApi` | 访客端对话初始化接口(收集 UA、语言等) |
| 类型定义 | `features/agent/types.ts` | 会话、消息、用户、WebSocket 负载等公共类型 |
| 工具 | `utils/format.ts` | 时间格式化、消息预览截断 |
| 工具 | `utils/highlight.tsx` | 关键词高亮渲染(返回 `<mark>` |
| 工具 | `utils/storage.ts` | localStorage 读写封装(获取/设置/清理客服账号) |
| 工具 | `lib/websocket.ts` | `WSClient`:负责连接、自动重连、消息广播 |
> 访客与客服共用同一套消息组件/服务:通过 `currentUserIsAgent`、`senderIsAgent` 等参数切换左右气泡与已读对勾。
## 五、为什么这样写?
### 为什么用 useState
- 如果不用:数据变了,页面不会自动更新,要手动刷新
- 用了:数据一变,页面自动刷新
### 为什么用 useEffect
- 如果不用:需要手动点击按钮才能拉取数据
- 用了:页面加载时自动拉取数据
### 为什么用 async/await
- 如果不用:代码不会等网络请求完成,可能会出错
- 用了:等网络请求完成再继续,保证顺序
### 为什么用 try/catch
- 如果不用:网络断了,程序会崩溃
- 用了:即使出错,也能显示错误信息,程序不崩溃
## 五、记忆口诀
1. **useState**:数据盒子,存和改
2. **useEffect**:当...的时候,做...
3. **useRef**:书签指针,找元素
4. **async/await**:等待外卖,别着急
5. **try/catch**:试试看,错了别慌
6. **useRouter**:地图导航,跳页面
7. **localStorage**:浏览器存储,存数据
8. **URL参数**:从路径拿数据,用 useParams
9. **WebSocket**:对讲机,实时通信
10. **多状态**:多个盒子,各管各的
11. **布局**:四栏布局,各司其职
## 六、常见错误和解决
1. **忘记写 await**
- 错误:`fetch(...)` 后面直接用 `.json()`
- 正确:`await fetch(...)` 然后 `await res.json()`
2. **useEffect 依赖写错**
- 错误:`[]` 写成了 `[data]`,导致无限循环
- 正确:想清楚"当谁改变时才执行"
3. **忘记检查数据是否存在**
- 错误:直接用 `data.id`,如果 data 是 null 就报错
- 正确:先检查 `if (data)` 再使用
4. **忘记检查登录状态**
- 错误:直接访问需要登录的页面,没有检查是否已登录
- 正确:在页面加载时检查 localStorage 中是否有登录信息
5. **路由跳转问题**
- 错误:使用 `window.location.href` 跳转(会刷新页面)
- 正确:使用 `router.push()` 跳转(不会刷新,更流畅)
6. **WebSocket 连接问题**
- 错误:在组件每次渲染时都创建新连接(导致连接泄漏)
- 正确:在 `useEffect` 中创建连接,在清理函数中断开连接
- 注意:对话切换时,需要断开旧连接,建立新连接
7. **状态管理问题**
- 错误:把所有数据放在一个状态里(难以管理)
- 正确:按功能拆分状态(对话列表、选中对话、消息列表等)
- 注意:状态更新时,只更新相关的 UI 部分
8. **对话切换问题**
- 错误:切换对话时不清空消息列表(可能显示错误的消息)
- 正确:切换对话时先清空消息列表,再加载新对话的消息
## 七、练习建议
1. **多写注释**:每行代码都写注释,解释"为什么要这样写"
2. **画流程图**:把代码执行流程画出来,理清思路
3. **小步调试**:每次只改一小部分,测试看看效果
4. **多看报错**:出错时看报错信息,学会查问题
---
**记住**:编程就像学开车,一开始慢,多练习就熟了!🚀
-458
View File
@@ -1,458 +0,0 @@
# 后端学习笔记(Go + Gin + GORM
## 一、核心概念理解
### 1. Gin 框架(Web框架)
**类比**:就像餐厅的服务员,接收客人的请求,然后告诉厨房(处理函数)做什么菜
- `gin.Default()` = 创建一个 Gin 服务器(就像开一家餐厅)
- `r.POST("/路径", 处理函数)` = 注册一个路由(就像在菜单上写:客人点这个菜,叫这个厨师做)
- `r.Run(":8080")` = 启动服务器,监听 8080 端口(就像开门营业)
**为什么需要?**
- 没有框架:需要自己处理 HTTP 请求、解析 JSON、路由等(很麻烦)
- 有了框架:只需要写业务逻辑,框架帮你处理其他事情(简单)
### 2. GORMORM框架)
**类比**:就像翻译官,帮你把 Go 代码翻译成 SQL 语句
- `db.Create(&user)` = 插入数据(翻译成 `INSERT INTO users ...`
- `db.Where("username=?", name).First(&user)` = 查询数据(翻译成 `SELECT * FROM users WHERE username=?`
- `db.AutoMigrate(&User{})` = 自动创建表(根据结构体自动建表)
**为什么需要?**
- 不用 GORM:需要手写 SQL 语句(容易出错,不安全)
- 用了 GORM:用 Go 代码操作数据库,自动生成 SQL(安全、简单)
### 3. 环境变量(.env 文件)
**类比**:就像餐厅的配置表,记录"数据库地址"、"密码"等敏感信息
- `godotenv.Load()` = 从 `.env` 文件读取配置
- `os.Getenv("DB_HOST")` = 获取环境变量的值
**为什么需要?**
- 不写在代码里:密码等信息不会泄露,不同环境可以不同配置
- 写在代码里:密码硬编码,不安全,换环境要改代码
### 4. 中间件(Middleware
**类比**:就像餐厅的"检查站",每个请求都要经过这些检查
- `r.Use(middleware.Logger())` = 记录每个请求的日志(就像记录每个客人点了什么)
- `r.Use(middleware.CORS())` = 允许跨域请求(就像允许其他网站访问我们的 API)
**为什么需要?**
- 每个路由都要写日志、跨域处理等代码(重复)
- 用中间件:统一处理,代码简洁
### 5. Handler(处理函数)
**类比**:就像餐厅的厨师,负责处理具体的业务逻辑
- `func Register(db *gorm.DB) gin.HandlerFunc` = 注册用户的处理函数
- `c.ShouldBindJSON(&u)` = 把请求的 JSON 数据解析成结构体
- `c.JSON(200, gin.H{"message": "成功"})` = 返回 JSON 响应给前端
**为什么需要?**
- 接收前端请求 → 处理业务逻辑 → 返回结果
### 6. 初始化默认数据
**类比**:就像餐厅开业时,自动准备一些基础食材
- `initDefaultAdmin(db)` = 在首次启动时自动创建默认管理员账号
- 检查数据库是否已有管理员,如果没有则创建
**为什么需要?**
- 系统首次启动时,需要有一个管理员账号才能登录
- 避免手动创建管理员账号的麻烦
## 二、常用英文单词记忆
| 英文 | 中文意思 | 记忆技巧 |
|------|---------|---------|
| **handler** | 处理函数 | 记住:handler = 处理者(处理请求的人) |
| **middleware** | 中间件 | 记住:middle(中间)+ ware(件)= 中间件 |
| **router** | 路由 | 记住:router = 路由器(把请求路由到对应的处理函数) |
| **context** | 上下文 | 记住:context = 上下文(包含请求和响应的信息) |
| **migrate** | 迁移 | 记住:migrate = 迁移(把结构体迁移成数据库表) |
| **query** | 查询 | 记住:query = 查询(从数据库查数据) |
| **bind** | 绑定 | 记住:bind = 绑定(把 JSON 绑定到结构体) |
| **hash** | 哈希 | 记住:hash = 哈希(密码加密) |
| **bcrypt** | 加密算法 | 记住:bcrypt = 密码加密算法(B + crypt = 加密) |
| **env** | 环境变量 | 记住:env = environment(环境)的缩写 |
| **error** | 错误 | 记住:error = 错误(Go 语言中常用) |
| **return** | 返回 | 记住:return = 返回(返回结果或退出函数) |
## 三、代码执行流程(就像讲故事)
### 1. 服务器启动(main.go
```
1. 加载 .env 文件(读取数据库配置)
2. 连接数据库(NewDB
3. 自动创建表(AutoMigrate
4. 初始化默认管理员(initDefaultAdmin
5. 创建 Gin 服务器(gin.Default
6. 注册中间件(Logger、CORS
7. 注册路由(POST /login, POST /conversation/init, GET /conversations, PUT /conversations/:id/contact 等)
8. 启动服务器(监听 8080 端口)
```
### 2. 处理一个请求(比如登录)
```
1. 前端发送 POST /login 请求
2. Gin 框架接收请求
3. 经过中间件(记录日志、允许跨域)
4. 找到对应的处理函数(Login)
5. 解析 JSON 数据(ShouldBindJSON
6. 查询用户是否存在(db.Where
7. 验证密码(bcrypt.CompareHashAndPassword
8. 返回 JSON 响应(包含 user_id、username、role
9. 前端收到响应,保存到 localStorage
```
### 3. 处理一个请求(比如获取对话列表)
```
1. 前端发送 GET /conversations 请求
2. Gin 框架接收请求
3. 经过中间件(记录日志、允许跨域)
4. 找到对应的处理函数(ListConversations
5. 查询数据库(db.Where("status != ?", "closed")
6. 按更新时间倒序排列(Order("updated_at desc")
7. 返回 JSON 响应(对话列表)
8. 前端收到响应,显示对话列表
```
### 4. 服务器首次启动流程
```
1. 加载 .env 文件
2. 连接数据库(NewDB
3. 自动创建表(AutoMigrate
4. 检查是否有管理员账号(initDefaultAdmin
5. 如果没有,创建默认管理员(admin/admin123
6. 创建 Gin 服务器
7. 注册路由和中间件
8. 启动服务器
```
### 5. 数据库操作流程
```
1. 定义结构体(models.User
2. GORM 自动创建表(AutoMigrate
3. 查询:db.Where("条件").First(&结果)
4. 插入:db.Create(&数据)
5. 更新:db.Save(&数据)
6. 删除:db.Delete(&数据)
```
## 四、为什么这样写?
### 为什么用 Gin 框架?
- 如果不用:需要自己处理 HTTP 协议、解析请求、路由等(很复杂)
- 用了:只需要写业务逻辑,框架帮你处理其他事情(简单高效)
### 为什么用 GORM
- 如果不用:需要手写 SQL,容易出错,有 SQL 注入风险
- 用了:用 Go 代码操作数据库,自动生成安全的 SQL(安全、简单)
### 为什么用环境变量?
- 如果不用:密码硬编码在代码里,不安全,换环境要改代码
- 用了:配置和代码分离,安全,灵活
### 为什么用中间件?
- 如果不用:每个路由都要写日志、跨域等代码(重复)
- 用了:统一处理,代码简洁,易维护
### 为什么用 bcrypt 加密密码?
- 如果不用:密码明文存储,泄露了别人就能登录
- 用了:密码加密存储,即使泄露也无法还原(只能验证)
## 五、代码结构说明
### 项目结构
```
backend/
├── main.go # 入口:环境加载、依赖初始化、路由注册
├── controller/ # HTTP 层:接收请求、参数校验、调用业务服务
├── service/ # 业务层:会话/消息/认证逻辑,使用仓储与 WebSocket
├── service/types.go # 业务层通用请求/返回结构体
├── repository/ # 仓储层:封装 GORM 操作(User/Conversation/Message
├── router/ # 路由注册集中处(RegisterRoutes
├── utils/ # 工具函数(请求信息解析、UA 解析等)
├── websocket/ # Hub/Client/Handler:管理长连接与广播
├── infra/ # 基础设施(数据库连接 NewDB 等)
├── middleware/ # 中间件(日志、CORS)
├── models/ # 数据模型(User、Conversation、Message
└── .env # 环境变量配置
```
### 分层架构
```
main.go
↓ 初始化仓储 / 服务 / 控制器 / WebSocket Hub
router.RegisterRoutes
↓ controller 层(解析请求 → 调用 service)
↓ service 层(编排业务 → 调用 repository、websocket
↓ repository 层(GORM 操作数据库)
↓ models + 数据库
```
**关键职责拆分:**
- **controller**:只做输入输出转换,避免直接访问数据库
- **service**:编排业务流程(例如发送消息时刷新会话更新时间、广播 WebSocket)
- **repository**:封装 `FindOpenByVisitorID``ListActive``MarkMessagesRead` 等数据库操作
- **websocket**`hub.go` 管理连接字典,`client.go` 负责读写协程,`handler.go` 升级 HTTP连接
- **utils/request.go**`GetClientIP``ParseUserAgent` 等通用方法
- **service/types.go**`ConversationSummary``MessageItem` 等跨服务共享结构体
> 重构之后:新增服务时,只需新增仓储方法 + service + controller,不再在 `main.go` 里堆业务代码。
### 关键结构体与函数速查
| 名称 | 所在文件 | 作用与要点 |
|------|----------|------------|
| `controller.AuthController` | `controller/auth_controller.go` | 处理登录/退出,依赖 `AuthService`,统一 JSON 响应格式 |
| `controller.ConversationController` | `controller/conversation_controller.go` | 初始化会话、列表、搜索、状态更新、详情查询、更新访客联系信息 |
| `controller.MessageController` | `controller/message_controller.go` | 创建消息、拉取消息、批量已读 |
| `service.AuthService` | `service/auth_service.go` | 登录校验、账号创建(管理员) |
| `service.ConversationService` | `service/conversation_service.go` | 会话编排:初始化系统消息、列表拼装、搜索、状态/联系信息更新 |
| `service.MessageService` | `service/message_service.go` | 发送消息、拉取消息、已读逻辑 + WebSocket 广播 |
| `service.AdminService` | `service/admin_service.go` | 创建客服/管理员账号 |
| `repository.ConversationRepository` | `repository/conversation_repository.go` | 会话数据库操作:按访客查找、批量查询、更新状态/字段等 |
| `repository.MessageRepository` | `repository/message_repository.go` | 消息数据库操作:保存、按会话拉取、统计未读、标记已读 |
| `repository.UserRepository` | `repository/user_repository.go` | 用户查询/创建 |
| `router.RegisterRoutes` | `router/router.go` | 集中注册所有 REST + WebSocket 路由(含 `/conversations/:id/contact` |
| `websocket.Hub` / `Client` | `websocket/hub.go``client.go` | 维护对话房间 → 客户端列表,负责广播消息/已读事件 |
| `utils.GetClientIP` / `ParseUserAgent` | `utils/request.go` | 请求上下文工具函数,供访客信息采集、日志使用 |
| `service/types.go` | - | 定义 `ConversationSummary``CreateMessageInput` 等跨层 DTO,保持类型统一 |
## 六、常见代码模式
### 1. Handler 函数模式
```go
func Register(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. 解析请求数据
var u models.User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 2. 业务逻辑处理
// ...
// 3. 返回响应
c.JSON(200, gin.H{"message": "成功"})
}
}
```
**为什么这样写?**
- 返回 `gin.HandlerFunc`:可以传入数据库连接等参数
- `c *gin.Context`:包含请求和响应的信息
- 先检查错误,再处理:提前返回,代码清晰
### 2. 错误处理模式
```go
if err != nil {
// 处理错误
return
}
// 继续执行
```
**为什么这样写?**
- Go 语言的习惯:错误优先处理
- 提前返回:避免嵌套,代码清晰
### 3. 数据库查询模式
```go
var user models.User
if err := db.Where("username=?", name).First(&user).Error; err != nil {
// 没找到
return
}
// 找到了,使用 user
```
**为什么这样写?**
- `First`:找到第一条记录,没找到返回错误
- `&user`:传入指针,结果存到 user 里
- 检查错误:确保数据存在才继续
## 七、记忆口诀
1. **Gin**Web框架,处理请求
2. **GORM**ORM框架,操作数据库
3. **Handler**:处理函数,处理业务逻辑
4. **Middleware**:中间件,统一处理
5. **Context**:上下文,包含请求响应
6. **Error**:错误优先,提前返回
## 八、常见错误和解决
### 1. 忘记处理错误
**错误**`db.Create(&user)` 不检查错误
**正确**
```go
if err := db.Create(&user).Error; err != nil {
// 处理错误
return
}
```
### 2. 环境变量没加载
**错误**`os.Getenv("DB_HOST")` 返回空字符串
**解决**
- 检查 `.env` 文件是否存在
- 检查文件编码(不能有 BOM
- 检查 `godotenv.Load()` 是否执行
### 3. 数据库连接失败
**错误**`数据库连接失败`
**解决**
- 检查 MySQL 是否启动
- 检查 `.env` 配置是否正确
- 检查数据库是否存在
### 4. 跨域问题
**错误**:前端请求被阻止
**解决**
- 检查是否添加了 CORS 中间件
- 检查中间件配置是否正确
### 5. 密码加密问题
**错误**:密码明文存储或验证失败
**解决**
- 注册时用 `bcrypt.GenerateFromPassword` 加密
- 登录时用 `bcrypt.CompareHashAndPassword` 验证
### 6. 默认管理员创建问题
**错误**:首次启动时没有管理员账号,无法登录
**解决**
- 使用 `initDefaultAdmin` 函数在首次启动时自动创建
- 检查是否已存在,避免重复创建
## 九、调试技巧
### 1. 查看日志
- 后端启动时看日志输出
- 请求处理时看日志记录
- 错误时看错误信息
### 2. 使用 Postman 测试
- 不用写前端,直接测试 API
- 可以测试各种情况(成功、失败等)
### 3. 检查数据库
- 用 MySQL 客户端查看数据
- 确认数据是否正确保存
### 4. 打印调试信息
```go
log.Printf("调试信息: %v", 变量)
```
## 十、学习建议
1. **先理解概念**:理解框架的作用,不要死记硬背
2. **多看代码**:看别人的代码,理解写法
3. **多写注释**:每行代码都写注释,理解为什么这样写
4. **小步调试**:每次只改一小部分,测试看看效果
5. **多看错误**:出错时看错误信息,学会查问题
6. **画流程图**:把代码执行流程画出来,理清思路
## 十一、WebSocket 实时通信(已实现)
### 1. WebSocket 的作用
**类比**:就像对讲机,可以实时双向通信
- HTTP:只能客户端主动请求,服务器被动响应
- WebSocket:客户端和服务器可以随时互相发送消息
**为什么需要?**
- 新消息需要实时推送给客服,无需手动刷新
- 对话状态更新需要实时同步
- 提升用户体验,感觉更流畅
### 2. WebSocket 架构
```
Hub(中心管理器)
├── conversations(对话ID → 客户端列表)
├── register(注册新客户端)
├── unregister(注销客户端)
└── broadcast(广播消息)
Client(客户端连接)
├── hub(所属的 Hub
├── connWebSocket 连接)
├── send(发送消息的通道)
└── conversationID(所属的对话ID
```
### 3. 消息流程
```
1. 客户端建立 WebSocket 连接
2. 客户端发送对话ID,注册到 Hub
3. Hub 将客户端加入对应对话的客户端列表
4. 当有新消息时,Hub 广播给该对话的所有客户端
5. 客户端收到消息,更新 UI
```
### 4. 心跳机制
- 客户端定期发送 ping 消息
- 服务器回复 pong 消息
- 如果长时间没有收到 ping,断开连接
**为什么需要?**
- 检测连接是否断开
- 保持连接活跃
- 自动清理断开的连接
## 十二、后端分层架构(2025-11 重构版)
### 1. 目录结构(核心部分)
```
backend/
├─ main.go // 程序入口,只负责依赖注入和启动
├─ router/ // 路由注册(把 URL 映射到控制器)
├─ controller/ // 控制器层,处理 HTTP 请求/响应
├─ service/ // 业务层,编排业务逻辑
├─ repository/ // 数据访问层,封装 GORM 操作
├─ utils/ // 工具函数(IP、User-Agent 等)
└─ websocket/ // WebSocket Hub、Client 等
```
### 2. 各层职责
- **router**:统一注册所有路由,中间件分组时也只在这里改。
- **controller**:只关心“请求 → 调用服务 → 返回响应”,不会出现 SQL 或业务细节。
- **service**:聚焦业务规则,可以组合多个 repository、触发 WebSocket 推送等。
- **repository**:集中管理所有数据库读写,便于修改查询逻辑或引入缓存。
- **utils**:存放通用工具,避免重复代码。
### 3. 调用顺序
```
HTTP 请求
router → controller → service → repository
MySQL
```
(若需要 WebSocket 推送,service 会在写库后调用 `hub.BroadcastMessage`。)
### 4. 为什么这样做?
- 代码职责更清晰:想改接口,看 controller;想改业务,看 service;想改 SQL 看 repository。
- 更容易写单元测试:service 不依赖 ginrepository 可以替换成 mock。
- 方便扩展:以后要接入 gRPC/消息队列,只需在 service 层复用已有逻辑。
### 5. 迁移提示
- 新增接口时先写 service & repository,再写 controller 和 router。
- 记得运行 `gofmt`PowerShell 举例):
```powershell
cd backend
Get-ChildItem main.go, router, controller, service, repository, utils -Recurse -Filter *.go -File |
ForEach-Object { gofmt -w $_.FullName }
```
- main.go 只负责依赖注入(NewRepository → NewService → NewController)。
---
**记住**:后端开发就像搭建一个服务系统,框架帮你处理底层细节,你只需要关注业务逻辑!🚀
-404
View File
@@ -1,404 +0,0 @@
# 📋 待实现需求清单
## 第一阶段:核心功能优化(优先级:高)
### 1. UI 布局改进(客服端四栏布局)✅ 部分完成
**完成状态**
-**四栏布局基础框架**(已实现)
- ✅ 最左侧导航菜单栏(固定宽度 64px)
- ✅ 左侧对话列表栏(固定宽度 320px)
- ✅ 中间聊天内容栏(自适应宽度)
- ✅ 右侧访客详情栏(固定宽度 320px)
- ✅ 统一顶部栏高度(h-16),三栏对齐
-**左侧栏对话列表**(基础功能已实现)
- ✅ 显示所有对话
- ✅ 对话头像(圆形,基于 visitor_id 生成颜色)
- ✅ 对话ID、访客ID、状态显示
- ✅ 最后更新时间显示
- ✅ 对话选择高亮
- ✅ 搜索框("Q Search"- **已完成**(支持搜索对话内容、对话ID、访客ID)
- ✅ 最后一条消息预览 - **已完成**(显示最后一条消息的前50个字符)
- ✅ 搜索后点击对话自动定位到匹配消息 - **已完成**(居中显示历史消息,最后一条滚动到底部)
- ✅ 关键词高亮显示 - **已完成**(黄色背景高亮)
- ✅ 已读/未读状态图标 - **基础完成**(单/双对勾 + 未读徽标,未读数量统计将继续迭代)
-**中间栏聊天内容**(核心功能已实现)
- ✅ 对话标题显示
- ✅ 消息区域(滚动显示)
- ✅ 访客消息(左侧,白色气泡)
- ✅ 客服消息(右侧,蓝色气泡)
- ✅ 时间戳显示(智能格式化)
- ✅ 输入框和发送按钮
- ✅ 自动滚动到底部
- ✅ WebSocket 实时消息推送
- ❌ 系统消息显示 - **待实现**
- ❌ "last seen" 信息 - **待实现**(已显示但为静态数据)
-**右侧栏访客详情**UI 已实现,数据待完善)
- ✅ 访客头像(基于 visitor_id 生成颜色)
- ✅ 在线/离线状态显示(基于对话状态)
- ✅ 联系信息区域(邮箱、电话、备注)
- ✅ 技术信息区域(网站、来源、位置等)
- ✅ 刷新按钮和更多选项图标
- ❌ 联系信息手动添加功能 - **待实现**(只有按钮占位)
- ❌ 技术信息自动收集和显示 - **待实现**(只有占位文本)
-**无需跳转切换对话**(已实现)
- ✅ 点击对话列表切换,中间栏和右侧栏自动更新
- ✅ 对话切换时自动加载消息
- ✅ WebSocket 连接自动切换
**需求描述**(剩余部分):
- **左侧栏增强**
- 搜索框("Q Search"),支持搜索对话内容
- 最后一条消息预览
- 已读/未读状态图标(未读数量统计上限、批量操作待完善)
- **中间栏增强**
- 系统消息显示(如 "Visitor opened the page", "You joined the chat"
- 实时 "last seen" 信息
- **右侧栏数据完善**
- 联系信息手动添加/编辑功能
- 技术信息自动收集和显示
**技术要点**
- ✅ 路由已改为 `/agent/dashboard`(统一页面)
- ✅ 使用 React 状态管理当前选中的对话
- ❌ 响应式设计(移动端可能需要折叠侧栏)- **待实现**
- ✅ 左侧栏:对话列表组件
- ✅ 中间栏:聊天内容组件
- ✅ 右侧栏:访客详情组件
**预计剩余工作量**:0.5-1 周(完成搜索、系统消息、数据收集等功能)
---
### 1.1. 会话搜索功能 ✅ 已完成
**完成状态**
- ✅ 搜索框UI(已实现)
- ✅ 后端搜索接口(`GET /conversations/search?q=关键词`
- ✅ 搜索范围:消息内容、对话ID、访客ID
- ✅ 前端实时搜索(300ms防抖)
- ✅ 搜索结果高亮显示(关键词黄色背景高亮)
- ✅ 点击搜索结果自动定位到匹配消息(历史消息居中显示,最后一条滚动到底部)
- ✅ 搜索结果实时更新
**技术实现**
- 后端:`SearchConversations` 函数,支持搜索消息内容和ID
- 前端:防抖搜索、关键词高亮、消息定位
---
### 1.2. 客服个人资料管理 ✅ 已完成
**完成状态**
-**数据库**(已实现)
- ✅ User 模型增加字段:`avatar_url``nickname``email``created_at``updated_at`
-**后端**(已实现)
- ✅ 文件存储服务(`backend/infra/storage.go`):本地存储服务,可扩展为云存储
- ✅ 个人资料服务(`backend/service/profile_service.go`):提供获取、更新个人资料和上传头像功能
- ✅ 个人资料控制器(`backend/controller/profile_controller.go`):处理 HTTP 请求
- ✅ 新增接口:
-`GET /agent/profile/:user_id`:获取个人资料
-`PUT /agent/profile/:user_id`:更新个人资料(昵称、邮箱)
-`POST /agent/avatar/:user_id`:上传头像(支持 jpg、png、gif,最大 10MB
- ✅ 静态文件服务:`/uploads` 路径用于访问上传的头像等文件
-**前端**(已实现)
- ✅ 个人资料 API 服务(`frontend/features/agent/services/profileApi.ts`
- ✅ 个人资料 Hook`frontend/features/agent/hooks/useProfile.ts`):管理个人资料状态
- ✅ 个人资料弹窗组件(`frontend/components/dashboard/ProfileModal.tsx`):
- ✅ 显示和编辑昵称、邮箱
- ✅ 上传头像(支持预览、上传进度、错误提示)
- ✅ 实时更新个人资料
- ✅ DashboardHeader:显示头像和设置按钮,点击打开个人资料弹窗
- ✅ 头像工具函数(`frontend/utils/avatar.ts`):拼接完整的头像 URL、生成头像颜色、获取头像显示文本
**功能特性**
- ✅ 支持头像上传(jpg、png、gif,最大 10MB
- ✅ 支持修改昵称和邮箱
- ✅ 头像实时预览
- ✅ 如果没有上传头像,显示彩色圆形头像(基于用户ID生成颜色)
- ✅ 头像显示在 DashboardHeader 中
**技术要点**
- ✅ 文件存储采用可扩展设计,目前使用本地存储,后续可切换为云存储(OSS、S3 等)
- ✅ 头像 URL 拼接逻辑:如果后端返回相对路径,前端自动拼接 API_BASE_URL
- ✅ 头像上传支持文件类型和大小验证
- ✅ 个人资料更新实时刷新 UI
**预计工作量**:已完成
---
### 1.3. 访客信息收集和显示 ✅ 部分完成
**完成状态**
-**右侧栏 UI 显示**(已实现)
- ✅ 联系信息区域(邮箱、电话、备注)
- ✅ 技术信息区域(网站、来源、位置、语言、浏览器、操作系统、User Agent、IP地址)
- ✅ 添加按钮(邮箱、电话、备注)
-**自动收集技术信息**
- ✅ 网站(当前页面URL
- ✅ 来源(referrer,从哪个页面跳转过来)
- ✅ 浏览器信息(User Agent解析)
- ✅ 操作系统(User Agent解析)
- ✅ 语言(浏览器语言设置)
- ✅ IP地址(后端获取)
- ❌ 位置(通过IP地址定位,需要第三方服务)
-**手动添加联系信息**
- ✅ 邮箱添加/编辑功能(`PUT /conversations/:id/contact`
- ✅ 电话添加/编辑功能(`PUT /conversations/:id/contact`
- ✅ 备注添加/编辑功能(`PUT /conversations/:id/contact`
**需求描述**(剩余部分):
- **自动收集**:访客端自动收集技术信息
- 网站(当前页面URL
- 来源(referrer,从哪个页面跳转过来)
- 浏览器信息(User Agent解析)
- 操作系统(User Agent解析)
- 语言(浏览器语言设置)
- IP地址(后端获取)
- 位置(通过IP地址定位,需要第三方服务)
- **手动添加**:客服可以手动添加/编辑联系信息(已支持,后续可迭代优化校验/权限)
- **新增能力**:支持清空邮箱/电话/备注(输入空值即可)
**技术要点**
- 数据库:
- 对话表增加字段:`website`, `referrer`, `browser`, `os`, `language`, `ip_address`, `location`
- 访客表或对话表增加字段:`email`, `phone`, `notes`(客服手动添加)
- 前端(访客端):
- 页面加载时收集:`window.location.href`, `document.referrer`, `navigator.userAgent`, `navigator.language`
- 发送到后端保存
- 后端:
- 接收访客信息并保存
- 获取IP地址(从请求头)
- IP地址定位(可选,需要第三方API)
- ✅ 新增接口:`PUT /conversations/:id/contact`(更新访客邮箱/电话/备注)
- 前端(客服端):
- ✅ 右侧栏显示所有信息(UI 已实现)
- ✅ 编辑弹窗支持新增/修改邮箱、电话、备注
- ✅ 保存后实时刷新访客详情
- ❌ 更丰富的表单校验/批量编辑 - **待实现**
**预计工作量**0.5 周(完成 IP->地理位置、表单校验优化等收尾工作)
---
### 1.4. 系统消息记录 ✅ 初版完成
**完成状态**
- ✅ 消息表新增 `message_type` 字段(区分 `user_message` / `system_message`
- ✅ 新建对话时自动记录访客事件
- "Visitor opened the page [页面URL]"
- "Visitor came from [referrer URL]"(有来源时记录)
- ✅ 前端以灰色居中样式展示系统消息
- ❌ 客服行为事件(如 "You joined the chat"- 后续扩展
**后续迭代方向**
- 扩展更多系统事件(客服加入、对话状态变化等)
- 支持系统消息筛选/折叠
---
### 2. 对话状态管理(在线/离线)✅ 基础功能已完成,优化待实现
**完成状态**
-**右侧栏状态显示**(已实现,基于对话状态)
- ✅ 显示在线/离线状态(绿色圆点表示在线,灰色表示离线)
- ✅ 基于对话状态(`status === "open"` 显示在线)
-**实时状态更新**(基础功能已实现)
- ✅ 访客打开页面时自动标记为在线(WebSocket 连接建立)
- ✅ 访客关闭/离开页面时自动标记为离线(WebSocket 断开)
- ✅ 后端维护在线状态,通过 WebSocket 推送给客服端
- ✅ 实时同步状态到客服端
- ✅ 在对话列表中显示在线/离线图标(绿色圆点 = 在线)
- ✅ 记录最后活跃时间(`last_seen_at`- **已实现**(在 `ConversationDetail` 中)
**需求描述**(剩余部分):
- ❌ 通过 WebSocket 心跳机制检测在线状态(定期更新 `last_seen_at`- **待实现**
- ❌ 根据 `last_seen_at` 判断是否在线(例如,如果 `last_seen_at` 在最近 60 秒内,则认为在线)- **待实现**
- ❌ 在右侧栏显示"last seen"信息(如 "last seen today at 11:08"- **待实现**(已显示但为静态数据)
- ❌ 定期轮询对话列表,更新所有对话的状态 - **待实现**
**技术要点**
- ✅ 右侧栏状态显示(已实现,基于对话状态)
- ✅ 使用 WebSocket 检测访客在线状态 - **已实现**(连接/断开时更新状态)
- ❌ 心跳间隔:30 秒 - **待实现**
- ❌ 离线判定:断开后 60 秒标记为离线 - **待实现**(目前是断开后立即标记为离线)
- ✅ 后端维护在线状态,通过 WebSocket 推送给客服端 - **已实现**
- ✅ 记录最后活跃时间(`last_seen_at`- **已实现**
- ✅ 对话列表中显示在线/离线图标(绿色圆点 = 在线)- **已实现**
**已实现功能**
- 后端:`ConversationService.UpdateVisitorOnlineStatus``UpdateLastSeenAt` 方法
- 后端:`Hub` 回调机制,在客户端连接/断开时调用回调函数
- 后端:`Client` 添加 `isVisitor` 字段,区分访客和客服
- 后端:通过 WebSocket 广播 `visitor_status_update` 事件
- 前端:WebSocket 客户端添加 `isVisitor` 参数
- 前端:客服端接收 `visitor_status_update` 事件,刷新对话详情
- 前端:在对话列表中显示在线/离线图标
**预计工作量**:0.5 周(完成心跳机制和离线判定优化)
---
## 第二阶段:AI 功能(优先级:中高)
### 3. AI 客服功能 ❌ 待实现
**需求描述**
- 客户可以选择:人工客服 / AI 客服
- AI 客服通过 API 调用(如 OpenAI、文心一言等)
- 对话中可切换人工客服
**技术要点**
- 对话表增加字段:`service_type``human` / `ai`
- 前端:选择服务类型界面
- 后端:根据类型路由到人工或 AI 处理
- AI 处理:调用 AI API,返回回复
- 需要配置 AI API Key
**待确认**
- AI 服务商选择(OpenAI、文心一言、通义千问等)
- API Key 配置方式
- 成本控制(是否需要限制调用次数)
**预计工作量**2-3 周
---
## 第三阶段:完整功能(优先级:中)
### 4. 文件上传和图片发送 ❌ 待实现
**需求描述**
- 支持文件上传(文档、图片等)
- 支持粘贴图片发送
- 显示文件/图片预览
- 文件下载功能
**技术要点**
- 文件存储:**可扩展设计**(目前本地存储,后续可切换云存储)
- 文件大小限制:10MB
- 支持类型:图片(jpg, png, gif)、文档(pdf, doc, docx, txt
- 数据库:消息表增加 `file_url``file_type``file_name` 字段
- 后端:文件上传接口,文件存储抽象层(便于切换云存储)
- 前端:文件选择器、拖拽上传、粘贴图片监听
- 图片预览:缩略图显示
**预计工作量**1 周
---
### 5. 消息已读/未读状态 ✅ 部分完成
**完成状态**
- ✅ 数据库:消息表新增 `is_read``read_at` 字段
- ✅ 后端:提供批量标记已读接口 `PUT /messages/read`
- ✅ WebSocket:推送 `messages_read` 事件,同步状态
- ✅ 前端(客服端):聊天气泡与对话列表显示单/双对勾,系统消息灰色居中
- ✅ 前端(访客端):聊天气泡显示已读回执
- ✅ 自动标记逻辑:双方打开对话(滚动到底部)即触发已读
- ❌ 未读数量统计(对话列表数字提示)- 待实现
- ❌ 自定义已读触发条件(更精确的滚动检测)- 待优化
**后续计划**
- 统计对话未读数量并在列表展示
- 为客服提供“标记全部已读”操作
- 进一步区分系统消息、客服消息的回执逻辑
---
### 6. 事件管理页面(FAQ/知识库)❌ 待实现
**需求描述**
- 事件的增删查改
- 记录问题和答案
- 关键词组合搜索(用 `%` 分隔关键词)
- 搜索逻辑:包含所有关键词(AND 查询)
- 示例:`openai%api%调用` → 搜索包含 "openai" AND "api" AND "调用" 的记录
**技术要点**
- 数据库:事件表(`id`, `question`, `answer`, `keywords`, `created_at`, `updated_at` 等)
- 后端:CRUD 接口 + 关键词搜索接口
- 搜索范围:问题、答案、关键词字段
- 匹配方式:包含所有关键词(AND
- 前端:事件管理页面(列表、添加、编辑、删除、搜索)
- 搜索框:支持 `%` 分隔关键词
**待确认**
- 事件分类:暂时不需要
- 关联对话:是否关联到具体对话(待定)
**预计工作量**1-2 周
---
### 7. 知识库功能(AI 结合文档)❌ 待实现
**需求描述**
- 客服上传技术文档(PDF、Word、TXT、Markdown 等)
- AI 结合知识库内容回复
- 支持文档管理(上传、删除、查看)
**技术要点**
- 文档存储:本地文件系统或对象存储
- 文档处理:解析、向量化(用于检索)
- AI 回复:检索相关文档 → 结合文档内容生成回复
- 管理界面:文档上传、列表、删除
- 向量化方案:本地向量库或云服务
**待确认**
- 文档格式支持(PDF、Word、TXT、Markdown
- 向量化方案选择
- 存储方式(本地存储还是云存储)
**预计工作量**2 周
---
### 8. 用户管理功能 ❌ 待实现
**需求描述**
- 管理员对客服的增删查改
- 权限管理(角色、权限分配)
**技术要点**
- 用户列表页面
- CRUD 操作
- 权限系统(基于角色的访问控制)
- 角色:管理员、客服、只读(待定)
- 权限粒度:查看、编辑、删除、创建用户
**预计工作量**1 周
---
## 实施时间线
### 第一阶段(2-3 周)
- [ ] UI 布局改进(四栏布局)
- [x] 左侧栏:对话列表
- [x] 中间栏:聊天内容
- [x] 右侧栏:访客详情
- [ ] 会话搜索功能
- [ ] 客服个人资料管理(头像上传等)
- [ ] 访客信息收集和显示
- [ ] 自动收集技术信息
- [ ] 手动添加联系信息
- [ ] 系统消息记录
- [ ] 对话状态管理(在线/离线)
### 第二阶段(2-3 周)
- [ ] AI 客服功能
### 第三阶段(3-5 周)
- [ ] 文件上传和图片发送
- [ ] 消息已读/未读状态(未读计数待实现)
- [ ] 事件管理页面
- [ ] 知识库功能
- [ ] 用户管理功能
---
## 已确认的技术决策
1. **文件存储**:可扩展设计,目前本地存储,后续可切换云存储
2. **已读判定**:客服打开对话并滑动到底部
3. **事件搜索**:包含所有关键词(AND 查询)
4. **事件分类**:暂时不需要
5. **UI 布局**:四栏布局(最左侧导航、左侧对话列表、中间聊天内容、右侧访客详情)
6. **会话搜索**:支持搜索对话内容,定位到对应消息
7. **客服资料**:支持头像上传和个人信息修改
8. **访客信息**
- 自动收集:网站、来源、浏览器、OS、语言、IP地址、位置
- 手动添加:邮箱、电话、备注(客服可编辑)
9. **系统消息**:记录访客打开页面、来源页面、客服加入等事件
10. **头像**:访客使用默认头像(颜色区分),客服可上传头像
-982
View File
@@ -1,982 +0,0 @@
# AI-CS 智能客服系统 - 完整测试指南
> 📋 本文档用于全面测试已实现的功能和需求
>
> 最后更新:2025-11-12
## 📑 目录
- [一、准备工作](#一准备工作)
- [二、启动后端](#二启动后端)
- [三、启动前端](#三启动前端)
- [四、访客端测试流程](#四访客端测试流程)
- [测试 1:访问聊天页面](#测试-1访问聊天页面访客端)
- [测试 2:发送第一条消息](#测试-2发送第一条消息)
- [测试 3:发送多条消息](#测试-3发送多条消息)
- [测试 4:接收客服消息(实时推送)](#测试-4接收客服消息实时推送)
- [测试 5:已读状态同步](#测试-5已读状态同步)
- [测试 6:刷新页面](#测试-6刷新页面)
- [测试 7:打开新标签页](#测试-7打开新标签页验证-localstorage-共享)
- [测试 8:测试错误处理](#测试-8测试错误处理)
- [五、客服端测试流程](#五客服端测试流程)
- [测试 1:客服登录](#测试-1客服登录)
- [测试 2:客服工作台(四栏布局)](#测试-2客服工作台四栏布局)
- [测试 3:查看对话列表](#测试-3查看对话列表)
- [测试 4:选择对话](#测试-4选择对话)
- [测试 5:查看对话消息](#测试-5查看对话消息)
- [测试 6:客服发送消息](#测试-6客服发送消息)
- [测试 7:接收访客消息(实时推送)](#测试-7接收访客消息实时推送)
- [测试 8:已读状态同步](#测试-8已读状态同步)
- [测试 9:对话搜索功能](#测试-9对话搜索功能)
- [测试 10:查看访客详情](#测试-10查看访客详情)
- [测试 11:编辑联系信息](#测试-11编辑联系信息)
- [测试 12:在线/离线状态显示](#测试-12在线离线状态显示)
- [测试 13:刷新访客详情](#测试-13刷新访客详情)
- [测试 14:返回对话列表](#测试-14返回对话列表)
- [测试 15:退出登录](#测试-15退出登录)
- [测试 16:登录状态检查](#测试-16登录状态检查)
- [六、调试技巧](#六调试技巧)
- [七、常见问题](#七常见问题)
- [八、完整测试检查清单](#八完整测试检查清单)
- [九、快速测试流程(核心功能验证)](#九快速测试流程核心功能验证)
- [十、高级测试场景](#十高级测试场景)
- [场景 1:多个访客同时聊天](#场景-1多个访客同时聊天)
- [场景 2:并发消息测试](#场景-2并发消息测试)
- [场景 3:长时间连接测试](#场景-3长时间连接测试)
- [场景 4:网络中断恢复测试](#场景-4网络中断恢复测试)
- [场景 5:大量消息历史测试](#场景-5大量消息历史测试)
- [场景 6:搜索性能测试](#场景-6搜索性能测试)
- [场景 7:联系信息编辑并发测试](#场景-7联系信息编辑并发测试)
- [场景 8:在线状态多访客测试](#场景-8在线状态多访客测试)
- [十一、测试结果记录](#十一测试结果记录)
---
## 一、准备工作
### ⚡ 快速开始(5分钟快速验证)
如果你想快速验证系统是否正常工作,可以按照以下步骤:
1. **启动后端**1分钟)
```bash
cd backend
go run main.go
```
看到 `🚀 服务器启动成功,监听 :8080` 表示启动成功
2. **启动前端**1分钟)
```bash
cd frontend
npm run dev
```
看到 `Ready` 表示启动成功
3. **测试访客端**1分钟)
- 访问 `http://localhost:3000/chat`
- 发送消息:"你好,这是测试消息"
- 确认消息显示在右侧(蓝色气泡)
4. **测试客服端**2分钟)
- 访问 `http://localhost:3000/`,使用 admin/admin123 登录
- 确认进入客服工作台(四栏布局)
- 确认对话列表显示刚才的对话
- 点击对话,确认消息显示在中间栏
**如果以上步骤都正常,说明系统基本功能正常!** ✅
详细测试请参考 [九、快速测试流程(核心功能验证)](#九快速测试流程核心功能验证)
---
### 1. 检查数据库配置
确保 `backend/.env` 文件存在并配置正确:
```env
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=CS
```
**注意**
- `DB_PASSWORD` 改成你的 MySQL 密码
- `DB_NAME=CS` 是数据库名,如果不存在会自动创建(需要先手动创建数据库)
### 2. 创建数据库(如果还没有)
```sql
CREATE DATABASE CS CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
## 二、启动后端
### 步骤 1:进入后端目录
```bash
cd backend
```
### 步骤 2:安装依赖(如果还没安装)
```bash
go mod tidy
```
### 步骤 3:启动后端服务
```bash
go run main.go
```
**成功标志**
- 看到类似这样的输出:
```
✅ 找到 .env 文件: D:\tools\AI-CS\backend\.env
✅ .env 文件加载成功
✅ 管理员账号已存在
[GIN-debug] POST /login --> ...
[GIN-debug] POST /logout --> ...
[GIN-debug] POST /conversation/init --> ...
[GIN-debug] GET /conversations --> ...
[GIN-debug] GET /conversations/:id --> ...
[GIN-debug] PUT /conversations/:id/contact --> ...
[GIN-debug] GET /conversations/search --> ...
[GIN-debug] POST /messages --> ...
[GIN-debug] GET /messages --> ...
[GIN-debug] PUT /messages/read --> ...
[GIN-debug] GET /ws --> ...
🚀 服务器启动成功,监听 :8080
📡 WebSocket 服务已启动,路径: /ws?conversation_id=<对话ID>
```
**如果出错**
- 数据库连接失败:检查 `.env` 配置和 MySQL 是否启动
- 端口被占用:检查 8080 端口是否被其他程序占用
## 三、启动前端
### 步骤 1:打开新的终端窗口
**重要**:后端和前端要在不同的终端窗口运行!
### 步骤 2:进入前端目录
```bash
cd frontend
```
### 步骤 3:安装依赖(如果还没安装)
```bash
npm install
```
### 步骤 4:启动前端开发服务器
```bash
npm run dev
```
**成功标志**
- 看到类似这样的输出:
```
▲ Next.js 15.5.3
- Local: http://localhost:3000
- Ready in 2.3s
```
### 步骤 5:打开浏览器
访问:`http://localhost:3000`
## 四、测试流程
### ⚠️ 重要说明
**系统包含两个端**
- **访客端** (`/chat`):不需要登录,直接访问即可使用
- **客服端** (`/` → `/agent/dashboard`):需要登录,使用管理员/客服账号
**测试时你需要分别测试两个端**
**已实现的核心功能**
- ✅ 客服工作台(四栏布局:导航栏、对话列表、聊天内容、访客详情)
- ✅ WebSocket 实时通信(消息实时推送、已读状态同步、在线状态更新)
- ✅ 对话搜索功能(支持搜索消息内容、对话ID、访客ID)
- ✅ 访客信息收集和显示(网站、来源、浏览器、操作系统、语言、IP地址)
- ✅ 联系信息编辑(邮箱、电话、备注)
- ✅ 在线/离线状态显示(实时更新)
- ✅ 已读状态同步(单对勾/双对勾)
- ✅ 系统消息显示(访客打开页面、来源信息等)
- ✅ 关键词高亮(搜索时高亮显示)
- ✅ 消息预览(对话列表显示最后一条消息)
## 四、访客端测试流程
### 测试 1:访问聊天页面(访客端)
1. 在浏览器打开 `http://localhost:3000/chat`
2. **预期结果**
- 看到顶部标题栏(渐变背景)
- 显示"访客聊天"和会话ID、访客ID(自动生成)
- 消息列表区域显示系统消息(如 "Visitor opened the page [URL]"
- 系统消息居中显示(灰色背景,圆角边框)
- 显示当前页面URL(如果有)
- 如果有来源页面,显示 "Visitor came from [referrer URL]"
- 系统消息居中显示(灰色背景,圆角边框)
- 显示来源页面URL
- 底部有输入框和"发送"按钮
- **不需要登录,直接可以使用!**
- WebSocket 连接自动建立(查看浏览器控制台,应该看到 "✅ WebSocket 连接成功: 对话ID=X"
- 后端日志显示:`✅ WebSocket 连接已建立: 对话ID=X, 是访客=true`
- 后端日志显示:`✅ 客户端已连接: 对话ID=X, 总连接数=1, 访客连接数=1`
### 测试 2:发送第一条消息
1. 在输入框输入:"你好,这是第一条消息"
2. 点击"发送"按钮
3. **预期结果**
- 输入框清空
- 消息立即出现在右侧(蓝色气泡,显示"我")
- 显示时间(如 "14:30"
- 显示已读状态(单对勾,表示已发送但未读)
- 页面自动滚动到底部
- 消息通过 WebSocket 实时推送到客服端
### 测试 3:发送多条消息
1. 连续发送 3-5 条消息
2. **预期结果**
- 每条消息都显示在右侧
- 消息按时间顺序排列
- 每次发送后自动滚动到底部
- 每条消息显示已读状态(单对勾)
### 测试 4:接收客服消息(实时推送)
1. **打开两个浏览器标签页**
- 标签页1:访客端 (`http://localhost:3000/chat`)
- 标签页2:客服端 (`http://localhost:3000/agent/dashboard`),需要先登录
2. **在客服端发送消息**:"您好,我是客服,有什么可以帮您的吗?"
3. **预期结果**(访客端):
- **无需刷新页面**,消息自动出现在左侧(白色气泡)
- 消息显示时间
- 自动滚动到底部(如果当前在底部)
- 显示已读状态(单对勾,表示客服已读)
- 这是通过 WebSocket 实时推送的!
### 测试 5:已读状态同步
1. **在客服端查看访客消息**(客服端会自动标记已读)
2. **预期结果**(访客端):
- **无需刷新页面**,已读状态从单对勾变为双对勾
- 对勾颜色变为蓝色(表示已读)
- 这是通过 WebSocket 实时推送 `messages_read` 事件实现的!
### 测试 6:刷新页面
1. 按 `F5` 刷新页面
2. **预期结果**
- 之前发送的消息还在(因为存到数据库了)
- 自动拉取历史消息
- 访客ID不变(因为存在 localStorage
- 系统消息显示(如 "Visitor opened the page"
- WebSocket 连接重新建立
### 测试 7:打开新标签页(验证 localStorage 共享)
1. 打开新的浏览器标签页,访问 `http://localhost:3000/chat`
2. **预期结果**
- **使用的是同一个访客ID**(因为 localStorage 在同一浏览器不同标签页之间共享)
- **显示同一个对话**(因为 visitor_id 相同)
- **能看到之前发送的消息**(因为是同一个对话)
- 系统消息显示(如 "Visitor opened the page"
**注意**
- 同一浏览器不同标签页 → 共享 localStorage → 同一个访客ID → 同一个对话
- 如果要测试新访客,需要:
- 使用不同的浏览器(Chrome、Edge、Firefox等)
- 或者清除浏览器缓存/localStorage
- 或者使用隐私/无痕模式
### 测试 8:测试错误处理
1. **停止后端**(在运行 `go run main.go` 的终端按 `Ctrl+C`
2. 尝试发送消息
3. **预期结果**
- 弹出错误提示:"发送消息失败,请稍后重试"
- 按钮恢复正常(不再是"发送中..."
- WebSocket 连接断开(查看浏览器控制台,应该看到 "❌ WebSocket 连接关闭"
---
## 五、客服端测试流程
### 测试 1:客服登录
1. 访问 `http://localhost:3000/`(根路径)
2. **预期结果**
- 看到客服登录页面
- 有用户名和密码输入框
- 有"登录"按钮
- 显示默认管理员账号提示(用户名: admin,密码: admin123
3. **输入登录信息**
- 用户名:`admin`
- 密码:`admin123`
4. 点击"登录"按钮
5. **预期结果**
- 登录成功,自动跳转到 `/agent/dashboard`(客服工作台)
- 浏览器 localStorage 中保存了 `agent_user_id`、`agent_username`、`agent_role`
### 测试 2:客服工作台(四栏布局)
1. 登录成功后,自动进入客服工作台
2. **预期结果**
- **最左侧导航栏**(固定宽度 64px):显示导航图标
- **左侧对话列表栏**(固定宽度 320px):显示所有对话列表
- **中间聊天内容栏**(自适应宽度):显示当前选中对话的消息
- **右侧访客详情栏**(固定宽度 320px):显示当前选中对话的访客详情
- 顶部显示当前登录的用户名和角色(如:"admin (管理员)"
- 显示"退出"按钮
### 测试 3:查看对话列表
1. 在工作台左侧查看对话列表
2. **预期结果**
- 显示所有未关闭的对话列表(如果有访客发送过消息)
- 每条对话显示:
- 对话头像(圆形,基于 visitor_id 生成颜色)
- 对话ID、访客ID
- 在线/离线状态图标(绿色圆点 = 在线)
- 状态标签("进行中"或"已关闭"
- 最后一条消息预览(显示最后一条消息的前50个字符)
- 未读消息数量(蓝色徽标,如果有未读消息)
- 已读/未读状态图标(单/双对勾)
- 最后更新时间(智能格式化:今天显示时间,更早显示日期+时间)
- 对话按更新时间倒序排列(最新的在前)
- 选中对话高亮显示(蓝色背景)
3. **如果没有对话**
- 需要先访问 `/chat` 页面(访客端)发送一些消息
- 然后刷新客服端,就能看到对话了
### 测试 4:选择对话
1. 在对话列表中,点击任意一条对话
2. **预期结果**
- 对话高亮显示(蓝色背景)
- **中间栏**自动加载该对话的消息
- **右侧栏**自动加载该对话的访客详情
- WebSocket 连接自动切换到该对话
- 无需跳转页面,所有内容在同一页面更新
### 测试 5:查看对话消息
1. 选择一条对话后,查看中间栏的消息列表
2. **预期结果**
- 显示该对话的所有消息(包括系统消息)
- **访客消息在左侧**(白色气泡)
- **客服消息在右侧**(蓝色气泡)
- 系统消息居中显示(灰色背景,如 "Visitor opened the page [URL]"
- 每条消息显示时间(智能格式化)
- 客服消息显示已读/未读状态(单/双对勾)
- 自动滚动到底部(如果是新对话)
- 消息按时间顺序排列
### 测试 6:客服发送消息
1. 在中间栏输入框输入消息:"您好,我是客服,有什么可以帮您的吗?"
2. 点击"发送"按钮
3. **预期结果**
- 输入框清空
- 消息立即出现在右侧(蓝色气泡,客服消息)
- 显示时间
- 显示已读状态(单对勾,表示已发送但未读)
- 页面自动滚动到底部
- 消息通过 WebSocket 实时推送到访客端
- 对话列表中的最后一条消息预览自动更新
### 测试 7:接收访客消息(实时推送)
1. **打开两个浏览器标签页**
- 标签页1:访客端 (`http://localhost:3000/chat`)
- 标签页2:客服端 (`http://localhost:3000/agent/dashboard`),需要先登录
2. **在访客端发送消息**:"你好,我需要帮助"
3. **预期结果**(客服端):
- **无需刷新页面**,消息自动出现在左侧(白色气泡)
- 消息显示时间
- 自动滚动到底部(如果当前在底部)
- 对话列表中的最后一条消息预览自动更新
- 未读消息数量自动更新(蓝色徽标)
- 这是通过 WebSocket 实时推送的!
### 测试 8:已读状态同步
1. **在客服端查看访客消息**(客服端会自动标记已读)
2. **预期结果**(访客端):
- **无需刷新页面**,已读状态从单对勾变为双对勾
- 对勾颜色变为蓝色(表示已读)
- 这是通过 WebSocket 实时推送 `messages_read` 事件实现的!
3. **预期结果**(客服端):
- 未读消息数量自动减少
- 已读状态图标更新(双对勾)
### 测试 9:对话搜索功能
1. 在左侧对话列表顶部,找到搜索框(显示 "Q Search" 或搜索图标)
2. **输入搜索关键词**(例如:对话ID、访客ID、消息内容)
- 例如:搜索 "你好"(如果消息中包含"你好")
- 例如:搜索对话ID "10"(如果对话ID为10
- 例如:搜索访客ID "176"(如果访客ID为176
3. **预期结果**
- 实时搜索(300ms 防抖,输入后等待 300ms 才开始搜索)
- 显示匹配的对话列表(只显示包含关键词的对话)
- 搜索结果高亮显示(关键词黄色背景高亮,使用 `<mark>` 标签)
- 点击搜索结果,自动定位到匹配消息
- 如果匹配消息在历史记录中,自动滚动到匹配消息并居中显示
- 如果匹配消息是最后一条,自动滚动到底部
- 高亮显示持续 3 秒后自动清除
- 清除搜索后,显示所有对话
- 搜索框为空时,显示所有对话
### 测试 10:查看访客详情
1. 选择一条对话后,查看右侧栏的访客详情
2. **预期结果**
- 显示访客头像(圆形,基于 visitor_id 生成颜色)
- 显示访客ID(例如:"访客 #176"
- 显示在线/离线状态(绿色圆点 = 在线,灰色 = 离线)
- 显示刷新按钮(圆形箭头图标)和更多选项图标(三个点)
- **联系信息区域**
- 邮箱(可以编辑,显示"+ Add"或"编辑"按钮)
- 电话(可以编辑,显示"+ Add"或"编辑"按钮)
- 备注(可以编辑,显示"+ Add"或"编辑"按钮)
- **技术信息区域**
- 网站(当前页面URL,如果有,显示为链接)
- 来源(referrer,从哪个页面跳转过来,如果有,显示为链接)
- 语言(浏览器语言设置,例如:"zh-CN"
- 浏览器(浏览器信息,例如:"Edge"、"Chrome"
- 操作系统(操作系统信息,例如:"Windows"、"macOS"
- IP地址(后端获取,例如:"127.0.0.1"
- 位置(暂未实现,显示"暂未收集")
- 最后活跃时间(智能格式化,例如:"刚刚"、"5分钟前"、"今天 14:30"
### 测试 11:编辑联系信息
1. 在右侧栏访客详情中,找到"联系信息"区域
2. **编辑邮箱**
- 点击"邮箱"右侧的"+ Add"或"编辑"按钮
- 弹出编辑弹窗(输入框 + "保存"和"取消"按钮)
- 输入邮箱地址(例如:`visitor@example.com`
- 点击"保存"按钮
- **预期结果**
- 编辑弹窗关闭
- 邮箱立即更新显示
- 数据保存到后端(查看后端日志,应该看到 `PUT /conversations/:id/contact` 请求)
- 刷新后数据还在
- 如果保存失败,显示错误提示
3. **编辑电话**
- 点击"电话"右侧的"+ Add"或"编辑"按钮
- 弹出编辑弹窗
- 输入电话号码(例如:`13800138000`
- 点击"保存"按钮
- **预期结果**
- 编辑弹窗关闭
- 电话立即更新显示
- 数据保存到后端
- 刷新后数据还在
4. **编辑备注**
- 点击"备注"右侧的"+ Add"或"编辑"按钮
- 弹出编辑弹窗(支持多行文本)
- 输入备注内容(例如:`重要客户,需要重点关注`
- 点击"保存"按钮
- **预期结果**
- 编辑弹窗关闭
- 备注立即更新显示(支持换行显示)
- 数据保存到后端
- 刷新后数据还在
5. **取消编辑**
- 点击编辑按钮,弹出编辑弹窗
- 修改内容(或不修改)
- 点击"取消"按钮
- **预期结果**
- 编辑弹窗关闭
- 内容不更新(保持原值)
6. **清空联系信息**
- 点击编辑按钮,弹出编辑弹窗
- 清空输入框内容
- 点击"保存"按钮
- **预期结果**
- 编辑弹窗关闭
- 联系信息清空,显示"暂未填写"
- 数据保存到后端(空值)
- 刷新后数据还在(显示"暂未填写")
### 测试 12:在线/离线状态显示
1. **访客在线时**
- 在访客端打开 `/chat` 页面
- **预期结果**(客服端):
- 对话列表中显示绿色圆点(在线)
- 右侧栏显示"● 在线"
- 这是通过 WebSocket 实时更新的!
2. **访客离线时**
- 关闭访客端页面(或关闭浏览器标签页)
- **预期结果**(客服端):
- 对话列表中绿色圆点消失(或变为灰色)
- 右侧栏显示"● 离线"
- 最后活跃时间更新
- 这是通过 WebSocket 实时更新的!
### 测试 13:刷新访客详情
1. 在右侧栏访客详情顶部,找到刷新按钮(圆形箭头图标)
2. 点击刷新按钮
3. **预期结果**
- 访客详情重新加载
- 显示最新的访客信息(包括联系信息、技术信息、最后活跃时间等)
### 测试 14:返回对话列表
1. 在工作台左侧对话列表中,点击其他对话
2. **预期结果**
- 对话切换,中间栏和右侧栏自动更新
- WebSocket 连接自动切换
- 无需跳转页面,所有内容在同一页面更新
### 测试 15:退出登录
1. 在工作台顶部,找到"退出"按钮
2. 点击"退出"按钮
3. **预期结果**
- localStorage 中的 `agent_user_id`、`agent_username`、`agent_role` 被清除
- 自动跳转到登录页面 (`/`)
4. **验证登录状态**
- 尝试直接访问 `/agent/dashboard`
- **预期结果**:自动跳转到登录页面(因为未登录)
### 测试 16:登录状态检查
1. **清除 localStorage**(在浏览器控制台执行):
```javascript
localStorage.clear()
```
2. 尝试直接访问 `/agent/dashboard`
3. **预期结果**
- 自动跳转到登录页面(因为未登录)
---
## 六、调试技巧
### 1. 打开浏览器开发者工具
按 `F12` 打开开发者工具
### 2. 查看控制台(Console
- 如果有红色错误,说明代码有问题
- 如果有 `console.error` 输出,说明后端请求失败
- 查看 WebSocket 连接状态:
- `✅ WebSocket 连接成功: 对话ID=X` → 连接成功
- `❌ WebSocket 连接关闭: 对话ID=X` → 连接断开
- `WebSocket 连接错误: {}` → 连接失败(可能是后端未启动)
### 3. 查看网络请求(Network
- 点击"发送"后,查看是否有 `POST /messages` 请求
- 查看 WebSocket 连接:找到 `ws` 或 `wss` 类型的请求
- 查看请求状态:
- `200` = 成功
- `400` = 请求参数错误
- `500` = 服务器错误
- 查看 WebSocket 消息:
- 点击 WebSocket 连接,查看 "Messages" 标签
- 应该看到 `new_message`、`messages_read`、`visitor_status_update` 等事件
### 4. 查看后端日志
在后端运行的终端窗口,查看日志输出:
- 如果看到 `✅ 客户端已连接: 对话ID=X, 总连接数=Y, 访客连接数=Z`,说明 WebSocket 连接成功
- 如果看到 `❌ 客户端已断开: 对话ID=X, 剩余访客连接数=Y`,说明 WebSocket 断开
- 如果看到 `✅ WebSocket 连接已建立: 对话ID=X, 是访客=true/false`,说明连接建立
- 如果看到 `更新访客在线状态失败: ...`,说明状态更新失败
- 如果有错误信息,说明后端处理有问题
## 七、常见问题
### 问题 1:后端启动失败
**错误**`数据库连接失败`
**解决**
- 检查 MySQL 是否启动
- 检查 `.env` 配置是否正确
- 检查数据库是否存在
### 问题 2:前端启动失败
**错误**`npm install` 失败
**解决**
- 检查 Node.js 版本(需要 18+
- 删除 `node_modules` 文件夹,重新 `npm install`
### 问题 3:页面空白
**错误**:浏览器显示空白页
**解决**
- 打开开发者工具(F12)查看错误
- 检查后端是否启动
- 检查前端是否启动在 3000 端口
### 问题 4:发送消息失败
**错误**:点击发送没反应
**解决**
- 打开浏览器控制台(F12)查看错误
- 检查后端是否启动在 8080 端口
- 检查 `frontend/lib/config.ts` 中的 API 地址是否正确
### 问题 5WebSocket 连接失败
**错误**`WebSocket 连接错误: {}`
**解决**
- 检查后端是否启动
- 检查后端是否监听 8080 端口
- 检查浏览器控制台错误信息
- 查看后端日志,确认 WebSocket 服务是否启动
### 问题 6:消息不实时推送
**错误**:发送消息后,对方看不到
**解决**
- 检查 WebSocket 连接状态(查看浏览器控制台)
- 检查后端日志,确认 WebSocket 是否正常广播
- 检查后端是否正常处理 WebSocket 消息
- 确认双方都建立了 WebSocket 连接
### 问题 7:已读状态不同步
**错误**:已读状态不更新
**解决**
- 检查 WebSocket 连接状态
- 检查后端是否正常推送 `messages_read` 事件
- 检查前端是否正常处理 `messages_read` 事件
- 查看浏览器控制台和后端日志
### 问题 8:在线状态不更新
**错误**:在线状态不实时更新
**解决**
- 检查 WebSocket 连接状态
- 检查后端是否正常推送 `visitor_status_update` 事件
- 检查前端是否正常处理 `visitor_status_update` 事件
- 查看浏览器控制台和后端日志
- 确认访客端 WebSocket 连接是否正常(`is_visitor=true`
### 问题 9:对话列表不显示
**错误**:对话列表为空
**解决**
- 确认是否有访客发送过消息
- 检查后端是否正常返回对话列表
- 查看浏览器控制台错误信息
- 检查后端日志,确认数据库查询是否正常
### 问题 10:搜索功能不工作
**错误**:搜索无结果
**解决**
- 检查搜索关键词是否正确
- 检查后端搜索接口是否正常
- 查看浏览器控制台错误信息
- 检查后端日志,确认搜索查询是否正常
## 八、完整测试检查清单
完成以下测试后,打勾:
### 一、基础功能测试
#### 访客端基础功能:
- [ ] 后端启动成功(显示监听 :8080 和 WebSocket 服务)
- [ ] 前端启动成功(显示 Ready)
- [ ] 访问 `/chat` 页面正常显示
- [ ] 自动生成/获取访客ID
- [ ] 自动初始化对话(获取对话ID)
- [ ] 显示系统消息(如 "Visitor opened the page [URL]"
- [ ] WebSocket 连接自动建立(查看浏览器控制台)
- [ ] 发送第一条消息成功
- [ ] 消息显示在右侧(蓝色气泡)
- [ ] 显示已读状态(单对勾)
- [ ] 发送后自动滚动到底部
- [ ] 输入框清空,可以继续输入
- [ ] 时间显示正确(今天的只显示时间,更早的显示日期+时间)
- [ ] 可以连续发送多条消息
- [ ] 输入框为空时,发送按钮禁用
- [ ] 刷新页面后消息还在
- [ ] 访客信息自动收集(网站、来源、浏览器、操作系统、语言、IP地址)
#### 客服端基础功能:
- [ ] 访问 `/` 显示登录页面
- [ ] 使用 admin/admin123 可以成功登录
- [ ] 登录后跳转到 `/agent/dashboard`(客服工作台)
- [ ] 四栏布局正常显示(导航栏、对话列表、聊天内容、访客详情)
- [ ] 对话列表显示所有未关闭的对话
- [ ] 对话列表显示在线/离线状态图标(绿色圆点 = 在线)
- [ ] 对话列表显示最后一条消息预览
- [ ] 对话列表显示未读消息数量(蓝色徽标)
- [ ] 对话列表显示已读/未读状态图标(单/双对勾)
- [ ] 选择对话后,中间栏和右侧栏自动更新
- [ ] WebSocket 连接自动切换
- [ ] 客服消息显示在右侧(蓝色气泡)
- [ ] 访客消息显示在左侧(白色气泡)
- [ ] 系统消息居中显示(灰色背景)
- [ ] 客服可以发送消息
- [ ] 退出登录功能正常
- [ ] 未登录访问客服页面会跳转到登录页
### 二、实时通信测试
#### WebSocket 实时推送:
- [ ] 访客发送消息后,客服端实时收到(无需刷新)
- [ ] 客服发送消息后,访客端实时收到(无需刷新)
- [ ] 消息通过 WebSocket 实时推送
- [ ] WebSocket 连接自动重连(断开后自动重连)
#### 已读状态同步:
- [ ] 客服查看访客消息后,访客端实时更新已读状态(单对勾 → 双对勾)
- [ ] 访客查看客服消息后,客服端实时更新已读状态(单对勾 → 双对勾)
- [ ] 已读状态通过 WebSocket 实时推送
- [ ] 对勾颜色正确(已读 = 蓝色,未读 = 灰色)
#### 在线状态更新:
- [ ] 访客打开页面后,客服端实时显示在线状态(绿色圆点)
- [ ] 访客关闭页面后,客服端实时显示离线状态(灰色圆点或消失)
- [ ] 在线状态通过 WebSocket 实时推送
- [ ] 最后活跃时间实时更新
### 三、搜索功能测试
#### 对话搜索:
- [ ] 搜索框UI正常显示
- [ ] 输入搜索关键词后,实时搜索(300ms 防抖)
- [ ] 搜索结果正确显示(匹配的对话列表)
- [ ] 搜索结果高亮显示(关键词黄色背景高亮)
- [ ] 点击搜索结果,自动定位到匹配消息
- [ ] 历史消息居中显示,最后一条滚动到底部
- [ ] 清除搜索后,显示所有对话
- [ ] 支持搜索消息内容、对话ID、访客ID
### 四、访客信息测试
#### 访客信息收集:
- [ ] 访客端自动收集网站(当前页面URL)
- [ ] 访客端自动收集来源(referrer)
- [ ] 访客端自动收集浏览器信息
- [ ] 访客端自动收集操作系统信息
- [ ] 访客端自动收集语言信息
- [ ] 后端自动获取IP地址
- [ ] 客服端显示所有访客信息
#### 联系信息编辑:
- [ ] 编辑邮箱功能正常(新增、修改、清空)
- [ ] 编辑电话功能正常(新增、修改、清空)
- [ ] 编辑备注功能正常(新增、修改、清空)
- [ ] 联系信息保存到后端
- [ ] 刷新后联系信息还在
- [ ] 编辑弹窗正常显示和关闭
### 五、界面交互测试
#### 滚动行为:
- [ ] 消息列表内部滚动正常(不会滚动整个页面)
- [ ] 对话列表内部滚动正常(不会滚动整个页面)
- [ ] 访客详情内部滚动正常(不会滚动整个页面)
- [ ] 发送新消息后自动滚动到底部
- [ ] 长消息历史正常滚动
#### 响应式布局:
- [ ] 四栏布局正常显示
- [ ] 各栏宽度正确(导航栏 64px,对话列表 320px,访客详情 320px,聊天内容自适应)
- [ ] 各栏高度正确(顶部栏 h-16,内容区域自适应)
- [ ] 选中对话高亮显示(蓝色背景)
### 六、错误处理测试
#### 网络错误:
- [ ] 后端停止后,发送消息显示错误提示
- [ ] WebSocket 连接断开后,显示连接关闭提示
- [ ] 网络恢复后,WebSocket 自动重连
- [ ] 错误提示清晰明确
#### 数据验证:
- [ ] 输入框为空时,发送按钮禁用
- [ ] 输入框有内容时,发送按钮启用
- [ ] 编辑联系信息时,输入验证正常
- [ ] 清空联系信息时,数据正确保存
### 七、性能测试
#### 加载性能:
- [ ] 页面加载速度正常(< 2秒)
- [ ] 对话列表加载速度正常(< 1秒)
- [ ] 消息列表加载速度正常(< 1秒)
- [ ] 访客详情加载速度正常(< 1秒)
#### 实时性能:
- [ ] 消息实时推送延迟 < 100ms
- [ ] 已读状态同步延迟 < 100ms
- [ ] 在线状态更新延迟 < 100ms
- [ ] WebSocket 连接稳定(不断开)
### 八、兼容性测试
#### 浏览器兼容:
- [ ] Chrome 浏览器正常
- [ ] Edge 浏览器正常
- [ ] Firefox 浏览器正常
- [ ] Safari 浏览器正常(如果可用)
#### 数据持久化:
- [ ] 刷新页面后,消息还在
- [ ] 刷新页面后,访客ID不变
- [ ] 刷新页面后,登录状态保持
- [ ] 联系信息编辑后,刷新后数据还在
---
## 九、快速测试流程(核心功能验证)
> 💡 如果你时间有限,可以按照以下流程快速验证核心功能是否正常
### 快速测试步骤:
1. **启动系统**5分钟)
- [ ] 启动后端:`cd backend && go run main.go`
- [ ] 启动前端:`cd frontend && npm run dev`
- [ ] 访问 `http://localhost:3000`
2. **访客端测试**5分钟)
- [ ] 访问 `http://localhost:3000/chat`
- [ ] 发送消息:"你好,这是测试消息"
- [ ] 确认消息显示在右侧(蓝色气泡)
- [ ] 确认 WebSocket 连接建立(查看浏览器控制台)
3. **客服端测试**5分钟)
- [ ] 访问 `http://localhost:3000/`,使用 admin/admin123 登录
- [ ] 确认进入客服工作台(四栏布局)
- [ ] 确认对话列表显示刚才的对话
- [ ] 点击对话,确认消息显示在中间栏
- [ ] 发送消息:"您好,我是客服"
- [ ] 确认消息显示在右侧(蓝色气泡)
4. **实时通信测试**5分钟)
- [ ] 打开两个浏览器标签页(访客端和客服端)
- [ ] 在访客端发送消息:"测试实时推送"
- [ ] 确认客服端实时收到消息(无需刷新)
- [ ] 在客服端发送消息:"测试实时推送回复"
- [ ] 确认访客端实时收到消息(无需刷新)
5. **已读状态测试**3分钟)
- [ ] 在客服端查看访客消息
- [ ] 确认访客端已读状态从单对勾变为双对勾(无需刷新)
6. **在线状态测试**3分钟)
- [ ] 确认客服端对话列表显示绿色圆点(在线)
- [ ] 关闭访客端页面
- [ ] 确认客服端绿色圆点消失(或变为灰色)
7. **搜索功能测试**2分钟)
- [ ] 在客服端搜索框输入关键词
- [ ] 确认搜索结果高亮显示
- [ ] 点击搜索结果,确认自动定位到匹配消息
8. **联系信息编辑测试**(3分钟)
- [ ] 在客服端右侧栏,点击"邮箱"右侧的"编辑"按钮
- [ ] 输入邮箱地址,点击"保存"
- [ ] 确认邮箱立即更新显示
**如果以上测试都通过,说明核心功能正常!** ✅
---
## 十、高级测试场景
### 场景 1:多个访客同时聊天
1. **打开多个浏览器**(或使用隐私/无痕模式)
- 浏览器1:访客A (`http://localhost:3000/chat`)
- 浏览器2:访客B (`http://localhost:3000/chat`)
- 浏览器3:客服端 (`http://localhost:3000/agent/dashboard`)
2. **测试流程**
- 访客A 发送消息:"我是访客A"
- 访客B 发送消息:"我是访客B"
- 客服端应该看到两个不同的对话
- 客服可以选择不同的对话进行回复
- 每个访客只能看到自己的对话
### 场景 2:并发消息测试
1. **打开两个浏览器标签页**(访客端和客服端)
2. **快速发送多条消息**
- 访客端连续发送 10 条消息
- 客服端连续发送 10 条消息
3. **预期结果**
- 所有消息都实时推送
- 消息顺序正确
- 已读状态正确同步
- 无消息丢失
- 无重复消息
### 场景 3:长时间连接测试
1. **打开访客端和客服端**
2. **保持连接 30 分钟**
3. **预期结果**
- WebSocket 连接保持稳定(不断开)
- 消息正常推送
- 已读状态正常同步
- 在线状态正常更新
- 无内存泄漏
### 场景 4:网络中断恢复测试
1. **打开访客端和客服端**
2. **断开网络**(关闭 WiFi 或拔掉网线)
3. **等待 10 秒**
4. **恢复网络**
5. **预期结果**
- WebSocket 连接自动重连
- 消息正常推送
- 已读状态正常同步
- 在线状态正常更新
### 场景 5:大量消息历史测试
1. **创建一个对话**
2. **发送 100+ 条消息**
3. **预期结果**
- 消息列表正常加载
- 滚动性能正常(不卡顿)
- 消息显示正确
- 搜索功能正常(可以搜索历史消息)
- 关键词高亮正常
### 场景 6:搜索性能测试
1. **创建多个对话**10+ 个对话)
2. **每个对话发送多条消息**10+ 条消息)
3. **测试搜索功能**
- 搜索消息内容
- 搜索对话ID
- 搜索访客ID
4. **预期结果**
- 搜索速度正常(< 500ms
- 搜索结果正确
- 关键词高亮正常
- 点击搜索结果自动定位正常
### 场景 7:联系信息编辑并发测试
1. **打开多个客服端**(使用不同的浏览器)
2. **同时编辑同一个访客的联系信息**
- 客服A 编辑邮箱
- 客服B 编辑电话
3. **预期结果**
- 数据正确保存
- 无数据冲突
- 刷新后数据正确
### 场景 8:在线状态多访客测试
1. **打开多个访客端**3+ 个访客)
2. **测试在线状态**
- 访客A 在线
- 访客B 在线
- 访客C 离线
3. **预期结果**(客服端):
- 对话列表正确显示在线/离线状态
- 在线状态实时更新
- 最后活跃时间正确更新
---
## 十一、测试结果记录
### 测试日期:__________
### 测试人员:__________
### 测试环境:
- 后端版本:__________
- 前端版本:__________
- 数据库版本:__________
- 浏览器版本:__________
### 测试结果:
- [ ] 所有测试通过
- [ ] 部分测试通过(请列出未通过的测试)
- [ ] 测试失败(请列出失败的测试)
### 问题记录:
1. __________
2. __________
3. __________
### 备注:
__________
---
**测试完成后,你就知道整个系统是否正常工作了!** 🎉
**如果所有测试都通过,恭喜你!系统已经可以正常使用了!**
-130
View File
@@ -1,130 +0,0 @@
# 系统角色说明
## 当前系统状态
### ✅ 已实现:访客端(`/chat` 页面)
**访客的特点**
- 不需要登录注册
- 访问 `/chat` 页面时自动生成访客ID
- 访客ID存储在浏览器的 localStorage 中
- 发送消息时,`sender_is_agent: false`, `sender_id: 0`
- **可以直接测试,无需任何配置!**
**访客能做什么**
- ✅ 自动创建对话
- ✅ 发送消息
- ✅ 查看历史消息
- ✅ 刷新页面后消息还在
**测试访客功能**
```bash
# 1. 启动后端和前端
# 2. 浏览器打开 http://localhost:3000/chat
# 3. 直接发送消息测试,无需登录!
```
---
### ❌ 未实现:客服端(需要单独开发)
**客服的特点**(未来功能):
- 需要登录(使用 `/register``/login` 接口)
- 登录后获取客服ID
- 发送消息时,`sender_is_agent: true`, `sender_id: 客服ID`
- 需要创建一个新的页面,比如 `/agent/chat`
**客服需要能做什么**(未来功能):
- 登录系统
- 查看所有对话列表
- 选择某个对话进行回复
- 发送消息时标识为客服
**后端已支持**
- ✅ 后端代码已经支持客服发送消息(`sender_is_agent: true`
- ✅ 后端会检查:如果是客服,`sender_id` 必须提供且不能为0
---
## 代码逻辑说明
### 后端检查逻辑(`backend/service/service.go`
```go
// 第122-125行
if req.SenderIsAgent && req.SenderID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "客服id不能为空"})
return
}
```
**含义**
- 如果 `sender_is_agent == true`(是客服),那么 `sender_id` 不能为0
- 如果 `sender_is_agent == false`(是访客),`sender_id` 可以为0
**所以访客可以直接发送消息,不需要ID!**
### 前端发送逻辑(`frontend/app/chat/page.tsx`
```typescript
// 第187-188行
sender_is_agent: false, // false = 访客发的
sender_id: 0, // 访客ID0表示不需要)
```
**这是访客端页面,所以固定为 `false` 和 `0`**
---
## 未来开发计划
### 客服端页面开发(待实现)
需要创建:
1. **客服登录页面**`/agent/login`
- 使用现有的 `/login` 接口
- 登录后保存客服ID和token
2. **客服对话列表页面**`/agent/conversations`
- 显示所有未关闭的对话
- 显示每个对话的访客ID、最后消息时间等
3. **客服聊天页面**`/agent/chat/[conversationId]`
- 类似访客聊天页面
- 发送消息时:`sender_is_agent: true`, `sender_id: 客服ID`
- 显示对话双方的消息(访客在左,客服在右)
---
## 测试建议
### 当前可以测试的(访客端)
1. ✅ 访问 `/chat` 页面
2. ✅ 发送消息(以访客身份)
3. ✅ 查看历史消息
4. ✅ 刷新页面后消息还在
### 暂时无法测试的(客服端)
1. ❌ 客服登录(页面还没创建)
2. ❌ 客服查看对话列表(页面还没创建)
3. ❌ 客服回复消息(页面还没创建)
**但你可以用两个浏览器窗口测试**
- 窗口1:访客A,发送消息
- 窗口2:访客B,查看对话(看到访客A的消息)
---
## 总结
**当前状态**
- ✅ 访客端完全可用,可以直接测试
- ❌ 客服端还未开发,需要单独实现
**测试时**
- 你就是在模拟访客角色
- 不需要任何登录或配置
- 直接访问 `/chat` 就可以发送消息!
**后端已经准备好了**,支持访客和客服两种角色,只是前端客服页面还没做而已。