Implement sensitive word filtering feature with configurable database and Redis support. Add security enhancements including WebSocket origin validation, improved password strength requirements, timing attack prevention, and production JWT secret validation. Add batch operation validation limits and optimize user blocking queries with subqueries. BREAKING CHANGE: Password validation now requires minimum 8 characters with uppercase, lowercase, and digit (was 6-50 characters without complexity requirements)
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]any{
|
||
"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]any{
|
||
"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]any{
|
||
"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{}, "", "", 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{}, "", "", 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{}, "", "", 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{}, "", "", 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{}, "", "", 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
|
||
}
|