Разработка блока "хиты продаж" 1С-Битрикс
«Хиты продаж» — блок с товарами, которые покупают чаще всего. Работает на главной странице как витрина популярности, в каталоге — для ориентации покупателя, в карточке товара — как доверительный сигнал («другие выбирают это»). В Битрикс нет встроенного автоматического механизма расчёта хитов — ручная пометка свойства HIT не масштабируется. Нужна система, которая считает по данным.
Источники данных для расчёта
Хиты определяются по реальным продажам — из b_sale_basket + b_sale_order. Дополнительно можно учитывать просмотры карточек (если ведётся трекинг) с меньшим весом.
-- Топ продаваемых товаров за 30 дней
SELECT
b.product_id,
SUM(b.quantity) AS total_qty,
COUNT(DISTINCT b.order_id) AS total_orders,
SUM(b.price * b.quantity) AS total_revenue
FROM b_sale_basket b
JOIN b_sale_order o ON b.order_id = o.id
WHERE
o.canceled = 'N'
AND o.date_insert >= DATE_SUB(NOW(), INTERVAL 30 DAY)
AND b.product_id IS NOT NULL
GROUP BY b.product_id
ORDER BY total_orders DESC, total_qty DESC
LIMIT 100;
Ранжирование по total_orders (число заказов), а не по total_qty — иначе один заказ на 100 единиц поднимет товар выше, чем 50 разных заказов на 1 единицу. Количество уникальных заказов объективнее отражает популярность.
Многоуровневые хиты: весовая формула
Для более точного ранжирования — формула с несколькими факторами:
function calculateHitScore(array $stats, int $windowDays = 30): float {
$ordersWeight = 0.5;
$revenueWeight = 0.3;
$viewsWeight = 0.2;
// Нормализация: делим на максимальное значение в выборке
$normOrders = $stats['total_orders'] / ($stats['max_orders'] ?: 1);
$normRevenue = $stats['total_revenue'] / ($stats['max_revenue'] ?: 1);
$normViews = $stats['total_views'] / ($stats['max_views'] ?: 1);
// Временной коэффициент: недавние продажи ценнее
$recencyBoost = 1.0;
if ($stats['last_sale_days_ago'] <= 7) {
$recencyBoost = 1.2;
} elseif ($stats['last_sale_days_ago'] <= 14) {
$recencyBoost = 1.1;
}
return ($normOrders * $ordersWeight + $normRevenue * $revenueWeight + $normViews * $viewsWeight)
* $recencyBoost;
}
Таблица хитов и агент пересчёта
CREATE TABLE custom_hits (
product_id INT NOT NULL PRIMARY KEY,
score FLOAT NOT NULL,
total_orders INT DEFAULT 0,
total_qty INT DEFAULT 0,
total_revenue DECIMAL(12,2) DEFAULT 0,
category_rank INT, -- ранг внутри категории
is_hit TINYINT DEFAULT 1,
calculated_at DATETIME DEFAULT NOW(),
INDEX idx_score (score DESC),
INDEX idx_category_rank (category_rank)
);
Агент пересчитывает таблицу раз в сутки:
function RecalcHitsAgent(): string {
$connection = \Bitrix\Main\Application::getConnection();
// Очищаем таблицу и заполняем заново
$connection->truncateTable('custom_hits');
$data = calcSalesStats(30); // за 30 дней
$max = getMaxValues($data);
foreach ($data as $productId => $stats) {
$stats = array_merge($stats, $max);
$score = calculateHitScore($stats);
$connection->add('custom_hits', [
'product_id' => $productId,
'score' => $score,
'total_orders' => $stats['total_orders'],
'total_qty' => $stats['total_qty'],
'total_revenue' => $stats['total_revenue'],
'is_hit' => $score > 0.1 ? 1 : 0,
'calculated_at' => new \Bitrix\Main\Type\DateTime(),
]);
}
// Рассчитываем ранг внутри каждой категории
updateCategoryRanks();
return 'RecalcHitsAgent();';
}
Категориальные хиты
Помимо общего рейтинга — хиты по разделам каталога. В карточке товара блок показывает «Хиты в этой категории»:
function getCategoryHits(int $sectionId, int $limit = 8, int $excludeId = 0): array {
// Получаем товары раздела, отсортированные по рангу внутри категории
$hitIds = \Bitrix\Main\Application::getConnection()->query("
SELECT ch.product_id
FROM custom_hits ch
JOIN b_iblock_element ie ON ch.product_id = ie.id
WHERE ie.iblock_section_id = {$sectionId}
AND ie.active = 'Y'
AND ch.is_hit = 1
AND ch.product_id != {$excludeId}
ORDER BY ch.score DESC
LIMIT {$limit}
")->fetchAll();
return getProductsByIds(array_column($hitIds, 'product_id'));
}
Компонент и кеширование
// company:catalog.hits — component.php
$cacheKey = "hits_{$arParams['SECTION_ID']}_{$arParams['LIMIT']}";
$cache = \Bitrix\Main\Data\Cache::createInstance();
if ($cache->initCache(3600 * 6, $cacheKey, '/catalog/hits')) {
$arResult = $cache->getVars();
} elseif ($cache->startDataCache()) {
$arResult = getCategoryHits(
(int)$arParams['SECTION_ID'],
(int)$arParams['LIMIT'],
(int)$arParams['EXCLUDE_ID']
);
$cache->endDataCache($arResult);
}
Кеш на 6 часов — данные о хитах меняются раз в сутки, но частое обновление кеша не нужно.
Ручное управление: пометка хитов редактором
Помимо автоматических хитов — возможность ручной пометки. Менеджер заходит в карточку товара и ставит флаг «Редакционный хит». Такие товары всегда показываются в блоке, независимо от статистики. Используется для акционных и новых товаров.
// Свойство инфоблока EDITORIAL_HIT (тип: список, значения: Y/N)
// При формировании блока ручные хиты идут первыми
$manualHits = getEditorialHits($sectionId, $limit);
$autoHits = getCategoryHits($sectionId, $limit - count($manualHits), $excludeIds);
$result = array_merge($manualHits, $autoHits);
Отображение значка "Хит" на карточках
В шаблоне карточки товара и в листинге добавляем метку:
// В template.php листинга
if (!empty($arItem['PROPERTIES']['HIT']['VALUE'])) {
echo '<span class="product-badge product-badge--hit">Хит продаж</span>';
}
// Или через таблицу хитов — не требует свойства в инфоблоке
$isHit = isProductHit($arItem['ID']); // читает из custom_hits
Сроки
| Этап | Срок |
|---|---|
| SQL расчёта + таблица хитов | 1–2 дня |
| Агент пересчёта + весовая формула | 2–3 дня |
| Компонент (общий + категориальный) | 2–3 дня |
| Ручная пометка в административной части | 1–2 дня |
| Значки на карточках и в листинге | 1 день |
| Тестирование | 1–2 дня |
Итого: 1–1.5 недели.







