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) {
|
||||
// 验证会话是否存在
|
||||
|
||||
@@ -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 点赞评论
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 点赞
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user