docs: Phase 2b 即时通讯消息系统设计文档
单聊核心设计:全双工 WS 消息收发、会话管理、离线消息推送、 消息撤回、正在输入、全局搜索、4 个前端页面。 Made-with: Cursor
This commit is contained in:
469
docs/plans/2026-03-03-phase2b-design.md
Normal file
469
docs/plans/2026-03-03-phase2b-design.md
Normal file
@@ -0,0 +1,469 @@
|
||||
# Phase 2b 设计文档:即时通讯消息系统(单聊)
|
||||
|
||||
> **状态:** 📋 设计完成,待实施
|
||||
> **分支:** `feature/phase2b-instant-messaging`
|
||||
> **前置依赖:** Phase 2a 全部完成(WebSocket + 联系人管理)
|
||||
> **架构备忘:** `docs/plans/2026-03-02-phase2b-architecture-notes.md`
|
||||
> **最后更新:** 2026-03-03
|
||||
|
||||
---
|
||||
|
||||
## 一、设计目标
|
||||
|
||||
基于 Phase 2a 的 WebSocket 基础设施,实现单聊即时通讯核心功能,打通端到端消息收发链路。
|
||||
|
||||
**核心交付物:**
|
||||
- 会话管理(创建/列表/置顶/删除/清空)
|
||||
- 消息收发(全双工 WebSocket,三态 ACK 确认)
|
||||
- 离线消息推送(WS 连接后服务端主动推送未读摘要)
|
||||
- 消息撤回(2 分钟内)
|
||||
- "正在输入"实时提示
|
||||
- 未读消息展示(会话 badge + TabBar 总未读数)
|
||||
- 全局消息搜索
|
||||
- 前台 4 个页面:会话列表 + 聊天页 + 会话设置 + 消息搜索
|
||||
|
||||
**不包含(留待 Phase 2c):**
|
||||
- 群聊
|
||||
- 已读回执
|
||||
- 图片/语音/文件消息
|
||||
- 管理端消息管理
|
||||
|
||||
---
|
||||
|
||||
## 二、需求确认汇总
|
||||
|
||||
| 决策项 | 选择 |
|
||||
|--------|------|
|
||||
| 范围 | 单聊核心,群聊/已读回执放 Phase 2c |
|
||||
| 消息通道 | 全双工 WebSocket(发送 + 接收都走 WS) |
|
||||
| 消息类型 | 仅文本,预留 type + extra JSON 字段 |
|
||||
| 管理端 | 暂不做消息管理 |
|
||||
| 正在输入 | 包含 |
|
||||
| 前端页面 | 4 个:会话列表 + 聊天页 + 会话设置 + 消息搜索 |
|
||||
| 会话创建 | 发第一条消息时自动创建(无空会话) |
|
||||
| 离线消息 | 服务端 WS 连接后主动推送未读消息 |
|
||||
| 消息状态 | 三态:发送中 → 已发送(ACK) → 发送失败 |
|
||||
| 消息撤回 | 支持(2 分钟内) |
|
||||
| 未读展示 | 会话 badge + TabBar 总未读数 |
|
||||
| 会话操作 | 置顶 + 删除 + 清空聊天记录 |
|
||||
| 联系人联动 | 好友详情页"发消息"按钮 + 列表项点击进入聊天 |
|
||||
| 会话设置 | 对方信息卡片 + 置顶 + 清空 + 删除 |
|
||||
| 消息搜索 | 全局搜索,结果按会话分组 |
|
||||
| WS 事件路由 | 事件路由表 `map[string]EventHandler` |
|
||||
| 分支 | `feature/phase2b-instant-messaging` |
|
||||
|
||||
---
|
||||
|
||||
## 三、架构方案
|
||||
|
||||
### 3.1 消息收发核心链路
|
||||
|
||||
```
|
||||
发送方 Client
|
||||
│ WS: im.message.send { target_user_id, content, client_msg_id }
|
||||
▼
|
||||
WebSocket Handler(事件路由表分发)
|
||||
│ → IM MessageHandler.HandleSendMessage()
|
||||
▼
|
||||
IM Service.SendMessage()
|
||||
├── FriendChecker.IsFriend() 校验好友关系
|
||||
├── FindOrCreateConversation() 查找/创建会话
|
||||
├── DB: INSERT im_messages
|
||||
├── DB: UPDATE im_conversations (last_message 信息)
|
||||
├── DB: UPDATE im_conversation_members (接收方 unread_count +1)
|
||||
└── Redis: HINCRBY echo:im:unread:{receiverID} convID 1
|
||||
│
|
||||
├──→ WS Response: im.message.send.ack { message_id, conversation_id }
|
||||
│ (返回给发送方,确认消息已存储)
|
||||
│
|
||||
└──→ PubSub.PublishToUser(receiverID, im.message.new)
|
||||
(推送给接收方,实时展示新消息)
|
||||
```
|
||||
|
||||
### 3.2 WS 事件路由表机制
|
||||
|
||||
Phase 2a 的 `handler.go` 使用 switch-case 分发事件(仅心跳)。Phase 2b 引入事件路由表,支持模块化注册:
|
||||
|
||||
```go
|
||||
// EventHandler WS 事件处理函数签名
|
||||
type EventHandler func(client *Client, msg *Message)
|
||||
|
||||
// Hub 新增字段
|
||||
eventHandlers map[string]EventHandler
|
||||
|
||||
// RegisterEvent 注册事件处理器
|
||||
func (h *Hub) RegisterEvent(event string, handler EventHandler)
|
||||
|
||||
// DispatchEvent 分发事件(onMessage 中优先查路由表,未命中走原 switch-case)
|
||||
func (h *Hub) DispatchEvent(client *Client, msg *Message) bool
|
||||
```
|
||||
|
||||
IM 模块在启动时向 Hub 注册事件:`im.message.send`、`im.message.recall`、`im.typing.start`、`im.typing.stop`、`im.conversation.sync`。
|
||||
|
||||
### 3.3 离线消息推送
|
||||
|
||||
```
|
||||
用户 WS 连接成功
|
||||
→ OnlineService.UserOnline()
|
||||
→ IMService.PushOfflineMessages(userID)
|
||||
├── 查询 unread_count > 0 的所有会话
|
||||
└── PubSub.PublishToUser: im.conversation.unread 事件
|
||||
{ conversations: [{ id, unread_count, last_msg_content, last_msg_time, ... }] }
|
||||
```
|
||||
|
||||
前端收到 `im.conversation.unread` 后更新会话列表和 TabBar 未读数。
|
||||
|
||||
### 3.4 消息撤回链路
|
||||
|
||||
```
|
||||
发送方 Client
|
||||
│ WS: im.message.recall { message_id, conversation_id }
|
||||
▼
|
||||
IM Service.RecallMessage()
|
||||
├── 校验:是否为自己的消息、是否在 2 分钟内
|
||||
├── DB: UPDATE im_messages SET status = 2(已撤回)
|
||||
├── DB: UPDATE im_conversations (last_msg_content = "XX 撤回了一条消息")
|
||||
├── WS Response: im.message.recall.ack { message_id }
|
||||
└── PubSub.PublishToUser(receiverID, im.message.recalled)
|
||||
```
|
||||
|
||||
### 3.5 跨模块依赖(接口注入模式)
|
||||
|
||||
沿用 Phase 2a 的接口注入标准,避免 `im` 包直接 import `contact` 包:
|
||||
|
||||
```go
|
||||
// 定义在 app/im/ 包内
|
||||
type FriendChecker interface {
|
||||
IsFriend(ctx context.Context, userID, targetID int64) (bool, error)
|
||||
}
|
||||
|
||||
type UserInfoGetter interface {
|
||||
GetUsersByIDs(ctx context.Context, userIDs []int64) ([]model.AuthUser, error)
|
||||
}
|
||||
```
|
||||
|
||||
Wire 注入链:`contact.FriendshipDAO` 隐式实现两个接口。
|
||||
|
||||
离线消息推送需要 `ws` 模块调用 IM:定义 `OfflineMessagePusher` 接口在 `app/ws/` 中,由 `IMService` 实现。
|
||||
|
||||
---
|
||||
|
||||
## 四、数据库设计
|
||||
|
||||
### 4.1 im_conversations(会话表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_conversations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
type SMALLINT NOT NULL DEFAULT 1, -- 1=单聊(预留 2=群聊)
|
||||
creator_id BIGINT NOT NULL, -- 创建者 ID
|
||||
last_message_id BIGINT DEFAULT NULL, -- 最后一条消息 ID
|
||||
last_msg_content TEXT DEFAULT '', -- 最后消息预览文本
|
||||
last_msg_time TIMESTAMP WITH TIME ZONE, -- 最后消息时间
|
||||
last_msg_sender_id BIGINT DEFAULT NULL, -- 最后消息发送者
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_im_conversations_updated ON im_conversations(updated_at DESC);
|
||||
```
|
||||
|
||||
### 4.2 im_conversation_members(会话成员表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_conversation_members (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
conversation_id BIGINT NOT NULL REFERENCES im_conversations(id),
|
||||
user_id BIGINT NOT NULL, -- 成员用户 ID
|
||||
is_pinned BOOLEAN DEFAULT FALSE, -- 是否置顶
|
||||
is_deleted BOOLEAN DEFAULT FALSE, -- 是否删除会话(软删除,不影响对方)
|
||||
unread_count INT DEFAULT 0, -- 未读消息数
|
||||
last_read_msg_id BIGINT DEFAULT 0, -- 最后已读消息 ID
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
|
||||
UNIQUE(conversation_id, user_id)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_im_conv_members_user ON im_conversation_members(user_id, is_deleted);
|
||||
CREATE INDEX idx_im_conv_members_conv ON im_conversation_members(conversation_id);
|
||||
```
|
||||
|
||||
### 4.3 im_messages(消息表)
|
||||
|
||||
```sql
|
||||
CREATE TABLE im_messages (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
conversation_id BIGINT NOT NULL REFERENCES im_conversations(id),
|
||||
sender_id BIGINT NOT NULL, -- 发送者 ID
|
||||
type SMALLINT NOT NULL DEFAULT 1, -- 1=文本(预留 2=图片 3=语音...)
|
||||
content TEXT NOT NULL, -- 消息内容
|
||||
extra JSONB DEFAULT NULL, -- 扩展数据(预留)
|
||||
status SMALLINT NOT NULL DEFAULT 1, -- 1=正常 2=已撤回 3=已删除
|
||||
client_msg_id VARCHAR(64) DEFAULT '', -- 客户端消息唯一 ID(幂等用)
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 游标分页核心索引
|
||||
CREATE INDEX idx_im_messages_conv_time ON im_messages(conversation_id, created_at DESC);
|
||||
CREATE INDEX idx_im_messages_conv_id ON im_messages(conversation_id, id DESC);
|
||||
-- 全局消息搜索索引
|
||||
CREATE INDEX idx_im_messages_content_search ON im_messages USING gin(to_tsvector('simple', content));
|
||||
```
|
||||
|
||||
### 4.4 单聊会话去重
|
||||
|
||||
Service 层通过查询 `im_conversation_members` 判断两人间是否已存在单聊会话:
|
||||
|
||||
```sql
|
||||
SELECT cm1.conversation_id FROM im_conversation_members cm1
|
||||
JOIN im_conversation_members cm2 ON cm1.conversation_id = cm2.conversation_id
|
||||
JOIN im_conversations c ON c.id = cm1.conversation_id
|
||||
WHERE cm1.user_id = ? AND cm2.user_id = ? AND c.type = 1
|
||||
LIMIT 1
|
||||
```
|
||||
|
||||
### 4.5 Redis 数据结构
|
||||
|
||||
| Key | 类型 | 说明 |
|
||||
|-----|------|------|
|
||||
| `echo:im:unread:{user_id}` | HASH | 每个会话的未读数 `{ conv_id: count }` |
|
||||
|
||||
- 收到新消息:`HINCRBY echo:im:unread:{user_id} {conv_id} 1`
|
||||
- 标记已读:`HDEL echo:im:unread:{user_id} {conv_id}`
|
||||
- 获取总未读数:`HVALS echo:im:unread:{user_id}` 求和
|
||||
|
||||
---
|
||||
|
||||
## 五、WebSocket 事件协议
|
||||
|
||||
### 5.1 客户端 → 服务端(发送类)
|
||||
|
||||
| 事件 | 说明 | Data 字段 |
|
||||
|------|------|-----------|
|
||||
| `im.message.send` | 发送消息 | `{ target_user_id, content, type, client_msg_id }` |
|
||||
| `im.message.recall` | 撤回消息 | `{ message_id, conversation_id }` |
|
||||
| `im.typing.start` | 开始输入 | `{ conversation_id }` |
|
||||
| `im.typing.stop` | 停止输入 | `{ conversation_id }` |
|
||||
| `im.conversation.sync` | 请求同步离线消息 | `{ conversation_id, last_msg_id }` |
|
||||
|
||||
### 5.2 服务端 → 客户端(ACK 响应)
|
||||
|
||||
使用现有 `Response` 结构体(`event` + `.ack` 后缀):
|
||||
|
||||
| 事件 | 说明 | Data 字段 |
|
||||
|------|------|-----------|
|
||||
| `im.message.send.ack` | 发送成功确认 | `{ message_id, conversation_id, created_at }` |
|
||||
| `im.message.recall.ack` | 撤回成功确认 | `{ message_id }` |
|
||||
|
||||
### 5.3 服务端 → 客户端(推送类)
|
||||
|
||||
使用现有 `PushMessage` 结构体:
|
||||
|
||||
| 事件 | 说明 | Data 字段 |
|
||||
|------|------|-----------|
|
||||
| `im.message.new` | 新消息推送 | `{ message_id, conversation_id, sender_id, sender_name, sender_avatar, type, content, created_at }` |
|
||||
| `im.message.recalled` | 消息被撤回 | `{ message_id, conversation_id, sender_id }` |
|
||||
| `im.typing.notify` | 正在输入通知 | `{ conversation_id, user_id, is_typing }` |
|
||||
| `im.conversation.unread` | 离线未读摘要 | `{ conversations: [{ id, type, unread_count, last_msg_content, last_msg_time, peer_user_id, peer_nickname, peer_avatar }] }` |
|
||||
|
||||
---
|
||||
|
||||
## 六、REST API 设计(查询类)
|
||||
|
||||
所有查询类操作仍走 REST,前缀 `/api/v1/im/`:
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/conversations` | 获取会话列表(含未读数、最后消息、对方信息) |
|
||||
| GET | `/conversations/:id/messages` | 获取历史消息(游标分页:`before_id` + `limit`) |
|
||||
| PUT | `/conversations/:id/pin` | 置顶/取消置顶 |
|
||||
| DELETE | `/conversations/:id` | 删除会话(软删除) |
|
||||
| DELETE | `/conversations/:id/messages` | 清空聊天记录 |
|
||||
| PUT | `/conversations/:id/read` | 标记会话已读(清零未读数 + 清 Redis) |
|
||||
| GET | `/messages/search?keyword=xxx` | 全局消息搜索(结果按会话分组) |
|
||||
|
||||
共 **7 个 REST API**,均需 JWT 认证。
|
||||
|
||||
---
|
||||
|
||||
## 七、后端模块设计
|
||||
|
||||
### 7.1 IM 模块目录结构
|
||||
|
||||
```
|
||||
app/im/
|
||||
├── controller/
|
||||
│ └── im_controller.go # REST API 控制器
|
||||
├── service/
|
||||
│ └── im_service.go # 核心业务逻辑
|
||||
├── dao/
|
||||
│ ├── conversation_dao.go # 会话 CRUD
|
||||
│ └── message_dao.go # 消息 CRUD + 搜索
|
||||
├── model/
|
||||
│ ├── conversation.go # im_conversations GORM 模型
|
||||
│ ├── conversation_member.go # im_conversation_members 模型
|
||||
│ └── message.go # im_messages 模型
|
||||
├── handler/
|
||||
│ └── message_handler.go # WS 事件处理器
|
||||
├── router.go # REST 路由注册
|
||||
└── provider.go # Wire ProviderSet
|
||||
```
|
||||
|
||||
### 7.2 依赖注入设计
|
||||
|
||||
```go
|
||||
type IMService struct {
|
||||
conversationDAO *dao.ConversationDAO
|
||||
messageDAO *dao.MessageDAO
|
||||
pubsub *ws.PubSub // 复用 Phase 2a
|
||||
friendChecker FriendChecker // 接口注入 → FriendshipDAO.IsFriend
|
||||
userInfoGetter UserInfoGetter // 接口注入 → FriendshipDAO.GetUsersByIDs
|
||||
}
|
||||
```
|
||||
|
||||
Wire 注入链:`contact.FriendshipDAO` → 隐式实现 `FriendChecker` + `UserInfoGetter`。
|
||||
|
||||
### 7.3 IMService 核心方法
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `SendMessage(ctx, senderID, targetUserID, content, clientMsgID)` | 发送消息(含创建会话、写库、推送) |
|
||||
| `RecallMessage(ctx, userID, messageID, conversationID)` | 撤回消息 |
|
||||
| `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)` | 标记已读 |
|
||||
| `SearchMessages(ctx, userID, keyword)` | 全局搜索 |
|
||||
| `PushOfflineMessages(ctx, userID)` | 推送离线未读摘要 |
|
||||
| `FindOrCreateConversation(ctx, userID, targetUserID)` | 查找/创建单聊会话 |
|
||||
|
||||
---
|
||||
|
||||
## 八、前端设计
|
||||
|
||||
### 8.1 页面结构
|
||||
|
||||
```
|
||||
frontend/src/pages/chat/
|
||||
├── index.vue # 会话列表页(消息 Tab 主页面)
|
||||
├── conversation.vue # 聊天页(消息收发界面)
|
||||
├── settings.vue # 会话设置页
|
||||
└── search.vue # 消息搜索页
|
||||
```
|
||||
|
||||
### 8.2 Store 设计(store/chat.js)
|
||||
|
||||
**状态:**
|
||||
- `conversations`:会话列表(按置顶 + 最后消息时间排序)
|
||||
- `currentConversation`:当前打开的会话
|
||||
- `messages`:当前会话的消息列表(内存缓存最新 N 条)
|
||||
- `totalUnread`:总未读消息数
|
||||
- `typingUsers`:正在输入的用户 Map
|
||||
|
||||
**Actions:**
|
||||
- `loadConversations()`:REST 拉取会话列表
|
||||
- `sendMessage(targetUserId, content)`:WS 发送消息
|
||||
- `loadHistory(conversationId, beforeId)`:REST 拉取历史消息
|
||||
- `markAsRead(conversationId)`:标记已读 + 清 Redis
|
||||
- `recallMessage(messageId, conversationId)`:撤回消息
|
||||
- `pinConversation(id, isPinned)`:置顶/取消置顶
|
||||
- `deleteConversation(id)`:删除会话
|
||||
- `clearMessages(id)`:清空聊天记录
|
||||
- `searchMessages(keyword)`:全局搜索
|
||||
|
||||
**WS 事件监听:**
|
||||
- `im.message.new` → 更新 conversations + messages + totalUnread
|
||||
- `im.message.recalled` → 更新消息状态为"已撤回"
|
||||
- `im.typing.notify` → 更新 typingUsers
|
||||
- `im.conversation.unread` → 初始化未读数 + 会话列表
|
||||
|
||||
### 8.3 API 封装(api/chat.js)
|
||||
|
||||
封装 7 个 REST API 的请求函数。
|
||||
|
||||
### 8.4 页面功能描述
|
||||
|
||||
**会话列表页 (index.vue)**:
|
||||
- 顶部搜索栏(跳转消息搜索页)
|
||||
- 会话列表:头像 + 昵称 + 最后消息预览 + 时间 + 未读 badge + 置顶标记
|
||||
- 长按操作菜单:置顶/取消置顶、删除会话、清空聊天记录
|
||||
- 空状态提示("暂无消息,去找好友聊聊吧")
|
||||
|
||||
**聊天页 (conversation.vue)**:
|
||||
- 顶部导航:对方昵称 + "正在输入…"指示 + 设置入口
|
||||
- 消息列表:支持向上滚动加载历史消息
|
||||
- 消息气泡:自己(右侧蓝)/ 对方(左侧灰)
|
||||
- 消息状态图标:发送中(loading) / 已发送(check) / 失败(retry 可重发)
|
||||
- 长按消息:撤回(2 分钟内自己的消息)
|
||||
- 底部输入区:文本框 + 发送按钮
|
||||
- 输入时自动触发 typing 事件(节流 3s)
|
||||
|
||||
**会话设置页 (settings.vue)**:
|
||||
- 对方信息卡片(头像 + 昵称 + 用户名)
|
||||
- 置顶聊天开关
|
||||
- 清空聊天记录按钮(二次确认)
|
||||
- 删除会话按钮(二次确认)
|
||||
|
||||
**消息搜索页 (search.vue)**:
|
||||
- 搜索输入框(防抖 300ms)
|
||||
- 搜索结果按会话分组展示(每组:会话名 + 匹配消息摘要列表)
|
||||
- 点击结果跳转到对应聊天页
|
||||
|
||||
所有前端页面使用 **ui-ux-pro-max** 技能包进行设计和开发。
|
||||
|
||||
---
|
||||
|
||||
## 九、联系人模块改造
|
||||
|
||||
### 9.1 好友详情页 (contact/detail.vue)
|
||||
|
||||
将现有 `sendMessage()` 占位方法改为跳转聊天页:
|
||||
|
||||
```javascript
|
||||
const sendMessage = () => {
|
||||
uni.navigateTo({ url: `/pages/chat/conversation?userId=${friend.value.user_id}` })
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 好友列表页 (contact/index.vue)
|
||||
|
||||
好友列表项添加点击事件,点击直接跳转到聊天页。
|
||||
|
||||
### 9.3 CustomTabBar 改造
|
||||
|
||||
- 新增消息 Tab(跳转 `/pages/chat/index`)
|
||||
- 消息 Tab 添加未读数 badge(从 `chatStore.totalUnread` 读取)
|
||||
|
||||
---
|
||||
|
||||
## 十、关键设计决策
|
||||
|
||||
| # | 决策 | 选择 | 理由 |
|
||||
|---|------|------|------|
|
||||
| 1 | 事件路由表 vs switch-case | 事件路由表 | 模块化注册,便于后续 Phase 扩展 |
|
||||
| 2 | 发送走 WS vs REST | 全双工 WS | 实时体验更好,延迟更低 |
|
||||
| 3 | 离线消息 | 服务端主动推送 | 用户体验好,无需手动刷新 |
|
||||
| 4 | 历史消息分页 | 游标分页 (before_id) | 避免 OFFSET 性能劣化 |
|
||||
| 5 | 消息幂等 | client_msg_id | 防止网络重试导致重复消息 |
|
||||
| 6 | 单聊去重 | Service 层查询 | 灵活,不依赖数据库 unique constraint |
|
||||
| 7 | 消息撤回时限 | 2 分钟 | 参考微信等主流 IM |
|
||||
|
||||
---
|
||||
|
||||
## 十一、Task 拆分(10 个 Task)
|
||||
|
||||
| Task | 描述 | 涉及端 |
|
||||
|------|------|--------|
|
||||
| Task 0 | 设计文档 + 新分支 + 数据库表迁移 | 后端 |
|
||||
| Task 1 | WS 事件路由表机制(改造 pkg/ws + app/ws/handler) | 后端 |
|
||||
| Task 2 | IM Model + DAO 层 | 后端 |
|
||||
| Task 3 | IM Service 核心业务(发消息、撤回、会话管理) | 后端 |
|
||||
| Task 4 | IM WS 事件处理器 + 离线消息推送 | 后端 |
|
||||
| Task 5 | IM REST Controller + Router + Wire 集成 | 后端 |
|
||||
| Task 6 | 前台 chat Store + API 封装 + WS 事件集成 | 前端 |
|
||||
| Task 7 | 前台会话列表页 + 聊天页 | 前端 |
|
||||
| Task 8 | 前台会话设置页 + 消息搜索页 + 联系人模块改造 | 前端 |
|
||||
| Task 9 | 集成测试 + 文档更新 + 代码审查 | 全端 |
|
||||
Reference in New Issue
Block a user