Разработка системы рейтингов товаров 1С-Битрикс
Рейтинг товара — агрегированная оценка, которая влияет на ранжирование в каталоге, выводится в карточке и в списках. В 1С-Битрикс есть модуль vote (голосования), но для рейтинга товаров он избыточен и плохо интегрируется с каталогом. Практичнее хранить оценки в отдельной таблице и агрегировать значение в свойство инфоблока.
Архитектура хранения оценок
Таблица b_product_vote для индивидуальных оценок:
| Поле | Тип | Назначение |
|---|---|---|
| ID | int | Первичный ключ |
| PRODUCT_ID | int | ID товара |
| USER_ID | int | ID пользователя (NULL = гость) |
| IP | varchar(45) | IP-адрес (для гостей и анти-накрутки) |
| RATING | tinyint | Оценка 1–5 |
| CREATED_AT | datetime | Когда проголосовал |
ORM-класс ProductVoteTable наследуется от \Bitrix\Main\ORM\Data\DataManager.
В инфоблоке товаров добавляются два свойства числового типа:
-
RATING_AVG— средняя оценка (float, обновляется после каждого голосования) -
RATING_COUNT— количество оценок
Это позволяет сортировать и фильтровать по рейтингу через стандартный CIBlockElement::GetList() без JOIN.
Алгоритм голосования
Голосование реализуется через AJAX-запрос. Компонент отдаёт форму со звёздочками, клик отправляет POST на контроллер:
// local/ajax/product_vote.php
\Bitrix\Main\Loader::includeModule('main');
\Bitrix\Main\Loader::includeModule('catalog');
$productId = (int)($_POST['product_id'] ?? 0);
$rating = (int)($_POST['rating'] ?? 0);
if ($rating < 1 || $rating > 5 || $productId <= 0) {
echo json_encode(['success' => false, 'error' => 'invalid_data']);
exit;
}
$userId = $GLOBALS['USER']->GetID() ?: null;
$ip = \Bitrix\Main\Context::getCurrent()->getRequest()->getRemoteAddress();
// Проверка: уже голосовал?
$existing = ProductVoteTable::getList([
'filter' => ['=PRODUCT_ID' => $productId, '=USER_ID' => $userId ?: false, '=IP' => $ip],
'limit' => 1,
])->fetch();
if ($existing && $userId === null) {
echo json_encode(['success' => false, 'error' => 'already_voted']);
exit;
}
Для авторизованных пользователей проверяем по USER_ID. Для гостей — по IP. Авторизованный пользователь может изменить свою оценку (обновляем существующую запись вместо создания новой).
Пересчёт агрегированного рейтинга
После каждого голосования обновляем агрегаты:
function updateProductRating(int $productId): void
{
$conn = \Bitrix\Main\Application::getConnection();
$row = $conn->query(
"SELECT AVG(RATING) as AVG_RATING, COUNT(*) as CNT
FROM b_product_vote
WHERE PRODUCT_ID = {$productId}"
)->fetch();
\CIBlockElement::SetPropertyValuesEx($productId, false, [
'RATING_AVG' => round((float)$row['AVG_RATING'], 2),
'RATING_COUNT' => (int)$row['CNT'],
]);
}
SetPropertyValuesEx работает быстрее, чем Update() всего элемента — он обновляет только указанные свойства.
Визуализация: звёздочный виджет
На фронтенде рейтинг отображается как набор SVG-звёзд. Логика частичного заполнения: для оценки 4.3 четыре звезды заполнены полностью, пятая — на 30%. Реализуется через CSS-clip или SVG-gradient с шириной, пропорциональной дробной части.
Компонент для вывода рейтинга в карточке товара и в списке принимает параметры:
$APPLICATION->IncludeComponent('custom:product.rating', '', [
'PRODUCT_ID' => $arResult['ID'],
'SHOW_FORM' => $USER->IsAuthorized() ? 'Y' : 'N',
'CURRENT_RATING' => $arResult['PROPERTIES']['RATING_AVG']['VALUE'],
'VOTE_COUNT' => $arResult['PROPERTIES']['RATING_COUNT']['VALUE'],
]);
Защита от накрутки
Ограничения по IP хороши для гостей, но не для организованных накруток:
- Для авторизованных — одна оценка на товар (жёсткая проверка по
USER_ID + PRODUCT_ID). - Ограничение: нельзя голосовать за товар, который ни разу не просматривался (проверка через
b_stat_sessionили кастомную таблицу просмотров). - Опционально: разрешить голосование только пользователям, купившим товар (аналогично верификации в системе отзывов).
Сроки разработки
| Масштаб | Состав | Срок |
|---|---|---|
| Базовый | ORM-модель, AJAX-голосование, звёздочный виджет, агрегация в свойство | 3–4 дня |
| Полный | Изменение оценки, защита от накрутки, сортировка в каталоге по рейтингу, история голосований | 5–7 дней |







