feat(api): add cursor-based pagination for posts, comments, messages, groups, and notifications
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:
@@ -2,6 +2,8 @@ package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/cursor"
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -43,6 +45,12 @@ type GroupRepository interface {
|
||||
UpdateGroupStatus(groupID string, status string) error
|
||||
GetGroupPostCount(groupID string) (int64, error)
|
||||
GetMembersWithUserInfo(groupID string, page, pageSize int) ([]GroupMemberWithUser, int64, error)
|
||||
|
||||
// 游标分页方法
|
||||
GetGroupsByCursor(ctx context.Context, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Group], error)
|
||||
GetUserGroupsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Group], error)
|
||||
GetMembersByCursor(ctx context.Context, groupID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.GroupMember], error)
|
||||
GetAnnouncementsByCursor(ctx context.Context, groupID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.GroupAnnouncement], error)
|
||||
}
|
||||
|
||||
// GroupMemberWithUser 群成员带用户信息的结构
|
||||
@@ -410,3 +418,227 @@ func (r *groupRepository) GetMembersWithUserInfo(groupID string, page, pageSize
|
||||
|
||||
return members, total, nil
|
||||
}
|
||||
|
||||
// ========== Cursor Pagination Methods ==========
|
||||
|
||||
// GetGroupsByCursor 游标分页获取群组列表
|
||||
// 排序方式:created_at DESC
|
||||
func (r *groupRepository) GetGroupsByCursor(ctx context.Context, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Group], error) {
|
||||
// 构建基础查询
|
||||
query := r.db.WithContext(ctx).Model(&model.Group{})
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.Group]([]*model.Group{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var groups []*model.Group
|
||||
query = builder.Build()
|
||||
if err := query.Find(&groups).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(groups), pageSize)
|
||||
if hasMore {
|
||||
groups = groups[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(groups) > 0 {
|
||||
// 下一页游标
|
||||
if hasMore {
|
||||
lastGroup := groups[len(groups)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastGroup.CreatedAt),
|
||||
lastGroup.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
// 上一页游标
|
||||
firstGroup := groups[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstGroup.CreatedAt),
|
||||
firstGroup.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(groups, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
// GetUserGroupsByCursor 游标分页获取用户加入的群组列表
|
||||
// 排序方式:created_at DESC
|
||||
func (r *groupRepository) GetUserGroupsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Group], error) {
|
||||
// 通过群成员表查询用户加入的群组
|
||||
subQuery := r.db.Model(&model.GroupMember{}).
|
||||
Select("group_id").
|
||||
Where("user_id = ?", userID)
|
||||
|
||||
// 构建基础查询
|
||||
query := r.db.WithContext(ctx).Model(&model.Group{}).Where("id IN (?)", subQuery)
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.Group]([]*model.Group{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var groups []*model.Group
|
||||
query = builder.Build()
|
||||
if err := query.Find(&groups).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(groups), pageSize)
|
||||
if hasMore {
|
||||
groups = groups[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(groups) > 0 {
|
||||
// 下一页游标
|
||||
if hasMore {
|
||||
lastGroup := groups[len(groups)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastGroup.CreatedAt),
|
||||
lastGroup.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
// 上一页游标
|
||||
firstGroup := groups[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstGroup.CreatedAt),
|
||||
firstGroup.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(groups, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
// GetMembersByCursor 游标分页获取群成员列表
|
||||
// 排序方式:join_time DESC(新成员优先)
|
||||
func (r *groupRepository) GetMembersByCursor(ctx context.Context, groupID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.GroupMember], error) {
|
||||
// 构建基础查询
|
||||
query := r.db.WithContext(ctx).Model(&model.GroupMember{}).Where("group_id = ?", groupID)
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByJoinTimeDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.GroupMember]([]*model.GroupMember{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var members []*model.GroupMember
|
||||
query = builder.Build()
|
||||
if err := query.Find(&members).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(members), pageSize)
|
||||
if hasMore {
|
||||
members = members[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(members) > 0 {
|
||||
// 下一页游标
|
||||
if hasMore {
|
||||
lastMember := members[len(members)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastMember.JoinTime),
|
||||
lastMember.ID,
|
||||
cursor.SortByJoinTimeDesc,
|
||||
).Encode()
|
||||
}
|
||||
// 上一页游标
|
||||
firstMember := members[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstMember.JoinTime),
|
||||
firstMember.ID,
|
||||
cursor.SortByJoinTimeDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(members, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
// GetAnnouncementsByCursor 游标分页获取群公告列表
|
||||
// 排序方式:is_pinned DESC, created_at DESC(置顶优先,然后按时间倒序)
|
||||
// 注意:由于有置顶逻辑,这里使用 created_at DESC 作为游标排序
|
||||
func (r *groupRepository) GetAnnouncementsByCursor(ctx context.Context, groupID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.GroupAnnouncement], error) {
|
||||
// 构建基础查询
|
||||
query := r.db.WithContext(ctx).Model(&model.GroupAnnouncement{}).Where("group_id = ?", groupID)
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.GroupAnnouncement]([]*model.GroupAnnouncement{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询(置顶的排在前面)
|
||||
var announcements []*model.GroupAnnouncement
|
||||
query = builder.Build().Order("is_pinned DESC, created_at DESC")
|
||||
if err := query.Find(&announcements).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(announcements), pageSize)
|
||||
if hasMore {
|
||||
announcements = announcements[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(announcements) > 0 {
|
||||
// 下一页游标
|
||||
if hasMore {
|
||||
lastAnnouncement := announcements[len(announcements)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastAnnouncement.CreatedAt),
|
||||
lastAnnouncement.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
// 上一页游标
|
||||
firstAnnouncement := announcements[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstAnnouncement.CreatedAt),
|
||||
firstAnnouncement.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(announcements, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user