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:
@@ -15,29 +15,37 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TokenValidator 有状态 JWT 验证接口(检查 Token 是否在 Redis 中有效)
|
||||
// 由 auth.AuthService 实现,用于防止已登出用户建立 WebSocket 连接
|
||||
type TokenValidator interface {
|
||||
ValidateAccessToken(ctx context.Context, userID int64, clientType, token string) bool
|
||||
}
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // 开发阶段允许所有来源,生产环境需限制
|
||||
return true // TODO: 生产环境通过配置限制 allowed origins
|
||||
},
|
||||
}
|
||||
|
||||
// Handler WebSocket 连接处理器
|
||||
type Handler struct {
|
||||
hub *ws.Hub
|
||||
pubsub *ws.PubSub
|
||||
jwtCfg *config.JWTConfig
|
||||
onlineService *OnlineService
|
||||
hub *ws.Hub
|
||||
pubsub *ws.PubSub
|
||||
jwtCfg *config.JWTConfig
|
||||
onlineService *OnlineService
|
||||
tokenValidator TokenValidator
|
||||
}
|
||||
|
||||
// NewHandler 创建 WebSocket Handler 实例
|
||||
func NewHandler(hub *ws.Hub, pubsub *ws.PubSub, jwtCfg *config.JWTConfig, onlineService *OnlineService) *Handler {
|
||||
func NewHandler(hub *ws.Hub, pubsub *ws.PubSub, jwtCfg *config.JWTConfig, onlineService *OnlineService, tokenValidator TokenValidator) *Handler {
|
||||
return &Handler{
|
||||
hub: hub,
|
||||
pubsub: pubsub,
|
||||
jwtCfg: jwtCfg,
|
||||
onlineService: onlineService,
|
||||
hub: hub,
|
||||
pubsub: pubsub,
|
||||
jwtCfg: jwtCfg,
|
||||
onlineService: onlineService,
|
||||
tokenValidator: tokenValidator,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +67,17 @@ func (h *Handler) Upgrade(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
clientType := claims.ClientType
|
||||
if clientType == "" {
|
||||
clientType = "frontend"
|
||||
}
|
||||
if h.tokenValidator != nil && !h.tokenValidator.ValidateAccessToken(c.Request.Context(), claims.UserID, clientType, token) {
|
||||
logs.Warn(nil, funcName, "WebSocket Token 已失效(Redis 校验)",
|
||||
zap.Int64("user_id", claims.UserID))
|
||||
utils.ResponseUnauthorized(c, "认证已失效,请重新登录")
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
logs.Error(nil, funcName, "WebSocket 升级失败",
|
||||
@@ -68,6 +87,11 @@ func (h *Handler) Upgrade(c *gin.Context) {
|
||||
|
||||
client := ws.NewClient(h.hub, conn, claims.UserID)
|
||||
client.SetOnDisconnect(func(userID int64) {
|
||||
if client.IsClosedByHub() {
|
||||
logs.Info(nil, "ws.handler.onDisconnect", "连接被 Hub 踢出(重复连接),跳过下线清理",
|
||||
zap.Int64("user_id", userID))
|
||||
return
|
||||
}
|
||||
h.pubsub.Unsubscribe(userID)
|
||||
h.onlineService.UserOffline(context.Background(), userID)
|
||||
})
|
||||
@@ -97,7 +121,11 @@ func (h *Handler) createReadHandler(userID int64) ws.MessageHandler {
|
||||
case "heartbeat":
|
||||
h.onlineService.HeartbeatRenew(context.Background(), userID)
|
||||
resp := ws.NewResponse(msg.Event, msg.Seq, 0, "pong", nil)
|
||||
data, _ := ws.MarshalResponse(resp)
|
||||
data, err := ws.MarshalResponse(resp)
|
||||
if err != nil {
|
||||
logs.Error(nil, funcName, "序列化心跳响应失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
client.Send(data)
|
||||
default:
|
||||
resp := ws.NewResponse(msg.Event, msg.Seq, 0, "ok", nil)
|
||||
|
||||
@@ -25,19 +25,26 @@ type UserStatus struct {
|
||||
IP string `json:"ip"` // 连接 IP
|
||||
}
|
||||
|
||||
// FriendIDsGetter 获取好友 ID 列表的接口(避免直接依赖 contact 模块)
|
||||
type FriendIDsGetter interface {
|
||||
GetFriendIDs(ctx context.Context, userID int64) ([]int64, error)
|
||||
}
|
||||
|
||||
// OnlineService 在线状态管理服务
|
||||
type OnlineService struct {
|
||||
rdb *redis.Client
|
||||
hub *ws.Hub
|
||||
pubsub *ws.PubSub
|
||||
rdb *redis.Client
|
||||
hub *ws.Hub
|
||||
pubsub *ws.PubSub
|
||||
friendGetter FriendIDsGetter
|
||||
}
|
||||
|
||||
// NewOnlineService 创建 OnlineService 实例
|
||||
func NewOnlineService(rdb *redis.Client, hub *ws.Hub, pubsub *ws.PubSub) *OnlineService {
|
||||
func NewOnlineService(rdb *redis.Client, hub *ws.Hub, pubsub *ws.PubSub, friendGetter FriendIDsGetter) *OnlineService {
|
||||
return &OnlineService{
|
||||
rdb: rdb,
|
||||
hub: hub,
|
||||
pubsub: pubsub,
|
||||
rdb: rdb,
|
||||
hub: hub,
|
||||
pubsub: pubsub,
|
||||
friendGetter: friendGetter,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +60,11 @@ func (s *OnlineService) UserOnline(ctx context.Context, userID int64, ip string)
|
||||
ConnectAt: time.Now().Format("2006-01-02 15:04:05"),
|
||||
IP: ip,
|
||||
}
|
||||
statusJSON, _ := json.Marshal(status)
|
||||
statusJSON, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
logs.Error(ctx, funcName, "序列化用户状态失败", zap.Int64("user_id", userID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
pipe.Set(ctx, statusKey(userID), statusJSON, statusTTL)
|
||||
|
||||
if _, err := pipe.Exec(ctx); err != nil {
|
||||
@@ -63,6 +74,15 @@ func (s *OnlineService) UserOnline(ctx context.Context, userID int64, ip string)
|
||||
}
|
||||
|
||||
logs.Info(ctx, funcName, "用户上线", zap.Int64("user_id", userID))
|
||||
|
||||
if s.friendGetter != nil {
|
||||
friendIDs, err := s.friendGetter.GetFriendIDs(ctx, userID)
|
||||
if err != nil {
|
||||
logs.Warn(ctx, funcName, "获取好友列表失败,跳过上线通知", zap.Error(err))
|
||||
} else if len(friendIDs) > 0 {
|
||||
s.NotifyFriendsStatusChange(ctx, userID, true, friendIDs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UserOffline 用户下线:清除 Redis + 通知在线好友
|
||||
@@ -79,11 +99,23 @@ func (s *OnlineService) UserOffline(ctx context.Context, userID int64) {
|
||||
}
|
||||
|
||||
logs.Info(ctx, funcName, "用户下线", zap.Int64("user_id", userID))
|
||||
|
||||
if s.friendGetter != nil {
|
||||
friendIDs, err := s.friendGetter.GetFriendIDs(ctx, userID)
|
||||
if err != nil {
|
||||
logs.Warn(ctx, funcName, "获取好友列表失败,跳过下线通知", zap.Error(err))
|
||||
} else if len(friendIDs) > 0 {
|
||||
s.NotifyFriendsStatusChange(ctx, userID, false, friendIDs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HeartbeatRenew 心跳续期:延长状态 TTL
|
||||
func (s *OnlineService) HeartbeatRenew(ctx context.Context, userID int64) {
|
||||
s.rdb.Expire(ctx, statusKey(userID), statusTTL)
|
||||
if err := s.rdb.Expire(ctx, statusKey(userID), statusTTL).Err(); err != nil {
|
||||
logs.Warn(ctx, "ws.online_service.HeartbeatRenew", "心跳续期失败",
|
||||
zap.Int64("user_id", userID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// IsOnline 检查用户是否在线(Redis 查询)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
)
|
||||
|
||||
// ProvideHub 创建并启动 Hub 实例
|
||||
// Hub.Run() 在后台 goroutine 运行,服务关闭时通过 hub.Stop() 优雅退出
|
||||
func ProvideHub() *ws.Hub {
|
||||
hub := ws.NewHub()
|
||||
go hub.Run()
|
||||
@@ -20,13 +21,13 @@ func ProvidePubSub(rdb *redis.Client, hub *ws.Hub) *ws.PubSub {
|
||||
}
|
||||
|
||||
// ProvideOnlineService 创建在线状态管理服务
|
||||
func ProvideOnlineService(rdb *redis.Client, hub *ws.Hub, pubsub *ws.PubSub) *OnlineService {
|
||||
return NewOnlineService(rdb, hub, pubsub)
|
||||
func ProvideOnlineService(rdb *redis.Client, hub *ws.Hub, pubsub *ws.PubSub, friendGetter FriendIDsGetter) *OnlineService {
|
||||
return NewOnlineService(rdb, hub, pubsub, friendGetter)
|
||||
}
|
||||
|
||||
// ProvideWSHandler 创建 WebSocket Handler
|
||||
func ProvideWSHandler(hub *ws.Hub, pubsub *ws.PubSub, cfg *config.JWTConfig, onlineService *OnlineService) *Handler {
|
||||
return NewHandler(hub, pubsub, cfg, onlineService)
|
||||
func ProvideWSHandler(hub *ws.Hub, pubsub *ws.PubSub, cfg *config.JWTConfig, onlineService *OnlineService, tokenValidator TokenValidator) *Handler {
|
||||
return NewHandler(hub, pubsub, cfg, onlineService, tokenValidator)
|
||||
}
|
||||
|
||||
// WSSet WebSocket 模块 Wire Provider Set
|
||||
|
||||
Reference in New Issue
Block a user