Разработка системы групповых покупок на 1С-Битрикс

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Предлагаемые услуги
Показано 1 из 1 услугВсе 1626 услуг
Разработка системы групповых покупок на 1С-Битрикс
Средняя
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1177
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Разработка на базе Битрикс, Битрикс24, 1С для компании Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    747
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    976

Разработка системы групповых покупок на 1С-Битрикс

Групповая покупка — механизм, при котором цена на товар снижается по мере роста числа участников. Коробочный Битрикс этого не умеет: модуль sale оперирует индивидуальными заказами, а механизм скидок b_catalog_discount не привязан к счётчику участников в реальном времени. Вся логика пишется поверх стандартной архитектуры.

Модель данных

Система строится вокруг сущности «акция групповой покупки». Удобнее всего хранить её в отдельном HL-блоке или кастомной таблице.

Таблица b_group_deal:

CREATE TABLE b_group_deal (
    ID INT AUTO_INCREMENT PRIMARY KEY,
    PRODUCT_ID INT NOT NULL,          -- ID элемента инфоблока / торгового предложения
    DATE_START DATETIME NOT NULL,
    DATE_END DATETIME NOT NULL,
    MIN_PARTICIPANTS INT NOT NULL,    -- минимум для активации скидки
    MAX_PARTICIPANTS INT,             -- ограничение участников (NULL = без лимита)
    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)
);

Таблица b_group_deal_tier — шкала скидок:

CREATE TABLE b_group_deal_tier (
    ID INT AUTO_INCREMENT PRIMARY KEY,
    DEAL_ID INT NOT NULL,
    PARTICIPANTS_FROM INT NOT NULL,   -- от N участников
    DISCOUNT_PERCENT DECIMAL(5,2),   -- скидка в %
    PRICE_FIXED DECIMAL(10,2),        -- или фиксированная цена
    FOREIGN KEY (DEAL_ID) REFERENCES b_group_deal(ID)
);

Таблица b_group_deal_participant — участники:

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,                     -- NULL пока заказ не подтверждён
    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 минут и проверяет акции с истёкшим DATE_END:

  • Если CURRENT_COUNT >= MIN_PARTICIPANTS → статус success. Все участники с waiting получают задание оформить заказ (или автоматически создаётся заказ по минимальной цене тира).
  • Если CURRENT_COUNT < MIN_PARTICIPANTS → статус failed. Участникам с paid выполняется возврат через \Bitrix\Sale\PaySystem\Manager::refund() или REST-запрос к платёжной системе.

Уведомления — через \Bitrix\Main\Mail\Event::send() с кастомными почтовыми шаблонами типа GROUP_DEAL_SUCCESS, GROUP_DEAL_FAILED.

Сроки реализации

Масштаб Функционал Срок
MVP (одна акция, одна ступень скидки, ручное управление) HL-блок + обработчики + AJAX-счётчик 1–1.5 недели
Полная система (тиры, агенты, возвраты, ЛК участника) Кастомные таблицы + транзакции + модуль + почтовые шаблоны 2–3 недели
Маркетплейс акций (несколько поставщиков, витрина) Полноценный модуль с административным интерфейсом + API 4–5 недель

Критические моменты — атомарность при обновлении счётчика и корректность возвратов при провале акции. Без транзакций и FOR UPDATE система неизбежно даёт неверные данные при конкурентных запросах.