# 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)
}

// Redis Key 前缀（未读数 HASH）
const unreadKeyPrefix = "echo:im:unread:"
```

**Step 2: 创建 IMService**

Create: `backend/go-service/app/im/service/im_service.go`

IMService 注入：`ConversationDAO`、`MessageDAO`、`*ws.PubSub`、`*redis.Client`、`FriendChecker`、`UserInfoGetter`

注意：`*redis.Client` 用于操作未读数 Redis HASH（`echo:im:unread:{user_id}`）

核心方法（参考设计文档第七节）：
- `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
