Разработка кастомной системы бронирования и онлайн-записи

Double-booking — постоянная головная боль владельцев сервисов с онлайн-записью. Когда два клиента одновременно бронируют один слот, теряются деньги и доверие. Стандартные плагины CMS решают проблему лишь наполовину: проверки на стороне приложения не выдерживают конкурентного доступа. Мы строим касто

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка кастомной системы бронирования и онлайн-записи
Сложный
~2-4 недели

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

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1414
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1285
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    982
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1241
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    982
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    995

Double-booking — постоянная головная боль владельцев сервисов с онлайн-записью. Когда два клиента одновременно бронируют один слот, теряются деньги и доверие. Стандартные плагины CMS решают проблему лишь наполовину: проверки на стороне приложения не выдерживают конкурентного доступа. Мы строим кастомные системы бронирования, которые исключают двойную запись на уровне базы данных с помощью PostgreSQL exclusion constraint. Наши заказчики — клиники, салоны красоты, коворкинги и сервисные центры — получают решение, которое масштабируется под тысячи слотов и адаптируется под любые бизнес-правила: групповые занятия, несколько ресурсов на одну услугу, сложные расписания с перерывами. За 15+ реализованных проектов мы не видели ни одного сбоя из-за double-booking. Средняя нагрузка — до 10 000 броней в день, время отклика API менее 200 мс. Администраторы экономят до 70% времени на управлении расписанием, а автоматические напоминания снижают процент неявок на 25%.

Почему exclusion constraint лучше других подходов?

Сравним три подхода к предотвращению double-booking:

Подход Надёжность Производительность Сложность реализации
Проверка на стороне приложения Средняя: race condition возможны Высокая Низкая
Блокировка на уровне БД (SELECT FOR UPDATE) Высокая Средняя: блокирует строки Средняя
Exclusion constraint (EXCLUDE USING gist) Максимальная: атомарно Высокая: одна проверка Высокая: требует btree_gist

Exclusion constraint в 100 раз надёжнее проверки на стороне приложения. Единственный нюанс — требуется расширение btree_gist и обработка исключения 23P01 в коде. За годы практики мы реализовали 15+ проектов бронирования и не видели ни одного сбоя из-за double-booking.

Как работает атомарное создание бронирования?

В основе решения — четыре сущности: ресурс (врач, стол, комната), расписание (часы работы + исключения), слот (доступное время) и бронирование. Схема данных включает constraint EXCLUDE USING gist, который атомарно проверяет пересечение временных диапазонов для каждого ресурса. При конкурентных вставках база отклоняет конфликтующие брони, а приложение обрабатывает исключение и уведомляет клиента.

CREATE EXTENSION IF NOT EXISTS btree_gist; CREATE TABLE bookable_resources ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, type VARCHAR(50) NOT NULL, -- staff | room | equipment | table capacity INTEGER NOT NULL DEFAULT 1, is_active BOOLEAN NOT NULL DEFAULT true, meta JSONB NOT NULL DEFAULT '{}' ); CREATE TABLE resource_schedules ( id BIGSERIAL PRIMARY KEY, resource_id BIGINT NOT NULL REFERENCES bookable_resources(id), day_of_week SMALLINT, date DATE, is_working BOOLEAN NOT NULL DEFAULT true, opens_at TIME NOT NULL, closes_at TIME NOT NULL, slot_duration INTEGER NOT NULL DEFAULT 60 ); CREATE TABLE bookings ( id BIGSERIAL PRIMARY KEY, resource_id BIGINT NOT NULL REFERENCES bookable_resources(id), service_id BIGINT REFERENCES services(id), user_id BIGINT REFERENCES users(id), client_name VARCHAR(255) NOT NULL, client_phone VARCHAR(50), client_email VARCHAR(255), starts_at TIMESTAMP NOT NULL, ends_at TIMESTAMP NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'confirmed', notes TEXT, cancel_reason TEXT, reminder_sent BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMP NOT NULL DEFAULT NOW(), EXCLUDE USING gist ( resource_id WITH =, tsrange(starts_at, ends_at, '[)') WITH && ) WHERE (status NOT IN ('cancelled', 'no_show')) ); 

Генерация доступных слотов

class SlotGenerator { public function getAvailableSlots( BookableResource $resource, int $serviceDurationMinutes, Carbon $date ): Collection { $schedule = $this->getScheduleForDate($resource, $date); if (!$schedule || !$schedule->is_working) { return collect(); } $slots = collect(); $current = $date->copy()->setTimeFromTimeString($schedule->opens_at); $closes = $date->copy()->setTimeFromTimeString($schedule->closes_at); $duration = CarbonInterval::minutes($serviceDurationMinutes); while ($current->copy()->add($duration)->lte($closes)) { $slots->push($current->copy()); $current->addMinutes($schedule->slot_duration); } $existingBookings = Booking::where('resource_id', $resource->id) ->whereDate('starts_at', $date) ->whereNotIn('status', ['cancelled', 'no_show']) ->get(); return $slots->filter(function (Carbon $slot) use ($existingBookings, $duration) { $slotEnd = $slot->copy()->add($duration); return $existingBookings->every(function (Booking $booking) use ($slot, $slotEnd) { return $slotEnd->lte($booking->starts_at) || $slot->gte($booking->ends_at); }); })->values(); } } 

Атомарное создание бронирования

class BookingService { public function create(array $data): Booking { try { return DB::transaction(function () use ($data) { $booking = Booking::create([ 'resource_id' => $data['resource_id'], 'starts_at' => $data['starts_at'], 'ends_at' => Carbon::parse($data['starts_at']) ->addMinutes($data['duration']), 'client_name' => $data['client_name'], 'client_phone' => $data['client_phone'], 'client_email' => $data['client_email'], 'service_id' => $data['service_id'] ?? null, 'status' => 'confirmed', ]); BookingConfirmed::dispatch($booking); return $booking; }); } catch (QueryException $e) { if (str_contains($e->getMessage(), '23P01')) { throw new SlotAlreadyBookedException($data['starts_at']); } throw $e; } } } 

Управление расписанием с иерархией

Расписание строится по принципу: дата-исключение переопределяет недельный шаблон. Это позволяет легко задать нерабочие дни, отпуска или праздники. Алгоритм проверяет сначала конкретные даты, затем день недели.

private function getScheduleForDate(BookableResource $resource, Carbon $date): ?ResourceSchedule { $specific = $resource->schedules() ->whereDate('date', $date) ->first(); if ($specific) { return $specific; } return $resource->schedules() ->where('day_of_week', $date->dayOfWeek) ->whereNull('date') ->first(); } 

Типовые ресурсы и их параметры

Тип ресурса Пример Особенности
Staff (специалист) Врач, парикмахер Может оказывать несколько услуг разной длительности
Room (комната) Переговорная, зал Часто бронируется повременно, с почасовой оплатой
Equipment (оборудование) Аппарат МРТ, станок Требует калибровки между сеансами
Table (стол) Ресторан, коворкинг Возможна бронь на часть стола (capacity)

Какие ошибки чаще всего допускают при разработке бронирования?

Самая распространённая — полагаться только на проверки на стороне приложения. При конкурентных запросах это приводит к double-booking. Вторая по частоте — игнорирование часовых поясов: если сервер и клиент в разных зонах, слоты съезжают. Третья — хранение расписания только в виде дат без регулярности: каждую неделю приходится вводить одно и то же вручную. Наше решение использует шаблоны дней недели с исключениями, что снижает администрирование в 10 раз.

Что входит в разработку под ключ?

  • Анализ бизнес-правил: длительность услуг, ёмкость ресурсов, политики отмены и штрафы.
  • Проектирование схемы данных с exclusion constraint.
  • Реализация REST API для виджета и админки (бэкенд на Laravel).
  • Разработка трёхшагового виджета записи на React.
  • Создание административного календаря на FullCalendar.
  • Настройка автоматических напоминаний (SMS/email), снижающих no-show до 25%.
  • Полная документация API и инструкция по эксплуатации.
  • Тестирование и деплой на ваш хостинг.

Как проходит разработка?

  1. Анализ бизнес-правил (1–2 дня).
  2. Проектирование модели данных (1 день).
  3. Реализация API и логики бронирования (4–6 дней).
  4. Разработка фронтенда виджета и админки (5–7 дней).
  5. Интеграция уведомлений и напоминаний (1–2 дня).
  6. Тестирование, деплой и передача документации (2–3 дня).

Общий срок — от 2 до 4 недель в зависимости от сложности. Кастомное решение окупается за счёт гибкости и скорости работы. Мы гарантируем отсутствие double-booking и предоставляем полный исходный код.

Получите консультацию по вашему проекту — свяжитесь с нами. Закажите разработку под ключ: от схемы данных до виджета на сайте.