1

我有一个应用程序,可以为用户提供带有 game_type 的游戏的游戏积分。我想做的是:

  1. 基于游戏的分数小计
  2. 确定用户游戏排名(排名 1 = 最高分)基于他们与其他用户相比获得的 game_id 分数
  3. 根据 game_type 小计积分
  4. 确定用户 game_type 排名(排名 1 = 最高分)基于他们在 game_type_id 上与其他用户相比有多少分

根据上面的查询结果,我想只显示game_types和games之间的前五名排名。

例如,假设用户的计算是这样的:

game_id = 1, rank 200
game_id = 2, rank 10
game_id = 3, rank 6
game_id = 4, rank 31

game_type_id = 1, rank 500
game_type_id = 2, rank 400
game_type_id = 3, rank 1
game_type_id = 4, rank 7
game_type_id = 5, rank 100

然后我只想显示 games:2, 3, 4 和 game_types: 3, 4,5 的排名,因为这些是该用户的游戏和 game_types 中的前五名。

我考虑在用户表中为每个 game_type 和游戏(即 game_1_rank、game_2_rank、game_type_1_rank 等)创建一个 game_type 和游戏字段,这样我就可以每小时计算后台作业中的点数,然后尝试从中检索最高排名那里,但我认为这不是最好的方法,因为随着时间的推移会添加新游戏和游戏类型。

因此,我认为最好的方法是在加载 users#show 页面时进行计算,然后缓存该页面(并且每小时到期)。

我的模型如下所示:

user
  has_many :points

point
  belongs_to :game
  belongs_to :game_type
  belongs_to :user

game
  has_many :points
  has_one :game_type

game_type
  has_many :points

我在 Users#show 中有这段代码来计算所有游戏和游戏类型的总体排名,但我不确定如何调整它,以便我可以访问我认为的最高排名(我的视图中还没有代码显示该用户的前五名以及他们的游戏/游戏类型)。

# calculate ranks for all users for all games in order to find the user's rank
@games = Game.all

@games.each do |game|
  @users_by_game = Point.where(“game_id = ?”, game.id).select("sum(amount) as points, user_id").order("points desc").group("user_id")
  rank = 0
  points = 0

  @users_by_game.each_with_index do |user_by_game, index|           
    if user_by_game.points != points
      points = user_by_game.points
      rank += 1
  end
end

# calculate ranks for all users for all games_types in order to find the user's rank
@game_types = GameType.all   

@game_types.each do |game_type| 
  @users_by_game_type = Point.where(“game_type_id = ?”, game_type.id).select("sum(amount) as points, user_id").order("points desc").group("user_id")
  rank = 0
  points = 0

  @users_by_game_type.each_with_index do |user_by_game_type, index|           
    if user_by_game_type.points != points
      points = user_by_game_type.points
      rank += 1
    end
  end
end

我要确定的是:

  1. 这是计算这些排名的最佳方法,还是有更节省资源或 DRY 的方法?
  2. 如果这是最好的方法,我该如何更改我的代码和视图以显示此@user 的最高 5 个游戏/game_type 排名,因为代码现在只计算所有用户的排名?
4

1 回答 1

1

Introduce two new tables to store the ranks by game and ranks by game type. Calculate the over-all rank every hour. This way the page load will be much faster. You can use a gem like whenever to schedule the rank calculation every hour.

Your current implementation will not scale beyond few hundred users.

class User
  has_many :points
  has_many :game_ranks, :order => "rank DESC"
  has_many :game_type_ranks, :order => "rank DESC"

  # schedule this function every hour.
  def self.update_rank
    update_rank_by(Game)
    update_rank_by(GameType)
  end

  def self.top_games(page_size=5)
    game_ranks.includes(:game).limit(page_size)
  end

  def self.top_games_by_type(page_size=5)
    game_type_ranks.includes(:game).limit(page_size)
  end

  def self.update_rank_by klass
    rank_class = (klass.name + "Rank").constantize
    rank_by_col = "#{klass.name.underscore}_id".to_sym        

    rank = total_points = rank_by = 0
    page = 1;page_size=1000
    sql = sum_points_by(rank_by_col)
    while(points= sql.limit(page_size).offset((page-1)*page_size)).present?
      page += 1
      User.trasaction do
        points.each do |point|
          rank_by_col_val = point.send(rank_by_col) 

          # calculate rank
          if ( rank_by != rank_by_col_val)
            rank = total_points = 0
            rank_by = rank_by_col_val
          end

          if point.total_points > total_points
            total_points = point.total_points
            rank +=1
          end

          create_or_update_rank_object(rank_class, rank_by_col, point.user_id, rank_by_col_val, total_points, rank)
        end
      end      
    end    
  end

  def self.sum_points_by(rank_by_col)
    select_sql = "points.user_id, points.#{rank_by_col}, 
                  SUM(points.points) total_points"
    Point.select(select_sql).group(:user_id, rank_by_col).
      order("#{rank_by_col}, total_points DESC")
  end

  def self.create_or_update_rank_object(rank_class, rank_by_col, user_id, rank_by_col_val, total_points, rank)
    ro = rank_class.send(
      "find_or_initialize_by_user_id_and_#{rank_by_col}",
      user_id, rank_by_col_val)
    ro.total_points = total_points           
    ro.rank = rank
    ro.save
  end
end

Add a new model to hold ranks and total points for every user and game id

class GameTypeRank
  # Add columns total_points and rank 
  belongs_to :game_type
  belongs_to :user
end

Add a new model to hold ranks and total points for every user and game type

class GameRank
  # Add columns total_points and rank 
  belongs_to :game
  belongs_to :user
end

To get the top five games by rank for a user

# array of game_id, game_name and game rank
current_user.top_games.map {|r| [r.game.id, r.game.name, r.rank]} 
# array of game_type_id, game_type_name and game rank
current_user.top_games_by_type.map { |r| 
  [r.game_type.id, r.game_type.name, r.rank]
} 
于 2013-03-04T22:37:34.147 回答