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

Наша компанія займається розробкою, підтримкою та обслуговуванням рішень на Бітрікс та Бітрікс24 будь-якої складності. Від простих односторінкових сайтів до складних інтернет-магазинів, CRM систем з інтеграцією 1С та телефонії. Досвід розробників підтверджено сертифікатами від вендора.
Пропоновані послуги
Показано 1 з 1 послугУсі 1626 послуг
Розробка системи групових покупок на 1С-Бітрікс
Середня
~1-2 тижні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851
  • 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
    585
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Розробка на базі 1С Підприємство для компанії МИРСАНБЕЛ
    751
  • image_crm_dolbimby_434_0.webp
    Розробка сайту на CRM Бітрікс24 для компанії DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Розробка на базі Бітрікс24 для компанії ТЕХНОТОРГКОМПЛЕКС
    989

Розробка системи групових покупок на 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 система неминуче дає неправильні дані при конкурентних запитах.