2026-03-09 21:28:58 +08:00
package repository
import (
"carrot_bbs/internal/model"
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
2026-03-20 23:03:23 +08:00
"carrot_bbs/internal/pkg/cursor"
2026-03-13 09:38:18 +08:00
"context"
2026-03-15 02:25:10 +08:00
"strings"
2026-03-10 12:58:23 +08:00
"time"
2026-03-09 21:28:58 +08:00
"gorm.io/gorm"
)
// PostRepository 帖子仓储
type PostRepository struct {
db * gorm . DB
}
// NewPostRepository 创建帖子仓储
func NewPostRepository ( db * gorm . DB ) * PostRepository {
return & PostRepository { db : db }
}
2026-03-13 09:38:18 +08:00
// getDB 获取数据库连接(优先使用 context 中的事务)
func ( r * PostRepository ) getDB ( ctx context . Context ) * gorm . DB {
if tx := GetTxFromContext ( ctx ) ; tx != nil {
return tx
}
return r . db
}
2026-03-09 21:28:58 +08:00
// Create 创建帖子
func ( r * PostRepository ) Create ( post * model . Post , images [ ] string ) error {
return r . db . Transaction ( func ( tx * gorm . DB ) error {
// 创建帖子
if err := tx . Create ( post ) . Error ; err != nil {
return err
}
// 创建图片记录
2026-03-15 02:25:10 +08:00
// 支持格式:["url", "url|preview_url|preview_url_large", ...]
for i , imageData := range images {
parts := strings . Split ( imageData , "|" )
url := parts [ 0 ]
var previewURL , previewURLLarge string
if len ( parts ) >= 2 && parts [ 1 ] != "" {
previewURL = parts [ 1 ]
}
if len ( parts ) >= 3 && parts [ 2 ] != "" {
previewURLLarge = parts [ 2 ]
}
2026-03-09 21:28:58 +08:00
image := & model . PostImage {
2026-03-15 02:25:10 +08:00
PostID : post . ID ,
URL : url ,
PreviewURL : previewURL ,
PreviewURLLarge : previewURLLarge ,
SortOrder : i ,
2026-03-09 21:28:58 +08:00
}
if err := tx . Create ( image ) . Error ; err != nil {
return err
}
}
return nil
} )
}
// GetByID 根据ID获取帖子
func ( r * PostRepository ) GetByID ( id string ) ( * model . Post , error ) {
var post model . Post
err := r . db . Preload ( "User" ) . Preload ( "Images" ) . First ( & post , "id = ?" , id ) . Error
if err != nil {
return nil , err
}
return & post , nil
}
// Update 更新帖子
func ( r * PostRepository ) Update ( post * model . Post ) error {
2026-03-10 12:58:23 +08:00
post . UpdatedAt = time . Now ( )
2026-03-09 21:28:58 +08:00
return r . db . Save ( post ) . Error
}
2026-03-10 12:58:23 +08:00
// UpdateWithImages 更新帖子及其图片( images=nil 表示不更新图片)
func ( r * PostRepository ) UpdateWithImages ( post * model . Post , images * [ ] string ) error {
return r . db . Transaction ( func ( tx * gorm . DB ) error {
post . UpdatedAt = time . Now ( )
if err := tx . Save ( post ) . Error ; err != nil {
return err
}
if images == nil {
return nil
}
if err := tx . Where ( "post_id = ?" , post . ID ) . Delete ( & model . PostImage { } ) . Error ; err != nil {
return err
}
for i , url := range * images {
image := & model . PostImage {
PostID : post . ID ,
URL : url ,
SortOrder : i ,
}
if err := tx . Create ( image ) . Error ; err != nil {
return err
}
}
return nil
} )
}
2026-03-09 21:28:58 +08:00
// UpdateModerationStatus 更新帖子审核状态
func ( r * PostRepository ) UpdateModerationStatus ( postID string , status model . PostStatus , rejectReason string , reviewedBy string ) error {
2026-03-23 01:06:41 +08:00
// 审核状态更新属于管理操作, 不应影响帖子内容更新时间( updated_at)
2026-03-09 21:28:58 +08:00
updates := map [ string ] interface { } {
"status" : status ,
"reviewed_at" : gorm . Expr ( "CURRENT_TIMESTAMP" ) ,
"reviewed_by" : reviewedBy ,
"reject_reason" : rejectReason ,
}
2026-03-23 01:06:41 +08:00
return r . db . Model ( & model . Post { } ) . Where ( "id = ?" , postID ) . UpdateColumns ( updates ) . Error
2026-03-09 21:28:58 +08:00
}
// Delete 删除帖子(软删除,同时清理关联数据)
func ( r * PostRepository ) Delete ( id string ) error {
return r . db . Transaction ( func ( tx * gorm . DB ) error {
// 删除帖子图片
if err := tx . Where ( "post_id = ?" , id ) . Delete ( & model . PostImage { } ) . Error ; err != nil {
return err
}
// 删除帖子点赞记录
if err := tx . Where ( "post_id = ?" , id ) . Delete ( & model . PostLike { } ) . Error ; err != nil {
return err
}
// 删除帖子收藏记录
if err := tx . Where ( "post_id = ?" , id ) . Delete ( & model . Favorite { } ) . Error ; err != nil {
return err
}
// 删除评论点赞记录( 子查询获取该帖子所有评论ID)
if err := tx . Where ( "comment_id IN (SELECT id FROM comments WHERE post_id = ?)" , id ) . Delete ( & model . CommentLike { } ) . Error ; err != nil {
return err
}
// 删除帖子评论
if err := tx . Where ( "post_id = ?" , id ) . Delete ( & model . Comment { } ) . Error ; err != nil {
return err
}
// 最后删除帖子本身(软删除)
return tx . Delete ( & model . Post { } , "id = ?" , id ) . Error
} )
}
// List 分页获取帖子列表
2026-03-10 12:58:23 +08:00
// includePending=true 时,仅在指定 userID 下额外返回 pending( 用于作者查看自己待审核帖子)
func ( r * PostRepository ) List ( page , pageSize int , userID string , includePending bool ) ( [ ] * model . Post , int64 , error ) {
2026-03-09 21:28:58 +08:00
var posts [ ] * model . Post
var total int64
2026-03-10 12:58:23 +08:00
query := r . db . Model ( & model . Post { } )
2026-03-09 21:28:58 +08:00
if userID != "" {
query = query . Where ( "user_id = ?" , userID )
}
2026-03-10 12:58:23 +08:00
if includePending && userID != "" {
query = query . Where ( "status IN ?" , [ ] model . PostStatus {
model . PostStatusPublished ,
model . PostStatusPending ,
} )
} else {
query = query . Where ( "status = ?" , model . PostStatusPublished )
}
2026-03-09 21:28:58 +08:00
query . Count ( & total )
offset := ( page - 1 ) * pageSize
err := query . Preload ( "User" ) . Preload ( "Images" ) . Offset ( offset ) . Limit ( pageSize ) . Order ( "created_at DESC" ) . Find ( & posts ) . Error
return posts , total , err
}
// GetUserPosts 获取用户帖子
2026-03-10 12:58:23 +08:00
func ( r * PostRepository ) GetUserPosts ( userID string , page , pageSize int , includePending bool ) ( [ ] * model . Post , int64 , error ) {
2026-03-09 21:28:58 +08:00
var posts [ ] * model . Post
var total int64
2026-03-10 12:58:23 +08:00
statusQuery := r . db . Model ( & model . Post { } ) . Where ( "user_id = ?" , userID )
if includePending {
statusQuery = statusQuery . Where ( "status IN ?" , [ ] model . PostStatus {
model . PostStatusPublished ,
model . PostStatusPending ,
} )
} else {
statusQuery = statusQuery . Where ( "status = ?" , model . PostStatusPublished )
}
statusQuery . Count ( & total )
2026-03-09 21:28:58 +08:00
offset := ( page - 1 ) * pageSize
2026-03-10 12:58:23 +08:00
listQuery := r . db . Where ( "user_id = ?" , userID )
if includePending {
listQuery = listQuery . Where ( "status IN ?" , [ ] model . PostStatus {
model . PostStatusPublished ,
model . PostStatusPending ,
} )
} else {
listQuery = listQuery . Where ( "status = ?" , model . PostStatusPublished )
}
err := listQuery . Preload ( "User" ) . Preload ( "Images" ) . Offset ( offset ) . Limit ( pageSize ) . Order ( "created_at DESC" ) . Find ( & posts ) . Error
2026-03-09 21:28:58 +08:00
return posts , total , err
}
// GetFavorites 获取用户收藏
func ( r * PostRepository ) GetFavorites ( userID string , page , pageSize int ) ( [ ] * model . Post , int64 , error ) {
var posts [ ] * model . Post
var total int64
subQuery := r . db . Model ( & model . Favorite { } ) . Where ( "user_id = ?" , userID ) . Select ( "post_id" )
r . db . Model ( & model . Post { } ) . Where ( "id IN (?) AND status = ?" , subQuery , model . PostStatusPublished ) . Count ( & total )
offset := ( page - 1 ) * pageSize
err := r . db . Where ( "id IN (?) AND status = ?" , subQuery , model . PostStatusPublished ) . Preload ( "User" ) . Preload ( "Images" ) . Offset ( offset ) . Limit ( pageSize ) . Order ( "created_at DESC" ) . Find ( & posts ) . Error
return posts , total , err
}
// Like 点赞帖子
func ( r * PostRepository ) Like ( postID , userID string ) error {
return r . db . Transaction ( func ( tx * gorm . DB ) error {
// 检查是否已经点赞
var existing model . PostLike
err := tx . Where ( "post_id = ? AND user_id = ?" , postID , userID ) . First ( & existing ) . Error
if err == nil {
// 已经点赞,直接返回
return nil
}
if err != gorm . ErrRecordNotFound {
return err
}
// 创建点赞记录
if err := tx . Create ( & model . PostLike {
PostID : postID ,
UserID : userID ,
} ) . Error ; err != nil {
return err
}
// 增加帖子点赞数并同步热度分
2026-03-23 01:06:41 +08:00
// 点赞属于统计字段更新, 不应影响帖子内容更新时间( updated_at)
2026-03-09 21:28:58 +08:00
return tx . Model ( & model . Post { } ) . Where ( "id = ?" , postID ) .
2026-03-23 01:06:41 +08:00
UpdateColumns ( map [ string ] any {
2026-03-09 21:28:58 +08:00
"likes_count" : gorm . Expr ( "likes_count + 1" ) ,
"hot_score" : gorm . Expr ( "(likes_count + 1) * 2 + comments_count * 3 + views_count * 0.1" ) ,
} ) . Error
} )
}
// Unlike 取消点赞
func ( r * PostRepository ) Unlike ( postID , userID string ) error {
return r . db . Transaction ( func ( tx * gorm . DB ) error {
result := tx . Where ( "post_id = ? AND user_id = ?" , postID , userID ) . Delete ( & model . PostLike { } )
if result . Error != nil {
return result . Error
}
if result . RowsAffected > 0 {
// 减少帖子点赞数并同步热度分
2026-03-23 01:06:41 +08:00
// 取消点赞属于统计字段更新, 不应影响帖子内容更新时间( updated_at)
2026-03-09 21:28:58 +08:00
return tx . Model ( & model . Post { } ) . Where ( "id = ?" , postID ) .
2026-03-23 01:06:41 +08:00
UpdateColumns ( map [ string ] any {
2026-03-09 21:28:58 +08:00
"likes_count" : gorm . Expr ( "likes_count - 1" ) ,
"hot_score" : gorm . Expr ( "(likes_count - 1) * 2 + comments_count * 3 + views_count * 0.1" ) ,
} ) . Error
}
return nil
} )
}
// IsLiked 检查是否点赞
func ( r * PostRepository ) IsLiked ( postID , userID string ) bool {
var count int64
r . db . Model ( & model . PostLike { } ) . Where ( "post_id = ? AND user_id = ?" , postID , userID ) . Count ( & count )
return count > 0
}
2026-03-17 00:47:17 +08:00
// IsLikedBatch 批量检查是否点赞(解决 N+1 问题)
// 返回 map[postID]bool
func ( r * PostRepository ) IsLikedBatch ( postIDs [ ] string , userID string ) map [ string ] bool {
result := make ( map [ string ] bool )
if len ( postIDs ) == 0 || userID == "" {
return result
}
// 初始化所有 postID 为 false
for _ , postID := range postIDs {
result [ postID ] = false
}
// 一次性查询所有点赞记录
var likedPostIDs [ ] string
r . db . Model ( & model . PostLike { } ) .
Where ( "post_id IN ? AND user_id = ?" , postIDs , userID ) .
Pluck ( "post_id" , & likedPostIDs )
// 更新已点赞的帖子
for _ , postID := range likedPostIDs {
result [ postID ] = true
}
return result
}
2026-03-09 21:28:58 +08:00
// Favorite 收藏帖子
func ( r * PostRepository ) Favorite ( postID , userID string ) error {
return r . db . Transaction ( func ( tx * gorm . DB ) error {
// 检查是否已经收藏
var existing model . Favorite
err := tx . Where ( "post_id = ? AND user_id = ?" , postID , userID ) . First ( & existing ) . Error
if err == nil {
// 已经收藏,直接返回
return nil
}
if err != gorm . ErrRecordNotFound {
return err
}
// 创建收藏记录
if err := tx . Create ( & model . Favorite {
PostID : postID ,
UserID : userID ,
} ) . Error ; err != nil {
return err
}
// 增加帖子收藏数
return tx . Model ( & model . Post { } ) . Where ( "id = ?" , postID ) .
UpdateColumn ( "favorites_count" , gorm . Expr ( "favorites_count + 1" ) ) . Error
} )
}
// Unfavorite 取消收藏
func ( r * PostRepository ) Unfavorite ( postID , userID string ) error {
return r . db . Transaction ( func ( tx * gorm . DB ) error {
result := tx . Where ( "post_id = ? AND user_id = ?" , postID , userID ) . Delete ( & model . Favorite { } )
if result . Error != nil {
return result . Error
}
if result . RowsAffected > 0 {
// 减少帖子收藏数
return tx . Model ( & model . Post { } ) . Where ( "id = ?" , postID ) .
UpdateColumn ( "favorites_count" , gorm . Expr ( "favorites_count - 1" ) ) . Error
}
return nil
} )
}
// IsFavorited 检查是否收藏
func ( r * PostRepository ) IsFavorited ( postID , userID string ) bool {
var count int64
r . db . Model ( & model . Favorite { } ) . Where ( "post_id = ? AND user_id = ?" , postID , userID ) . Count ( & count )
return count > 0
}
2026-03-17 00:47:17 +08:00
// IsFavoritedBatch 批量检查是否收藏(解决 N+1 问题)
// 返回 map[postID]bool
func ( r * PostRepository ) IsFavoritedBatch ( postIDs [ ] string , userID string ) map [ string ] bool {
result := make ( map [ string ] bool )
if len ( postIDs ) == 0 || userID == "" {
return result
}
// 初始化所有 postID 为 false
for _ , postID := range postIDs {
result [ postID ] = false
}
// 一次性查询所有收藏记录
var favoritedPostIDs [ ] string
r . db . Model ( & model . Favorite { } ) .
Where ( "post_id IN ? AND user_id = ?" , postIDs , userID ) .
Pluck ( "post_id" , & favoritedPostIDs )
// 更新已收藏的帖子
for _ , postID := range favoritedPostIDs {
result [ postID ] = true
}
return result
}
2026-03-09 21:28:58 +08:00
// IncrementViews 增加帖子观看量
func ( r * PostRepository ) IncrementViews ( postID string ) error {
return r . db . Model ( & model . Post { } ) . Where ( "id = ?" , postID ) .
2026-03-10 12:58:23 +08:00
// 浏览量属于统计字段, 不应影响帖子内容更新时间( updated_at)
UpdateColumns ( map [ string ] interface { } {
2026-03-09 21:28:58 +08:00
"views_count" : gorm . Expr ( "views_count + 1" ) ,
"hot_score" : gorm . Expr ( "likes_count * 2 + comments_count * 3 + (views_count + 1) * 0.1" ) ,
} ) . Error
}
// Search 搜索帖子
func ( r * PostRepository ) Search ( keyword string , page , pageSize int ) ( [ ] * model . Post , int64 , error ) {
var posts [ ] * model . Post
var total int64
query := r . db . Model ( & model . Post { } ) . Where ( "status = ?" , model . PostStatusPublished )
// 搜索标题和内容
if keyword != "" {
if r . db . Dialector . Name ( ) == "postgres" {
// PostgreSQL 使用全文检索表达式,为 pg_trgm/GIN 索引升级预留路径
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 )
}
}
query . Count ( & total )
offset := ( page - 1 ) * pageSize
err := query . Preload ( "User" ) . Preload ( "Images" ) . Offset ( offset ) . Limit ( pageSize ) . Order ( "created_at DESC" ) . Find ( & posts ) . Error
return posts , total , err
}
// GetFollowingPosts 获取关注用户的帖子
func ( r * PostRepository ) GetFollowingPosts ( userID string , page , pageSize int ) ( [ ] * model . Post , int64 , error ) {
var posts [ ] * model . Post
var total int64
// 子查询: 获取当前用户关注的所有用户ID
subQuery := r . db . Model ( & model . Follow { } ) . Where ( "follower_id = ?" , userID ) . Select ( "following_id" )
// 统计总数
r . db . Model ( & model . Post { } ) . Where ( "user_id IN (?) AND status = ?" , subQuery , model . PostStatusPublished ) . Count ( & total )
offset := ( page - 1 ) * pageSize
err := r . db . Where ( "user_id IN (?) AND status = ?" , subQuery , model . PostStatusPublished ) .
Preload ( "User" ) . Preload ( "Images" ) .
Offset ( offset ) . Limit ( pageSize ) .
Order ( "created_at DESC" ) .
Find ( & posts ) . Error
return posts , total , err
}
// GetHotPosts 获取热门帖子(按点赞数和评论数排序)
func ( r * PostRepository ) GetHotPosts ( page , pageSize int ) ( [ ] * model . Post , int64 , error ) {
var posts [ ] * model . Post
var total int64
r . db . Model ( & model . Post { } ) . Where ( "status = ?" , model . PostStatusPublished ) . Count ( & total )
offset := ( page - 1 ) * pageSize
// 热门排序使用预计算热度分,避免每次请求进行表达式排序计算
err := r . db . Where ( "status = ?" , model . PostStatusPublished ) . Preload ( "User" ) . Preload ( "Images" ) .
Offset ( offset ) . Limit ( pageSize ) .
Order ( "hot_score DESC, created_at DESC" ) .
Find ( & posts ) . Error
return posts , total , err
}
// GetByIDs 根据ID列表获取帖子( 保持传入顺序)
func ( r * PostRepository ) GetByIDs ( ids [ ] string ) ( [ ] * model . Post , error ) {
if len ( ids ) == 0 {
return [ ] * model . Post { } , nil
}
var posts [ ] * model . Post
err := r . db . Preload ( "User" ) . Preload ( "Images" ) .
Where ( "id IN ? AND status = ?" , ids , model . PostStatusPublished ) .
Find ( & posts ) . Error
if err != nil {
return nil , err
}
// 按传入ID顺序排序
postMap := make ( map [ string ] * model . Post )
for _ , post := range posts {
postMap [ post . ID ] = post
}
ordered := make ( [ ] * model . Post , 0 , len ( ids ) )
for _ , id := range ids {
if post , ok := postMap [ id ] ; ok {
ordered = append ( ordered , post )
}
}
return ordered , nil
}
2026-03-13 09:38:18 +08:00
// ========== Context-aware methods for transaction support ==========
// CreateWithContext 创建帖子(支持事务)
func ( r * PostRepository ) CreateWithContext ( ctx context . Context , post * model . Post , images [ ] string ) error {
db := r . getDB ( ctx )
// 如果 context 中有事务,直接使用
if tx := GetTxFromContext ( ctx ) ; tx != nil {
return r . createWithTx ( tx , post , images )
}
// 否则开启新事务
return db . WithContext ( ctx ) . Transaction ( func ( tx * gorm . DB ) error {
return r . createWithTx ( tx , post , images )
} )
}
// createWithTx 使用指定事务创建帖子
func ( r * PostRepository ) createWithTx ( tx * gorm . DB , post * model . Post , images [ ] string ) error {
// 创建帖子
if err := tx . Create ( post ) . Error ; err != nil {
return err
}
// 创建图片记录
for i , url := range images {
image := & model . PostImage {
PostID : post . ID ,
URL : url ,
SortOrder : i ,
}
if err := tx . Create ( image ) . Error ; err != nil {
return err
}
}
return nil
}
// GetByIDWithContext 根据ID获取帖子( 支持事务)
func ( r * PostRepository ) GetByIDWithContext ( ctx context . Context , id string ) ( * model . Post , error ) {
var post model . Post
err := r . getDB ( ctx ) . WithContext ( ctx ) . Preload ( "User" ) . Preload ( "Images" ) . First ( & post , "id = ?" , id ) . Error
if err != nil {
return nil , err
}
return & post , nil
}
// UpdateWithContext 更新帖子(支持事务)
func ( r * PostRepository ) UpdateWithContext ( ctx context . Context , post * model . Post ) error {
post . UpdatedAt = time . Now ( )
return r . getDB ( ctx ) . WithContext ( ctx ) . Save ( post ) . Error
}
// DeleteWithContext 删除帖子(支持事务)
func ( r * PostRepository ) DeleteWithContext ( ctx context . Context , id string ) error {
db := r . getDB ( ctx )
// 如果 context 中有事务,直接使用
if tx := GetTxFromContext ( ctx ) ; tx != nil {
return r . deleteWithTx ( tx , id )
}
// 否则开启新事务
return db . WithContext ( ctx ) . Transaction ( func ( tx * gorm . DB ) error {
return r . deleteWithTx ( tx , id )
} )
}
// deleteWithTx 使用指定事务删除帖子
func ( r * PostRepository ) deleteWithTx ( tx * gorm . DB , id string ) error {
// 删除帖子图片
if err := tx . Where ( "post_id = ?" , id ) . Delete ( & model . PostImage { } ) . Error ; err != nil {
return err
}
// 删除帖子点赞记录
if err := tx . Where ( "post_id = ?" , id ) . Delete ( & model . PostLike { } ) . Error ; err != nil {
return err
}
// 删除帖子收藏记录
if err := tx . Where ( "post_id = ?" , id ) . Delete ( & model . Favorite { } ) . Error ; err != nil {
return err
}
// 删除评论点赞记录( 子查询获取该帖子所有评论ID)
if err := tx . Where ( "comment_id IN (SELECT id FROM comments WHERE post_id = ?)" , id ) . Delete ( & model . CommentLike { } ) . Error ; err != nil {
return err
}
// 删除帖子评论
if err := tx . Where ( "post_id = ?" , id ) . Delete ( & model . Comment { } ) . Error ; err != nil {
return err
}
// 最后删除帖子本身(软删除)
return tx . Delete ( & model . Post { } , "id = ?" , id ) . Error
}
2026-03-14 18:01:55 +08:00
// ========== Admin methods ==========
// AdminPostListQuery 管理端帖子列表查询参数
type AdminPostListQuery struct {
Page int
PageSize int
Keyword string
Status string // 为空表示所有状态
AuthorID string
StartDate string // 格式: 2006-01-02
EndDate string // 格式: 2006-01-02
}
// AdminList 管理端分页获取帖子列表(支持多条件筛选)
func ( r * PostRepository ) AdminList ( query AdminPostListQuery ) ( [ ] * model . Post , int64 , error ) {
var posts [ ] * model . Post
var total int64
db := r . db . Model ( & model . Post { } )
// 关键词搜索(标题或内容)
if query . Keyword != "" {
if r . db . Dialector . Name ( ) == "postgres" {
db = db . Where (
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)" ,
query . Keyword ,
)
} else {
searchPattern := "%" + query . Keyword + "%"
db = db . Where ( "title LIKE ? OR content LIKE ?" , searchPattern , searchPattern )
}
}
// 状态筛选
if query . Status != "" {
db = db . Where ( "status = ?" , query . Status )
}
// 作者筛选
if query . AuthorID != "" {
db = db . Where ( "user_id = ?" , query . AuthorID )
}
// 日期范围筛选
if query . StartDate != "" {
db = db . Where ( "created_at >= ?" , query . StartDate + " 00:00:00" )
}
if query . EndDate != "" {
db = db . Where ( "created_at <= ?" , query . EndDate + " 23:59:59" )
}
// 统计总数
db . Count ( & total )
// 分页查询
offset := ( query . Page - 1 ) * query . PageSize
err := db . Preload ( "User" ) . Preload ( "Images" ) .
Offset ( offset ) . Limit ( query . PageSize ) .
Order ( "created_at DESC" ) .
Find ( & posts ) . Error
return posts , total , err
}
// GetByIDForAdmin 管理端根据ID获取帖子( 包含所有状态)
func ( r * PostRepository ) GetByIDForAdmin ( id string ) ( * model . Post , error ) {
var post model . Post
err := r . db . Preload ( "User" ) . Preload ( "Images" ) . First ( & post , "id = ?" , id ) . Error
if err != nil {
return nil , err
}
return & post , nil
}
// BatchDelete 批量删除帖子
func ( r * PostRepository ) BatchDelete ( ids [ ] string ) ( [ ] string , error ) {
var failedIDs [ ] string
for _ , id := range ids {
if err := r . Delete ( id ) ; err != nil {
failedIDs = append ( failedIDs , id )
}
}
return failedIDs , nil
}
// BatchUpdateStatus 批量更新帖子审核状态
func ( r * PostRepository ) BatchUpdateStatus ( ids [ ] string , status model . PostStatus , reviewedBy string ) ( [ ] string , error ) {
var failedIDs [ ] string
for _ , id := range ids {
if err := r . UpdateModerationStatus ( id , status , "" , reviewedBy ) ; err != nil {
failedIDs = append ( failedIDs , id )
}
}
return failedIDs , nil
}
// UpdatePinStatus 更新帖子置顶状态
func ( r * PostRepository ) UpdatePinStatus ( postID string , isPinned bool ) error {
2026-03-23 01:06:41 +08:00
// 置顶状态属于管理操作, 不应影响帖子内容更新时间( updated_at)
2026-03-14 18:01:55 +08:00
return r . db . Model ( & model . Post { } ) . Where ( "id = ?" , postID ) .
2026-03-23 01:06:41 +08:00
UpdateColumn ( "is_pinned" , isPinned ) . Error
2026-03-14 18:01:55 +08:00
}
// UpdateFeatureStatus 更新帖子加精状态
func ( r * PostRepository ) UpdateFeatureStatus ( postID string , isFeatured bool ) error {
2026-03-23 01:06:41 +08:00
// 加精状态属于管理操作, 不应影响帖子内容更新时间( updated_at)
2026-03-14 18:01:55 +08:00
return r . db . Model ( & model . Post { } ) . Where ( "id = ?" , postID ) .
2026-03-23 01:06:41 +08:00
UpdateColumn ( "is_featured" , isFeatured ) . Error
2026-03-14 18:01:55 +08:00
}
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
2026-03-20 23:03:23 +08:00
// ========== 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 ) .
2026-03-21 02:24:11 +08:00
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
}
// 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 ) .
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
2026-03-20 23:03:23 +08:00
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
}