diff --git a/app/controllers/api/v3/users_controller.rb b/app/controllers/api/v3/users_controller.rb index 69648600c3..d26b8c6dd1 100644 --- a/app/controllers/api/v3/users_controller.rb +++ b/app/controllers/api/v3/users_controller.rb @@ -17,7 +17,7 @@ def index limit = params[:limit].to_i limit = 100 if limit > 100 - @users = User.fields_for_list.hot.limit(limit) + @users = @active_users = Counter.where(countable_type: "User", key: "yearly_replies_count").includes(:countable).order("value desc").limit(limit).map(&:countable) end # Get full detail of current user, for account setting. diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 90f4a3d6be..9255117b7a 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -13,7 +13,8 @@ class UsersController < ApplicationController def index @total_user_count = User.count - @active_users = User.without_team.fields_for_list.hot.limit(100) + key = params[:type] = "monthly" ? :monthly_replies_count : :yearly_replies_count + @active_users = Counter.where(countable_type: "User", key: key).includes(:countable).order("value desc").limit(100).map(&:countable) end def feed diff --git a/app/jobs/scheduler/total_user_stats_job.rb b/app/jobs/scheduler/total_user_stats_job.rb new file mode 100644 index 0000000000..c322f64f3d --- /dev/null +++ b/app/jobs/scheduler/total_user_stats_job.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Scheduler + # cleanup spam topic at 1 month ago + class TotalUserStatsJob < ApplicationJob + def perform(limit: 2000) + users = User.where("replies_count > 0") + users = if limit.to_i > 0 + users.order("updated_at desc").limit(limit) + else + users.where("replies_count > 0").order("updated_at desc").find_each + end + + users.each do |user| + user.monthly_replies_count.update(value: user.replies.where("created_at > ?", 1.month.ago).count) + user.yearly_replies_count.update(value: user.replies.where("created_at > ?", 1.year.ago).count) + end + end + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index f1b8f84881..95feacf56e 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -3,7 +3,8 @@ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true - include Counterable + include RedisCountable + include Countable scope :recent, -> { order(id: :desc) } scope :exclude_ids, ->(ids) { where.not(id: ids.map(&:to_i)) } diff --git a/app/models/concerns/countable.rb b/app/models/concerns/countable.rb new file mode 100644 index 0000000000..f947971149 --- /dev/null +++ b/app/models/concerns/countable.rb @@ -0,0 +1,25 @@ +# Countable for storage by use ActiveRecord +module Countable + extend ActiveSupport::Concern + + included do + scope :with_counters, -> { includes(@@countable_names) } + end + + class_methods do + def countable(*names) + class_eval do + @@countable_names ||= [] + + names.each do |name| + @@countable_names << "#{name}_counter".to_sym + has_one :"#{name}_counter", -> { where(key: name) }, as: :countable, class_name: "Counter" + + define_method :"#{name}" do + send(:"#{name}_counter") || send(:"create_#{name}_counter") + end + end + end + end + end +end diff --git a/app/models/concerns/counterable.rb b/app/models/concerns/redis_countable.rb similarity index 97% rename from app/models/concerns/counterable.rb rename to app/models/concerns/redis_countable.rb index e94b7f3764..bb3879b1b7 100644 --- a/app/models/concerns/counterable.rb +++ b/app/models/concerns/redis_countable.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # 增加访问量的功能 -module Counterable +module RedisCountable extend ActiveSupport::Concern class Counter diff --git a/app/models/counter.rb b/app/models/counter.rb new file mode 100644 index 0000000000..7fc632a58a --- /dev/null +++ b/app/models/counter.rb @@ -0,0 +1,22 @@ +class Counter < ApplicationRecord + belongs_to :countable, polymorphic: true + validates :countable, presence: true + + delegate :to_i, :to_s, :inspect, to: :value + + def incr(by = 1) + increment!(:value, by).value + end + + def decr(by = 1) + decrement!(:value, by).value + end + + def method_missing(method, *args, &block) + if value.respond_to?(method) + value.send(method, *args, &block) + else + super + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb index e9eb80e9bc..9eeb5c39df 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -36,6 +36,8 @@ class User < ApplicationRecord has_many :teams, through: :team_users has_one :sso, class_name: "UserSSO", dependent: :destroy + countable :monthly_replies_count, :yearly_replies_count + attr_accessor :password_confirmation validates :login, format: {with: ALLOW_LOGIN_FORMAT_REGEXP, message: I18n.t("users.username_allows_format")}, @@ -62,11 +64,11 @@ def self.find_for_database_authentication(warden_conditions) end def self.find_by_email(email) - fetch_by_uniq_keys(email: email) + fetch_by_uniq_keys(email:) end def self.find_by_login!(slug) - find_by_login(slug) || raise(ActiveRecord::RecordNotFound.new(slug: slug)) + find_by_login(slug) || raise(ActiveRecord::RecordNotFound.new(slug:)) end def self.find_by_login(slug) diff --git a/app/views/users/_sidebar.html.erb b/app/views/users/_sidebar.html.erb index 85e03519ed..c8143354ca 100644 --- a/app/views/users/_sidebar.html.erb +++ b/app/views/users/_sidebar.html.erb @@ -25,6 +25,7 @@
<%= t("users.number_of_user", no: @user.id)%> / "><%= @user.created_at.to_date %>
+
@@ -42,11 +43,10 @@ <%= icon_tag("location") %> <%= location_name_tag(@user.location) %> <% end %> -
- <%= t("users.topics_count_html", count: @user.topics_count) %> - / - <%= t("users.replies_count_html", count: @user.replies_count) %> -
+
+
<%= t("users.topics_count_html", count: @user.topics_count) %>
+
<%= t("users.replies_count_html", count: @user.replies_count, yearly_count: @user.yearly_replies_count) %>
+
<% if !@user.twitter.blank? %> <%= link_to icon_bold_tag("twitter"), @user.twitter_url, class: "twitter", rel: "nofollow" %> diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb index 0981202bc3..dca01afc49 100644 --- a/app/views/users/index.html.erb +++ b/app/views/users/index.html.erb @@ -3,9 +3,15 @@
-
-
<%= t("users.hot_users") %>
-
Total <%= @total_user_count %> users
+
+
+
<%= t("users.hot_users") %>
+
Total <%= @total_user_count %> users
+
+
<%= render "user_list", users: @active_users, columns: 4 %> diff --git a/config/locales/views.en.yml b/config/locales/views.en.yml index 56b91405b1..2b5fb75d5d 100644 --- a/config/locales/views.en.yml +++ b/config/locales/views.en.yml @@ -287,7 +287,7 @@ join_time: "Joined at" signature: "Status" topics_count_html: %{count} Topics - replies_count_html: %{count} Replies + replies_count_html: "%{count} Replies (Yearly: %{yearly_count})" personal_website: "Blog" recent_publish_topic: "Recent Topics" recent_reply_topic: "Recent Replies" diff --git a/config/locales/views.zh-CN.yml b/config/locales/views.zh-CN.yml index 502a5d50a8..12c9b12e4e 100644 --- a/config/locales/views.zh-CN.yml +++ b/config/locales/views.zh-CN.yml @@ -295,8 +295,8 @@ join_time: "Since" signature: "签名" personal_website: "博客" - topics_count_html: %{count} 篇帖子 - replies_count_html: %{count} 条回帖 + topics_count_html: 累计 %{count} 篇帖子 + replies_count_html: 累计 %{count} 条回帖 (最近一年 %{yearly_count} 条) recent_publish_topic: "最近发布的帖子" recent_reply_topic: "最近回复过的帖子" unbind_warning: "至少要保留一个关联账号,现在不能解绑。" @@ -317,7 +317,7 @@ already_have_account: "已经有账号了?" forget_password: "忘记了密码?" find_password: "找回密码" - find_password_help_text: "此功能将会发送一个找回密码的特别链接到你的邮箱,通过该链接可以进入重置密码的页面。" + find_password_help_text: "此功能将会发送一个找回密码的特别链接到你的邮箱,通过该链接可以进入重置密码的页面。" not_recieve_confirm_mail: "未收到确认邮件?" not_recieve_unlock_mail: "未收到解锁邮件?" resend_unlock_mail: "重新发送解锁邮件" diff --git a/config/schedule.yml b/config/schedule.yml index a478d37efd..f88ea3b9a8 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -15,3 +15,9 @@ spam_cleanup: class: "Scheduler::SpamCleanupJob" queue: cleanup description: "Cleanup spam contents before 1 month ago" + +update_user_stats: + cron: "0 0 * * 6" + class: "Scheduler::TotalUserStatsJob" + queue: default + description: "Update user stats" diff --git a/db/migrate/20220113130722_create_counters.rb b/db/migrate/20220113130722_create_counters.rb new file mode 100644 index 0000000000..7a56046fa6 --- /dev/null +++ b/db/migrate/20220113130722_create_counters.rb @@ -0,0 +1,13 @@ +class CreateCounters < ActiveRecord::Migration[7.0] + def change + create_table :counters do |t| + t.references :countable, polymorphic: true + t.string :key, null: false + t.integer :value, null: false, default: 0 + t.timestamps + end + + add_index :counters, [:countable_type, :countable_id, :key], unique: true + add_index :counters, [:countable_type, :key, :value] + end +end diff --git a/db/schema.rb b/db/schema.rb index a7cf0e0a24..ae3141874c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_04_09_032709) do +ActiveRecord::Schema.define(version: 2022_01_13_130722) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -50,6 +50,18 @@ t.index ["user_id"], name: "index_comments_on_user_id" end + create_table "counters", force: :cascade do |t| + t.string "countable_type" + t.bigint "countable_id" + t.string "key", null: false + t.integer "value", default: 0, null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["countable_type", "countable_id", "key"], name: "index_counters_on_countable_type_and_countable_id_and_key", unique: true + t.index ["countable_type", "countable_id"], name: "index_counters_on_countable" + t.index ["countable_type", "key", "value"], name: "index_counters_on_countable_type_and_key_and_value" + end + create_table "devices", id: :serial, force: :cascade do |t| t.integer "platform", null: false t.integer "user_id", null: false diff --git a/test/controllers/api/users_test.rb b/test/controllers/api/users_test.rb index 4412efa28d..4e0362f68b 100644 --- a/test/controllers/api/users_test.rb +++ b/test/controllers/api/users_test.rb @@ -5,13 +5,18 @@ describe Api::V3::UsersController do describe "GET /api/v3/users.json" do before do - create_list(:user, 10) + @users = create_list(:user, 10) + + @users.each do |user| + user.yearly_replies_count.to_i + user.monthly_replies_count.to_i + end end it "should work" do get "/api/v3/users.json" assert_equal 200, response.status - assert_equal User.count, json["users"].size + assert_equal @users.count, json["users"].size assert_has_keys json["users"][0], "id", "name", "login", "avatar_url" end @@ -90,7 +95,7 @@ describe "recent order" do it "should work" do - @topics = create_list(:topic, 3, user: user) + @topics = create_list(:topic, 3, user:) get "/api/v3/users/#{user.login}/topics.json", offset: 0, limit: 2 assert_equal 200, response.status assert_equal 2, json["topics"].size @@ -103,8 +108,8 @@ describe "hot order" do it "should work" do - @hot_topic = create(:topic, user: user, likes_count: 4) - @topics = create_list(:topic, 3, user: user) + @hot_topic = create(:topic, user:, likes_count: 4) + @topics = create_list(:topic, 3, user:) get "/api/v3/users/#{user.login}/topics.json", order: "likes", offset: 0, limit: 3 assert_equal 200, response.status @@ -115,8 +120,8 @@ describe "hot order" do it "should work" do - @hot_topic = create(:topic, user: user, replies_count: 4) - @topics = create_list(:topic, 3, user: user) + @hot_topic = create(:topic, user:, replies_count: 4) + @topics = create_list(:topic, 3, user:) get "/api/v3/users/#{user.login}/topics.json", order: "replies", offset: 0, limit: 3 assert_equal 200, response.status @@ -132,7 +137,7 @@ describe "recent order" do it "should work" do - @replies = create_list(:reply, 3, user: user, topic: topic) + @replies = create_list(:reply, 3, user:, topic:) get "/api/v3/users/#{user.login}/replies.json", offset: 0, limit: 2 assert_equal 2, json["replies"].size fields = %w[id user body_html topic_id topic_title] @@ -148,7 +153,7 @@ let(:user) { create(:user) } it "should work" do - @topics = create_list(:topic, 4, user: user) + @topics = create_list(:topic, 4, user:) user.favorite_topic(@topics[0].id) user.favorite_topic(@topics[1].id) user.favorite_topic(@topics[3].id) diff --git a/test/factories/counters.rb b/test/factories/counters.rb new file mode 100644 index 0000000000..c580479c6a --- /dev/null +++ b/test/factories/counters.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :counter do + association :countable, factory: :user + key { "foo_count" } + value { 0 } + end +end diff --git a/test/models/counter_test.rb b/test/models/counter_test.rb new file mode 100644 index 0000000000..0622ba1259 --- /dev/null +++ b/test/models/counter_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "test_helper" + +class CounterTest < ActiveSupport::TestCase + test "base" do + counter = create :counter + assert_equal 0, counter.value + assert_equal 0, counter.to_i + + counter.incr + assert_equal 1, counter.value + + counter.incr + assert_equal 2, counter.value + + counter.incr(3) + assert_equal 5, counter.value + + counter.decr + assert_equal 4, counter.value + + counter.decr(2) + assert_equal 2, counter.value + end + + test "User counter" do + user = create(:user) + + assert_equal 0, user.yearly_replies_count.to_i + user.yearly_replies_count.incr + assert_equal 1, user.yearly_replies_count.to_i + user.yearly_replies_count.decr + assert_equal 0, user.yearly_replies_count.to_i + + assert_equal 0, user.monthly_replies_count.to_i + user.monthly_replies_count.incr + assert_equal 1, user.monthly_replies_count.to_i + user.monthly_replies_count.decr + assert_equal 0, user.monthly_replies_count.to_i + end +end