← Все проекты уровня 7
Уровень 7 · Сервисы с базой данныхВариант A

Бронирование переговорных

Сервис бронирования комнат с атомарной защитой от пересечений и прозрачной отменой брони.

Техническое задание

Модель данных и назначение

Создайте на Go REST API бронирования переговорных с PostgreSQL и миграциями. SQL пишется вручную; ORM запрещён. Room хранит id, name, capacity, active и timestamps. Booking хранит id, room_id, owner_name, attendees, starts_at, ends_at, status и timestamps; attendees — положительное целое число. Время хранится в timestamptz и передаётся в RFC 3339 с часовым поясом. Интервалы полуоткрытые [starts_at, ends_at): бронь заканчивается ровно тогда, когда начинается следующая, поэтому эти две брони совместимы. starts_at должен быть раньше ends_at, длительность не превышает 12 часов, capacity комнаты должен покрывать число attendees, передаваемое при бронировании.

API, проверки и эксплуатация

POST /api/rooms создаёт комнату (201); name обязателен и уникален без учёта регистра после trim, capacity от 1 до 500. GET /api/rooms возвращает активные комнаты с limit/cursor; limit по умолчанию 20, максимум 100, cursor непрозрачный и стабильный. POST /api/rooms/{roomID}/bookings принимает ownerName длиной 1–120 символов после trim, attendees, startsAt и endsAt. Сервер возвращает 201 с бронью; неизвестная или отключённая комната — 404, неверные интервалы/вместимость — 400, конфликт времени — 409. При одновременных запросах для одной комнаты в активном состоянии может сохраниться только одна из пересекающихся броней; соседние полуоткрытые интервалы разрешены.

GET /api/bookings/{id} возвращает запись либо 404. GET /api/rooms/{roomID}/bookings принимает from, to и cursor; from/to — RFC 3339 UTC с from<to, иначе 400. Ответ содержит активные брони, пересекающие диапазон, в порядке starts_at,id. POST /api/bookings/{id}/cancel переводит активную бронь в cancelled один раз: первый запрос 200, повторный — 409 с кодом invalid_state. Отменённые брони не занимают время и сохраняются для аудита. Все изменения выполняются транзакционно, параметры SQL передаются отдельно, контекст каждого запроса получает конечный тайм-аут и соблюдает cancellation. Ошибки преобразуются в стабильный JSON и не раскрывают SQL. Запросы списков не создают N+1. Индексы покрывают комнату, статус и временные границы. Логи не содержат имён владельцев. HTTP имеет конечные таймауты и graceful shutdown.

Структура программы

Код разделён на небольшие internal-пакеты: internal/httpapi содержит обработчики и DTO, internal/booking — правила интервалов, вместимости и отмены, internal/postgres — SQL и миграции. main.go связывает пакеты и управляет сервером. Доменные типы независимы от DTO; правила тестируются без HTTP и базы.

Критерии готовности

Ожидаемый результат

Поставка

  • Запускаемый Go REST API использует PostgreSQL, миграции и параметризованный ручной SQL для комнат и бронирований.
  • README описывает интервалы, attendees, вместимость, cursor и коды ошибок; тесты используют локальную временную базу без внешней сети.

Приёмка

  • Две брони одной комнаты, где конец первой равен началу второй, обе получают 201.
  • Повтор имени комнаты без учёта регистра возвращает 409; offset после последней страницы даёт пустой список.
  • Параллельные пересекающиеся запросы дают ровно одну бронь с 201, остальные получают 409. Список по диапазону включает только пересекающиеся активные брони.
  • Attendees=0, endsAt не позже startsAt и превышение вместимости дают 400 без брони.
  • Отмена возвращает 200 и освобождает интервал; повторная отмена даёт 409, запись остаётся в истории.
  • Тест отменяет SQL-контекст и подтверждает прекращение запроса в установленный тайм-аут.
  • README показывает связи пакетов; правила брони проверяются без HTTP/БД, SQL-конкурентность — отдельными сценариями на локальной базе.