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