Files
backend/internal/pkg/websocket/websocket.go
lan 4d8f2ec997 Initial backend repository commit.
Set up project files and add .gitignore to exclude local build/runtime artifacts.

Made-with: Cursor
2026-03-09 21:28:58 +08:00

441 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(),
}
}