Перейти к содержимому

Запись на услугу - слоты, занятость, защита от двойной брони

Делаем онлайн-запись на услугу: хранение броней, сборка свободных слотов дня и защита от двойной записи на одно время.

Что нужно знать заранее

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

Свободные слоты нигде не хранят, а считают на лету при каждом показе формы. График работы и длительность услуги дают весь список слотов дня, а брони вычитают из него занятые отрезки.

Две заявки на один и тот же слот приходят заметно чаще, чем кажется. Проверка занятости и запись брони - это два разных запроса, и между ними успевает вклиниться чужая заявка.

Шаги

  1. Завести хранилище броней с началом, концом отрезка и ссылкой на ресурс.
  2. Описать график работы каждого ресурса и длительность оказания каждой услуги.
  3. Собрать слоты дня и отсеять занятые одним запросом к базе.
  4. Записывать бронь с уникальным индексом на пару ресурса и времени.
  5. Отправить клиенту уведомление о записи и напоминание за сутки до приёма.

Решение

Собираем слоты рабочего дня:

$slots = [];
$cursor = new \Bitrix\Main\Type\DateTime($day . ' 10:00:00', 'd.m.Y H:i:s');
$end = new \Bitrix\Main\Type\DateTime($day . ' 19:00:00', 'd.m.Y H:i:s');
while ($cursor < $end) {
$slots[] = clone $cursor;
$cursor->add('+30 minutes'); // шаг равен длительности услуги
}

Границы дня берут из графика ресурса, а не пишут числами в коде. Иначе первая же суббота с другим расписанием потребует правки кода вместо правки настройки.

Отсеиваем занятое одним запросом:

$busy = BookingTable::getList(['filter' => ['=RESOURCE_ID' => $resourceId,
'><DATE_FROM' => [$dayStart, $dayEnd]], 'select' => ['DATE_FROM']])->fetchAll();
$taken = array_flip(array_map(fn($r) => $r['DATE_FROM']->format('H:i'), $busy));
$free = array_filter($slots, fn($s) => !isset($taken[$s->format('H:i')]));

Занятость проверяют одним запросом на весь день, а не запросом на каждый слот. Двадцать слотов в цикле дают двадцать запросов и заметную задержку на странице записи.

Защищаемся от двойной брони:

ALTER TABLE vendor_booking ADD UNIQUE INDEX ux_resource_time (UF_RESOURCE_ID, UF_DATE_FROM);
-- база отклонит вторую запись на то же время, даже если проверка её пропустила

Уникальный индекс - единственная надёжная защита от гонки заявок. Проверка в коде нужна для понятного сообщения, а окончательное слово остаётся за базой данных.

Записываем бронь и ловим отказ:

$result = BookingTable::add(['UF_RESOURCE_ID' => $resourceId,
'UF_DATE_FROM' => $slot, 'UF_CLIENT' => $phone]);
if (!$result->isSuccess()) {
echo 'Это время только что заняли, выберите другое'; // отказ по индексу
}

Сообщение об отказе пишут человеческим языком и сразу предлагают другое время. Ошибка базы в ответе выглядит для клиента как поломка сайта, а не как занятый слот.

Уведомляем клиента и мастера:

\CEvent::Send('VENDOR_BOOKING', SITE_ID, ['PHONE' => $phone,
'DATE' => $slot->format('d.m.Y H:i'), 'MASTER' => $master]);
// напоминание за сутки отправляет отдельный агент по тем же данным

Типичные проблемы

Два клиента записались на одно и то же время.

Проверка занятости и запись брони идут разными запросами, а между ними успевает чужая заявка. Настоящую защиту здесь даёт уникальный индекс на пару ресурса и времени.

Страница выбора времени открывается несколько секунд.

Занятость каждого слота проверяется своим отдельным запросом к базе данных сайта. Все брони дня забирают одним запросом и сравнивают в коде.

Клиент видит время не то, которое выбрал в форме.

Время сохранено в поясе сервера, а показано в поясе посетителя сайта. Для записи на услугу пояс фиксируют настройкой и не пересчитывают по посетителю.

Отменённая запись не освобождает слот.

Отмена меняет признак брони, а фильтр занятости этот признак не учитывает. Отменённые брони либо удаляют, либо явно исключают из выборки занятого времени.

В праздничный день сайт продолжает принимать записи.

Слоты считаются от графика работы без проверки дня по производственному календарю. Нерабочие дни отсеивают до сборки слотов, а не после неё.

Частые вопросы

Где хранить записи на услугу?

В отдельной таблице или highload-блоке с началом, концом и ссылкой на ресурс. Инфоблок здесь избыточен: разделы и права записям на приём не нужны.

Нужно ли хранить свободные слоты?

Нет, их считают на лету из графика работы и длительности услуги. Хранение свободных слотов заводит вторую копию расписания, которая рассинхронизируется.

Как учесть перерыв на обед?

Описать его отрезком в графике ресурса и вычитать так же, как бронь. Отдельной логики для перерывов при этом не появляется.

Что делать с записями на несколько услуг подряд?

Считать длительность как сумму услуг и проверять весь отрезок целиком. Проверка только первого слота пропускает наложение на следующую бронь.

Как напомнить клиенту о записи?

Агентом раз в час: он выбирает брони на завтра и отправляет письмо или сообщение. Признак отправки хранят в самой брони, иначе напоминание уйдёт дважды.

Смежное

Первоисточник