feat(api): add tab-based post feed endpoints with cursor pagination
Some checks failed
Build Backend / build (push) Successful in 3m58s
Build Backend / build-docker (push) Failing after 4m49s

Add support for different post feed types via tab parameter:
- follow: posts from followed users (requires authentication)
- hot: trending posts using Gorse most_liked_weekly or database fallback
- recommend: personalized recommendations with random offset for diversity
- latest: default chronological feed

The cursor pagination detection is also improved to check for parameter
existence rather than non-empty string value.
This commit is contained in:
lafay
2026-03-21 02:24:11 +08:00
parent 57058b04da
commit 687ac92aea
3 changed files with 267 additions and 4 deletions

View File

@@ -906,3 +906,113 @@ func (r *PostRepository) GetUserPostsByCursor(ctx context.Context, userID string
return cursor.NewCursorPageResult(posts, nextCursor, prevCursor, hasMore), nil
}
// GetFollowingPostsByCursor 游标分页获取关注用户的帖子
func (r *PostRepository) GetFollowingPostsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
db := r.getDB(ctx).WithContext(ctx)
// 子查询获取当前用户关注的所有用户ID
subQuery := db.Model(&model.Follow{}).Where("follower_id = ?", userID).Select("following_id")
// 构建基础查询
query := db.Model(&model.Post{}).Where("user_id IN (?) AND status = ?", subQuery, 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
}
// GetHotPostsByCursor 游标分页获取热门帖子
func (r *PostRepository) GetHotPostsByCursor(ctx context.Context, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
db := r.getDB(ctx).WithContext(ctx)
// 构建基础查询 - 热门帖子按热度分数排序
query := db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished).
Order("hot_score DESC, created_at DESC")
// 使用游标构建器
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
}