Микросервис приёма и отбора заявок на проекты от внешних инициаторов.
Сервис реализован на Go с использованием:
net/httpи стандартногоServeMuxoapi-codegenдля generated роутинга и transport-типов- Postgres как основной БД
pgx/v5для доступа к БДsqlcдля статических SQL-запросовsquirrelиpgx.CollectRowsдля динамического списка заявокTaskfileдля локальных задач и CI- JWT для аутентификации
- Docker Compose для локального запуска
- GitHub Actions для CI
API-контракт задания находится в api.json.
- Публичная подача внешней заявки на проект
- Просмотр справочника типов проектов
- Логин внутренних пользователей
- Административный просмотр, принятие и отклонение заявок
- Инварианты жизненного цикла заявки:
PENDING -> ACCEPTEDPENDING -> REJECTED
- Миграции базы данных
- Idempotent demo seed
- Unit и integration tests
USERможет только логинитьсяADMINможет просматривать заявки, список заявок, принимать и отклонять их- Внешний инициатор подаёт заявку без авторизации
PENDINGACCEPTEDREJECTED
Причина отказа существует только для REJECTED и дополнительно защищена constraint-ами в БД.
Проект разделён на несколько слоёв:
-
cmd/service- bootstrap приложения
- создание HTTP-сервера
- подключение к БД
- запуск миграций и опционального demo seed
-
internal/domain- базовые сущности домена
- роли, actor, статусы, ошибки
-
internal/application- use-cases сервиса
- валидация входных данных
- проверка прав доступа на application boundary
- бизнес-правила переходов статусов
-
internal/platform/postgres- открытие
pgxpoolconnection - транзакции на
pgx.Tx - миграции
- seed
sqlc-generated queries для статических запросовsquirrel-based dynamic query для списка заявок
- открытие
-
internal/platform/auth- bcrypt password hashing
- JWT generation/parsing
-
internal/transport/httpoapi-codegengeneratedstd-http-server- HTTP handlers
- middleware
- strict JSON request parsing
- auth revalidation
- request id и logging
- Admin-права защищены на двух уровнях:
- transport-level middleware валидирует токен и перечитывает пользователя из БД
- application-layer use cases дополнительно требуют
domain.Actorи сами проверяют роль
- Изменение статуса заявки выполняется транзакционно с
SELECT ... FOR UPDATE - Раннер миграций использует Postgres advisory lock, чтобы не гоняться при конкурентном старте
- Статические SQL-запросы генерируются через
sqlc - Динамический список заявок строится через
squirrelи читается черезpgx.CollectRows - Routing и path/query binding берутся из
oapi-codegen, а strict JSON decode и error mapping остаются hand-written - Demo seed включается только через
ENABLE_DEMO_SEED=true - Для локального запуска, генерации и тестов используется
Taskfile
Основные таблицы:
usersproject_typesexternal_applicationsschema_migrations
Ключевые ограничения:
role IN ('ADMIN', 'USER')status IN ('PENDING', 'ACCEPTED', 'REJECTED')- согласованность
statusиrejection_reason - foreign key на
project_types
POST /login принимает логин и пароль и возвращает JWT.
Токен передаётся в заголовке:
X-API-TOKEN: <jwt>Даже после успешного логина admin-доступ не доверяет одному только JWT:
- пользователь перечитывается из БД на защищённых admin-ручках
- если пользователь удалён, сервис вернёт
401 - если роль больше не
ADMIN, сервис вернёт403
Основные ручки:
POST /loginGET /project/typePOST /project/application/externalGET /project/application/external/listGET /project/application/external/{applicationId}POST /project/application/external/{applicationId}/acceptPOST /project/application/external/{applicationId}/reject
POST /project/application/externalне принимаетrejectedReason- причина отказа передаётся только в
POST /project/application/external/{applicationId}/reject - запросы ограничены по размеру body
- сервис возвращает
X-Request-IDв response headers
docker compose up -dПосле старта сервис доступен на:
http://localhost:8000
Compose поднимает:
- приложение
- Postgres
В compose.yaml уже включён demo seed.
admin / admin123user / user123
Это demo-учётки для локальной проверки тестового задания.
Нужен доступный Postgres и переменные окружения:
export DATABASE_URL='postgres://postgres:postgres@127.0.0.1:5432/projects_service?sslmode=disable'
export JWT_SECRET='projects-service-secret'
export ENABLE_DEMO_SEED='true'
go run ./cmd/service-
HTTP_PORT- порт HTTP-сервера
- default:
8000
-
DATABASE_URL- строка подключения к Postgres
- обязательна
-
JWT_SECRET- секрет подписи JWT
- обязательна
-
ENABLE_DEMO_SEED- включает demo seed пользователей и типов проектов
- default:
false
task generatetask testЧто делает команда:
- поднимает
postgresчерез Docker Compose - запускает unit + integration tests
- прогоняет suite последовательно (
go test -p 1 ./...), чтобы избежать гонок за общей тестовой БД - останавливает контейнер после завершения
task test-unittask test-integrationПри необходимости можно переопределить БД:
task test TEST_DATABASE_URL='postgres://postgres:postgres@127.0.0.1:5432/projects_service?sslmode=disable'В репозитории настроен GitHub Actions pipeline:
- workflow запускается на
push - поднимает Postgres service container
- выполняет
task ci
Файл workflow: .github/workflows/ci.yaml
Что уже есть:
- request logging через
slog X-Request-ID- логирование
status,response_bytes,duration,user_id,error_category ReadHeaderTimeout,ReadTimeout,WriteTimeout,IdleTimeout- ограничение размера request body
- Запустить сервис:
docker compose up -d- Получить токен администратора:
curl -sS -X POST http://localhost:8000/login \
-H 'Content-Type: application/json' \
-d '{"login":"admin","password":"admin123"}'- Получить типы проектов:
curl -sS http://localhost:8000/project/type- Подать заявку:
curl -sS -X POST http://localhost:8000/project/application/external \
-H 'Content-Type: application/json' \
-d '{
"fullName":"Ivan Ivanov",
"email":"ivan@example.com",
"phone":"+7 (999) 123-45-67",
"organisationName":"ACME",
"organisationUrl":"https://acme.test",
"projectName":"New Venture",
"typeId":1,
"expectedResults":"Launch MVP",
"isPayed":true,
"additionalInformation":"Important details"
}'- Посмотреть список заявок:
curl -sS 'http://localhost:8000/project/application/external/list?active=true' \
-H 'X-API-TOKEN: <token>'- Принять или отклонить заявку:
curl -sS -X POST http://localhost:8000/project/application/external/1/accept \
-H 'X-API-TOKEN: <token>'или
curl -sS -X POST http://localhost:8000/project/application/external/1/reject \
-H 'Content-Type: application/json' \
-H 'X-API-TOKEN: <token>' \
-d '{"reason":"Out of scope"}'