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

@@ -2,6 +2,7 @@ package ws
import (
"encoding/json"
"sync/atomic"
"time"
"github.com/echochat/backend/pkg/logs"
@@ -28,6 +29,7 @@ type Client struct {
send chan []byte // 待发送消息缓冲队列
UserID int64 // 关联的用户 ID
onDisconnect DisconnectHandler // 断线回调(清理在线状态等)
closedByHub int32 // 原子标记:是否被 Hub 主动踢出(用于避免重复连接时的竞态清理)
}
// NewClient 创建客户端实例
@@ -45,6 +47,18 @@ func (c *Client) SetOnDisconnect(handler DisconnectHandler) {
c.onDisconnect = handler
}
// SetClosedByHub 标记此连接被 Hub 主动关闭(重复连接踢出场景)
// 线程安全,使用 atomic 操作
func (c *Client) SetClosedByHub() {
atomic.StoreInt32(&c.closedByHub, 1)
}
// IsClosedByHub 检查此连接是否被 Hub 主动关闭
// 断线回调中使用此方法判断是否需要执行下线清理
func (c *Client) IsClosedByHub() bool {
return atomic.LoadInt32(&c.closedByHub) == 1
}
// MessageHandler 消息处理回调函数类型
type MessageHandler func(client *Client, msg *Message)

View File

@@ -8,12 +8,13 @@ import (
)
// Hub 管理所有活跃的 WebSocket 客户端连接
// 提供按 userID 注册/注销/查找连接的能力
// 提供按 userID 注册/注销/查找连接的能力,支持优雅关闭
type Hub struct {
clients map[int64]*Client // userID -> Client 映射
register chan *Client // 注册通道
unregister chan *Client // 注销通道
mu sync.RWMutex // 保护 clients map 的读写锁
stopCh chan struct{} // 停止信号,用于优雅关闭 Run 循环
}
// NewHub 创建 Hub 实例
@@ -22,19 +23,32 @@ func NewHub() *Hub {
clients: make(map[int64]*Client),
register: make(chan *Client, 256),
unregister: make(chan *Client, 256),
stopCh: make(chan struct{}),
}
}
// Run 启动 Hub 主循环,处理连接注册和注销
// 应在单独的 goroutine 中运行
// 应在单独的 goroutine 中运行,通过 Stop() 方法优雅关闭
func (h *Hub) Run() {
for {
select {
case <-h.stopCh:
h.mu.Lock()
for uid, client := range h.clients {
client.SetClosedByHub()
close(client.send)
delete(h.clients, uid)
}
h.mu.Unlock()
logs.Info(nil, "ws.hub.Run", "Hub 已停止,所有连接已关闭")
return
case client := <-h.register:
h.mu.Lock()
if old, ok := h.clients[client.UserID]; ok {
logs.Info(nil, "ws.hub.Run", "用户重复连接,关闭旧连接",
zap.Int64("user_id", client.UserID))
old.SetClosedByHub()
close(old.send)
}
h.clients[client.UserID] = client
@@ -59,6 +73,11 @@ func (h *Hub) Run() {
}
}
// Stop 优雅关闭 Hub停止 Run 循环并断开所有客户端连接
func (h *Hub) Stop() {
close(h.stopCh)
}
// Register 注册客户端连接
func (h *Hub) Register(client *Client) {
h.register <- client
@@ -78,7 +97,7 @@ func (h *Hub) GetClient(userID int64) (*Client, bool) {
}
// SendToUser 向指定用户发送消息(仅本地 Hub
// 如果用户不在本实例,返回 false
// 如果用户不在本实例或缓冲区满,返回 false
func (h *Hub) SendToUser(userID int64, data []byte) bool {
h.mu.RLock()
client, ok := h.clients[userID]
@@ -92,6 +111,9 @@ func (h *Hub) SendToUser(userID int64, data []byte) bool {
case client.send <- data:
return true
default:
logs.Warn(nil, "ws.hub.SendToUser", "发送缓冲区已满,消息被丢弃",
zap.Int64("user_id", userID),
zap.Int("data_len", len(data)))
return false
}
}