package websocket import ( "carrot_bbs/internal/model" "encoding/json" "log" "sync" "time" "github.com/gorilla/websocket" ) // WebSocket消息类型常量 const ( MessageTypePing = "ping" MessageTypePong = "pong" MessageTypeMessage = "message" MessageTypeTyping = "typing" MessageTypeRead = "read" MessageTypeAck = "ack" MessageTypeError = "error" MessageTypeRecall = "recall" // 撤回消息 MessageTypeSystem = "system" // 系统消息 MessageTypeNotification = "notification" // 通知消息 MessageTypeAnnouncement = "announcement" // 公告消息 // 群组相关消息类型 MessageTypeGroupMessage = "group_message" // 群消息 MessageTypeGroupTyping = "group_typing" // 群输入状态 MessageTypeGroupNotice = "group_notice" // 群组通知(成员变动等) MessageTypeGroupMention = "group_mention" // @提及通知 MessageTypeGroupRead = "group_read" // 群消息已读 MessageTypeGroupRecall = "group_recall" // 群消息撤回 // Meta事件详细类型 MetaDetailTypeHeartbeat = "heartbeat" MetaDetailTypeTyping = "typing" MetaDetailTypeAck = "ack" // 消息发送确认 MetaDetailTypeRead = "read" // 已读回执 ) // WSMessage WebSocket消息结构 type WSMessage struct { Type string `json:"type"` Data interface{} `json:"data"` Timestamp int64 `json:"timestamp"` } // ChatMessage 聊天消息结构 type ChatMessage struct { ID string `json:"id"` ConversationID string `json:"conversation_id"` SenderID string `json:"sender_id"` Seq int64 `json:"seq"` Segments model.MessageSegments `json:"segments"` // 消息链(结构体数组) ReplyToID *string `json:"reply_to_id,omitempty"` CreatedAt int64 `json:"created_at"` } // SystemMessage 系统消息结构 type SystemMessage struct { ID string `json:"id"` // 消息ID Type string `json:"type"` // 消息子类型(如:account_banned, post_approved等) Title string `json:"title"` // 消息标题 Content string `json:"content"` // 消息内容 Data map[string]interface{} `json:"data"` // 额外数据 CreatedAt int64 `json:"created_at"` // 创建时间戳 } // NotificationMessage 通知消息结构 type NotificationMessage struct { ID string `json:"id"` // 通知ID Type string `json:"type"` // 通知类型(like, comment, follow, mention等) Title string `json:"title"` // 通知标题 Content string `json:"content"` // 通知内容 TriggerUser *NotificationUser `json:"trigger_user"` // 触发用户 ResourceType string `json:"resource_type"` // 资源类型(post, comment等) ResourceID string `json:"resource_id"` // 资源ID Extra map[string]interface{} `json:"extra"` // 额外数据 CreatedAt int64 `json:"created_at"` // 创建时间戳 } // NotificationUser 通知中的用户信息 type NotificationUser struct { ID string `json:"id"` Username string `json:"username"` Avatar string `json:"avatar"` } // AnnouncementMessage 公告消息结构 type AnnouncementMessage struct { ID string `json:"id"` // 公告ID Title string `json:"title"` // 公告标题 Content string `json:"content"` // 公告内容 Priority int `json:"priority"` // 优先级(1-10) CreatedAt int64 `json:"created_at"` // 创建时间戳 } // GroupMessage 群消息结构 type GroupMessage struct { ID string `json:"id"` // 消息ID ConversationID string `json:"conversation_id"` // 会话ID(群聊会话) GroupID string `json:"group_id"` // 群组ID SenderID string `json:"sender_id"` // 发送者ID Seq int64 `json:"seq"` // 消息序号 Segments model.MessageSegments `json:"segments"` // 消息链(结构体数组) ReplyToID *string `json:"reply_to_id,omitempty"` // 回复的消息ID MentionUsers []uint64 `json:"mention_users,omitempty"` // @的用户ID列表 MentionAll bool `json:"mention_all"` // 是否@所有人 CreatedAt int64 `json:"created_at"` // 创建时间戳 } // GroupTypingMessage 群输入状态消息 type GroupTypingMessage struct { GroupID string `json:"group_id"` // 群组ID UserID string `json:"user_id"` // 用户ID Username string `json:"username"` // 用户名 IsTyping bool `json:"is_typing"` // 是否正在输入 } // GroupNoticeMessage 群组通知消息 type GroupNoticeMessage struct { NoticeType string `json:"notice_type"` // 通知类型:member_join, member_leave, member_removed, role_changed, muted, unmuted, group_dissolved GroupID string `json:"group_id"` // 群组ID Data interface{} `json:"data"` // 通知数据 Timestamp int64 `json:"timestamp"` // 时间戳 MessageID string `json:"message_id,omitempty"` // 消息ID(如果通知保存为消息) Seq int64 `json:"seq,omitempty"` // 消息序号(如果通知保存为消息) } // GroupNoticeData 通知数据结构 type GroupNoticeData struct { // 成员变动 UserID string `json:"user_id,omitempty"` // 变动的用户ID Username string `json:"username,omitempty"` // 用户名 OperatorID string `json:"operator_id,omitempty"` // 操作者ID OpName string `json:"op_name,omitempty"` // 操作者名称 NewRole string `json:"new_role,omitempty"` // 新角色 OldRole string `json:"old_role,omitempty"` // 旧角色 MemberCount int `json:"member_count,omitempty"` // 当前成员数 // 群设置变更 MuteAll bool `json:"mute_all,omitempty"` // 全员禁言状态 } // GroupMentionMessage @提及通知消息 type GroupMentionMessage struct { GroupID string `json:"group_id"` // 群组ID MessageID string `json:"message_id"` // 消息ID FromUserID string `json:"from_user_id"` // 发送者ID FromName string `json:"from_name"` // 发送者名称 Content string `json:"content"` // 消息内容预览 MentionAll bool `json:"mention_all"` // 是否@所有人 CreatedAt int64 `json:"created_at"` // 创建时间戳 } // AckMessage 消息发送确认结构 type AckMessage struct { ConversationID string `json:"conversation_id"` // 会话ID GroupID string `json:"group_id,omitempty"` // 群组ID(群聊时) ID string `json:"id"` // 消息ID SenderID string `json:"sender_id"` // 发送者ID Seq int64 `json:"seq"` // 消息序号 Segments model.MessageSegments `json:"segments"` // 消息链(结构体数组) CreatedAt int64 `json:"created_at"` // 创建时间戳 } // Client WebSocket客户端 type Client struct { ID string UserID string Conn *websocket.Conn Send chan []byte Manager *WebSocketManager IsClosed bool Mu sync.Mutex closeOnce sync.Once // 确保 Send channel 只关闭一次 } // WebSocketManager WebSocket连接管理器 type WebSocketManager struct { clients map[string]*Client // userID -> Client register chan *Client unregister chan *Client broadcast chan *BroadcastMessage mutex sync.RWMutex } // BroadcastMessage 广播消息 type BroadcastMessage struct { Message *WSMessage ExcludeUser string // 排除的用户ID,为空表示不排除 TargetUser string // 目标用户ID,为空表示广播给所有用户 } // NewWebSocketManager 创建WebSocket管理器 func NewWebSocketManager() *WebSocketManager { return &WebSocketManager{ clients: make(map[string]*Client), register: make(chan *Client, 100), unregister: make(chan *Client, 100), broadcast: make(chan *BroadcastMessage, 100), } } // Start 启动管理器 func (m *WebSocketManager) Start() { go func() { for { select { case client := <-m.register: m.mutex.Lock() m.clients[client.UserID] = client m.mutex.Unlock() log.Printf("WebSocket client connected: userID=%s, 当前在线用户数=%d", client.UserID, len(m.clients)) case client := <-m.unregister: m.mutex.Lock() if _, ok := m.clients[client.UserID]; ok { delete(m.clients, client.UserID) // 使用 closeOnce 确保 channel 只关闭一次,避免 panic client.closeOnce.Do(func() { close(client.Send) }) log.Printf("WebSocket client disconnected: userID=%s", client.UserID) } m.mutex.Unlock() case broadcast := <-m.broadcast: m.sendMessage(broadcast) } } }() } // Register 注册客户端 func (m *WebSocketManager) Register(client *Client) { m.register <- client } // Unregister 注销客户端 func (m *WebSocketManager) Unregister(client *Client) { m.unregister <- client } // Broadcast 广播消息给所有用户 func (m *WebSocketManager) Broadcast(msg *WSMessage) { m.broadcast <- &BroadcastMessage{ Message: msg, TargetUser: "", } } // SendToUser 发送消息给指定用户 func (m *WebSocketManager) SendToUser(userID string, msg *WSMessage) { m.broadcast <- &BroadcastMessage{ Message: msg, TargetUser: userID, } } // SendToUsers 发送消息给指定用户列表 func (m *WebSocketManager) SendToUsers(userIDs []string, msg *WSMessage) { for _, userID := range userIDs { m.SendToUser(userID, msg) } } // GetClient 获取客户端 func (m *WebSocketManager) GetClient(userID string) (*Client, bool) { m.mutex.RLock() defer m.mutex.RUnlock() client, ok := m.clients[userID] return client, ok } // GetAllClients 获取所有客户端 func (m *WebSocketManager) GetAllClients() map[string]*Client { m.mutex.RLock() defer m.mutex.RUnlock() return m.clients } // GetClientCount 获取在线用户数量 func (m *WebSocketManager) GetClientCount() int { m.mutex.RLock() defer m.mutex.RUnlock() return len(m.clients) } // IsUserOnline 检查用户是否在线 func (m *WebSocketManager) IsUserOnline(userID string) bool { m.mutex.RLock() defer m.mutex.RUnlock() _, ok := m.clients[userID] log.Printf("[DEBUG IsUserOnline] 检查用户 %s, 结果=%v, 当前在线用户=%v", userID, ok, m.clients) return ok } // sendMessage 发送消息 func (m *WebSocketManager) sendMessage(broadcast *BroadcastMessage) { msgBytes, err := json.Marshal(broadcast.Message) if err != nil { log.Printf("Failed to marshal message: %v", err) return } log.Printf("[DEBUG WebSocket] sendMessage: 目标用户=%s, 当前在线用户数=%d, 消息类型=%s", broadcast.TargetUser, len(m.clients), broadcast.Message.Type) m.mutex.RLock() defer m.mutex.RUnlock() for userID, client := range m.clients { // 如果指定了目标用户,只发送给目标用户 if broadcast.TargetUser != "" && userID != broadcast.TargetUser { continue } // 如果指定了排除用户,跳过 if broadcast.ExcludeUser != "" && userID == broadcast.ExcludeUser { continue } select { case client.Send <- msgBytes: log.Printf("[DEBUG WebSocket] 成功发送消息到用户 %s, 消息类型=%s", userID, broadcast.Message.Type) default: log.Printf("Failed to send message to user %s: channel full", userID) } } } // SendPing 发送心跳 func (c *Client) SendPing() error { c.Mu.Lock() defer c.Mu.Unlock() if c.IsClosed { return nil } msg := WSMessage{ Type: MessageTypePing, Data: nil, Timestamp: time.Now().UnixMilli(), } msgBytes, _ := json.Marshal(msg) return c.Conn.WriteMessage(websocket.TextMessage, msgBytes) } // SendPong 发送Pong响应 func (c *Client) SendPong() error { c.Mu.Lock() defer c.Mu.Unlock() if c.IsClosed { return nil } msg := WSMessage{ Type: MessageTypePong, Data: nil, Timestamp: time.Now().UnixMilli(), } msgBytes, _ := json.Marshal(msg) return c.Conn.WriteMessage(websocket.TextMessage, msgBytes) } // WritePump 写入泵,将消息从Manager发送到客户端 func (c *Client) WritePump() { defer func() { c.Conn.Close() c.Mu.Lock() c.IsClosed = true c.Mu.Unlock() }() for { message, ok := <-c.Send if !ok { c.Conn.WriteMessage(websocket.CloseMessage, []byte{}) return } c.Mu.Lock() if c.IsClosed { c.Mu.Unlock() return } err := c.Conn.WriteMessage(websocket.TextMessage, message) c.Mu.Unlock() if err != nil { log.Printf("Write error: %v", err) return } } } // ReadPump 读取泵,从客户端读取消息 func (c *Client) ReadPump(handler func(msg *WSMessage)) { defer func() { c.Manager.Unregister(c) c.Conn.Close() c.Mu.Lock() c.IsClosed = true c.Mu.Unlock() }() c.Conn.SetReadLimit(512 * 1024) // 512KB c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) c.Conn.SetPongHandler(func(string) error { c.Conn.SetReadDeadline(time.Now().Add(60 * time.Second)) return nil }) for { _, message, err := c.Conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { log.Printf("WebSocket error: %v", err) } break } var wsMsg WSMessage if err := json.Unmarshal(message, &wsMsg); err != nil { log.Printf("Failed to unmarshal message: %v", err) continue } handler(&wsMsg) } } // CreateWSMessage 创建WebSocket消息 func CreateWSMessage(msgType string, data interface{}) *WSMessage { return &WSMessage{ Type: msgType, Data: data, Timestamp: time.Now().UnixMilli(), } }