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:
bujinyuan
2026-03-02 18:48:28 +08:00
parent 8ec0261427
commit 2c59500e27
26 changed files with 632 additions and 153 deletions

View File

@@ -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`,后期可分库 |

View 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/messagesHTTP 发消息)
│ 或
├─ WS: im.message.sendWebSocket 发消息)
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. 集成测试 + 文档更新

View File

@@ -1,10 +1,11 @@
# EchoChat 项目开发进度
> **最后更新**2026-03-02Phase 2a 完成 - WebSocket 实时通讯与联系人管理
> **当前阶段**Phase 2a - WebSocket 实时通讯与联系人管理
> **最后更新**2026-03-02Phase 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 - 即时通讯消息系统
- 会话管理(单聊/群聊)