https://github.com/aventhis/practice_avito
Разбираю тестовые задания авито
https://github.com/aventhis/practice_avito
Last synced: about 1 year ago
JSON representation
Разбираю тестовые задания авито
- Host: GitHub
- URL: https://github.com/aventhis/practice_avito
- Owner: aventhis
- Created: 2025-04-22T12:00:23.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2025-05-06T17:43:19.000Z (about 1 year ago)
- Last Synced: 2025-05-09T02:13:49.897Z (about 1 year ago)
- Language: Go
- Size: 58.6 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# 🚀 Пошаговое README: Как я строила инфраструктуру проекта
Этот README не стандартный. Это **моя инженерная хроника** — чтобы я сама (и любой, кто будет смотреть проект) могла понять:
* как я пришла к текущему решению,
* что я сделала в какой момент,
* и почему это имеет смысл.
---
## ✅ Шаг 1. Старт проекта
* Прочитала задание от Авито.
* Поняла, что сервис будет состоять минимум из двух компонентов:
* backend-приложение на Go;
* база данных PostgreSQL.
* Приняла решение использовать Docker и Docker Compose.
---
## ✅ Шаг 2. Написание Dockerfile (multi-stage)
* Использовала **двухэтапную сборку** для оптимизации:
* Сначала сборка Go-приложения в `golang:1.21-alpine`;
* Затем перенос только бинарника в `alpine:latest`.
* Установила `CGO_ENABLED=0` и `-ldflags="-s -w"`, чтобы сделать бинарник лёгким и безопасным.
* Настроила `WORKDIR`, `COPY`, `RUN go build`, `CMD`, `EXPOSE`.
* Проверила, что Dockerfile рабочий и адекватно собирается.
---
## ✅ Шаг 3. Составление docker-compose.yml
* Указала версию схемы: `version: "3.9"`.
* Добавила два сервиса:
* `app`: билд из Dockerfile, порт 8080, `depends_on`, `restart`, `environment` с переменными из `.env`;
* `db`: образ `postgres:14`, переменные среды, порт 5432, volume, `restart`, `healthcheck`.
---
## ✅ Шаг 4. Обсуждение стратегии запуска
* Поняла, что обычный `depends_on` **не ждёт готовности базы**.
* Добавила `healthcheck` для `postgres`, чтобы Docker понимал, когда он готов.
* Изучила, что можно использовать `depends_on: condition: service_healthy`, и **осознанно приняла решение его добавить** — для надёжного запуска `app` после `postgres`.
---
## ✅ Шаг 5. Подключение .env файла
* Создала файл `.env`, чтобы не хранить чувствительные данные в `docker-compose.yml`.
* Вынесла туда:
* `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_PORT`, `DB_HOST`, `DB_SSL_MODE`
* `APP_ENV`, `APP_PORT`, `GRPC_PORT`, `METRICS_PORT`
* Убедилась, что Docker Compose подхватывает `.env` автоматически.
* Добавила `.env` в `.gitignore`.
---
## ✅ Шаг 6. Отказ от DB\_URL в пользу отдельных переменных
* Решила не использовать `DB_URL` напрямую, чтобы избежать дублирования и повысить гибкость.
* Использую отдельные переменные окружения и собираю строку подключения в Go-коде с помощью `os.Getenv()`.
* Это упрощает поддержку и переключение между окружениями.
---
## ✅ Шаг 7. Добавление APP\_ENV и системных переменных
* Добавила `APP_ENV`, чтобы в будущем различать `development`, `staging`, `production` режимы.
* Подготовила `APP_PORT`, `GRPC_PORT`, `METRICS_PORT` — на случай, если потребуется раздельная маршрутизация.
* В коде можно использовать `os.Getenv("APP_ENV")` для смены логики поведения.
---
## ✅ Шаг 8. Проброс портов через .env
* В секции `ports:` использую переменные `${APP_PORT}`, `${GRPC_PORT}`, `${METRICS_PORT}` вместо жёстко заданных чисел.
* Это позволяет быстро переключать порты через `.env`, без правок в `docker-compose.yml`.
* Переменные также передаются в `environment`, чтобы Go-приложение знало, на каких портах слушать.
---
## ✅ Шаг 9. Создание .env.example для проверяющего
* Чтобы проверяющий мог легко запустить проект, создала файл `.env.example` с безопасными значениями по умолчанию.
* Он лежит в репозитории, и его можно скопировать в `.env` с помощью команды:
```bash
cp .env.example .env
```
* Это позволяет запускать проект без утечки чувствительных данных и без лишних шагов.
---
## ✅ Шаг 10. Начало main.go — конфигурация
* Начала реализацию проекта с `main.go`, как с точки входа и дирижёра системы.
* Задала переменные окружения с помощью `os.Getenv` + функция `getEnv` с дефолтами.
* Включила: `APP_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_PORT`, `DB_HOST`, `DB_SSL_MODE`.
* Добавила `JWT_SECRET` — с защитой: если не задан, выводится предупреждение и используется `dev-secret-key`.
* Проверила и логично оформила преобразования (например, `strconv.Atoi` для портов).
---
## ✅ Шаг 11. Вынос конфигурации в отдельный пакет config
* Создала отдельный модуль `config`, который инкапсулирует работу с переменными окружения.
* Разделила структуру на два блока: `DatabaseConfig` и `ServerConfig`, вложенные в `Config`.
* Добавила генерацию строки подключения `DSN` на этапе загрузки конфига.
* Обработала `JWT_SECRET` с предупреждением, если он не задан.
* В `main.go` подключаю конфиг с помощью `config.LoadConfig()` и передаю `cfg.Database.DSN` в `storage.NewStorage`.
* Привела переменные к единому стилю и исправила возможные ошибки (русская `с`, `%v` в `log.Fatalf`).
---
## ✅ Шаг 12. Проверка готовности базы данных
* После создания `storage` вызываю `storage.Ping()` для проверки соединения с базой.
* Если база недоступна, приложение логирует ошибку и завершает работу.
* Затем вызываю `storage.InitDB()` для инициализации таблиц и подготовки схемы базы данных.
* Таким образом, на этапе запуска программы база всегда проверяется и готовится к работе.
---
## ✅ Шаг 13. Создание структуры Storage
* В `internal/storage/storage.go` создала структуру `Storage` с полем `db *sql.DB`.
* Реализовала конструктор `NewStorage(DSN string)`, который открывает соединение с базой и сразу проверяет его через `Ping()`.
* Подключение теперь полностью централизовано через `storage`.
---
## ✅ Шаг 14. Реализация метода InitDB
* Внутри `Storage` реализовала метод `InitDB()`.
* При старте приложения автоматически создаётся таблица `users`, если она отсутствует.
* Таблица включает в себя:
* UUID ID;
* Email;
* Хэш пароля;
* Роль (`employee`, `moderator`) с проверкой через `CHECK`;
* Время создания записи.
* Запрос максимально адаптирован для запуска без дополнительных расширений PostgreSQL.
---
## ✅ Шаг 15. Создание модели User
* В `internal/models/models.go` создала структуру `User`.
* В неё входят поля: `ID`, `Email`, `PasswordHash`, `Role`, `CreatedAt`.
* Используются только `db:"..."` теги для корректной работы с базой данных.
* Структура соответствует требованиям задания и правильной архитектуре: хранит только внутренние данные.
---
## ✅ Шаг 16. Реализация dummyLoginHandler
* Добавила в `internal/api/api.go` хендлер `dummyLoginHandler`.
* Создала вспомогательную структуру `DummyLoginRequest` в `models`, с единственным полем `Role`.
* В случае невалидного JSON или недопустимой роли — возвращается ошибка с соответствующим сообщением.
* Ответы стандартизированы: структура `ErrorResponse` в `models` и функция `respondWithError()` в `api`.
* Все тексты ошибок вынесены в отдельный файл `errors.go`, что упрощает поддержку и локализацию.
* Статус `200 OK` теперь устанавливается явно через `WriteHeader`.
* Возвращается структура `TokenResponse`, содержащая JWT-токен.
---
## ✅ Шаг 17. Реализация GenerateToken
* Внутри `auth.go` реализовала метод `GenerateToken(role Role)`.
* Создала структуру `CustomClaims`, в которую входят поле `Role` и встраиваются `jwt.RegisteredClaims`.
* В `RegisteredClaims` задала поля `IssuedAt` и `ExpiresAt` — токен действует 24 часа.
* Использовала `jwt.NewWithClaims(...)` и `token.SignedString(...)`, чтобы создать и подписать токен.
* Подпись выполняется с помощью `jwtSecret`, передаваемого в `AuthService`.
* В итоге возвращается строка-токен, которую отправляем клиенту в `dummyLoginHandler`.
---
## ✅ Шаг 18. Проверка dummyLogin через Postman
* Создала запрос POST `/dummyLogin` с телом `{ "role": "moderator" }`.
* Получила успешный ответ со статусом `200 OK` и валидным JWT токеном в поле `token`.
* Убедилась, что токен содержит корректные claims и работает как ожидается.
* Это значит, что связка `API → AuthService → JWT` функционирует полностью.
---
## 💬 Зачем всё это?
* Чтобы проект был:
* легко переносимым;
* быстро разворачиваемым одной командой;
* расширяемым (можно будет добавить Prometheus, pgAdmin и т.д.);
* и показывал мой уровень инженерного мышления.