# FoodAI Backend Go REST API с авторизацией через Firebase, JWT и PostgreSQL. Включает инфраструктуру импорта и перевода справочных данных (ингредиенты, рецепты). ## Стек - **Go 1.23** — язык - **chi** — HTTP-роутер - **pgx / pgxpool** — PostgreSQL-драйвер - **goose** — миграции - **golang-jwt/v5** — JWT - **Firebase Admin SDK** — верификация токенов - **cobra** — CLI для команд импорта - **generative-ai-go** — Gemini API (переводы) ## Требования - Go 1.23+ - Docker & Docker Compose - [goose](https://github.com/pressly/goose) (`go install github.com/pressly/goose/v3/cmd/goose@latest`) - Файл `firebase-credentials.json` (сервисный аккаунт Firebase) ## Быстрый старт ### 1. Переменные окружения ```bash cp .env.example .env ``` Отредактируйте `.env`: | Переменная | Описание | По умолчанию | |---|---|---| | `DATABASE_URL` | DSN подключения к PostgreSQL | `postgres://food_ai:food_ai_local@localhost:5432/food_ai?sslmode=disable` | | `FIREBASE_CREDENTIALS_FILE` | Путь к JSON-ключу сервисного аккаунта Firebase | `./firebase-credentials.json` | | `JWT_SECRET` | Секрет для подписи JWT | — | | `JWT_ACCESS_DURATION` | Время жизни access-токена | `15m` | | `JWT_REFRESH_DURATION` | Время жизни refresh-токена | `720h` | | `PORT` | Порт сервера | `8080` | | `ALLOWED_ORIGINS` | CORS-разрешённые источники | `http://localhost:3000` | | `SPOONACULAR_API_KEY` | Ключ Spoonacular API (нужен для команд `import`) | — | | `GEMINI_API_KEY` | Ключ Gemini API (нужен для команд `translate`) | — | ### 2. Запуск через Docker Compose Поднимает PostgreSQL и собирает приложение: ```bash make docker-up ``` ### 3. Запуск локально (только БД в Docker) ```bash # Запустить только PostgreSQL docker compose up -d postgres # Применить миграции make migrate-up # Запустить сервер make run ``` ### 4. Запуск воркера Сервер (`cmd/server`) и воркер (`cmd/worker`) — два отдельных процесса: - **Сервер** — обслуживает HTTP API и SSE-стримы; публикует задания распознавания в Kafka. - **Воркер** — потребляет задания из Kafka, вызывает AI, сохраняет результаты в БД и оповещает сервер через `pg_notify`. **Локально** (инфраструктура в Docker, процессы локально): ```bash # Поднять Kafka + PostgreSQL docker compose up -d postgres kafka kafka-init # В отдельном терминале — сервер make run # В другом терминале — воркер go run ./cmd/worker ``` **Переменные окружения воркера:** | Переменная | Описание | |---|---| | `DATABASE_URL` | DSN подключения к PostgreSQL | | `OPENAI_API_KEY` | Ключ Gemini API (для распознавания блюд) | | `KAFKA_BROKERS` | Адреса Kafka-брокеров (по умолчанию `kafka:9092`) | **Через Docker Compose** (сервер + воркер + инфраструктура): ```bash OPENAI_API_KEY=your-key docker compose up app worker ``` Воркеры можно масштабировать горизонтально — запустите несколько контейнеров `worker`, они разделят нагрузку через consumer group `dish-recognition-workers`. ## Команды ### Сервер и тесты | Команда | Описание | |---|---| | `make run` | Запустить сервер в режиме разработки | | `make test` | Unit-тесты | | `make test-integration` | Интеграционные тесты (требует Docker) | | `make lint` | Проверка через golangci-lint | | `make docker-up` | Поднять PostgreSQL + приложение | | `make docker-down` | Остановить контейнеры | | `make docker-logs` | Логи приложения | | `make migrate-up` | Применить миграции | | `make migrate-down` | Откатить последнюю миграцию | | `make migrate-status` | Статус миграций | | `make migrate-create name=` | Создать новую миграцию | ### Импорт данных (требует `SPOONACULAR_API_KEY`) | Команда | Описание | |---|---| | `make import-ingredients` | Импортировать ~1 000 ингредиентов | | `make import-recipes` | Импортировать ~5 000 рецептов | | `make import-recipes-full` | Импортировать ~10 000 рецептов | ### Перевод (требует `GEMINI_API_KEY`) | Команда | Описание | |---|---| | `make translate-recipes` | Перевести рецепты на русский | | `make translate-ingredients` | Перевести топ-200 ингредиентов | | `make import-all` | Полный пайплайн: ингредиенты → рецепты → переводы | Все команды импорта идемпотентны (`ON CONFLICT DO UPDATE`) — можно запускать повторно. Для возобновления прерванного импорта используйте флаги `--skip-queries` / `--offset`. ### CLI напрямую ```bash # Тестовый прогон (без сохранения в БД) go run ./cmd/import import ingredients --limit 50 --dry-run go run ./cmd/import import recipes --count 100 --dry-run # Возобновление импорта go run ./cmd/import import ingredients --limit 1000 --skip-queries 10 go run ./cmd/import import recipes --count 5000 --offset 2000 # Перевод части рецептов go run ./cmd/import translate recipes --limit 1000 go run ./cmd/import translate ingredients --top 50 ``` ## API ### Публичные эндпоинты | Метод | Путь | Описание | |---|---|---| | `GET` | `/health` | Проверка состояния сервера и БД | | `POST` | `/auth/login` | Вход через Firebase ID-токен | | `POST` | `/auth/refresh` | Обновление JWT по refresh-токену | | `POST` | `/auth/logout` | Выход (инвалидация refresh-токена) | ### Защищённые эндпоинты (Bearer JWT) | Метод | Путь | Описание | |---|---|---| | `GET` | `/profile` | Получить профиль пользователя | | `PUT` | `/profile` | Обновить профиль пользователя | ### Примеры запросов **Логин:** ```bash curl -X POST http://localhost:8080/auth/login \ -H "Content-Type: application/json" \ -d '{"firebase_token": ""}' ``` **Получить профиль:** ```bash curl http://localhost:8080/profile \ -H "Authorization: Bearer " ``` **Обновить профиль:** ```bash curl -X PUT http://localhost:8080/profile \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"height_cm": 180, "weight_kg": 75.5, "age": 28, "gender": "male", "activity": "moderate", "goal": "maintain"}' ``` ## Структура проекта ``` backend/ ├── cmd/ │ ├── server/ # HTTP-сервер (точка входа) │ └── import/ # CLI для импорта и перевода данных (Cobra) ├── internal/ │ ├── auth/ # Firebase-верификация, JWT, сервис и хэндлер авторизации │ ├── config/ # Конфигурация через переменные окружения │ ├── database/ # Подключение к PostgreSQL (pgxpool) │ ├── ingredient/ # Модель, репозиторий, сервис импорта ингредиентов │ ├── middleware/ # RequestID, Logging, Recovery, CORS, Auth │ ├── recipe/ # Модель, репозиторий, сервис импорта рецептов │ ├── server/ # Роутер (chi) │ ├── spoonacular/ # HTTP-клиент Spoonacular API (интерфейс + реализация) │ ├── testutil/ # Вспомогательные утилиты для тестов │ ├── translation/ # Gemini-переводчик, сервис батчевого перевода │ └── user/ # Модель, репозиторий, сервис, хэндлер, расчёт калорий ├── migrations/ │ ├── 001_create_users.sql │ ├── 002_create_ingredient_mappings.sql # GIN-индекс по aliases │ └── 003_create_recipes.sql # FTS + GIN по ingredients/tags ├── .env.example ├── docker-compose.yml ├── Dockerfile └── Makefile ``` ## Схема БД ### `ingredient_mappings` Канонический справочник ингредиентов. Каждая запись — один вид продукта. | Поле | Тип | Описание | |---|---|---| | `id` | UUID | Первичный ключ | | `canonical_name` | varchar | Нормализованное EN-название (`chicken_breast`) | | `canonical_name_ru` | varchar | Русское название (`куриная грудка`) | | `spoonacular_id` | integer | Уникальный ID из Spoonacular | | `aliases` | JSONB | Массив альтернативных названий (EN + RU) | | `category` | varchar | `produce`, `dairy`, `meat`, `seafood`, `grains`, `spices`, `canned`, `frozen`, `beverages`, `other` | | `default_unit` | varchar | Единица измерения по умолчанию (`g`, `ml`) | | `calories_per_100g` | decimal | Нутриенты на 100 г | | `storage_days` | integer | Типичный срок хранения (дни) | Индексы: GIN по `aliases` (поиск `@>`), `canonical_name`, `category`, UNIQUE по `spoonacular_id`. ### `recipes` Каталог рецептов. Заполняется из Spoonacular, переводится через Gemini. | Поле | Тип | Описание | |---|---|---| | `id` | UUID | Первичный ключ | | `source` | enum | `spoonacular`, `ai`, `user` | | `spoonacular_id` | integer | Уникальный ID из Spoonacular | | `title` / `title_ru` | varchar | Название EN/RU | | `difficulty` | enum | `easy` (≤30 мин), `medium` (≤60 мин), `hard` | | `ingredients` | JSONB | Массив `{spoonacular_id, mapping_id, name, amount, unit}` | | `steps` | JSONB | Массив `{number, description, description_ru, timer_seconds}` | | `tags` | JSONB | `["vegetarian", "gluten-free", "meal:dinner", ...]` | | `calories_per_serving` | decimal | Нутриенты на порцию | | `avg_rating` / `review_count` | decimal/int | Рейтинг (обновляется при отзывах) | Индексы: GIN по `ingredients` (поиск по `mapping_id`), GIN по `tags`, FTS по `title + title_ru`. ## Тесты ```bash # Unit-тесты (~69 тестов, ~13 сек) make test # Интеграционные тесты (PostgreSQL в Docker через testcontainers) make test-integration ``` | Пакет | Unit | Integration | |---|---|---| | `auth` | 17 | 10 | | `ingredient` | 9 | 5 | | `middleware` | 10 | — | | `recipe` | 12 | 7 | | `translation` | 6 | — | | `user` | 14 | 12 |