feat(websocket): integrate WebSocket support and refactor SSE handling
All checks were successful
Build Backend / build (push) Successful in 18m57s
Build Backend / build-docker (push) Successful in 1m4s

- Added WebSocket support by introducing a new WSHandler and related infrastructure.
- Replaced SSE implementations with WebSocket equivalents across message and QR code handlers.
- Updated service and repository layers to utilize WebSocket for real-time messaging.
- Removed obsolete SSE hub and related code, streamlining the application for WebSocket usage.
- Enhanced router to include WebSocket endpoints for real-time communication.
This commit is contained in:
lafay
2026-03-26 21:17:49 +08:00
parent c6848aba06
commit 6d335e393d
19 changed files with 1277 additions and 315 deletions

View File

@@ -2,10 +2,7 @@ package handler
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
@@ -13,7 +10,7 @@ import (
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/cursor"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/pkg/sse"
"carrot_bbs/internal/pkg/ws"
"carrot_bbs/internal/service"
)
@@ -23,95 +20,17 @@ type MessageHandler struct {
messageService *service.MessageService
userService service.UserService
groupService service.GroupService
sseHub *sse.Hub
wsHub *ws.Hub
}
// NewMessageHandler 创建消息处理器
func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService service.UserService, groupService service.GroupService, sseHub *sse.Hub) *MessageHandler {
func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService service.UserService, groupService service.GroupService, wsHub *ws.Hub) *MessageHandler {
return &MessageHandler{
chatService: chatService,
messageService: messageService,
userService: userService,
groupService: groupService,
sseHub: sseHub,
}
}
// HandleSSE 实时消息订阅SSE
// GET /api/v1/realtime/sse
func (h *MessageHandler) HandleSSE(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
if h.sseHub == nil {
response.InternalServerError(c, "sse hub not available")
return
}
lastID := sse.ParseEventID(c.GetHeader("Last-Event-ID"))
if lastID == 0 {
lastID = sse.ParseEventID(c.Query("last_event_id"))
}
ch, cancel, replay := h.sseHub.Subscribe(userID, lastID)
defer cancel()
w := c.Writer
flusher, ok := w.(http.Flusher)
if !ok {
response.InternalServerError(c, "streaming unsupported")
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache, no-transform")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no")
c.Status(http.StatusOK)
flusher.Flush()
writeEvent := func(ev sse.Event) bool {
data, err := sse.EncodeData(ev)
if err != nil {
return false
}
if _, err := fmt.Fprintf(w, "id: %d\nevent: %s\ndata: %s\n\n", ev.ID, ev.Event, data); err != nil {
return false
}
flusher.Flush()
return true
}
for _, ev := range replay {
if !writeEvent(ev) {
return
}
}
// Use a shorter heartbeat interval to survive common 50-60s proxy idle timeout.
heartbeat := time.NewTicker(10 * time.Second)
defer heartbeat.Stop()
for {
select {
case <-c.Request.Context().Done():
return
case ev, ok := <-ch:
if !ok || !writeEvent(ev) {
return
}
case <-heartbeat.C:
// Comment keepalive line for SSE intermediaries/proxies.
if _, err := fmt.Fprint(w, ": keepalive\n\n"); err != nil {
return
}
flusher.Flush()
if _, err := fmt.Fprint(w, "event: heartbeat\ndata: {}\n\n"); err != nil {
return
}
flusher.Flush()
}
wsHub: wsHub,
}
}