Files
mce/internal/repository/post_repo.go
Victor_Jay 7cdb4d6698 refactor: GetOverview 5 次独立查询合并为单次 GROUP BY
- 新增 PostRepo.GetOverviewByUserID 单次 CASE WHEN + GROUP BY 查询
- Service 层 GetOverview 从 5 次 DB 查询减少为 1 次
- 接口 postStore 新增 GetOverviewByUserID 声明
2026-06-02 15:25:17 +08:00

270 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"time"
"metazone.cc/metalab/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
// PostRepo 帖子数据访问
type PostRepo struct {
db *gorm.DB
}
// NewPostRepo 构造函数
func NewPostRepo(db *gorm.DB) *PostRepo {
return &PostRepo{db: db}
}
// Create 创建帖子
func (r *PostRepo) Create(post *model.Post) error {
return r.db.Create(post).Error
}
// FindByID 按 ID 查找帖子
func (r *PostRepo) FindByID(id uint) (*model.Post, error) {
var post model.Post
err := r.db.First(&post, id).Error
if err != nil {
return nil, err
}
return &post, nil
}
// FindByIDWithAuthor 按 ID 查找帖子(含作者用户名)
func (r *PostRepo) FindByIDWithAuthor(id uint) (*model.Post, error) {
var post model.Post
err := r.db.Table("posts").
Select("posts.*, users.username as author_name").
Joins("LEFT JOIN users ON users.uid = posts.user_id").
Where("posts.id = ?", id).
First(&post).Error
if err != nil {
return nil, err
}
return &post, nil
}
// buildQuery 构建帖子查询公共部分(联表 + 未删除 + 关键词 + 可选状态过滤)
func (r *PostRepo) buildQuery(keyword, status string) *gorm.DB {
query := r.db.Table("posts").
Select("posts.*, users.username as author_name").
Joins("LEFT JOIN users ON users.uid = posts.user_id").
Where("posts.deleted_at IS NULL")
if keyword != "" {
like := "%" + keyword + "%"
query = query.Where("posts.title LIKE ?", like)
}
if status != "" {
query = query.Where("posts.status = ?", status)
}
return query
}
// FindPageable 分页查询帖子列表(仅 approved 状态,关键词模糊搜索标题)
func (r *PostRepo) FindPageable(keyword string, offset, limit int) ([]model.Post, int64, error) {
query := r.buildQuery(keyword, model.PostStatusApproved)
return r.pageResults(query, offset, limit)
}
// FindAdminPageable 管理后台分页查询(全状态筛选,待审置顶)
func (r *PostRepo) FindAdminPageable(keyword, status string, offset, limit int) ([]model.Post, int64, error) {
query := r.buildQuery(keyword, status)
return r.pageResultsAdmin(query, offset, limit)
}
// FindByUserID 分页查询某用户发布的帖子(仅 approved含作者用户名
func (r *PostRepo) FindByUserID(userID uint, offset, limit int) ([]model.Post, int64, error) {
query := r.buildQuery("", model.PostStatusApproved).
Where("posts.user_id = ?", userID)
return r.pageResults(query, offset, limit)
}
// CountUserPosts 统计某用户的已发布文章数
func (r *PostRepo) CountUserPosts(userID uint) (int64, error) {
var count int64
err := r.db.Model(&model.Post{}).
Where("user_id = ? AND status = ? AND deleted_at IS NULL", userID, model.PostStatusApproved).
Count(&count).Error
return count, err
}
// GetUserStats 获取用户的文章统计数据(总获赞/总收藏/总阅读)
func (r *PostRepo) GetUserStats(userID uint) (likes, favorites, reads int64, err error) {
type stats struct {
TotalLikes int64
TotalFavorites int64
TotalReads int64
}
var s stats
err = r.db.Model(&model.Post{}).
Select("COALESCE(SUM(likes_count), 0) AS total_likes, COALESCE(SUM(favorites_count), 0) AS total_favorites, COALESCE(SUM(views_count), 0) AS total_reads").
Where("user_id = ? AND status = ? AND deleted_at IS NULL", userID, model.PostStatusApproved).
Scan(&s).Error
return s.TotalLikes, s.TotalFavorites, s.TotalReads, err
}
// overviewRow GetOverviewByUserID 单次 GROUP BY 查询结果行
type overviewRow struct {
TotalPosts int64
DraftCount int64
PendingCount int64
ApprovedCount int64
RejectedCount int64
}
// GetOverviewByUserID 用单次 GROUP BY 查询聚合用户各状态文章数
func (r *PostRepo) GetOverviewByUserID(userID uint) (totalPosts, draftCount, pendingCount, approvedCount, rejectedCount int64, err error) {
var row overviewRow
err = r.db.Model(&model.Post{}).
Select(`
COUNT(*) AS total_posts,
COALESCE(SUM(CASE WHEN status = 'draft' THEN 1 ELSE 0 END), 0) AS draft_count,
COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0) AS pending_count,
COALESCE(SUM(CASE WHEN status = 'approved' THEN 1 ELSE 0 END), 0) AS approved_count,
COALESCE(SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END), 0) AS rejected_count
`).
Where("user_id = ? AND deleted_at IS NULL", userID).
Scan(&row).Error
return row.TotalPosts, row.DraftCount, row.PendingCount, row.ApprovedCount, row.RejectedCount, err
}
// FindByUserIDAndStatus 分页查询某用户指定状态的帖子(空 status=全状态)
func (r *PostRepo) FindByUserIDAndStatus(userID uint, status string, offset, limit int) ([]model.Post, int64, error) {
query := r.buildQuery("", status).
Where("posts.user_id = ?", userID)
return r.pageResults(query, offset, limit)
}
// FindPendingRevisions 查询有待审修订的帖子approved 且 pending_body 不为空)
func (r *PostRepo) FindPendingRevisions(keyword string, offset, limit int) ([]model.Post, int64, error) {
query := r.buildQuery(keyword, model.PostStatusApproved).
Where("posts.pending_body != ''")
return r.pageResults(query, offset, limit)
}
// pageResults 执行 Count + Offset/Limit + ORDER BY
func (r *PostRepo) pageResults(query *gorm.DB, offset, limit int) ([]model.Post, int64, error) {
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var posts []model.Post
err := query.Order("posts.created_at DESC").
Offset(offset).Limit(limit).Find(&posts).Error
if err != nil {
return nil, 0, err
}
return posts, total, nil
}
// pageResultsAdmin 管理后台分页待审稿件pending 或 修订待审)置顶优先
func (r *PostRepo) pageResultsAdmin(query *gorm.DB, offset, limit int) ([]model.Post, int64, error) {
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var posts []model.Post
err := query.Order(`CASE WHEN posts.status = 'pending' OR (posts.status = 'approved' AND posts.pending_body != '') THEN 0 ELSE 1 END ASC, posts.created_at DESC`).
Offset(offset).Limit(limit).Find(&posts).Error
if err != nil {
return nil, 0, err
}
return posts, total, nil
}
// Update 更新帖子
func (r *PostRepo) Update(post *model.Post) error {
return r.db.Save(post).Error
}
// SoftDelete 软删除帖子
func (r *PostRepo) SoftDelete(id uint) error {
return r.db.Delete(&model.Post{}, id).Error
}
// CountPostsByStatus 统计帖子数量status 为空则统计全部未删除帖子)
func (r *PostRepo) CountPostsByStatus(status string) (int64, error) {
query := r.buildQuery("", status)
var total int64
err := query.Count(&total).Error
return total, err
}
// CountPending 统计待审核帖子数pending 状态 + approved 但有待审修订)
func (r *PostRepo) CountPending() (int64, error) {
var total int64
err := r.db.Table("posts").
Where("deleted_at IS NULL").
Where("status = ? OR (status = ? AND pending_body != '')", model.PostStatusPending, model.PostStatusApproved).
Count(&total).Error
return total, err
}
// Restore 恢复软删除
func (r *PostRepo) Restore(id uint) error {
result := r.db.Unscoped().Model(&model.Post{}).Where("id = ?", id).Update("deleted_at", nil)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
// readCooldown 两次阅读记录最小间隔
const readCooldown = 2 * time.Second
// RecordRead 记录已登录用户阅读文章(防刷量:取去重+冷却)
// 返回 true 表示首次阅读新记录false 表示重复或冷却中
func (r *PostRepo) RecordRead(userID, postID uint) (bool, error) {
// 冷却检查:同用户两次阅读之间至少间隔 2 秒
var lastRead model.PostReadLog
if err := r.db.Where("user_id = ?", userID).
Order("read_at DESC").First(&lastRead).Error; err == nil {
if time.Since(lastRead.ReadAt) < readCooldown {
return false, nil
}
}
result := r.db.Clauses(clause.OnConflict{DoNothing: true}).
Create(&model.PostReadLog{UserID: userID, PostID: postID})
if result.Error != nil {
return false, result.Error
}
return result.RowsAffected > 0, nil
}
// RecordGuestRead 记录访客阅读文章(基于 Cookie visitor_id 去重+冷却)
// 返回 true 表示首次阅读新记录false 表示重复或冷却中
func (r *PostRepo) RecordGuestRead(visitorID string, postID uint) (bool, error) {
// 冷却检查:同访客两次阅读之间至少间隔 2 秒
var lastRead model.PostGuestReadLog
if err := r.db.Where("visitor_id = ?", visitorID).
Order("read_at DESC").First(&lastRead).Error; err == nil {
if time.Since(lastRead.ReadAt) < readCooldown {
return false, nil
}
}
result := r.db.Clauses(clause.OnConflict{DoNothing: true}).
Create(&model.PostGuestReadLog{VisitorID: visitorID, PostID: postID})
if result.Error != nil {
return false, result.Error
}
return result.RowsAffected > 0, nil
}
// IncrementViewsCount 阅读数 +1
func (r *PostRepo) IncrementViewsCount(postID uint) error {
return r.db.Model(&model.Post{}).Where("id = ?", postID).
Update("views_count", gorm.Expr("views_count + 1")).Error
}