- Removed Gorse-related configurations, handlers, and dependencies from the codebase. - Introduced HotRank feature with configuration options for ranking posts based on recent activity. - Updated application structure to support HotRank processing, including new caching mechanisms and database interactions. - Cleaned up related DTOs and repository methods to reflect the removal of Gorse and the addition of HotRank functionality.
577 lines
17 KiB
Go
577 lines
17 KiB
Go
package repository
|
||
|
||
import (
|
||
"carrot_bbs/internal/model"
|
||
"carrot_bbs/internal/pkg/cursor"
|
||
"context"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
// CommentRepository 评论仓储
|
||
type CommentRepository struct {
|
||
db *gorm.DB
|
||
}
|
||
|
||
// NewCommentRepository 创建评论仓储
|
||
func NewCommentRepository(db *gorm.DB) *CommentRepository {
|
||
return &CommentRepository{db: db}
|
||
}
|
||
|
||
// Create 创建评论
|
||
func (r *CommentRepository) Create(comment *model.Comment) error {
|
||
return r.db.Create(comment).Error
|
||
}
|
||
|
||
// GetByID 根据ID获取评论
|
||
func (r *CommentRepository) GetByID(id string) (*model.Comment, error) {
|
||
var comment model.Comment
|
||
err := r.db.Preload("User").First(&comment, "id = ?", id).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &comment, nil
|
||
}
|
||
|
||
// Update 更新评论
|
||
func (r *CommentRepository) Update(comment *model.Comment) error {
|
||
return r.db.Save(comment).Error
|
||
}
|
||
|
||
// UpdateModerationStatus 更新评论审核状态
|
||
func (r *CommentRepository) UpdateModerationStatus(commentID string, status model.CommentStatus) error {
|
||
// 审核状态更新属于管理操作,不应影响评论内容更新时间(updated_at)
|
||
return r.db.Model(&model.Comment{}).
|
||
Where("id = ?", commentID).
|
||
UpdateColumn("status", status).Error
|
||
}
|
||
|
||
// Delete 删除评论(软删除,同时清理关联数据)
|
||
func (r *CommentRepository) Delete(id string) error {
|
||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||
// 先查询评论获取post_id和parent_id
|
||
var comment model.Comment
|
||
if err := tx.First(&comment, "id = ?", id).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// 删除评论点赞记录
|
||
if err := tx.Where("comment_id = ?", id).Delete(&model.CommentLike{}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// 删除评论(软删除)
|
||
if err := tx.Delete(&model.Comment{}, "id = ?", id).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// 仅已发布评论才参与统计,避免 pending/rejected 影响计数
|
||
if comment.Status == model.CommentStatusPublished {
|
||
// 减少帖子的评论数并同步热度分
|
||
// 评论数属于统计字段,不应影响帖子内容更新时间(updated_at)
|
||
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
|
||
UpdateColumns(map[string]interface{}{
|
||
"comments_count": gorm.Expr("comments_count - 1"),
|
||
"updated_at": gorm.Expr("updated_at"),
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// 如果是回复,减少父评论的回复数
|
||
if comment.ParentID != nil && *comment.ParentID != "" {
|
||
if err := tx.Model(&model.Comment{}).Where("id = ?", *comment.ParentID).
|
||
UpdateColumn("replies_count", gorm.Expr("replies_count - 1")).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// ApplyPublishedStats 在评论审核通过后更新帖子评论数/回复数
|
||
func (r *CommentRepository) ApplyPublishedStats(comment *model.Comment) error {
|
||
if comment == nil {
|
||
return nil
|
||
}
|
||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||
// 增加帖子的评论数并同步热度分
|
||
// 评论数属于统计字段,不应影响帖子内容更新时间(updated_at)
|
||
if err := tx.Model(&model.Post{}).Where("id = ?", comment.PostID).
|
||
UpdateColumns(map[string]any{
|
||
"comments_count": gorm.Expr("comments_count + 1"),
|
||
"updated_at": gorm.Expr("updated_at"),
|
||
}).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
// 如果是回复,增加父评论的回复数
|
||
if comment.ParentID != nil && *comment.ParentID != "" {
|
||
if err := tx.Model(&model.Comment{}).Where("id = ?", *comment.ParentID).
|
||
UpdateColumn("replies_count", gorm.Expr("replies_count + 1")).Error; err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
}
|
||
|
||
// GetByPostID 获取帖子评论
|
||
func (r *CommentRepository) GetByPostID(postID string, page, pageSize int) ([]*model.Comment, int64, error) {
|
||
var comments []*model.Comment
|
||
var total int64
|
||
|
||
r.db.Model(&model.Comment{}).Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).Count(&total)
|
||
|
||
offset := (page - 1) * pageSize
|
||
err := r.db.Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).
|
||
Preload("User").
|
||
Offset(offset).Limit(pageSize).
|
||
Order("created_at ASC").
|
||
Find(&comments).Error
|
||
|
||
return comments, total, err
|
||
}
|
||
|
||
// GetByPostIDWithReplies 获取帖子评论(包含回复,扁平化结构)
|
||
// 所有层级的回复都扁平化展示在顶级评论的 replies 中
|
||
func (r *CommentRepository) GetByPostIDWithReplies(postID string, page, pageSize, replyLimit int) ([]*model.Comment, int64, error) {
|
||
var comments []*model.Comment
|
||
var total int64
|
||
|
||
r.db.Model(&model.Comment{}).Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).Count(&total)
|
||
|
||
offset := (page - 1) * pageSize
|
||
err := r.db.Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished).
|
||
Preload("User").
|
||
Offset(offset).Limit(pageSize).
|
||
Order("created_at ASC").
|
||
Find(&comments).Error
|
||
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
if len(comments) == 0 {
|
||
return comments, total, nil
|
||
}
|
||
|
||
rootIDs := make([]string, 0, len(comments))
|
||
commentsByID := make(map[string]*model.Comment, len(comments))
|
||
for _, comment := range comments {
|
||
rootIDs = append(rootIDs, comment.ID)
|
||
commentsByID[comment.ID] = comment
|
||
}
|
||
|
||
// 批量加载所有回复,内存中按 root_id 分组并裁剪每个根评论的返回条数
|
||
var allReplies []*model.Comment
|
||
if err := r.db.Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
|
||
Preload("User").
|
||
Order("created_at ASC").
|
||
Find(&allReplies).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
repliesByRoot := make(map[string][]*model.Comment, len(rootIDs))
|
||
for _, reply := range allReplies {
|
||
if reply.RootID == nil {
|
||
continue
|
||
}
|
||
rootID := *reply.RootID
|
||
if replyLimit <= 0 || len(repliesByRoot[rootID]) < replyLimit {
|
||
repliesByRoot[rootID] = append(repliesByRoot[rootID], reply)
|
||
}
|
||
}
|
||
|
||
type replyCountRow struct {
|
||
RootID string
|
||
Total int64
|
||
}
|
||
var replyCountRows []replyCountRow
|
||
if err := r.db.Model(&model.Comment{}).
|
||
Select("root_id, COUNT(*) AS total").
|
||
Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
|
||
Group("root_id").
|
||
Scan(&replyCountRows).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
replyCountMap := make(map[string]int64, len(replyCountRows))
|
||
for _, row := range replyCountRows {
|
||
replyCountMap[row.RootID] = row.Total
|
||
}
|
||
|
||
for _, rootID := range rootIDs {
|
||
comment := commentsByID[rootID]
|
||
comment.Replies = repliesByRoot[rootID]
|
||
comment.RepliesCount = int(replyCountMap[rootID])
|
||
}
|
||
|
||
return comments, total, nil
|
||
}
|
||
|
||
// loadFlatReplies 加载评论的所有回复(扁平化,所有层级都在同一层)
|
||
func (r *CommentRepository) loadFlatReplies(rootComment *model.Comment, limit int) {
|
||
var allReplies []*model.Comment
|
||
|
||
// 查询所有以该评论为根评论的回复(不包括顶级评论本身)
|
||
r.db.Where("root_id = ? AND status = ?", rootComment.ID, model.CommentStatusPublished).
|
||
Preload("User").
|
||
Order("created_at ASC").
|
||
Limit(limit).
|
||
Find(&allReplies)
|
||
|
||
rootComment.Replies = allReplies
|
||
}
|
||
|
||
// GetRepliesByRootID 根据根评论ID分页获取回复(扁平化)
|
||
func (r *CommentRepository) GetRepliesByRootID(rootID string, page, pageSize int) ([]*model.Comment, int64, error) {
|
||
var replies []*model.Comment
|
||
var total int64
|
||
|
||
// 统计总数
|
||
r.db.Model(&model.Comment{}).Where("root_id = ? AND status = ?", rootID, model.CommentStatusPublished).Count(&total)
|
||
|
||
// 分页查询
|
||
offset := (page - 1) * pageSize
|
||
err := r.db.Where("root_id = ? AND status = ?", rootID, model.CommentStatusPublished).
|
||
Preload("User").
|
||
Order("created_at ASC").
|
||
Offset(offset).
|
||
Limit(pageSize).
|
||
Find(&replies).Error
|
||
|
||
return replies, total, err
|
||
}
|
||
|
||
// GetReplies 获取回复
|
||
func (r *CommentRepository) GetReplies(parentID string) ([]*model.Comment, error) {
|
||
var comments []*model.Comment
|
||
err := r.db.Where("parent_id = ? AND status = ?", parentID, model.CommentStatusPublished).
|
||
Preload("User").
|
||
Order("created_at ASC").
|
||
Find(&comments).Error
|
||
return comments, err
|
||
}
|
||
|
||
// Like 点赞评论
|
||
func (r *CommentRepository) Like(commentID, userID string) error {
|
||
// 检查是否已经点赞
|
||
var existing model.CommentLike
|
||
err := r.db.Where("comment_id = ? AND user_id = ?", commentID, userID).First(&existing).Error
|
||
if err == nil {
|
||
// 已经点赞
|
||
return nil
|
||
}
|
||
if err != gorm.ErrRecordNotFound {
|
||
return err
|
||
}
|
||
|
||
// 创建点赞记录
|
||
like := &model.CommentLike{
|
||
CommentID: commentID,
|
||
UserID: userID,
|
||
}
|
||
err = r.db.Create(like).Error
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 增加评论点赞数
|
||
return r.db.Model(&model.Comment{}).Where("id = ?", commentID).
|
||
UpdateColumn("likes_count", gorm.Expr("likes_count + 1")).Error
|
||
}
|
||
|
||
// Unlike 取消点赞评论
|
||
func (r *CommentRepository) Unlike(commentID, userID string) error {
|
||
result := r.db.Where("comment_id = ? AND user_id = ?", commentID, userID).Delete(&model.CommentLike{})
|
||
if result.Error != nil {
|
||
return result.Error
|
||
}
|
||
if result.RowsAffected > 0 {
|
||
// 减少评论点赞数
|
||
return r.db.Model(&model.Comment{}).Where("id = ?", commentID).
|
||
UpdateColumn("likes_count", gorm.Expr("likes_count - 1")).Error
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// IsLiked 检查是否已点赞
|
||
func (r *CommentRepository) IsLiked(commentID, userID string) bool {
|
||
var count int64
|
||
r.db.Model(&model.CommentLike{}).Where("comment_id = ? AND user_id = ?", commentID, userID).Count(&count)
|
||
return count > 0
|
||
}
|
||
|
||
// AdminCommentListFilter 管理端评论列表筛选条件
|
||
type AdminCommentListFilter struct {
|
||
Keyword string
|
||
PostID string
|
||
AuthorID string
|
||
Status string
|
||
StartDate string
|
||
EndDate string
|
||
Page int
|
||
PageSize int
|
||
}
|
||
|
||
// GetAdminCommentList 获取管理端评论列表
|
||
func (r *CommentRepository) GetAdminCommentList(filter AdminCommentListFilter) ([]*model.Comment, int64, error) {
|
||
var comments []*model.Comment
|
||
var total int64
|
||
|
||
query := r.db.Model(&model.Comment{}).Preload("User")
|
||
|
||
// 应用筛选条件
|
||
if filter.Keyword != "" {
|
||
query = query.Where("content LIKE ?", "%"+filter.Keyword+"%")
|
||
}
|
||
if filter.PostID != "" {
|
||
query = query.Where("post_id = ?", filter.PostID)
|
||
}
|
||
if filter.AuthorID != "" {
|
||
query = query.Where("user_id = ?", filter.AuthorID)
|
||
}
|
||
if filter.Status != "" {
|
||
query = query.Where("status = ?", filter.Status)
|
||
}
|
||
if filter.StartDate != "" {
|
||
query = query.Where("created_at >= ?", filter.StartDate)
|
||
}
|
||
if filter.EndDate != "" {
|
||
query = query.Where("created_at <= ?", filter.EndDate+" 23:59:59")
|
||
}
|
||
|
||
// 统计总数
|
||
if err := query.Count(&total).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
// 分页查询
|
||
offset := (filter.Page - 1) * filter.PageSize
|
||
if err := query.Offset(offset).Limit(filter.PageSize).Order("created_at DESC").Find(&comments).Error; err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
return comments, total, nil
|
||
}
|
||
|
||
// GetAdminCommentByID 获取管理端评论详情(包含关联信息)
|
||
func (r *CommentRepository) GetAdminCommentByID(id string) (*model.Comment, error) {
|
||
var comment model.Comment
|
||
err := r.db.Preload("User").First(&comment, "id = ?", id).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &comment, nil
|
||
}
|
||
|
||
// BatchDelete 批量删除评论
|
||
func (r *CommentRepository) 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
|
||
}
|
||
|
||
// GetCommentsByPostIDForAdmin 管理端获取帖子的评论列表(包含所有状态)
|
||
func (r *CommentRepository) GetCommentsByPostIDForAdmin(postID string, page, pageSize int) ([]*model.Comment, int64, error) {
|
||
var comments []*model.Comment
|
||
var total int64
|
||
|
||
r.db.Model(&model.Comment{}).Where("post_id = ?", postID).Count(&total)
|
||
|
||
offset := (page - 1) * pageSize
|
||
err := r.db.Where("post_id = ?", postID).
|
||
Preload("User").
|
||
Offset(offset).Limit(pageSize).
|
||
Order("created_at DESC").
|
||
Find(&comments).Error
|
||
|
||
return comments, total, err
|
||
}
|
||
|
||
// ========== Cursor Pagination Methods ==========
|
||
|
||
// GetCommentsByCursor 游标分页获取帖子评论(包含回复)
|
||
// 排序方式:created_at DESC
|
||
func (r *CommentRepository) GetCommentsByCursor(ctx context.Context, postID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
|
||
// 构建基础查询 - 只查询顶级评论
|
||
query := r.db.WithContext(ctx).Model(&model.Comment{}).
|
||
Where("post_id = ? AND parent_id IS NULL AND status = ?", postID, model.CommentStatusPublished)
|
||
|
||
// 使用游标构建器
|
||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||
WithCursor(req.Cursor, req.Direction).
|
||
WithPageSize(req.PageSize)
|
||
|
||
if builder.Error() != nil {
|
||
// 无效游标,返回空列表
|
||
return cursor.NewCursorPageResult[*model.Comment]([]*model.Comment{}, "", "", false), nil
|
||
}
|
||
|
||
// 执行查询
|
||
var comments []*model.Comment
|
||
query = builder.Build().Preload("User")
|
||
if err := query.Find(&comments).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 构建响应
|
||
pageSize := builder.GetPageSize()
|
||
hasMore := cursor.HasMore(len(comments), pageSize)
|
||
if hasMore {
|
||
comments = comments[:pageSize]
|
||
}
|
||
|
||
// 加载每个评论的回复(默认加载前3条)
|
||
if len(comments) > 0 {
|
||
r.loadRepliesForComments(comments, 3)
|
||
}
|
||
|
||
// 生成游标
|
||
var nextCursor, prevCursor string
|
||
if len(comments) > 0 {
|
||
// 下一页游标
|
||
if hasMore {
|
||
lastComment := comments[len(comments)-1]
|
||
nextCursor = cursor.NewCursor(
|
||
cursor.FormatTime(lastComment.CreatedAt),
|
||
lastComment.ID,
|
||
cursor.SortByCreatedAtDesc,
|
||
).Encode()
|
||
}
|
||
// 上一页游标
|
||
firstComment := comments[0]
|
||
prevCursor = cursor.NewCursor(
|
||
cursor.FormatTime(firstComment.CreatedAt),
|
||
firstComment.ID,
|
||
cursor.SortByCreatedAtDesc,
|
||
).Encode()
|
||
}
|
||
|
||
return cursor.NewCursorPageResult(comments, nextCursor, prevCursor, hasMore), nil
|
||
}
|
||
|
||
// loadRepliesForComments 为评论列表加载回复
|
||
func (r *CommentRepository) loadRepliesForComments(comments []*model.Comment, replyLimit int) {
|
||
if len(comments) == 0 {
|
||
return
|
||
}
|
||
|
||
rootIDs := make([]string, 0, len(comments))
|
||
commentsByID := make(map[string]*model.Comment, len(comments))
|
||
for _, comment := range comments {
|
||
rootIDs = append(rootIDs, comment.ID)
|
||
commentsByID[comment.ID] = comment
|
||
}
|
||
|
||
// 批量加载所有回复
|
||
var allReplies []*model.Comment
|
||
if err := r.db.Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
|
||
Preload("User").
|
||
Order("created_at ASC").
|
||
Find(&allReplies).Error; err != nil {
|
||
return
|
||
}
|
||
|
||
// 按 root_id 分组并裁剪
|
||
repliesByRoot := make(map[string][]*model.Comment, len(rootIDs))
|
||
for _, reply := range allReplies {
|
||
if reply.RootID == nil {
|
||
continue
|
||
}
|
||
rootID := *reply.RootID
|
||
if replyLimit <= 0 || len(repliesByRoot[rootID]) < replyLimit {
|
||
repliesByRoot[rootID] = append(repliesByRoot[rootID], reply)
|
||
}
|
||
}
|
||
|
||
// 查询每个根评论的回复总数
|
||
type replyCountRow struct {
|
||
RootID string
|
||
Total int64
|
||
}
|
||
var replyCountRows []replyCountRow
|
||
if err := r.db.Model(&model.Comment{}).
|
||
Select("root_id, COUNT(*) AS total").
|
||
Where("root_id IN ? AND status = ?", rootIDs, model.CommentStatusPublished).
|
||
Group("root_id").
|
||
Scan(&replyCountRows).Error; err != nil {
|
||
return
|
||
}
|
||
|
||
replyCountMap := make(map[string]int64, len(replyCountRows))
|
||
for _, row := range replyCountRows {
|
||
replyCountMap[row.RootID] = row.Total
|
||
}
|
||
|
||
// 设置回复和回复数
|
||
for _, rootID := range rootIDs {
|
||
comment := commentsByID[rootID]
|
||
comment.Replies = repliesByRoot[rootID]
|
||
comment.RepliesCount = int(replyCountMap[rootID])
|
||
}
|
||
}
|
||
|
||
// GetRepliesByCursor 游标分页获取根评论的回复
|
||
// 排序方式:created_at ASC(回复按时间正序)
|
||
func (r *CommentRepository) GetRepliesByCursor(ctx context.Context, rootID string, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.Comment], error) {
|
||
// 构建基础查询
|
||
query := r.db.WithContext(ctx).Model(&model.Comment{}).
|
||
Where("root_id = ? AND status = ?", rootID, model.CommentStatusPublished)
|
||
|
||
// 使用游标构建器(回复按时间升序)
|
||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtAsc).
|
||
WithCursor(req.Cursor, req.Direction).
|
||
WithPageSize(req.PageSize)
|
||
|
||
if builder.Error() != nil {
|
||
// 无效游标,返回空列表
|
||
return cursor.NewCursorPageResult[*model.Comment]([]*model.Comment{}, "", "", false), nil
|
||
}
|
||
|
||
// 执行查询
|
||
var replies []*model.Comment
|
||
query = builder.Build().Preload("User")
|
||
if err := query.Find(&replies).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 构建响应
|
||
pageSize := builder.GetPageSize()
|
||
hasMore := cursor.HasMore(len(replies), pageSize)
|
||
if hasMore {
|
||
replies = replies[:pageSize]
|
||
}
|
||
|
||
// 生成游标
|
||
var nextCursor, prevCursor string
|
||
if len(replies) > 0 {
|
||
// 下一页游标
|
||
if hasMore {
|
||
lastReply := replies[len(replies)-1]
|
||
nextCursor = cursor.NewCursor(
|
||
cursor.FormatTime(lastReply.CreatedAt),
|
||
lastReply.ID,
|
||
cursor.SortByCreatedAtAsc,
|
||
).Encode()
|
||
}
|
||
// 上一页游标
|
||
firstReply := replies[0]
|
||
prevCursor = cursor.NewCursor(
|
||
cursor.FormatTime(firstReply.CreatedAt),
|
||
firstReply.ID,
|
||
cursor.SortByCreatedAtAsc,
|
||
).Encode()
|
||
}
|
||
|
||
return cursor.NewCursorPageResult(replies, nextCursor, prevCursor, hasMore), nil
|
||
}
|