feat:单聊页面bug修复+后台管理端好友页面bug修复+项目进度、记忆文件更新

This commit is contained in:
bujinyuan
2026-03-04 10:03:16 +08:00
parent 7b1d061955
commit 9399f86a19
30 changed files with 838 additions and 530 deletions

View File

@@ -353,93 +353,101 @@ COMMENT ON COLUMN contact_groups.sort_order IS '排序权重,数值越小越
COMMENT ON COLUMN contact_groups.created_at IS '创建时间';
```
#### im 模块 — 即时通讯(统一会话模型)
#### im 模块 — 即时通讯(统一会话模型)✅ Phase 2b 已实现
单聊和群聊统一抽象为"会话"本质都是"一组人在一个空间里收发消息"。这是微信钉钉Slack 等主流 IM 的标准模型
> **Phase 2b 实际实现说明:** 当前已实现单聊功能,表结构已针对实际需求进行了优化(冗余 last_msg_* 字段提升查询效率,新增 clear_before_msg_id 实现个人视图清空,新增 client_msg_id 实现消息幂等。群聊相关字段name/avatar/owner_id/max_members/role/nickname/is_muted保留在总体设计中将在 Phase 2c 实现。
```sql
-- ============================================================
-- im_conversations: 会话表
-- 统一抽象单聊和群聊,单聊时 name/avatar 为空(前端用对方信息展示)
-- im_conversations: 会话表Phase 2b 实际实现版本)
-- 统一抽象单聊和群聊,含冗余 last_msg_* 字段避免 JOIN 查询
-- ============================================================
CREATE TABLE im_conversations (
id BIGSERIAL PRIMARY KEY,
type SMALLINT NOT NULL,
name VARCHAR(100) DEFAULT '',
avatar VARCHAR(500) DEFAULT '',
owner_id BIGINT DEFAULT NULL,
max_members INT NOT NULL DEFAULT 200,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
id BIGSERIAL PRIMARY KEY,
type SMALLINT NOT NULL DEFAULT 1,
creator_id BIGINT NOT NULL,
last_message_id BIGINT DEFAULT NULL,
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()
);
COMMENT ON TABLE im_conversations IS '会话表,统一管理单聊和群聊会话';
COMMENT ON COLUMN im_conversations.id IS '会话唯一标识';
COMMENT ON COLUMN im_conversations.type IS '会话类型1=单聊两人私聊2=群聊(多人群组';
COMMENT ON COLUMN im_conversations.name IS '会话名称,群聊时为群名,单聊时为空(前端取对方昵称展示)';
COMMENT ON COLUMN im_conversations.avatar IS '会话头像 URL群聊时为群头像单聊时为空前端取对方头像展示';
COMMENT ON COLUMN im_conversations.owner_id IS '群主用户 ID仅群聊时有值单聊时为 NULL';
COMMENT ON COLUMN im_conversations.max_members IS '最大成员数单聊固定为2群聊默认200';
COMMENT ON COLUMN im_conversations.status IS '会话状态1=正常2=已解散(仅群聊可解散';
COMMENT ON COLUMN im_conversations.created_at IS '会话创建时间';
COMMENT ON COLUMN im_conversations.updated_at IS '最后更新时间';
COMMENT ON TABLE im_conversations IS '会话表,统一管理单聊和群聊会话';
COMMENT ON COLUMN im_conversations.id IS '会话唯一标识';
COMMENT ON COLUMN im_conversations.type IS '会话类型1=单聊2=群聊Phase 2c';
COMMENT ON COLUMN im_conversations.creator_id IS '会话创建者用户 ID';
COMMENT ON COLUMN im_conversations.last_message_id IS '最后一条消息 ID冗余字段避免 JOIN';
COMMENT ON COLUMN im_conversations.last_msg_content IS '最后消息预览文本(冗余字段)';
COMMENT ON COLUMN im_conversations.last_msg_time IS '最后消息时间(冗余字段,用于排序)';
COMMENT ON COLUMN im_conversations.last_msg_sender_id IS '最后消息发送者 ID冗余字段';
CREATE INDEX idx_im_conversations_updated ON im_conversations(updated_at DESC);
-- ============================================================
-- im_conversation_members: 会话成员表
-- 记录每个会话中的参与成员及其个性化设置
-- im_conversation_members: 会话成员表Phase 2b 实际实现版本)
-- 记录每个会话中的参与成员及其个人视图设置
-- ============================================================
CREATE TABLE im_conversation_members (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL REFERENCES im_conversations(id),
user_id BIGINT NOT NULL REFERENCES auth_users(id),
role SMALLINT NOT NULL DEFAULT 0,
nickname VARCHAR(50) DEFAULT '',
is_muted BOOLEAN NOT NULL DEFAULT FALSE,
is_pinned BOOLEAN NOT NULL DEFAULT FALSE,
last_read_msg_id BIGINT DEFAULT 0,
joined_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (conversation_id, user_id)
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL REFERENCES im_conversations(id),
user_id BIGINT NOT NULL,
is_pinned BOOLEAN DEFAULT FALSE,
is_deleted BOOLEAN DEFAULT FALSE,
unread_count INT DEFAULT 0,
last_read_msg_id BIGINT DEFAULT 0,
clear_before_msg_id BIGINT DEFAULT 0,
created_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
UNIQUE(conversation_id, user_id)
);
COMMENT ON TABLE im_conversation_members IS '会话成员表,记录成员列表及每人的个性化设置';
COMMENT ON COLUMN im_conversation_members.id IS '记录唯一标识';
COMMENT ON COLUMN im_conversation_members.conversation_id IS '所属会话 ID';
COMMENT ON COLUMN im_conversation_members.user_id IS '成员用户 ID';
COMMENT ON COLUMN im_conversation_members.role IS '成员角色0=普通成员1=管理员群聊可设置2=群主';
COMMENT ON COLUMN im_conversation_members.nickname IS '群内昵称,仅在该群聊中生效,为空则使用用户全局昵称';
COMMENT ON COLUMN im_conversation_members.is_muted IS '是否被禁言false=正常发言true=已被禁言(仅管理员/群主可操作)';
COMMENT ON COLUMN im_conversation_members.is_pinned IS '是否置顶该会话false=不置顶true=置顶(个人设置,不影响他人)';
COMMENT ON COLUMN im_conversation_members.last_read_msg_id IS '最后已读消息 ID用于计算未读消息';
COMMENT ON COLUMN im_conversation_members.joined_at IS '加入会话的时间';
COMMENT ON TABLE im_conversation_members IS '会话成员表,记录成员列表及个人视图设置';
COMMENT ON COLUMN im_conversation_members.id IS '记录唯一标识';
COMMENT ON COLUMN im_conversation_members.conversation_id IS '所属会话 ID';
COMMENT ON COLUMN im_conversation_members.user_id IS '成员用户 ID';
COMMENT ON COLUMN im_conversation_members.is_pinned IS '是否置顶(个人设置)';
COMMENT ON COLUMN im_conversation_members.is_deleted IS '是否软删除(个人视图,不影响对方)';
COMMENT ON COLUMN im_conversation_members.unread_count IS '未读消息数';
COMMENT ON COLUMN im_conversation_members.last_read_msg_id IS '最后已读消息 ID';
COMMENT ON COLUMN im_conversation_members.clear_before_msg_id IS '清空记录截止消息 ID个人视图查询时过滤 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);
-- ============================================================
-- im_messages: 消息表
-- 系统数据量最大的表,存储所有聊天消息内容
-- im_messages: 消息表Phase 2b 实际实现版本)
-- 系统数据量最大的表,含 client_msg_id 实现幂等 + GIN 全文搜索索引
-- ============================================================
CREATE TABLE im_messages (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL,
sender_id BIGINT NOT NULL,
conversation_id BIGINT NOT NULL REFERENCES im_conversations(id),
sender_id BIGINT NOT NULL,
type SMALLINT NOT NULL DEFAULT 1,
content TEXT NOT NULL DEFAULT '',
extra JSONB DEFAULT '{}',
content TEXT NOT NULL,
extra JSONB DEFAULT NULL,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
client_msg_id VARCHAR(64) DEFAULT '',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
COMMENT ON TABLE im_messages IS '聊天消息表,存储所有会话的消息记录(系统数据量最大的表)';
COMMENT ON TABLE im_messages IS '聊天消息表,存储所有会话的消息记录';
COMMENT ON COLUMN im_messages.id IS '消息唯一标识,全局自增';
COMMENT ON COLUMN im_messages.conversation_id IS '所属会话 ID';
COMMENT ON COLUMN im_messages.sender_id IS '发送者用户 ID';
COMMENT ON COLUMN im_messages.type IS '消息类型1=文本消息2=图片消息3=文件消息4=语音消息,5=系统通知消息';
COMMENT ON COLUMN im_messages.content IS '消息内容,文本消息为文字,其他类型为描述文字或为空';
COMMENT ON COLUMN im_messages.extra IS '附加数据JSON图片消息存 {url,width,height},文件消息存 {url,name,size},语音消息存 {url,duration}';
COMMENT ON COLUMN im_messages.status IS '消息状态1=正常2=已撤回发送者撤回3=已删除(管理员删除)';
COMMENT ON COLUMN im_messages.type IS '消息类型1=文本(预留 2=图片 3=语音 4=文件 5=系统通知';
COMMENT ON COLUMN im_messages.content IS '消息内容';
COMMENT ON COLUMN im_messages.extra IS '附加数据JSON预留扩展';
COMMENT ON COLUMN im_messages.status IS '消息状态1=正常2=已撤回3=已删除';
COMMENT ON COLUMN im_messages.client_msg_id IS '客户端消息唯一 ID用于幂等去重';
COMMENT ON COLUMN im_messages.created_at IS '消息发送时间';
CREATE INDEX idx_im_messages_conv_time ON im_messages(conversation_id, created_at DESC);
COMMENT ON INDEX idx_im_messages_conv_time IS '会话消息时间索引,用于按时间倒序查询会话历史消息';
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));
```
#### meeting 模块 — 音视频会议
@@ -586,9 +594,8 @@ echo:auth:refresh:{client_type}:{user_id} → Refresh Token (STRING, TTL 由
echo:user:online → 在线用户集合 (SET)
echo:user:status:{user_id} → 用户状态 JSON (STRING, TTL 自动过期)
# 即时通讯
echo:im:unread:{user_id} → 各会话未读数 (HASH: conv_id → count)
echo:im:typing:{conversation_id} → 正在输入的用户 (SET, TTL 5秒)
# 即时通讯Phase 2b 实际实现)
echo:im:unread:{user_id} → 全局未读消息总数 (STRINGLua 脚本原子递减,下限 0)
# 会议实时状态
echo:meeting:room:{room_code} → 房间实时状态 JSON (STRING)
@@ -702,13 +709,14 @@ GET /api/v1/contacts/recommend 好友推荐
# 在线状态
GET /api/v1/contacts/online 批量查询好友在线状态
# 即时通讯模块
GET /api/v1/conversations
POST /api/v1/conversations
GET /api/v1/conversations/:id
GET /api/v1/conversations/:id/messages
POST /api/v1/conversations/:id/members
DELETE /api/v1/conversations/:id/members/:uid
# 即时通讯模块Phase 2b 已实现)
GET /api/v1/im/conversations 会话列表(含未读数、对方信息)
GET /api/v1/im/messages 历史消息(游标分页 before_id + limit
PUT /api/v1/im/conversations/:id/pin 置顶/取消置顶
DELETE /api/v1/im/conversations/:id 删除会话(软删除)
DELETE /api/v1/im/conversations/:id/messages 清空聊天记录(个人视图)
GET /api/v1/im/messages/search 全局消息搜索GIN 全文索引)
GET /api/v1/im/unread 全局未读消息总数
# 会议模块
POST /api/v1/meetings
@@ -811,9 +819,11 @@ pages/
│ ├── register.vue # 注册页
│ └── forgot-password.vue # 忘记密码
├── chat/
│ ├── index.vue # 会话列表
│ ├── conversation.vue # 聊天对话页
── group-create.vue # 创建群聊
│ ├── index.vue # 会话列表 ✅ Phase 2b
│ ├── conversation.vue # 聊天对话页 ✅ Phase 2b
── settings.vue # 聊天设置页 ✅ Phase 2b
│ ├── search.vue # 消息搜索页 ✅ Phase 2b
│ └── group-create.vue # 创建群聊 📋 Phase 2c
├── contact/
│ ├── index.vue # 联系人列表(含搜索/在线状态) ✅ Phase 2a
│ ├── search.vue # 搜索添加好友 + 好友推荐 ✅ Phase 2a
@@ -937,10 +947,25 @@ services:
- 在线状态管理混合推拉方案
- 管理端扩展在线监控 + 好友关系管理
#### Phase 2b即时通讯消息系统 🔜 待开发
- 即时聊天单聊 + 群聊文字/图片/文件
#### Phase 2b即时通讯消息系统 ✅ 已完成
- 单聊即时通讯WebSocket 全双工消息收发三态 ACK 确认
- 会话管理自动创建置顶软删除清空聊天记录 个人视图
- 消息撤回2 分钟内)、正在输入提示
- 离线消息推送WS 连接后服务端主动推送未读摘要
- 未读消息管理会话 badge + TabBar 总未读数Lua 脚本原子操作
- 全局消息搜索PostgreSQL GIN 全文索引
- WS 事件路由表机制Hub.RegisterEvent/DispatchEvent
- 前台 4 个页面 + chat Store + API 封装
- 设计文档`docs/plans/2026-03-03-phase2b-design.md`
#### Phase 2c会议与通知 📋 待规划
#### Phase 2c群聊与增强 📋 待规划
- 群聊会话建群/加入/退出/管理
- 群消息收发
- 已读回执单聊 + 群聊
- 消息类型扩展图片/语音/文件
- 管理端消息管理功能
#### Phase 2d会议与通知 📋 待规划
- 多人音视频会议即时会议 + 预约会议
- 消息通知系统

View File

@@ -1,10 +1,10 @@
# Phase 2b 设计文档:即时通讯消息系统(单聊)
> **状态:** 📋 设计完成,待实施
> **状态:** ✅ 已完成(含代码审查修复)
> **分支:** `feature/phase2b-instant-messaging`
> **前置依赖:** Phase 2a 全部完成WebSocket + 联系人管理)
> **架构备忘:** `docs/plans/2026-03-02-phase2b-architecture-notes.md`
> **最后更新:** 2026-03-03
> **最后更新:** 2026-03-03(含代码审查修复 7 项 + 用户测试修复 8 项 + 文档同步)
---
@@ -98,7 +98,7 @@ func (h *Hub) RegisterEvent(event string, handler EventHandler)
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`
IM 模块在启动时向 Hub 注册事件:`im.message.send``im.message.recall``im.conversation.read``im.typing`
### 3.3 离线消息推送
@@ -117,14 +117,14 @@ IM 模块在启动时向 Hub 注册事件:`im.message.send`、`im.message.reca
```
发送方 Client
│ WS: im.message.recall { message_id, conversation_id }
│ WS: im.message.recall { message_id }
IM Service.RecallMessage()
├── 校验:是否为自己的消息、是否在 2 分钟内
├── DB: UPDATE im_messages SET status = 2(已撤回)
├── DB: UPDATE im_conversations (last_msg_content = "XX 撤回了一条消息")
├── 若被撤回的是会话最后一条消息 → UPDATE im_conversations (last_msg_content = "XX 撤回了一条消息")
├── WS Response: im.message.recall.ack { message_id }
└── PubSub.PublishToUser(receiverID, im.message.recalled)
└── PubSub.PublishToUser(receiverID, im.message.recalled { message_id, conversation_id, sender_id })
```
### 3.5 消息收发时序图
@@ -198,15 +198,16 @@ CREATE INDEX idx_im_conversations_updated ON im_conversations(updated_at DESC);
```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(),
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
clear_before_msg_id BIGINT DEFAULT 0, -- 清空记录截止消息 ID个人视图不影响对方
created_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
UNIQUE(conversation_id, user_id)
);
@@ -215,6 +216,8 @@ CREATE INDEX idx_im_conv_members_user ON im_conversation_members(user_id, is_del
CREATE INDEX idx_im_conv_members_conv ON im_conversation_members(conversation_id);
```
> **设计说明**`clear_before_msg_id` 用于实现"清空聊天记录"的个人视图。用户 A 清空记录后,仅 A 看不到清空前的消息,用户 B 不受影响。查询历史消息时增加 `id > clear_before_msg_id` 过滤条件。
### 4.3 im_messages消息表
```sql
@@ -253,11 +256,13 @@ LIMIT 1
| Key | 类型 | 说明 |
|-----|------|------|
| `echo:im:unread:{user_id}` | HASH | 每个会话的未读数 `{ conv_id: count }` |
| `echo:im:unread:{user_id}` | STRING | 全局未读消息总数(会话级别的未读数存储在 DB `im_conversation_members.unread_count` |
- 收到新消息:`HINCRBY echo:im:unread:{user_id} {conv_id} 1`
- 标记已读`HDEL echo:im:unread:{user_id} {conv_id}`
- 获取总未读数:`HVALS echo:im:unread:{user_id}` 求和
- 收到新消息:`INCR echo:im:unread:{user_id}`(全局 +1
- 标记已读/清空/删除会话Lua 脚本原子递减(下限为 0防止负数
- 获取总未读数:`GET echo:im:unread:{user_id}`
> **设计说明**:选择 STRING 而非 HASH 的原因DB 已有会话级别的 `unread_count` 字段Redis 仅用于 TabBar badge 的快速读取不需要冗余存储每个会话的未读数。Lua 脚本保证递减操作的原子性和下限保护。
---
@@ -267,11 +272,12 @@ LIMIT 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 }` |
| `im.message.send` | 发送消息 | `{ conversation_id, target_user_id, content, type, client_msg_id }` |
| `im.message.recall` | 撤回消息 | `{ message_id }` |
| `im.conversation.read` | 标记已读 | `{ conversation_id }` |
| `im.typing` | 正在输入 | `{ conversation_id }` |
> **说明**`im.message.send` 中 `conversation_id` 和 `target_user_id` 二选一。首次发消息时使用 `target_user_id`(自动创建会话),后续使用 `conversation_id`。
### 5.2 服务端 → 客户端ACK 响应)
@@ -288,10 +294,12 @@ LIMIT 1
| 事件 | 说明 | Data 字段 |
|------|------|-----------|
| `im.message.new` | 新消息推送 | `{ message_id, conversation_id, sender_id, sender_name, sender_avatar, type, content, created_at }` |
| `im.message.new` | 新消息推送 | `{ id, conversation_id, sender_id, sender_name, sender_avatar, type, content, client_msg_id, 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 }] }` |
| `im.typing` | 正在输入通知 | `{ conversation_id, user_id }` |
| `im.offline.sync` | 离线未读摘要 | `{ total_unread, conversations: [{ conversation_id, unread_count, last_msg_content, last_msg_time }] }` |
> **说明**:正在输入通知前端收到后设置 3 秒超时自动清除。离线同步事件在 WS 连接成功后由服务端主动推送。
---
@@ -302,14 +310,14 @@ LIMIT 1
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/conversations` | 获取会话列表(含未读数、最后消息、对方信息) |
| GET | `/conversations/:id/messages` | 获取历史消息(游标分页:`before_id` + `limit` |
| GET | `/messages?conversation_id=xx&before_id=xx&limit=30` | 获取历史消息(游标分页 |
| PUT | `/conversations/:id/pin` | 置顶/取消置顶 |
| DELETE | `/conversations/:id` | 删除会话(软删除) |
| DELETE | `/conversations/:id/messages` | 清空聊天记录 |
| PUT | `/conversations/:id/read` | 标记会话已读(清零未读数 + 清 Redis |
| GET | `/messages/search?keyword=xxx` | 全局消息搜索(结果按会话分组) |
| DELETE | `/conversations/:id/messages` | 清空聊天记录(个人视图,不影响对方) |
| GET | `/messages/search?keyword=xxx&limit=50` | 全局消息搜索GIN 全文索引 |
| GET | `/unread` | 获取全局未读消息总数 |
**7 个 REST API**,均需 JWT 认证。
**7 个 REST API**,均需 JWT 认证。标记已读通过 WS 事件 `im.conversation.read` 实现。
---
@@ -363,7 +371,7 @@ Wire 注入链:
| `GetMessages(ctx, userID, conversationID, beforeID, limit)` | 获取历史消息(游标分页) |
| `PinConversation(ctx, userID, conversationID, isPinned)` | 置顶/取消置顶 |
| `DeleteConversation(ctx, userID, conversationID)` | 删除会话(软删除) |
| `ClearMessages(ctx, userID, conversationID)` | 清空聊天记录 |
| `ClearMessages(ctx, userID, conversationID)` | 清空聊天记录个人视图ClearBeforeMsgID |
| `MarkAsRead(ctx, userID, conversationID)` | 标记已读 |
| `SearchMessages(ctx, userID, keyword)` | 全局搜索 |
| `PushOfflineMessages(ctx, userID)` | 推送离线未读摘要 |
@@ -406,8 +414,8 @@ frontend/src/pages/chat/
**WS 事件监听:**
- `im.message.new` → 更新 conversations + messages + totalUnread
- `im.message.recalled` → 更新消息状态为"已撤回"
- `im.typing.notify` → 更新 typingUsers
- `im.conversation.unread` → 初始化未读数 + 会话列表
- `im.typing` → 更新 typingUsers3 秒自动清除)
- `im.offline.sync` → 初始化未读数 + 刷新会话列表
### 8.3 API 封装api/chat.js