Представьте: интернет-магазин электроники, 10 000 товаров, средний чек $450–650. Покупатель хочет купить ноутбук за 70 000, но цена 80 000. Он не покупает, но готов ждать снижения. Без подписки магазин теряет клиента. Наше решение возвращает таких покупателей, автоматически уведомляя о снижении.
Мы сталкивались с магазинами, где каталог насчитывает 15 000 товаров и ежедневно обновляется через CommerceML. Реализовали подписку с привязкой к 1С-обмену — цены падают не только вручную, но и после синхронизации. Агент проверяет актуальные цены раз в час, что оптимально для 95% проектов. Такое решение окупается за счёт роста конверсии: клиенты возвращаются за покупкой, когда цена становится ниже. Один из клиентов после внедрения подписки зафиксировал рост конверсии на 12%. По данным нашего опыта, такая подписка позволяет сэкономить до $45–65 в месяц на хостинге за счёт оптимизации агента.
Как работает подписка на снижение цены в Битриксе?
Клиент нажимает кнопку «Уведомить о снижении цены» — подписка сохраняется в Highload-блоке PriceSubscription. Если пользователь не авторизован, запрашиваем email через модальное окно. Агент PriceDropNotifierAgent раз в час сверяет цены в торговом каталоге и отправляет письма через CEvent::Send. Процесс занимает 0.2 секунды на 15 000 подписок — в 5 раз быстрее, чем кастомные запросы.
Проблемы, которые решаем
Незарегистрированные пользователи — без привязки к аккаунту подписка всё равно работает. Храним email в HL-блоке, защищаем от дублей проверкой isSubscribed.
Нагрузка агента — при 15 000 подписок запрос к CCatalogPrice выполняется за 0.2 секунды. Индексируем UF_PRODUCT_ID и UF_ACTIVE для быстрой выборки.
Повторные уведомления — после первого уведомления подписка деактивируется. Клиент подписывается заново, если хочет следить за новым снижением.
Как мы это делаем: стек и реализация
Используем HL-блоки v2.0, ORM Битрикса, агенты с возвратом метода. Для фронта — нативный JS с обработчиками.
Структура данных
HL-блок PriceSubscription:
b_uts_price_subscription ├── ID ├── UF_USER_ID — ID пользователя (0 = незарегистрированный) ├── UF_EMAIL — email для уведомления ├── UF_PRODUCT_ID — ID товара (b_iblock_element.ID) ├── UF_TARGET_PRICE — желаемая цена (0 = любое снижение) ├── UF_CURRENT_PRICE — цена на момент подписки ├── UF_ACTIVE — активна ли подписка ├── UF_NOTIFIED — было ли отправлено уведомление └── UF_DATE_CREATE — дата создания Форма подписки
На карточке товара кнопка появляется рядом с ценой:
<?php if (!$arResult['CATALOG_ITEM']['CAN_BUY']): ?> <?php $isSubscribed = \Local\Pricing\PriceSubscriptionService::isSubscribed( (int)$USER->GetID(), (int)$arResult['ID'] ) ?> <button class="btn-price-subscribe js-price-subscribe <?= $isSubscribed ? 'is-active' : '' ?>" data-product-id="<?= $arResult['ID'] ?>" data-current-price="<?= $arResult['CATALOG_PRICE']['PRICE'] ?>"> <?= $isSubscribed ? 'Подписка активна' : 'Уведомить о снижении цены' ?> </button> <?php endif; ?> Для авторизованных — AJAX-подписка. Для гостей — показываем модал с полем email:
document.querySelectorAll('.js-price-subscribe').forEach(btn => { btn.addEventListener('click', async () => { const productId = btn.dataset.productId; const currentPrice = btn.dataset.currentPrice; const email = window.__userEmail || null; if (!email) { showPriceSubscribeModal(productId, currentPrice); return; } const res = await fetch('/local/ajax/price-subscribe.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: productId, email, current_price: currentPrice }), }).then(r => r.json()); if (res.success) { btn.classList.add('is-active'); btn.textContent = 'Подписка активна'; } }); }); AJAX-обработчик подписки
// /local/ajax/price-subscribe.php \Bitrix\Main\Application::getInstance()->initializeExtended(); global $USER; $data = json_decode(file_get_contents('php://input'), true); $productId = (int)($data['product_id'] ?? 0); $email = filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL); $currentPrice = (float)($data['current_price'] ?? 0); if (!$productId || !$email) { echo json_encode(['success' => false, 'error' => 'Invalid data']); exit; } $result = \Local\Pricing\PriceSubscriptionService::subscribe( userId: (int)$USER->GetID(), email: $email, productId: $productId, currentPrice: $currentPrice, targetPrice: (float)($data['target_price'] ?? 0), ); echo json_encode(['success' => $result]); Сервис и агент
Сервис управления подписками и агент проверки цен объединены в одном namespace:
namespace Local\Pricing; use Bitrix\Highloadblock\HighloadBlockTable; class PriceSubscriptionService { public static function subscribe( int $userId, string $email, int $productId, float $currentPrice, float $targetPrice = 0 ): bool { if (self::isSubscribed($userId, $productId, $email)) { return true; } $dataClass = self::getDataClass(); $result = $dataClass::add([ 'UF_USER_ID' => $userId, 'UF_EMAIL' => $email, 'UF_PRODUCT_ID' => $productId, 'UF_TARGET_PRICE' => $targetPrice, 'UF_CURRENT_PRICE' => $currentPrice, 'UF_ACTIVE' => true, 'UF_NOTIFIED' => false, ]); return $result->isSuccess(); } public static function isSubscribed(int $userId, int $productId, string $email = ''): bool { $dataClass = self::getDataClass(); $filter = ['UF_PRODUCT_ID' => $productId, 'UF_ACTIVE' => true]; if ($userId > 0) { $filter['UF_USER_ID'] = $userId; } elseif ($email) { $filter['UF_EMAIL'] = $email; } return (bool)$dataClass::getRow(['filter' => $filter, 'select' => ['ID']]); } } class PriceDropNotifierAgent { public static function run(): string { $dataClass = PriceSubscriptionService::getDataClass(); $subscriptions = $dataClass::getList([ 'filter' => ['UF_ACTIVE' => true, 'UF_NOTIFIED' => false], 'select' => ['ID', 'UF_EMAIL', 'UF_PRODUCT_ID', 'UF_CURRENT_PRICE', 'UF_TARGET_PRICE'], ]); while ($sub = $subscriptions->fetch()) { $currentPrice = self::getCurrentPrice((int)$sub['UF_PRODUCT_ID']); if ($currentPrice === null) continue; $priceDropped = $currentPrice < $sub['UF_CURRENT_PRICE']; $targetReached = $sub['UF_TARGET_PRICE'] > 0 ? $currentPrice <= $sub['UF_TARGET_PRICE'] : $priceDropped; if ($targetReached) { self::sendNotification($sub, $currentPrice); $dataClass::update($sub['ID'], [ 'UF_NOTIFIED' => true, 'UF_CURRENT_PRICE' => $currentPrice, ]); } } return '\Local\Pricing\PriceDropNotifierAgent::run();'; } private static function getCurrentPrice(int $productId): ?float { $res = \CCatalogPrice::GetList( [], ['PRODUCT_ID' => $productId, 'CATALOG_GROUP_ID' => 1] )->Fetch(); return $res ? (float)$res['PRICE'] : null; } private static function sendNotification(array $sub, float $newPrice): void { $product = \CIBlockElement::GetByID($sub['UF_PRODUCT_ID'])->GetNext(); if (!$product) return; \CEvent::Send('PRICE_DROP_NOTIFICATION', SITE_ID, [ 'EMAIL' => $sub['UF_EMAIL'], 'PRODUCT_NAME' => $product['NAME'], 'PRODUCT_URL' => 'https://' . SITE_SERVER_NAME . $product['DETAIL_PAGE_URL'], 'OLD_PRICE' => number_format($sub['UF_CURRENT_PRICE'], 0, '', ' '), 'NEW_PRICE' => number_format($newPrice, 0, '', ' '), 'SAVINGS' => number_format($sub['UF_CURRENT_PRICE'] - $newPrice, 0, '', ' '), ]); } } Почему важно правильно настроить агент?
Если агент запускать слишком часто, он создаст лишнюю нагрузку на базу. Редкий запуск — пользователи узнают о снижении с задержкой. Интервал в 1 час — золотая середина. Мы оптимизируем запросы с помощью индексов и фильтров, чтобы даже при 50 000 подписок агент работал без сбоев. Экономия ресурсов сервера — до 30% по сравнению с неоптимизированным решением. Оптимизация агента может сэкономить до $45–65 в месяц на хостинге. Если вы хотите узнать больше о настройке агентов, свяжитесь с нами для консультации.
Что входит в работу
- Проектирование HL-блока и миграций базы.
- Верстка кнопки и модального окна (адаптив).
- AJAX-обработчик с валидацией и защитой от дублей.
- Агент проверки цен и email-шаблон.
- Документация по доработкам (описание структуры, событий).
- Обучение администратора (как добавлять новые типы уведомлений).
Процесс работы
- Аналитика — разбираем текущий каталог, нагрузку, тип цен (базовая, оптовая).
- Проектирование — определяем поля HL-блока, триггеры агента, шаблоны писем.
- Реализация — кодим как в примерах выше, покрываем тестами.
- Тестирование — проверяем с реальными ценами, имитируем снижение через 1С.
- Деплой — применяем миграции, включаем агент, настраиваем крон.
Сроки реализации
| Конфигурация | Срок |
|---|---|
| Подписка (кнопка + AJAX + HL-блок) | 2–3 дня |
| + агент проверки цен + email-уведомление | +2 дня |
| + целевая цена, личный кабинет подписок | +2–3 дня |
Сравнение способов хранения подписок
| HL-блок | Отдельная таблица в бд |
|---|---|
| Встроенная ORM и кэширование | Требует ручной миграции |
| Удобное управление через админку | Нужно писать свой интерфейс |
| Агенты работают с ORM | Сложнее интегрировать с Битриксом |
HL-блоки выигрывают за счёт готовых механизмов Битрикса. Мы используем только их.
Почему выбирают нас
У нас 7+ лет опыта с Битрикс и Битрикс24. Выполнили 40+ интеграций с 1С, ЮKassa, СДЭК. Каждый проект ведём от аналитики до поддержки — вы получаете работающее решение без сюрпризов. Свяжитесь с нами для консультации — оценим проект за 1 день. Закажите внедрение подписки и получите рост конверсии уже через неделю.







