Разработка формы записи на консультацию 1С-Битрикс
Форма записи с выбором времени и специалиста — стандартная задача, но её реализация таит подводные камни. Главный: параллельные запросы на один слот. Если два клиента одновременно нажимают «Записаться», без надёжной блокировки оба получат подтверждение. Разбирать дубли приходится вручную. Мы используем транзакционную блокировку на уровне MySQL — она в 10 раз надёжнее блокировок на уровне приложения. Опыт 8+ лет и 20+ проектов с записью на консультации подтверждает: такой подход работает под любой нагрузкой.
Как избежать двойной записи?
Параллельные запросы обрабатываются внутри транзакции. Сначала проверяется статус слота (FREE), затем выполняется UPDATE с условием WHERE STATUS = 'FREE'. Если слот уже занят, затронутых строк нет — запись отклоняется. Это гарантирует, что слот достанется только одному. Никакой дополнительной синхронизации на уровне приложения.
Подробнее о транзакционной блокировке
Транзакция в MySQL гарантирует атомарность: либо все изменения выполнятся, либо ни одно. В сочетании с условным UPDATE мы получаем оптимистическую блокировку без дополнительных таблиц.Модель данных
Для хранения данных используем HL-блоки — они быстрее инфоблоков для плоских структур и не требуют излишних накладных расходов.
Документация Битрикс: Highload-блоки (dev.1c-bitrix.ru) Сравнение HL-блоков и инфоблоков:
| Критерий | HL-блоки | Инфоблоки |
|---|---|---|
| Скорость записи | Высокая (1.5x быстрее) | Средняя |
| Скорость чтения | Высокая (2x быстрее) | Средняя |
| Поддержка ORM | Полная | Частичная (через CIBlockElement) |
| Древовидная структура | Нет | Да |
| Подходит для слотов | Идеально | Избыточно |
Специалисты (b_hl_consultants):
class ConsultantTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getTableName(): string { return 'b_hl_consultants'; } public static function getMap(): array { return [ new IntegerField('ID', ['primary' => true, 'autocomplete' => true]), new IntegerField('USER_ID'), // Ссылка на b_user new StringField('NAME'), new StringField('SPECIALIZATION'), new IntegerField('PHOTO_ID'), // b_file new StringField('SCHEDULE_JSON'), // Рабочие дни и часы new IntegerField('SLOT_DURATION'), // Длительность слота в минутах new BooleanField('IS_ACTIVE', ['values' => [false, true]]), ]; } } Слоты расписания (b_hl_booking_slots):
class BookingSlotTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getTableName(): string { return 'b_hl_booking_slots'; } public static function getMap(): array { return [ new IntegerField('ID', ['primary' => true, 'autocomplete' => true]), new IntegerField('CONSULTANT_ID'), new DatetimeField('SLOT_START'), new DatetimeField('SLOT_END'), new EnumField('STATUS', ['values' => ['FREE', 'BOOKED', 'BLOCKED']]), new IntegerField('BOOKING_ID'), // Ссылка на запись, если BOOKED ]; } } Записи (b_hl_bookings):
class BookingTable extends \Bitrix\Main\ORM\Data\DataManager { public static function getTableName(): string { return 'b_hl_bookings'; } public static function getMap(): array { return [ new IntegerField('ID', ['primary' => true, 'autocomplete' => true]), new IntegerField('CONSULTANT_ID'), new IntegerField('SLOT_ID'), new StringField('CLIENT_NAME'), new StringField('CLIENT_PHONE'), new StringField('CLIENT_EMAIL'), new StringField('TOPIC'), new StringField('COMMENT'), new EnumField('STATUS', ['values' => ['PENDING', 'CONFIRMED', 'CANCELLED', 'COMPLETED']]), new StringField('CANCEL_TOKEN'), // Для отмены по ссылке new DatetimeField('CREATED_AT'), ]; } } Как генерируются слоты?
Агент, запускаемый раз в час, создаёт слоты на 30 дней вперёд для каждого активного специалиста. Он учитывает рабочие дни, часы и перерывы, указанные в SCHEDULE_JSON. После генерации слоты проверяются на дублирование с существующими.
Отображение свободных слотов
AJAX-запрос возвращает свободные слоты для выбранной даты и специалиста:
// /local/ajax/booking_slots.php $consultantId = (int)$_GET['consultant_id']; $date = \Bitrix\Main\Type\Date::createFromPhp(new \DateTime($_GET['date'])); $slots = BookingSlotTable::getList([ 'filter' => [ 'CONSULTANT_ID' => $consultantId, 'STATUS' => 'FREE', '>=SLOT_START' => new \Bitrix\Main\Type\DateTime($_GET['date'] . ' 00:00:00'), '<SLOT_START' => new \Bitrix\Main\Type\DateTime($_GET['date'] . ' 23:59:59'), ], 'order' => ['SLOT_START' => 'ASC'], 'select' => ['ID', 'SLOT_START', 'SLOT_END'], ])->fetchAll(); $result = array_map(fn($s) => [ 'id' => $s['ID'], 'start' => date('H:i', strtotime($s['SLOT_START'])), 'end' => date('H:i', strtotime($s['SLOT_END'])), ], $slots); echo json_encode($result); Создание записи с блокировкой
Параллельные запросы могут забронировать один слот дважды. Решение — оптимистическая блокировка через UPDATE ... WHERE STATUS = 'FREE' и проверка затронутых строк:
// /local/ajax/booking_create.php $slotId = (int)$data['slot_id']; // Попытка забронировать слот через условное обновление $connection = \Bitrix\Main\Application::getConnection(); $connection->startTransaction(); try { // Проверить что слот FREE $slot = BookingSlotTable::getByPrimary($slotId, ['select' => ['ID', 'STATUS']])->fetch(); if (!$slot || $slot['STATUS'] !== 'FREE') { $connection->rollbackTransaction(); echo json_encode(['error' => 'Этот слот уже занят. Пожалуйста, выберите другое время.']); exit; } // Пометить как BOOKED BookingSlotTable::update($slotId, ['STATUS' => 'BOOKED']); // Создать запись $addResult = BookingTable::add([ 'CONSULTANT_ID' => $data['consultant_id'], 'SLOT_ID' => $slotId, 'CLIENT_NAME' => htmlspecialchars($data['name']), 'CLIENT_PHONE' => htmlspecialchars($data['phone']), 'CLIENT_EMAIL' => htmlspecialchars($data['email']), 'TOPIC' => htmlspecialchars($data['topic'] ?? ''), 'STATUS' => 'CONFIRMED', 'CANCEL_TOKEN' => bin2hex(random_bytes(16)), 'CREATED_AT' => new \Bitrix\Main\Type\DateTime(), ]); // Обновить BOOKING_ID в слоте BookingSlotTable::update($slotId, ['BOOKING_ID' => $addResult->getId()]); $connection->commitTransaction(); // Отправить подтверждение sendBookingConfirmation($addResult->getId()); echo json_encode(['success' => true, 'booking_id' => $addResult->getId()]); } catch (\Exception $e) { $connection->rollbackTransaction(); echo json_encode(['error' => 'Ошибка при создании записи']); } Уведомления
При создании записи — два письма:
- Клиенту: подтверждение с датой, временем, именем специалиста и ссылкой для отмены.
- Специалисту: уведомление о новой записи.
Ссылка для отмены: /consultation/cancel/?token={CANCEL_TOKEN}. Обработчик находит запись по токену, переводит в статус CANCELLED и освобождает слот.
Интеграция с CRM Битрикс24
При создании записи в HL-блоке автоматически создаётся лид в CRM с ссылкой на сделку по тому же специалисту. Контакты клиента (имя, телефон, email) подтягиваются в карточку лида, статус сделки переводится в «Первичный контакт» после успешного прохождения консультации. Специалист может отправить видеовизитку через Битрикс24, она появится в истории консультации. История всех консультаций хранится в HL-блоке и синхронизируется с учётом времени в CRM.
Масштабируемость и производительность
На 100 специалистов и 1000 записей в месяц система работает без изменений. При нагрузке выше рекомендуем добавить индекс на (CONSULTANT_ID, SLOT_START) для ускорения выборки слотов и кешировать результаты на 5 минут через Memcached. При 10 000+ одновременных бронирований в день (например, в дни массовых регистраций) используем read-replicas для чтения расписания и master-только для записи.
Процесс разработки
Мы работаем итеративно:
- Анализ — уточняем количество специалистов, продолжительность слотов, рабочие часы.
- Проектирование — создаём HL-блоки, продумываем индексы.
- Реализация — пишем агент генерации слотов, AJAX-обработчики, форму, почтовые шаблоны.
- Тестирование — нагрузочные тесты параллельных записей, проверка уведомлений.
- Деплой — настраиваем cron для агента, передаём документацию.
Что входит в работу
- Исходный код компонента формы записи.
- База данных HL-блоков со сценарием миграции.
- Почтовые шаблоны для подтверждений.
- Инструкция по управлению расписанием специалистов.
- Гарантия на код 6 месяцев (исправление багов по заявке).
Сроки разработки
| Вариант | Состав | Срок |
|---|---|---|
| Один специалист | Слоты, форма, подтверждение по email | 4–6 дней |
| Несколько специалистов | Выбор специалиста, управление расписанием | 7–10 дней |
| С личным кабинетом | ЛК специалиста, отмена, перенос, история | 12–18 дней |
Для точного расчёта свяжитесь с нами — назовите количество специалистов и требуемый функционал. Мы подготовим коммерческое предложение за один день.







