fix(phase2a): 代码审查修复 - 8项关键/重要问题
安全修复: - WebSocket Token 增加 Redis 有效性校验(已登出用户无法建立 WS) 功能修复: - GetRecommendFriends 改为批量查询,正确返回用户名/昵称/头像 - 上下线通知:OnlineService 通过接口注入获取好友列表推送状态变更 - 管理端在线用户 API 补充用户名信息 代码质量: - 所有 json.Marshal/Redis 错误增加检查与日志 - ContactController 13 个 endpoint 统一走 handleError 业务错误映射 - 管理端 Controller 补全包注释、函数注释和结构化日志 - 前端 5 个联系人页面 avatar 工具函数抽取到 utils/avatar.js Made-with: Cursor
This commit is contained in:
229
docs/plans/2026-03-02-phase2b-architecture-notes.md
Normal file
229
docs/plans/2026-03-02-phase2b-architecture-notes.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Phase 2b 架构建议备忘录:即时通讯消息系统
|
||||
|
||||
> **创建日期:** 2026-03-02
|
||||
> **状态:** 📋 待设计(供 Phase 2b 设计阶段参考)
|
||||
> **前置依赖:** Phase 2a 全部完成(WebSocket + 联系人管理)
|
||||
|
||||
---
|
||||
|
||||
## 一、Phase 2a 经验总结与架构教训
|
||||
|
||||
### 1.1 接口注入模式(Interface Injection)
|
||||
|
||||
Phase 2a 后期修复中,`ws.OnlineService` 需要获取好友列表来推送上下线通知,但不应直接依赖 `contact` 包。通过定义 `FriendIDsGetter` 接口,由 `FriendshipDAO` 隐式实现,在 Wire 中注入——这是跨模块通信的标准模式。
|
||||
|
||||
**Phase 2b 建议:** IM 模块同样需要获取好友/联系人数据(如验证单聊权限、获取会话成员信息),必须遵循相同的接口注入模式,避免 `im` 包直接 import `contact` 包。
|
||||
|
||||
需要预定义的接口:
|
||||
|
||||
```go
|
||||
// im 模块可能需要的外部依赖接口
|
||||
type FriendChecker interface {
|
||||
IsFriend(ctx context.Context, userID, targetID int64) (bool, error)
|
||||
}
|
||||
|
||||
type UserInfoGetter interface {
|
||||
GetUsersByIDs(ctx context.Context, userIDs []int64) ([]UserBasicInfo, error)
|
||||
}
|
||||
```
|
||||
|
||||
### 1.2 批量查询模式
|
||||
|
||||
Phase 2a 的 `GetRecommendFriends` 最初对每个候选人单独查询用户信息,性能极差。修复后使用 `GetUsersByIDs` 一次批量查询 + Map 映射。
|
||||
|
||||
**Phase 2b 建议:** IM 消息列表中需要展示发送者信息(头像、昵称),必须使用批量查询模式:
|
||||
- 收集一页消息的所有 `sender_id`
|
||||
- 去重后一次 IN 查询获取用户信息
|
||||
- 构建 `map[int64]UserBasicInfo` 映射
|
||||
- 按消息顺序填充发送者信息
|
||||
|
||||
### 1.3 Admin 端查询模式
|
||||
|
||||
`OnlineManageService` 最初只返回 `[]int64`,缺少用户名导致管理端无法展示。修复后注入 `*gorm.DB` 查询用户表补充信息。
|
||||
|
||||
**Phase 2b 建议:** 管理端的消息管理/会话管理 API 从一开始就设计为返回完整信息的 DTO,不要只返回 ID。
|
||||
|
||||
---
|
||||
|
||||
## 二、IM 模块架构建议
|
||||
|
||||
### 2.1 模块结构
|
||||
|
||||
```
|
||||
app/im/
|
||||
├── controller/
|
||||
│ └── im_controller.go # REST API 控制器
|
||||
├── service/
|
||||
│ └── im_service.go # 业务逻辑(会话管理、消息收发)
|
||||
├── dao/
|
||||
│ ├── conversation_dao.go # 会话 DAO
|
||||
│ └── message_dao.go # 消息 DAO
|
||||
├── model/
|
||||
│ ├── conversation.go # im_conversations 模型
|
||||
│ ├── conversation_member.go # im_conversation_members 模型
|
||||
│ └── message.go # im_messages 模型
|
||||
├── handler/
|
||||
│ └── message_handler.go # WebSocket 消息事件处理器
|
||||
├── router.go # 路由注册
|
||||
└── provider.go # Wire Provider Set
|
||||
```
|
||||
|
||||
### 2.2 消息收发链路设计
|
||||
|
||||
```
|
||||
发送者 Client
|
||||
│
|
||||
├─ REST: POST /api/v1/im/conversations/:id/messages(HTTP 发消息)
|
||||
│ 或
|
||||
├─ WS: im.message.send(WebSocket 发消息)
|
||||
│
|
||||
▼
|
||||
IM Service
|
||||
│── 权限校验(是否为会话成员)
|
||||
│── 消息写入 PostgreSQL (im_messages)
|
||||
│── 更新会话 last_message 信息
|
||||
│── 更新 Redis 未读计数 (HINCRBY echo:im:unread:{member_id} conv_id 1)
|
||||
│
|
||||
▼
|
||||
PubSub.PublishToUser(复用 Phase 2a 的 Pub/Sub 基础设施)
|
||||
│── 推送给会话内所有在线成员
|
||||
│── 事件: im.message.new
|
||||
│
|
||||
▼
|
||||
接收者 Client(实时收到消息推送)
|
||||
```
|
||||
|
||||
### 2.3 依赖注入设计
|
||||
|
||||
```go
|
||||
type IMService struct {
|
||||
conversationDAO *dao.ConversationDAO
|
||||
messageDAO *dao.MessageDAO
|
||||
pubsub *ws.PubSub // 复用 Phase 2a 的 Pub/Sub
|
||||
friendChecker FriendChecker // 接口注入,contact.FriendshipDAO 实现
|
||||
userInfoGetter UserInfoGetter // 接口注入,批量获取用户信息
|
||||
}
|
||||
```
|
||||
|
||||
Wire 注入链:
|
||||
- `FriendshipDAO` → 隐式实现 `FriendChecker`(已有 `IsFriend` 方法)
|
||||
- `FriendshipDAO` → 隐式实现 `UserInfoGetter`(已有 `GetUsersByIDs` 方法)
|
||||
|
||||
### 2.4 WebSocket 事件处理器
|
||||
|
||||
Phase 2a 的 WebSocket Handler 只处理心跳和连接管理。Phase 2b 需要增加消息事件路由:
|
||||
|
||||
```go
|
||||
// 建议在 app/ws/handler.go 中增加事件分发机制
|
||||
// 或在 app/im/handler/ 下实现 IM 事件处理器,注册到 Hub
|
||||
|
||||
// 需要处理的 WS 事件
|
||||
// im.message.send → 发送消息
|
||||
// im.message.read → 标记已读
|
||||
// im.typing.start → 正在输入
|
||||
// im.typing.stop → 停止输入
|
||||
```
|
||||
|
||||
**建议方案:** 在 `pkg/ws/hub.go` 或 `app/ws/handler.go` 中实现事件路由表(`map[string]EventHandler`),各模块注册自己的事件处理器,避免 handler.go 膨胀成巨型文件。
|
||||
|
||||
---
|
||||
|
||||
## 三、数据库注意事项
|
||||
|
||||
### 3.1 单聊会话去重
|
||||
|
||||
单聊会话应保证两个用户之间只有一个会话。建议方案:
|
||||
- 创建单聊时,用两个 user_id 的较小值和较大值组合查询是否已存在
|
||||
- 或增加唯一约束 + 规范化存储(小 ID 在前)
|
||||
|
||||
### 3.2 消息分页查询
|
||||
|
||||
`im_messages` 将是数据量最大的表。已有索引 `idx_im_messages_conv_time (conversation_id, created_at DESC)`。
|
||||
|
||||
分页建议:
|
||||
- 使用游标分页(`WHERE created_at < ? AND conversation_id = ? ORDER BY created_at DESC LIMIT ?`)而非 OFFSET 分页
|
||||
- 前端传 `before_msg_id` 或 `before_time` 参数
|
||||
|
||||
### 3.3 未读消息计数
|
||||
|
||||
Redis HASH `echo:im:unread:{user_id}` → `{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}` 求和
|
||||
|
||||
---
|
||||
|
||||
## 四、DTO 设计建议
|
||||
|
||||
```go
|
||||
// 会话列表项(首页会话列表展示用)
|
||||
type ConversationItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Type int `json:"type"` // 1=单聊, 2=群聊
|
||||
Name string `json:"name"` // 群聊名称 / 对方昵称
|
||||
Avatar string `json:"avatar"` // 群头像 / 对方头像
|
||||
LastMessage string `json:"last_message"` // 最后一条消息预览
|
||||
LastMessageTime string `json:"last_message_time"` // 最后消息时间
|
||||
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"`
|
||||
Extra interface{} `json:"extra,omitempty"`
|
||||
Status int `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、前端注意事项
|
||||
|
||||
### 5.1 WebSocket 事件集成
|
||||
|
||||
前台 `services/websocket.js` 已支持事件监听(`on/off`),Phase 2b 需要注册新事件:
|
||||
- `im.message.new` → 更新会话列表 + 未读数 + 当前聊天窗口
|
||||
- `im.typing.start/stop` → 显示"正在输入..."
|
||||
|
||||
### 5.2 Store 设计
|
||||
|
||||
```
|
||||
store/
|
||||
├── chat.js # 会话列表、当前会话、消息缓存(新增)
|
||||
└── ...existing stores
|
||||
```
|
||||
|
||||
消息缓存策略:
|
||||
- 每个会话缓存最新 N 条消息在内存中
|
||||
- 向上翻页时 REST 拉取历史消息
|
||||
- 新消息通过 WS 推送自动追加
|
||||
|
||||
---
|
||||
|
||||
## 六、管理端扩展建议
|
||||
|
||||
Phase 2b 管理端可暂不实现消息管理,但如果实现,参考 Phase 2a 的模式:
|
||||
- `admin/service/im_manage_service.go`
|
||||
- 注入 `*gorm.DB` 查询 im_messages + auth_users
|
||||
- 返回包含发送者用户名的完整 DTO(不要只返回 ID)
|
||||
|
||||
---
|
||||
|
||||
## 七、Phase 2b 建议的 Task 拆分思路
|
||||
|
||||
1. 设计文档 + 数据库迁移(im_conversations, im_conversation_members, im_messages)
|
||||
2. IM Model + DAO 层
|
||||
3. IM Service 层(含接口注入设计)
|
||||
4. IM Controller + 路由 + Wire 集成
|
||||
5. WebSocket 事件路由机制 + IM 事件处理器
|
||||
6. 前台会话列表页 + 聊天页
|
||||
7. 前台 chat Store + API 封装
|
||||
8. 管理端扩展(可选)
|
||||
9. 集成测试 + 文档更新
|
||||
Reference in New Issue
Block a user