Files
food-ai/backend/README.md
dbastrikin 48fd2baa8c feat: split worker into separate binary (cmd/worker)
Kafka consumers and WorkerPool are moved out of the server process into
a dedicated worker binary. Server now handles HTTP + SSE only; worker
handles Kafka consumption and AI processing.

- cmd/worker/main.go + init.go: new binary with minimal config
  (DATABASE_URL, OPENAI_API_KEY, KAFKA_BROKERS)
- cmd/server: remove WorkerPool, paidConsumer, freeConsumer
- Dockerfile: builds both server and worker binaries
- docker-compose.yml: add worker service
- Makefile: add run-worker and docker-logs-worker targets
- README.md: document worker startup and env vars

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 20:09:33 +02:00

282 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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=<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": "<FIREBASE_ID_TOKEN>"}'
```
**Получить профиль:**
```bash
curl http://localhost:8080/profile \
-H "Authorization: Bearer <ACCESS_TOKEN>"
```
**Обновить профиль:**
```bash
curl -X PUT http://localhost:8080/profile \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-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 |