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:
@@ -89,8 +89,8 @@ EchoChat 采用 **「精简单体 + 媒体微服务」** 架构,核心思想
|
||||
| 模块 | 职责 | 状态 |
|
||||
|------|------|------|
|
||||
| auth | 用户注册/登录、有状态 JWT Token 管理(Redis 存储 + client_type 隔离)、RBAC 角色权限(level 等级体系:1=超管, 10=管理员, 100=用户) | ✅ Phase 1 |
|
||||
| ws | WebSocket 连接管理(Hub/Client/PubSub)、在线状态管理(Redis SET + TTL 心跳续期) | ✅ Phase 2a |
|
||||
| contact | 好友关系管理(申请/接受/拒绝/删除/拉黑)、好友分组(CRUD + 移动)、用户搜索、好友推荐 | ✅ Phase 2a |
|
||||
| ws | WebSocket 连接管理(Hub/Client/PubSub)、在线状态管理(Redis SET + TTL 心跳续期)、好友上下线实时通知(FriendIDsGetter 接口注入) | ✅ Phase 2a |
|
||||
| contact | 好友关系管理(申请/接受/拒绝/删除/拉黑)、好友分组(CRUD + 移动)、用户搜索、好友推荐(批量查询优化) | ✅ Phase 2a |
|
||||
| im | 即时消息收发、会话管理、消息存储 | 🔜 Phase 2b |
|
||||
| meeting | 会议创建/管理、信令转发、mediasoup 资源编排 | 📋 后续 |
|
||||
| notify | 通知推送、会议邀请、好友申请通知 | 📋 后续 |
|
||||
@@ -209,7 +209,7 @@ mediasoup C++ SFU 的 **"遥控器"**,不懂业务、不懂用户、只懂媒
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 模块间零直接引用 | `auth` 不会 import `im` 内部代码,通过 interface 通信 |
|
||||
| 模块间零直接引用 | `auth` 不会 import `im` 内部代码,通过 interface 通信(实例:ws.FriendIDsGetter 由 contact.FriendshipDAO 实现) |
|
||||
| 模块自包含路由 | 每个模块在自己目录内维护 `router.go`,拆分时整目录搬走 |
|
||||
| 主路由仅做汇总 | `router/router.go` 只注册各模块路由,不含具体路由定义 |
|
||||
| 数据库表按模块前缀 | `auth_users`、`im_messages`、`meeting_rooms`,后期可分库 |
|
||||
|
||||
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. 集成测试 + 文档更新
|
||||
@@ -1,10 +1,11 @@
|
||||
# EchoChat 项目开发进度
|
||||
|
||||
> **最后更新**:2026-03-02(Phase 2a 完成 - WebSocket 实时通讯与联系人管理)
|
||||
> **当前阶段**:Phase 2a - WebSocket 实时通讯与联系人管理
|
||||
> **最后更新**:2026-03-02(Phase 2a 代码审查全面修复 + Phase 2b 架构建议)
|
||||
> **当前阶段**:Phase 2a 已完成,Phase 2b 待设计
|
||||
> **当前分支**:`feature/phase2a-websocket-contacts`
|
||||
> **实施计划**:`phase_2a_实施计划_221003ce.plan.md`
|
||||
> **设计文档**:`docs/plans/2026-03-02-phase2a-design.md`
|
||||
> **Phase 2b 架构备忘**:`docs/plans/2026-03-02-phase2b-architecture-notes.md`
|
||||
|
||||
---
|
||||
|
||||
@@ -200,7 +201,46 @@ cd frontend && npm run dev:h5
|
||||
|
||||
---
|
||||
|
||||
## 八、下一阶段规划
|
||||
## 八、Phase 2a 后期 Bug 修复记录(2026-03-02)
|
||||
|
||||
### 修复 1: GetRecommendFriends 返回空数据
|
||||
- **问题**:对每个候选人调用 `SearchUsers` 并忽略结果,返回的 DTO 只有 ID 字段
|
||||
- **修复**:FriendshipDAO 新增 `GetUsersByIDs` 批量查询方法,Service 层收集所有候选人 ID 后一次查询,正确填充 Username/Nickname/Avatar
|
||||
- **改进**:排序算法从手动冒泡改为 `sort.Slice`
|
||||
|
||||
### 修复 2: 上下线好友通知未实现
|
||||
- **问题**:`UserOnline`/`UserOffline` 没有调用已有的 `NotifyFriendsStatusChange` 方法
|
||||
- **修复**:OnlineService 新增 `FriendIDsGetter` 接口依赖(由 FriendshipDAO 隐式实现),在上线/下线时获取好友列表并推送状态变更通知
|
||||
- **架构**:使用接口注入避免 ws 包直接依赖 contact 包
|
||||
|
||||
### 修复 3: 管理端在线用户 API 缺少用户名
|
||||
- **问题**:`GetOnlineUsers` 只返回 `[]int64`,管理端无法展示用户名
|
||||
- **修复**:OnlineManageService 注入 `*gorm.DB`,返回 `[]OnlineUserInfo`(含 user_id + username),查询 auth_users 表补充信息
|
||||
|
||||
### 修复 4: WebSocket Token 未校验 Redis 状态(I2)
|
||||
- **问题**:`ws.handler.Upgrade` 只校验 JWT 签名和过期时间,未检查 Token 是否仍在 Redis 中有效(已登出用户仍可建立 WS 连接)
|
||||
- **修复**:新增 `TokenValidator` 接口,由 `AuthService` 实现;`Upgrade` 流程增加 Redis Token 校验步骤
|
||||
- **架构**:使用接口注入避免 ws 包直接依赖 auth 包
|
||||
|
||||
### 修复 5: HeartbeatRenew Redis 错误未检查(M6)
|
||||
- **问题**:`HeartbeatRenew` 中 `rdb.Expire` 调用结果被忽略
|
||||
- **修复**:增加错误检查与日志记录
|
||||
|
||||
### 修复 6: json.Marshal 错误未处理(M2)
|
||||
- **问题**:`handler.go` 心跳/默认响应的 `MarshalResponse` 和 `online_service.go` 中 `json.Marshal` 错误被忽略
|
||||
- **修复**:所有 Marshal 调用增加错误检查,失败时记录日志并提前返回
|
||||
|
||||
### 修复 7: Controller 统一错误处理(I8)
|
||||
- **问题**:`contact_controller.go` 中多数 endpoint 使用硬编码的 `utils.ResponseError`,未经过 `handleError` 业务错误映射
|
||||
- **修复**:所有 13 个 endpoint 统一走 `handleError`,确保已知业务错误(404/400/403)被正确返回
|
||||
|
||||
### 修复 8: 管理端 Controller 注释与日志规范化(M1)
|
||||
- **问题**:`online_controller.go` 和 `contact_manage_controller.go` 缺少包注释、函数注释和 `funcName` 日志模式
|
||||
- **修复**:补全所有注释,增加 `funcName` + `logs.Error/Info` 结构化日志
|
||||
|
||||
---
|
||||
|
||||
## 九、下一阶段规划
|
||||
|
||||
### Phase 2b - 即时通讯消息系统
|
||||
- 会话管理(单聊/群聊)
|
||||
|
||||
Reference in New Issue
Block a user