Set up project files and add .gitignore to exclude local build/runtime artifacts. Made-with: Cursor
441 lines
14 KiB
Go
441 lines
14 KiB
Go
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(),
|
||
}
|
||
}
|