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"
|
||||
)
|
||||
@@ -391,3 +393,181 @@ func (r *CommentRepository) GetCommentsByPostIDForAdmin(postID string, page, pag
|
||||
|
||||
return comments, total, err
|
||||
}
|
||||
|
||||
// ========== Cursor Pagination Methods ==========
|
||||
|
||||
// GetCommentsByCursor 游标分页获取帖子评论(包含回复)
|
||||
// 排序方式:created_at DESC
|
||||
func (r *CommentRepository) GetCommentsByCursor(ctx context.Context, postID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
|
||||
// 构建基础查询 - 只查询顶级评论
|
||||
query := r.db.WithContext(ctx).Model(&model.Comment{}).
|
||||
Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished)
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.Comment]([]*model.Comment{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var comments []*model.Comment
|
||||
query = builder.Build().Preload("User")
|
||||
if err := query.Find(&comments).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(comments), pageSize)
|
||||
if hasMore {
|
||||
comments = comments[:pageSize]
|
||||
}
|
||||
|
||||
// 加载每个评论的回复(默认加载前3条)
|
||||
if len(comments) > 0 {
|
||||
r.loadRepliesForComments(comments, 3)
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(comments) > 0 {
|
||||
// 下一页游标
|
||||
if hasMore {
|
||||
lastComment := comments[len(comments)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastComment.CreatedAt),
|
||||
lastComment.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
// 上一页游标
|
||||
firstComment := comments[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstComment.CreatedAt),
|
||||
firstComment.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(comments, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
// loadRepliesForComments 为评论列表加载回复
|
||||
func (r *CommentRepository) loadRepliesForComments(comments []*model.Comment, replyLimit int) {
|
||||
if len(comments) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
rootIDs := make([]string, 0, len(comments))
|
||||
commentsByID := make(map[string]*model.Comment, len(comments))
|
||||
for _, comment := range comments {
|
||||
rootIDs = append(rootIDs, comment.ID)
|
||||
commentsByID[comment.ID] = comment
|
||||
}
|
||||
|
||||
// 批量加载所有回复
|
||||
var allReplies []*model.Comment
|
||||
if err := r.db.Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
|
||||
Preload("User").
|
||||
Order("created_at ASC").
|
||||
Find(&allReplies).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 按 root_id 分组并裁剪
|
||||
repliesByRoot := make(map[string][]*model.Comment, len(rootIDs))
|
||||
for _, reply := range allReplies {
|
||||
if reply.RootID == nil {
|
||||
continue
|
||||
}
|
||||
rootID := *reply.RootID
|
||||
if replyLimit <= 0 || len(repliesByRoot[rootID]) < replyLimit {
|
||||
repliesByRoot[rootID] = append(repliesByRoot[rootID], reply)
|
||||
}
|
||||
}
|
||||
|
||||
// 查询每个根评论的回复总数
|
||||
type replyCountRow struct {
|
||||
RootID string
|
||||
Total int64
|
||||
}
|
||||
var replyCountRows []replyCountRow
|
||||
if err := r.db.Model(&model.Comment{}).
|
||||
Select("root_id, COUNT(*) AS total").
|
||||
Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
|
||||
Group("root_id").
|
||||
Scan(&replyCountRows).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
replyCountMap := make(map[string]int64, len(replyCountRows))
|
||||
for _, row := range replyCountRows {
|
||||
replyCountMap[row.RootID] = row.Total
|
||||
}
|
||||
|
||||
// 设置回复和回复数
|
||||
for _, rootID := range rootIDs {
|
||||
comment := commentsByID[rootID]
|
||||
comment.Replies = repliesByRoot[rootID]
|
||||
comment.RepliesCount = int(replyCountMap[rootID])
|
||||
}
|
||||
}
|
||||
|
||||
// GetRepliesByCursor 游标分页获取根评论的回复
|
||||
// 排序方式:created_at ASC(回复按时间正序)
|
||||
func (r *CommentRepository) GetRepliesByCursor(ctx context.Context, rootID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
|
||||
// 构建基础查询
|
||||
query := r.db.WithContext(ctx).Model(&model.Comment{}).
|
||||
Where("root_id = ? AND status = ?", rootID, model.CommentStatusPublished)
|
||||
|
||||
// 使用游标构建器(回复按时间升序)
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtAsc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.Comment]([]*model.Comment{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var replies []*model.Comment
|
||||
query = builder.Build().Preload("User")
|
||||
if err := query.Find(&replies).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(replies), pageSize)
|
||||
if hasMore {
|
||||
replies = replies[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(replies) > 0 {
|
||||
// 下一页游标
|
||||
if hasMore {
|
||||
lastReply := replies[len(replies)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastReply.CreatedAt),
|
||||
lastReply.ID,
|
||||
cursor.SortByCreatedAtAsc,
|
||||
).Encode()
|
||||
}
|
||||
// 上一页游标
|
||||
firstReply := replies[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstReply.CreatedAt),
|
||||
firstReply.ID,
|
||||
cursor.SortByCreatedAtAsc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(replies, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/cursor"
|
||||
"context"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -76,3 +78,63 @@ func (r *NotificationRepository) GetUnreadCount(userID string) (int64, error) {
|
||||
func (r *NotificationRepository) DeleteAllByUserID(userID string) error {
|
||||
return r.db.Where("user_id = ?", userID).Delete(&model.Notification{}).Error
|
||||
}
|
||||
|
||||
// ========== Cursor Pagination Methods ==========
|
||||
|
||||
// GetNotificationsByCursor 游标分页获取用户通知
|
||||
// 排序方式:created_at DESC
|
||||
func (r *NotificationRepository) GetNotificationsByCursor(ctx context.Context, userID string, unreadOnly bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Notification], error) {
|
||||
// 构建基础查询
|
||||
query := r.db.WithContext(ctx).Model(&model.Notification{}).Where("user_id = ?", userID)
|
||||
|
||||
if unreadOnly {
|
||||
query = query.Where("is_read = ?", false)
|
||||
}
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.Notification]([]*model.Notification{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var notifications []*model.Notification
|
||||
query = builder.Build()
|
||||
if err := query.Find(¬ifications).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(notifications), pageSize)
|
||||
if hasMore {
|
||||
notifications = notifications[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(notifications) > 0 {
|
||||
// 下一页游标
|
||||
if hasMore {
|
||||
lastNotification := notifications[len(notifications)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastNotification.CreatedAt),
|
||||
lastNotification.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
// 上一页游标
|
||||
firstNotification := notifications[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstNotification.CreatedAt),
|
||||
firstNotification.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(notifications, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/cursor"
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -709,3 +710,199 @@ func (r *PostRepository) UpdateFeatureStatus(postID string, isFeatured bool) err
|
||||
return r.db.Model(&model.Post{}).Where("id = ?", postID).
|
||||
Update("is_featured", isFeatured).Error
|
||||
}
|
||||
|
||||
// ========== Cursor Pagination Methods ==========
|
||||
|
||||
// GetPostsByCursor 游标分页获取帖子列表
|
||||
// includePending=true 时,仅在指定 userID 下额外返回 pending(用于作者查看自己待审核帖子)
|
||||
func (r *PostRepository) GetPostsByCursor(ctx context.Context, userID string, includePending bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
|
||||
// 构建基础查询
|
||||
query := db.Model(&model.Post{})
|
||||
|
||||
if userID != "" {
|
||||
query = query.Where("user_id = ?", userID)
|
||||
}
|
||||
if includePending && userID != "" {
|
||||
query = query.Where("status IN ?", []model.PostStatus{
|
||||
model.PostStatusPublished,
|
||||
model.PostStatusPending,
|
||||
})
|
||||
} else {
|
||||
query = query.Where("status = ?", model.PostStatusPublished)
|
||||
}
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var posts []*model.Post
|
||||
query = builder.Build().Preload("User").Preload("Images")
|
||||
if err := query.Find(&posts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(posts), pageSize)
|
||||
if hasMore {
|
||||
posts = posts[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(posts) > 0 {
|
||||
// 下一页游标
|
||||
if hasMore {
|
||||
lastPost := posts[len(posts)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastPost.CreatedAt),
|
||||
lastPost.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
// 上一页游标
|
||||
firstPost := posts[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstPost.CreatedAt),
|
||||
firstPost.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(posts, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
// SearchPostsByCursor 游标分页搜索帖子
|
||||
func (r *PostRepository) SearchPostsByCursor(ctx context.Context, keyword string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
|
||||
query := db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished)
|
||||
|
||||
// 搜索标题和内容
|
||||
if keyword != "" {
|
||||
if r.db.Dialector.Name() == "postgres" {
|
||||
// PostgreSQL 使用全文检索表达式
|
||||
query = query.Where(
|
||||
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)",
|
||||
keyword,
|
||||
)
|
||||
} else {
|
||||
searchPattern := "%" + keyword + "%"
|
||||
query = query.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var posts []*model.Post
|
||||
query = builder.Build().Preload("User").Preload("Images")
|
||||
if err := query.Find(&posts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(posts), pageSize)
|
||||
if hasMore {
|
||||
posts = posts[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(posts) > 0 {
|
||||
if hasMore {
|
||||
lastPost := posts[len(posts)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastPost.CreatedAt),
|
||||
lastPost.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
firstPost := posts[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstPost.CreatedAt),
|
||||
firstPost.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(posts, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
// GetUserPostsByCursor 游标分页获取用户帖子
|
||||
func (r *PostRepository) GetUserPostsByCursor(ctx context.Context, userID string, includePending bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
|
||||
query := db.Model(&model.Post{}).Where("user_id = ?", userID)
|
||||
if includePending {
|
||||
query = query.Where("status IN ?", []model.PostStatus{
|
||||
model.PostStatusPublished,
|
||||
model.PostStatusPending,
|
||||
})
|
||||
} else {
|
||||
query = query.Where("status = ?", model.PostStatusPublished)
|
||||
}
|
||||
|
||||
// 使用游标构建器
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
// 无效游标,返回空列表
|
||||
return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
var posts []*model.Post
|
||||
query = builder.Build().Preload("User").Preload("Images")
|
||||
if err := query.Find(&posts).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(posts), pageSize)
|
||||
if hasMore {
|
||||
posts = posts[:pageSize]
|
||||
}
|
||||
|
||||
// 生成游标
|
||||
var nextCursor, prevCursor string
|
||||
if len(posts) > 0 {
|
||||
if hasMore {
|
||||
lastPost := posts[len(posts)-1]
|
||||
nextCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(lastPost.CreatedAt),
|
||||
lastPost.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
firstPost := posts[0]
|
||||
prevCursor = cursor.NewCursor(
|
||||
cursor.FormatTime(firstPost.CreatedAt),
|
||||
firstPost.ID,
|
||||
cursor.SortByCreatedAtDesc,
|
||||
).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(posts, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user