📋 待实现需求清单请查看:待实现需求清单.md
avatar_url(头像URL)、nickname(昵称)、email(邮箱)、created_at、updated_atbackend/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 路径用于访问上传的头像等文件frontend/features/agent/services/profileApi.ts)frontend/features/agent/hooks/useProfile.ts):管理个人资料状态frontend/components/dashboard/ProfileModal.tsx):frontend/utils/avatar.ts):getAvatarUrl:拼接完整的头像 URLgetAvatarColor:根据种子值生成头像颜色getAvatarInitial:获取头像显示文本(首字母)disableAutoScroll={true},导致滚动监听被禁用disableAutoScroll 在滚动监听 useEffect 中的检查,即使 disableAutoScroll 为 true,也应该允许通过滚动来标记消息为已读frontend/components/dashboard/MessageList.tsx:disableAutoScroll 在第一个滚动监听 useEffect 中的检查useEffect 中的已读标记逻辑:如果用户已经在底部附近(isNearBottom),即使没有自动滚动,也应该标记为已读handleNewMessage 直接返回,不会更新消息的已读状态new_message 事件在 messages_read 事件之后到达,会覆盖已读状态handleMessagesReadBroadcast 没有检查是否有需要更新的消息,可能进行不必要的状态更新handleNewMessage:当消息已存在时,更新消息内容(包括已读状态),确保保持最新的已读状态handleMessagesReadBroadcast:增加检查是否有需要更新的消息,避免不必要的状态更新new_message 事件,也会保持已读状态frontend/features/agent/hooks/useMessages.ts:handleNewMessage 中,当消息已存在时,更新消息内容(包括已读状态)handleMessagesReadBroadcast 中,增加检查是否有需要更新的消息,避免不必要的状态更新messages_read 事件时,payload 中没有包含 conversation_idhandleMessagesReadBroadcast 在更新消息列表时,没有检查 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 的检查,只有当匹配时才更新消息列表disableAutoScroll={true},导致整个滚动逻辑被禁用disableAutoScroll 的行为:不再完全禁用滚动逻辑disableAutoScroll 为 true 时,只禁用"收到对方消息时的自动滚动"disableAutoScroll 是什么值,都会自动滚动到底部disableAutoScroll 为 true),收到对方消息不会自动滚动disableAutoScroll 是什么值,都会自动滚动到底部frontend/components/dashboard/MessageList.tsx:disableAutoScroll 在 useEffect 开头的早期返回shouldAutoScroll = hasNewMessage && (isLastMessageFromCurrentUser || (!disableAutoScroll && isNearBottom))disableAutoScroll 为 false 且在底部附近时才会滚动requestAnimationFrame 确保 DOM 已更新后再检查位置frontend/components/dashboard/MessageList.tsx:lastMessageIdRef 和 lastMessageCountRef 来跟踪最后一条消息requestAnimationFrame 确保 DOM 已更新后再检查位置和决定是否滚动requestAnimationFrame 回调中从 containerRef.current 重新获取容器,确保使用最新的 DOM 元素MessageList 组件中添加滚动检测逻辑,当用户滚动到底部附近(距离底部 < 100px)时,延迟 500ms 后标记未读消息为已读messages_read 事件处理,确保正确更新已读状态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 组件优化日志记录,仅保留关键错误
修复通道关闭问题
Hub.unregister 中,使用 select 检查通道是否已经关闭优化 WebSocket 连接管理
WSClient.disconnect 中,设置 reconnectAttempts = maxReconnectAttempts 避免重连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 消息接收日志、优化断开连接逻辑测试消息发送:
📨 开始发送消息: 对话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测试消息接收:
📨 收到 WebSocket 消息: 对话ID=X, 类型=new_message📨 处理 WebSocket 消息(客服端): 对话ID=X, 类型=new_message📨 处理新消息(客服端): {...}✅ 添加新消息: 消息ID=X, 内容="..."如果消息没有发送:
POST /messages 请求如果消息发送了但没有广播:
📤 准备通过 WebSocket 广播消息 日志📢 广播消息 日志⚠️ WebSocket Hub 为空 日志(如果有,说明 Hub 没有正确初始化)如果消息广播了但没有收到:
📤 WebSocket 消息已发送 日志⚠️ 发送消息失败 日志📨 收到 WebSocket 消息 日志{},错误信息不够详细onerror 事件中检查 readyState 和 url,提供详细的错误信息onclose 事件中获取关闭代码和原因,提供详细的关闭信息useRef 存储回调函数,避免因回调函数变化导致重新连接frontend/lib/websocket.ts:改进错误处理和关闭处理frontend/features/agent/hooks/useWebSocket.ts:使用 useRef 存储回调函数frontend/app/chat/page.tsx:明确设置 isVisitor: trueonerror 事件中,检查 readyState 和 url,提供详细错误信息onclose 事件中,获取关闭代码(code)、原因(reason)和是否干净关闭(wasClean)!wasClean && code !== 1000 时才尝试重连useRef 存储回调函数,避免因回调函数变化导致 useEffect 重新执行connect() 方法中,检查是否已存在连接,如果存在则先断开npm run lint(frontend,无警告)✅ 手动验证:WebSocket 错误处理和关闭处理正常,错误信息详细
MessageInput 组件中添加自动聚焦功能useRef 引用输入框元素useEffect 监听 sending 状态变化sending 从 true 变为 false 时(发送完成),自动聚焦到输入框setTimeout 确保 DOM 更新完成后再聚焦frontend/components/dashboard/MessageInput.tsx:添加自动聚焦功能useRef 创建输入框引用 inputRefuseRef 记录上一次的 sending 状态 prevSendingRefuseEffect 中监听 sending 状态变化prevSendingRef.current === true && sending === false 时,说明刚刚发送完成inputRef.current?.focus() 聚焦到输入框setTimeout(..., 0) 确保 DOM 更新完成后再聚焦npm run lint(frontend,无警告)✅ 手动验证:发送消息后,输入框自动聚焦,可以直接继续输入
doc/测试指南.md:完整重写,添加所有已实现功能的测试指南visitor_status_update 事件到客服端ConversationService 中添加 UpdateVisitorOnlineStatus 和 UpdateLastSeenAt 方法Hub 中添加回调机制,在客户端连接/断开时调用回调函数Client 中添加 isVisitor 字段,区分访客和客服isVisitor 参数,默认值为 trueisVisitor=falsevisitor_status_update 事件,刷新对话详情backend/service/conversation_service.go、backend/websocket/hub.go、backend/websocket/client.go、backend/websocket/handler.go、backend/main.gofrontend/lib/websocket.ts、frontend/features/agent/hooks/useWebSocket.ts、frontend/features/agent/hooks/useMessages.ts、frontend/features/agent/types.ts、frontend/components/dashboard/ConversationListItem.tsxnpm run lint(frontend,无警告)gofmt(backend,无错误)UpdateVisitorOnlineStatus(conversationID, true) 更新在线状态UpdateVisitorOnlineStatus(conversationID, false) 更新离线状态visitor_status_update 事件到该对话的所有客户端(包括客服)visitor_status_update 事件时,刷新当前对话详情,更新在线状态status === "open" 判断)✅ 手动验证:访客连接/断开 WebSocket 时,客服端实时更新在线状态
last_seen_at)last_seen_at 判断是否在线(例如,如果 last_seen_at 在最近 60 秒内,则认为在线)ConversationSummary 中添加 last_seen_at 字段,以便在对话列表中显示最后活跃时间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.tsnpm run lint(frontend,无警告)messages_read 事件时,会包含 reader_is_agent 字段,表示读取者是客服还是访客messages_read 事件时,没有判断 reader_is_agent,导致错误地更新了消息的已读状态reader_is_agent === true)时,才应该更新访客消息的已读状态reader_is_agent === false)时,才应该更新客服消息的已读状态✅ 手动验证:访客发送消息后,客服查看消息,访客端显示双对勾(已读状态)
PUT /conversations/:id/contact 接口,ConversationService.UpdateConversationContact 落库邮箱/电话/备注VisitorDetailPanel 增加弹窗编辑,支持新增、修改、清空邮箱/电话/备注并即时刷新conversationApi.updateConversationContact 封装更新接口,统一返回结构useMessages 暴露 updateContactInfo,DashboardShell 和 VisitorDetailPanel 通过钩子完成联动backend/controller/conversation_controller.go、backend/service/conversation_service.go、backend/router/router.go、backend/service/types.gofeatures/agent/services/conversationApi.ts、features/agent/hooks/useMessages.ts、components/dashboard/VisitorDetailPanel.tsxnpm run lint(frontend)✅ 手动验证:客服工作台编辑邮箱/电话/备注,数据保存后右栏即时更新
客服工作台前端架构拆分
app/agent/dashboard/page.tsx 只保留页面入口,改由 DashboardShell 负责布局编排components/dashboard/*,将导航栏、会话列表、消息区、访客详情拆分为独立组件features/agent/hooks 与 features/agent/services,分别承载状态逻辑与 API 调用utils/format.ts、utils/highlight.tsx、utils/storage.ts,统一时间格式、关键词高亮与本地存储操作会话/消息状态管理优化
useAuth 统一处理本地登录信息与退出逻辑useConversations 负责对话列表、搜索、防抖与排序useMessages + useWebSocket 负责消息拉取、已读回执、WebSocket 广播与高亮定位TypeScript 类型补全
features/agent/types.ts 汇总会话、消息、用户等公共类型lib/websocket.ts、useWebSocket、useMessages 改用强类型定义,消除 any旧版客服聊天页迁移
/agent/chat/[conversationId] 复用统一的消息组件、输入框与 WebSocket 逻辑访客聊天页重构
/chat 页面改用统一的 MessageList、MessageInput 组件和消息服务components/dashboard/、features/agent/hooks/、features/agent/services/、utils/npm run lint 通过✅ npm run lint(frontend,无警告)
services 层补充基础错误处理与重试策略访客信息采集落地
InitConversation 接口接收并保存网站、来源、浏览器、系统、语言、IP 等信息last_seen_at 字段初始化,便于后续在线状态展示系统消息写入与展示
message_type 字段,区分普通消息与系统消息客服工作台访客详情完善
GET /conversations/:id 接口返回完整访客信息搜索体验优化
消息已读/未读状态(基础版)
is_read / read_at 字段,支持已读记录PUT /messages/read 接口及 WebSocket messages_read 事件,同步状态conversations 新增多项访客字段,messages 新增 message_typeConversationDetailRes、GetConversationDetail,并统一时间格式输出✅ 通过手动测试:访问 /chat 生成新对话,确认数据库记录访客信息与系统消息
✅ 通过客服工作台验证:系统消息样式正常,访客详情与数据库数据一致
✅ 搜索“关键词”后点击结果,可自动定位系统消息并高亮
last_seen_at 和 WebSocket 心跳完成在线状态实时更新客服工作台四栏布局实现
中间栏聊天功能集成
右侧栏访客详情实现
UI 优化
useEffect 实现对话切换时自动加载消息useRef 实现自动滚动功能✅ 功能测试通过:四栏布局、对话切换、消息发送、实时通信均正常工作
WebSocket 实时通信功能
技术实现
gorilla/websocket 库文档更新
doc/WebSocket学习笔记.md:详细解释 WebSocket 工作原理/ws?conversation_id=<对话ID>{ type: "new_message", conversation_id: number, data: Message }✅ 功能实现完成,待测试
客服端功能完整实现
/):使用默认管理员账号(admin/admin123)登录/agent/conversations):显示所有未关闭的对话,支持点击进入聊天/agent/chat/[conversationId]):客服可以查看和回复访客消息消息显示优化
后端功能完善
GET /conversations):返回所有未关闭的对话POST /admin/users):管理员可以创建新的客服/管理员账号POST /logout):用于前端清除登录状态文档更新
localStorage 存储客服登录信息(agent_user_id、agent_username、agent_role)initDefaultAdmin 函数在首次启动时自动创建默认管理员✅ 功能测试通过:登录、对话列表、客服聊天、退出登录均正常工作
访客聊天页面完整实现
POST /messages)GET /messages)代码优化和注释
文档完善
doc/前端学习笔记.md:核心概念解释、英文单词记忆、常见错误doc/测试指南.md:完整的测试步骤和问题排查指南doc/系统角色说明.md:解释访客和客服的区别Bug修复
.env 文件 UTF-8 BOM 编码问题(godotenv 不支持 BOM).env 文件加载的详细调试信息useState 管理消息列表、输入框、加载状态useEffect 实现自动拉取消息和自动滚动useRef 实现自动滚动到底部的功能localStorage 持久化访客ID(同一浏览器标签页共享)/chat 即可使用✅ 功能测试通过:消息发送、接收、显示、自动滚动均正常工作
frontend/lib/config.ts 统一管理 API 地址配置NEXT_PUBLIC_API_BASE_URL 配置后端地址.env.local 配置不同环境的后端地址http://127.0.0.1:8080.env.local 中的 NEXT_PUBLIC_API_BASE_URL 为实际域名POST /conversation/init、POST /messages、GET /messages/initconversation、/createmessage、/listmessage.env 读取 DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME.env 使用说明backend/.env 正确配置数据库连接参数完善对话初始化功能
InitConversation 函数中不完整的代码逻辑创建项目文档
代码优化