Replace websocket flow with SSE support in backend.
Update handlers, services, router, and data conversion logic to support server-sent events and related message pipeline changes. Made-with: Cursor
This commit is contained in:
@@ -1,435 +0,0 @@
|
||||
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]
|
||||
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
|
||||
}
|
||||
|
||||
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:
|
||||
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(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user