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

View File

@@ -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) {
// 验证会话是否存在

View File

@@ -7,6 +7,7 @@ import (
"log"
"strings"
"carrot_bbs/internal/cache"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/gorse"
"carrot_bbs/internal/repository"
@@ -17,6 +18,7 @@ type CommentService struct {
commentRepo *repository.CommentRepository
postRepo *repository.PostRepository
systemMessageService SystemMessageService
cache cache.Cache
gorseClient gorse.Client
postAIService *PostAIService
}
@@ -27,6 +29,7 @@ func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repo
commentRepo: commentRepo,
postRepo: postRepo,
systemMessageService: systemMessageService,
cache: cache.GetCache(),
gorseClient: gorseClient,
postAIService: postAIService,
}
@@ -96,6 +99,10 @@ func (s *CommentService) reviewCommentAsync(
log.Printf("[WARN] Failed to publish comment without AI moderation: %v", err)
return
}
if err := s.applyCommentPublishedStats(commentID); err != nil {
log.Printf("[WARN] Failed to apply published stats for comment %s: %v", commentID, err)
}
s.invalidatePostCaches(postID)
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
return
}
@@ -116,6 +123,10 @@ func (s *CommentService) reviewCommentAsync(
log.Printf("[WARN] Failed to publish comment %s after moderation error: %v", commentID, updateErr)
return
}
if statsErr := s.applyCommentPublishedStats(commentID); statsErr != nil {
log.Printf("[WARN] Failed to apply published stats for comment %s: %v", commentID, statsErr)
}
s.invalidatePostCaches(postID)
log.Printf("[WARN] Comment moderation failed, fallback publish comment=%s err=%v", commentID, err)
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
return
@@ -125,9 +136,26 @@ func (s *CommentService) reviewCommentAsync(
log.Printf("[WARN] Failed to publish comment %s: %v", commentID, updateErr)
return
}
if statsErr := s.applyCommentPublishedStats(commentID); statsErr != nil {
log.Printf("[WARN] Failed to apply published stats for comment %s: %v", commentID, statsErr)
}
s.invalidatePostCaches(postID)
s.afterCommentPublished(userID, postID, commentID, parentID, parentUserID, postOwnerID)
}
func (s *CommentService) applyCommentPublishedStats(commentID string) error {
comment, err := s.commentRepo.GetByID(commentID)
if err != nil {
return err
}
return s.commentRepo.ApplyPublishedStats(comment)
}
func (s *CommentService) invalidatePostCaches(postID string) {
cache.InvalidatePostDetail(s.cache, postID)
cache.InvalidatePostList(s.cache)
}
func (s *CommentService) afterCommentPublished(userID, postID, commentID string, parentID *string, parentUserID, postOwnerID string) {
// 发送系统消息通知
if s.systemMessageService != nil {
@@ -212,7 +240,15 @@ func (s *CommentService) Update(ctx context.Context, comment *model.Comment) err
// Delete 删除评论
func (s *CommentService) Delete(ctx context.Context, id string) error {
return s.commentRepo.Delete(id)
comment, err := s.commentRepo.GetByID(id)
if err != nil {
return err
}
if err := s.commentRepo.Delete(id); err != nil {
return err
}
s.invalidatePostCaches(comment.PostID)
return nil
}
// Like 点赞评论

View File

@@ -9,8 +9,8 @@ import (
"carrot_bbs/internal/cache"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/sse"
"carrot_bbs/internal/pkg/utils"
"carrot_bbs/internal/pkg/websocket"
"carrot_bbs/internal/repository"
"gorm.io/gorm"
@@ -18,7 +18,7 @@ import (
// 缓存TTL常量
const (
GroupMembersTTL = 120 * time.Second // 群组成员缓存120秒
GroupMembersTTL = 120 * time.Second // 群组成员缓存120秒
GroupMembersNullTTL = 5 * time.Second
GroupCacheJitter = 0.1
)
@@ -99,12 +99,12 @@ type groupService struct {
messageRepo *repository.MessageRepository
requestRepo repository.GroupJoinRequestRepository
notifyRepo *repository.SystemNotificationRepository
wsManager *websocket.WebSocketManager
sseHub *sse.Hub
cache cache.Cache
}
// NewGroupService 创建群组服务
func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo *repository.UserRepository, messageRepo *repository.MessageRepository, wsManager *websocket.WebSocketManager) GroupService {
func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo *repository.UserRepository, messageRepo *repository.MessageRepository, sseHub *sse.Hub) GroupService {
return &groupService{
db: db,
groupRepo: groupRepo,
@@ -112,11 +112,39 @@ func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo
messageRepo: messageRepo,
requestRepo: repository.NewGroupJoinRequestRepository(db),
notifyRepo: repository.NewSystemNotificationRepository(db),
wsManager: wsManager,
sseHub: sseHub,
cache: cache.GetCache(),
}
}
type groupNoticeData struct {
UserID string `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
OperatorID string `json:"operator_id,omitempty"`
}
type groupNoticeMessage struct {
NoticeType string `json:"notice_type"`
GroupID string `json:"group_id"`
Data groupNoticeData `json:"data"`
Timestamp int64 `json:"timestamp"`
MessageID string `json:"message_id,omitempty"`
Seq int64 `json:"seq,omitempty"`
}
func (s *groupService) publishGroupNotice(groupID string, notice groupNoticeMessage) {
members, _, err := s.groupRepo.GetMembers(groupID, 1, 1000)
if err != nil {
log.Printf("[groupService] 获取群成员失败: groupID=%s, err=%v", groupID, err)
return
}
if s.sseHub != nil {
for _, m := range members {
s.sseHub.PublishToUser(m.UserID, "group_notice", notice)
}
}
}
// ==================== 群组管理 ====================
// CreateGroup 创建群组
@@ -422,14 +450,10 @@ func (s *groupService) broadcastMemberJoinNotice(groupID string, targetUserID st
}
}
if s.wsManager == nil {
return
}
noticeMsg := websocket.GroupNoticeMessage{
noticeMsg := groupNoticeMessage{
NoticeType: "member_join",
GroupID: groupID,
Data: websocket.GroupNoticeData{
Data: groupNoticeData{
UserID: targetUserID,
Username: targetUserName,
OperatorID: operatorID,
@@ -441,17 +465,7 @@ func (s *groupService) broadcastMemberJoinNotice(groupID string, targetUserID st
noticeMsg.Seq = savedMessage.Seq
}
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeGroupNotice, noticeMsg)
members, _, err := s.groupRepo.GetMembers(groupID, 1, 1000)
if err != nil {
log.Printf("[broadcastMemberJoinNotice] 获取群成员失败: groupID=%s, err=%v", groupID, err)
return
}
for _, m := range members {
if s.wsManager.IsUserOnline(m.UserID) {
s.wsManager.SendToUser(m.UserID, wsMsg)
}
}
s.publishGroupNotice(groupID, noticeMsg)
}
func (s *groupService) addMemberToGroupAndConversation(group *model.Group, userID string, operatorID string) error {
@@ -1282,46 +1296,20 @@ func (s *groupService) MuteMember(userID string, groupID string, targetUserID st
}
}
// 发送WebSocket通知给群成员
if s.wsManager != nil {
log.Printf("[MuteMember] 准备发送禁言通知: groupID=%s, targetUserID=%s, noticeType=%s, operatorID=%s", groupID, targetUserID, noticeType, userID)
// 构建通知消息,包含保存的消息信息
noticeMsg := websocket.GroupNoticeMessage{
NoticeType: noticeType,
GroupID: groupID,
Data: websocket.GroupNoticeData{
UserID: targetUserID,
OperatorID: userID,
},
Timestamp: time.Now().UnixMilli(),
}
// 如果消息已保存添加消息ID和seq
if savedMessage != nil {
noticeMsg.MessageID = savedMessage.ID
noticeMsg.Seq = savedMessage.Seq
}
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeGroupNotice, noticeMsg)
log.Printf("[MuteMember] 创建的WebSocket消息: Type=%s, Data=%+v", wsMsg.Type, wsMsg.Data)
// 获取所有群成员并发送通知
members, _, err := s.groupRepo.GetMembers(groupID, 1, 1000)
if err == nil {
log.Printf("[MuteMember] 获取到群成员数量: %d", len(members))
for _, m := range members {
isOnline := s.wsManager.IsUserOnline(m.UserID)
log.Printf("[MuteMember] 成员 %s 在线状态: %v", m.UserID, isOnline)
if isOnline {
s.wsManager.SendToUser(m.UserID, wsMsg)
log.Printf("[MuteMember] 已发送通知给成员: %s", m.UserID)
}
}
} else {
log.Printf("[MuteMember] 获取群成员失败: %v", err)
}
noticeMsg := groupNoticeMessage{
NoticeType: noticeType,
GroupID: groupID,
Data: groupNoticeData{
UserID: targetUserID,
OperatorID: userID,
},
Timestamp: time.Now().UnixMilli(),
}
if savedMessage != nil {
noticeMsg.MessageID = savedMessage.ID
noticeMsg.Seq = savedMessage.Seq
}
s.publishGroupNotice(groupID, noticeMsg)
// 失效群组成员缓存
cache.InvalidateGroupMembers(s.cache, groupID)

View File

@@ -77,6 +77,8 @@ func (s *PostService) reviewPostAsync(postID, userID, title, content string, ima
if s.postAIService == nil || !s.postAIService.IsEnabled() {
if err := s.postRepo.UpdateModerationStatus(postID, model.PostStatusPublished, "", "system"); err != nil {
log.Printf("[WARN] Failed to publish post without AI moderation: %v", err)
} else {
s.invalidatePostCaches(postID)
}
return
}
@@ -87,6 +89,8 @@ func (s *PostService) reviewPostAsync(postID, userID, title, content string, ima
if errors.As(err, &rejectedErr) {
if updateErr := s.postRepo.UpdateModerationStatus(postID, model.PostStatusRejected, rejectedErr.UserMessage(), "ai"); updateErr != nil {
log.Printf("[WARN] Failed to reject post %s: %v", postID, updateErr)
} else {
s.invalidatePostCaches(postID)
}
s.notifyModerationRejected(userID, rejectedErr.Reason)
return
@@ -95,6 +99,8 @@ func (s *PostService) reviewPostAsync(postID, userID, title, content string, ima
// 规则审核不可用时降级为发布避免长时间pending
if updateErr := s.postRepo.UpdateModerationStatus(postID, model.PostStatusPublished, "", "system"); updateErr != nil {
log.Printf("[WARN] Failed to publish post %s after moderation error: %v", postID, updateErr)
} else {
s.invalidatePostCaches(postID)
}
log.Printf("[WARN] Post moderation failed, fallback publish post=%s err=%v", postID, err)
return
@@ -104,6 +110,7 @@ func (s *PostService) reviewPostAsync(postID, userID, title, content string, ima
log.Printf("[WARN] Failed to publish post %s: %v", postID, err)
return
}
s.invalidatePostCaches(postID)
if s.gorseClient.IsEnabled() {
post, getErr := s.postRepo.GetByID(postID)
@@ -120,6 +127,11 @@ func (s *PostService) reviewPostAsync(postID, userID, title, content string, ima
}
}
func (s *PostService) invalidatePostCaches(postID string) {
cache.InvalidatePostDetail(s.cache, postID)
cache.InvalidatePostList(s.cache)
}
func (s *PostService) notifyModerationRejected(userID, reason string) {
if s.systemMessageService == nil || strings.TrimSpace(userID) == "" {
return
@@ -149,7 +161,12 @@ func (s *PostService) GetByID(ctx context.Context, id string) (*model.Post, erro
// Update 更新帖子
func (s *PostService) Update(ctx context.Context, post *model.Post) error {
err := s.postRepo.Update(post)
return s.UpdateWithImages(ctx, post, nil)
}
// UpdateWithImages 更新帖子并可选更新图片images=nil 表示不更新图片)
func (s *PostService) UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error {
err := s.postRepo.UpdateWithImages(post, images)
if err != nil {
return err
}
@@ -185,7 +202,7 @@ func (s *PostService) Delete(ctx context.Context, id string) error {
}
// List 获取帖子列表(带缓存)
func (s *PostService) List(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
func (s *PostService) List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) {
cacheSettings := cache.GetSettings()
postListTTL := cacheSettings.PostListTTL
if postListTTL <= 0 {
@@ -200,8 +217,12 @@ func (s *PostService) List(ctx context.Context, page, pageSize int, userID strin
jitter = PostListJitterRatio
}
// 生成缓存键(包含 userID 维度,避免过滤查询与全量查询互相污染
cacheKey := cache.PostListKey("latest", userID, page, pageSize)
// 生成缓存键(包含 userID 维度与可见性维度,避免作者视角污染公开视角
visibilityUserKey := userID
if includePending && userID != "" {
visibilityUserKey = "owner:" + userID
}
cacheKey := cache.PostListKey("latest", visibilityUserKey, page, pageSize)
result, err := cache.GetOrLoadTyped[*PostListResult](
s.cache,
@@ -210,7 +231,7 @@ func (s *PostService) List(ctx context.Context, page, pageSize int, userID strin
jitter,
nullTTL,
func() (*PostListResult, error) {
posts, total, err := s.postRepo.List(page, pageSize, userID)
posts, total, err := s.postRepo.List(page, pageSize, userID, includePending)
if err != nil {
return nil, err
}
@@ -234,7 +255,7 @@ func (s *PostService) List(ctx context.Context, page, pageSize int, userID strin
}
}
if missingAuthor {
posts, total, loadErr := s.postRepo.List(page, pageSize, userID)
posts, total, loadErr := s.postRepo.List(page, pageSize, userID, includePending)
if loadErr != nil {
return nil, 0, loadErr
}
@@ -247,12 +268,17 @@ func (s *PostService) List(ctx context.Context, page, pageSize int, userID strin
// GetLatestPosts 获取最新帖子(语义化别名)
func (s *PostService) GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
return s.List(ctx, page, pageSize, userID)
return s.List(ctx, page, pageSize, userID, false)
}
// GetLatestPostsForOwner 获取作者视角帖子列表(包含待审核)
func (s *PostService) GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
return s.List(ctx, page, pageSize, userID, true)
}
// GetUserPosts 获取用户帖子
func (s *PostService) GetUserPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
return s.postRepo.GetUserPosts(userID, page, pageSize)
func (s *PostService) GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error) {
return s.postRepo.GetUserPosts(userID, page, pageSize, includePending)
}
// Like 点赞

View File

@@ -8,7 +8,7 @@ import (
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/websocket"
"carrot_bbs/internal/pkg/sse"
"carrot_bbs/internal/repository"
)
@@ -42,8 +42,6 @@ type PushService interface {
// 系统消息推送
PushSystemMessage(ctx context.Context, userID string, msgType, title, content string, data map[string]interface{}) error
PushNotification(ctx context.Context, userID string, notification *websocket.NotificationMessage) error
PushAnnouncement(ctx context.Context, announcement *websocket.AnnouncementMessage) error
// 系统通知推送(新接口,使用独立的 SystemNotification 模型)
PushSystemNotification(ctx context.Context, userID string, notification *model.SystemNotification) error
@@ -67,7 +65,7 @@ type pushServiceImpl struct {
pushRepo *repository.PushRecordRepository
deviceRepo *repository.DeviceTokenRepository
messageRepo *repository.MessageRepository
wsManager *websocket.WebSocketManager
sseHub *sse.Hub
// 推送队列
pushQueue chan *pushTask
@@ -86,13 +84,13 @@ func NewPushService(
pushRepo *repository.PushRecordRepository,
deviceRepo *repository.DeviceTokenRepository,
messageRepo *repository.MessageRepository,
wsManager *websocket.WebSocketManager,
sseHub *sse.Hub,
) PushService {
return &pushServiceImpl{
pushRepo: pushRepo,
deviceRepo: deviceRepo,
messageRepo: messageRepo,
wsManager: wsManager,
sseHub: sseHub,
pushQueue: make(chan *pushTask, PushQueueSize),
stopChan: make(chan struct{}),
}
@@ -140,11 +138,7 @@ func (s *pushServiceImpl) PushToUser(ctx context.Context, userID string, message
// pushViaWebSocket 通过WebSocket推送消息
// 返回true表示推送成功false表示用户不在线
func (s *pushServiceImpl) pushViaWebSocket(ctx context.Context, userID string, message *model.Message) bool {
if s.wsManager == nil {
return false
}
if !s.wsManager.IsUserOnline(userID) {
if s.sseHub == nil || !s.sseHub.HasSubscribers(userID) {
return false
}
@@ -154,36 +148,33 @@ func (s *pushServiceImpl) pushViaWebSocket(ctx context.Context, userID string, m
// 从 segments 中提取文本内容
content := dto.ExtractTextContentFromModel(message.Segments)
notification := &websocket.NotificationMessage{
ID: fmt.Sprintf("%s", message.ID),
Type: string(message.SystemType),
Content: content,
Extra: make(map[string]interface{}),
CreatedAt: message.CreatedAt.UnixMilli(),
notification := map[string]interface{}{
"id": fmt.Sprintf("%s", message.ID),
"type": string(message.SystemType),
"content": content,
"extra": map[string]interface{}{},
"created_at": message.CreatedAt.UnixMilli(),
}
// 填充额外数据
if message.ExtraData != nil {
notification.Extra["actor_id"] = message.ExtraData.ActorID
notification.Extra["actor_name"] = message.ExtraData.ActorName
notification.Extra["avatar_url"] = message.ExtraData.AvatarURL
notification.Extra["target_id"] = message.ExtraData.TargetID
notification.Extra["target_type"] = message.ExtraData.TargetType
notification.Extra["action_url"] = message.ExtraData.ActionURL
notification.Extra["action_time"] = message.ExtraData.ActionTime
// 设置触发用户信息
extra := notification["extra"].(map[string]interface{})
extra["actor_id"] = message.ExtraData.ActorID
extra["actor_name"] = message.ExtraData.ActorName
extra["avatar_url"] = message.ExtraData.AvatarURL
extra["target_id"] = message.ExtraData.TargetID
extra["target_type"] = message.ExtraData.TargetType
extra["action_url"] = message.ExtraData.ActionURL
extra["action_time"] = message.ExtraData.ActionTime
if message.ExtraData.ActorID > 0 {
notification.TriggerUser = &websocket.NotificationUser{
ID: fmt.Sprintf("%d", message.ExtraData.ActorID),
Username: message.ExtraData.ActorName,
Avatar: message.ExtraData.AvatarURL,
notification["trigger_user"] = map[string]interface{}{
"id": fmt.Sprintf("%d", message.ExtraData.ActorID),
"username": message.ExtraData.ActorName,
"avatar": message.ExtraData.AvatarURL,
}
}
}
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeNotification, notification)
s.wsManager.SendToUser(userID, wsMsg)
s.sseHub.PublishToUser(userID, "system_notification", notification)
return true
}
@@ -208,8 +199,10 @@ func (s *pushServiceImpl) pushViaWebSocket(ctx context.Context, userID string, m
SenderID: message.SenderID,
}
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeMessage, event)
s.wsManager.SendToUser(userID, wsMsg)
s.sseHub.PublishToUser(userID, "chat_message", map[string]interface{}{
"detail_type": detailType,
"message": event,
})
return true
}
@@ -451,73 +444,21 @@ func (s *pushServiceImpl) PushSystemMessage(ctx context.Context, userID string,
// pushSystemViaWebSocket 通过WebSocket推送系统消息
func (s *pushServiceImpl) pushSystemViaWebSocket(ctx context.Context, userID string, msgType, title, content string, data map[string]interface{}) bool {
if s.wsManager == nil {
if s.sseHub == nil || !s.sseHub.HasSubscribers(userID) {
return false
}
if !s.wsManager.IsUserOnline(userID) {
return false
sysMsg := map[string]interface{}{
"type": msgType,
"title": title,
"content": content,
"data": data,
"created_at": time.Now().UnixMilli(),
}
sysMsg := &websocket.SystemMessage{
Type: msgType,
Title: title,
Content: content,
Data: data,
CreatedAt: time.Now().UnixMilli(),
}
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeSystem, sysMsg)
s.wsManager.SendToUser(userID, wsMsg)
s.sseHub.PublishToUser(userID, "system_notification", sysMsg)
return true
}
// PushNotification 推送通知消息
func (s *pushServiceImpl) PushNotification(ctx context.Context, userID string, notification *websocket.NotificationMessage) error {
// 首先尝试WebSocket推送
if s.pushNotificationViaWebSocket(ctx, userID, notification) {
return nil
}
// 用户不在线,创建待推送记录
// 通知消息可以等用户上线后拉取
return errors.New("user is offline, notification will be available on next sync")
}
// pushNotificationViaWebSocket 通过WebSocket推送通知消息
func (s *pushServiceImpl) pushNotificationViaWebSocket(ctx context.Context, userID string, notification *websocket.NotificationMessage) bool {
if s.wsManager == nil {
return false
}
if !s.wsManager.IsUserOnline(userID) {
return false
}
if notification.CreatedAt == 0 {
notification.CreatedAt = time.Now().UnixMilli()
}
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeNotification, notification)
s.wsManager.SendToUser(userID, wsMsg)
return true
}
// PushAnnouncement 广播公告消息
func (s *pushServiceImpl) PushAnnouncement(ctx context.Context, 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
}
// PushSystemNotification 推送系统通知(使用独立的 SystemNotification 模型)
func (s *pushServiceImpl) PushSystemNotification(ctx context.Context, userID string, notification *model.SystemNotification) error {
// 首先尝试WebSocket推送
@@ -531,45 +472,40 @@ func (s *pushServiceImpl) PushSystemNotification(ctx context.Context, userID str
// pushSystemNotificationViaWebSocket 通过WebSocket推送系统通知
func (s *pushServiceImpl) pushSystemNotificationViaWebSocket(ctx context.Context, userID string, notification *model.SystemNotification) bool {
if s.wsManager == nil {
if s.sseHub == nil || !s.sseHub.HasSubscribers(userID) {
return false
}
if !s.wsManager.IsUserOnline(userID) {
return false
}
// 构建 WebSocket 通知消息
wsNotification := &websocket.NotificationMessage{
ID: fmt.Sprintf("%d", notification.ID),
Type: string(notification.Type),
Title: notification.Title,
Content: notification.Content,
Extra: make(map[string]interface{}),
CreatedAt: notification.CreatedAt.UnixMilli(),
sseNotification := map[string]interface{}{
"id": fmt.Sprintf("%d", notification.ID),
"type": string(notification.Type),
"title": notification.Title,
"content": notification.Content,
"extra": map[string]interface{}{},
"created_at": notification.CreatedAt.UnixMilli(),
}
// 填充额外数据
if notification.ExtraData != nil {
wsNotification.Extra["actor_id_str"] = notification.ExtraData.ActorIDStr
wsNotification.Extra["actor_name"] = notification.ExtraData.ActorName
wsNotification.Extra["avatar_url"] = notification.ExtraData.AvatarURL
wsNotification.Extra["target_id"] = notification.ExtraData.TargetID
wsNotification.Extra["target_type"] = notification.ExtraData.TargetType
wsNotification.Extra["action_url"] = notification.ExtraData.ActionURL
wsNotification.Extra["action_time"] = notification.ExtraData.ActionTime
extra := sseNotification["extra"].(map[string]interface{})
extra["actor_id_str"] = notification.ExtraData.ActorIDStr
extra["actor_name"] = notification.ExtraData.ActorName
extra["avatar_url"] = notification.ExtraData.AvatarURL
extra["target_id"] = notification.ExtraData.TargetID
extra["target_type"] = notification.ExtraData.TargetType
extra["action_url"] = notification.ExtraData.ActionURL
extra["action_time"] = notification.ExtraData.ActionTime
// 设置触发用户信息
if notification.ExtraData.ActorIDStr != "" {
wsNotification.TriggerUser = &websocket.NotificationUser{
ID: notification.ExtraData.ActorIDStr,
Username: notification.ExtraData.ActorName,
Avatar: notification.ExtraData.AvatarURL,
sseNotification["trigger_user"] = map[string]interface{}{
"id": notification.ExtraData.ActorIDStr,
"username": notification.ExtraData.ActorName,
"avatar": notification.ExtraData.AvatarURL,
}
}
}
wsMsg := websocket.CreateWSMessage(websocket.MessageTypeNotification, wsNotification)
s.wsManager.SendToUser(userID, wsMsg)
s.sseHub.PublishToUser(userID, "system_notification", sseNotification)
return true
}