# FoodAI Backend Go REST API с авторизацией через Firebase и JWT, асинхронным распознаванием блюд через Gemini AI, и каталогом продуктов питания на базе OpenFoodFacts. ## Стек - **Go 1.25.5** — язык - **chi v5** — HTTP-роутер - **pgx v5 / pgxpool** — драйвер PostgreSQL - **goose v3** — миграции - **golang-jwt/v5** — JWT - **Firebase Admin SDK** — верификация токенов - **franz-go** — Kafka-продюсер и консьюмер (pure Go) - **watermill + watermill-kafka** — маршрутизация событий (воркер) - **envconfig** — конфигурация через переменные окружения - **testcontainers-go** — интеграционные тесты с реальной БД - **generative-ai-go / OpenAI API** — распознавание блюд, генерация рецептов и меню - **Pexels API** — изображения для рецептов ## Требования - Go 1.25+ - 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:5433/food_ai?sslmode=disable` | | `FIREBASE_CREDENTIALS_FILE` | Путь к JSON-ключу сервисного аккаунта Firebase | `./firebase-credentials.json` | | `JWT_SECRET` | Секрет для подписи JWT | — | | `JWT_ACCESS_DURATION` | TTL access-токена | `15m` | | `JWT_REFRESH_DURATION` | TTL refresh-токена | `720h` | | `PORT` | Порт HTTP-сервера | `8080` | | `ALLOWED_ORIGINS` | CORS-разрешённые источники | `http://localhost:3000` | | `KAFKA_BROKERS` | Адреса Kafka-брокеров | `localhost:9093` (dev), `kafka:9092` (Docker) | | `OPENAI_API_KEY` | API-ключ Gemini/OpenAI (распознавание, генерация) | — | | `PEXELS_API_KEY` | API-ключ Pexels (изображения для рецептов) | — | ### 2. Запуск через Docker Compose Поднимает PostgreSQL, Kafka, сервер и оба воркера: ```bash OPENAI_API_KEY=your-key make docker-up ``` ### 3. Запуск локально (инфраструктура в Docker) ```bash # Поднять только PostgreSQL + Kafka make dev-infra-up # Применить миграции make migrate-up # Запустить всё локально (сервер + оба воркера + инфраструктура) make dev ``` Или по отдельности в разных терминалах: ```bash make run # HTTP-сервер (порт 8080) make run-worker-paid # Воркер paid-очереди make run-worker-free # Воркер free-очереди ``` ### 4. Сервер и воркеры Два типа процессов работают независимо: - **Сервер** (`cmd/server`) — обслуживает HTTP API и SSE-стримы; публикует задания распознавания в Kafka. - **Воркер** (`cmd/worker`) — потребляет задания из Kafka, вызывает AI, сохраняет результаты в БД и оповещает сервер через `pg_notify`. Воркеры можно масштабировать горизонтально — они разделят нагрузку через consumer group `dish-recognition-workers`. **Переменные окружения воркера:** | Переменная | Описание | |---|---| | `DATABASE_URL` | DSN подключения к PostgreSQL | | `OPENAI_API_KEY` | API-ключ Gemini/OpenAI | | `KAFKA_BROKERS` | Адреса Kafka-брокеров | ## Команды ### Сервер и тесты | Команда | Описание | |---|---| | `make run` | Запустить HTTP-сервер (dev) | | `make run-worker-paid` | Запустить воркер paid-очереди | | `make run-worker-free` | Запустить воркер free-очереди | | `make dev` | Поднять инфраструктуру + сервер + оба воркера | | `make dev-infra-up` | Поднять только PostgreSQL + Kafka | | `make dev-infra-down` | Остановить инфраструктуру | | `make test` | Unit-тесты (с race detector) | | `make test-integration` | Интеграционные тесты (требует Docker) | | `make lint` | Проверка через golangci-lint | | `make docker-up` | Полный Docker Compose (app + workers + infra) | | `make docker-down` | Остановить все контейнеры | | `make docker-logs` | Логи app-контейнера | | `make docker-logs-worker` | Логи worker-контейнеров | | `make migrate-up` | Применить миграции | | `make migrate-down` | Откатить последнюю миграцию | | `make migrate-status` | Статус миграций | | `make migrate-create name=` | Создать новую миграцию | | `make import-off file=` | Импортировать каталог из OpenFoodFacts JSONL/GZ | ## Импорт каталога продуктов Каталог продуктов заполняется из дампа [OpenFoodFacts](https://world.openfoodfacts.org/data). ```bash # Скачать дамп (~66 ГБ, ~4.4 млн записей) # https://world.openfoodfacts.org/data/openfoodfacts-products.jsonl.gz # Через make (DSN берётся из .env) make import-off file=openfoodfacts-products.jsonl.gz # Или напрямую с дополнительными флагами go run ./cmd/importoff \ -file=openfoodfacts-products.jsonl.gz \ -dsn="$DATABASE_URL" \ -min-scans=1 ``` **Флаги:** | Флаг | Описание | По умолчанию | |---|---|---| | `-file` | Путь к JSONL или JSONL.GZ файлу | обязательный | | `-dsn` | PostgreSQL DSN (или `$DATABASE_URL`) | `$DATABASE_URL` | | `-limit` | Остановиться после N принятых записей (0 = без ограничения) | `0` | | `-batch` | Записей на один upsert-батч | `500` | | `-min-scans` | Минимальное число уникальных сканирований (0 = без фильтра) | `1` | **Алгоритм:** 1. Создаёт временную staging-таблицу без уникальных ограничений. 2. Читает файл построчно (gzip-прозрачно); пропускает записи без баркода, названия или калорий. 3. Дедуплицирует `canonical_name` внутри батча. 4. Загружает батч через `COPY` (без ошибок на дублях). 5. Обнуляет barcodes, уже занятые другой записью в каталоге. 6. `INSERT … ON CONFLICT (canonical_name) DO UPDATE` — безопасное слияние. Команда идемпотентна — можно запускать повторно и прерывать. ## API ### Публичные эндпоинты | Метод | Путь | Описание | |---|---|---| | `GET` | `/health` | Статус сервера и БД | | `GET` | `/languages` | Список поддерживаемых языков | | `GET` | `/units` | Справочник единиц измерения | | `GET` | `/cuisines` | Список кухонь | | `GET` | `/tags` | Теги рецептов и блюд | | `POST` | `/auth/login` | Вход через Firebase ID-токен → JWT | | `POST` | `/auth/refresh` | Обновить JWT по refresh-токену | | `POST` | `/auth/logout` | Выход (инвалидация refresh-токена) | ### Защищённые эндпоинты (Bearer JWT) | Метод | Путь | Описание | |---|---|---| | `GET/PUT` | `/profile` | Профиль пользователя | | `GET` | `/products/search?q=&limit=` | Поиск в каталоге продуктов | | `GET` | `/products/barcode/{barcode}` | Поиск по баркоду (OpenFoodFacts как fallback) | | `GET/POST/DELETE` | `/user-products`, `/user-products/{id}` | Продукты пользователя (холодильник) | | `POST` | `/user-products/batch` | Массовое добавление продуктов | | `GET` | `/diary?date=YYYY-MM-DD` | Записи дневника питания за дату | | `POST/DELETE` | `/diary`, `/diary/{id}` | Добавить / удалить запись дневника | | `GET` | `/home/summary` | Сводка для главного экрана | | `GET` | `/recommendations` | AI-рекомендации блюд | | `GET/POST/DELETE` | `/saved-recipes`, `/saved-recipes/{id}` | Сохранённые рецепты | | `GET/PUT/DELETE` | `/menu/items/{id}` | Меню на неделю | | `GET/POST/PATCH` | `/shopping-list`, `/shopping-list/{i}/check` | Список покупок | | `GET` | `/dishes/{id}` | Блюдо по ID | | `GET` | `/recipes/{id}` | Рецепт по ID | | `POST` | `/ai/recognize-receipt` | Распознать чек (синхронно) | | `POST` | `/ai/recognize-products` | Распознать продукты на фото (синхронно) | | `POST` | `/ai/recognize-dish` | Распознать блюдо — 202 + `job_id` (асинхронно) | | `GET` | `/ai/jobs` | Задания за сегодня | | `GET` | `/ai/jobs/history` | Все задания пользователя | | `GET` | `/ai/jobs/{id}` | Результат задания | | `GET` | `/ai/jobs/{id}/stream` | SSE-стрим обновлений задания | | `POST` | `/ai/generate-menu` | Сгенерировать меню через AI | ### Асинхронное распознавание блюд `POST /ai/recognize-dish` ставит задание в очередь и сразу возвращает: ```json {"job_id": "...", "queue_position": 2, "estimated_seconds": 18} ``` Статус отслеживается через SSE: `GET /ai/jobs/{id}/stream` — события `queued`, `processing`, `done`, `failed`. При повторном открытии приложения результат можно получить через `GET /ai/jobs/{id}`. Две очереди с разным приоритетом обслуживания: - `ai.recognize.paid` (3 партиции) — платные пользователи - `ai.recognize.free` (1 партиция) — бесплатные пользователи ### Примеры запросов **Логин:** ```bash curl -X POST http://localhost:8080/auth/login \ -H "Content-Type: application/json" \ -d '{"firebase_token": ""}' ``` **Поиск продукта:** ```bash curl "http://localhost:8080/products/search?q=butter&limit=5" \ -H "Authorization: Bearer " ``` **Поиск по баркоду:** ```bash curl http://localhost:8080/products/barcode/3017620422003 \ -H "Authorization: Bearer " ``` **Добавить запись в дневник:** ```bash curl -X POST http://localhost:8080/diary \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"date": "2026-03-21", "meal_type": "lunch", "product_id": "...", "portion_g": 150}' ``` ## Структура проекта ``` backend/ ├── cmd/ │ ├── server/ # HTTP-сервер (точка входа) │ ├── worker/ # Async-воркер распознавания блюд (Kafka) │ └── importoff/ # Импорт каталога продуктов из OpenFoodFacts JSONL ├── internal/ │ ├── domain/ │ │ ├── auth/ # Firebase-верификация, JWT, login/refresh/logout │ │ ├── user/ # Профиль, калькулятор КБЖУ │ │ ├── product/ # Каталог продуктов (поиск, баркод, OFF-адаптер) │ │ ├── userproduct/ # Продукты пользователя (холодильник/список) │ │ ├── recipe/ # Каталог рецептов с переводами │ │ ├── savedrecipe/ # Сохранённые рецепты пользователя │ │ ├── dish/ # Блюда, рецепты блюд, переводы │ │ ├── menu/ # Меню на неделю, список покупок │ │ ├── diary/ # Дневник питания │ │ ├── recognition/ # Async распознавание: jobs, SSE-broker, Kafka-воркер │ │ ├── recommendation/ # AI-рекомендации │ │ └── home/ # Сводка главного экрана │ ├── adapters/ │ │ ├── ai/ # Общие типы AI-ответов │ │ ├── openai/ # Gemini/OpenAI клиент (распознавание, генерация) │ │ ├── firebase/ # Верификация Firebase-токенов │ │ ├── kafka/ # Продюсер и консьюмер (franz-go) │ │ └── pexels/ # Поиск изображений через Pexels API │ └── infra/ │ ├── config/ # Конфигурация (envconfig) │ ├── database/ # pgxpool-соединение │ ├── locale/ # Определение языка из заголовка Accept-Language │ ├── middleware/ # RequestID, Logging, Recovery, CORS, Auth, Language │ └── server/ # Chi-роутер со всеми маршрутами ├── migrations/ │ └── 001_initial_schema.sql # Полная схема БД ├── tests/ # Интеграционные и unit-тесты по доменам ├── .env.example ├── docker-compose.yml ├── Dockerfile └── Makefile ``` ## Схема БД Вся схема содержится в одном файле `migrations/001_initial_schema.sql`. Основные таблицы: - `users`, `refresh_tokens` — пользователи и токены - `products`, `product_categories`, `product_category_translations` — каталог продуктов - `user_products` — продукты пользователя - `ingredients`, `ingredient_translations`, `ingredient_aliases` — справочник ингредиентов - `dishes`, `dish_translations`, `recipes`, `recipe_steps`, `recipe_ingredients` — блюда и рецепты - `saved_recipes`, `saved_recipe_translations` — сохранённые рецепты - `meal_diary` — дневник питания - `recognition_jobs` — задания AI-распознавания - `cuisines`, `cuisine_translations`, `tags`, `tag_translations`, `units`, `unit_translations` — справочники Для каждой таблицы с пользовательским текстом используется паттерн `{table}_translations` (companion table). Базовые таблицы хранят английский canonical text, переводы — только в `_translations` таблицах. ## Тесты ```bash # Unit-тесты (с race detector) make test # Интеграционные тесты (PostgreSQL поднимается через testcontainers) make test-integration ``` Покрытые пакеты: `auth`, `diary`, `middleware`, `recognition`, `recipe`, `savedrecipe`, `user`, `userproduct`, `locale`, `menu`.