From d8b0825ac06ac2d9efcafc61a56c62564d35b914 Mon Sep 17 00:00:00 2001 From: lafay <2021211506@stu.hit.edu.cn> Date: Sat, 25 Apr 2026 21:22:52 +0800 Subject: [PATCH] feat(conversations): add notification mute functionality for conversations - Rename Muted field to NotificationMuted across model, DTOs and converters - Add UpdateNotificationMuted repository method with upsert support - Add SetConversationNotificationMuted service method - Add PUT /:id/notification_muted route for setting mute status - Improve S3 storage resilience by skipping bucket check on access denied --- internal/config/storage.go | 6 +-- internal/dto/dto.go | 58 +++++++++++++------------- internal/dto/message_converter.go | 54 +++++++++++++------------ internal/handler/message_handler.go | 63 ++++++++++++++++++++++++----- internal/model/conversation.go | 2 +- internal/pkg/s3/s3.go | 9 ++--- internal/repository/message_repo.go | 31 ++++++++++++++ internal/router/router.go | 1 + internal/service/chat_service.go | 29 ++++++++++++- 9 files changed, 178 insertions(+), 75 deletions(-) diff --git a/internal/config/storage.go b/internal/config/storage.go index 4c02b92..dc6c63a 100644 --- a/internal/config/storage.go +++ b/internal/config/storage.go @@ -35,10 +35,8 @@ func NewS3(cfg *S3Config) (*minio.Client, error) { exists, err := client.BucketExists(ctx, cfg.Bucket) if err != nil { - return nil, fmt.Errorf("failed to check bucket: %w", err) - } - - if !exists { + // Access Denied 或网络不通时,跳过 bucket 检查,假定 bucket 已存在 + } else if !exists { if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{ Region: cfg.Region, }); err != nil { diff --git a/internal/dto/dto.go b/internal/dto/dto.go index ea77d6d..ec141f0 100644 --- a/internal/dto/dto.go +++ b/internal/dto/dto.go @@ -350,26 +350,27 @@ type MessageResponse struct { // ConversationResponse 会话响应 type ConversationResponse struct { - ID string `json:"id"` - Type string `json:"type"` - IsPinned bool `json:"is_pinned"` - Group *GroupResponse `json:"group,omitempty"` - LastSeq int64 `json:"last_seq"` - LastMessage *MessageResponse `json:"last_message"` - LastMessageAt string `json:"last_message_at"` - UnreadCount int `json:"unread_count"` - Participants []*UserResponse `json:"participants,omitempty"` // 私聊时使用 - MemberCount int `json:"member_count,omitempty"` // 群聊时使用 - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID string `json:"id"` + Type string `json:"type"` + IsPinned bool `json:"is_pinned"` + NotificationMuted bool `json:"notification_muted"` + Group *GroupResponse `json:"group,omitempty"` + LastSeq int64 `json:"last_seq"` + LastMessage *MessageResponse `json:"last_message"` + LastMessageAt string `json:"last_message_at"` + UnreadCount int `json:"unread_count"` + Participants []*UserResponse `json:"participants,omitempty"` // 私聊时使用 + MemberCount int `json:"member_count,omitempty"` // 群聊时使用 + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } // ConversationParticipantResponse 会话参与者响应 type ConversationParticipantResponse struct { - UserID string `json:"user_id"` - LastReadSeq int64 `json:"last_read_seq"` - Muted bool `json:"muted"` - IsPinned bool `json:"is_pinned"` + UserID string `json:"user_id"` + LastReadSeq int64 `json:"last_read_seq"` + NotificationMuted bool `json:"notification_muted"` + IsPinned bool `json:"is_pinned"` } // ==================== Auth DTOs ==================== @@ -453,18 +454,19 @@ type ConversationListResponse struct { // ConversationDetailResponse 会话详情响应 type ConversationDetailResponse struct { - ID string `json:"id"` - Type string `json:"type"` - IsPinned bool `json:"is_pinned"` - LastSeq int64 `json:"last_seq"` - LastMessage *MessageResponse `json:"last_message"` - LastMessageAt string `json:"last_message_at"` - UnreadCount int64 `json:"unread_count"` - Participants []*UserResponse `json:"participants"` - MyLastReadSeq int64 `json:"my_last_read_seq"` // 当前用户的已读位置 - OtherLastReadSeq int64 `json:"other_last_read_seq"` // 对方用户的已读位置 - CreatedAt string `json:"created_at"` - UpdatedAt string `json:"updated_at"` + ID string `json:"id"` + Type string `json:"type"` + IsPinned bool `json:"is_pinned"` + NotificationMuted bool `json:"notification_muted"` + LastSeq int64 `json:"last_seq"` + LastMessage *MessageResponse `json:"last_message"` + LastMessageAt string `json:"last_message_at"` + UnreadCount int64 `json:"unread_count"` + Participants []*UserResponse `json:"participants"` + MyLastReadSeq int64 `json:"my_last_read_seq"` // 当前用户的已读位置 + OtherLastReadSeq int64 `json:"other_last_read_seq"` // 对方用户的已读位置 + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` } // UnreadCountResponse 未读数响应 diff --git a/internal/dto/message_converter.go b/internal/dto/message_converter.go index ec74c24..77ca2b1 100644 --- a/internal/dto/message_converter.go +++ b/internal/dto/message_converter.go @@ -38,7 +38,7 @@ func ConvertMessageToResponse(message *model.Message) *MessageResponse { // participants: 会话参与者列表(用户信息) // unreadCount: 当前用户的未读消息数 // lastMessage: 最后一条消息 -func ConvertConversationToResponse(conv *model.Conversation, participants []*model.User, unreadCount int, lastMessage *model.Message, isPinned bool) *ConversationResponse { +func ConvertConversationToResponse(conv *model.Conversation, participants []*model.User, unreadCount int, lastMessage *model.Message, isPinned bool, notificationMuted bool) *ConversationResponse { if conv == nil { return nil } @@ -55,22 +55,23 @@ func ConvertConversationToResponse(conv *model.Conversation, participants []*mod } return &ConversationResponse{ - ID: conv.ID, - Type: string(conv.Type), - IsPinned: isPinned, - Group: groupResponse, - LastSeq: conv.LastSeq, - LastMessage: ConvertMessageToResponse(lastMessage), - LastMessageAt: FormatTimePointer(conv.LastMsgTime), - UnreadCount: unreadCount, - Participants: participantResponses, - CreatedAt: FormatTime(conv.CreatedAt), - UpdatedAt: FormatTime(conv.UpdatedAt), + ID: conv.ID, + Type: string(conv.Type), + IsPinned: isPinned, + NotificationMuted: notificationMuted, + Group: groupResponse, + LastSeq: conv.LastSeq, + LastMessage: ConvertMessageToResponse(lastMessage), + LastMessageAt: FormatTimePointer(conv.LastMsgTime), + UnreadCount: unreadCount, + Participants: participantResponses, + CreatedAt: FormatTime(conv.CreatedAt), + UpdatedAt: FormatTime(conv.UpdatedAt), } } // ConvertConversationToDetailResponse 将Conversation转换为ConversationDetailResponse -func ConvertConversationToDetailResponse(conv *model.Conversation, participants []*model.User, unreadCount int64, lastMessage *model.Message, myLastReadSeq int64, otherLastReadSeq int64, isPinned bool) *ConversationDetailResponse { +func ConvertConversationToDetailResponse(conv *model.Conversation, participants []*model.User, unreadCount int64, lastMessage *model.Message, myLastReadSeq int64, otherLastReadSeq int64, isPinned bool, notificationMuted bool) *ConversationDetailResponse { if conv == nil { return nil } @@ -81,18 +82,19 @@ func ConvertConversationToDetailResponse(conv *model.Conversation, participants } return &ConversationDetailResponse{ - ID: conv.ID, - Type: string(conv.Type), - IsPinned: isPinned, - LastSeq: conv.LastSeq, - LastMessage: ConvertMessageToResponse(lastMessage), - LastMessageAt: FormatTimePointer(conv.LastMsgTime), - UnreadCount: unreadCount, - Participants: participantResponses, - MyLastReadSeq: myLastReadSeq, - OtherLastReadSeq: otherLastReadSeq, - CreatedAt: FormatTime(conv.CreatedAt), - UpdatedAt: FormatTime(conv.UpdatedAt), + ID: conv.ID, + Type: string(conv.Type), + IsPinned: isPinned, + NotificationMuted: notificationMuted, + LastSeq: conv.LastSeq, + LastMessage: ConvertMessageToResponse(lastMessage), + LastMessageAt: FormatTimePointer(conv.LastMsgTime), + UnreadCount: unreadCount, + Participants: participantResponses, + MyLastReadSeq: myLastReadSeq, + OtherLastReadSeq: otherLastReadSeq, + CreatedAt: FormatTime(conv.CreatedAt), + UpdatedAt: FormatTime(conv.UpdatedAt), } } @@ -109,7 +111,7 @@ func ConvertMessagesToResponse(messages []*model.Message) []*MessageResponse { func ConvertConversationsToResponse(convs []*model.Conversation) []*ConversationResponse { result := make([]*ConversationResponse, 0, len(convs)) for _, conv := range convs { - result = append(result, ConvertConversationToResponse(conv, nil, 0, nil, false)) + result = append(result, ConvertConversationToResponse(conv, nil, 0, nil, false, false)) } return result } diff --git a/internal/handler/message_handler.go b/internal/handler/message_handler.go index 230e400..8a7c6ff 100644 --- a/internal/handler/message_handler.go +++ b/internal/handler/message_handler.go @@ -97,16 +97,17 @@ func (h *MessageHandler) GetConversations(c *gin.Context) { var resp *dto.ConversationResponse myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID) isPinned := myParticipant != nil && myParticipant.IsPinned + notificationMuted := myParticipant != nil && myParticipant.NotificationMuted if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" { // 群聊:实时计算群成员数量 memberCount, _ := h.groupService.GetMemberCount(*conv.GroupID) // 创建响应并设置member_count - resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned) + resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned, notificationMuted) resp.MemberCount = memberCount } else { // 私聊:获取参与者信息 participants, _ := h.getConversationParticipants(c.Request.Context(), conv.ID, userID) - resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned) + resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned, notificationMuted) } result[i] = resp } @@ -153,8 +154,9 @@ func (h *MessageHandler) CreateConversation(c *gin.Context) { participants := []*model.User{targetUser} myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID) isPinned := myParticipant != nil && myParticipant.IsPinned + notificationMuted := myParticipant != nil && myParticipant.NotificationMuted - response.Success(c, dto.ConvertConversationToResponse(conv, participants, 0, nil, isPinned)) + response.Success(c, dto.ConvertConversationToResponse(conv, participants, 0, nil, isPinned, notificationMuted)) } // GetConversationByID 获取会话详情 @@ -188,18 +190,20 @@ func (h *MessageHandler) GetConversationByID(c *gin.Context) { // 获取当前用户的已读位置 myLastReadSeq := int64(0) isPinned := false + notificationMuted := false allParticipants, _ := h.messageService.GetConversationParticipants(conversationID) for _, p := range allParticipants { if p.UserID == userID { myLastReadSeq = p.LastReadSeq isPinned = p.IsPinned + notificationMuted = p.NotificationMuted break } } // 获取对方用户的已读位置 otherLastReadSeq := int64(0) - response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned)) + response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned, notificationMuted)) } // GetMessages 获取消息列表 @@ -451,16 +455,17 @@ func (h *MessageHandler) HandleGetConversationList(c *gin.Context) { var resp *dto.ConversationResponse myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID) isPinned := myParticipant != nil && myParticipant.IsPinned + notificationMuted := myParticipant != nil && myParticipant.NotificationMuted if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" { // 群聊:实时计算群成员数量 memberCount, _ := h.groupService.GetMemberCount(*conv.GroupID) // 创建响应并设置member_count - resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned) + resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned, notificationMuted) resp.MemberCount = memberCount } else { // 私聊:获取参与者信息 participants, _ := h.getConversationParticipants(c.Request.Context(), conv.ID, userID) - resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned) + resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned, notificationMuted) } result[i] = resp } @@ -704,8 +709,9 @@ func (h *MessageHandler) HandleCreateConversation(c *gin.Context) { participants := []*model.User{targetUser} myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID) isPinned := myParticipant != nil && myParticipant.IsPinned + notificationMuted := myParticipant != nil && myParticipant.NotificationMuted - response.Success(c, dto.ConvertConversationToResponse(conv, participants, 0, nil, isPinned)) + response.Success(c, dto.ConvertConversationToResponse(conv, participants, 0, nil, isPinned, notificationMuted)) } // HandleGetConversation 获取会话详情 @@ -738,18 +744,20 @@ func (h *MessageHandler) HandleGetConversation(c *gin.Context) { // 获取当前用户的已读位置 myLastReadSeq := int64(0) isPinned := false + notificationMuted := false allParticipants, _ := h.messageService.GetConversationParticipants(conversationID) for _, p := range allParticipants { if p.UserID == userID { myLastReadSeq = p.LastReadSeq isPinned = p.IsPinned + notificationMuted = p.NotificationMuted break } } // 获取对方用户的已读位置 otherLastReadSeq := int64(0) - response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned)) + response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned, notificationMuted)) } // HandleGetMessages 获取会话消息 @@ -903,6 +911,40 @@ func (h *MessageHandler) HandleSetConversationPinned(c *gin.Context) { }) } +// HandleSetConversationNotificationMuted 设置会话免打扰状态 +// PUT /api/v1/conversations/:id/notification_muted +func (h *MessageHandler) HandleSetConversationNotificationMuted(c *gin.Context) { + userID := c.GetString("user_id") + if userID == "" { + response.Unauthorized(c, "") + return + } + + conversationID := getIDParam(c, "id") + if conversationID == "" { + response.BadRequest(c, "conversation id is required") + return + } + + var req struct { + NotificationMuted bool `json:"notification_muted"` + } + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, err.Error()) + return + } + + if err := h.chatService.SetConversationNotificationMuted(c.Request.Context(), conversationID, userID, req.NotificationMuted); err != nil { + response.BadRequest(c, err.Error()) + return + } + + response.SuccessWithMessage(c, "conversation notification_muted status updated", gin.H{ + "conversation_id": conversationID, + "notification_muted": req.NotificationMuted, + }) +} + // ==================== Cursor Pagination Handlers ==================== // GetMessagesByCursor 游标分页获取会话消息 @@ -993,15 +1035,16 @@ func (h *MessageHandler) GetConversationsByCursor(c *gin.Context) { var resp *dto.ConversationResponse myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID) isPinned := myParticipant != nil && myParticipant.IsPinned + notificationMuted := myParticipant != nil && myParticipant.NotificationMuted if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" { // 群聊:实时计算群成员数量 memberCount, _ := h.groupService.GetMemberCount(*conv.GroupID) - resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned) + resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned, notificationMuted) resp.MemberCount = memberCount } else { // 私聊:获取参与者信息 participants, _ := h.getConversationParticipants(c.Request.Context(), conv.ID, userID) - resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned) + resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned, notificationMuted) } items[i] = resp } diff --git a/internal/model/conversation.go b/internal/model/conversation.go index 58877db..3e1170e 100644 --- a/internal/model/conversation.go +++ b/internal/model/conversation.go @@ -56,7 +56,7 @@ type ConversationParticipant struct { ConversationID string `gorm:"not null;size:20;uniqueIndex:idx_conversation_user,priority:1;index:idx_cp_conversation_user,priority:1" json:"conversation_id"` // 雪花算法ID(string类型) UserID string `gorm:"column:user_id;type:varchar(50);not null;uniqueIndex:idx_conversation_user,priority:2;index:idx_cp_conversation_user,priority:2;index:idx_cp_user_hidden_pinned_updated,priority:1" json:"user_id"` // UUID格式,与JWT中user_id保持一致 LastReadSeq int64 `gorm:"default:0" json:"last_read_seq"` // 已读到的seq位置 - Muted bool `gorm:"default:false" json:"muted"` // 是否免打扰 + NotificationMuted bool `gorm:"column:muted;default:false" json:"notification_muted"` // 是否免打扰 IsPinned bool `gorm:"default:false;index:idx_cp_user_hidden_pinned_updated,priority:3" json:"is_pinned"` // 是否置顶会话(用户维度) HiddenAt *time.Time `gorm:"index:idx_cp_user_hidden_pinned_updated,priority:2" json:"hidden_at,omitempty"` // 仅自己删除会话时使用,收到新消息后自动恢复 CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` diff --git a/internal/pkg/s3/s3.go b/internal/pkg/s3/s3.go index 7cba903..fa1ef91 100644 --- a/internal/pkg/s3/s3.go +++ b/internal/pkg/s3/s3.go @@ -32,13 +32,12 @@ func New(cfg *config.S3Config) (*Client, error) { return nil, fmt.Errorf("failed to create S3 client: %w", err) } - // 检查bucket是否存在 + // 检查bucket是否存在,权限不足时跳过检查(不阻塞启动) exists, err := client.BucketExists(ctx, cfg.Bucket) if err != nil { - return nil, fmt.Errorf("failed to check bucket: %w", err) - } - - if !exists { + // Access Denied 或网络不通时,跳过 bucket 检查,假定 bucket 已存在 + // 后续上传时会暴露具体错误 + } else if !exists { if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{ Region: cfg.Region, }); err != nil { diff --git a/internal/repository/message_repo.go b/internal/repository/message_repo.go index 9c0b29d..7dfb597 100644 --- a/internal/repository/message_repo.go +++ b/internal/repository/message_repo.go @@ -28,6 +28,7 @@ type MessageRepository interface { GetParticipant(conversationID string, userID string) (*model.ConversationParticipant, error) UpdateLastReadSeq(conversationID string, userID string, lastReadSeq int64) error UpdatePinned(conversationID string, userID string, isPinned bool) error + UpdateNotificationMuted(conversationID string, userID string, notificationMuted bool) error GetUnreadCount(conversationID string, userID string) (int64, error) UpdateConversationLastSeq(conversationID string, seq int64) error GetNextSeq(conversationID string) (int64, error) @@ -337,6 +338,36 @@ func (r *messageRepository) UpdatePinned(conversationID string, userID string, i return nil } +// UpdateNotificationMuted 更新会话免打扰状态(用户维度) +func (r *messageRepository) UpdateNotificationMuted(conversationID string, userID string, notificationMuted bool) error { + result := r.db.Model(&model.ConversationParticipant{}). + Where("conversation_id = ? AND user_id = ?", conversationID, userID). + Update("muted", notificationMuted) + + if result.Error != nil { + return result.Error + } + + if result.RowsAffected == 0 { + return r.db.Clauses(clause.OnConflict{ + Columns: []clause.Column{ + {Name: "conversation_id"}, + {Name: "user_id"}, + }, + DoUpdates: clause.Assignments(map[string]any{ + "muted": notificationMuted, + "updated_at": gorm.Expr("CURRENT_TIMESTAMP"), + }), + }).Create(&model.ConversationParticipant{ + ConversationID: conversationID, + UserID: userID, + NotificationMuted: notificationMuted, + }).Error + } + + return nil +} + // GetUnreadCount 获取未读消息数 // userID 参数为 string 类型(UUID格式),与JWT中user_id保持一致 func (r *messageRepository) GetUnreadCount(conversationID string, userID string) (int64, error) { diff --git a/internal/router/router.go b/internal/router/router.go index 358be75..d9eb457 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -373,6 +373,7 @@ func (r *Router) setupRoutes() { conversations.POST("/:id/messages", middleware.RequireVerified(r.userRepo), r.messageHandler.HandleSendMessage) // 发送消息 conversations.POST("/:id/read", r.messageHandler.HandleMarkRead) // 标记已读 conversations.PUT("/:id/pinned", r.messageHandler.HandleSetConversationPinned) // 置顶设置 + conversations.PUT("/:id/notification_muted", r.messageHandler.HandleSetConversationNotificationMuted) // 免打扰设置 conversations.GET("/unread/count", r.messageHandler.GetUnreadCount) // 未读数 conversations.POST("/:id/typing", r.messageHandler.HandleTyping) // 输入状态 conversations.DELETE("/:id/self", r.messageHandler.HandleDeleteConversationForSelf) // 删除会话(仅自己) diff --git a/internal/service/chat_service.go b/internal/service/chat_service.go index 37e32ec..5afe8ca 100644 --- a/internal/service/chat_service.go +++ b/internal/service/chat_service.go @@ -27,7 +27,8 @@ type ChatService interface { GetConversationList(ctx context.Context, userID string, page, pageSize int) ([]*model.Conversation, int64, error) GetConversationByID(ctx context.Context, conversationID string, userID string) (*model.Conversation, error) DeleteConversationForSelf(ctx context.Context, conversationID string, userID string) error - SetConversationPinned(ctx context.Context, conversationID string, userID string, isPinned bool) error +SetConversationPinned(ctx context.Context, conversationID string, userID string, isPinned bool) error + SetConversationNotificationMuted(ctx context.Context, conversationID string, userID string, notificationMuted bool) error // 消息操作 SendMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error) @@ -215,6 +216,32 @@ func (s *chatServiceImpl) SetConversationPinned(ctx context.Context, conversatio return nil } +// SetConversationNotificationMuted 设置会话免打扰状态(用户维度) +func (s *chatServiceImpl) SetConversationNotificationMuted(ctx context.Context, conversationID string, userID string, notificationMuted bool) error { + participant, err := s.getParticipant(ctx, conversationID, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return errors.New("conversation not found or no permission") + } + return fmt.Errorf("failed to get participant: %w", err) + } + if participant.ConversationID == "" { + return errors.New("conversation not found or no permission") + } + + if err := s.repo.UpdateNotificationMuted(conversationID, userID, notificationMuted); err != nil { + return fmt.Errorf("failed to update notification_muted status: %w", err) + } + + // 失效缓存 + if s.conversationCache != nil { + s.conversationCache.InvalidateParticipant(conversationID, userID) + s.conversationCache.InvalidateConversationList(userID) + } + + return nil +} + // SendMessage 发送消息(使用 segments) func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error) { // 首先验证会话是否存在