Модель данных и назначение
Создайте на 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 и базы.