diff --git a/docs/plans/2026-03-03-phase2b-implementation.plan.md b/docs/plans/2026-03-03-phase2b-implementation.plan.md new file mode 100644 index 0000000..95fee9a --- /dev/null +++ b/docs/plans/2026-03-03-phase2b-implementation.plan.md @@ -0,0 +1,860 @@ +# Phase 2b 即时通讯消息系统(单聊)实施计划 + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 基于 Phase 2a WebSocket 基础设施,实现单聊即时通讯核心功能 + +**Architecture:** 新建 `app/im/` 模块(MVC 分层),扩展 `pkg/ws/` 事件路由,前端新增 chat Store + 4 个页面 + +**Tech Stack:** Go (Gin + GORM + Wire) / Redis / PostgreSQL / Vue 3 (uni-app + Pinia) / WebSocket + +**Design Doc:** `docs/plans/2026-03-03-phase2b-design.md` + +--- + +## Task 0: 设计文档 + 新分支 + 数据库表迁移 + +**Files:** +- 已完成: `docs/plans/2026-03-03-phase2b-design.md` +- Modify: `backend/go-service/cmd/server/main.go`(AutoMigrate 新表) + +**Step 1: 创建新分支** + +```bash +cd /Users/bojinyuan/Documents/workspace/webrtc/EchoChat +git checkout -b feature/phase2b-instant-messaging +``` + +**Step 2: 创建 IM 模型文件** + +Create: `backend/go-service/app/im/model/conversation.go` + +```go +package model + +import "time" + +type Conversation struct { + ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` + Type int `json:"type" gorm:"not null;default:1;comment:1=单聊 2=群聊"` + CreatorID int64 `json:"creator_id" gorm:"not null"` + LastMessageID *int64 `json:"last_message_id"` + LastMsgContent string `json:"last_msg_content" gorm:"type:text;default:''"` + LastMsgTime *time.Time `json:"last_msg_time"` + LastMsgSenderID *int64 `json:"last_msg_sender_id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (Conversation) TableName() string { return "im_conversations" } +``` + +Create: `backend/go-service/app/im/model/conversation_member.go` + +```go +package model + +import "time" + +type ConversationMember struct { + ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` + ConversationID int64 `json:"conversation_id" gorm:"not null;uniqueIndex:idx_conv_user"` + UserID int64 `json:"user_id" gorm:"not null;uniqueIndex:idx_conv_user"` + IsPinned bool `json:"is_pinned" gorm:"default:false"` + IsDeleted bool `json:"is_deleted" gorm:"default:false"` + UnreadCount int `json:"unread_count" gorm:"default:0"` + LastReadMsgID int64 `json:"last_read_msg_id" gorm:"default:0"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (ConversationMember) TableName() string { return "im_conversation_members" } +``` + +Create: `backend/go-service/app/im/model/message.go` + +```go +package model + +import "time" + +type Message struct { + ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` + ConversationID int64 `json:"conversation_id" gorm:"not null;index:idx_msg_conv_time;index:idx_msg_conv_id"` + SenderID int64 `json:"sender_id" gorm:"not null"` + Type int `json:"type" gorm:"not null;default:1;comment:1=文本"` + Content string `json:"content" gorm:"type:text;not null"` + Extra *string `json:"extra" gorm:"type:jsonb"` + Status int `json:"status" gorm:"not null;default:1;comment:1=正常 2=已撤回 3=已删除"` + ClientMsgID string `json:"client_msg_id" gorm:"size:64;default:''"` + CreatedAt time.Time `json:"created_at" gorm:"index:idx_msg_conv_time"` +} + +func (Message) TableName() string { return "im_messages" } +``` + +**Step 3: 在 main.go 添加 AutoMigrate** + +在 `cmd/server/main.go` 中(Wire 初始化之后),新增 IM 表 AutoMigrate: + +```go +// 在 app, err := provider.InitializeApp(cfg) 之后添加 +import imModel "github.com/echochat/backend/app/im/model" + +if err := app.DB.AutoMigrate( + &imModel.Conversation{}, + &imModel.ConversationMember{}, + &imModel.Message{}, +); err != nil { + logs.Fatal(ctx, "main", "IM 表迁移失败", zap.Error(err)) +} +``` + +**Step 4: 创建 Redis 索引 SQL(手动执行)** + +全文搜索索引需手动执行(GORM AutoMigrate 不支持 GIN 索引): + +```sql +CREATE INDEX IF NOT EXISTS idx_im_messages_content_search +ON im_messages USING gin(to_tsvector('simple', content)); +``` + +**Step 5: 验证编译** + +```bash +cd backend/go-service && go build ./... +``` + +Expected: 编译成功 + +**Step 6: Commit** + +```bash +git add -A && git commit -m "feat(im): Task 0 - IM 模型定义 + 数据库表迁移" +``` + +--- + +## Task 1: WS 事件路由表机制 + +**Files:** +- Modify: `backend/go-service/pkg/ws/hub.go`(添加事件路由表) +- Modify: `backend/go-service/pkg/ws/message.go`(添加 MessageHandler 类型导出) +- Modify: `backend/go-service/app/ws/handler.go`(onMessage 优先查路由表) + +**Step 1: 在 hub.go 添加事件路由表** + +Hub 新增字段和方法: + +```go +// EventHandler WS 事件处理函数签名 +type EventHandler func(client *Client, msg *Message) + +// Hub 新增字段 +type Hub struct { + // ...现有字段 + eventHandlers map[string]EventHandler + ehMu sync.RWMutex // 保护 eventHandlers +} + +// NewHub 修改:初始化 eventHandlers +func NewHub() *Hub { + return &Hub{ + clients: make(map[int64]*Client), + register: make(chan *Client, 256), + unregister: make(chan *Client, 256), + stopCh: make(chan struct{}), + eventHandlers: make(map[string]EventHandler), + } +} + +// RegisterEvent 注册事件处理器(启动时调用,非热路径) +func (h *Hub) RegisterEvent(event string, handler EventHandler) { + h.ehMu.Lock() + defer h.ehMu.Unlock() + h.eventHandlers[event] = handler +} + +// DispatchEvent 分发事件到注册的处理器 +// 返回 true 表示已处理,false 表示无匹配处理器 +func (h *Hub) DispatchEvent(client *Client, msg *Message) bool { + h.ehMu.RLock() + handler, ok := h.eventHandlers[msg.Event] + h.ehMu.RUnlock() + if !ok { + return false + } + handler(client, msg) + return true +} +``` + +**Step 2: 修改 handler.go 的 onMessage** + +在 `createReadHandler` 的 switch 之前,先尝试路由表分发: + +```go +func (h *Handler) createReadHandler(userID int64) ws.MessageHandler { + return func(client *ws.Client, msg *ws.Message) { + // 先尝试事件路由表 + if h.hub.DispatchEvent(client, msg) { + return + } + + // 内置事件 fallback + switch msg.Event { + case "heartbeat": + // ...现有逻辑 + default: + // ...现有逻辑 + } + } +} +``` + +**Step 3: 验证编译** + +```bash +cd backend/go-service && go build ./... +``` + +**Step 4: Commit** + +```bash +git add -A && git commit -m "feat(ws): Task 1 - Hub 事件路由表机制" +``` + +--- + +## Task 2: IM DAO 层 + +**Files:** +- Create: `backend/go-service/app/im/dao/conversation_dao.go` +- Create: `backend/go-service/app/im/dao/message_dao.go` + +**Step 1: 创建 ConversationDAO** + +Create: `backend/go-service/app/im/dao/conversation_dao.go` + +核心方法: +- `FindPrivateConversation(ctx, userID, targetUserID)` — 查找两人的单聊会话 +- `CreateWithMembers(ctx, conv, memberIDs)` — 事务创建会话 + 成员 +- `GetUserConversations(ctx, userID)` — 获取用户会话列表(JOIN members + conversations) +- `GetMember(ctx, conversationID, userID)` — 获取成员信息 +- `UpdateMember(ctx, member)` — 更新成员信息(置顶、未读数等) +- `SoftDeleteMember(ctx, conversationID, userID)` — 软删除会话 +- `UpdateLastMessage(ctx, conversationID, msgID, content, senderID, time)` — 更新最后消息 +- `IncrementUnread(ctx, conversationID, userID)` — 未读数 +1 +- `ClearUnread(ctx, conversationID, userID)` — 清零未读 +- `GetUnreadConversations(ctx, userID)` — 获取有未读的会话列表 + +**Step 2: 创建 MessageDAO** + +Create: `backend/go-service/app/im/dao/message_dao.go` + +核心方法: +- `Create(ctx, message)` — 写入消息 +- `GetByID(ctx, id)` — 查询单条消息 +- `GetByConversation(ctx, conversationID, beforeID, limit)` — 游标分页查消息 +- `UpdateStatus(ctx, id, status)` — 更新消息状态(撤回) +- `DeleteByConversation(ctx, conversationID)` — 清空会话消息 +- `SearchMessages(ctx, userID, keyword, limit)` — 全局搜索(JOIN members 确保权限) +- `FindByClientMsgID(ctx, conversationID, clientMsgID)` — 幂等查询 + +**Step 3: 验证编译** + +```bash +cd backend/go-service && go build ./... +``` + +**Step 4: Commit** + +```bash +git add -A && git commit -m "feat(im): Task 2 - IM DAO 层(会话 + 消息)" +``` + +--- + +## Task 3: IM Service 核心业务 + +**Files:** +- Create: `backend/go-service/app/im/service/im_service.go` +- Create: `backend/go-service/app/im/service/interfaces.go`(接口定义) + +**Step 1: 创建接口定义文件** + +Create: `backend/go-service/app/im/service/interfaces.go` + +```go +package service + +import ( + "context" + authModel "github.com/echochat/backend/app/auth/model" +) + +// FriendChecker 好友关系校验接口(contact.FriendshipDAO 隐式实现) +type FriendChecker interface { + IsFriend(ctx context.Context, userID, targetID int64) (bool, error) +} + +// UserInfoGetter 用户信息批量查询接口(contact.FriendshipDAO 隐式实现) +type UserInfoGetter interface { + GetUsersByIDs(ctx context.Context, userIDs []int64) ([]authModel.User, error) +} +``` + +**Step 2: 创建 IMService** + +Create: `backend/go-service/app/im/service/im_service.go` + +IMService 注入:`ConversationDAO`、`MessageDAO`、`*ws.PubSub`、`FriendChecker`、`UserInfoGetter` + +核心方法(参考设计文档第七节): +- `SendMessage(ctx, senderID, targetUserID, content, msgType, clientMsgID)` — 核心链路:校验好友 → 查找/创建会话 → 写消息 → 更新 last_message → 增加未读 → 推送 +- `RecallMessage(ctx, userID, messageID, conversationID)` — 校验 2 分钟 + 更新状态 + 推送 +- `GetConversations(ctx, userID)` — 会话列表(含对方用户信息) +- `GetMessages(ctx, userID, conversationID, beforeID, limit)` — 历史消息 + 发送者信息 +- `PinConversation(ctx, userID, conversationID, isPinned)` — 置顶 +- `DeleteConversation(ctx, userID, conversationID)` — 软删除 +- `ClearMessages(ctx, userID, conversationID)` — 清空 +- `MarkAsRead(ctx, userID, conversationID)` — 标记已读 + 清 Redis +- `SearchMessages(ctx, userID, keyword)` — 全局搜索 +- `PushOfflineMessages(ctx, userID)` — 查未读会话 → 推送 im.conversation.unread +- `FindOrCreateConversation(ctx, userID, targetUserID)` — 内部方法 + +DTO 定义放在 `app/dto/im_dto.go`(新建): +- `ConversationItem` — 会话列表项 +- `MessageItem` — 消息项 +- `SearchResult` — 搜索结果(按会话分组) + +**Step 3: 创建 IM DTO** + +Create: `backend/go-service/app/dto/im_dto.go` + +```go +package dto + +type ConversationItem struct { + ID int64 `json:"id"` + Type int `json:"type"` + PeerUserID int64 `json:"peer_user_id"` + PeerNickname string `json:"peer_nickname"` + PeerAvatar string `json:"peer_avatar"` + PeerUsername string `json:"peer_username"` + LastMsgContent string `json:"last_msg_content"` + LastMsgTime string `json:"last_msg_time"` + LastMsgSenderID int64 `json:"last_msg_sender_id"` + UnreadCount int `json:"unread_count"` + IsPinned bool `json:"is_pinned"` +} + +type MessageItem struct { + ID int64 `json:"id"` + SenderID int64 `json:"sender_id"` + SenderName string `json:"sender_name"` + SenderAvatar string `json:"sender_avatar"` + Type int `json:"type"` + Content string `json:"content"` + Status int `json:"status"` + ClientMsgID string `json:"client_msg_id"` + CreatedAt string `json:"created_at"` +} + +type MessageSearchGroup struct { + ConversationID int64 `json:"conversation_id"` + PeerNickname string `json:"peer_nickname"` + PeerAvatar string `json:"peer_avatar"` + Messages []MessageItem `json:"messages"` +} +``` + +**Step 4: 添加 IM 常量** + +Create: `backend/go-service/app/constants/im_constants.go` + +```go +package constants + +const ( + ConversationTypePrivate = 1 + ConversationTypeGroup = 2 + + MessageTypeText = 1 + + MessageStatusNormal = 1 + MessageStatusRecalled = 2 + MessageStatusDeleted = 3 + + MessageRecallTimeLimit = 2 * 60 // 撤回时限(秒) +) +``` + +**Step 5: 验证编译** + +```bash +cd backend/go-service && go build ./... +``` + +**Step 6: Commit** + +```bash +git add -A && git commit -m "feat(im): Task 3 - IM Service + DTO + 常量定义" +``` + +--- + +## Task 4: IM WS 事件处理器 + 离线消息推送 + +**Files:** +- Create: `backend/go-service/app/im/handler/message_handler.go` +- Modify: `backend/go-service/app/ws/handler.go`(离线消息推送接口 + 连接回调) +- Modify: `backend/go-service/app/ws/online_service.go`(添加 OfflineMessagePusher 接口) + +**Step 1: 创建 IM 事件处理器** + +Create: `backend/go-service/app/im/handler/message_handler.go` + +```go +package handler + +// MessageHandler IM 模块的 WebSocket 事件处理器 +// 负责处理:im.message.send、im.message.recall、im.typing.start/stop、im.conversation.sync + +type MessageHandler struct { + imService *service.IMService + hub *ws.Hub +} + +// RegisterEvents 向 Hub 注册 IM 事件处理器 +func (h *MessageHandler) RegisterEvents(hub *ws.Hub) { + hub.RegisterEvent("im.message.send", h.HandleSendMessage) + hub.RegisterEvent("im.message.recall", h.HandleRecallMessage) + hub.RegisterEvent("im.typing.start", h.HandleTypingStart) + hub.RegisterEvent("im.typing.stop", h.HandleTypingStop) + hub.RegisterEvent("im.conversation.sync", h.HandleConversationSync) +} +``` + +每个 Handler 方法:解析 msg.Data → 调用 IMService → 返回 ACK Response + +**Step 2: 添加离线消息推送接口** + +在 `app/ws/online_service.go` 中定义接口: + +```go +// OfflineMessagePusher 离线消息推送接口(由 IM Service 实现) +type OfflineMessagePusher interface { + PushOfflineMessages(ctx context.Context, userID int64) error +} +``` + +Handler 新增 `offlinePusher OfflineMessagePusher` 字段,在 `Upgrade` 中 WS 连接成功后调用。 + +**Step 3: 修改 Handler.Upgrade 添加离线推送调用** + +在 `handler.go` Upgrade 方法的 `h.onlineService.UserOnline()` 之后,添加: + +```go +if h.offlinePusher != nil { + go h.offlinePusher.PushOfflineMessages(c.Request.Context(), claims.UserID) +} +``` + +**Step 4: 验证编译** + +```bash +cd backend/go-service && go build ./... +``` + +**Step 5: Commit** + +```bash +git add -A && git commit -m "feat(im): Task 4 - IM WS 事件处理器 + 离线消息推送" +``` + +--- + +## Task 5: IM REST Controller + Router + Wire 集成 + +**Files:** +- Create: `backend/go-service/app/im/controller/im_controller.go` +- Create: `backend/go-service/app/im/router.go` +- Create: `backend/go-service/app/im/provider.go` +- Modify: `backend/go-service/app/provider/provider.go`(App 新增 IM 字段) +- Modify: `backend/go-service/app/provider/wire.go`(添加 IMSet) +- Regenerate: `backend/go-service/app/provider/wire_gen.go` +- Modify: `backend/go-service/router/router.go`(注册 IM 路由) + +**Step 1: 创建 IM Controller** + +Create: `backend/go-service/app/im/controller/im_controller.go` + +```go +package controller + +type IMController struct { + imService *service.IMService +} + +func NewIMController(svc *service.IMService) *IMController { + return &IMController{imService: svc} +} +``` + +实现 7 个 REST handler: +- `GetConversations` — GET /conversations +- `GetMessages` — GET /conversations/:id/messages +- `PinConversation` — PUT /conversations/:id/pin +- `DeleteConversation` — DELETE /conversations/:id +- `ClearMessages` — DELETE /conversations/:id/messages +- `MarkAsRead` — PUT /conversations/:id/read +- `SearchMessages` — GET /messages/search + +**Step 2: 创建 IM Router** + +Create: `backend/go-service/app/im/router.go` + +```go +package im + +func RegisterRoutes(r *gin.Engine, ctrl *controller.IMController, jwtAuth gin.HandlerFunc) { + authed := r.Group("/api/v1/im") + authed.Use(jwtAuth) + { + authed.GET("/conversations", ctrl.GetConversations) + authed.GET("/conversations/:id/messages", ctrl.GetMessages) + authed.PUT("/conversations/:id/pin", ctrl.PinConversation) + authed.DELETE("/conversations/:id", ctrl.DeleteConversation) + authed.DELETE("/conversations/:id/messages", ctrl.ClearMessages) + authed.PUT("/conversations/:id/read", ctrl.MarkAsRead) + authed.GET("/messages/search", ctrl.SearchMessages) + } +} +``` + +**Step 3: 创建 IM Provider** + +Create: `backend/go-service/app/im/provider.go` + +```go +package im + +var IMSet = wire.NewSet( + dao.NewConversationDAO, + dao.NewMessageDAO, + service.NewIMService, + controller.NewIMController, + handler.NewMessageHandler, +) +``` + +**Step 4: 修改 provider.go — App 新增字段** + +在 `App` struct 和 `NewApp` 中添加: +- `IMController *imController.IMController` +- `IMHandler *imHandler.MessageHandler` + +**Step 5: 修改 wire.go 和重新生成 wire_gen.go** + +添加 `IMSet` 到 Wire injector,添加接口绑定: +- `wire.Bind(new(imService.FriendChecker), new(*friendshipDAO))` +- `wire.Bind(new(imService.UserInfoGetter), new(*friendshipDAO))` +- `wire.Bind(new(wsApp.OfflineMessagePusher), new(*imService.IMService))` + +```bash +cd backend/go-service && wire ./app/provider/ +``` + +**Step 6: 修改 router.go** + +在 `router/router.go` 的 Setup 函数中添加: + +```go +import imApp "github.com/echochat/backend/app/im" + +imApp.RegisterRoutes(engine, app.IMController, jwtAuth) +``` + +并在 Router.Setup 中注册 IM 事件处理器: + +```go +app.IMHandler.RegisterEvents(app.Hub) +``` + +**Step 7: 验证编译** + +```bash +cd backend/go-service && go build ./... +``` + +**Step 8: 启动后端验证** + +```bash +cd backend/go-service && go run cmd/server/main.go +``` + +Expected: 服务启动成功,im_conversations / im_conversation_members / im_messages 表自动创建 + +**Step 9: Commit** + +```bash +git add -A && git commit -m "feat(im): Task 5 - REST Controller + Router + Wire 完整集成" +``` + +--- + +## Task 6: 前台 chat Store + API 封装 + WS 事件集成 + +**Files:** +- Create: `frontend/src/api/chat.js` +- Create: `frontend/src/store/chat.js` +- Modify: `frontend/src/services/websocket.js`(如需添加 send 到 store) +- Modify: `frontend/src/components/CustomTabBar.vue`(未读 badge) + +**Step 1: 创建 chat API** + +Create: `frontend/src/api/chat.js` + +封装 7 个 REST API: +- `getConversations()` — GET /api/v1/im/conversations +- `getMessages(conversationId, beforeId, limit)` — GET /api/v1/im/conversations/:id/messages +- `pinConversation(conversationId, isPinned)` — PUT /api/v1/im/conversations/:id/pin +- `deleteConversation(conversationId)` — DELETE /api/v1/im/conversations/:id +- `clearMessages(conversationId)` — DELETE /api/v1/im/conversations/:id/messages +- `markAsRead(conversationId)` — PUT /api/v1/im/conversations/:id/read +- `searchMessages(keyword)` — GET /api/v1/im/messages/search + +**Step 2: 创建 chat Store** + +Create: `frontend/src/store/chat.js` + +Pinia Store,包含: +- 状态:conversations, currentConversation, messages, totalUnread, typingUsers, pendingMessages +- Actions:loadConversations, sendMessage, loadHistory, markAsRead, recallMessage, pinConversation, deleteConversation, clearMessages, searchMessages +- WS 事件注册:initWsListeners() + - `im.message.new` → 更新会话列表 + 消息列表 + 未读数 + - `im.message.send.ack` → 更新 pending 消息状态为已发送 + - `im.message.recalled` → 标记消息为已撤回 + - `im.typing.notify` → 更新 typingUsers + - `im.conversation.unread` → 初始化会话未读数 + +**Step 3: 修改 CustomTabBar 添加未读 badge** + +修改 `frontend/src/components/CustomTabBar.vue`: +- 引入 `useChatStore` +- 消息 Tab 显示 `chatStore.totalUnread` badge(红色圆点/数字) + +**Step 4: 验证编译** + +```bash +cd frontend && npm run build:h5 2>&1 | head -20 +``` + +Expected: 编译成功 + +**Step 5: Commit** + +```bash +git add -A && git commit -m "feat(frontend): Task 6 - chat Store + API + WS 事件 + TabBar badge" +``` + +--- + +## Task 7: 前台会话列表页 + 聊天页 + +**前置操作:** 使用 ui-ux-pro-max 技能包进行页面设计 + +**Files:** +- Modify: `frontend/src/pages/chat/index.vue`(改造为会话列表) +- Create: `frontend/src/pages/chat/conversation.vue`(聊天页) +- Modify: `frontend/src/pages.json`(添加 conversation 页面路由) + +**Step 1: 读取 ui-ux-pro-max 技能包** + +```bash +npx openskills read ui-ux-pro-max +``` + +使用脚本搜索聊天/消息页面的设计参考。 + +**Step 2: 注册新页面路由** + +在 `pages.json` 的 pages 数组中添加: + +```json +{ + "path": "pages/chat/conversation", + "style": { + "navigationBarTitleText": "聊天", + "navigationStyle": "custom" + } +} +``` + +**Step 3: 实现会话列表页 (chat/index.vue)** + +主要功能: +- 搜索栏(点击跳转 search 页) +- 会话列表:v-for conversations,展示头像/昵称/最后消息/时间/未读badge +- 排序:置顶在前,再按 lastMsgTime 降序 +- 长按操作菜单(uni.showActionSheet):置顶、删除、清空 +- 点击跳转:`uni.navigateTo({ url: '/pages/chat/conversation?conversationId=X&userId=Y' })` +- 空状态:"暂无消息" +- 使用 `CustomTabBar :current="0"` + +**Step 4: 实现聊天页 (chat/conversation.vue)** + +主要功能: +- 自定义顶部导航:对方昵称、"正在输入…"、设置图标 +- 消息列表(scroll-view,scroll-into-view 自动滚底) +- 上拉加载历史(loadHistory with beforeId) +- 消息气泡: + - 自己的消息右对齐(蓝色背景) + - 对方消息左对齐(灰色背景) + - 已撤回消息灰色提示 +- 消息状态:发送中(spinner)、已发送(✓)、失败(重试图标) +- 长按消息 → 撤回(仅自己的、2分钟内) +- 底部输入区:textarea + 发送按钮 +- 输入时触发 im.typing.start(节流 3s),停止输入 3s 后发 im.typing.stop +- onLoad:通过 userId 参数加载/进入会话 +- onUnload:清理 typing 状态 + +**Step 5: 验证编译** + +```bash +cd frontend && npm run build:h5 2>&1 | head -20 +``` + +**Step 6: Commit** + +```bash +git add -A && git commit -m "feat(frontend): Task 7 - 会话列表页 + 聊天页(ui-ux-pro-max)" +``` + +--- + +## Task 8: 前台会话设置页 + 消息搜索页 + 联系人模块改造 + +**前置操作:** 使用 ui-ux-pro-max 技能包 + +**Files:** +- Create: `frontend/src/pages/chat/settings.vue` +- Create: `frontend/src/pages/chat/search.vue` +- Modify: `frontend/src/pages.json`(添加 settings/search 路由) +- Modify: `frontend/src/pages/contact/detail.vue`(sendMessage 改为跳转) +- Modify: `frontend/src/pages/contact/index.vue`(列表项点击进入聊天) + +**Step 1: 注册新页面路由** + +在 `pages.json` 添加: + +```json +{ "path": "pages/chat/settings", "style": { "navigationBarTitleText": "聊天设置" } }, +{ "path": "pages/chat/search", "style": { "navigationBarTitleText": "搜索消息", "navigationStyle": "custom" } } +``` + +**Step 2: 实现会话设置页 (chat/settings.vue)** + +- 对方信息卡片(头像 + 昵称 + 用户名) +- 置顶聊天 Switch +- 清空聊天记录按钮(uni.showModal 二次确认) +- 删除会话按钮(uni.showModal 二次确认 → navigateBack) + +**Step 3: 实现消息搜索页 (chat/search.vue)** + +- 搜索输入框(防抖 300ms) +- 搜索结果按会话分组(v-for groups → 会话名 + 消息列表) +- 点击消息跳转到聊天页 + +**Step 4: 改造好友详情页 (contact/detail.vue)** + +```javascript +const sendMessage = () => { + uni.navigateTo({ url: `/pages/chat/conversation?userId=${userId.value}` }) +} +``` + +**Step 5: 改造好友列表页 (contact/index.vue)** + +好友列表项添加点击跳转聊天的快捷入口(或双击/右侧图标)。 + +**Step 6: 验证编译** + +```bash +cd frontend && npm run build:h5 2>&1 | head -20 +``` + +**Step 7: Commit** + +```bash +git add -A && git commit -m "feat(frontend): Task 8 - 设置页 + 搜索页 + 联系人联动" +``` + +--- + +## Task 9: 集成测试 + 文档更新 + 代码审查 + +**Files:** +- Modify: `docs/progress/CURRENT_STATUS.md` +- Modify: `docs/plans/2026-03-03-phase2b-design.md`(状态更新) +- Modify: `docs/plans/2026-02-27-echochat-system-design.md` +- Modify: `docs/architecture/system-architecture.md`(如存在) +- Modify: `.cursor/rules/project-context.mdc` +- Create: `docs/api/frontend/im.md`(IM API 文档) +- Create: `docs/api/websocket-im.md`(WS IM 事件文档) + +**Step 1: 三端编译验证** + +```bash +cd backend/go-service && go build ./... +cd frontend && npm run build:h5 +cd admin && npm run build +``` + +Expected: 全部编译成功 + +**Step 2: 启动后端 + 前端功能测试** + +```bash +cd backend/go-service && go run cmd/server/main.go & +cd frontend && npm run dev:h5 +``` + +使用 Playwright MCP 验证核心流程: +1. 用 testuser1 登录 → 联系人 → 点击好友 → 发消息跳转聊天页 +2. 发送一条消息 → 收到 ACK +3. 用另一个账号登录 → 会话列表显示新消息 → 进入聊天 → 看到消息 +4. 撤回消息 → 对方看到撤回提示 +5. 消息搜索 → 输入关键词 → 显示结果 + +**Step 3: 更新项目文档** + +按项目规则(文档自动同步规则)更新: +- `CURRENT_STATUS.md`:Phase 2b Task 状态表 +- `project-context.mdc`:当前进度更新 +- 总体设计文档:API 列表更新 +- 新增 IM API 文档 +- 新增 WS IM 事件文档 + +**Step 4: 代码审查** + +使用 `code-reviewer` 子代理进行结构化审查。 + +**Step 5: Commit** + +```bash +git add -A && git commit -m "docs: Task 9 - Phase 2b 集成测试通过 + 文档同步" +``` + +--- + +## 执行说明 + +本计划共 **10 个 Task**,建议执行方式: + +1. **Task 0-5(后端)**:按顺序执行,每个 Task 完成后编译验证 + 提交 +2. **Task 6-8(前端)**:依赖后端 API 就绪,前端页面使用 ui-ux-pro-max 设计 +3. **Task 9(集成)**:全端验证 + 文档更新 + +预计工作量:后端 6 个 Task + 前端 3 个 Task + 集成 1 个 Task