Калибровка геймификации

Это запрос, который воспроизводит баллы геймификации и предоставляет детализацию: кто что получил и почему:

-- [params]
-- date :start_date
-- date :end_date
-- int :day_visited_score_value = 1
-- int :time_read_score_value = 1
-- int :posts_read_score_value = 1
-- int :posts_created_score_value = 2
-- int :topics_created_score_value = 5
-- int :likes_received_score_value = 1
-- int :likes_given_score_value = 1
-- int :solutions_score_value = 10
-- int :flag_created_score_value = 10
-- int :user_invited_score_value = 10

WITH visits AS (

    SELECT
        uv.user_id,
        COUNT(*) AS user_visits,
        COUNT(*) * :day_visited_score_value AS visits_score
    FROM user_visits uv
    WHERE uv.visited_at BETWEEN :start_date AND :end_date
    GROUP BY uv.user_id

),

     time_read AS (

         SELECT
             uv.user_id,
             SUM(uv.time_read) /3600 AS time_read,
             SUM(uv.time_read) /3600 * :time_read_score_value AS time_read_score
         FROM user_visits uv
         WHERE uv.visited_at BETWEEN :start_date AND :end_date
           AND uv.time_read >= 60
         GROUP BY uv.user_id

     ),

     posts_read AS (

         SELECT
             uv.user_id,
             SUM(uv.posts_read) AS posts_read,
             SUM(uv.posts_read) /100 * :posts_read_score_value AS posts_read_score
         FROM user_visits uv
         WHERE uv.visited_at BETWEEN :start_date AND :end_date
           AND uv.posts_read >= 5
         GROUP BY uv.user_id

     ),

     posts_created AS (

         SELECT
             p.user_id,
             COUNT(*) AS posts_created,
             COUNT(*) * :posts_created_score_value AS posts_created_score
         FROM posts p
                  INNER JOIN topics t ON t.id = p.topic_id
         WHERE p.deleted_at IS NULL
           AND t.archetype <> 'private_message'
           AND p.wiki IS FALSE
           AND p.created_at::date BETWEEN :start_date AND :end_date
         GROUP BY p.user_id
     ),

     topics_created AS (

         SELECT
             t.user_id,
             COUNT(*) AS topics_created,
             COUNT(*) * :topics_created_score_value AS topics_created_score
         FROM topics t
         WHERE t.deleted_at IS NULL
           AND t.archetype <> 'private_message'
           AND t.created_at::date BETWEEN :start_date AND :end_date
         GROUP BY t.user_id

     ),

     likes_received AS (

         SELECT
             p.user_id,
             COUNT(*) AS likes_received,
             COUNT(*) * :likes_received_score_value AS likes_received_score
         FROM post_actions pa
                  INNER JOIN posts p ON p.id = pa.post_id
                  INNER JOIN topics t ON t.id = p.topic_id
         WHERE p.deleted_at IS NULL
           AND t.archetype <> 'private_message'
           AND p.wiki IS FALSE
           AND post_action_type_id = 2
           AND pa.created_at::date BETWEEN :start_date AND :end_date
         GROUP BY p.user_id

     ),


     likes_given AS (

         SELECT
             pa.user_id AS user_id,
             COUNT(*) AS likes_given,
             COUNT(*) * :likes_given_score_value AS likes_given_score
         FROM post_actions pa
                  INNER JOIN posts p ON p.id = pa.post_id
                  INNER JOIN topics t ON t.id = p.topic_id
         WHERE p.deleted_at IS NULL
           AND t.archetype <> 'private_message'
           AND p.wiki IS FALSE
           AND post_action_type_id = 2
           AND pa.created_at::date BETWEEN :start_date AND :end_date
         GROUP BY pa.user_id
     ),

     solutions AS (

         SELECT
             p.user_id,
             COUNT(ta.answer_post_id) AS solutions,
             COUNT(ta.answer_post_id) * :solutions_score_value AS solutions_score
         FROM discourse_solved_solved_topics st
         JOIN discourse_solved_topic_answers ta ON ta.solved_topic_id = st.id
                  INNER JOIN topics t ON st.topic_id = t.id
                  INNER JOIN posts p ON p.id = ta.answer_post_id
         WHERE p.deleted_at IS NULL
           AND t.deleted_at IS NULL
           AND t.archetype <> 'private_message'
           AND p.user_id <> t.user_id
           AND ta.updated_at::date BETWEEN :start_date AND :end_date
         GROUP BY p.user_id

     ),

     flags AS (

         SELECT
             r.created_by_id AS user_id,
             COUNT(*) AS flags,
             COUNT(*) * :flag_created_score_value AS flags_score
         FROM reviewables r
         WHERE created_at::date BETWEEN :start_date AND :end_date
           AND status = 1
         GROUP BY user_id

     ),

     invites AS (

         SELECT
             inv.invited_by_id AS user_id,
             SUM(inv.redemption_count) AS invites,
             (SUM(inv.redemption_count) * :user_invited_score_value)::int AS invites_score
         FROM invites inv
         WHERE inv.created_at::date BETWEEN :start_date AND :end_date
           AND inv.redemption_count > 0
         GROUP BY inv.invited_by_id

     )

SELECT
    u.id AS user_id,
    (
        COALESCE(v.visits_score,0) +
        COALESCE(tr.time_read_score,0) +
        COALESCE(pr.posts_read_score,0) +
        COALESCE(pc.posts_created_score,0) +
        COALESCE(tc.topics_created_score,0) +
        COALESCE(lr.likes_received_score,0) +
        COALESCE(lg.likes_given_score,0) +
        COALESCE(s.solutions_score,0) +
        COALESCE(f.flags_score,0) +
        COALESCE(i.invites_score,0)
        ) AS «Общее количество cheers»,
    COALESCE(v.user_visits,0) || ' (' || COALESCE(v.visits_score,0) || ')' AS «Посещения (cheers)»,
    COALESCE(tr.time_read,0) || ' ч' || ' (' || COALESCE(tr.time_read_score,0) || ')' AS «Время чтения
  (cheers)»,
    COALESCE(pr.posts_read,0) || ' (' || COALESCE(pr.posts_read_score,0) || ')' AS «Прочитанные посты
  (cheers)»,
    COALESCE(pc.posts_created,0) || ' (' || COALESCE(pc.posts_created_score,0) || ')' AS «Созданные посты
   (cheers)»,
    COALESCE(tc.topics_created,0) || ' (' || COALESCE(tc.topics_created_score,0) || ')' AS «Созданные
  темы (cheers)»,
    COALESCE(lr.likes_received,0) || ' (' || COALESCE(lr.likes_received_score,0) || ')' AS «Полученные
  лайки (cheers)»,
    COALESCE(lg.likes_given,0) || ' (' || COALESCE(lg.likes_given_score,0) || ')' AS «Отданные
  лайки (cheers)»,
    COALESCE(s.solutions,0) || ' (' || COALESCE(s.solutions_score,0) || ')' AS «Решения (cheers)»,
    COALESCE(f.flags,0) || ' (' || COALESCE(f.flags_score,0) || ')' AS «Подтверждённые флаги (cheers)»,
    COALESCE(i.invites,0) || ' (' || COALESCE(i.invites_score,0) || ')' AS «Отработанные приглашения (cheers)»
FROM users u
         LEFT JOIN visits v ON v.user_id = u.id
         LEFT JOIN posts_read pr USING (user_id)
         LEFT JOIN time_read tr USING (user_id)
         LEFT JOIN flags f USING (user_id)
         LEFT JOIN posts_created pc USING (user_id)
         LEFT JOIN topics_created tc USING (user_id)
         LEFT JOIN likes_given lg USING (user_id)
         LEFT JOIN likes_received lr USING (user_id)
         LEFT JOIN solutions s USING (user_id)
         LEFT JOIN invites i USING (user_id)
WHERE u.id > 0
  AND u.id NOT IN (SELECT user_id FROM group_users WHERE group_id = 3)
ORDER BY «Общее количество cheers» DESC
LIMIT 100

С увеличением временного промежутка возникает расхождение в баллах: корректная версия геймификации округляет такие показатели, как время и количество прочитанных постов, на ежедневной основе, тогда как этот запрос суммирует все значения, включая остатки. Тем не менее, он остаётся весьма полезным для проверки того, как изменения в ваших значениях по умолчанию для начисления баллов отразятся на результатах, без необходимости пересчитывать все текущие баллы вашего сайта.

Также запрос не включает участников группы @staff, хотя при желании это легко изменить. :+1: :slight_smile:

В зависимости от размера вашего сайта может потребоваться уменьшить временной масштаб или сузить группы, которые вы анализируете, чтобы запрос не завершался с ошибкой по таймауту.

Хочу отметить, что этот проект, на мой взгляд, немного устарел.

В него нужно добавить новые реакции и систему оценки чата, а также обновить использование новых таблиц Solved.

Я пока не знаю, когда смогу уделить ему больше внимания и обновить, но считаю, что основа всё ещё достаточно крепкая, если кто-то захочет использовать её как источник вдохновения. :+1: