Геймификация и приглашения

При использовании плагина геймификации очки начисляются за отработанные приглашения.
Это легко злоупотребляется: пользователь рассылает приглашения и отработывает их с помощью дублирующего (фейкового) аккаунта.
Когда такой фейковый аккаунт удаляется администратором, поведение плагина геймификации (и системы приглашений) зависит от того, как было отправлено приглашение.

  • Если приглашение было привязано к конкретному адресу электронной почты и пользователь удалён, то как запись invite, так и записи user_invite удаляются безвозвратно (!). При следующем запуске задачи Sidekiq UpdateScoresFor* плагин геймификации снова вычтет начисленные очки из общего счёта.

  • Если приглашение не было привязано к конкретному адресу электронной почты и пользователь удалён, то запись invited_users удаляется, но запись invite остаётся. Значение redemption_count не уменьшается. С одной точки зрения это логично, поскольку отработка приглашения уже состоялась, даже если пользователь был удалён позже. Однако плагин геймификации использует redemption_count для расчёта счёта, поэтому очки не будут вычтены.

В процессе отладки я также обнаружил, что задача подсчёта очков учитывает только приглашения, созданные менее 10 дней назад. Таким образом, если приглашение отработано через 11 дней, очки вообще не начисляются. Я хотел бы сказать: «Наверное, нужно смотреть на поле updated_at вместо created_at», но при увеличении счётчика отработок приглашения временная метка updated_at не обновляется.

11 лайков

Не подойдет ли что-то вроде этого (адаптированное под формат оцениваемых запросов — это тестовый запрос для Data Explorer :slight_smile:):

-- [params]
-- date :start_date
-- date :end_date

SELECT 
    invited_by_id AS user_id,
    COUNT(*) AS user_invites,
    COUNT(*) * 10 AS invite_score
FROM invited_users iu
  JOIN invites i ON i.id = iu.invite_id
  JOIN users u ON u.id = iu.user_id
WHERE iu.redeemed_at::date BETWEEN :start_date AND :end_date
  AND iu.user_id <> i.invited_by_id 
  AND u.created_at > iu.redeemed_at
GROUP BY invited_by_id
ORDER BY user_invites DESC

При удалении пользователя он удаляется из таблицы invited_users, поэтому больше не будет учитываться в подсчёте. Если удаление произошло в течение 10 дней, счётчик автоматически скорректируется; если прошло больше времени, потребуется ручное обновление очков.

Использование даты redeemed_at учтёт те приглашения, которые были созданы более 10 дней назад.

Условие AND iu.user_id <> i.invited_by_id также исключит самовызовы.

Присоединение таблицы users и добавление условия AND u.created_at > iu.redeemed_at позволит исключить приглашение уже существующих пользователей.

3 лайка

Это был бы хороший подход, за одним исключением:

Это работает не очень хорошо. Иногда пользователь создаётся чуть раньше, чем происходит искупление. Не знаю почему. В основном это десятые доли секунды, но я также нашёл несколько случаев с десятками секунд.

Протестировано на реальной базе данных.

select 
  iu.redeemed_at iu_AS redeemed_at, 
  u.created_at AS u_created_at, 
  u.created_at > iu.redeemed_at AS u_created_gt_iu_redeemed 
from invited_users iu 
left join users u on u.id = iu.user_id 
where iu.redeemed_at is not null
order by iu.id desc;
       iu_redeemed_at       |        u_created_at        | u_created_gt_iu_redeemed 
----------------------------+----------------------------+--------------------------
 2023-09-08 00:00:47.557057 | 2023-09-08 00:00:48.376446 | t
 2023-08-25 20:09:03.486362 | 2023-08-25 20:09:03.201357 | f
 2023-08-15 23:38:32.271709 | 2023-08-15 23:38:33.570299 | t
 2023-08-14 10:44:34.19912  | 2023-08-14 10:44:35.429371 | t
 2023-08-12 13:41:10.428013 | 2023-08-12 13:41:11.733973 | t
 2023-07-31 17:58:13.511289 | 2023-07-31 17:57:50.427111 | f
 2023-07-23 00:56:33.455185 | 2023-07-23 00:55:47.999263 | f
 2023-07-19 08:42:44.908096 | 2023-07-19 08:42:46.040201 | t
 2023-06-30 09:11:38.829692 | 2023-06-30 09:11:39.618586 | t
 2023-06-30 08:37:02.322192 | 2023-06-30 08:37:03.133769 | t
 2023-06-29 16:24:01.705616 | 2023-06-29 16:24:02.55067  | t
 2023-06-29 12:53:33.245688 | 2023-06-29 12:53:34.067159 | t

1 лайк

Это интересно. Я точно не учёл это. :slight_smile:

Не компенсирует ли добавление небольшого буфера, например AND u.created_at + INTERVAL '1 SECOND' > iu.redeemed_at, это, не оказывая при этом слишком сильного влияния?

Мне это кажется излишней «магией», так как мы не знаем, что вызывает задержку.

Я подозреваю, что задержка вызвана большой очередью Sidekiq или чем-то подобным. Хотя большинство задержек составляет менее одной секунды, около 3% находятся в диапазоне от 0:00:01 до 0:15:00 (от одной секунды до 15 минут).

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

2 лайка

Это должно решить проблему:

1 лайк

Это больше похоже на запрос новой функции, чем на баг. (cc @Falco)

Эта тема была автоматически закрыта через 3 дня. Новые ответы больше не допускаются.