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

@@ -9,6 +9,7 @@ import (
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/cursor"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/service"
)
@@ -143,8 +144,16 @@ func (h *PostHandler) RecordView(c *gin.Context) {
response.Success(c, gin.H{"success": true})
}
// List 获取帖子列表
// List 获取帖子列表(支持游标分页和偏移分页)
func (h *PostHandler) List(c *gin.Context) {
// 检查是否使用游标分页
cursorStr := c.Query("cursor")
if cursorStr != "" {
h.ListByCursor(c)
return
}
// 偏移分页(向后兼容)
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
page, pageSize = normalizePagination(page, pageSize)
@@ -207,6 +216,45 @@ func (h *PostHandler) List(c *gin.Context) {
response.Paginated(c, postResponses, total, page, pageSize)
}
// ListByCursor 游标分页获取帖子列表
func (h *PostHandler) ListByCursor(c *gin.Context) {
userID := c.Query("user_id")
currentUserID := c.GetString("user_id")
// 解析游标分页请求
req := h.parseCursorRequest(c)
// 判断是否包含待审核帖子
includePending := userID != "" && userID == currentUserID
// 调用游标分页服务
result, err := h.postService.ListByCursor(c.Request.Context(), userID, includePending, req)
if err != nil {
response.InternalServerError(c, "failed to get posts")
return
}
// 批量获取交互状态
postIDs := make([]string, len(result.Items))
for i, post := range result.Items {
postIDs[i] = post.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
// 转换为响应结构
postResponses := dto.BuildPostsWithInteractionResponse(result.Items, isLikedMap, isFavoritedMap)
// 构建游标分页响应
cursorResp := &dto.PostCursorPageResponse{
Items: postResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// Update 更新帖子
func (h *PostHandler) Update(c *gin.Context) {
userID := c.GetString("user_id")
@@ -412,8 +460,16 @@ func (h *PostHandler) Unfavorite(c *gin.Context) {
response.Success(c, dto.BuildPostResponse(post, isLiked, isFavorited))
}
// GetUserPosts 获取用户帖子列表
// GetUserPosts 获取用户帖子列表(支持游标分页和偏移分页)
func (h *PostHandler) GetUserPosts(c *gin.Context) {
// 检查是否使用游标分页
cursorStr := c.Query("cursor")
if cursorStr != "" {
h.GetUserPostsByCursor(c)
return
}
// 偏移分页(向后兼容)
userID := c.Param("id")
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
@@ -440,6 +496,45 @@ func (h *PostHandler) GetUserPosts(c *gin.Context) {
response.Paginated(c, postResponses, total, page, pageSize)
}
// GetUserPostsByCursor 游标分页获取用户帖子列表
func (h *PostHandler) GetUserPostsByCursor(c *gin.Context) {
userID := c.Param("id")
currentUserID := c.GetString("user_id")
// 解析游标分页请求
req := h.parseCursorRequest(c)
// 判断是否包含待审核帖子
includePending := currentUserID != "" && currentUserID == userID
// 调用游标分页服务
result, err := h.postService.GetUserPostsByCursor(c.Request.Context(), userID, includePending, req)
if err != nil {
response.InternalServerError(c, "failed to get user posts")
return
}
// 批量获取交互状态
postIDs := make([]string, len(result.Items))
for i, post := range result.Items {
postIDs[i] = post.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
// 转换为响应结构
postResponses := dto.BuildPostsWithInteractionResponse(result.Items, isLikedMap, isFavoritedMap)
// 构建游标分页响应
cursorResp := &dto.PostCursorPageResponse{
Items: postResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// GetFavorites 获取收藏列表
func (h *PostHandler) GetFavorites(c *gin.Context) {
userID := c.Param("id")
@@ -470,6 +565,15 @@ func (h *PostHandler) GetFavorites(c *gin.Context) {
// Search 搜索帖子
func (h *PostHandler) Search(c *gin.Context) {
keyword := c.Query("keyword")
// 检查是否使用游标分页
cursorStr := c.Query("cursor")
if cursorStr != "" {
h.SearchByCursor(c)
return
}
// 偏移分页(向后兼容)
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
page, pageSize = normalizePagination(page, pageSize)
@@ -493,3 +597,48 @@ func (h *PostHandler) Search(c *gin.Context) {
response.Paginated(c, postResponses, total, page, pageSize)
}
// SearchByCursor 游标分页搜索帖子
func (h *PostHandler) SearchByCursor(c *gin.Context) {
keyword := c.Query("keyword")
currentUserID := c.GetString("user_id")
// 解析游标分页请求
req := h.parseCursorRequest(c)
// 调用游标分页服务
result, err := h.postService.SearchByCursor(c.Request.Context(), keyword, req)
if err != nil {
response.InternalServerError(c, "failed to search posts")
return
}
// 批量获取交互状态
postIDs := make([]string, len(result.Items))
for i, post := range result.Items {
postIDs[i] = post.ID
}
isLikedMap, isFavoritedMap, _ := h.postService.GetPostInteractionStatus(c.Request.Context(), postIDs, currentUserID)
// 转换为响应结构
postResponses := dto.BuildPostsWithInteractionResponse(result.Items, isLikedMap, isFavoritedMap)
// 构建游标分页响应
cursorResp := &dto.PostCursorPageResponse{
Items: postResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// parseCursorRequest 解析游标分页请求参数
func (h *PostHandler) parseCursorRequest(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)
}