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:
@@ -4,11 +4,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/dto"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/websocket"
|
||||
"carrot_bbs/internal/pkg/sse"
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -41,17 +41,13 @@ type ChatService interface {
|
||||
RecallMessage(ctx context.Context, messageID string, userID string) error
|
||||
DeleteMessage(ctx context.Context, messageID string, userID string) error
|
||||
|
||||
// WebSocket相关
|
||||
// 实时事件相关
|
||||
SendTyping(ctx context.Context, senderID string, conversationID string)
|
||||
BroadcastMessage(ctx context.Context, msg *websocket.WSMessage, targetUser string)
|
||||
|
||||
// 系统消息推送
|
||||
// 在线状态
|
||||
IsUserOnline(userID string) bool
|
||||
PushSystemMessage(userID string, msgType, title, content string, data map[string]interface{}) error
|
||||
PushNotificationMessage(userID string, notification *websocket.NotificationMessage) error
|
||||
PushAnnouncementMessage(announcement *websocket.AnnouncementMessage) error
|
||||
|
||||
// 仅保存消息到数据库,不发送 WebSocket 推送(供群聊等自行推送的场景使用)
|
||||
// 仅保存消息到数据库,不发送实时推送(供群聊等自行推送的场景使用)
|
||||
SaveMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error)
|
||||
}
|
||||
|
||||
@@ -61,7 +57,7 @@ type chatServiceImpl struct {
|
||||
repo *repository.MessageRepository
|
||||
userRepo *repository.UserRepository
|
||||
sensitive SensitiveService
|
||||
wsManager *websocket.WebSocketManager
|
||||
sseHub *sse.Hub
|
||||
}
|
||||
|
||||
// NewChatService 创建聊天服务
|
||||
@@ -70,17 +66,24 @@ func NewChatService(
|
||||
repo *repository.MessageRepository,
|
||||
userRepo *repository.UserRepository,
|
||||
sensitive SensitiveService,
|
||||
wsManager *websocket.WebSocketManager,
|
||||
sseHub *sse.Hub,
|
||||
) ChatService {
|
||||
return &chatServiceImpl{
|
||||
db: db,
|
||||
repo: repo,
|
||||
userRepo: userRepo,
|
||||
sensitive: sensitive,
|
||||
wsManager: wsManager,
|
||||
sseHub: sseHub,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *chatServiceImpl) publishSSEToUsers(userIDs []string, event string, payload interface{}) {
|
||||
if s.sseHub == nil || len(userIDs) == 0 {
|
||||
return
|
||||
}
|
||||
s.sseHub.PublishToUsers(userIDs, event, payload)
|
||||
}
|
||||
|
||||
// GetOrCreateConversation 获取或创建私聊会话
|
||||
func (s *chatServiceImpl) GetOrCreateConversation(ctx context.Context, user1ID, user2ID string) (*model.Conversation, error) {
|
||||
return s.repo.GetOrCreatePrivateConversation(user1ID, user2ID)
|
||||
@@ -228,30 +231,30 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
||||
return nil, fmt.Errorf("failed to save message: %w", err)
|
||||
}
|
||||
|
||||
// 发送消息给接收者
|
||||
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeMessage, websocket.ChatMessage{
|
||||
ID: message.ID,
|
||||
ConversationID: message.ConversationID,
|
||||
SenderID: senderID,
|
||||
Segments: message.Segments,
|
||||
Seq: message.Seq,
|
||||
CreatedAt: message.CreatedAt.UnixMilli(),
|
||||
})
|
||||
|
||||
// 获取会话中的其他参与者
|
||||
// 获取会话中的参与者并发送 SSE
|
||||
participants, err := s.repo.GetConversationParticipants(conversationID)
|
||||
if err == nil {
|
||||
targetIDs := make([]string, 0, len(participants))
|
||||
for _, p := range participants {
|
||||
targetIDs = append(targetIDs, p.UserID)
|
||||
}
|
||||
detailType := "private"
|
||||
if conv.Type == model.ConversationTypeGroup {
|
||||
detailType = "group"
|
||||
}
|
||||
s.publishSSEToUsers(targetIDs, "chat_message", map[string]interface{}{
|
||||
"detail_type": detailType,
|
||||
"message": dto.ConvertMessageToResponse(message),
|
||||
})
|
||||
for _, p := range participants {
|
||||
// 不发给自己
|
||||
if p.UserID == senderID {
|
||||
continue
|
||||
}
|
||||
// 如果接收者在线,发送实时消息
|
||||
if s.wsManager != nil {
|
||||
isOnline := s.wsManager.IsUserOnline(p.UserID)
|
||||
if isOnline {
|
||||
s.wsManager.SendToUser(p.UserID, wsMsg)
|
||||
}
|
||||
if totalUnread, uErr := s.repo.GetAllUnreadCount(p.UserID); uErr == nil {
|
||||
s.publishSSEToUsers([]string{p.UserID}, "conversation_unread", map[string]interface{}{
|
||||
"conversation_id": conversationID,
|
||||
"total_unread": totalUnread,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,25 +340,33 @@ func (s *chatServiceImpl) MarkAsRead(ctx context.Context, conversationID string,
|
||||
return fmt.Errorf("failed to update last read seq: %w", err)
|
||||
}
|
||||
|
||||
// 发送已读回执(作为 meta 事件)
|
||||
if s.wsManager != nil {
|
||||
wsMsg := websocket.CreateWSMessage("meta", map[string]interface{}{
|
||||
"detail_type": websocket.MetaDetailTypeRead,
|
||||
"conversation_id": conversationID,
|
||||
"seq": seq,
|
||||
"user_id": userID,
|
||||
})
|
||||
|
||||
// 获取会话中的所有参与者
|
||||
participants, err := s.repo.GetConversationParticipants(conversationID)
|
||||
if err == nil {
|
||||
// 推送给会话中的所有参与者(包括自己)
|
||||
for _, p := range participants {
|
||||
if s.wsManager.IsUserOnline(p.UserID) {
|
||||
s.wsManager.SendToUser(p.UserID, wsMsg)
|
||||
}
|
||||
participants, pErr := s.repo.GetConversationParticipants(conversationID)
|
||||
if pErr == nil {
|
||||
detailType := "private"
|
||||
groupID := ""
|
||||
if conv, convErr := s.repo.GetConversation(conversationID); convErr == nil && conv.Type == model.ConversationTypeGroup {
|
||||
detailType = "group"
|
||||
if conv.GroupID != nil {
|
||||
groupID = *conv.GroupID
|
||||
}
|
||||
}
|
||||
targetIDs := make([]string, 0, len(participants))
|
||||
for _, p := range participants {
|
||||
targetIDs = append(targetIDs, p.UserID)
|
||||
}
|
||||
s.publishSSEToUsers(targetIDs, "message_read", map[string]interface{}{
|
||||
"detail_type": detailType,
|
||||
"conversation_id": conversationID,
|
||||
"group_id": groupID,
|
||||
"user_id": userID,
|
||||
"seq": seq,
|
||||
})
|
||||
}
|
||||
if totalUnread, uErr := s.repo.GetAllUnreadCount(userID); uErr == nil {
|
||||
s.publishSSEToUsers([]string{userID}, "conversation_unread", map[string]interface{}{
|
||||
"conversation_id": conversationID,
|
||||
"total_unread": totalUnread,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -407,29 +418,35 @@ func (s *chatServiceImpl) RecallMessage(ctx context.Context, messageID string, u
|
||||
return errors.New("message recall timeout (2 minutes)")
|
||||
}
|
||||
|
||||
// 更新消息状态为已撤回
|
||||
err = s.db.Model(&message).Update("status", model.MessageStatusRecalled).Error
|
||||
// 更新消息状态为已撤回,并清空原始消息内容,仅保留撤回占位
|
||||
err = s.db.Model(&message).Updates(map[string]interface{}{
|
||||
"status": model.MessageStatusRecalled,
|
||||
"segments": model.MessageSegments{},
|
||||
}).Error
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to recall message: %w", err)
|
||||
}
|
||||
|
||||
// 发送撤回通知
|
||||
if s.wsManager != nil {
|
||||
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeRecall, map[string]interface{}{
|
||||
"messageId": messageID,
|
||||
"conversationId": message.ConversationID,
|
||||
"senderId": userID,
|
||||
})
|
||||
|
||||
// 通知会话中的所有参与者
|
||||
participants, err := s.repo.GetConversationParticipants(message.ConversationID)
|
||||
if err == nil {
|
||||
for _, p := range participants {
|
||||
if s.wsManager.IsUserOnline(p.UserID) {
|
||||
s.wsManager.SendToUser(p.UserID, wsMsg)
|
||||
}
|
||||
if participants, pErr := s.repo.GetConversationParticipants(message.ConversationID); pErr == nil {
|
||||
detailType := "private"
|
||||
groupID := ""
|
||||
if conv, convErr := s.repo.GetConversation(message.ConversationID); convErr == nil && conv.Type == model.ConversationTypeGroup {
|
||||
detailType = "group"
|
||||
if conv.GroupID != nil {
|
||||
groupID = *conv.GroupID
|
||||
}
|
||||
}
|
||||
targetIDs := make([]string, 0, len(participants))
|
||||
for _, p := range participants {
|
||||
targetIDs = append(targetIDs, p.UserID)
|
||||
}
|
||||
s.publishSSEToUsers(targetIDs, "message_recall", map[string]interface{}{
|
||||
"detail_type": detailType,
|
||||
"conversation_id": message.ConversationID,
|
||||
"group_id": groupID,
|
||||
"message_id": messageID,
|
||||
"sender_id": userID,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -473,7 +490,7 @@ func (s *chatServiceImpl) DeleteMessage(ctx context.Context, messageID string, u
|
||||
|
||||
// SendTyping 发送正在输入状态
|
||||
func (s *chatServiceImpl) SendTyping(ctx context.Context, senderID string, conversationID string) {
|
||||
if s.wsManager == nil {
|
||||
if s.sseHub == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -489,98 +506,34 @@ func (s *chatServiceImpl) SendTyping(ctx context.Context, senderID string, conve
|
||||
return
|
||||
}
|
||||
|
||||
detailType := "private"
|
||||
if conv, convErr := s.repo.GetConversation(conversationID); convErr == nil && conv.Type == model.ConversationTypeGroup {
|
||||
detailType = "group"
|
||||
}
|
||||
for _, p := range participants {
|
||||
if p.UserID == senderID {
|
||||
continue
|
||||
}
|
||||
// 发送正在输入状态
|
||||
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeTyping, map[string]string{
|
||||
"conversationId": conversationID,
|
||||
"senderId": senderID,
|
||||
})
|
||||
|
||||
if s.wsManager.IsUserOnline(p.UserID) {
|
||||
s.wsManager.SendToUser(p.UserID, wsMsg)
|
||||
if s.sseHub != nil {
|
||||
s.sseHub.PublishToUser(p.UserID, "typing", map[string]interface{}{
|
||||
"detail_type": detailType,
|
||||
"conversation_id": conversationID,
|
||||
"user_id": senderID,
|
||||
"is_typing": true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastMessage 广播消息给用户
|
||||
func (s *chatServiceImpl) BroadcastMessage(ctx context.Context, msg *websocket.WSMessage, targetUser string) {
|
||||
if s.wsManager != nil {
|
||||
s.wsManager.SendToUser(targetUser, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// IsUserOnline 检查用户是否在线
|
||||
func (s *chatServiceImpl) IsUserOnline(userID string) bool {
|
||||
if s.wsManager == nil {
|
||||
return false
|
||||
if s.sseHub != nil {
|
||||
return s.sseHub.HasSubscribers(userID)
|
||||
}
|
||||
return s.wsManager.IsUserOnline(userID)
|
||||
return false
|
||||
}
|
||||
|
||||
// PushSystemMessage 推送系统消息给指定用户
|
||||
func (s *chatServiceImpl) PushSystemMessage(userID string, msgType, title, content string, data map[string]interface{}) error {
|
||||
if s.wsManager == nil {
|
||||
return errors.New("websocket manager not available")
|
||||
}
|
||||
|
||||
if !s.wsManager.IsUserOnline(userID) {
|
||||
return errors.New("user is offline")
|
||||
}
|
||||
|
||||
sysMsg := &websocket.SystemMessage{
|
||||
ID: "", // 由调用方生成
|
||||
Type: msgType,
|
||||
Title: title,
|
||||
Content: content,
|
||||
Data: data,
|
||||
CreatedAt: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeSystem, sysMsg)
|
||||
s.wsManager.SendToUser(userID, wsMsg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushNotificationMessage 推送通知消息给指定用户
|
||||
func (s *chatServiceImpl) PushNotificationMessage(userID string, notification *websocket.NotificationMessage) error {
|
||||
if s.wsManager == nil {
|
||||
return errors.New("websocket manager not available")
|
||||
}
|
||||
|
||||
if !s.wsManager.IsUserOnline(userID) {
|
||||
return errors.New("user is offline")
|
||||
}
|
||||
|
||||
// 确保时间戳已设置
|
||||
if notification.CreatedAt == 0 {
|
||||
notification.CreatedAt = time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeNotification, notification)
|
||||
s.wsManager.SendToUser(userID, wsMsg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// PushAnnouncementMessage 广播公告消息给所有在线用户
|
||||
func (s *chatServiceImpl) PushAnnouncementMessage(announcement *websocket.AnnouncementMessage) error {
|
||||
if s.wsManager == nil {
|
||||
return errors.New("websocket manager not available")
|
||||
}
|
||||
|
||||
// 确保时间戳已设置
|
||||
if announcement.CreatedAt == 0 {
|
||||
announcement.CreatedAt = time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeAnnouncement, announcement)
|
||||
s.wsManager.Broadcast(wsMsg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveMessage 仅保存消息到数据库,不发送 WebSocket 推送
|
||||
// SaveMessage 仅保存消息到数据库,不发送实时推送
|
||||
// 适用于群聊等由调用方自行负责推送的场景
|
||||
func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error) {
|
||||
// 验证会话是否存在
|
||||
|
||||
Reference in New Issue
Block a user