Сайт медицинской клиники с формой «Оставьте заявку и мы перезвоним» теряет до 60% конверсии — пользователь хочет выбрать конкретного врача, конкретное время и получить подтверждение немедленно. Онлайн-запись с выбором слота — стандарт для любой современной клиники, причём лучшие реализации используют документированные компоненты 1С-Битрикс, обеспечивающие безопасность и масштабирование. Реализация на 1С-Битрикс позволяет связать расписание с МИС или управлять им внутри системы, если МИС нет. Наши инженеры с сертификацией Битрикс внедрили такие решения в 20+ клиниках, сократив время записи в среднем на 50%.
Экономия от внедрения: клиенты уменьшают нагрузку на колл-центр на 40% и получают окупаемость проекта за 6–8 месяцев. В сравнении с кастомным PHP-решением скорость внедрения на 1С-Битрикс выше в 2–3 раза, а стоимость поддержки — ниже на 30%. Готовые компоненты и API платформы ускоряют разработку: встроенная авторизация, уведомления, личный кабинет и защита от атак.
Техническая реализация онлайн-записи
Источник расписания — ключевой момент при проектировании. Рассмотрим два варианта.
Вариант A: Расписание в Битрикс. Администратор клиники управляет расписанием врачей через интерфейс в Битрикс. Записи хранятся в Битрикс и передаются в МИС (или не передаются — клиника без МИС). Подходит для небольших клиник без сложной МИС.
Вариант B: Расписание из МИС. Битрикс синхронизирует расписание из МИС каждые N минут. Записи создаются через API МИС. Сайт — только интерфейс, мастер-данные в МИС.
Далее описываем Вариант A — автономное расписание в Битрикс.
Структура таблиц
-- Шаблон рабочего времени врача CREATE TABLE local_doctor_schedule_template ( ID INT AUTO_INCREMENT PRIMARY KEY, DOCTOR_ID INT NOT NULL, -- ID элемента инфоблока «Врачи» DAY_OF_WEEK TINYINT NOT NULL, -- 1=Пн, 7=Вс TIME_FROM TIME NOT NULL, TIME_TO TIME NOT NULL, SLOT_DURATION INT DEFAULT 30, -- минут на приём ACTIVE CHAR(1) DEFAULT 'Y' ); -- Конкретные слоты (генерируются из шаблона) CREATE TABLE local_doctor_slots ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, DOCTOR_ID INT NOT NULL, SLOT_DATE DATE NOT NULL, SLOT_TIME TIME NOT NULL, STATUS ENUM('free','reserved','booked','blocked') DEFAULT 'free', APPOINTMENT_ID BIGINT, INDEX idx_doctor_date (DOCTOR_ID, SLOT_DATE, STATUS) ); -- Записи пациентов CREATE TABLE local_appointments ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, DOCTOR_ID INT NOT NULL, SLOT_ID BIGINT NOT NULL, USER_ID INT, -- NULL для незарегистрированных PATIENT_NAME VARCHAR(200), PATIENT_PHONE VARCHAR(20), PATIENT_EMAIL VARCHAR(200), SERVICE_ID INT, -- Услуга (инфоблок услуг) COMMENT TEXT, STATUS ENUM('pending','confirmed','cancelled','completed') DEFAULT 'pending', CREATED_AT DATETIME, CONFIRMED_AT DATETIME, CANCELLED_AT DATETIME ); Генерация слотов из шаблона
Агент, запускаемый ежедневно, генерирует слоты на следующие 30 дней:
function GenerateDoctorSlots(): string { $targetDate = (new \DateTime())->modify('+30 days'); $today = new \DateTime(); $templates = LocalDoctorScheduleTemplateTable::getList([ 'filter' => ['ACTIVE' => 'Y'], 'select' => ['DOCTOR_ID', 'DAY_OF_WEEK', 'TIME_FROM', 'TIME_TO', 'SLOT_DURATION'], ]); while ($tpl = $templates->fetch()) { $date = clone $today; while ($date <= $targetDate) { if ((int)$date->format('N') === (int)$tpl['DAY_OF_WEEK']) { generateSlotsForDay($tpl, $date); } $date->modify('+1 day'); } } return __FUNCTION__ . '();'; } function generateSlotsForDay(array $tpl, \DateTime $date): void { $from = new \DateTime($date->format('Y-m-d') . ' ' . $tpl['TIME_FROM']); $to = new \DateTime($date->format('Y-m-d') . ' ' . $tpl['TIME_TO']); $interval = new \DateInterval('PT' . $tpl['SLOT_DURATION'] . 'M'); $current = clone $from; while ($current < $to) { // Не создаём дубли $exists = LocalDoctorSlotsTable::getCount([ 'DOCTOR_ID' => $tpl['DOCTOR_ID'], 'SLOT_DATE' => $date->format('Y-m-d'), 'SLOT_TIME' => $current->format('H:i:s'), ]); if (!$exists) { LocalDoctorSlotsTable::add([ 'DOCTOR_ID' => $tpl['DOCTOR_ID'], 'SLOT_DATE' => $date->format('Y-m-d'), 'SLOT_TIME' => $current->format('H:i:s'), 'STATUS' => 'free', ]); } $current->add($interval); } } Компонент бронирования
Компонент /local/components/local/appointment.booking/ разработан на основе Компонент 2.0 с шагами:
Шаг 1 — Выбор врача/специализации. Фильтр по специализации из инфоблока врачей. AJAX-обновление списка врачей.
Шаг 2 — Выбор даты и времени. Календарь с подсвеченными доступными датами. При выборе даты — AJAX-запрос доступных слотов:
// AJAX-обработчик /local/ajax/get-slots.php $doctorId = (int)$_POST['doctor_id']; $date = $_POST['date']; // Y-m-d $slots = LocalDoctorSlotsTable::getList([ 'filter' => [ 'DOCTOR_ID' => $doctorId, 'SLOT_DATE' => $date, 'STATUS' => 'free', ], 'order' => ['SLOT_TIME' => 'ASC'], 'select' => ['ID', 'SLOT_TIME'], ])->fetchAll(); header('Content-Type: application/json'); echo json_encode(['slots' => $slots]); Шаг 3 — Форма пациента. Имя, телефон, email, комментарий. Для авторизованных — данные подставляются из профиля. Валидация номера телефона.
Шаг 4 — Подтверждение и бронирование.
public function bookSlot(int $slotId, array $patientData, int $serviceId = 0): int { $connection = \Bitrix\Main\Application::getConnection(); $connection->startTransaction(); try { // Атомарное резервирование слота $connection->queryExecute(" UPDATE local_doctor_slots SET STATUS = 'reserved' WHERE ID = ? AND STATUS = 'free' ", [$slotId]); if ($connection->getAffectedRowsCount() === 0) { throw new \RuntimeException('Этот слот уже занят'); } $appointmentId = LocalAppointmentsTable::add([ 'DOCTOR_ID' => $this->getSlotDoctorId($slotId), 'SLOT_ID' => $slotId, 'USER_ID' => $patientData['user_id'] ?? null, 'PATIENT_NAME' => $patientData['name'], 'PATIENT_PHONE' => $patientData['phone'], 'PATIENT_EMAIL' => $patientData['email'], 'SERVICE_ID' => $serviceId, 'COMMENT' => $patientData['comment'] ?? '', 'STATUS' => 'confirmed', ])->getId(); // Обновляем слот — статус и привязка к записи LocalDoctorSlotsTable::update($slotId, [ 'STATUS' => 'booked', 'APPOINTMENT_ID' => $appointmentId, ]); $connection->commitTransaction(); // Уведомления вне транзакции $this->sendConfirmationSms($patientData['phone'], $appointmentId); $this->sendConfirmationEmail($patientData['email'], $appointmentId); return $appointmentId; } catch (\Exception $e) { $connection->rollbackTransaction(); throw $e; } } Транзакция с UPDATE ... WHERE STATUS = 'free' и проверкой affectedRows — защита от race condition при одновременной записи двух пользователей на один слот. При атомарном UPDATE используется блокировка строки на уровне БД. В случае конфликта второй запрос получит исключение. Это стандартный паттерн обработки параллельных бронирований.
Как мы решаем проблему двойной записи?
Атомарные UPDATE с проверкой affectedRows внутри транзакции. Если два пользователя одновременно отправляют запрос, только один успешно обновит слот. Второй получит ошибку «Этот слот уже занят». Дополнительно блокируем строку в БД на время транзакции — это гарантирует консистентность.
Как отменить запись из личного кабинета?
Пациент может отменить запись не позднее чем за N часов до приёма:
public function cancelAppointment(int $appointmentId, int $userId): void { $appointment = LocalAppointmentsTable::getById($appointmentId)->fetch(); if (!$appointment || (int)$appointment['USER_ID'] !== $userId) { throw new \RuntimeException('Запись не найдена'); } $slot = LocalDoctorSlotsTable::getById($appointment['SLOT_ID'])->fetch(); $slotDateTime = new \DateTime($slot['SLOT_DATE'] . ' ' . $slot['SLOT_TIME']); if ($slotDateTime <= (new \DateTime())->modify('+2 hours')) { throw new \RuntimeException('Отмена записи возможна не позднее чем за 2 часа'); } LocalAppointmentsTable::update($appointmentId, ['STATUS' => 'cancelled']); LocalDoctorSlotsTable::update($appointment['SLOT_ID'], ['STATUS' => 'free', 'APPOINTMENT_ID' => null]); } Сравнение вариантов интеграции
| Характеристика | Расписание в Битрикс | Расписание из МИС |
|---|---|---|
| Сложность внедрения | Низкая — 3–5 недель | Высокая — 6–10 недель |
| Управление данными | Через админку Битрикс | Через МИС, Битрикс только интерфейс |
| Автономность | Полная | Зависимость от МИС |
| Подходит для | Клиник без МИС или с простой МИС | Клиник с существующей МИС |
| Затраты на разработку | на 50% ниже | выше из-за интеграции |
Что входит в работу
- Проектирование структуры таблиц и агентов
- Разработка и установка компонента бронирования (выбор врача → дата → слот → форма → подтверждение)
- Настройка агента генерации слотов
- AJAX-обработчики для динамического обновления слотов
- Реализация защиты от race condition
- Интеграция SMS/email-уведомлений и напоминаний
- Личный кабинет пациента с историей и отменой записи
- Документация по API и администрированию
- Обучение администраторов (2–4 часа)
- Техническая поддержка на 30 дней после запуска
Процесс внедрения и сроки
- Аналитика — изучаем текущее расписание, загрузку врачей, МИС (если есть). Оцениваем объём: в среднем 30 врачей генерируют 600 слотов в день.
- Проектирование — согласовываем структуру данных, логику слотов. Создаём прототип интерфейса.
- Реализация — пишем код, настраиваем компоненты. Включаем агент генерации слотов на 30 дней вперёд.
- Тестирование — проверяем на реальных сценариях: двойная запись, перенос, отмена. Проводим нагрузочное тестирование до 1000 одновременных запросов.
- Деплой — выкатываем на бой, настраиваем права доступа. Мониторим логи первые 24 часа.
- Обучение — показываем администраторам интерфейс и резервное копирование. Предоставляем документацию.
Сроки: 3–5 недель автономная система без МИС. 6–10 недель с интеграцией МИС.
Свяжитесь с нами для расчёта точных сроков под ваш проект. Получите консультацию сертифицированного инженера по 1С-Битрикс.







