- Refactored repository structures to use interfaces instead of concrete types, enhancing flexibility and testability. - Updated various repository methods to accept interfaces, allowing for better dependency management. - Modified wire generation to accommodate new repository interfaces, ensuring proper service injection throughout the application. - Enhanced handler methods to utilize DTOs for filtering and data handling, improving code clarity and maintainability.
1275 lines
38 KiB
Go
1275 lines
38 KiB
Go
package repository
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"time"
|
||
|
||
"carrot_bbs/internal/model"
|
||
"carrot_bbs/internal/pkg/cursor"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// 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)
|
||
GetFollowingPosts(userID string, page, pageSize int) ([]*model.Post, int64, error)
|
||
GetHotPosts(page, pageSize int) ([]*model.Post, int64, error)
|
||
ListPublishedPostIDsSince(since time.Time, limit int) ([]string, error)
|
||
ListPinnedPublishedPostIDs() ([]string, error)
|
||
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)
|
||
GetFollowingPostsByCursor(ctx context.Context, userID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error)
|
||
GetHotPostsByCursor(ctx context.Context, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error)
|
||
}
|
||
|
||
// postRepository 帖子仓储实现
|
||
type postRepository struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// NewPostRepository 创建帖子仓储
|
||
func NewPostRepository(db *gorm.DB) PostRepository {
|
||
return &postRepository{db: db}
|
||
}
|
||
|
||
// getDB 获取数据库连接(优先使用 context 中的事务)
|
||
func (r *postRepository) getDB(ctx context.Context) *gorm.DB {
|
||
if tx := GetTxFromContext(ctx); tx != nil {
|
||
return tx
|
||
}
|
||
return r.db
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 创建图片记录
|
||
// 支持格式:["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]
|
||
}
|
||
|
||
image := &model.PostImage{
|
||
PostID: post.ID,
|
||
URL: url,
|
||
PreviewURL: previewURL,
|
||
PreviewURLLarge: previewURLLarge,
|
||
SortOrder: i,
|
||
}
|
||
if err := tx.Create(image).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 新建帖子:保证 updated_at 与 created_at 一致(避免历史 NULL/库默认值导致前端误判为已编辑)
|
||
return tx.Model(post).Where("id = ?", post.ID).UpdateColumn("updated_at", post.CreatedAt).Error
|
||
})
|
||
}
|
||
|
||
// 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 {
|
||
now := time.Now()
|
||
post.UpdatedAt = now
|
||
post.ContentEditedAt = &now
|
||
return r.db.Save(post).Error
|
||
}
|
||
|
||
// UpdateWithImages 更新帖子及其图片(images=nil 表示不更新图片)
|
||
func (r *postRepository) UpdateWithImages(post *model.Post, images *[]string) error {
|
||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||
now := time.Now()
|
||
post.UpdatedAt = now
|
||
post.ContentEditedAt = &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
|
||
})
|
||
}
|
||
|
||
// UpdateModerationStatus 更新帖子审核状态
|
||
func (r *postRepository) UpdateModerationStatus(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error {
|
||
// 审核状态更新属于管理操作,不应影响帖子内容更新时间(updated_at)
|
||
updates := map[string]interface{}{
|
||
"status": status,
|
||
"reviewed_at": gorm.Expr("CURRENT_TIMESTAMP"),
|
||
"reviewed_by": reviewedBy,
|
||
"reject_reason": rejectReason,
|
||
"updated_at": gorm.Expr("updated_at"), // 防止 MySQL ON UPDATE CURRENT_TIMESTAMP 误刷新
|
||
}
|
||
return r.db.Model(&model.Post{}).Where("id = ?", postID).UpdateColumns(updates).Error
|
||
}
|
||
|
||
// 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 分页获取帖子列表
|
||
// includePending=true 时,仅在指定 userID 下额外返回 pending(用于作者查看自己待审核帖子)
|
||
func (r *postRepository) List(page, pageSize int, userID string, includePending bool, channelID *string) ([]*model.Post, int64, error) {
|
||
var posts []*model.Post
|
||
var total int64
|
||
|
||
query := r.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)
|
||
}
|
||
if channelID != nil && *channelID != "" {
|
||
query = query.Where("channel_id = ?", *channelID)
|
||
}
|
||
|
||
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 获取用户帖子
|
||
func (r *postRepository) GetUserPosts(userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error) {
|
||
var posts []*model.Post
|
||
var total int64
|
||
|
||
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)
|
||
|
||
offset := (page - 1) * pageSize
|
||
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
|
||
|
||
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
|
||
}
|
||
|
||
// 增加帖子点赞数(热门排序由定时任务写 Redis ZSET / top_ids,不写库)
|
||
return tx.Model(&model.Post{}).Where("id = ?", postID).
|
||
UpdateColumns(map[string]any{
|
||
"likes_count": gorm.Expr("likes_count + 1"),
|
||
"updated_at": gorm.Expr("updated_at"),
|
||
}).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 {
|
||
return tx.Model(&model.Post{}).Where("id = ?", postID).
|
||
UpdateColumns(map[string]any{
|
||
"likes_count": gorm.Expr("likes_count - 1"),
|
||
"updated_at": gorm.Expr("updated_at"),
|
||
}).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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 增加帖子收藏数(显式固定 updated_at,避免 MySQL ON UPDATE 误刷新)
|
||
return tx.Model(&model.Post{}).Where("id = ?", postID).
|
||
UpdateColumns(map[string]any{
|
||
"favorites_count": gorm.Expr("favorites_count + 1"),
|
||
"updated_at": gorm.Expr("updated_at"),
|
||
}).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).
|
||
UpdateColumns(map[string]any{
|
||
"favorites_count": gorm.Expr("favorites_count - 1"),
|
||
"updated_at": gorm.Expr("updated_at"),
|
||
}).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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// IncrementViews 增加帖子观看量
|
||
func (r *postRepository) IncrementViews(postID string) error {
|
||
return r.db.Model(&model.Post{}).Where("id = ?", postID).
|
||
UpdateColumns(map[string]interface{}{
|
||
"views_count": gorm.Expr("views_count + 1"),
|
||
"updated_at": gorm.Expr("updated_at"),
|
||
}).Error
|
||
}
|
||
|
||
// IncrementShares 已发布帖子分享次数 +1,返回新的 shares_count;非已发布或不存在返回 gorm.ErrRecordNotFound
|
||
func (r *postRepository) IncrementShares(postID string) (int, error) {
|
||
var newCount int
|
||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||
res := tx.Model(&model.Post{}).
|
||
Where("id = ? AND status = ?", postID, model.PostStatusPublished).
|
||
Updates(map[string]interface{}{
|
||
"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
|
||
}
|
||
|
||
// 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 热门榜降级:Redis 未就绪时按最新发布排序
|
||
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("created_at DESC").
|
||
Find(&posts).Error
|
||
|
||
return posts, total, err
|
||
}
|
||
|
||
// 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(热门候选池,避免全表扫快照)
|
||
func (r *postRepository) ListPublishedPostIDsSince(since time.Time, limit int) ([]string, error) {
|
||
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(越新发布的置顶越靠前)
|
||
func (r *postRepository) ListPinnedPublishedPostIDs() ([]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 = ?", 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
|
||
}
|
||
|
||
// GetPostHotSnapshotsByIDs 仅拉取候选 ID 对应的热度字段(分批 IN 查询)
|
||
func (r *postRepository) GetPostHotSnapshotsByIDs(ids []string) ([]PostHotSnapshot, error) {
|
||
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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// ========== 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 tx.Model(post).Where("id = ?", post.ID).UpdateColumn("updated_at", post.CreatedAt).Error
|
||
}
|
||
|
||
// 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 {
|
||
now := time.Now()
|
||
post.UpdatedAt = now
|
||
post.ContentEditedAt = &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
|
||
}
|
||
|
||
// ========== 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 批量删除帖子(使用单次事务批量删除,避免 N+1 问题)
|
||
func (r *postRepository) BatchDelete(ids []string) ([]string, error) {
|
||
if len(ids) == 0 {
|
||
return nil, nil
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
// 批量删除帖子点赞记录
|
||
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
|
||
}
|
||
|
||
// BatchUpdateStatus 批量更新帖子审核状态(使用单条 SQL 批量更新,避免 N+1 问题)
|
||
func (r *postRepository) BatchUpdateStatus(ids []string, status model.PostStatus, reviewedBy string) ([]string, error) {
|
||
if len(ids) == 0 {
|
||
return nil, nil
|
||
}
|
||
|
||
// 使用单条 SQL 批量更新
|
||
result := r.db.Model(&model.Post{}).
|
||
Where("id IN ?", ids).
|
||
Updates(map[string]any{
|
||
"status": status,
|
||
"reviewed_at": gorm.Expr("CURRENT_TIMESTAMP"),
|
||
"reviewed_by": reviewedBy,
|
||
"reject_reason": "",
|
||
"updated_at": gorm.Expr("updated_at"),
|
||
})
|
||
|
||
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)
|
||
}
|
||
}
|
||
return failedIDs, nil
|
||
}
|
||
|
||
return nil, nil
|
||
}
|
||
|
||
// UpdatePinStatus 更新帖子置顶状态
|
||
func (r *postRepository) UpdatePinStatus(postID string, isPinned bool) error {
|
||
// 置顶状态属于管理操作,不应影响帖子内容更新时间(updated_at)
|
||
return r.db.Model(&model.Post{}).Where("id = ?", postID).
|
||
UpdateColumns(map[string]any{
|
||
"is_pinned": isPinned,
|
||
"updated_at": gorm.Expr("updated_at"),
|
||
}).Error
|
||
}
|
||
|
||
// UpdateFeatureStatus 更新帖子加精状态
|
||
func (r *postRepository) UpdateFeatureStatus(postID string, isFeatured bool) error {
|
||
// 加精状态属于管理操作,不应影响帖子内容更新时间(updated_at)
|
||
return r.db.Model(&model.Post{}).Where("id = ?", postID).
|
||
UpdateColumns(map[string]any{
|
||
"is_featured": isFeatured,
|
||
"updated_at": gorm.Expr("updated_at"),
|
||
}).Error
|
||
}
|
||
|
||
// ========== Cursor Pagination Methods ==========
|
||
|
||
// GetPostsByCursor 游标分页获取帖子列表
|
||
// includePending=true 时,仅在指定 userID 下额外返回 pending(用于作者查看自己待审核帖子)
|
||
func (r *postRepository) GetPostsByCursor(ctx context.Context, userID string, includePending bool, channelID *string, 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)
|
||
}
|
||
if channelID != nil && *channelID != "" {
|
||
query = query.Where("channel_id = ?", *channelID)
|
||
}
|
||
|
||
// 使用游标构建器
|
||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||
WithCursor(req.Cursor, req.Direction).
|
||
WithPageSize(req.PageSize)
|
||
|
||
if builder.Error() != nil {
|
||
// 无效游标,返回空列表
|
||
return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil
|
||
}
|
||
|
||
// 执行查询
|
||
var posts []*model.Post
|
||
query = builder.Build().Preload("User").Preload("Images")
|
||
if err := query.Find(&posts).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 构建响应
|
||
pageSize := builder.GetPageSize()
|
||
hasMore := cursor.HasMore(len(posts), pageSize)
|
||
if hasMore {
|
||
posts = posts[:pageSize]
|
||
}
|
||
|
||
// 生成游标
|
||
var nextCursor, prevCursor string
|
||
if len(posts) > 0 {
|
||
// 下一页游标
|
||
if hasMore {
|
||
lastPost := posts[len(posts)-1]
|
||
nextCursor = cursor.NewCursor(
|
||
cursor.FormatTime(lastPost.CreatedAt),
|
||
lastPost.ID,
|
||
cursor.SortByCreatedAtDesc,
|
||
).Encode()
|
||
}
|
||
// 上一页游标
|
||
firstPost := posts[0]
|
||
prevCursor = cursor.NewCursor(
|
||
cursor.FormatTime(firstPost.CreatedAt),
|
||
firstPost.ID,
|
||
cursor.SortByCreatedAtDesc,
|
||
).Encode()
|
||
}
|
||
|
||
return cursor.NewCursorPageResult(posts, nextCursor, prevCursor, hasMore), nil
|
||
}
|
||
|
||
// SearchPostsByCursor 游标分页搜索帖子
|
||
func (r *postRepository) SearchPostsByCursor(ctx context.Context, keyword string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
|
||
db := r.getDB(ctx).WithContext(ctx)
|
||
|
||
query := db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished)
|
||
|
||
// 搜索标题和内容
|
||
if keyword != "" {
|
||
if r.db.Dialector.Name() == "postgres" {
|
||
// PostgreSQL 使用全文检索表达式
|
||
query = query.Where(
|
||
"to_tsvector('simple', COALESCE(title, '') || ' ' || COALESCE(content, '')) @@ plainto_tsquery('simple', ?)",
|
||
keyword,
|
||
)
|
||
} else {
|
||
searchPattern := "%" + keyword + "%"
|
||
query = query.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern)
|
||
}
|
||
}
|
||
|
||
// 使用游标构建器
|
||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||
WithCursor(req.Cursor, req.Direction).
|
||
WithPageSize(req.PageSize)
|
||
|
||
if builder.Error() != nil {
|
||
// 无效游标,返回空列表
|
||
return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil
|
||
}
|
||
|
||
// 执行查询
|
||
var posts []*model.Post
|
||
query = builder.Build().Preload("User").Preload("Images")
|
||
if err := query.Find(&posts).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 构建响应
|
||
pageSize := builder.GetPageSize()
|
||
hasMore := cursor.HasMore(len(posts), pageSize)
|
||
if hasMore {
|
||
posts = posts[:pageSize]
|
||
}
|
||
|
||
// 生成游标
|
||
var nextCursor, prevCursor string
|
||
if len(posts) > 0 {
|
||
if hasMore {
|
||
lastPost := posts[len(posts)-1]
|
||
nextCursor = cursor.NewCursor(
|
||
cursor.FormatTime(lastPost.CreatedAt),
|
||
lastPost.ID,
|
||
cursor.SortByCreatedAtDesc,
|
||
).Encode()
|
||
}
|
||
firstPost := posts[0]
|
||
prevCursor = cursor.NewCursor(
|
||
cursor.FormatTime(firstPost.CreatedAt),
|
||
firstPost.ID,
|
||
cursor.SortByCreatedAtDesc,
|
||
).Encode()
|
||
}
|
||
|
||
return cursor.NewCursorPageResult(posts, nextCursor, prevCursor, hasMore), nil
|
||
}
|
||
|
||
// GetUserPostsByCursor 游标分页获取用户帖子
|
||
func (r *postRepository) GetUserPostsByCursor(ctx context.Context, userID string, includePending bool, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Post], error) {
|
||
db := r.getDB(ctx).WithContext(ctx)
|
||
|
||
query := db.Model(&model.Post{}).Where("user_id = ?", userID)
|
||
if includePending {
|
||
query = query.Where("status IN ?", []model.PostStatus{
|
||
model.PostStatusPublished,
|
||
model.PostStatusPending,
|
||
})
|
||
} else {
|
||
query = query.Where("status = ?", model.PostStatusPublished)
|
||
}
|
||
|
||
// 使用游标构建器
|
||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||
WithCursor(req.Cursor, req.Direction).
|
||
WithPageSize(req.PageSize)
|
||
|
||
if builder.Error() != nil {
|
||
// 无效游标,返回空列表
|
||
return cursor.NewCursorPageResult[*model.Post]([]*model.Post{}, "", "", false), nil
|
||
}
|
||
|
||
// 执行查询
|
||
var posts []*model.Post
|
||
query = builder.Build().Preload("User").Preload("Images")
|
||
if err := query.Find(&posts).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 构建响应
|
||
pageSize := builder.GetPageSize()
|
||
hasMore := cursor.HasMore(len(posts), pageSize)
|
||
if hasMore {
|
||
posts = posts[:pageSize]
|
||
}
|
||
|
||
// 生成游标
|
||
var nextCursor, prevCursor string
|
||
if len(posts) > 0 {
|
||
if hasMore {
|
||
lastPost := posts[len(posts)-1]
|
||
nextCursor = cursor.NewCursor(
|
||
cursor.FormatTime(lastPost.CreatedAt),
|
||
lastPost.ID,
|
||
cursor.SortByCreatedAtDesc,
|
||
).Encode()
|
||
}
|
||
firstPost := posts[0]
|
||
prevCursor = cursor.NewCursor(
|
||
cursor.FormatTime(firstPost.CreatedAt),
|
||
firstPost.ID,
|
||
cursor.SortByCreatedAtDesc,
|
||
).Encode()
|
||
}
|
||
|
||
return cursor.NewCursorPageResult(posts, nextCursor, prevCursor, hasMore), nil
|
||
}
|
||
|
||
// 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)
|
||
|
||
// 热门游标降级:与 GetHotPosts 一致,按最新发布排序
|
||
query := db.Model(&model.Post{}).Where("status = ?", model.PostStatusPublished).
|
||
Order("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
|
||
}
|