Модель данных и назначение
Разработайте REST API расписания волонтёров на Go с PostgreSQL и миграциями, используя ручной SQL без ORM. Пользователь хранит id, email, password_hash; членство в организации задаёт роль organizer или volunteer. Shift содержит id, organization_id, title, location, starts_at, ends_at, capacity, status (open, closed, cancelled), created_by и timestamps. Signup содержит id, shift_id, volunteer_id, created_at и cancelled_at; пара shift/volunteer уникальна. Активная заявка имеет cancelled_at=NULL, снятые заявки остаются в истории и не занимают место. Время — timestamptz, интервалы полуоткрытые. Смена имеет положительную длительность до 12 часов и capacity от 1 до 500. Административная команда bootstrap-org требует флаги --organization-name, --organizer-email и --organizer-password, транзакционно создаёт организацию и первого organizer. README содержит синтаксис команды, обязательность и пределы флагов, уникальность email, поведение при повторном запуске и коды завершения: 0 при успехе, 1 при ошибке конфигурации или базы.
API, проверки и эксплуатация
POST /api/auth/register принимает organizationId, email, password и displayName, создаёт только volunteer в указанной организации и отвечает 201; неизвестная организация даёт 404, повторный email — 409. Публичный запрос никогда не принимает role и не создаёт organizer. Email уникален без учёта регистра, пароль длиной 12–128 символов и хранится как стойкий хэш. POST /api/auth/login возвращает 200 и создаёт случайную сессию с TTL 24 часа; POST /api/auth/logout отзывает текущую сессию и возвращает 204. Защищённые запросы без действующей сессии дают 401. POST /api/shifts разрешён organizer, проверяет поля и возвращает 201. PATCH /api/shifts/{id} доступен organizer своей организации и отвечает 200; уменьшение capacity ниже текущего signupCount возвращает 409 capacity_below_signup_count. DELETE /api/shifts/{id} отвечает 204 и выполняет мягкую отмену: сохраняет смену и заявки, устанавливая status=cancelled. Участник другой организации получает 404; пользователь той же организации с ролью volunteer при управлении сменой получает 403.
GET /api/shifts принимает from, to, status, cursor и limit 1–100, возвращает смены собственной организации в порядке starts_at,id вместе с signupCount активных заявок и remainingCapacity. from и to задаются в RFC 3339 UTC с from включительно и to исключительно; требуется from<to. POST /api/shifts/{id}/signups доступен volunteer: смена должна быть open и не завершившейся; пользователь может записаться один раз и не может иметь пересекающиеся активные заявки на другие смены. Перекрытие полуоткрытых интервалов возвращает 409 overlapping_shift, соседние интервалы разрешены. Проверка и вставка заявки выполняются атомарно. Одновременные заявки не могут превысить capacity: применяется блокировка смены или эквивалентная гарантия БД. Повторная заявка даёт 409 already_signed_up; заполненная/закрытая смена — 409 capacity_reached или invalid_state. DELETE /api/shifts/{id}/signups/me отменяет собственную заявку и освобождает место; cancelled смены сохраняют историю заявок, но не принимают новые. GET /api/shifts/{id}/signups доступен только organizer своей организации и возвращает записи с limit 1–100 и cursor. Единые ошибки 400/401/403/404/409 не раскрывают чужие смены. SQL-запросы используют тайм-аут и context cancellation; индексы покрывают организацию, время, статус и участника. Не выполняйте N+1 запрос для числа участников. HTTP задаёт таймауты и graceful shutdown; логи не содержат email.
Структура программы
Код разделён на небольшие internal-пакеты: internal/httpapi содержит обработчики и DTO, internal/schedule — роли, пересечения интервалов и вместимость, internal/postgres — смены и активные заявки. Проверка сессии отделена от правил расписания. main.go связывает приложение, запускает сервер и выполняет shutdown; правила и SQL-сценарии тестируются раздельно.