Практически в каждой компании есть корпоративная система поощрений. Но вот как прописать для неё правила, да ещё и на Битриксе, — вопрос. Делюсь личным опытом.
Привет! Я 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С-Битрикс», спрашивайте в комментариях. Если вы придумали альтернативное решение — пишите, очень интересно «сверить показания» :)