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,8 +2,10 @@ package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/cursor"
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -655,3 +657,125 @@ func (r *MessageRepository) UpdateConversationLastSeqWithTx(tx *gorm.DB, convID
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
// ========== Cursor Pagination Methods ==========
|
||||
|
||||
// GetMessagesByCursor 游标分页获取会话消息
|
||||
// 消息按 seq DESC 排序
|
||||
func (r *MessageRepository) GetMessagesByCursor(ctx context.Context, conversationID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Message], error) {
|
||||
// 构建基础查询
|
||||
query := r.db.WithContext(ctx).Model(&model.Message{}).Where("conversation_id = ?", conversationID)
|
||||
|
||||
// 使用游标构建器,消息按 seq DESC 排序
|
||||
builder := cursor.NewBuilder(query, cursor.SortBySeqDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.Message]([]*model.Message{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var messages []*model.Message
|
||||
if err := builder.Build().Find(&messages).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解密消息
|
||||
if len(messages) > 0 {
|
||||
model.BatchDecryptMessagesParallel(messages)
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(messages), pageSize)
|
||||
if hasMore {
|
||||
messages = messages[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(messages) > 0 {
|
||||
// 下一页游标
|
||||
if hasMore {
|
||||
lastMsg := messages[len(messages)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
strconv.FormatInt(lastMsg.Seq, 10),
|
||||
lastMsg.ID,
|
||||
cursor.SortBySeqDesc,
|
||||
).Encode()
|
||||
}
|
||||
// 上一页游标
|
||||
firstMsg := messages[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
strconv.FormatInt(firstMsg.Seq, 10),
|
||||
firstMsg.ID,
|
||||
cursor.SortBySeqDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(messages, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
// GetConversationsByCursor 游标分页获取用户会话列表
|
||||
// 会话按置顶优先,然后按 updated_at DESC 排序
|
||||
// 注意:会话列表的排序涉及 conversation_participants 表的 is_pinned 和 updated_at 字段
|
||||
func (r *MessageRepository) GetConversationsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Conversation], error) {
|
||||
// 构建基础查询 - 关联 conversation_participants 表
|
||||
query := r.db.WithContext(ctx).Model(&model.Conversation{}).
|
||||
Joins("INNER JOIN conversation_participants cp ON conversations.id = cp.conversation_id").
|
||||
Where("cp.user_id = ? AND cp.hidden_at IS NULL", userID).
|
||||
Preload("Group")
|
||||
|
||||
// 会话列表需要特殊处理:先按置顶排序,再按更新时间排序
|
||||
// 由于涉及多字段排序(is_pinned DESC, updated_at DESC),需要自定义处理
|
||||
builder := cursor.NewBuilder(query, cursor.SortByUpdatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.Conversation]([]*model.Conversation{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询 - 需要添加置顶排序
|
||||
var conversations []*model.Conversation
|
||||
query = builder.Build()
|
||||
// 添加置顶排序(在游标排序之前)
|
||||
query = query.Order("cp.is_pinned DESC").Order("conversations.updated_at DESC")
|
||||
|
||||
if err := query.Find(&conversations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(conversations), pageSize)
|
||||
if hasMore {
|
||||
conversations = conversations[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(conversations) > 0 {
|
||||
// 下一页游标
|
||||
if hasMore {
|
||||
lastConv := conversations[len(conversations)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastConv.UpdatedAt),
|
||||
lastConv.ID,
|
||||
cursor.SortByUpdatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
// 上一页游标
|
||||
firstConv := conversations[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstConv.UpdatedAt),
|
||||
firstConv.ID,
|
||||
cursor.SortByUpdatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(conversations, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user