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:
2026-03-10 12:58:23 +08:00
parent 4c0177149a
commit 86ef150fec
19 changed files with 689 additions and 1719 deletions

152
internal/pkg/sse/hub.go Normal file
View File

@@ -0,0 +1,152 @@
package sse
import (
"encoding/json"
"strconv"
"sync"
"sync/atomic"
"time"
)
const (
defaultUserBufferSize = 128
maxReplayEvents = 200
)
type Event struct {
ID uint64 `json:"event_id"`
Event string `json:"event"`
TS int64 `json:"ts"`
Payload interface{} `json:"payload"`
}
type subscriber struct {
id uint64
ch chan Event
quit chan struct{}
}
type Hub struct {
seq uint64
mu sync.RWMutex
subscribers map[string]map[uint64]*subscriber
history map[string][]Event
}
func NewHub() *Hub {
return &Hub{
subscribers: make(map[string]map[uint64]*subscriber),
history: make(map[string][]Event),
}
}
func (h *Hub) NextID() uint64 {
return atomic.AddUint64(&h.seq, 1)
}
func ParseEventID(raw string) uint64 {
if raw == "" {
return 0
}
id, err := strconv.ParseUint(raw, 10, 64)
if err != nil {
return 0
}
return id
}
func (h *Hub) Subscribe(userID string, afterID uint64) (chan Event, func(), []Event) {
subID := h.NextID()
sub := &subscriber{
id: subID,
ch: make(chan Event, defaultUserBufferSize),
quit: make(chan struct{}),
}
h.mu.Lock()
if _, ok := h.subscribers[userID]; !ok {
h.subscribers[userID] = make(map[uint64]*subscriber)
}
h.subscribers[userID][subID] = sub
replay := make([]Event, 0)
for _, e := range h.history[userID] {
if e.ID > afterID {
replay = append(replay, e)
}
}
h.mu.Unlock()
cancel := func() {
h.mu.Lock()
defer h.mu.Unlock()
if userSubs, ok := h.subscribers[userID]; ok {
if s, exists := userSubs[subID]; exists {
close(s.quit)
delete(userSubs, subID)
close(s.ch)
}
if len(userSubs) == 0 {
delete(h.subscribers, userID)
}
}
}
return sub.ch, cancel, replay
}
func (h *Hub) HasSubscribers(userID string) bool {
h.mu.RLock()
defer h.mu.RUnlock()
return len(h.subscribers[userID]) > 0
}
func (h *Hub) PublishToUser(userID string, eventName string, payload interface{}) Event {
ev := Event{
ID: h.NextID(),
Event: eventName,
TS: time.Now().UnixMilli(),
Payload: payload,
}
h.publish(userID, ev)
return ev
}
func (h *Hub) PublishToUsers(userIDs []string, eventName string, payload interface{}) {
for _, uid := range userIDs {
h.PublishToUser(uid, eventName, payload)
}
}
func (h *Hub) publish(userID string, ev Event) {
h.mu.Lock()
history := append(h.history[userID], ev)
if len(history) > maxReplayEvents {
history = history[len(history)-maxReplayEvents:]
}
h.history[userID] = history
targets := make([]*subscriber, 0, len(h.subscribers[userID]))
for _, s := range h.subscribers[userID] {
targets = append(targets, s)
}
h.mu.Unlock()
for _, s := range targets {
select {
case <-s.quit:
case s.ch <- ev:
default:
// 慢消费者丢弃单条消息,客户端可通过 Last-Event-ID + HTTP 同步补偿
}
}
}
func EncodeData(ev Event) (string, error) {
body, err := json.Marshal(ev.Payload)
if err != nil {
return "", err
}
return string(body), nil
}

View File

@@ -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(),
}
}