Практически в каждой компании есть корпоративная система поощрений. Но вот как прописать для неё правила, да ещё и на Битриксе, — вопрос. Делюсь личным опытом.

Привет! Я full-stack веб-разработчик в IBS, меня зовут Вячеслав Степин, и это мой дебют на Хабре.

Недавно мне дали такую задачу — сделать зарабатываемые достижения для пользователей корпоративного портала компании в рамках геймификации системы поощрения сотрудников «Спасибо». На первый взгляд задача не кажется экстремально сложной, да и описанных систем лояльности уже есть немало. Но я начал изучать эту тему и столкнулся с недостаточным объёмом информации. В основном на форумах подробно расхваливают системы с точки зрения эйчаров, а вот как именно описать правила для достижений — все почему-то молчат (в лучшем случае поверхностно описывают создание основных таблиц в двух-трёх абзацах).

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

Итак, вводные: реализовать функциональность достижений на CMS «1С-Битрикс» (на ней построен корпоративный портал). Типы достижений: ручные и автоматические. Ручные назначаются администратором портала за какие-нибудь заслуги (участие в турнирах, благотворительные инициативы и т. п.). Автоматические зарабатываются путём выполнения определённых действий, заданных в правилах достижений (комментарии, лайки, найденные пасхалки и т. п.).

Начнём!

Логика решения

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

[текущий прогресс достижения] =

            [общий прогресс действий достижения (getCountEventsByUser)]

            - [количество полученных достижений (таблица «Полученные достижения»)]

            * [количество в правиле настроенного достижения COUNT (таблица «Достижения»)]

Далее если [текущий прогресс достижения] больше или равен [количество в правиле настроенного достижения] то

[количество достижений для вручения (обычно одно)] =

            [текущий прогресс достижения]

            / [количество в правиле настроенного достижения COUNT (таблица «Достижения»)]

[количество достижений для вручения (обычно одно)] округляем до целого числа в меньшую сторону, и столько достижений вручаем пользователю.

Минимальный набор таблиц для достижений

Переходим от идеи к реализации. Для начала определимся с ключевыми таблицами.

Пользователи

ID

NAME

1

Вася

2

Петя

3

Ефим

Достижения (их визуальная настройка и правила работы)

ID

NAME

ICON

RULE

COUNT

DESCRIPTION

1

Участие в турнирах

1.svg

manual

 

Принять участие в турнире или чемпионате

2

Лайки новостей

2.svg

like_news

30

Полайкать 30 новостей на портале

3

Комментирование новостей

3.svg

comment_news

10

Прокомментировать 10 новостей на портале

4

Пасхалка

4.svg

easter_secret

3

Найти 3 пасхалки на портале

RULE — код правила достижения

COUNT — количество действий для получения текущего достижения

Как это выглядит в админке:

Полученные достижения (список достижений, который уже получили пользователи)

ID

ACHIEVEMENT

USER

DATE_TIME

COMMENT

1

2 (лайки новостей)

1 (Вася)

18-12-2022 18:00

 

2

1 (участие в турнирах)

2 (Петя)

19-12-2022 10:30

За победу в шахматном турнире

3

4 (пасхалка)

2 (Петя)

20-12-2022 10:10

 

4

2 (лайки новостей)

3 (Ефим)

20-12-2022 11:10

 

ACHIEVEMENT — достижение, ID из таблицы «Достижения»

USER — пользователь, ID из таблицы «Пользователи»

DATE_TIME — когда было получено достижение

COMMENT — комментарий для достижений с типом «ручное»

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

Правила достижений

Ручные правила назначаются, ясное дело, вручную администратором портала за какие-либо заслуги путём добавления записи в таблицу «Полученные достижения» с помощью CMS-интерфейса либо другим удобным способом (например, в том же разделе в CMS будет кнопка «Вручить достижение», где нужно будет указать вид достижения, вписать имя сотрудника и добавить комментарий).

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

Реализация добавления новых правил через интерфейс CMS была бы слишком громоздкой, так как правила могут быть совершенно разными и предусмотреть их все заранее невозможно. Наша внутрикорпоративная система лояльности сейчас развивается очень бурно; на данный момент на портале реализовано большое число сущностей, которые пополняются практически еженедельно.

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

Вместо этого я решил сделать классы-правила. Каждое
правило — это отдельный класс модуля с минимальным набором наследуемых
абстрактных свойств и методов, которые необходимо реализовать для корректной работы
правила.

Структура модуля:

Далее я разбираю каталог классов-правил из директории Rules, исключая абстрактные классы и интерфейсы, и вывожу в интерфейсе настроек достижений в виде html-селекта (отдельное кастомное свойство IblockPropertyRules.php), взяв названия из свойства класса $params[‘name’] и value из $params[‘code’].

Рассмотрим пример:

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

Achievements / lib / Achievements.php — основной класс модуля (получение списка достижений getList со списком правил с текущим прогрессом пользователя и метод проверки прогресса пользователя в момент целевого события-действия с отправкой выполненного достижения checkProgress). Метод checkProgress пробегается по всем правилам и проверяет, не выполнилось ли одно из них.

Achievements / lib / Rules / — здесь лежат классы-правила.

Вот один из них, отвечающий за комментарии в блоге новостей:


class CommentNews extends Base
{
    private static array $params = [
        'name' => 'Комментарии',
        'code' => 'comment_news',
        'auto' => true,
        'sort' => 200,
    ];
    public static function getParams(): array
    {
        return self::$params;
    }
    public static function getCountEventsByUser(int $userId = 0): int
    {
        $comments = CommentTable::query()
            ->addSelect('ID')
            ->where('USER_ID', self::getUserId($userId))
            ->where('BLOG', ‘news’)
            ->where('DATE_CREATE', '>=', ‘01-01-2022’)
            ->fetchAll();

        return count($comments);
    }

Выглядит довольно просто и компактно.

В каждом классе-правиле (по структуре они все одинаковые, кроме названия самого класса и параметров с выборкой) необходимо задать:

  • params — параметры правила: название (name), код (code), автоматическое (auto) или нет (true/false), сортировка правил (sort) для вывода в интерфейсе CMS;

  • getCountEventsByUser — метод для получения общего количества действий (в данном примере — оставленных комментариев в блоге) текущего или выбранного пользователя (таким образом мы вычисляем прогресс достижения, для которого установлено данное правило).

Ещё один пример класса-правила:

class LikeNews extends Base
{
   private static array $params = [
       'class' => self::class,
       'name' => 'Лайк новости',
       'code' => 'like_news',
       'auto' => true,
       'repeat' => true,
       'sort' => 300,
   ];

   public static function get(): array
   {
       self::$params['name'] = Loc::getMessage('IBS_M_ACHIEVEMENTS_RULES_LIKE_NEWS__NAME');
       return self::$params;
   }

   public static function getCountEventsByUser(int $userId = 0): int
   {
       $likes = RatingVoteTable::query()
           ->addSelect('ID')
           ->where('ENTITY_TYPE_ID', 'IBLOCK_ELEMENT')
           ->where('USER_ID', self::getUserId($userId))
           ->where('CREATED', '>=', new DateTime(self::$dateFrom))
           ->registerRuntimeField(
               'NEWS',
               new ReferenceField(
                   'NEWS',
                   ElementTable::class,
                   Join::on('this.ENTITY_ID', 'ref.ID')
                       ->where('ref.IBLOCK_ID', Helper::getIBlockID())
                   ,
                   ['join_type' => Join::TYPE_INNER]
               )
           )
           ->fetchAll();

       return count($likes);
   }

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

  • get — параметры класса-правила;

  • getCountEventsByUser — получение количества лайков.

Для подсчёта прогресса и автоматического вручения достижения необходимо использовать метод checkProgress, который будет срабатывать при каждом целевом действии (лайк, комментарий и т. п.). Система будет пробегаться по настроенным достижениям и привязанным к ним классам-правилам rule_code (таблица «Достижения») с auto=true, а также по количеству уже полученных достижений пользователя (таблица «Полученные достижения»). Далее из класса-правила будет получен прогресс действий getCountEventsByUser. После сверки с настройкой достижения COUNT (таблица «Достижения») из него будет вычтено количество полученных достижений соответствующего типа. В итоге текущий прогресс должен получиться больше либо равен настройке достижения.

Пример части метода checkProgress:


foreach ($achievements as $achievement) {
   // подсчет ранее полученных достижений
   $achievementCompletedCount = count(
       array_filter(
           $achievementsHistory,
           static fn($historyItem) => $historyItem['ACHIEVEMENT_ID'] == $achievement['ID']
       )
   );

   // правило текущего достижения
   $achievementRule = unserialize($achievement['RULE_VALUE'], ['allowed_classes' => false]);
   $rule = $rules[$achievementRule['type']];

   if ($rule['auto']) {
       // общий прогресс достижения
       if (!class_exists($rule['class'])) {
           continue;
       }
       $progressCount = $rule['class']::getCountEventsByUser($userId);

       // текущий прогресс с учетом ранее полученных достижений
       $progressCount -= $achievementCompletedCount * $achievementRule['count'];

       // кол-во неполученных достижений
       if ($progressCount >= $achievementRule['count']) {
           $achievementNotCompletedCount = floor($progressCount / $achievementRule['count']);
           // вручаем достижение пользователю
           while ($achievementNotCompletedCount) {
               AchievementsHistory::add(
                   $userId,
                   [
                       'ACHIEVEMENT' => $achievement,
                       'COMMENT' => '',
                       'NOTIFY' => true,
                   ]
               );
               $achievementNotCompletedCount--;
           }
       }
   }
}

Фронт

Напоследок — пример «пасхалки» на фронтенде. В нашем случае пасхалки — это запрятанные кнопки в ленте новостей на портале, этакий бонус для самых внимательных :)

Спасибо, что уделили время! Надеюсь, моя статья была вам полезна. Если кому-то интересны подробности реализации системы на CMS «1С-Битрикс», спрашивайте в комментариях. Если вы придумали альтернативное решение — пишите, очень интересно «сверить показания» :)

Комментарии (0)