Реализация бронирования столиков ресторана на сайте
Бронирование столика — задача с несколькими нетривиальными деталями: вместимость стола должна совпадать с количеством гостей, ресторан работает по сменам (обед/ужин), а оверкинг (намеренная перепродажа) — иногда желаемое поведение.
Модель зала и столов
CREATE TABLE restaurant_halls (
id SERIAL PRIMARY KEY,
name VARCHAR(100), -- 'Основной зал', 'Терраса', 'VIP'
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE restaurant_tables (
id SERIAL PRIMARY KEY,
hall_id INTEGER REFERENCES restaurant_halls(id),
table_number VARCHAR(10),
min_guests SMALLINT DEFAULT 1,
max_guests SMALLINT NOT NULL,
is_combinable BOOLEAN DEFAULT FALSE, -- можно ли сдвигать со смежными
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE table_bookings (
id BIGSERIAL PRIMARY KEY,
table_id INTEGER REFERENCES restaurant_tables(id),
guest_name VARCHAR(255) NOT NULL,
guest_phone VARCHAR(50) NOT NULL,
guest_email VARCHAR(255),
guests_count SMALLINT NOT NULL,
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
special_wishes TEXT,
deposit_paid BOOLEAN DEFAULT FALSE,
deposit_amount NUMERIC(10,2),
source VARCHAR(30) DEFAULT 'website', -- 'website','phone','walkin'
created_at TIMESTAMP DEFAULT NOW()
);
Смены и временны́е слоты
Ресторан не бронирует стол «на 15:37» — только на смену или фиксированный слот:
SHIFTS = {
'lunch': {'start': time(12, 0), 'end': time(15, 0), 'duration': timedelta(hours=2)},
'dinner': {'start': time(18, 0), 'end': time(23, 0), 'duration': timedelta(hours=2)},
}
def get_booking_slots(date: date) -> list[dict]:
slots = []
for shift_name, shift in SHIFTS.items():
current = datetime.combine(date, shift['start'])
end_dt = datetime.combine(date, shift['end'])
while current + shift['duration'] <= end_dt:
slots.append({
'shift': shift_name,
'starts_at': current,
'ends_at': current + shift['duration'],
})
current += timedelta(hours=1) # слоты каждый час
return slots
Подбор стола по количеству гостей
def find_available_table(guests: int, starts_at: datetime, ends_at: datetime, hall_id: int = None):
query = """
SELECT t.*
FROM restaurant_tables t
WHERE t.min_guests <= %(guests)s
AND t.max_guests >= %(guests)s
AND t.is_active = TRUE
AND (%(hall_id)s IS NULL OR t.hall_id = %(hall_id)s)
AND t.id NOT IN (
SELECT table_id FROM table_bookings
WHERE status NOT IN ('cancelled', 'no_show')
AND tsrange(starts_at, ends_at, '[)') && tsrange(%(starts)s, %(ends)s, '[)')
)
ORDER BY t.max_guests ASC -- выбрать наименьший подходящий стол
LIMIT 1
"""
return db.fetchone(query, {
'guests': guests, 'hall_id': hall_id,
'starts': starts_at, 'ends': ends_at,
})
Объединение столов
Если нет стола на 8 человек, но есть два смежных стола по 4 — можно предложить объединение. Это требует таблицы смежности:
CREATE TABLE table_adjacency (
table_a INTEGER REFERENCES restaurant_tables(id),
table_b INTEGER REFERENCES restaurant_tables(id),
PRIMARY KEY (table_a, table_b)
);
Подтверждение и напоминания
| Время | Действие |
|---|---|
| Сразу | SMS/email клиенту с деталями брони |
| За 24 часа | SMS-напоминание |
| За 2 часа | SMS «Ждём вас сегодня в 19:00» |
| +30 мин после старта | Если не пришли — смена статуса на no_show, стол освобождается |
| +1 день | Email с просьбой об отзыве |
Депозит при бронировании (опционально) — интеграция с платёжным шлюзом. Депозит списывается, если гость не пришёл и не отменил за N часов.
Сроки реализации
Базовая система с одним залом, сменами и SMS/email уведомлениями — 6–8 рабочих дней. Несколько залов, объединение столов, депозит, управление через CMS, история брони клиента — 10–13 рабочих дней.







