package handler import ( "context" "strconv" "github.com/gin-gonic/gin" "with_you/internal/dto" "with_you/internal/model" "with_you/internal/pkg/cursor" "with_you/internal/pkg/response" "with_you/internal/pkg/ws" "with_you/internal/service" ) // enrichConversations 批量填充会话列表响应数据(解决 N+1 问题) func (h *MessageHandler) enrichConversations(ctx context.Context, convs []*model.Conversation, userID string) []*dto.ConversationResponse { if len(convs) == 0 { return nil } convIDs := make([]string, len(convs)) for i, c := range convs { convIDs[i] = c.ID } // 批量查询:4-5 次查询替代 N*5 次 unreadCounts, _ := h.chatService.GetUnreadCountBatch(ctx, userID, convIDs) lastMessages, _ := h.chatService.GetLastMessagesBatch(ctx, convIDs) myParticipants, _ := h.messageService.GetMyParticipantsBatch(ctx, convIDs, userID) allParticipants, _ := h.messageService.GetParticipantsBatch(ctx, convIDs) // 收集需要额外查询的 ID var groupIDs []string var userIDs []string for _, conv := range convs { if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" { groupIDs = append(groupIDs, *conv.GroupID) } } for _, participants := range allParticipants { for _, p := range participants { if p.UserID != userID { userIDs = append(userIDs, p.UserID) } } } memberCounts, _ := h.groupService.GetMemberCountBatch(ctx, groupIDs) usersMap, _ := h.userService.GetUsersByIDs(ctx, userIDs) // 组装响应 result := make([]*dto.ConversationResponse, len(convs)) for i, conv := range convs { unreadCount := unreadCounts[conv.ID] lastMessage := lastMessages[conv.ID] myP := myParticipants[conv.ID] isPinned := myP != nil && myP.IsPinned notificationMuted := myP != nil && myP.NotificationMuted var resp *dto.ConversationResponse if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" { resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned, notificationMuted) if mc, ok := memberCounts[*conv.GroupID]; ok { resp.MemberCount = int(mc) } } else { participants := allParticipants[conv.ID] var users []*model.User for _, p := range participants { if p.UserID == userID { continue } if u, ok := usersMap[p.UserID]; ok { users = append(users, u) } } resp = dto.ConvertConversationToResponse(conv, users, int(unreadCount), lastMessage, isPinned, notificationMuted) } result[i] = resp } return result } // MessageHandler 消息处理器 type MessageHandler struct { chatService service.ChatService messageService *service.MessageService userService service.UserService groupService service.GroupService wsPublisher ws.MessagePublisher } // NewMessageHandler 创建消息处理器 func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService service.UserService, groupService service.GroupService, wsPublisher ws.MessagePublisher) *MessageHandler { return &MessageHandler{ chatService: chatService, messageService: messageService, userService: userService, groupService: groupService, wsPublisher: wsPublisher, } } // HandleTyping 输入状态上报 // POST /api/v1/conversations/typing func (h *MessageHandler) HandleTyping(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 } h.chatService.SendTyping(c.Request.Context(), userID, conversationID) response.SuccessWithMessage(c, "typing sent", nil) } // GetConversations 获取会话列表 // GET /api/conversations func (h *MessageHandler) GetConversations(c *gin.Context) { userID := c.GetString("user_id") // 添加调试日志 if userID == "" { response.Unauthorized(c, "") return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) convs, _, err := h.chatService.GetConversationList(c.Request.Context(), userID, page, pageSize) if err != nil { response.InternalServerError(c, "failed to get conversations") return } // 过滤掉系统会话(系统通知现在使用独立的表) filteredConvs := make([]*model.Conversation, 0) for _, conv := range convs { if conv.ID != model.SystemConversationID { filteredConvs = append(filteredConvs, conv) } } // 批量填充会话数据(解决 N+1 问题) result := h.enrichConversations(c.Request.Context(), filteredConvs, userID) // 更新 total 为过滤后的数量 response.Paginated(c, result, int64(len(filteredConvs)), page, pageSize) } // CreateConversation 创建私聊会话 // POST /api/conversations func (h *MessageHandler) CreateConversation(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } var req dto.CreateConversationRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } // 验证目标用户是否存在 targetUser, err := h.userService.GetUserByID(c.Request.Context(), req.UserID) if err != nil { response.BadRequest(c, "target user not found") return } // 不能和自己创建会话 if userID == req.UserID { response.BadRequest(c, "cannot create conversation with yourself") return } conv, err := h.chatService.GetOrCreateConversation(c.Request.Context(), userID, req.UserID) if err != nil { response.InternalServerError(c, "failed to create conversation") return } // 获取参与者信息 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, notificationMuted)) } // GetConversationByID 获取会话详情 // GET /api/conversations/:id func (h *MessageHandler) GetConversationByID(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } conversationIDStr := c.Param("id") conversationID, err := service.ParseConversationID(conversationIDStr) if err != nil { response.BadRequest(c, "invalid conversation id") return } conv, err := h.chatService.GetConversationByID(c.Request.Context(), conversationID, userID) if err != nil { response.BadRequest(c, err.Error()) return } // 获取未读数 unreadCount, _ := h.chatService.GetUnreadCount(c.Request.Context(), conversationID, userID) // 获取参与者信息 participants, _ := h.getConversationParticipants(c.Request.Context(), conversationID, userID) // 获取当前用户的已读位置 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) for _, p := range allParticipants { if p.UserID != userID { otherLastReadSeq = p.LastReadSeq break } } response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned, notificationMuted)) } // GetMessages 获取消息列表 // GET /api/conversations/:id/messages func (h *MessageHandler) GetMessages(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } conversationIDStr := c.Param("id") conversationID, err := service.ParseConversationID(conversationIDStr) if err != nil { response.BadRequest(c, "invalid conversation id") return } // 检查是否使用增量同步(after_seq参数) afterSeqStr := c.Query("after_seq") if afterSeqStr != "" { // 增量同步模式 afterSeq, err := strconv.ParseInt(afterSeqStr, 10, 64) if err != nil { response.BadRequest(c, "invalid after_seq") return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) messages, err := h.chatService.GetMessagesAfterSeq(c.Request.Context(), conversationID, userID, afterSeq, limit) if err != nil { response.BadRequest(c, err.Error()) return } // 转换为响应格式 result := dto.ConvertMessagesToResponse(messages) response.Success(c, &dto.MessageSyncResponse{ Messages: result, HasMore: len(messages) == limit, }) return } // 检查是否使用历史消息加载(before_seq参数) beforeSeqStr := c.Query("before_seq") if beforeSeqStr != "" { // 加载更早的消息(下拉加载更多) beforeSeq, err := strconv.ParseInt(beforeSeqStr, 10, 64) if err != nil { response.BadRequest(c, "invalid before_seq") return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) messages, err := h.chatService.GetMessagesBeforeSeq(c.Request.Context(), conversationID, userID, beforeSeq, limit) if err != nil { response.BadRequest(c, err.Error()) return } // 转换为响应格式 result := dto.ConvertMessagesToResponse(messages) response.Success(c, &dto.MessageSyncResponse{ Messages: result, HasMore: len(messages) == limit, }) return } // 分页模式 page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) messages, total, err := h.chatService.GetMessages(c.Request.Context(), conversationID, userID, page, pageSize) if err != nil { response.BadRequest(c, err.Error()) return } // 转换为响应格式 result := dto.ConvertMessagesToResponse(messages) response.Paginated(c, result, total, page, pageSize) } // SendMessage 发送消息 // POST /api/conversations/:id/messages func (h *MessageHandler) SendMessage(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } conversationIDStr := c.Param("id") conversationID, err := service.ParseConversationID(conversationIDStr) if err != nil { response.BadRequest(c, "invalid conversation id") return } var req dto.SendMessageRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } msg, err := h.chatService.SendMessage(c.Request.Context(), userID, conversationID, req.Segments, req.ReplyToID, req.ClientMsgID) if err != nil { response.BadRequest(c, err.Error()) return } response.Success(c, dto.ConvertMessageToResponse(msg)) } // HandleSendMessage RESTful 风格的发送消息端点 // POST /api/v1/conversations/:id/messages // 请求体格式: {"detail_type": "private", "segments": [{"type": "text", "data": {"text": "嗨~"}}]} func (h *MessageHandler) HandleSendMessage(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 params dto.SendMessageParams if err := c.ShouldBindJSON(¶ms); err != nil { response.BadRequest(c, err.Error()) return } // 验证参数 if params.DetailType == "" { response.BadRequest(c, "detail_type is required") return } if len(params.Segments) == 0 { response.BadRequest(c, "segments is required") return } // 发送消息 msg, err := h.chatService.SendMessage(c.Request.Context(), userID, conversationID, params.Segments, params.ReplyToID, params.ClientMsgID) if err != nil { response.BadRequest(c, err.Error()) return } // 构建 WSEventResponse 格式响应 wsResponse := dto.WSEventResponse{ ID: msg.ID, Time: msg.CreatedAt.UnixMilli(), Type: "message", DetailType: params.DetailType, ConversationID: conversationID, Seq: strconv.FormatInt(msg.Seq, 10), Segments: params.Segments, SenderID: userID, } response.Success(c, wsResponse) } // HandleDeleteMsg 撤回消息 // POST /api/v1/messages/delete_msg // 请求体格式: {"message_id": "xxx"} func (h *MessageHandler) HandleDeleteMsg(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } var params dto.DeleteMsgParams if err := c.ShouldBindJSON(¶ms); err != nil { response.BadRequest(c, err.Error()) return } // 验证参数 if params.MessageID == "" { response.BadRequest(c, "message_id is required") return } // 撤回消息 err := h.chatService.RecallMessage(c.Request.Context(), params.MessageID, userID) if err != nil { response.BadRequest(c, err.Error()) return } response.SuccessWithMessage(c, "消息已撤回", nil) } // HandleGetConversationList 获取会话列表 // GET /api/v1/conversations func (h *MessageHandler) HandleGetConversationList(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) convs, _, err := h.chatService.GetConversationList(c.Request.Context(), userID, page, pageSize) if err != nil { response.InternalServerError(c, "failed to get conversations") return } // 过滤掉系统会话(系统通知现在使用独立的表) filteredConvs := make([]*model.Conversation, 0) for _, conv := range convs { if conv.ID != model.SystemConversationID { filteredConvs = append(filteredConvs, conv) } } // 批量填充会话数据(解决 N+1 问题) result := h.enrichConversations(c.Request.Context(), filteredConvs, userID) response.Paginated(c, result, int64(len(filteredConvs)), page, pageSize) } // HandleDeleteConversationForSelf 仅自己删除会话 // DELETE /api/v1/conversations/:id/self func (h *MessageHandler) HandleDeleteConversationForSelf(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 } if err := h.chatService.DeleteConversationForSelf(c.Request.Context(), conversationID, userID); err != nil { response.BadRequest(c, err.Error()) return } response.SuccessWithMessage(c, "conversation deleted for self", nil) } // MarkAsRead 标记为已读 // POST /api/conversations/:id/read func (h *MessageHandler) MarkAsRead(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } conversationIDStr := c.Param("id") conversationID, err := service.ParseConversationID(conversationIDStr) if err != nil { response.BadRequest(c, "invalid conversation id") return } var req dto.MarkReadRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "last_read_seq is required") return } err = h.chatService.MarkAsRead(c.Request.Context(), conversationID, userID, req.LastReadSeq) if err != nil { response.BadRequest(c, err.Error()) return } response.SuccessWithMessage(c, "marked as read", nil) } // GetUnreadCount 获取未读消息总数 // GET /api/conversations/unread/count func (h *MessageHandler) GetUnreadCount(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } count, err := h.chatService.GetAllUnreadCount(c.Request.Context(), userID) if err != nil { response.InternalServerError(c, "failed to get unread count") return } response.Success(c, &dto.UnreadCountResponse{ TotalUnreadCount: count, }) } // HandleGetSyncData 获取同步元数据(轻量级 seq + 时间) // GET /api/v1/conversations/sync-data func (h *MessageHandler) HandleGetSyncData(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } items, err := h.chatService.GetSyncData(c.Request.Context(), userID) if err != nil { response.InternalServerError(c, "failed to get sync data") return } response.Success(c, &dto.SyncDataResponse{ Conversations: items, }) } // GetConversationUnreadCount 获取单个会话的未读数 // GET /api/conversations/:id/unread/count func (h *MessageHandler) GetConversationUnreadCount(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } conversationIDStr := c.Param("id") conversationID, err := service.ParseConversationID(conversationIDStr) if err != nil { response.BadRequest(c, "invalid conversation id") return } count, err := h.chatService.GetUnreadCount(c.Request.Context(), conversationID, userID) if err != nil { response.BadRequest(c, err.Error()) return } response.Success(c, &dto.ConversationUnreadCountResponse{ ConversationID: conversationID, UnreadCount: count, }) } // RecallMessage 撤回消息 // POST /api/messages/:id/recall func (h *MessageHandler) RecallMessage(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } messageIDStr := c.Param("id") // 直接使用 string 类型的 messageID err := h.chatService.RecallMessage(c.Request.Context(), messageIDStr, userID) if err != nil { response.BadRequest(c, err.Error()) return } response.SuccessWithMessage(c, "message recalled", nil) } // DeleteMessage 删除消息 // DELETE /api/messages/:id func (h *MessageHandler) DeleteMessage(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } messageIDStr := c.Param("id") // 直接使用 string 类型的 messageID err := h.chatService.DeleteMessage(c.Request.Context(), messageIDStr, userID) if err != nil { response.BadRequest(c, err.Error()) return } response.SuccessWithMessage(c, "message deleted", nil) } // 辅助函数:验证内容类型 func isValidContentType(contentType model.ContentType) bool { switch contentType { case model.ContentTypeText, model.ContentTypeImage, model.ContentTypeVideo, model.ContentTypeAudio, model.ContentTypeFile: return true default: return false } } // 辅助函数:获取会话参与者信息 func (h *MessageHandler) getConversationParticipants(ctx context.Context, conversationID string, currentUserID string) ([]*model.User, error) { // 从repository获取参与者列表 participants, err := h.messageService.GetConversationParticipants(conversationID) if err != nil { return nil, err } // 获取参与者用户信息 var users []*model.User for _, p := range participants { // 跳过当前用户 if p.UserID == currentUserID { continue } user, err := h.userService.GetUserByID(ctx, p.UserID) if err != nil { continue } users = append(users, user) } return users, nil } // 获取当前用户在会话中的参与者信息 func (h *MessageHandler) getMyConversationParticipant(conversationID string, userID string) (*model.ConversationParticipant, error) { participants, err := h.messageService.GetConversationParticipants(conversationID) if err != nil { return nil, err } for _, p := range participants { if p.UserID == userID { return p, nil } } return nil, nil } // getIDParam 从路径参数获取 ID func getIDParam(c *gin.Context, paramName string) string { return c.Param(paramName) } // ==================== RESTful Action 端点 ==================== // HandleCreateConversation 创建会话 // POST /api/v1/conversations/create func (h *MessageHandler) HandleCreateConversation(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } var params dto.CreateConversationParams if err := c.ShouldBindJSON(¶ms); err != nil { response.BadRequest(c, err.Error()) return } // 验证目标用户是否存在 targetUser, err := h.userService.GetUserByID(c.Request.Context(), params.UserID) if err != nil { response.BadRequest(c, "target user not found") return } // 不能和自己创建会话 if userID == params.UserID { response.BadRequest(c, "cannot create conversation with yourself") return } conv, err := h.chatService.GetOrCreateConversation(c.Request.Context(), userID, params.UserID) if err != nil { response.InternalServerError(c, "failed to create conversation") return } // 获取参与者信息 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, notificationMuted)) } // HandleGetConversation 获取会话详情 // GET /api/v1/conversations/:id func (h *MessageHandler) HandleGetConversation(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 } conv, err := h.chatService.GetConversationByID(c.Request.Context(), conversationID, userID) if err != nil { response.BadRequest(c, err.Error()) return } // 获取未读数 unreadCount, _ := h.chatService.GetUnreadCount(c.Request.Context(), conversationID, userID) // 获取参与者信息 participants, _ := h.getConversationParticipants(c.Request.Context(), conversationID, userID) // 获取当前用户的已读位置(优先从 Redis 缓存读取) 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 } } // 获取对方用户的已读位置(优先从 Redis 缓存读取) otherLastReadSeq := int64(0) for _, p := range allParticipants { if p.UserID != userID { otherLastReadSeq = p.LastReadSeq break } } response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned, notificationMuted)) } // HandleGetMessages 获取会话消息 // GET /api/v1/conversations/:id/messages func (h *MessageHandler) HandleGetMessages(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 } // 检查是否使用增量同步(after_seq参数) afterSeqStr := c.Query("after_seq") if afterSeqStr != "" { // 增量同步模式 afterSeq, err := strconv.ParseInt(afterSeqStr, 10, 64) if err != nil { response.BadRequest(c, "invalid after_seq") return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "100")) messages, err := h.chatService.GetMessagesAfterSeq(c.Request.Context(), conversationID, userID, afterSeq, limit) if err != nil { response.BadRequest(c, err.Error()) return } // 转换为响应格式 result := dto.ConvertMessagesToResponse(messages) response.Success(c, &dto.MessageSyncResponse{ Messages: result, HasMore: len(messages) == limit, }) return } // 检查是否使用历史消息加载(before_seq参数) beforeSeqStr := c.Query("before_seq") if beforeSeqStr != "" { // 加载更早的消息(下拉加载更多) beforeSeq, err := strconv.ParseInt(beforeSeqStr, 10, 64) if err != nil { response.BadRequest(c, "invalid before_seq") return } limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) messages, err := h.chatService.GetMessagesBeforeSeq(c.Request.Context(), conversationID, userID, beforeSeq, limit) if err != nil { response.BadRequest(c, err.Error()) return } // 转换为响应格式 result := dto.ConvertMessagesToResponse(messages) response.Success(c, &dto.MessageSyncResponse{ Messages: result, HasMore: len(messages) == limit, }) return } // 分页模式 page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) messages, total, err := h.chatService.GetMessages(c.Request.Context(), conversationID, userID, page, pageSize) if err != nil { response.BadRequest(c, err.Error()) return } // 转换为响应格式 result := dto.ConvertMessagesToResponse(messages) response.Paginated(c, result, total, page, pageSize) } // HandleMarkRead 标记已读 // POST /api/v1/conversations/:id/read func (h *MessageHandler) HandleMarkRead(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 dto.MarkReadRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } err := h.chatService.MarkAsRead(c.Request.Context(), conversationID, userID, req.LastReadSeq) if err != nil { response.BadRequest(c, err.Error()) return } response.SuccessWithMessage(c, "marked as read", nil) } // HandleMarkReadAll 批量标记所有会话已读 // POST /api/v1/conversations/read-all func (h *MessageHandler) HandleMarkReadAll(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } var req dto.BatchMarkReadRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } successCount, err := h.chatService.MarkAsReadBatch(c.Request.Context(), userID, req.Conversations) if err != nil { response.InternalServerError(c, "failed to mark all as read") return } response.Success(c, &dto.BatchMarkReadResponse{ SuccessCount: successCount, TotalCount: len(req.Conversations), }) } // HandleSetConversationPinned 设置会话置顶 // PUT /api/v1/conversations/:id/pinned func (h *MessageHandler) HandleSetConversationPinned(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 { IsPinned bool `json:"is_pinned"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } if err := h.chatService.SetConversationPinned(c.Request.Context(), conversationID, userID, req.IsPinned); err != nil { response.BadRequest(c, err.Error()) return } response.SuccessWithMessage(c, "conversation pinned status updated", gin.H{ "conversation_id": conversationID, "is_pinned": req.IsPinned, }) } // 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 游标分页获取会话消息 // GET /api/v1/conversations/:id/messages/cursor // 请求参数: // - cursor: 游标字符串(可选) // - direction: 分页方向 forward 或 backward(默认 forward) // - page_size: 每页数量(默认 20,最大 100) func (h *MessageHandler) GetMessagesByCursor(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 } // 解析游标分页参数 pageReq := parseCursorPageRequest(c) // 调用服务层 result, err := h.messageService.GetMessagesByCursor(c.Request.Context(), conversationID, pageReq) if err != nil { response.InternalServerError(c, "failed to get messages") return } // 转换为响应格式 items := dto.ConvertMessagesToResponse(result.Items) response.Success(c, &dto.MessageCursorPageResponse{ Items: items, NextCursor: result.NextCursor, PrevCursor: result.PrevCursor, HasMore: result.HasMore, }) } // GetConversationsByCursor 游标分页获取会话列表 // GET /api/v1/conversations/cursor // 请求参数: // - cursor: 游标字符串(可选) // - direction: 分页方向 forward 或 backward(默认 forward) // - page_size: 每页数量(默认 20,最大 100) func (h *MessageHandler) GetConversationsByCursor(c *gin.Context) { userID := c.GetString("user_id") if userID == "" { response.Unauthorized(c, "") return } // 解析游标分页参数 pageReq := parseCursorPageRequest(c) // 调用服务层 result, err := h.messageService.GetConversationsByCursor(c.Request.Context(), userID, pageReq) if err != nil { response.InternalServerError(c, "failed to get conversations") return } // 过滤掉系统会话 filteredItems := make([]*model.Conversation, 0) for _, conv := range result.Items { if conv.ID != model.SystemConversationID { filteredItems = append(filteredItems, conv) } } // 批量填充会话数据(解决 N+1 问题) items := h.enrichConversations(c.Request.Context(), filteredItems, userID) response.Success(c, &dto.ConversationCursorPageResponse{ Items: items, NextCursor: result.NextCursor, PrevCursor: result.PrevCursor, HasMore: result.HasMore, }) } // parseCursorPageRequest 解析游标分页请求参数 func parseCursorPageRequest(c *gin.Context) *cursor.PageRequest { cursorStr := c.Query("cursor") direction := cursor.Direction(c.DefaultQuery("direction", "forward")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) return cursor.NewPageRequest(cursorStr, direction, pageSize) }