Как организовать структуру реферальной системы в базе данных?

Всем привет. Подскажите, как правильно реализовать структуру реферальной системы? Регистрация по реф. ссылке уже есть, новому пользователю записывается id пользователя, от которого он пришел. Но надо сделать страницу, на которой пользователь может увидеть всю структуру, которая "под" ним (включая всех тех, кто регистрировался под теми, кого позвал наш пользователь и т.д.).

Линий в этой структуре должно быть не ограниченное количество, то есть, если спустя 10, 20 или 30 человек кто-то зарегистрируется в "моей" реферальной системе, то я должен его видеть в своей структуре.

Мне в голову пришло только 2 варианта решений: 1)Хранить у каждого пользователя id того, "под" кем он зарегистрирован, но тогда на странице структуры у меня будет огромное кол-во запросов к БД, чтобы по цепочке достать всех пользователей, их пользователей и т.д.

2)Сделать таблицу со связями, где будут создаваться записи при регистрации каждого пользователя для каждой линии отдельно. Допустим, Иван позвал Андрея, а Андрей позвал Олега, а Олег в свою очередь позвал Михаила. Вот при регистрации Михаила создавались бы в таблице записи примерно следующие:

userId - id Михаила.
refId - id Олега.
line - 1.

userId - id Михаила.
refId - id Андрея.
line - 2.

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

Подскажите, как правильно организовать хранение данных для реферальной системы с неограниченным количеством линий?

Заранее спасибо!


Ответы (1 шт):

Автор решения: Dmitrii Sedov

Собственно если перефразировать ваш вопрос, то будет он звучать так:

Как сформировать многоуровневое меню

Решение для данной ситуации, когда структура бд должна представлять собой parent_child где каждый ребёнок может являться родителем другому значению, следующее

Структура бд должна быть следующей:
Таблица users с колонками id, name, parent_id (это абстрактный пример, помимо id и parent_id колонок может быть сколько угодно, но эти две обязательны) parent_id это ваш ref_id по сути

  1. Анализируя структуру мы понимаем, что здесь связь 1 ко многим поэтому указываем в нашей модели User следующее:

    public function parent()
    {
        return $this->belongsTo(User::class, 'parent_id');
    }
    
  2. Никаких 1000 запросов в бд не будет. Будет всего 1 запрос на получение данных.

    $items = \App\Models\User::orderBy('parent_id', 'DESC')->get();
    

Сортировка по убыванию parent_id обязательно. Почему? Когда мы будем бежать в цикле и подставлять рекурсивно значения, если мы не отсортируем по убыванию, то 3 и дальше уровни вложенности пропадут (можете потом по экспериментировать). Далее мы получаем коллекцию всех пользователей системы.

  1. Следующий этап это группировка по parent_id для того чтобы объединить сразу всех пользователей.

    $grouped = $items->groupBy('parent_id');
    

В результате мы получим

Illuminate\Database\Eloquent\Collection {#1589 ▼
  #items: array:12 [▼
  "" => Illuminate\Database\Eloquent\Collection {#1496 ▶}
  36 => Illuminate\Database\Eloquent\Collection {#1486 ▶}
  34 => Illuminate\Database\Eloquent\Collection {#1585 ▶}
  30 => Illuminate\Database\Eloquent\Collection {#1588 ▶}
  27 => Illuminate\Database\Eloquent\Collection {#1600 ▶}
  16 => Illuminate\Database\Eloquent\Collection {#1590 ▶}
  ...
]
  1. Далее мы пробегаем по всем элементам и проверяем есть ли дочерние элементы и добавляем их под ключом children.

    foreach ($items as $item) {
       if ($grouped->has($item->id)) {
         $item->children = $grouped[$item->id];
       }
    }
    
  2. И последний этап это отдаём фронту получившийся массив. Избавляясь от всех элементов которые являются дочерними, так как они уже находятся в нужном месте при проходе цикла.

    view()->with('struct', $items->whereNull('parent_id'))
    

Итог: пример данного кода, можно найти в интернете, суть везде одна и та же.

На фронте данный массив вы выводите так же с помощью рекурсии проверяя наличие children ключа. Это Вам домашнее задание

Проблемы при формировании.

Что если пользователей будет 100000? Запрос отработает быстро, так как id является индексом и parent_id то же должен быть. В результате сортировка то же будет быстрой, нагрузка на бд будет не большая. НО! далее начинается обработка сервером что приводит к большому циклу плюс рекурсия (groupBy). И когда пользователи будут заходить на сайт они будут ждать пока загрузится данное меню, а потом только получать данные. При переходе на другую страницу будет то же самое.

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

При заходе на страницу пользователю будет отдаваться следующее:

view()->with('struct', Cache::get('user_structure'));

А когда изменяете таблицу, будет что-то типо метода. Данный код является псевдо-кодом и показывает всего-лишь суть генерации и хранения.

//Какой-то сервис генерации 

...

public function generateMenu()
{
    //код выше из ответа

   return Cache::rememberForever('user_structure', $result);
}
→ Ссылка