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:
@@ -2,12 +2,16 @@ package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"carrot_bbs/internal/dto"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/sse"
|
||||
"carrot_bbs/internal/pkg/response"
|
||||
"carrot_bbs/internal/service"
|
||||
)
|
||||
@@ -18,18 +22,111 @@ type MessageHandler struct {
|
||||
messageService *service.MessageService
|
||||
userService *service.UserService
|
||||
groupService service.GroupService
|
||||
sseHub *sse.Hub
|
||||
}
|
||||
|
||||
// NewMessageHandler 创建消息处理器
|
||||
func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService *service.UserService, groupService service.GroupService) *MessageHandler {
|
||||
func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService *service.UserService, groupService service.GroupService, sseHub *sse.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")
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
heartbeat := time.NewTicker(25 * 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:
|
||||
if _, err := fmt.Fprint(w, "event: heartbeat\ndata: {}\n\n"); err != nil {
|
||||
return
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleTyping 输入状态上报
|
||||
// POST /api/v1/conversations/typing
|
||||
func (h *MessageHandler) HandleTyping(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "")
|
||||
return
|
||||
}
|
||||
var params struct {
|
||||
ConversationID string `json:"conversation_id" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(¶ms); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.chatService.SendTyping(c.Request.Context(), userID, params.ConversationID)
|
||||
response.SuccessWithMessage(c, "typing sent", nil)
|
||||
}
|
||||
|
||||
// GetConversations 获取会话列表
|
||||
// GET /api/conversations
|
||||
func (h *MessageHandler) GetConversations(c *gin.Context) {
|
||||
|
||||
Reference in New Issue
Block a user