辣评排行榜与星级评定功能(十三)

 

排行榜系统概述

排行榜系统是辣评平台的重要功能,用于展示用户的评论质量和活跃度排名。通过星级评定机制,系统能够识别和奖励高质量的评论者。

系统目标

  • 提供公平的排名机制
  • 实现灵活的排序功能
  • 支持多维度的筛选
  • 激励用户参与评论

排行榜数据模型

核心数据结构

// Leaderboard 排行榜模型
type Leaderboard struct {
    ID              uint      `gorm:"primaryKey"`
    UserID          uint      `gorm:"uniqueIndex:idx_user_competition"`
    CompetitionID   uint      `gorm:"uniqueIndex:idx_user_competition"`
    Rank            int       // 排名
    TotalComments   int       // 总评论数
    AverageScore    float64   // 平均评分
    HighQualityCount int      // 高质量评论数(评分>=4)
    TotalScore      int       // 总评分
    Points          int       // 积分(用于排名)
    LastUpdatedAt   time.Time
    CreatedAt       time.Time
    UpdatedAt       time.Time
    
    // 关联
    User            *User
    Competition     *Competition
}

// LeaderboardHistory 排行榜历史记录
type LeaderboardHistory struct {
    ID              uint
    LeaderboardID   uint
    Rank            int
    Points          int
    RecordedAt      time.Time
}

// UserRating 用户评分记录
type UserRating struct {
    ID              uint
    UserID          uint      `gorm:"index"`
    CompetitionID   uint      `gorm:"index"`
    TotalRatings    int       // 被评分的次数
    AverageRating   float64   // 平均被评分
    FiveStarCount   int       // 五星评分数
    FourStarCount   int       // 四星评分数
    ThreeStarCount  int       // 三星评分数
    TwoStarCount    int       // 二星评分数
    OneStarCount    int       // 一星评分数
    UpdatedAt       time.Time
}

排行榜计算逻辑

积分计算

// CalculateLeaderboardPoints 计算排行榜积分
func (s *LeaderboardService) CalculateLeaderboardPoints(ctx context.Context, userID, competitionID uint) (int, error) {
    // 1. 获取用户的评论统计
    var comments []Comment
    if err := s.db.Where("user_id = ? AND competition_id = ?", userID, competitionID).
        Find(&comments).Error; err != nil {
        return 0, err
    }
    
    if len(comments) == 0 {
        return 0, nil
    }
    
    // 2. 计算基础积分
    points := 0
    
    // 评论数积分:每条评论1分
    points += len(comments)
    
    // 评分积分:根据评分等级加分
    for _, comment := range comments {
        switch {
        case comment.Score >= 4:
            points += 5 // 高质量评论加5分
        case comment.Score >= 3:
            points += 2 // 中等质量评论加2分
        case comment.Score >= 2:
            points += 1 // 低质量评论加1分
        }
    }
    
    // 3. 计算平均评分奖励
    var totalScore int
    for _, comment := range comments {
        totalScore += comment.Score
    }
    averageScore := float64(totalScore) / float64(len(comments))
    
    // 平均评分>=4.5分额外加10分
    if averageScore >= 4.5 {
        points += 10
    } else if averageScore >= 4.0 {
        points += 5
    }
    
    // 4. 计算高质量评论比例奖励
    highQualityCount := 0
    for _, comment := range comments {
        if comment.Score >= 4 {
            highQualityCount++
        }
    }
    
    qualityRatio := float64(highQualityCount) / float64(len(comments))
    if qualityRatio >= 0.8 {
        points += 15 // 高质量评论比例>=80%加15分
    } else if qualityRatio >= 0.6 {
        points += 10
    } else if qualityRatio >= 0.4 {
        points += 5
    }
    
    return points, nil
}

// UpdateLeaderboard 更新排行榜
func (s *LeaderboardService) UpdateLeaderboard(ctx context.Context, competitionID uint) error {
    // 1. 获取所有参与该比赛的用户
    var userIDs []uint
    if err := s.db.Model(&Comment{}).
        Distinct("user_id").
        Where("competition_id = ?", competitionID).
        Pluck("user_id", &userIDs).Error; err != nil {
        return err
    }
    
    // 2. 为每个用户计算积分
    leaderboardEntries := make([]Leaderboard, 0)
    
    for _, userID := range userIDs {
        // 计算积分
        points, err := s.CalculateLeaderboardPoints(ctx, userID, competitionID)
        if err != nil {
            continue
        }
        
        // 获取评论统计
        var comments []Comment
        s.db.Where("user_id = ? AND competition_id = ?", userID, competitionID).
            Find(&comments)
        
        totalComments := len(comments)
        var totalScore int
        highQualityCount := 0
        
        for _, comment := range comments {
            totalScore += comment.Score
            if comment.Score >= 4 {
                highQualityCount++
            }
        }
        
        averageScore := 0.0
        if totalComments > 0 {
            averageScore = float64(totalScore) / float64(totalComments)
        }
        
        entry := Leaderboard{
            UserID:           userID,
            CompetitionID:    competitionID,
            TotalComments:    totalComments,
            AverageScore:     averageScore,
            HighQualityCount: highQualityCount,
            TotalScore:       totalScore,
            Points:           points,
            LastUpdatedAt:    time.Now(),
        }
        
        leaderboardEntries = append(leaderboardEntries, entry)
    }
    
    // 3. 按积分排序
    sort.Slice(leaderboardEntries, func(i, j int) bool {
        if leaderboardEntries[i].Points != leaderboardEntries[j].Points {
            return leaderboardEntries[i].Points > leaderboardEntries[j].Points
        }
        return leaderboardEntries[i].AverageScore > leaderboardEntries[j].AverageScore
    })
    
    // 4. 分配排名
    for i, entry := range leaderboardEntries {
        entry.Rank = i + 1
        
        // 查询是否已存在
        existing := &Leaderboard{}
        result := s.db.Where("user_id = ? AND competition_id = ?", entry.UserID, entry.CompetitionID).
            First(existing)
        
        if result.Error == gorm.ErrRecordNotFound {
            // 创建新记录
            s.db.Create(&entry)
        } else {
            // 更新现有记录
            s.db.Model(existing).Updates(entry)
            
            // 记录排名变化历史
            s.recordLeaderboardHistory(ctx, existing.ID, entry.Rank, entry.Points)
        }
    }
    
    return nil
}

// recordLeaderboardHistory 记录排行榜历史
func (s *LeaderboardService) recordLeaderboardHistory(ctx context.Context, leaderboardID uint, rank, points int) error {
    history := &LeaderboardHistory{
        LeaderboardID: leaderboardID,
        Rank:          rank,
        Points:        points,
        RecordedAt:    time.Now(),
    }
    
    return s.db.Create(history).Error
}

星级评定机制

评分系统

// RatingSystem 评分系统
type RatingSystem struct {
    MinScore int // 最低评分
    MaxScore int // 最高评分
    Step     int // 评分步长
}

// ValidateScore 验证评分
func (s *LeaderboardService) ValidateScore(score int) error {
    if score < 1 || score > 5 {
        return errors.New("评分必须在1-5之间")
    }
    return nil
}

// GetScoreDescription 获取评分描述
func (s *LeaderboardService) GetScoreDescription(score int) string {
    descriptions := map[int]string{
        1: "很差 - 内容不相关或有严重问题",
        2: "差 - 内容有问题或不够深入",
        3: "一般 - 内容基本合理但缺乏深度",
        4: "好 - 内容充分且有一定深度",
        5: "很好 - 内容优秀且有很高价值",
    }
    
    if desc, ok := descriptions[score]; ok {
        return desc
    }
    return "未知"
}

// CalculateUserRating 计算用户评分统计
func (s *LeaderboardService) CalculateUserRating(ctx context.Context, userID, competitionID uint) (*UserRating, error) {
    // 1. 获取用户收到的所有评分
    var comments []Comment
    if err := s.db.Where("user_id = ? AND competition_id = ?", userID, competitionID).
        Find(&comments).Error; err != nil {
        return nil, err
    }
    
    // 2. 统计各等级评分
    rating := &UserRating{
        UserID:        userID,
        CompetitionID: competitionID,
        TotalRatings:  len(comments),
    }
    
    var totalScore int
    for _, comment := range comments {
        totalScore += comment.Score
        
        switch comment.Score {
        case 5:
            rating.FiveStarCount++
        case 4:
            rating.FourStarCount++
        case 3:
            rating.ThreeStarCount++
        case 2:
            rating.TwoStarCount++
        case 1:
            rating.OneStarCount++
        }
    }
    
    // 3. 计算平均评分
    if rating.TotalRatings > 0 {
        rating.AverageRating = float64(totalScore) / float64(rating.TotalRatings)
    }
    
    rating.UpdatedAt = time.Now()
    
    return rating, nil
}

排序与筛选

排序功能

// SortOption 排序选项
type SortOption struct {
    Field     string // 排序字段:points, average_score, total_comments
    Direction string // 排序方向:asc, desc
}

// GetLeaderboard 获取排行榜
func (s *LeaderboardService) GetLeaderboard(ctx context.Context, competitionID uint, page, pageSize int, sortOption *SortOption) ([]Leaderboard, int64, error) {
    var leaderboard []Leaderboard
    var total int64
    
    query := s.db.Where("competition_id = ?", competitionID)
    
    // 应用排序
    if sortOption != nil {
        direction := "DESC"
        if sortOption.Direction == "asc" {
            direction = "ASC"
        }
        
        switch sortOption.Field {
        case "average_score":
            query = query.Order("average_score " + direction)
        case "total_comments":
            query = query.Order("total_comments " + direction)
        default:
            query = query.Order("points " + direction)
        }
    } else {
        query = query.Order("points DESC")
    }
    
    // 获取总数
    if err := query.Model(&Leaderboard{}).Count(&total).Error; err != nil {
        return nil, 0, err
    }
    
    // 分页查询
    offset := (page - 1) * pageSize
    if err := query.Offset(offset).Limit(pageSize).Find(&leaderboard).Error; err != nil {
        return nil, 0, err
    }
    
    return leaderboard, total, nil
}

// FilterLeaderboard 筛选排行榜
func (s *LeaderboardService) FilterLeaderboard(ctx context.Context, competitionID uint, filter *LeaderboardFilter) ([]Leaderboard, error) {
    query := s.db.Where("competition_id = ?", competitionID)
    
    // 按平均评分筛选
    if filter.MinAverageScore > 0 {
        query = query.Where("average_score >= ?", filter.MinAverageScore)
    }
    
    // 按评论数筛选
    if filter.MinComments > 0 {
        query = query.Where("total_comments >= ?", filter.MinComments)
    }
    
    // 按高质量评论比例筛选
    if filter.MinQualityRatio > 0 {
        query = query.Where("high_quality_count / total_comments >= ?", filter.MinQualityRatio)
    }
    
    var leaderboard []Leaderboard
    if err := query.Order("points DESC").Find(&leaderboard).Error; err != nil {
        return nil, err
    }
    
    return leaderboard, nil
}

前端展示

排行榜组件

<template>
  <div class="leaderboard-container">
    <div class="leaderboard-header">
      <h2>评论排行榜</h2>
      <div class="leaderboard-controls">
        <el-select v-model="sortBy" @change="handleSortChange">
          <el-option label="按积分排序" value="points"></el-option>
          <el-option label="按平均评分排序" value="average_score"></el-option>
          <el-option label="按评论数排序" value="total_comments"></el-option>
        </el-select>
      </div>
    </div>
    
    <el-table :data="leaderboard" stripe>
      <el-table-column prop="rank" label="排名" width="80"></el-table-column>
      <el-table-column prop="user.username" label="用户名" width="150"></el-table-column>
      <el-table-column prop="points" label="积分" width="100"></el-table-column>
      <el-table-column prop="totalComments" label="评论数" width="100"></el-table-column>
      <el-table-column prop="averageScore" label="平均评分" width="120">
        <template #default="{ row }">
          <el-rate v-model="row.averageScore" disabled></el-rate>
        </template>
      </el-table-column>
      <el-table-column prop="highQualityCount" label="高质量评论" width="120"></el-table-column>
      <el-table-column label="操作" width="100">
        <template #default="{ row }">
          <el-button type="text" @click="viewUserProfile(row.userId)">查看</el-button>
        </template>
      </el-table-column>
    </el-table>
    
    <el-pagination
      :current-page="currentPage"
      :page-size="pageSize"
      :total="total"
      @current-change="handlePageChange"
    ></el-pagination>
  </div>
</template>

<script>
export default {
  props: {
    competitionId: Number
  },
  data() {
    return {
      leaderboard: [],
      sortBy: 'points',
      currentPage: 1,
      pageSize: 10,
      total: 0
    }
  },
  methods: {
    async fetchLeaderboard() {
      try {
        const response = await this.$api.getLeaderboard(this.competitionId, {
          page: this.currentPage,
          page_size: this.pageSize,
          sort_by: this.sortBy
        })
        this.leaderboard = response.data
        this.total = response.total
      } catch (error) {
        this.$message.error('获取排行榜失败')
      }
    },
    handleSortChange() {
      this.currentPage = 1
      this.fetchLeaderboard()
    },
    handlePageChange(page) {
      this.currentPage = page
      this.fetchLeaderboard()
    },
    viewUserProfile(userId) {
      this.$router.push(`/user/${userId}`)
    }
  },
  mounted() {
    this.fetchLeaderboard()
  }
}
</script>

<style scoped>
.leaderboard-container {
  padding: 20px;
}

.leaderboard-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.leaderboard-controls {
  display: flex;
  gap: 10px;
}
</style>

API 接口

GET /api/competitions/{id}/leaderboard - 获取排行榜
GET /api/competitions/{id}/leaderboard/filter - 筛选排行榜
GET /api/users/{id}/rating - 获取用户评分统计
POST /api/leaderboard/update - 更新排行榜
GET /api/leaderboard/history - 获取排行榜历史

总结

排行榜与星级评定系统为辣评平台提供了完整的用户激励机制,通过公平的积分计算和排名展示,激励用户提交高质量的评论。

关键特性

  • 多维度的积分计算
  • 灵活的排序和筛选
  • 详细的评分统计
  • 排名变化历史记录
  • 用户激励机制

这个系统提升了平台的竞争性和用户参与度。

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