feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
All checks were successful
Build Backend / build (push) Successful in 4m42s
Build Backend / build-docker (push) Successful in 3m53s

Implement cursor-based pagination across multiple API endpoints to improve performance and enable efficient infinite scrolling. This replaces traditional offset-based pagination for high-volume data retrieval.

Changes:
- Add cursor pagination DTOs for all entity types (Post, Comment, Message, Conversation, Group, Notification, GroupMember, GroupAnnouncement)
- Implement cursor pagination methods in repositories with support for various sort orders (created_at, updated_at, join_time, seq)
- Add cursor pagination handlers that maintain backward compatibility with existing offset pagination
- Add new API routes for cursor-based endpoints (/cursor suffix)
- Add helper converter functions for pointer slice types in DTO conversions
This commit is contained in:
lafay
2026-03-20 23:03:23 +08:00
parent 98f0c9f2b6
commit 92babe509f
21 changed files with 2087 additions and 3 deletions

View File

@@ -11,6 +11,7 @@ import (
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/cursor"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/pkg/sse"
"carrot_bbs/internal/service"
@@ -975,3 +976,123 @@ func (h *MessageHandler) HandleSetConversationPinned(c *gin.Context) {
"is_pinned": req.IsPinned,
})
}
// ==================== 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)
}
}
// 转换为响应格式
items := make([]*dto.ConversationResponse, len(filteredItems))
for i, conv := range filteredItems {
// 获取未读数
unreadCount, _ := h.chatService.GetUnreadCount(c.Request.Context(), conv.ID, userID)
// 获取最后一条消息
var lastMessage *model.Message
messages, _, _ := h.chatService.GetMessages(c.Request.Context(), conv.ID, userID, 1, 1)
if len(messages) > 0 {
lastMessage = messages[0]
}
// 群聊时返回member_count私聊时返回participants
var resp *dto.ConversationResponse
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
isPinned := myParticipant != nil && myParticipant.IsPinned
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.MemberCount = memberCount
} else {
// 私聊:获取参与者信息
participants, _ := h.getConversationParticipants(c.Request.Context(), conv.ID, userID)
resp = dto.ConvertConversationToResponse(conv, participants, int(unreadCount), lastMessage, isPinned)
}
items[i] = resp
}
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)
}