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

@@ -8,6 +8,7 @@ import (
"carrot_bbs/internal/dto"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/cursor"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/service"
)
@@ -1279,6 +1280,163 @@ func (h *GroupHandler) DeleteAnnouncement(c *gin.Context) {
response.SuccessWithMessage(c, "公告已删除", nil)
}
// ==================== 游标分页方法 ====================
// GetGroupsByCursor 游标分页获取群组列表
// GET /api/groups/cursor
func (h *GroupHandler) GetGroupsByCursor(c *gin.Context) {
// 解析游标分页请求
req := h.parseCursorRequest(c)
// 调用游标分页服务
result, err := h.groupService.GetGroupsByCursor(c.Request.Context(), req)
if err != nil {
response.InternalServerError(c, err.Error())
return
}
// 转换为响应结构
groupResponses := dto.GroupsToResponseFromPtr(result.Items)
// 构建游标分页响应
cursorResp := &dto.GroupCursorPageResponse{
Items: groupResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// GetUserGroupsByCursor 游标分页获取用户加入的群组列表
// GET /api/users/:id/groups/cursor
func (h *GroupHandler) GetUserGroupsByCursor(c *gin.Context) {
userID := parseUserIDFromPath(c)
if userID == "" {
response.Unauthorized(c, "")
return
}
// 解析游标分页请求
req := h.parseCursorRequest(c)
// 调用游标分页服务
result, err := h.groupService.GetUserGroupsByCursor(c.Request.Context(), userID, req)
if err != nil {
response.InternalServerError(c, err.Error())
return
}
// 转换为响应结构
groupResponses := dto.GroupsToResponseFromPtr(result.Items)
// 构建游标分页响应
cursorResp := &dto.GroupCursorPageResponse{
Items: groupResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// GetMembersByCursor 游标分页获取群成员列表
// GET /api/groups/:id/members/cursor
func (h *GroupHandler) GetMembersByCursor(c *gin.Context) {
userID := parseUserID(c)
if userID == "" {
response.Unauthorized(c, "")
return
}
groupID := parseGroupID(c)
if groupID == "" {
response.BadRequest(c, "invalid group id")
return
}
// 解析游标分页请求
req := h.parseCursorRequest(c)
// 调用游标分页服务
result, err := h.groupService.GetMembersByCursor(c.Request.Context(), groupID, req)
if err != nil {
if err == service.ErrGroupNotFound {
response.NotFound(c, "群组不存在")
return
}
response.InternalServerError(c, err.Error())
return
}
// 转换为响应结构
memberResponses := dto.GroupMembersToResponseFromPtr(result.Items)
// 构建游标分页响应
cursorResp := &dto.GroupMemberCursorPageResponse{
Items: memberResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// GetAnnouncementsByCursor 游标分页获取群公告列表
// GET /api/groups/:id/announcements/cursor
func (h *GroupHandler) GetAnnouncementsByCursor(c *gin.Context) {
userID := parseUserID(c)
if userID == "" {
response.Unauthorized(c, "")
return
}
groupID := parseGroupID(c)
if groupID == "" {
response.BadRequest(c, "invalid group id")
return
}
// 解析游标分页请求
req := h.parseCursorRequest(c)
// 调用游标分页服务
result, err := h.groupService.GetAnnouncementsByCursor(c.Request.Context(), groupID, req)
if err != nil {
if err == service.ErrGroupNotFound {
response.NotFound(c, "群组不存在")
return
}
response.InternalServerError(c, err.Error())
return
}
// 转换为响应结构
announcementResponses := dto.GroupAnnouncementsToResponseFromPtr(result.Items)
// 构建游标分页响应
cursorResp := &dto.GroupAnnouncementCursorPageResponse{
Items: announcementResponses,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
}
response.Success(c, cursorResp)
}
// parseCursorRequest 解析游标分页请求参数
func (h *GroupHandler) 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)
}
// ==================== RESTful Action 端点 ====================
// HandleSetGroupKick 群组踢人