Разработка системы групповых покупок на 1С-Битрикс — задача, с которой к нам обращаются, когда стандартные скидки не работают. Коробочный Битрикс не умеет привязывать скидку к числу участников: модуль sale оперирует индивидуальными заказами, а b_catalog_discount статичен. Мы строим всю логику поверх стандартной архитектуры, используя кастомные таблицы и транзакции. Типичный кейс: интернет-магазин запускает акцию «чем больше покупателей, тем ниже цена». Без доработки каждый новый участник оформляет заказ по одной цене. Мы добавляем систему, которая динамически пересчитывает стоимость и синхронизирует счётчик участников в реальном времени. Наша команда имеет 10+ лет опыта в разработке на 1С-Битрикс и десятки успешных проектов. Мы знаем все подводные камни и готовы реализовать систему под ключ за 1–5 недель.
Система групповых покупок
Система строится вокруг сущности «акция групповой покупки». Удобнее всего хранить её в отдельном HL-блоке или кастомной таблице. Пример структуры:
CREATE TABLE b_group_deal ( ID INT AUTO_INCREMENT PRIMARY KEY, PRODUCT_ID INT NOT NULL, DATE_START DATETIME NOT NULL, DATE_END DATETIME NOT NULL, MIN_PARTICIPANTS INT NOT NULL, MAX_PARTICIPANTS INT, CURRENT_COUNT INT DEFAULT 0, STATUS ENUM('active','success','failed','closed') DEFAULT 'active', INDEX idx_product (PRODUCT_ID), INDEX idx_status_date (STATUS, DATE_END) ); CREATE TABLE b_group_deal_tier ( ID INT AUTO_INCREMENT PRIMARY KEY, DEAL_ID INT NOT NULL, PARTICIPANTS_FROM INT NOT NULL, DISCOUNT_PERCENT DECIMAL(5,2), PRICE_FIXED DECIMAL(10,2), FOREIGN KEY (DEAL_ID) REFERENCES b_group_deal(ID) ); CREATE TABLE b_group_deal_participant ( ID INT AUTO_INCREMENT PRIMARY KEY, DEAL_ID INT NOT NULL, USER_ID INT NOT NULL, ORDER_ID INT, DATE_ADD DATETIME NOT NULL, STATUS ENUM('waiting','paid','cancelled','refunded') ); Счётчик CURRENT_COUNT обновляется только по статусу paid — предварительные участия без оплаты не считаются.
Защита от race condition
Главная инженерная проблема — одновременное присоединение участников. Если два клиента читают CURRENT_COUNT = 9 при MIN_PARTICIPANTS = 10, оба могут стать «активирующим» участником. Защита — атомарное обновление:
use Bitrix\Main\Application; $connection = Application::getConnection(); $connection->startTransaction(); try { $row = $connection->query( "SELECT * FROM b_group_deal WHERE ID = {$dealId} AND STATUS = 'active' FOR UPDATE" )->fetch(); if (!$row || strtotime($row['DATE_END']) < time()) { $connection->rollbackTransaction(); return ['error' => 'Deal not available']; } $connection->query( "INSERT INTO b_group_deal_participant (DEAL_ID, USER_ID, DATE_ADD, STATUS) VALUES ({$dealId}, {$userId}, NOW(), 'waiting')" ); $connection->query( "UPDATE b_group_deal SET CURRENT_COUNT = CURRENT_COUNT + 1 WHERE ID = {$dealId}" ); $connection->commitTransaction(); } catch (\Exception $e) { $connection->rollbackTransaction(); throw $e; } После присоединения участник получает статус waiting. Оплата происходит двумя сценариями:
Сценарий A — предоплата: участник сразу оформляет заказ и платит. Если акция не набирает MIN_PARTICIPANTS к DATE_END, деньги возвращаются автоматически через обработчик агента.
Сценарий B — отложенный заказ: участник бронирует место без оплаты. При достижении минимума всем участникам уходит уведомление с предложением оформить заказ по сниженной цене. Дедлайн — 24–48 часов.
Сравнение сценариев:
| Сценарий | Оплата | Риск для покупателя | Риск для магазина |
|---|---|---|---|
| Предоплата | Сразу | Деньги заморожены до завершения акции | Возвраты при провале |
| Отложенный заказ | После достижения минимума | Нет риска, но нужно следить за уведомлением | Часть участников может не оформить заказ |
Ценообразование и скидки
Текущая скидка рассчитывается динамически по таблице b_group_deal_tier. Нельзя использовать стандартную систему скидок b_catalog_discount — она не умеет работать с динамическим счётчиком. Расчёт активного тира:
function getActiveTier(int $dealId, int $currentCount): ?array { $connection = Application::getConnection(); return $connection->query( "SELECT * FROM b_group_deal_tier WHERE DEAL_ID = {$dealId} AND PARTICIPANTS_FROM <= {$currentCount} ORDER BY PARTICIPANTS_FROM DESC LIMIT 1" )->fetch() ?: null; } При добавлении в корзину цена подставляется через обработчик OnSaleBasketItemRefreshData.
Визуальный прогресс-бар
Компонент прогресса — AJAX-виджет, обновляемый каждые 30 секунд. Данные отдаёт контроллер:
// /local/ajax/group-deal-status.php $deal = $connection->query( "SELECT gd.*, gt.DISCOUNT_PERCENT, gt.PARTICIPANTS_FROM as NEXT_TIER FROM b_group_deal gd LEFT JOIN b_group_deal_tier gt ON gt.DEAL_ID = gd.ID AND gt.PARTICIPANTS_FROM > gd.CURRENT_COUNT WHERE gd.ID = {$dealId} ORDER BY gt.PARTICIPANTS_FROM ASC LIMIT 1" )->fetch(); header('Content-Type: application/json'); echo json_encode([ 'current' => (int)$deal['CURRENT_COUNT'], 'next_tier' => (int)$deal['NEXT_TIER'], 'discount' => (float)$deal['DISCOUNT_PERCENT'], 'time_left' => strtotime($deal['DATE_END']) - time(), ]); Прогресс-бар отображает процент current / next_tier * 100 и показывает, сколько участников нужно до следующей ступени скидки.
Завершение акции и возвраты
Агент CAgent запускается каждые 5 минут (см. Bitrix Documentation) и проверяет акции с истёкшим DATE_END:
- Если
CURRENT_COUNT >= MIN_PARTICIPANTS→ статусsuccess. Участники сwaitingполучают задание оформить заказ. - Если
CURRENT_COUNT < MIN_PARTICIPANTS→ статусfailed. Участникам сpaidвыполняется возврат через\Bitrix\Sale\PaySystem\Manager::refund().
Уведомления отправляются через \Bitrix\Main\Mail\Event::send() с кастомными шаблонами.
Что входит в работу
- Анализ требований и проектирование схемы данных
- Реализация кастомных таблиц и ORM-моделей
- Настройка агентов и почтовых шаблонов
- Интеграция с платёжными системами (54-ФЗ, ОФД)
- Тестирование на конкурентную нагрузку
- Документация и обучение администраторов
- Гарантийная поддержка после запуска
Сроки реализации
| Масштаб | Функционал | Срок |
|---|---|---|
| MVP (одна акция, одна ступень скидки, ручное управление) | HL-блок + обработчики + AJAX-счётчик | 1–1.5 недели |
| Полная система (тиры, агенты, возвраты, ЛК участника) | Кастомные таблицы + транзакции + модуль + почтовые шаблоны | 2–3 недели |
| Маркетплейс акций (несколько поставщиков, витрина) | Полноценный модуль с административным интерфейсом + API | 4–5 недель |
Нагрузочное тестирование и безопасность
Перед запуском системы групповых покупок необходимо протестировать конкурентный сценарий: несколько десятков одновременных присоединений к одной акции. Без этого race condition обнаруживается только на реальном трафике, когда CURRENT_COUNT выходит за пределы MAX_PARTICIPANTS.
Тест: инструментом Apache JMeter или k6 имитируем 50 одновременных запросов к endpoint-у присоединения. Ожидаемый результат — ровно MAX_PARTICIPANTS записей в b_group_deal_participant со статусом waiting. Если записей больше — транзакции не отрабатывают корректно. Проверьте уровень изоляции MySQL (REPEATABLE READ) и наличие блокировки FOR UPDATE в запросе выборки акции. При использовании MariaDB дополнительно убедитесь, что включён режим строгих транзакций (STRICT_TRANS_TABLES).
Дополнительно: добавьте rate limiting на endpoint присоединения — не более 3 запросов с одного IP в секунду. Это защищает от ботов, которые могут заполнить акцию фиктивными участниками.
Свяжитесь с нами для оценки вашего проекта. Получите консультацию по реализации системы групповых покупок.







