2026-04-22 16:01:59 +08:00
package repository
2026-03-09 21:28:58 +08:00
import (
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
2026-04-22 16:01:59 +08:00
"with_you/internal/model"
"with_you/internal/pkg/cursor"
2026-04-30 12:26:25 +08:00
"with_you/internal/pkg/utils"
2026-03-24 05:18:30 +08:00
2026-03-09 21:28:58 +08:00
"gorm.io/gorm"
)
2026-03-26 18:14:16 +08:00
// PostRepository 帖子仓储接口
type PostRepository interface {
Create ( post * model . Post , images [ ] string ) error
GetByID ( id string ) ( * model . Post , error )
Update ( post * model . Post ) error
UpdateWithImages ( post * model . Post , images * [ ] string ) error
UpdateModerationStatus ( postID string , status model . PostStatus , rejectReason string , reviewedBy string ) error
Delete ( id string ) error
List ( page , pageSize int , userID string , includePending bool , channelID * string ) ( [ ] * model . Post , int64 , error )
GetUserPosts ( userID string , page , pageSize int , includePending bool ) ( [ ] * model . Post , int64 , error )
GetFavorites ( userID string , page , pageSize int ) ( [ ] * model . Post , int64 , error )
Like ( postID , userID string ) error
Unlike ( postID , userID string ) error
IsLiked ( postID , userID string ) bool
IsLikedBatch ( postIDs [ ] string , userID string ) map [ string ] bool
Favorite ( postID , userID string ) error
Unfavorite ( postID , userID string ) error
IsFavorited ( postID , userID string ) bool
IsFavoritedBatch ( postIDs [ ] string , userID string ) map [ string ] bool
IncrementViews ( postID string ) error
IncrementShares ( postID string ) ( int , error )
Search ( keyword string , page , pageSize int ) ( [ ] * model . Post , int64 , error )
2026-04-26 11:39:41 +08:00
GetFollowingPosts ( userID string , page , pageSize int , channelID * string ) ( [ ] * model . Post , int64 , error )
GetHotPosts ( page , pageSize int , channelID * string ) ( [ ] * model . Post , int64 , error )
2026-03-26 18:14:16 +08:00
ListPublishedPostIDsSince ( since time . Time , limit int ) ( [ ] string , error )
2026-04-26 11:39:41 +08:00
ListPublishedPostIDsByChannel ( channelID string , recentHours int , limit int ) ( [ ] string , error )
2026-03-26 18:14:16 +08:00
ListPinnedPublishedPostIDs ( ) ( [ ] string , error )
2026-04-26 11:39:41 +08:00
ListPinnedPublishedPostIDsByChannel ( channelID string ) ( [ ] string , error )
2026-03-26 18:14:16 +08:00
GetPostHotSnapshotsByIDs ( ids [ ] string ) ( [ ] PostHotSnapshot , error )
GetByIDs ( ids [ ] string ) ( [ ] * model . Post , error )
CreateWithContext ( ctx context . Context , post * model . Post , images [ ] string ) error
GetByIDWithContext ( ctx context . Context , id string ) ( * model . Post , error )
UpdateWithContext ( ctx context . Context , post * model . Post ) error
DeleteWithContext ( ctx context . Context , id string ) error
AdminList ( query AdminPostListQuery ) ( [ ] * model . Post , int64 , error )
GetByIDForAdmin ( id string ) ( * model . Post , error )
BatchDelete ( ids [ ] string ) ( [ ] string , error )
BatchUpdateStatus ( ids [ ] string , status model . PostStatus , reviewedBy string ) ( [ ] string , error )
UpdatePinStatus ( postID string , isPinned bool ) error
UpdateFeatureStatus ( postID string , isFeatured bool ) error
GetPostsByCursor ( ctx context . Context , userID string , includePending bool , channelID * string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Post ] , error )
SearchPostsByCursor ( ctx context . Context , keyword string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Post ] , error )
GetUserPostsByCursor ( ctx context . Context , userID string , includePending bool , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Post ] , error )
2026-04-26 11:39:41 +08:00
GetFollowingPostsByCursor ( ctx context . Context , userID string , channelID * string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Post ] , error )
GetHotPostsByCursor ( ctx context . Context , channelID * string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Post ] , error )
2026-03-26 18:14:16 +08:00
}
// postRepository 帖子仓储实现
type postRepository struct {
2026-03-09 21:28:58 +08:00
db * gorm . DB
}
// NewPostRepository 创建帖子仓储
2026-03-26 18:14:16 +08:00
func NewPostRepository ( db * gorm . DB ) PostRepository {
return & postRepository { db : db }
2026-03-09 21:28:58 +08:00
}
2026-03-13 09:38:18 +08:00
// getDB 获取数据库连接(优先使用 context 中的事务)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) getDB ( ctx context . Context ) * gorm . DB {
2026-03-13 09:38:18 +08:00
if tx := GetTxFromContext ( ctx ) ; tx != nil {
return tx
}
return r . db
}
2026-03-09 21:28:58 +08:00
// Create 创建帖子
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) Create ( post * model . Post , images [ ] string ) error {
2026-03-09 21:28:58 +08:00
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
}
}
2026-03-23 14:08:36 +08:00
// 新建帖子:保证 updated_at 与 created_at 一致(避免历史 NULL/库默认值导致前端误判为已编辑)
return tx . Model ( post ) . Where ( "id = ?" , post . ID ) . UpdateColumn ( "updated_at" , post . CreatedAt ) . Error
2026-03-09 21:28:58 +08:00
} )
}
// GetByID 根据ID获取帖子
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) GetByID ( id string ) ( * model . Post , error ) {
2026-03-09 21:28:58 +08:00
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 更新帖子
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) Update ( post * model . Post ) error {
2026-03-23 14:08:36 +08:00
now := time . Now ( )
post . UpdatedAt = now
post . ContentEditedAt = & 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 表示不更新图片)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) UpdateWithImages ( post * model . Post , images * [ ] string ) error {
2026-03-10 12:58:23 +08:00
return r . db . Transaction ( func ( tx * gorm . DB ) error {
2026-03-23 14:08:36 +08:00
now := time . Now ( )
post . UpdatedAt = now
post . ContentEditedAt = & now
2026-03-10 12:58:23 +08:00
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
}
2026-04-10 01:13:58 +08:00
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-10 12:58:23 +08:00
image := & model . PostImage {
2026-04-10 01:13:58 +08:00
PostID : post . ID ,
URL : url ,
PreviewURL : previewURL ,
PreviewURLLarge : previewURLLarge ,
SortOrder : i ,
2026-03-10 12:58:23 +08:00
}
if err := tx . Create ( image ) . Error ; err != nil {
return err
}
}
return nil
} )
}
2026-03-09 21:28:58 +08:00
// UpdateModerationStatus 更新帖子审核状态
2026-03-26 18:14:16 +08:00
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-30 04:49:35 +08:00
updates := map [ string ] any {
2026-03-09 21:28:58 +08:00
"status" : status ,
"reviewed_at" : gorm . Expr ( "CURRENT_TIMESTAMP" ) ,
"reviewed_by" : reviewedBy ,
"reject_reason" : rejectReason ,
2026-03-23 14:08:36 +08:00
"updated_at" : gorm . Expr ( "updated_at" ) , // 防止 MySQL ON UPDATE CURRENT_TIMESTAMP 误刷新
2026-03-09 21:28:58 +08:00
}
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 删除帖子(软删除,同时清理关联数据)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) Delete ( id string ) error {
2026-03-09 21:28:58 +08:00
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( 用于作者查看自己待审核帖子)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) List ( page , pageSize int , userID string , includePending bool , channelID * string ) ( [ ] * 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-24 22:27:53 +08:00
if channelID != nil && * channelID != "" {
query = query . Where ( "channel_id = ?" , * channelID )
}
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-26 18:14:16 +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 获取用户收藏
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) GetFavorites ( userID string , page , pageSize int ) ( [ ] * model . Post , int64 , error ) {
2026-03-09 21:28:58 +08:00
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 点赞帖子
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) Like ( postID , userID string ) error {
2026-03-09 21:28:58 +08:00
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-24 05:18:30 +08:00
// 增加帖子点赞数(热门排序由定时任务写 Redis ZSET / top_ids, 不写库)
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" ) ,
2026-03-23 14:08:36 +08:00
"updated_at" : gorm . Expr ( "updated_at" ) ,
2026-03-09 21:28:58 +08:00
} ) . Error
} )
}
// Unlike 取消点赞
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) Unlike ( postID , userID string ) error {
2026-03-09 21:28:58 +08:00
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 {
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" ) ,
2026-03-23 14:08:36 +08:00
"updated_at" : gorm . Expr ( "updated_at" ) ,
2026-03-09 21:28:58 +08:00
} ) . Error
}
return nil
} )
}
// IsLiked 检查是否点赞
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) IsLiked ( postID , userID string ) bool {
2026-03-09 21:28:58 +08:00
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
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) IsLikedBatch ( postIDs [ ] string , userID string ) map [ string ] bool {
2026-03-17 00:47:17 +08:00
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 收藏帖子
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) Favorite ( postID , userID string ) error {
2026-03-09 21:28:58 +08:00
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
}
2026-03-23 14:08:36 +08:00
// 增加帖子收藏数(显式固定 updated_at, 避免 MySQL ON UPDATE 误刷新)
2026-03-09 21:28:58 +08:00
return tx . Model ( & model . Post { } ) . Where ( "id = ?" , postID ) .
2026-03-23 14:08:36 +08:00
UpdateColumns ( map [ string ] any {
"favorites_count" : gorm . Expr ( "favorites_count + 1" ) ,
2026-03-24 05:18:30 +08:00
"updated_at" : gorm . Expr ( "updated_at" ) ,
2026-03-23 14:08:36 +08:00
} ) . Error
2026-03-09 21:28:58 +08:00
} )
}
// Unfavorite 取消收藏
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) Unfavorite ( postID , userID string ) error {
2026-03-09 21:28:58 +08:00
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 ) .
2026-03-23 14:08:36 +08:00
UpdateColumns ( map [ string ] any {
"favorites_count" : gorm . Expr ( "favorites_count - 1" ) ,
2026-03-24 05:18:30 +08:00
"updated_at" : gorm . Expr ( "updated_at" ) ,
2026-03-23 14:08:36 +08:00
} ) . Error
2026-03-09 21:28:58 +08:00
}
return nil
} )
}
// IsFavorited 检查是否收藏
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) IsFavorited ( postID , userID string ) bool {
2026-03-09 21:28:58 +08:00
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
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) IsFavoritedBatch ( postIDs [ ] string , userID string ) map [ string ] bool {
2026-03-17 00:47:17 +08:00
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 增加帖子观看量
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) IncrementViews ( postID string ) error {
2026-03-09 21:28:58 +08:00
return r . db . Model ( & model . Post { } ) . Where ( "id = ?" , postID ) .
2026-03-30 04:49:35 +08:00
UpdateColumns ( map [ string ] any {
2026-03-09 21:28:58 +08:00
"views_count" : gorm . Expr ( "views_count + 1" ) ,
2026-03-23 14:08:36 +08:00
"updated_at" : gorm . Expr ( "updated_at" ) ,
2026-03-09 21:28:58 +08:00
} ) . Error
}
2026-03-24 05:21:36 +08:00
// IncrementShares 已发布帖子分享次数 +1, 返回新的 shares_count; 非已发布或不存在返回 gorm.ErrRecordNotFound
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) IncrementShares ( postID string ) ( int , error ) {
2026-03-24 05:21:36 +08:00
var newCount int
err := r . db . Transaction ( func ( tx * gorm . DB ) error {
res := tx . Model ( & model . Post { } ) .
Where ( "id = ? AND status = ?" , postID , model . PostStatusPublished ) .
2026-03-30 04:49:35 +08:00
Updates ( map [ string ] any {
2026-03-24 05:21:36 +08:00
"shares_count" : gorm . Expr ( "shares_count + 1" ) ,
"updated_at" : gorm . Expr ( "updated_at" ) ,
} )
if res . Error != nil {
return res . Error
}
if res . RowsAffected == 0 {
return gorm . ErrRecordNotFound
}
var p model . Post
if err := tx . Select ( "shares_count" ) . Where ( "id = ?" , postID ) . First ( & p ) . Error ; err != nil {
return err
}
newCount = p . SharesCount
return nil
} )
return newCount , err
}
2026-03-09 21:28:58 +08:00
// Search 搜索帖子
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) Search ( keyword string , page , pageSize int ) ( [ ] * model . Post , int64 , error ) {
2026-03-09 21:28:58 +08:00
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" {
2026-04-30 12:26:25 +08:00
searchPattern := "%" + utils . EscapeLikeWildcard ( keyword ) + "%"
2026-04-28 14:53:04 +08:00
query = query . Where ( "title ILIKE ? OR content ILIKE ?" , searchPattern , searchPattern )
2026-03-09 21:28:58 +08:00
} else {
2026-04-30 12:26:25 +08:00
searchPattern := "%" + utils . EscapeLikeWildcard ( keyword ) + "%"
2026-03-09 21:28:58 +08:00
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 获取关注用户的帖子
2026-04-26 11:39:41 +08:00
func ( r * postRepository ) GetFollowingPosts ( userID string , page , pageSize int , channelID * string ) ( [ ] * model . Post , int64 , error ) {
2026-03-09 21:28:58 +08:00
var posts [ ] * model . Post
var total int64
subQuery := r . db . Model ( & model . Follow { } ) . Where ( "follower_id = ?" , userID ) . Select ( "following_id" )
2026-04-26 11:39:41 +08:00
baseQuery := r . db . Model ( & model . Post { } ) . Where ( "user_id IN (?) AND status = ?" , subQuery , model . PostStatusPublished )
if channelID != nil && * channelID != "" {
baseQuery = baseQuery . Where ( "channel_id = ?" , * channelID )
}
baseQuery . Count ( & total )
2026-03-09 21:28:58 +08:00
offset := ( page - 1 ) * pageSize
2026-04-26 11:39:41 +08:00
err := baseQuery .
2026-03-09 21:28:58 +08:00
Preload ( "User" ) . Preload ( "Images" ) .
Offset ( offset ) . Limit ( pageSize ) .
Order ( "created_at DESC" ) .
Find ( & posts ) . Error
return posts , total , err
}
2026-03-24 05:18:30 +08:00
// GetHotPosts 热门榜降级: Redis 未就绪时按最新发布排序
2026-04-26 11:39:41 +08:00
func ( r * postRepository ) GetHotPosts ( page , pageSize int , channelID * string ) ( [ ] * model . Post , int64 , error ) {
2026-03-09 21:28:58 +08:00
var posts [ ] * model . Post
var total int64
2026-04-26 11:39:41 +08:00
baseQuery := r . db . Model ( & model . Post { } ) . Where ( "status = ?" , model . PostStatusPublished )
if channelID != nil && * channelID != "" {
baseQuery = baseQuery . Where ( "channel_id = ?" , * channelID )
}
baseQuery . Count ( & total )
2026-03-09 21:28:58 +08:00
offset := ( page - 1 ) * pageSize
2026-04-26 11:39:41 +08:00
err := baseQuery . Preload ( "User" ) . Preload ( "Images" ) .
2026-03-09 21:28:58 +08:00
Offset ( offset ) . Limit ( pageSize ) .
2026-03-24 05:18:30 +08:00
Order ( "created_at DESC" ) .
2026-03-09 21:28:58 +08:00
Find ( & posts ) . Error
return posts , total , err
}
2026-03-24 05:18:30 +08:00
// PostHotSnapshot 热门重算用字段
type PostHotSnapshot struct {
ID string ` gorm:"column:id" `
LikesCount int ` gorm:"column:likes_count" `
CommentsCount int ` gorm:"column:comments_count" `
FavoritesCount int ` gorm:"column:favorites_count" `
SharesCount int ` gorm:"column:shares_count" `
ViewsCount int ` gorm:"column:views_count" `
CreatedAt time . Time ` gorm:"column:created_at" `
}
// ListPublishedPostIDsSince 按发布时间倒序取 ID( 热门候选池, 避免全表扫快照)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) ListPublishedPostIDsSince ( since time . Time , limit int ) ( [ ] string , error ) {
2026-03-24 05:18:30 +08:00
if limit <= 0 {
return nil , nil
}
type row struct {
ID string ` gorm:"column:id" `
}
var rows [ ] row
err := r . db . Model ( & model . Post { } ) .
Select ( "id" ) .
Where ( "status = ? AND created_at >= ?" , model . PostStatusPublished , since ) .
Order ( "created_at DESC" ) .
Limit ( limit ) .
Find ( & rows ) . Error
if err != nil {
return nil , err
}
out := make ( [ ] string , 0 , len ( rows ) )
for _ , x := range rows {
if x . ID != "" {
out = append ( out , x . ID )
}
}
return out , nil
}
// ListPinnedPublishedPostIDs 已发布且置顶的帖子 ID( 越新发布的置顶越靠前)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) ListPinnedPublishedPostIDs ( ) ( [ ] string , error ) {
2026-03-24 05:18:30 +08:00
type row struct {
ID string ` gorm:"column:id" `
}
var rows [ ] row
err := r . db . Model ( & model . Post { } ) .
Select ( "id" ) .
Where ( "status = ? AND is_pinned = ?" , model . PostStatusPublished , true ) .
Order ( "created_at DESC" ) .
Find ( & rows ) . Error
if err != nil {
return nil , err
}
out := make ( [ ] string , 0 , len ( rows ) )
for _ , x := range rows {
if x . ID != "" {
out = append ( out , x . ID )
}
}
return out , nil
}
2026-04-26 11:39:41 +08:00
func ( r * postRepository ) ListPublishedPostIDsByChannel ( channelID string , recentHours int , limit int ) ( [ ] string , error ) {
if limit <= 0 {
return nil , nil
}
type row struct {
ID string ` gorm:"column:id" `
}
var rows [ ] row
q := r . db . Model ( & model . Post { } ) .
Select ( "id" ) .
Where ( "status = ? AND channel_id = ?" , model . PostStatusPublished , channelID )
if recentHours > 0 {
since := time . Now ( ) . Add ( - time . Duration ( recentHours ) * time . Hour )
q = q . Where ( "created_at >= ?" , since )
}
err := q . Order ( "created_at DESC" ) .
Limit ( limit ) .
Find ( & rows ) . Error
if err != nil {
return nil , err
}
out := make ( [ ] string , 0 , len ( rows ) )
for _ , x := range rows {
if x . ID != "" {
out = append ( out , x . ID )
}
}
return out , nil
}
func ( r * postRepository ) ListPinnedPublishedPostIDsByChannel ( channelID string ) ( [ ] string , error ) {
type row struct {
ID string ` gorm:"column:id" `
}
var rows [ ] row
err := r . db . Model ( & model . Post { } ) .
Select ( "id" ) .
Where ( "status = ? AND is_pinned = ? AND channel_id = ?" , model . PostStatusPublished , true , channelID ) .
Order ( "created_at DESC" ) .
Find ( & rows ) . Error
if err != nil {
return nil , err
}
out := make ( [ ] string , 0 , len ( rows ) )
for _ , x := range rows {
if x . ID != "" {
out = append ( out , x . ID )
}
}
return out , nil
}
2026-03-24 05:18:30 +08:00
// GetPostHotSnapshotsByIDs 仅拉取候选 ID 对应的热度字段(分批 IN 查询)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) GetPostHotSnapshotsByIDs ( ids [ ] string ) ( [ ] PostHotSnapshot , error ) {
2026-03-24 05:18:30 +08:00
if len ( ids ) == 0 {
return nil , nil
}
const chunkSize = 400
var all [ ] PostHotSnapshot
for i := 0 ; i < len ( ids ) ; i += chunkSize {
end := i + chunkSize
if end > len ( ids ) {
end = len ( ids )
}
batch := ids [ i : end ]
var rows [ ] PostHotSnapshot
err := r . db . Model ( & model . Post { } ) .
Select ( "id" , "likes_count" , "comments_count" , "favorites_count" , "shares_count" , "views_count" , "created_at" ) .
Where ( "status = ? AND id IN ?" , model . PostStatusPublished , batch ) .
Find ( & rows ) . Error
if err != nil {
return nil , err
}
all = append ( all , rows ... )
}
return all , nil
}
2026-03-09 21:28:58 +08:00
// GetByIDs 根据ID列表获取帖子( 保持传入顺序)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) GetByIDs ( ids [ ] string ) ( [ ] * model . Post , error ) {
2026-03-09 21:28:58 +08:00
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 创建帖子(支持事务)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) CreateWithContext ( ctx context . Context , post * model . Post , images [ ] string ) error {
2026-03-13 09:38:18 +08:00
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 使用指定事务创建帖子
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) createWithTx ( tx * gorm . DB , post * model . Post , images [ ] string ) error {
2026-03-13 09:38:18 +08:00
// 创建帖子
if err := tx . Create ( post ) . Error ; err != nil {
return err
}
// 创建图片记录
2026-04-10 01:13:58 +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-13 09:38:18 +08:00
image := & model . PostImage {
2026-04-10 01:13:58 +08:00
PostID : post . ID ,
URL : url ,
PreviewURL : previewURL ,
PreviewURLLarge : previewURLLarge ,
SortOrder : i ,
2026-03-13 09:38:18 +08:00
}
if err := tx . Create ( image ) . Error ; err != nil {
return err
}
}
2026-03-23 14:08:36 +08:00
return tx . Model ( post ) . Where ( "id = ?" , post . ID ) . UpdateColumn ( "updated_at" , post . CreatedAt ) . Error
2026-03-13 09:38:18 +08:00
}
// GetByIDWithContext 根据ID获取帖子( 支持事务)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) GetByIDWithContext ( ctx context . Context , id string ) ( * model . Post , error ) {
2026-03-13 09:38:18 +08:00
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 更新帖子(支持事务)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) UpdateWithContext ( ctx context . Context , post * model . Post ) error {
2026-03-23 14:08:36 +08:00
now := time . Now ( )
post . UpdatedAt = now
post . ContentEditedAt = & now
2026-03-13 09:38:18 +08:00
return r . getDB ( ctx ) . WithContext ( ctx ) . Save ( post ) . Error
}
// DeleteWithContext 删除帖子(支持事务)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) DeleteWithContext ( ctx context . Context , id string ) error {
2026-03-13 09:38:18 +08:00
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 使用指定事务删除帖子
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) deleteWithTx ( tx * gorm . DB , id string ) error {
2026-03-13 09:38:18 +08:00
// 删除帖子图片
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 管理端分页获取帖子列表(支持多条件筛选)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) AdminList ( query AdminPostListQuery ) ( [ ] * model . Post , int64 , error ) {
2026-03-14 18:01:55 +08:00
var posts [ ] * model . Post
var total int64
db := r . db . Model ( & model . Post { } )
// 关键词搜索(标题或内容)
if query . Keyword != "" {
if r . db . Dialector . Name ( ) == "postgres" {
2026-04-30 12:26:25 +08:00
searchPattern := "%" + utils . EscapeLikeWildcard ( query . Keyword ) + "%"
2026-04-28 14:53:04 +08:00
db = db . Where ( "title ILIKE ? OR content ILIKE ?" , searchPattern , searchPattern )
2026-03-14 18:01:55 +08:00
} else {
2026-04-30 12:26:25 +08:00
searchPattern := "%" + utils . EscapeLikeWildcard ( query . Keyword ) + "%"
2026-03-14 18:01:55 +08:00
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获取帖子( 包含所有状态)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) GetByIDForAdmin ( id string ) ( * model . Post , error ) {
2026-03-14 18:01:55 +08:00
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
}
2026-03-26 01:50:54 +08:00
// BatchDelete 批量删除帖子(使用单次事务批量删除,避免 N+1 问题)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) BatchDelete ( ids [ ] string ) ( [ ] string , error ) {
2026-03-26 01:50:54 +08:00
if len ( ids ) == 0 {
return nil , nil
}
2026-03-14 18:01:55 +08:00
2026-03-26 01:50:54 +08:00
err := r . db . Transaction ( func ( tx * gorm . DB ) error {
// 批量删除帖子图片
if err := tx . Where ( "post_id IN ?" , ids ) . Delete ( & model . PostImage { } ) . Error ; err != nil {
return err
2026-03-14 18:01:55 +08:00
}
2026-03-26 01:50:54 +08:00
// 批量删除帖子点赞记录
if err := tx . Where ( "post_id IN ?" , ids ) . Delete ( & model . PostLike { } ) . Error ; err != nil {
return err
}
// 批量删除帖子收藏记录
if err := tx . Where ( "post_id IN ?" , ids ) . Delete ( & model . Favorite { } ) . Error ; err != nil {
return err
}
// 批量删除评论点赞记录( 子查询获取所有评论ID)
if err := tx . Where ( "comment_id IN (SELECT id FROM comments WHERE post_id IN ?)" , ids ) . Delete ( & model . CommentLike { } ) . Error ; err != nil {
return err
}
// 批量删除帖子评论
if err := tx . Where ( "post_id IN ?" , ids ) . Delete ( & model . Comment { } ) . Error ; err != nil {
return err
}
// 批量删除投票选项
if err := tx . Where ( "post_id IN ?" , ids ) . Delete ( & model . VoteOption { } ) . Error ; err != nil {
return err
}
// 批量删除用户投票记录
if err := tx . Where ( "post_id IN ?" , ids ) . Delete ( & model . UserVote { } ) . Error ; err != nil {
return err
}
// 最后批量删除帖子本身(软删除)
return tx . Where ( "id IN ?" , ids ) . Delete ( & model . Post { } ) . Error
} )
if err != nil {
return ids , err
}
return nil , nil
2026-03-14 18:01:55 +08:00
}
2026-03-26 01:50:54 +08:00
// BatchUpdateStatus 批量更新帖子审核状态(使用单条 SQL 批量更新,避免 N+1 问题)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) BatchUpdateStatus ( ids [ ] string , status model . PostStatus , reviewedBy string ) ( [ ] string , error ) {
2026-03-26 01:50:54 +08:00
if len ( ids ) == 0 {
return nil , nil
}
2026-03-14 18:01:55 +08:00
2026-03-26 01:50:54 +08:00
// 使用单条 SQL 批量更新
result := r . db . Model ( & model . Post { } ) .
Where ( "id IN ?" , ids ) .
Updates ( map [ string ] any {
2026-03-30 04:49:35 +08:00
"status" : status ,
"reviewed_at" : gorm . Expr ( "CURRENT_TIMESTAMP" ) ,
"reviewed_by" : reviewedBy ,
2026-03-26 01:50:54 +08:00
"reject_reason" : "" ,
2026-03-30 04:49:35 +08:00
"updated_at" : gorm . Expr ( "updated_at" ) ,
2026-03-26 01:50:54 +08:00
} )
if result . Error != nil {
return ids , result . Error
}
// 返回实际更新的帖子数量
if result . RowsAffected < int64 ( len ( ids ) ) {
// 部分帖子可能不存在,查询实际更新的 ID
var updatedIDs [ ] string
r . db . Model ( & model . Post { } ) . Where ( "id IN ?" , ids ) . Pluck ( "id" , & updatedIDs )
// 找出未更新的 ID
updatedMap := make ( map [ string ] bool , len ( updatedIDs ) )
for _ , id := range updatedIDs {
updatedMap [ id ] = true
}
var failedIDs [ ] string
for _ , id := range ids {
if ! updatedMap [ id ] {
failedIDs = append ( failedIDs , id )
}
2026-03-14 18:01:55 +08:00
}
2026-03-26 01:50:54 +08:00
return failedIDs , nil
2026-03-14 18:01:55 +08:00
}
2026-03-26 01:50:54 +08:00
return nil , nil
2026-03-14 18:01:55 +08:00
}
// UpdatePinStatus 更新帖子置顶状态
2026-03-26 18:14:16 +08:00
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 14:08:36 +08:00
UpdateColumns ( map [ string ] any {
"is_pinned" : isPinned ,
"updated_at" : gorm . Expr ( "updated_at" ) ,
} ) . Error
2026-03-14 18:01:55 +08:00
}
// UpdateFeatureStatus 更新帖子加精状态
2026-03-26 18:14:16 +08:00
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 14:08:36 +08:00
UpdateColumns ( map [ string ] any {
"is_featured" : isFeatured ,
"updated_at" : gorm . Expr ( "updated_at" ) ,
} ) . 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( 用于作者查看自己待审核帖子)
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) GetPostsByCursor ( ctx context . Context , userID string , includePending bool , channelID * string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Post ] , error ) {
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
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 )
}
2026-03-24 22:27:53 +08:00
if channelID != nil && * channelID != "" {
query = query . Where ( "channel_id = ?" , * channelID )
}
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
// 使用游标构建器
builder := cursor . NewBuilder ( query , cursor . SortByCreatedAtDesc ) .
WithCursor ( req . Cursor , req . Direction ) .
WithPageSize ( req . PageSize )
if builder . Error ( ) != nil {
// 无效游标,返回空列表
2026-04-07 00:07:40 +08:00
return cursor . NewCursorPageResult ( [ ] * model . Post { } , "" , "" , false ) , nil
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
}
// 执行查询
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 游标分页搜索帖子
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) SearchPostsByCursor ( ctx context . Context , keyword string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Post ] , error ) {
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
db := r . getDB ( ctx ) . WithContext ( ctx )
query := db . Model ( & model . Post { } ) . Where ( "status = ?" , model . PostStatusPublished )
// 搜索标题和内容
if keyword != "" {
if r . db . Dialector . Name ( ) == "postgres" {
2026-04-30 12:26:25 +08:00
searchPattern := "%" + utils . EscapeLikeWildcard ( keyword ) + "%"
2026-04-28 14:53:04 +08:00
query = query . Where ( "title ILIKE ? OR content ILIKE ?" , searchPattern , searchPattern )
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
} else {
2026-04-30 12:26:25 +08:00
searchPattern := "%" + utils . EscapeLikeWildcard ( keyword ) + "%"
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
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 {
// 无效游标,返回空列表
2026-04-07 00:07:40 +08:00
return cursor . NewCursorPageResult ( [ ] * model . Post { } , "" , "" , false ) , nil
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
}
// 执行查询
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 游标分页获取用户帖子
2026-03-26 18:14:16 +08:00
func ( r * postRepository ) GetUserPostsByCursor ( ctx context . Context , userID string , includePending bool , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Post ] , error ) {
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
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 {
// 无效游标,返回空列表
2026-04-07 00:07:40 +08:00
return cursor . NewCursorPageResult ( [ ] * model . Post { } , "" , "" , false ) , nil
2026-03-21 02:24:11 +08:00
}
// 执行查询
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 游标分页获取关注用户的帖子
2026-04-26 11:39:41 +08:00
func ( r * postRepository ) GetFollowingPostsByCursor ( ctx context . Context , userID string , channelID * string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Post ] , error ) {
2026-03-21 02:24:11 +08:00
db := r . getDB ( ctx ) . WithContext ( ctx )
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 )
2026-04-26 11:39:41 +08:00
if channelID != nil && * channelID != "" {
query = query . Where ( "channel_id = ?" , * channelID )
}
2026-03-21 02:24:11 +08:00
// 使用游标构建器
builder := cursor . NewBuilder ( query , cursor . SortByCreatedAtDesc ) .
WithCursor ( req . Cursor , req . Direction ) .
WithPageSize ( req . PageSize )
if builder . Error ( ) != nil {
// 无效游标,返回空列表
2026-04-07 00:07:40 +08:00
return cursor . NewCursorPageResult ( [ ] * model . Post { } , "" , "" , false ) , nil
2026-03-21 02:24:11 +08:00
}
// 执行查询
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 游标分页获取热门帖子
2026-04-26 11:39:41 +08:00
func ( r * postRepository ) GetHotPostsByCursor ( ctx context . Context , channelID * string , req * cursor . PageRequest ) ( * cursor . CursorPageResult [ * model . Post ] , error ) {
2026-03-21 02:24:11 +08:00
db := r . getDB ( ctx ) . WithContext ( ctx )
2026-04-26 11:39:41 +08:00
query := db . Model ( & model . Post { } ) . Where ( "status = ?" , model . PostStatusPublished )
if channelID != nil && * channelID != "" {
query = query . Where ( "channel_id = ?" , * channelID )
}
query = query . Order ( "created_at DESC" )
2026-03-21 02:24:11 +08:00
// 使用游标构建器
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 {
// 无效游标,返回空列表
2026-04-07 00:07:40 +08:00
return cursor . NewCursorPageResult ( [ ] * model . Post { } , "" , "" , false ) , nil
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
}
// 执行查询
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
}