feat:单聊页面bug修复+后台管理端好友页面bug修复+项目进度、记忆文件更新
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
| [frontend/auth.md](frontend/auth.md) | 用户认证 | ✅ Phase 1 | 注册、登录、Token 刷新、个人信息管理 |
|
||||
| [frontend/contact.md](frontend/contact.md) | 联系人 | ✅ Phase 2a | 17 个 API:好友申请/管理、好友分组、黑名单、搜索/推荐、在线状态 |
|
||||
| [frontend/websocket.md](frontend/websocket.md) | WebSocket | ✅ Phase 2a | 前端 WebSocket 连接管理、事件协议、心跳、重连 |
|
||||
| [frontend/im.md](frontend/im.md) | 即时通讯 | 📋 Phase 2b | 会话列表、消息历史、群聊创建与管理 |
|
||||
| [frontend/im.md](frontend/im.md) | 即时通讯 | ✅ Phase 2b | 7 个 API:会话列表/置顶/删除/清空、历史消息、全局搜索、未读数 |
|
||||
| [frontend/meeting.md](frontend/meeting.md) | 会议 | 📋 后续 | 即时会议、预约会议、加入/离开、会议列表 |
|
||||
| [frontend/notify.md](frontend/notify.md) | 通知 | 📋 后续 | 通知列表、标记已读 |
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
| 文档 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| [websocket.md](websocket.md) | ✅ Phase 2a | WebSocket 实时事件协议(联系人通知、在线状态、心跳) |
|
||||
| [websocket.md](websocket.md) | ✅ Phase 2a/2b | WebSocket 实时事件协议(IM 消息收发/撤回/已读/输入、联系人通知、在线状态、心跳) |
|
||||
|
||||
---
|
||||
|
||||
@@ -194,7 +194,7 @@ docs/api/
|
||||
│ ├── auth.md # 用户认证 ✅ Phase 1
|
||||
│ ├── contact.md # 联系人管理(17 个 API) ✅ Phase 2a
|
||||
│ ├── websocket.md # WebSocket 事件协议 ✅ Phase 2a
|
||||
│ ├── im.md # 即时通讯 📋 Phase 2b
|
||||
│ ├── im.md # 即时通讯(7 个 API) ✅ Phase 2b
|
||||
│ ├── meeting.md # 会议 📋 后续
|
||||
│ └── notify.md # 通知 📋 后续
|
||||
├── admin/ # 后台管理端 API
|
||||
@@ -204,5 +204,5 @@ docs/api/
|
||||
│ ├── contact.md # 好友关系管理 ✅ Phase 2a
|
||||
│ ├── meeting.md # 会议管理 📋 后续
|
||||
│ └── system.md # 系统管理 📋 待定
|
||||
└── websocket.md # WebSocket 全量事件协议 ✅ Phase 2a
|
||||
└── websocket.md # WebSocket 全量事件协议 ✅ Phase 2a/2b
|
||||
```
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# 即时通讯模块 API (IM)
|
||||
# 即时通讯模块 REST API (IM)
|
||||
|
||||
> 通用规范(认证方式、响应格式、错误码)见 [README.md](../README.md)
|
||||
> 消息的实时收发通过 WebSocket 完成,见 [websocket.md](../websocket.md)
|
||||
> 消息的实时收发(发送/撤回/标记已读/正在输入)通过 WebSocket 完成,见 [websocket.md](../websocket.md)
|
||||
> 本文档中的接口用于会话管理和消息历史查询等非实时操作。
|
||||
> **最后更新:** 2026-03-03(代码审查修复后同步)
|
||||
|
||||
---
|
||||
|
||||
@@ -10,166 +11,84 @@
|
||||
|
||||
| 方法 | 路径 | 权限 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 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 | 需认证 | 移除群聊成员 |
|
||||
| GET | /api/v1/im/conversations | 需认证 | 获取会话列表 |
|
||||
| GET | /api/v1/im/messages | 需认证 | 获取历史消息(游标分页) |
|
||||
| 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 | 需认证 | 全局消息搜索 |
|
||||
| GET | /api/v1/im/unread | 需认证 | 获取全局未读消息总数 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 获取会话列表
|
||||
|
||||
`GET /api/v1/conversations`
|
||||
`GET /api/v1/im/conversations`
|
||||
|
||||
**权限:** 需认证
|
||||
|
||||
**说明:** 返回当前用户的所有会话,按最后消息时间倒序排列。单聊会话的 `name`/`avatar` 为空,前端应使用 `target_user` 的信息展示。
|
||||
**说明:** 返回当前用户的所有会话,排序:置顶优先 → 最后消息时间降序。已软删除的会话不返回。通过 LEFT JOIN 一次获取单聊对方用户 ID,避免 N+1 查询。
|
||||
|
||||
**成功响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": 1,
|
||||
"name": "",
|
||||
"avatar": "",
|
||||
"target_user": {
|
||||
"id": 2,
|
||||
"nickname": "李四",
|
||||
"avatar": "https://cdn.echochat.com/avatar/2.jpg",
|
||||
"online": true
|
||||
},
|
||||
"last_message": {
|
||||
"id": 100,
|
||||
"type": 1,
|
||||
"content": "你好",
|
||||
"sender_id": 2,
|
||||
"created_at": "2026-02-27 10:30:00"
|
||||
},
|
||||
"unread_count": 3,
|
||||
"is_pinned": false
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": 2,
|
||||
"name": "产品讨论组",
|
||||
"avatar": "https://cdn.echochat.com/group/5.jpg",
|
||||
"target_user": null,
|
||||
"last_message": {
|
||||
"id": 205,
|
||||
"type": 1,
|
||||
"content": "明天开会",
|
||||
"sender_id": 3,
|
||||
"created_at": "2026-02-27 11:00:00"
|
||||
},
|
||||
"unread_count": 0,
|
||||
"is_pinned": true,
|
||||
"member_count": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 创建群聊
|
||||
|
||||
`POST /api/v1/conversations`
|
||||
|
||||
**权限:** 需认证
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| name | string | 是 | 群聊名称 |
|
||||
| member_ids | int[] | 是 | 初始成员用户 ID 列表(不含自己,至少 2 人) |
|
||||
|
||||
**说明:** 创建者自动成为群主(role=2),被邀请的成员为普通成员(role=0)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 获取会话详情
|
||||
|
||||
`GET /api/v1/conversations/:id`
|
||||
|
||||
**权限:** 需认证,且为该会话成员
|
||||
|
||||
**成功响应(群聊示例):**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"id": 5,
|
||||
"type": 2,
|
||||
"name": "产品讨论组",
|
||||
"avatar": "https://cdn.echochat.com/group/5.jpg",
|
||||
"owner_id": 1,
|
||||
"max_members": 200,
|
||||
"member_count": 8,
|
||||
"members": [
|
||||
{ "user_id": 1, "nickname": "张三", "role": 2, "online": true },
|
||||
{ "user_id": 2, "nickname": "李四", "role": 0, "online": false }
|
||||
],
|
||||
"created_at": "2026-02-20 09:00:00"
|
||||
"list": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": 1,
|
||||
"peer_user_id": 2,
|
||||
"peer_nickname": "李四",
|
||||
"peer_avatar": "https://...",
|
||||
"last_msg_content": "你好",
|
||||
"last_msg_time": "2026-03-03 10:30:00",
|
||||
"last_msg_sender_id": 2,
|
||||
"is_pinned": false,
|
||||
"unread_count": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 获取消息历史
|
||||
## 2. 获取历史消息
|
||||
|
||||
`GET /api/v1/conversations/:id/messages`
|
||||
`GET /api/v1/im/messages`
|
||||
|
||||
**权限:** 需认证,且为该会话成员
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| before_id | int | 无 | 获取此消息 ID 之前的消息(用于向上翻页加载历史) |
|
||||
| limit | int | 30 | 每次获取数量,最大 50 |
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| conversation_id | int | 是 | - | 会话 ID |
|
||||
| before_id | int | 否 | 0 | 游标:查询 ID 小于此值的消息,0=最新 |
|
||||
| limit | int | 否 | 30 | 每次获取数量,最大 100 |
|
||||
|
||||
**说明:** 支持 `clear_before_msg_id` 个人视图过滤(清空聊天记录后,仅过滤当前用户视图,不影响对方)。
|
||||
|
||||
**成功响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"messages": [
|
||||
{
|
||||
"id": 98,
|
||||
"sender_id": 1,
|
||||
"sender_name": "张三",
|
||||
"sender_avatar": "https://...",
|
||||
"type": 1,
|
||||
"content": "明天几点开会?",
|
||||
"extra": {},
|
||||
"status": 1,
|
||||
"created_at": "2026-02-27 10:28:00"
|
||||
},
|
||||
"list": [
|
||||
{
|
||||
"id": 99,
|
||||
"conversation_id": 1,
|
||||
"sender_id": 2,
|
||||
"sender_name": "李四",
|
||||
"sender_avatar": "https://...",
|
||||
"type": 2,
|
||||
"content": "",
|
||||
"extra": {
|
||||
"url": "https://cdn.echochat.com/img/xxx.jpg",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"thumbnail": "https://cdn.echochat.com/img/xxx_thumb.jpg"
|
||||
},
|
||||
"type": 1,
|
||||
"content": "你好",
|
||||
"status": 1,
|
||||
"created_at": "2026-02-27 10:29:00"
|
||||
"client_msg_id": "",
|
||||
"created_at": "2026-03-03 10:29:00"
|
||||
}
|
||||
],
|
||||
"has_more": true
|
||||
@@ -179,30 +98,97 @@
|
||||
|
||||
---
|
||||
|
||||
## 5. 邀请成员加入群聊
|
||||
## 3. 置顶/取消置顶会话
|
||||
|
||||
`POST /api/v1/conversations/:id/members`
|
||||
`PUT /api/v1/im/conversations/:id/pin`
|
||||
|
||||
**权限:** 需认证,且为该群聊成员
|
||||
**权限:** 需认证,且为该会话成员
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| user_ids | int[] | 是 | 要邀请的用户 ID 列表 |
|
||||
|
||||
**可能的错误码:** 3001(会话不存在),3002(非会话成员),4003(超出人数上限)
|
||||
| is_pinned | bool | 是 | true=置顶, false=取消 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 移除群聊成员
|
||||
## 4. 删除会话
|
||||
|
||||
`DELETE /api/v1/conversations/:id/members/:uid`
|
||||
`DELETE /api/v1/im/conversations/:id`
|
||||
|
||||
**权限:** 需认证,且为群主或管理员
|
||||
**权限:** 需认证,且为该会话成员
|
||||
|
||||
**路径参数:**
|
||||
- `id` — 会话 ID
|
||||
- `uid` — 被移除的用户 ID
|
||||
**说明:** 软删除,仅影响当前用户视图,不影响对方。同时清零未读数并更新 Redis 全局未读。
|
||||
|
||||
**可能的错误码:** 3001, 3002, 1003(非群主/管理员无权操作)
|
||||
---
|
||||
|
||||
## 5. 清空聊天记录
|
||||
|
||||
`DELETE /api/v1/im/conversations/:id/messages`
|
||||
|
||||
**权限:** 需认证,且为该会话成员
|
||||
|
||||
**说明:** 个人视图操作,不影响对方的消息。实现方式:记录清空时的最后消息 ID(`clear_before_msg_id`),后续查询历史消息时过滤。同时清零该会话未读数。
|
||||
|
||||
---
|
||||
|
||||
## 6. 全局消息搜索
|
||||
|
||||
`GET /api/v1/im/messages/search`
|
||||
|
||||
**权限:** 需认证
|
||||
|
||||
**查询参数:**
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| keyword | string | 是 | - | 搜索关键词 |
|
||||
| limit | int | 否 | 50 | 返回条数上限 |
|
||||
|
||||
**说明:** 使用 PostgreSQL GIN 全文索引(`to_tsvector('simple', content) @@ plainto_tsquery('simple', ?)`),仅搜索用户所在会话的消息。
|
||||
|
||||
**成功响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"list": [
|
||||
{
|
||||
"id": 99,
|
||||
"conversation_id": 1,
|
||||
"sender_id": 2,
|
||||
"type": 1,
|
||||
"content": "你好世界",
|
||||
"status": 1,
|
||||
"created_at": "2026-03-03 10:29:00",
|
||||
"sender_nickname": "李四",
|
||||
"sender_avatar": "https://..."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 获取全局未读消息总数
|
||||
|
||||
`GET /api/v1/im/unread`
|
||||
|
||||
**权限:** 需认证
|
||||
|
||||
**说明:** 从 Redis STRING 读取全局未读总数,用于 TabBar badge 显示。
|
||||
|
||||
**成功响应:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"total_unread": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -84,24 +84,38 @@
|
||||
|
||||
**方向:** 客户端 → 服务端
|
||||
|
||||
**说明:** 发送消息到会话
|
||||
**说明:** 发送消息到会话。`conversation_id` 和 `target_user_id` 二选一:首次发消息使用 `target_user_id`(自动创建会话),后续使用 `conversation_id`。
|
||||
|
||||
**data 参数:**
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| conversation_id | int | 是 | 目标会话 ID |
|
||||
| type | int | 是 | 消息类型:1=文本,2=图片,3=文件,4=语音 |
|
||||
| content | string | 否 | 文本内容 |
|
||||
| extra | object | 否 | 附加数据(图片/文件信息) |
|
||||
| conversation_id | int | 否 | 已有会话 ID(与 target_user_id 二选一) |
|
||||
| target_user_id | int | 否 | 对方用户 ID(首次发消息时使用) |
|
||||
| type | int | 是 | 消息类型:1=文本 |
|
||||
| content | string | 是 | 文本内容 |
|
||||
| client_msg_id | string | 否 | 客户端消息唯一 ID,用于幂等去重 |
|
||||
|
||||
**ACK 响应 data:** `{ "msg_id": 10086 }`
|
||||
**ACK 响应 data:**
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 10086,
|
||||
"conversation_id": 1,
|
||||
"sender_id": 1,
|
||||
"type": 1,
|
||||
"content": "你好",
|
||||
"status": 1,
|
||||
"client_msg_id": "xxxx-xxxx",
|
||||
"created_at": "2026-03-03 10:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### im.message.new
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
**方向:** 服务端 → 客户端(推送)
|
||||
|
||||
**说明:** 收到新消息推送
|
||||
|
||||
@@ -116,50 +130,76 @@
|
||||
"sender_avatar": "https://...",
|
||||
"type": 1,
|
||||
"content": "你好",
|
||||
"extra": {},
|
||||
"created_at": "2026-02-27 10:30:00"
|
||||
"client_msg_id": "",
|
||||
"created_at": "2026-03-03 10:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### im.message.revoke
|
||||
### im.message.recall
|
||||
|
||||
**方向:** 客户端 → 服务端
|
||||
|
||||
**说明:** 撤回消息(发送后 2 分钟内)
|
||||
**说明:** 撤回消息(发送后 2 分钟内)。撤回成功后若该消息是会话最后一条,会同步更新会话预览为"XX 撤回了一条消息"。
|
||||
|
||||
**data 参数:** `{ "message_id": 10086 }`
|
||||
|
||||
---
|
||||
|
||||
### im.message.read
|
||||
### im.message.recalled
|
||||
|
||||
**方向:** 客户端 → 服务端
|
||||
**方向:** 服务端 → 客户端(推送)
|
||||
|
||||
**说明:** 消息已读回执
|
||||
**说明:** 消息被撤回通知
|
||||
|
||||
**data 参数:** `{ "conversation_id": 1, "message_id": 10086 }`
|
||||
**data 内容:** `{ "message_id": 10086, "conversation_id": 1, "sender_id": 2 }`
|
||||
|
||||
---
|
||||
|
||||
### im.typing.start
|
||||
### im.conversation.read
|
||||
|
||||
**方向:** 客户端 → 服务端
|
||||
|
||||
**说明:** 通知对方"正在输入"
|
||||
**说明:** 标记会话已读(清零未读数 + 更新 Redis 全局未读数)
|
||||
|
||||
**data 参数:** `{ "conversation_id": 1 }`
|
||||
|
||||
---
|
||||
|
||||
### im.typing.stop
|
||||
### im.typing
|
||||
|
||||
**方向:** 客户端 → 服务端
|
||||
**方向:** 双向(客户端发送 → 服务端转发给对方)
|
||||
|
||||
**说明:** 停止输入
|
||||
**说明:** 正在输入通知。客户端发送后服务端转发给对方,前端收到后设置 3 秒超时自动清除。
|
||||
|
||||
**data 参数:** `{ "conversation_id": 1 }`
|
||||
**data 参数(客户端发送):** `{ "conversation_id": 1 }`
|
||||
|
||||
**data 内容(服务端推送):** `{ "conversation_id": 1, "user_id": 2 }`
|
||||
|
||||
---
|
||||
|
||||
### im.offline.sync
|
||||
|
||||
**方向:** 服务端 → 客户端(推送)
|
||||
|
||||
**说明:** WebSocket 连接成功后服务端主动推送离线未读摘要
|
||||
|
||||
**data 内容:**
|
||||
|
||||
```json
|
||||
{
|
||||
"total_unread": 5,
|
||||
"conversations": [
|
||||
{
|
||||
"conversation_id": 1,
|
||||
"unread_count": 3,
|
||||
"last_msg_content": "你好",
|
||||
"last_msg_time": "2026-03-03 10:30:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ EchoChat 采用 **「精简单体 + 媒体微服务」** 架构,核心思想
|
||||
| auth | 用户注册/登录、有状态 JWT Token 管理(Redis 存储 + client_type 隔离)、RBAC 角色权限(level 等级体系:1=超管, 10=管理员, 100=用户) | ✅ Phase 1 |
|
||||
| ws | WebSocket 连接管理(Hub/Client/PubSub)、在线状态管理(Redis SET + TTL 心跳续期)、好友上下线实时通知(FriendIDsGetter 接口注入) | ✅ Phase 2a |
|
||||
| contact | 好友关系管理(申请/接受/拒绝/删除/拉黑)、好友分组(CRUD + 移动)、用户搜索、好友推荐(批量查询优化) | ✅ Phase 2a |
|
||||
| im | 即时消息收发、会话管理、消息存储 | 🔜 Phase 2b |
|
||||
| im | 即时消息收发(单聊)、会话管理、消息存储、撤回、搜索、离线推送 | ✅ Phase 2b |
|
||||
| meeting | 会议创建/管理、信令转发、mediasoup 资源编排 | 📋 后续 |
|
||||
| notify | 通知推送、会议邀请、好友申请通知 | 📋 后续 |
|
||||
| admin | 后台管理(用户管理 + 角色权限管理 + 在线监控 + 好友关系管理、会议监控、系统配置) | ✅ Phase 1/2a |
|
||||
@@ -126,7 +126,7 @@ main.go
|
||||
├── admin.RegisterRoutes(engine, ...) // /api/v1/admin/*
|
||||
├── contact.RegisterRoutes(engine, ...) // /api/v1/contacts/* ✅ Phase 2a
|
||||
├── ws.RegisterRoutes(engine, ...) // /ws ✅ Phase 2a
|
||||
├── im.RegisterRoutes(engine, ...) // /api/v1/im/* (Phase 2b)
|
||||
├── im.RegisterRoutes(engine, ...) // /api/v1/im/* ✅ Phase 2b
|
||||
└── meeting.RegisterRoutes(engine, ...) // /api/v1/meeting/* (后续)
|
||||
```
|
||||
|
||||
@@ -209,7 +209,7 @@ mediasoup C++ SFU 的 **"遥控器"**,不懂业务、不懂用户、只懂媒
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 模块间零直接引用 | `auth` 不会 import `im` 内部代码,通过 interface 通信(实例:ws.FriendIDsGetter 由 contact.FriendshipDAO 实现) |
|
||||
| 模块间零直接引用 | `auth` 不会 import `im` 内部代码,通过 interface 通信(实例:ws.FriendIDsGetter / im.FriendChecker / contact.OnlineChecker 均通过接口注入) |
|
||||
| 模块自包含路由 | 每个模块在自己目录内维护 `router.go`,拆分时整目录搬走 |
|
||||
| 主路由仅做汇总 | `router/router.go` 只注册各模块路由,不含具体路由定义 |
|
||||
| 数据库表按模块前缀 | `auth_users`、`im_messages`、`meeting_rooms`,后期可分库 |
|
||||
|
||||
@@ -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} → 全局未读消息总数 (STRING,Lua 脚本原子递减,下限 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:会议与通知 📋 待规划
|
||||
- 多人音视频会议(即时会议 + 预约会议)
|
||||
- 消息通知系统
|
||||
|
||||
|
||||
@@ -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` → 更新 typingUsers(3 秒自动清除)
|
||||
- `im.offline.sync` → 初始化未读数 + 刷新会话列表
|
||||
|
||||
### 8.3 API 封装(api/chat.js)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# EchoChat 项目开发进度
|
||||
|
||||
> **最后更新**:2026-03-03(Phase 2b 即时通讯核心 + ui-ux-pro-max 规范改造完成)
|
||||
> **当前阶段**:Phase 2b 已完成
|
||||
> **最后更新**:2026-03-03(Phase 2b 全部完成,含代码审查修复 + 用户测试修复)
|
||||
> **当前阶段**:Phase 2b 全部完成,准备进入 Phase 2c
|
||||
> **当前分支**:`feature/phase2b-instant-messaging`
|
||||
> **实施计划**:`docs/plans/2026-03-03-phase2b-implementation.plan.md`
|
||||
> **设计文档**:`docs/plans/2026-03-03-phase2b-design.md`
|
||||
@@ -23,6 +23,33 @@
|
||||
| Task 8 | 设置页 + 搜索页 + 联系人改造 | ✅ 完成 | 2 个辅助页面 + 发消息跳转 |
|
||||
| Task 9 | 文档更新 + 代码审查 | ✅ 完成 | 进度/架构文档同步 |
|
||||
| UI 改造 | ui-ux-pro-max 规范改造 | ✅ 完成 | uni-icons 替换 emoji + 设计规范文件 |
|
||||
| 代码审查修复 | 后端 7 项修复 | ✅ 完成 | P0×2 + P1×3 + 推送补全×2 |
|
||||
| 用户测试修复 | 8 项 Bug 修复 | ✅ 完成 | 好友申请/接受、在线状态、UI 布局等 |
|
||||
|
||||
### 代码审查修复详情
|
||||
|
||||
| # | 优先级 | 修复内容 |
|
||||
|---|--------|----------|
|
||||
| Fix 1 | P0 | ClearHistory 改为个人视图操作(ClearBeforeMsgID),不再删除双方消息 |
|
||||
| Fix 2 | P0 | Redis 未读数负数保护(Lua 脚本原子递减,下限为 0) |
|
||||
| Fix 3 | P1 | GetConversationList N+1 查询优化(LEFT JOIN 一次获取 peerID) |
|
||||
| Fix 4 | P1 | 消息搜索改用 GIN 全文索引(to_tsvector/plainto_tsquery 替代 LIKE) |
|
||||
| Fix 5 | P1 | 撤回消息后更新会话预览(last_msg_content = "XX 撤回了一条消息") |
|
||||
| Fix 6 | - | im.message.new 推送补充 sender_name、sender_avatar |
|
||||
| Fix 7 | - | im.message.recalled 推送补充 sender_id |
|
||||
|
||||
### 用户测试修复详情
|
||||
|
||||
| # | 修复内容 |
|
||||
|---|----------|
|
||||
| Fix 8 | 好友申请拒绝后重新申请失败 — FriendshipDAO 新增 ReactivateRejectedRequest 方法 |
|
||||
| Fix 9 | 好友接受申请失败(反向记录 UNIQUE 冲突)— AcceptRequest 先查后改,避免重复插入 |
|
||||
| Fix 10 | Redis 在线状态残留 — OnlineService 启动时清理旧在线数据(cleanStaleOnlineData) |
|
||||
| Fix 11 | WS 断开时在线状态未清理 — 修正 onDisconnect 判断条件(closedByHub && isOnline) |
|
||||
| Fix 12 | 前端 WS 连接未全局初始化 — App.vue onLaunch/onShow + login.vue 登录后建立连接 |
|
||||
| Fix 13 | 后台管理端好友关系页用户 A 列名称错误 — 修正字段绑定 row.user_username |
|
||||
| Fix 14 | 前台好友在线状态初始值缺失 — ContactService 注入 OnlineChecker,GetFriendList 返回 is_online |
|
||||
| Fix 15 | 聊天页消息过多时输入框被挤出 — scroll-view 添加 height:0 + min-height:0 约束 |
|
||||
|
||||
---
|
||||
|
||||
@@ -37,9 +64,9 @@
|
||||
|
||||
### 会话管理
|
||||
- **自动创建**:首次发消息时自动创建单聊会话
|
||||
- **会话列表**:置顶优先 → 最后消息时间降序,冗余 last_msg_* 避免 JOIN
|
||||
- **会话操作**:置顶/取消、软删除(不影响对方)、清空聊天记录
|
||||
- **未读管理**:DB unread_count + Redis 全局未读数,TabBar badge 显示
|
||||
- **会话列表**:置顶优先 → 最后消息时间降序,LEFT JOIN 一次获取 peerID(N+1 优化)
|
||||
- **会话操作**:置顶/取消、软删除(不影响对方)、清空聊天记录(个人视图 ClearBeforeMsgID)
|
||||
- **未读管理**:DB unread_count + Redis STRING 全局未读数(Lua 脚本负数保护),TabBar badge 显示
|
||||
|
||||
### WebSocket 事件路由表
|
||||
- **Hub.RegisterEvent**:业务模块注册事件处理器
|
||||
@@ -212,8 +239,9 @@ cd frontend && npm run dev:h5
|
||||
|
||||
## 八、下一阶段规划
|
||||
|
||||
### Phase 2c - 群聊与增强
|
||||
- 群聊会话(建群/加入/退出)
|
||||
### Phase 2c - 群聊与增强(待规划)
|
||||
- 群聊会话(建群/加入/退出/管理)
|
||||
- 群消息收发
|
||||
- 已读回执
|
||||
- 已读回执(单聊 + 群聊)
|
||||
- 消息类型扩展(图片/语音/文件)
|
||||
- 管理端消息管理功能
|
||||
|
||||
Reference in New Issue
Block a user