辣评评论系统的构建与优化(三)

 

评论系统概述

评论系统是辣评平台的核心功能之一,用于用户对投稿作品的评价和讨论。系统需要支持评论的创建、展示、统计、排序等多种功能。

系统目标

  • 支持灵活的评论创建和管理
  • 提供高效的评论查询和展示
  • 实现准确的评论统计
  • 支持评论排行榜功能

评论数据模型设计

核心数据结构

// Comment 评论模型
type Comment struct {
    ID              uint      `gorm:"primaryKey"`
    UserID          uint      `gorm:"index"`
    SubmissionID    uint      `gorm:"index"`
    CompetitionID   uint      `gorm:"index"`
    Content         string    `gorm:"type:text"`
    Score           int       // 评分
    Status          string    // 评论状态
    CreatedAt       time.Time
    UpdatedAt       time.Time
    DeletedAt       gorm.DeletedAt `gorm:"index"`
    
    // 关联
    User            *User
    Submission      *Submission
    Competition     *Competition
}

// CommentStatistics 评论统计模型
type CommentStatistics struct {
    ID              uint
    UserID          uint
    CompetitionID   uint
    TotalComments   int       // 总评论数
    AverageScore    float64   // 平均评分
    HighQualityCount int      // 高质量评论数
    CreatedAt       time.Time
    UpdatedAt       time.Time
}

索引设计

// 创建复合索引以提升查询性能
db.Model(&Comment{}).
    AddIndex("idx_user_competition", "user_id", "competition_id").
    AddIndex("idx_submission_competition", "submission_id", "competition_id")

评论收集与处理逻辑

评论创建流程

// CreateComment 创建评论
func (s *CommentService) CreateComment(ctx context.Context, req *CreateCommentRequest) error {
    // 1. 验证权限
    if err := s.validateUserPermission(ctx, req.UserID); err != nil {
        return err
    }
    
    // 2. 验证投稿存在
    submission, err := s.getSubmission(ctx, req.SubmissionID)
    if err != nil {
        return err
    }
    
    // 3. 检查重复评论
    if err := s.checkDuplicateComment(ctx, req); err != nil {
        return err
    }
    
    // 4. 创建评论
    comment := &Comment{
        UserID:        req.UserID,
        SubmissionID:  req.SubmissionID,
        CompetitionID: submission.CompetitionID,
        Content:       req.Content,
        Score:         req.Score,
        Status:        "active",
    }
    
    // 5. 保存到数据库
    if err := s.db.Create(comment).Error; err != nil {
        return err
    }
    
    // 6. 更新统计数据
    s.updateCommentStatistics(ctx, req.UserID, submission.CompetitionID)
    
    return nil
}

评论验证规则

// ValidateComment 验证评论内容
func (s *CommentService) ValidateComment(comment *Comment) error {
    // 1. 检查内容长度
    if len(comment.Content) < 10 {
        return errors.New("评论内容过短")
    }
    if len(comment.Content) > 5000 {
        return errors.New("评论内容过长")
    }
    
    // 2. 检查评分范围
    if comment.Score < 0 || comment.Score > 5 {
        return errors.New("评分范围不正确")
    }
    
    // 3. 检查用户是否为投稿者
    if s.isAuthor(comment.UserID, comment.SubmissionID) {
        return errors.New("投稿者不能评论自己的作品")
    }
    
    return nil
}

评论统计与排行榜功能

统计计算

// CalculateCommentStatistics 计算评论统计
func (s *CommentService) CalculateCommentStatistics(ctx context.Context, userID, competitionID uint) error {
    // 1. 查询用户的所有评论
    var comments []Comment
    if err := s.db.Where("user_id = ? AND competition_id = ?", userID, competitionID).
        Find(&comments).Error; err != nil {
        return err
    }
    
    // 2. 计算统计数据
    totalComments := len(comments)
    var totalScore int
    highQualityCount := 0
    
    for _, comment := range comments {
        totalScore += comment.Score
        if comment.Score >= 4 {
            highQualityCount++
        }
    }
    
    averageScore := float64(totalScore) / float64(totalComments)
    
    // 3. 保存统计数据
    stats := &CommentStatistics{
        UserID:           userID,
        CompetitionID:    competitionID,
        TotalComments:    totalComments,
        AverageScore:     averageScore,
        HighQualityCount: highQualityCount,
    }
    
    return s.db.Save(stats).Error
}

排行榜生成

// GetLeaderboard 获取排行榜
func (s *CommentService) GetLeaderboard(ctx context.Context, competitionID uint, limit int) ([]LeaderboardEntry, error) {
    var entries []LeaderboardEntry
    
    // 按平均评分和评论数排序
    err := s.db.Table("comment_statistics").
        Select("user_id, total_comments, average_score").
        Where("competition_id = ?", competitionID).
        Order("average_score DESC, total_comments DESC").
        Limit(limit).
        Scan(&entries).Error
    
    return entries, err
}

重复评论处理机制

重复评论检测

// CheckDuplicateComment 检查重复评论
func (s *CommentService) CheckDuplicateComment(ctx context.Context, req *CreateCommentRequest) error {
    var count int64
    
    // 检查同一用户在同一投稿上是否已有评论
    err := s.db.Model(&Comment{}).
        Where("user_id = ? AND submission_id = ?", req.UserID, req.SubmissionID).
        Count(&count).Error
    
    if err != nil {
        return err
    }
    
    if count > 0 {
        return errors.New("您已经评论过此作品")
    }
    
    return nil
}

重复提交防护

// 使用幂等性 token 防止重复提交
type CreateCommentRequest struct {
    UserID       uint   `json:"user_id"`
    SubmissionID uint   `json:"submission_id"`
    Content      string `json:"content"`
    Score        int    `json:"score"`
    IdempotencyKey string `json:"idempotency_key"` // 幂等性 key
}

// 检查幂等性 key
func (s *CommentService) CheckIdempotency(ctx context.Context, key string) error {
    var count int64
    err := s.db.Model(&Comment{}).
        Where("idempotency_key = ?", key).
        Count(&count).Error
    
    if err != nil {
        return err
    }
    
    if count > 0 {
        return errors.New("请求已处理")
    }
    
    return nil
}

评论撤回与修复

评论撤回

// WithdrawComment 撤回评论
func (s *CommentService) WithdrawComment(ctx context.Context, commentID, userID uint) error {
    // 1. 验证权限
    comment := &Comment{}
    if err := s.db.First(comment, commentID).Error; err != nil {
        return err
    }
    
    if comment.UserID != userID {
        return errors.New("无权撤回此评论")
    }
    
    // 2. 检查撤回时间限制(例如:24小时内)
    if time.Since(comment.CreatedAt) > 24*time.Hour {
        return errors.New("超过撤回时间限制")
    }
    
    // 3. 标记为已撤回
    return s.db.Model(comment).Update("status", "withdrawn").Error
}

评论编辑

// EditComment 编辑评论
func (s *CommentService) EditComment(ctx context.Context, commentID, userID uint, newContent string) error {
    comment := &Comment{}
    
    // 1. 获取评论
    if err := s.db.First(comment, commentID).Error; err != nil {
        return err
    }
    
    // 2. 验证权限
    if comment.UserID != userID {
        return errors.New("无权编辑此评论")
    }
    
    // 3. 验证新内容
    if err := s.ValidateCommentContent(newContent); err != nil {
        return err
    }
    
    // 4. 更新评论
    return s.db.Model(comment).Update("content", newContent).Error
}

评论查询优化

高效查询

// GetCommentsBySubmission 获取投稿的所有评论
func (s *CommentService) GetCommentsBySubmission(ctx context.Context, submissionID uint, page, pageSize int) ([]Comment, int64, error) {
    var comments []Comment
    var total int64
    
    offset := (page - 1) * pageSize
    
    // 使用预加载优化查询
    query := s.db.Preload("User").
        Where("submission_id = ? AND status = ?", submissionID, "active").
        Order("created_at DESC")
    
    // 获取总数
    if err := query.Model(&Comment{}).Count(&total).Error; err != nil {
        return nil, 0, err
    }
    
    // 获取分页数据
    if err := query.Offset(offset).Limit(pageSize).Find(&comments).Error; err != nil {
        return nil, 0, err
    }
    
    return comments, total, nil
}

缓存策略

// 使用缓存减少数据库查询
func (s *CommentService) GetCommentStatisticsWithCache(ctx context.Context, userID, competitionID uint) (*CommentStatistics, error) {
    // 1. 尝试从缓存获取
    cacheKey := fmt.Sprintf("comment_stats:%d:%d", userID, competitionID)
    if cached, err := s.cache.Get(ctx, cacheKey); err == nil {
        return cached.(*CommentStatistics), nil
    }
    
    // 2. 从数据库查询
    stats := &CommentStatistics{}
    if err := s.db.Where("user_id = ? AND competition_id = ?", userID, competitionID).
        First(stats).Error; err != nil {
        return nil, err
    }
    
    // 3. 存入缓存(1小时过期)
    s.cache.Set(ctx, cacheKey, stats, time.Hour)
    
    return stats, nil
}

评论系统的 API 接口

创建评论

POST /api/comments
{
    "submission_id": 1,
    "content": "这是一条评论",
    "score": 4
}

获取评论列表

GET /api/submissions/{id}/comments?page=1&page_size=10

编辑评论

PUT /api/comments/{id}
{
    "content": "更新后的评论内容"
}

删除评论

DELETE /api/comments/{id}

获取排行榜

GET /api/competitions/{id}/leaderboard?limit=10

总结

评论系统的构建涉及多个方面的考虑:

关键设计原则

  • 数据一致性: 确保评论数据的准确性
  • 性能优化: 使用索引和缓存提升查询速度
  • 用户体验: 提供灵活的评论管理功能
  • 系统稳定性: 防止重复提交和异常情况

后续优化方向

  • 实现评论的点赞和回复功能
  • 添加评论内容审核机制
  • 实现评论的实时通知
  • 优化大数据量下的查询性能

评论系统的完善为辣评平台提供了强大的用户互动能力,是平台成功的重要基础。

本文遵守 Attribution-NonCommercial 4.0 International 许可协议。 Attribution-NonCommercial 4.0 International