feat: implement Iteration 0 foundation (backend + Flutter client)

Backend (Go):
- Project structure with chi router, pgxpool, goose migrations
- JWT auth (access/refresh tokens) with Firebase token verification
- NoopTokenVerifier for local dev without Firebase credentials
- PostgreSQL user repository with atomic profile updates (transactions)
- Mifflin-St Jeor calorie calculation based on profile data
- REST API: POST /auth/login, /auth/refresh, /auth/logout, GET/PUT /profile, GET /health
- Middleware: auth, CORS (localhost wildcard), logging, recovery, request_id
- Unit tests (51 passing) and integration tests (testcontainers)
- Docker Compose setup with postgres healthcheck and graceful shutdown

Flutter client:
- Riverpod state management with GoRouter navigation
- Firebase Auth (email/password + Google sign-in with web popup support)
- Platform-aware API URLs (web/Android/iOS)
- Dio HTTP client with JWT auth interceptor and concurrent refresh handling
- Secure token storage
- Screens: Login, Register, Home (tabs: Menu, Recipes, Products, Profile)
- Unit tests (17 passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-02-20 13:14:58 +02:00
commit 24219b611e
140 changed files with 13062 additions and 0 deletions

250
docs/Description.md Normal file
View File

@@ -0,0 +1,250 @@
# FoodAI — мобильное приложение для управления питанием
## Концепция
FoodAI — мобильное приложение, которое помогает пользователю управлять своим питанием: планировать меню, вести учёт калорий и контролировать запасы продуктов. Ключевая особенность — использование камеры телефона для распознавания продуктов, чеков и готовых блюд с автоматическим подсчётом калорий и подбором рецептов.
## Целевая аудитория
- Люди, следящие за питанием и калорийностью рациона
- Те, кто хочет разнообразить своё меню и научиться готовить
- Люди, которые хотят минимизировать пищевые отходы, используя имеющиеся продукты
- Начинающие кулинары, которым важен пошаговый процесс готовки
## Основные функции
### 1. Распознавание продуктов через камеру
- **Сканирование чека.** Пользователь фотографирует чек из магазина. Приложение распознаёт список купленных продуктов и автоматически добавляет их в запасы.
- **Фото продуктов.** Пользователь фотографирует продукты (например, содержимое холодильника). Приложение определяет, что на фото, и добавляет распознанные продукты в запасы.
- **Фото готового блюда.** Пользователь фотографирует готовое блюдо или порцию еды. Приложение определяет блюдо и примерную калорийность.
- **Мультифото.** При фотографировании продуктов можно сделать несколько снимков подряд (например, разные полки холодильника), и результаты объединяются в один общий список.
- **Корректировка результатов.** После любого распознавания пользователь может отредактировать каждый продукт: изменить название, вес, количество, категорию, удалить ошибочно распознанный или добавить пропущенный.
### 2. Учёт продуктов
- Список имеющихся продуктов с возможностью ручного редактирования.
- Автоматическое пополнение запасов через сканирование чеков и фото.
- Автоматическое списание продуктов при отметке приготовленных рецептов.
- **Срок годности как период хранения.** Вместо фиксированной даты «до XX.XX» используется модель «срок хранения после покупки» (например, молоко — 5 дней). Дата вычисляется автоматически от даты добавления. Период можно скорректировать вручную для конкретного продукта. Для каждой категории продуктов есть значения по умолчанию, которые пользователь может настроить.
- Уведомления о продуктах с истекающим сроком годности.
- **Частичное использование.** Возможность отметить, что продукт использован частично (открытое молоко, отрезанный кусок сыра), с корректировкой оставшегося количества.
- **Сброс и перезаполнение.** Возможность очистить все продукты и перезаполнить список заново — через фото содержимого холодильника или сканирование чека. Полезно при инвентаризации запасов.
- **Объединение дубликатов.** При добавлении продукта, который уже есть в запасах, приложение предлагает объединить (прибавить количество) или добавить отдельной позицией (если разные сроки годности/партии).
- **Единицы измерения.** Каждый продукт хранится с единицей измерения (г, кг, мл, л, шт, пучок, упаковка). При добавлении система предлагает наиболее подходящую единицу для категории, с возможностью смены.
### 3. Подбор меню и рекомендации
На основе имеющихся продуктов приложение предлагает рецепты с учётом фильтров:
- **По периоду:** на один приём пищи, на день, на неделю.
- **По сложности:** быстрые и простые блюда, средняя сложность, сложные рецепты.
- **По кухне:** азиатская, европейская, средиземноморская, русская и другие.
- **По типу приёма пищи:** завтрак, обед, ужин, перекус.
- **По диетическим предпочтениям:** вегетарианское, безглютеновое, низкокалорийное и т.д.
Если для рецепта не хватает некоторых продуктов, приложение показывает, чего именно не хватает, и формирует список покупок.
**Рекомендации.** Приложение проактивно предлагает рецепты и меню:
- На главном экране — блок «Рекомендуем приготовить» на основе имеющихся продуктов (приоритет: продукты с истекающим сроком).
- После добавления продуктов (чек/фото) — экран-предложение «Составить меню из купленного?» с вариантами кухонь, сложности и периода.
- В каталоге рецептов — персональная лента «Для вас» на основе предпочтений, истории, оценок.
- На экране меню при пустых слотах — подсказка «Подобрать блюдо» с учётом оставшегося бюджета калорий на день.
- **Замена ингредиентов.** Если для рецепта не хватает ингредиента, приложение может предложить замену из имеющихся продуктов (например, пекорино → пармезан).
- **Быстрый повтор.** Часто готовимые блюда отображаются в секции «Готовили недавно» для быстрого доступа.
### 4. Планирование меню
- Составление меню на каждый день недели.
- **Автогенерация меню.** Приложение может сгенерировать меню на день или неделю целиком по заданным параметрам (кухня, калорийность, сложность, из имеющихся продуктов). Пользователь может принять, заменить отдельные блюда или сгенерировать заново.
- Редактирование и перестановка блюд между днями.
- Автоматический подсчёт суммарной калорийности за день/неделю.
- Формирование списка покупок на основе запланированного меню и имеющихся запасов.
- История меню — возможность вернуться к удачному плану питания прошлых недель.
- **Шаблоны меню.** Возможность сохранить текущее меню как шаблон (например, «Моя рабочая неделя», «Безглютеновая неделя») и применять его повторно.
### 5. Учёт калорий
- Автоматический подсчёт калорий на основе добавленных в дневник приёмов пищи.
- Распознавание калорийности по фото готового блюда или продуктов.
- Дневник питания: запись того, что было съедено за день.
- **Размер порции.** При записи в дневник можно указать, какую долю порции съел пользователь (1, 0.5, 1.5 порции и т.д.).
- Отображение баланса БЖУ (белки, жиры, углеводы).
- Настройка целевой калорийности и отслеживание прогресса.
- Графики и статистика за день, неделю, месяц.
- **Быстрое добавление.** Перекусы, не являющиеся рецептом (банан, чай с сахаром), можно добавить через поиск по базе продуктов без необходимости фотографировать.
### 6. Рецепты
- Каталог рецептов с поиском и фильтрацией.
- Карточка рецепта: список ингредиентов, калорийность, время приготовления, сложность, кухня.
- Возможность оставить отзыв и оценку к рецепту.
- Добавление рецепта в избранное.
- Просмотр отзывов других пользователей.
- **Замена ингредиентов.** В карточке рецепта для каждого ингредиента отображаются возможные замены. Если основного ингредиента нет в запасах, но есть подходящая замена — это отражается в наличии.
- **Пользовательские рецепты.** Возможность создать и сохранить свой рецепт, который будет доступен в личном каталоге.
### 7. Пошаговая готовка с таймерами
- Режим готовки: рецепт разбит на последовательные шаги с фото и описанием.
- На шагах, требующих ожидания (варить 15 минут, дать настояться 30 минут), отображается кнопка запуска таймера.
- Нажатие на шаг с таймером запускает обратный отсчёт с уведомлением по завершении.
- Возможность запуска нескольких таймеров параллельно (например, пока варится паста, готовится соус).
- Экран не гаснет в режиме готовки.
- **Завершение готовки:** после последнего шага — предложение записать в дневник, оценить рецепт и поделиться фото результата.
### 8. Онбординг
Первый запуск приложения включает короткий пошаговый процесс:
1. Приветствие и краткое описание возможностей (23 карточки с иллюстрациями).
2. Указание базовых параметров: пол, возраст, рост, вес, уровень активности.
3. Выбор цели: похудение, поддержание, набор массы. Расчёт рекомендуемой калорийности.
4. Указание ограничений и предпочтений (аллергии, диеты).
5. Предпочтения по кухням (выбрать 23 из списка).
6. Предложение добавить первые продукты: сфотографировать холодильник, сканировать чек или пропустить.
7. Переход на главный экран.
## Экраны приложения
### Главный экран
- Сводка на сегодня: запланированные приёмы пищи, текущий баланс калорий.
- Быстрые действия: сфотографировать чек, сфотографировать еду, найти рецепт.
- **Рекомендации:** блок «Рекомендуем приготовить» — 23 карточки рецептов на основе имеющихся продуктов (приоритет — продукты с истекающим сроком).
- **Быстрый повтор:** секция «Готовили недавно» — последние 35 приготовленных рецептов для быстрого повтора.
### Мои продукты
- Список имеющихся продуктов, сгруппированных по категориям.
- Кнопка добавления: вручную, через фото, через сканирование чека.
- **Кнопка сброса/перезаполнения** в контекстном меню.
- Срок годности отображается как «осталось X дней» (вычисляется от даты добавления + период хранения).
### Экран корректировки после распознавания
- Общий для всех типов распознавания (чек, фото продуктов, фото блюда).
- Каждый продукт: редактируемое название, вес/количество (инлайн-редактирование), единица измерения, категория, период хранения.
- **Переход к составлению меню** — после подтверждения продуктов.
### Переходный экран «Составить меню?»
- Появляется после добавления продуктов через чек или фото.
- Предлагает составить меню из добавленных (и имеющихся) продуктов.
- Быстрый выбор: на день / на неделю, кухня, сложность.
- Кнопки «Составить меню» и «Пропустить».
### Меню
- Календарь с запланированными приёмами пищи.
- Возможность перетаскивания блюд между днями.
- Итоговая калорийность по каждому дню.
- **Кнопка автогенерации** меню на неделю.
- **Подсказки** в пустых слотах с рекомендациями.
### Каталог рецептов
- Поиск и фильтры (кухня, сложность, время, калорийность, тип приёма пищи).
- Карточки рецептов с фото, названием, калорийностью и рейтингом.
- Кнопка «Подобрать из моих продуктов».
- **Секция «Для вас»** — персональные рекомендации.
- **Секция «Готовили недавно»** — быстрый повтор.
### Карточка рецепта
- Фото блюда.
- Описание, время приготовления, сложность, калорийность, БЖУ.
- Список ингредиентов с отметкой наличия в запасах и **предложениями замен**.
- Кнопка «Начать готовить» — переход в режим пошаговой готовки.
- Отзывы и оценки.
- Кнопка «Добавить в меню».
### Режим готовки
- Пошаговое отображение процесса.
- Крупный текст и фото для удобства на кухне.
- Кнопки таймеров на соответствующих шагах.
- Панель активных таймеров внизу экрана.
- **Экран завершения:** предложение записать в дневник, оценить, поделиться.
### Дневник питания
- Записи о приёмах пищи за день.
- Добавление: из рецепта, из меню, через фото, вручную, **из быстрого поиска по базе продуктов**.
- **Регулировка размера порции** при добавлении.
- Итоги дня: калории, БЖУ, прогресс к цели.
### Статистика
- Графики калорийности и БЖУ за выбранный период.
- Сравнение с целевыми показателями.
- Тренды и динамика.
### Список покупок
- Формируется автоматически из запланированного меню с учётом имеющихся запасов.
- Возможность ручного редактирования.
- Отметка купленных позиций.
### Профиль
- Персональные данные (вес, рост, возраст, уровень активности).
- Цели (похудение, набор массы, поддержание).
- Расчёт рекомендуемой суточной калорийности.
- Диетические предпочтения и ограничения.
- **Настройки сроков хранения** по категориям продуктов.
- **Мои рецепты** — пользовательские рецепты.
## Пользовательские сценарии
### Сценарий 1: Первый запуск
1. Пользователь скачивает приложение и проходит онбординг.
2. Указывает параметры, цели, предпочтения по кухням.
3. Фотографирует содержимое холодильника.
4. Корректирует список продуктов (вес, количество, названия).
5. Приложение предлагает составить меню из того, что есть.
6. Пользователь получает первое сгенерированное меню.
### Сценарий 2: Пришёл из магазина
1. Пользователь фотографирует чек.
2. Приложение распознаёт продукты. Пользователь корректирует (редактирует вес/количество, удаляет лишнее, добавляет пропущенное).
3. Для дубликатов — предложение объединить с имеющимися.
4. Подтверждает → переходный экран: «Составить меню из купленного?»
5. Выбирает: на неделю, европейская кухня, средняя сложность.
6. Получает сгенерированное меню, корректирует, сохраняет.
### Сценарий 3: Что приготовить из того, что есть?
1. Пользователь открывает подбор рецептов.
2. Выбирает фильтры: «из моих продуктов», «ужин», «до 30 минут», «азиатская кухня».
3. Получает список подходящих рецептов, отсортированных по степени совпадения с имеющимися продуктами.
4. Для рецепта, где не хватает ингредиента, видит предложение замены (пекорино → пармезан).
5. Выбирает рецепт и начинает пошаговую готовку.
### Сценарий 4: Планирование недели
1. Пользователь открывает экран меню.
2. Нажимает «Автогенерация» → выбирает параметры (кухня, калорийность, из моих продуктов).
3. Приложение генерирует меню на неделю. Пользователь заменяет пару блюд вручную.
4. Формируется список покупок — с учётом имеющихся запасов.
### Сценарий 5: Учёт калорий на ходу
1. Пользователь обедает в кафе.
2. Фотографирует блюдо.
3. Приложение определяет блюдо и его примерную калорийность.
4. Пользователь корректирует размер порции.
5. Данные записываются в дневник питания.
### Сценарий 6: Готовка по рецепту
1. Пользователь открывает рецепт и нажимает «Начать готовить».
2. Следует пошаговым инструкциям.
3. На шаге «Варить бульон 40 минут» нажимает на таймер.
4. Переходит к параллельным шагам (нарезка овощей) пока таймер тикает.
5. Получает уведомление о готовности бульона.
6. На последнем шаге — предложение записать блюдо в дневник и оценить рецепт.
### Сценарий 7: Инвентаризация продуктов
1. Пользователь давно не обновлял список продуктов.
2. Открывает «Мои продукты» → контекстное меню → «Очистить и перезаполнить».
3. Подтверждает сброс.
4. Фотографирует содержимое холодильника (несколько фото).
5. Корректирует распознанный список.
6. Продукты обновлены, сроки пересчитаны от текущей даты.
### Сценарий 8: Использование рекомендаций
1. Пользователь открывает приложение утром.
2. На главном экране видит: «Рекомендуем приготовить: Стир-фрай с курицей — используйте курицу (срок истекает завтра) и овощи из запасов».
3. Тап по карточке → карточка рецепта → «Начать готовить».
## Монетизация (варианты)
- **Freemium.** Базовые функции бесплатно (ручной ввод продуктов, просмотр рецептов, планирование меню). Распознавание через камеру, расширенная аналитика, персональные рекомендации — по подписке.
- **Подписка.** Ежемесячная/годовая подписка на полный функционал.
- **Без рекламы.** Бесплатная версия с рекламой, платная — без.

1735
docs/Design.md Normal file

File diff suppressed because it is too large Load Diff

835
docs/Tech.md Normal file
View File

@@ -0,0 +1,835 @@
# FoodAI — Техническая архитектура
## 1. Стек технологий
| Компонент | Технология | Обоснование |
|-----------|-----------|-------------|
| Backend | Go | Высокая производительность, встроенная конкурентность (горутины для очередей), строгая типизация, простой деплой (один бинарник) |
| База данных | PostgreSQL | Надёжная реляционная БД, JSONB для гибких структур (нутриенты, шаги рецептов), полнотекстовый поиск, зрелая экосистема |
| Клиент | Flutter (Android, iOS, Web) | Единая кодовая база на три платформы, нативная производительность на мобильных, зрелая экосистема виджетов |
| Авторизация | Firebase Auth | Бесплатно до 50K MAU, email + Google + Apple из коробки, официальный Go SDK, отличная интеграция с Flutter |
| AI (vision) | Google Gemini 2.5 Flash | Лучшее соотношение цена/качество для распознавания еды (~$0.15/1M input tokens), бесплатный tier для разработки, прецедент CalCam |
| AI (текст) | Google Gemini 2.5 Flash-Lite | Самый дешёвый вариант для текстовых задач ($0.10/1M input tokens) — генерация рецептов, меню, замены ингредиентов |
| База рецептов | Spoonacular API | 365K+ рецептов с нутриентами, фото, ингредиентами, шагами. $29/мес на старте |
| Хранение файлов | S3-совместимое (MinIO / Cloud Storage) | Фото блюд от пользователей, фото рецептов |
---
## 2. Архитектура системы
```
┌──────────────────────────────────────────────────────────┐
│ Flutter Client │
│ (Android / iOS / Web) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Firebase │ │ REST │ │ File │ │
│ │ Auth SDK │ │ Client │ │ Upload │ │
│ └─────┬────┘ └─────┬────┘ └─────┬────┘ │
└────────┼─────────────┼─────────────┼─────────────────────┘
│ │ │
│ idToken │ JWT │ multipart
▼ ▼ ▼
┌──────────────────────────────────────────────────────────┐
│ Go Backend │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────┐ │
│ │ Auth │ │ API │ │ AI Queue │ │
│ │ Middleware │ │ Handlers │ │ Manager │ │
│ │ │ │ │ │ │ │
│ │ Firebase │ │ /products │ │ Priority │ │
│ │ Token │ │ /recipes │ │ Queues: │ │
│ │ Verify │ │ /menu │ │ - paid (fast) │ │
│ │ → JWT │ │ /diary │ │ - free (slow) │ │
│ └──────┬──────┘ │ /ai │ │ │ │
│ │ │ /shopping │ │ Rate Limiter │ │
│ │ └──────┬───────┘ │ Budget Guard │ │
│ │ │ └───────┬────────┘ │
│ │ │ │ │
│ ┌──────┴────────────────┴──────────────────┴────────┐ │
│ │ Service Layer │ │
│ │ │ │
│ │ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │ │
│ │ │ Product │ │ Recipe │ │ AI Service │ │ │
│ │ │ Service │ │ Service │ │ (interface) │ │ │
│ │ └────┬─────┘ └─────┬─────┘ └────┬─────────────┘ │ │
│ └───────┼─────────────┼────────────┼────────────────┘ │
│ │ │ │ │
└──────────┼─────────────┼────────────┼────────────────────┘
│ │ │
▼ │ ▼
┌──────────────────┐ │ ┌──────────────────┐
│ │ │ │ │
│ PostgreSQL │ │ │ Gemini API │
│ │ │ │ (Flash / │
│ - users │ │ │ Flash-Lite) │
│ - products │ │ │ │
│ - recipes │ │ └──────────────────┘
│ - menu_plans │ │
│ - meal_diary │ ▼
│ - reviews │ ┌──────────────────┐
│ - ai_tasks │ │ │
│ │ │ Spoonacular │
└──────────────────┘ │ API │
│ │
└──────────────────┘
```
---
## 3. Авторизация
### Поток авторизации
```
Flutter Firebase Go Backend
│ │ │
│ 1. signInWithGoogle() │ │
│ ──────────────────────► │ │
│ │ │
│ 2. Firebase idToken │ │
│ ◄────────────────────── │ │
│ │ │
│ 3. POST /auth/login │ │
│ {firebase_token} │ │
│ ─────────────────────────┼─────────────────► │
│ │ │
│ │ 4. VerifyIDToken() │
│ │ ◄─────────────── │
│ │ ───────────────► │
│ │ (uid, email) │
│ │ │
│ │ 5. Upsert user │
│ │ в PostgreSQL │
│ │ │
│ 6. {jwt, refresh_token, │ │
│ user} │ │
│ ◄────────────────────────┼────────────────── │
│ │ │
│ 7. Сохранить JWT │ │
│ в Secure Storage │ │
```
### Детали
- **Firebase Auth** обрабатывает все провайдеры (email/password, Google, Apple) на стороне клиента.
- **Go backend** получает Firebase `idToken`, верифицирует его через Firebase Admin SDK (`firebase.google.com/go/v4/auth`), извлекает `uid`, `email`, `name`.
- **Backend выдаёт собственный JWT** (подписанный секретом сервера) с `user_id`, `role` (free/paid), `exp`. Это позволяет не зависеть от Firebase при каждом API-запросе.
- **Refresh token** хранится в БД, ротируется при каждом использовании.
- **Apple Sign-In** обязателен на iOS при наличии любого стороннего входа (политика App Store). Firebase Auth обрабатывает Apple прозрачно.
- Flutter-пакеты: `firebase_auth`, `google_sign_in`, `sign_in_with_apple`.
---
## 4. AI-подсистема (ядро приложения)
### Абстракция
AI-провайдер скрыт за интерфейсами. Это позволяет заменить Gemini на OpenAI или специализированный API без изменения бизнес-логики.
```
┌─────────────────────────────────────────┐
│ AI Service (interface) │
│ │
│ FoodRecognizer │
│ ├── RecognizeReceipt(image) → []Item │
│ ├── RecognizeProducts(image) → []Item │
│ └── RecognizeDish(image) → DishInfo │
│ │
│ RecipeGenerator │
│ ├── GenerateRecipes(req) → []Recipe │
│ └── SuggestSubstitutions(req) → []Sub │
│ │
│ MenuPlanner │
│ └── GenerateMenu(req) → MenuPlan │
│ │
│ NutritionEstimator │
│ └── EstimateNutrition(dish) → KBJU │
│ │
└──────────────┬──────────────────────────┘
┌────────┴────────┐
▼ ▼
┌───────────┐ ┌───────────┐
│ Gemini │ │ OpenAI │
│ Adapter │ │ Adapter │
│ (active) │ │ (резерв) │
└───────────┘ └───────────┘
```
### AI-задачи: детали
#### 4.1. Распознавание чека
- **Модель:** Gemini 2.5 Flash (vision)
- **Вход:** фото чека (JPEG/PNG)
- **Промпт:** системная инструкция + фото → structured JSON
- **Выход:**
```json
{
"items": [
{
"name": "Молоко 2.5%",
"quantity": 1,
"unit": "л",
"category": "dairy",
"price": 49.0,
"confidence": 0.95
}
],
"unrecognized": [
{ "raw_text": "ТОВ АРТИК 1ШТ", "price": 89.0 }
]
}
```
- **Стратегия промпта:** «Ты — OCR-система для чеков из продуктовых магазинов. Извлеки список продуктов. Для каждого определи название, количество, единицу измерения, категорию. Если не можешь распознать позицию — добавь в unrecognized с оригинальным текстом. Ответ строго в JSON.»
- **Fallback:** если confidence < 0.5 — элемент идёт в `unrecognized`.
#### 4.2. Распознавание продуктов (фото)
- **Модель:** Gemini 2.5 Flash (vision)
- **Вход:** фото продуктов (холодильник, стол)
- **Выход:** аналогичная структура (без цены), но с приблизительным весом/количеством
- **Нюанс:** поддержка нескольких фото — результаты объединяются на бэкенде (дедупликация по названию + суммирование количеств).
#### 4.3. Распознавание блюда
- **Модель:** Gemini 2.5 Flash (vision)
- **Вход:** фото готового блюда
- **Выход:**
```json
{
"dish_name": "Паста Карбонара",
"weight_grams": 450,
"calories": 580,
"protein": 24,
"fat": 28,
"carbs": 56,
"confidence": 0.85,
"similar_dishes": ["Паста с беконом", "Спагетти алла Грича"]
}
```
- **Нюанс:** `similar_dishes` используется для поиска похожих рецептов в нашей БД (по названию, fuzzy search).
#### 4.4. Генерация рецептов / подбор меню
- **Модель:** Gemini 2.5 Flash-Lite (текст) — дешевле, vision не нужен
- **Ключевой принцип:** AI НЕ генерирует рецепты с нуля. Вместо этого:
```
1. Backend выбирает из БД кандидатов-рецептов (по фильтрам: кухня, сложность, время, ингредиенты)
2. AI получает список кандидатов (ID + название + ингредиенты + калории) + контекст юзера
3. AI ранжирует, комбинирует в меню, предлагает замены
4. Backend возвращает полные рецепты по ID из БД
```
- **Промпт для подбора меню:**
```
Контекст пользователя:
- Цель: 2100 ккал/день
- Ограничения: без орехов
- Предпочтения: русская, азиатская кухня
- Продукты в наличии: [список с количествами и сроками]
Доступные рецепты (ID, название, калории, основные ингредиенты):
[список из 50100 кандидатов из БД]
Задача: составь меню на 7 дней (завтрак, обед, ужин, перекус).
Приоритет: использовать продукты с истекающим сроком.
Ответ: JSON с recipe_id для каждого слота.
```
- **Выход:** массив `{ day, meal_type, recipe_id }` — бэкенд подтягивает полные данные рецептов из БД.
Это гарантирует согласованность: AI не придумывает рецепты, а выбирает из проверенной базы.
#### 4.5. AI-генерация персональных рецептов
Отдельный кейс — когда в БД нет подходящего рецепта из имеющихся продуктов:
- **Модель:** Gemini 2.5 Flash-Lite
- **Промпт:** «Из продуктов [список] предложи рецепт. Формат: JSON с названием, ингредиентами (с граммовками), шагами, калорийностью, БЖУ.»
- **Результат:** сохраняется в БД как `source = 'ai_generated'`, помечается в UI как «AI-рецепт».
- **Валидация нутриентов:** бэкенд пересчитывает калории/БЖУ по справочнику (USDA/Spoonacular nutrition data) и корректирует, если AI ошибся более чем на 20%.
#### 4.6. Замена ингредиентов
- **Модель:** Gemini 2.5 Flash-Lite
- **Вход:** ингредиент + доступные продукты
- **Выход:** `[{ "original": "пекорино", "substitute": "пармезан", "ratio": "1:1", "note": "Менее острый вкус" }]`
- **Кеширование:** результаты замен кешируются в БД (таблица `ingredient_substitutions`), чтобы не запрашивать AI повторно для одинаковых пар.
---
## 5. Маппинг ингредиентов (связь AI ↔ БД рецептов)
### Проблема
Gemini оперирует свободным текстом ("куриное филе", "помидоры черри"), а Spoonacular — структурированными ID ингредиентов. Продукты пользователя — тоже свободный текст. Нужен слой, который связывает все три мира.
### Решение: таблица `ingredient_mappings`
Каноническая таблица ингредиентов с алиасами на разных языках и привязкой к Spoonacular ID.
```
┌───────────────────────────────────────────────────────┐
│ ingredient_mappings │
│───────────────────────────────────────────────────────│
│ id UUID │
│ canonical_name "chicken_breast" │
│ spoonacular_id 1015062 │
│ aliases (JSONB) ["куриное филе", "куриная грудка", │
│ "филе курицы", "chicken breast", │
│ "chicken fillet"] │
│ category "meat" │
│ default_unit "g" │
│ calories_per_100g 165 │
│ protein_per_100g 31 │
│ fat_per_100g 3.6 │
│ carbs_per_100g 0 │
│ storage_days 3 │
└───────────────────────────────────────────────────────┘
```
### Как работает связка
```
Продукт юзера ingredient_mappings Рецепт из Spoonacular
────────────── ─────────────────── ─────────────────────
"Куриное филе" │ Ингредиент рецепта:
│ │ spoonacular_id: 1015062
│ Fuzzy search │ │
└────по aliases ───► canonical_name: ◄─── по spoonacular_id
"chicken_breast"
MATCH: оба
ссылаются на
одну сущность
```
### Потоки по сценариям
#### Сценарий 1: Добавление продукта (чек/фото → запасы)
```
1. Gemini возвращает: "Куриное филе, 500г"
2. Backend: fuzzy search по ingredient_mappings.aliases
SELECT * FROM ingredient_mappings
WHERE aliases @> '"куриное филе"'::jsonb
OR similarity(canonical_name, 'куриное филе') > 0.3
3. Найдено → product.mapping_id = ingredient_mappings.id
Автоподставляются: category, default_unit, storage_days, нутриенты
4. Не найдено → разовый запрос к Gemini:
"К какому каноническому ингредиенту относится 'филе индейки'?
Ответь JSON: { canonical_name, category, calories_per_100g, ... }"
Результат сохраняется в ingredient_mappings (новая строка)
Следующий юзер с таким же продуктом — AI не нужен
```
#### Сценарий 2: Проверка "есть ли ингредиент рецепта в запасах"
```
1. Рецепт "Паста Карбонара" имеет ингредиент: spoonacular_id = 1015062
2. ingredient_mappings: spoonacular_id 1015062 → canonical_name "chicken_breast"
3. products: mapping_id → ingredient_mappings.id WHERE canonical_name = "chicken_breast"
4. Найдено в products → отметка ✅ (есть в запасах)
Не найдено → отметка ❌ (нет) или 🔄 (есть замена)
```
#### Сценарий 3: Поиск рецептов "из моих продуктов"
```
1. Продукты юзера → через mapping_id → набор canonical_names
["chicken_breast", "rice_white", "carrot", "onion"]
2. SQL-запрос по рецептам:
SELECT r.*,
(SELECT count(*) FROM jsonb_array_elements(r.ingredients) i
WHERE i->>'mapping_id' IN (выбранные mapping_id)) as matched,
jsonb_array_length(r.ingredients) as total
FROM recipes r
ORDER BY matched::float / total DESC
3. Если в локальной БД мало результатов — дозапрос Spoonacular:
GET /recipes/findByIngredients?ingredients=chicken,rice,carrot,onion
Новые рецепты сохраняются в нашу БД
4. Gemini НЕ участвует — это чистый SQL + Spoonacular
```
#### Сценарий 4: Распознавание блюда (фото → калории)
```
1. Gemini определяет: "Паста Карбонара"
2. Backend: full-text search по recipes.title
SELECT * FROM recipes
WHERE to_tsvector('russian', title) @@ plainto_tsquery('russian', 'Паста Карбонара')
ORDER BY review_count DESC LIMIT 5
3. Найдено → привязка к рецепту (точные нутриенты из БД)
Не найдено → используются нутриенты от Gemini (помечены "≈ приблизительно")
```
#### Сценарий 5: Генерация меню
```
1. Backend отбирает кандидатов из БД (SQL: фильтры + наличие ингредиентов)
2. Формирует промпт с recipe_id:
"ID:42 Борщ (550ккал, ингр: свёкла✅ картофель✅ говядина✅ лук✅)
ID:87 Том Ям (320ккал, ингр: креветки❌ лемонграсс❌ кокос.молоко❌)"
3. Gemini ранжирует и возвращает recipe_id
4. Backend подгружает полные данные по ID
→ Gemini работает ТОЛЬКО с ID из нашей БД
→ Никакой рассинхронизации
```
### Наполнение таблицы маппингов
| Этап | Источник | Объём |
|------|----------|-------|
| Начальный импорт | Spoonacular Ingredient API | ~1 000 базовых ингредиентов |
| Ручная локализация | Перевод топ-200 ингредиентов | Aliases на русском |
| Batch-перевод | Gemini Flash-Lite (оффлайн) | Остальные aliases |
| Рантайм-дополнение | Gemini (при нераспознанном продукте) | По мере роста юзеров |
| Пользовательская обратная связь | Юзер корректирует продукт на экране редактирования | Новые aliases |
Со временем маппинг растёт, и AI для распознавания ингредиентов вызывается всё реже.
### Сводная таблица: где Gemini, а где нет
| Задача | Gemini? | Как стыкуется с БД |
|--------|---------|---------------------|
| OCR чека → продукты | Да (vision) | Fuzzy match по `ingredient_mappings.aliases` |
| Фото продуктов → список | Да (vision) | То же |
| Фото блюда → калории | Да (vision) | Full-text search по `recipes.title` |
| Поиск рецептов из продуктов | **Нет** | SQL по `mapping_id` + Spoonacular `findByIngredients` |
| Ранжировка/подбор меню | Да (текст) | Получает `recipe_id` из БД, возвращает `recipe_id` |
| Замена ингредиентов | Да (текст) | Кеш в `ingredient_substitutions` |
| Нераспознанный ингредиент | Да (текст, разово) | Результат сохраняется в `ingredient_mappings` |
---
## 6. Система очередей AI-запросов
### Проблема
Gemini API имеет ограничения по RPM (requests per minute) и стоит денег. Нужно:
1. Не допустить перерасход бюджета
2. Дать приоритет платным пользователям
3. Не блокировать систему при пиковых нагрузках
### Архитектура
```
Входящий AI-запрос
┌─────────────────────┐
│ Rate Limiter │
│ (per-user) │
│ │
│ Free: 20 req/час │
│ Paid: 100 req/час │
└──────────┬──────────┘
┌──────┴──────┐
│ │
Paid user Free user
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Paid Queue │ │ Free Queue │
│ │ │ │
│ N воркеров │ │ 1 воркер │
│ (быстро) │ │ (медленно) │
└──────┬──────┘ └──────┬──────┘
│ │
└───────┬───────┘
┌─────────────────────┐
│ Budget Guard │
│ │
│ Daily limit: $X │
│ Current: $Y │
│ │
│ Y >= X? → REJECT │
│ Y >= 0.8X? → WARN │
└──────────┬──────────┘
┌─────────────────────┐
│ Gemini API Call │
│ │
│ + Track cost │
│ + Log to ai_tasks │
└─────────────────────┘
```
### Компоненты
#### Rate Limiter (per-user)
- Алгоритм: token bucket (горутина + каналы в Go).
- Лимиты хранятся в конфигурации, разделены по тарифу (free/paid).
- При превышении: HTTP 429 + заголовок `Retry-After`.
- Клиент показывает: «Слишком много запросов. Попробуйте через N секунд.»
#### Priority Queues
- **Paid Queue:** N воркеров (горутин), обрабатывающих запросы параллельно. N зависит от лимита RPM Gemini API (например, при лимите 60 RPM и двух очередях — 50 RPM на paid, 10 RPM на free).
- **Free Queue:** 1 воркер, обрабатывает запросы последовательно. Пользователи free-тарифа ждут дольше, но результат получают.
- Реализация: `chan AITask` в Go с горутинами-воркерами. Без внешних брокеров сообщений на данном этапе.
- При масштабировании: миграция на Redis Streams или NATS.
#### Budget Guard
- **Daily budget cap:** максимальная сумма затрат на Gemini API в день (например, $50).
- **Учёт затрат:** каждый запрос логируется с оценочной стоимостью (input_tokens × price + output_tokens × price). Gemini API возвращает `usage_metadata` с точными токенами.
- **Пороги:**
- 80% бюджета: предупреждение в логи, приоритет только paid-очереди.
- 100% бюджета: free-очередь останавливается, paid продолжает (из резерва).
- 120% бюджета (абсолютный лимит): все AI-запросы отклоняются.
- **Graceful degradation:** при отклонении запроса клиент показывает: «AI-распознавание временно недоступно. Вы можете добавить продукты вручную.»
#### Таблица `ai_tasks` (лог)
| Поле | Тип | Описание |
|------|-----|----------|
| id | UUID | |
| user_id | UUID | FK → users |
| task_type | ENUM | receipt_ocr, product_recognition, dish_recognition, recipe_generation, menu_planning, substitution |
| status | ENUM | queued, processing, completed, failed, rejected |
| priority | ENUM | free, paid |
| input_tokens | INT | Токены на вход (от API) |
| output_tokens | INT | Токены на выход (от API) |
| estimated_cost | DECIMAL | Оценочная стоимость в $ |
| queue_time_ms | INT | Время ожидания в очереди |
| process_time_ms | INT | Время обработки |
| created_at | TIMESTAMP | |
| completed_at | TIMESTAMP | |
Используется для: аналитики затрат, мониторинга, оптимизации промптов.
---
## 7. Рецепты и контент
### Источники рецептов
```
┌────────────────────────────────────────────────────┐
│ Recipe Service │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │
│ │ Spoonacular │ │ AI-generated │ │ User │ │
│ │ Import │ │ Recipes │ │ Recipes │ │
│ │ │ │ │ │ │ │
│ │ source: │ │ source: │ │ source: │ │
│ │ 'spoonacular'│ │ 'ai' │ │ 'user' │ │
│ └──────┬───────┘ └──────┬───────┘ └────┬─────┘ │
│ │ │ │ │
│ └────────┬────────┘───────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────┐ │
│ │ Единая таблица │ │
│ │ recipes в БД │ │
│ └────────────────┘ │
└────────────────────────────────────────────────────┘
```
### Импорт из Spoonacular
- **Начальный импорт:** 5 00010 000 самых популярных рецептов (по рейтингу).
- **Данные:** название, описание, ингредиенты с граммовками, шаги приготовления, калории, БЖУ, время, сложность, кухня, фото, теги.
- **Хранение:** в нашей БД (PostgreSQL). Не зависим от Spoonacular API при показе рецептов пользователю.
- **Синхронизация:** фоновый джоб раз в неделю — обновление данных, добавление новых рецептов из популярных.
- **Spoonacular API** используется также для: расширенного поиска (когда в локальной БД нет подходящего), справочника нутриентов, данных по ингредиентам.
- **Локализация:** рецепты из Spoonacular на английском. Перевод — через Gemini Flash-Lite (batch, оффлайн). Переведённые рецепты кешируются в БД.
### Согласованность AI и рецептов
**Критическое правило:** AI при подборе меню работает ТОЛЬКО с рецептами из нашей БД.
Поток:
1. Backend выбирает кандидатов из БД по фильтрам (SQL-запрос: кухня, сложность, калории, ингредиенты).
2. Backend формирует промпт с ID и метаданными кандидатов.
3. AI ранжирует и компонует меню, возвращая `recipe_id`.
4. Backend подгружает полные данные рецептов по ID.
Это гарантирует: точные нутриенты, наличие фото, корректные шаги, возможность оставить отзыв.
**Исключение:** AI-генерация нового рецепта «из того, что есть» — когда в БД нет подходящего. Такой рецепт сохраняется в БД как `source = 'ai'` и доступен другим пользователям после модерации (по рейтингу).
---
## 8. Доменные сущности (схема БД)
```
┌──────────────┐ ┌──────────────────┐
│ users │ │ products │
│──────────────│ │──────────────────│
│ id │◄──┐ │ id │
│ firebase_uid │ │ │ user_id (FK) │──► users
│ email │ │ │ mapping_id (FK) │──► ingredient_mappings
│ name │ │ │ name │
│ avatar_url │ │ │ quantity │
│ height_cm │ │ │ unit │
│ weight_kg │ │ │ category │
│ age │ │ │ storage_days │
│ gender │ │ │ added_at │
│ activity │ │ │ expires_at │
│ goal │ │ │ (computed) │
│ │ │ └──────────────────┘
│ plan (free/ │ │
│ paid) │ │ ┌──────────────────┐
│ preferences │ │ │ recipes │
│ (JSONB) │ │ │──────────────────│
│ created_at │ │ │ id │
└──────────────┘ │ │ source │
│ │ spoonacular_id │
│ │ title │
│ │ description │
│ │ cuisine │
│ │ difficulty │
│ │ prep_time_min │
│ │ calories │
│ │ protein │
│ │ fat │
│ │ carbs │
│ │ servings │
│ │ image_url │
│ │ ingredients │
│ │ (JSONB) │
│ │ steps (JSONB) │
│ │ tags (JSONB) │
│ │ avg_rating │
│ │ review_count │
│ │ created_by │──► users (NULL для spoonacular)
│ └──────────────────┘
┌──────────────────┤ ┌──────────────────┐
│ menu_plans │ │ menu_items │
│──────────────────│ │──────────────────│
│ id │ │ id │
│ user_id (FK) ───┘ │ menu_plan_id(FK) │──► menu_plans
│ week_start │ │ day_of_week │
│ created_at │ │ meal_type │
│ template_name │ │ recipe_id (FK) │──► recipes
│ (NULL если не │ │ servings │
│ шаблон) │ └──────────────────┘
└──────────────────┘
┌──────────────────┐
│ meal_diary │
│──────────────────│
│ id │
│ user_id (FK) │──► users
│ date │
│ meal_type │
│ recipe_id (FK) │──► recipes (NULL для ручного ввода)
│ name │
│ portions │
│ calories │
│ protein │
│ fat │
│ carbs │
│ source │
│ (menu/photo/ │
│ manual/recipe) │
│ created_at │
└──────────────────┘
┌──────────────────┐ ┌──────────────────┐
│ reviews │ │ shopping_lists │
│──────────────────│ │──────────────────│
│ id │ │ id │
│ user_id (FK) │──►│ user_id (FK) │──► users
│ recipe_id (FK) │──►│ menu_plan_id(FK) │──► menu_plans
│ rating (1-5) │ │ items (JSONB) │
│ text │ │ created_at │
│ photo_url │ └──────────────────┘
│ created_at │
└──────────────────┘ ┌──────────────────┐
│ water_tracker │
│──────────────────│
│ id │
│ user_id (FK) │──► users
│ date │
│ glasses │
└──────────────────┘
┌──────────────────┐ ┌──────────────────────────┐
│ ai_tasks │ │ ingredient_substitutions │
│──────────────────│ │──────────────────────────│
│ (см. раздел 6) │ │ id │
└──────────────────┘ │ original_mapping_id (FK) │──► ingredient_mappings
│ substitute_mapping_id(FK)│──► ingredient_mappings
│ ratio │
│ note │
│ created_at │
└──────────────────────────┘
┌────────────────────────────┐
│ ingredient_mappings │
│────────────────────────────│
│ id │
│ canonical_name │
│ spoonacular_id (UNIQUE) │
│ aliases (JSONB) │
│ category │
│ default_unit │
│ calories_per_100g │
│ protein_per_100g │
│ fat_per_100g │
│ carbs_per_100g │
│ storage_days │
│ created_at │
└────────────────────────────┘
```
### Ключевые решения по схеме
- **`products.mapping_id`** — FK на `ingredient_mappings`. Связывает продукт пользователя с каноническим ингредиентом. Через эту связь определяется наличие ингредиентов рецепта в запасах. Может быть NULL (если продукт не удалось сопоставить).
- **`ingredient_mappings`** — каноническая таблица ингредиентов. `aliases` (JSONB) содержит все варианты написания на разных языках. `spoonacular_id` связывает с ингредиентами рецептов из Spoonacular. Нутриенты на 100г используются для пересчёта калорийности AI-генерированных рецептов. `storage_days` — дефолтный срок хранения для категории.
- **`ingredient_substitutions`** — кеш замен ингредиентов. Ссылается на `ingredient_mappings` по обоим ингредиентам (оригинал и замена). Один раз определённая AI-замена переиспользуется для всех пользователей.
- **`products.storage_days`** — период хранения после покупки (не фиксированная дата). `expires_at` вычисляется как `added_at + storage_days`. При отображении: «осталось X дней». Дефолт берётся из `ingredient_mappings.storage_days` при привязке.
- **`products.unit`** — ENUM: g, kg, ml, l, pcs, bunch, pack. Фронтенд отображает локализованные названия. Дефолт берётся из `ingredient_mappings.default_unit`.
- **`recipes.ingredients`** — JSONB массив: `[{ "name": "...", "amount": 200, "unit": "g", "optional": false }]`. JSONB позволяет гибко хранить без нормализации, при этом поддерживая поиск (`@>` оператор).
- **`recipes.steps`** — JSONB массив: `[{ "order": 1, "title": "...", "description": "...", "image_url": "...", "timer_seconds": null }]`. `timer_seconds` не null → на шаге отображается таймер.
- **`recipes.source`** — ENUM: spoonacular, ai, user. Определяет происхождение рецепта.
- **`meal_diary.source`** — откуда записано: из меню, через фото, вручную, из рецепта.
- **`shopping_lists.items`** — JSONB: `[{ "name": "...", "amount": 1, "unit": "l", "category": "dairy", "checked": false, "manual": false }]`.
---
## 9. API-дизайн (ключевые эндпоинты)
### Авторизация
| Метод | Путь | Описание |
|-------|------|----------|
| POST | `/auth/login` | Firebase token → JWT |
| POST | `/auth/refresh` | Обновить JWT |
| POST | `/auth/logout` | Инвалидировать refresh token |
### Продукты
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/products` | Список продуктов (фильтры: category, expiring) |
| POST | `/products` | Добавить продукт вручную |
| POST | `/products/batch` | Массовое добавление (после распознавания) |
| PUT | `/products/{id}` | Обновить (вес, количество, срок) |
| PATCH | `/products/{id}/consume` | Частичное использование |
| DELETE | `/products/{id}` | Удалить |
| DELETE | `/products` | Очистить все (с подтверждением в query param) |
| GET | `/products/storage-defaults` | Дефолтные сроки хранения по категориям |
| PUT | `/products/storage-defaults` | Обновить дефолтные сроки |
### AI-операции
| Метод | Путь | Описание |
|-------|------|----------|
| POST | `/ai/recognize-receipt` | Фото чека → список продуктов |
| POST | `/ai/recognize-products` | Фото продуктов → список |
| POST | `/ai/recognize-dish` | Фото блюда → калории, БЖУ |
| POST | `/ai/suggest-recipes` | Подбор рецептов из продуктов |
| POST | `/ai/generate-menu` | Генерация меню на период |
| POST | `/ai/substitute` | Замена ингредиента |
| GET | `/ai/tasks/{id}` | Статус AI-задачи (для polling) |
Все `/ai/*` эндпоинты возвращают `{ task_id }` (HTTP 202 Accepted). Клиент получает результат через polling `/ai/tasks/{id}` или через WebSocket (будущее улучшение). Это позволяет обрабатывать запросы асинхронно через очереди.
### Рецепты
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/recipes` | Каталог (фильтры, поиск, пагинация) |
| GET | `/recipes/{id}` | Карточка рецепта |
| GET | `/recipes/recommended` | Персональные рекомендации |
| GET | `/recipes/recent` | Недавно приготовленные |
| POST | `/recipes` | Создать пользовательский рецепт |
| POST | `/recipes/{id}/favorite` | Добавить в избранное |
| DELETE | `/recipes/{id}/favorite` | Убрать из избранного |
### Отзывы
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/recipes/{id}/reviews` | Отзывы к рецепту |
| POST | `/recipes/{id}/reviews` | Написать отзыв |
### Меню
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/menu?week=2026-W08` | Меню на неделю |
| PUT | `/menu/items/{id}` | Обновить слот меню |
| POST | `/menu/items` | Добавить блюдо в слот |
| DELETE | `/menu/items/{id}` | Убрать блюдо |
| POST | `/menu/templates` | Сохранить как шаблон |
| GET | `/menu/templates` | Список шаблонов |
| POST | `/menu/from-template/{id}` | Применить шаблон |
### Дневник питания
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/diary?date=2026-02-15` | Записи за день |
| POST | `/diary` | Добавить запись |
| PUT | `/diary/{id}` | Изменить (порция, и т.д.) |
| DELETE | `/diary/{id}` | Удалить запись |
### Список покупок
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/shopping-list` | Текущий список |
| POST | `/shopping-list/generate` | Сгенерировать из меню |
| PUT | `/shopping-list/items/{index}` | Обновить позицию |
| PATCH | `/shopping-list/items/{index}/check` | Отметить купленным |
| POST | `/shopping-list/items` | Добавить вручную |
### Статистика
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/stats?period=week` | Статистика за период |
| GET | `/stats/water?date=2026-02-15` | Трекер воды |
| PUT | `/stats/water` | Обновить стаканы |
### Профиль
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/profile` | Данные профиля |
| PUT | `/profile` | Обновить |
| PUT | `/profile/preferences` | Обновить предпочтения кухонь |
| PUT | `/profile/restrictions` | Обновить ограничения |
---
## 10. Оценка затрат
### Стоимость по стадиям (в месяц)
| Компонент | 1K MAU | 10K MAU | 50K MAU |
|-----------|--------|---------|---------|
| Firebase Auth | $0 | $0 | $0 (лимит 50K) |
| Gemini API | $50150 | $5001 500 | $2 5007 500 |
| Spoonacular | $29 | $149 | $149 |
| Хостинг (Cloud Run / Fly.io) | $1020 | $50100 | $200500 |
| PostgreSQL (managed) | $15 | $50 | $150300 |
| S3 (фото) | $5 | $20 | $50100 |
| **Итого** | **$110220** | **$7701 820** | **$3 0508 550** |
### Оптимизация затрат на AI
- **Кеширование результатов:** замены ингредиентов, распознавание типовых блюд.
- **Batch-запросы:** при генерации меню на неделю — один запрос вместо семи.
- **Снижение detail для фото:** Gemini поддерживает `low` / `high` detail. Для чеков `high`, для фото продуктов `low` достаточно.
- **Мониторинг:** дашборд затрат по `ai_tasks` — какие задачи дорогие, где можно оптимизировать промпты.

1884
docs/plans/Iteration_0.md Normal file

File diff suppressed because it is too large Load Diff

499
docs/plans/Summary.md Normal file
View File

@@ -0,0 +1,499 @@
# FoodAI — План реализации
## Обзор итераций
| # | Итерация | Цель | Зависит от |
|---|----------|------|------------|
| 0 | Фундамент | Go-проект, БД, авторизация, Flutter-каркас | — |
| 1 | Справочник ингредиентов и рецепты | Наполнение БД рецептами, маппинг ингредиентов | 0 |
| 2 | Управление продуктами | CRUD продуктов, сроки хранения | 0 |
| 3 | AI-ядро | Очереди, Gemini-адаптер, rate limiter, budget guard | 0 |
| 4 | AI-распознавание | OCR чека, фото продуктов, фото блюд | 2, 3 |
| 5 | Каталог рецептов | Поиск, фильтры, «из моих продуктов», замены | 1, 2 |
| 6 | Планирование меню | Меню на неделю, AI-генерация, список покупок | 3, 5 |
| 7 | Дневник питания | Записи, порции, трекер воды, калории | 5 |
| 8 | Режим готовки | Пошаговая готовка, таймеры | 5 |
| 9 | Рекомендации и статистика | Рекомендации на главной, графики, тренды | 6, 7 |
| 10 | Полировка | Онбординг, пустые состояния, уведомления, отзывы | 9 |
## Карта зависимостей
```
┌──────────────┐
│ 0. Фундамент │
└──────┬───────┘
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌───────────┐ ┌────────────────┐
│ 1. Справочник │ │ 2. Продук-│ │ 3. AI-ядро │
│ ингредиентов │ │ ты │ │ (очереди, │
│ + рецепты │ │ │ │ Gemini) │
└───────┬────────┘ └─────┬─────┘ └───────┬────────┘
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ │ ┌────┴──────────┘
│ │ │
│ ▼ ▼
│ ┌──────────────────┐
│ │ 4. AI-распозна- │
│ │ вание │
│ └──────────────────┘
│ │
└─────┬─────┘
┌────────────────┐
│ 5. Каталог │
│ рецептов │
└───────┬────────┘
┌──────────┼──────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 6. Меню │ │ 7. Днев- │ │ 8. Режим │
│ + список │ │ ник пита-│ │ готовки │
│ покупок │ │ ния │ │ │
└─────┬────┘ └─────┬────┘ └──────────┘
│ │
└──────┬─────┘
┌────────────────┐
│ 9. Рекоменда- │
│ ции + стат-ка │
└───────┬────────┘
┌────────────────┐
│ 10. Полировка │
└────────────────┘
```
**Параллельная разработка:** итерации 1, 2, 3 могут выполняться параллельно. Итерации 6, 7, 8 — тоже параллельно после завершения 5.
---
## Итерация 0: Фундамент
> **Детальный план:** [Iteration_0.md](./Iteration_0.md)
**Цель:** развернуть скелет проекта, базу данных, авторизацию и каркас мобильного приложения. После итерации можно зарегистрироваться, войти и увидеть пустые экраны.
**Зависимости:** нет.
### User Stories
#### Backend
| ID | Story | Описание |
|----|-------|----------|
| 0.1 | Инициализация Go-проекта | Структура проекта (cmd/, internal/, pkg/), go.mod, конфигурация (envconfig), логгер (slog), graceful shutdown |
| 0.2 | PostgreSQL + миграции | Подключение к PostgreSQL (pgx), система миграций (goose или golang-migrate). Начальная миграция: таблица `users` |
| 0.3 | HTTP-сервер + роутер | HTTP-сервер (net/http или chi), middleware (CORS, request ID, logging, recovery), healthcheck endpoint |
| 0.4 | Firebase Auth интеграция | Firebase Admin SDK. Middleware для верификации Firebase idToken. Выдача собственного JWT. Эндпоинты: `POST /auth/login`, `POST /auth/refresh`, `POST /auth/logout` |
| 0.5 | Таблица users | Миграция: users (id, firebase_uid, email, name, avatar_url, параметры тела, цель, preferences JSONB, plan, created_at). CRUD-сервис |
| 0.6 | Docker Compose | docker-compose.yml: PostgreSQL, приложение. Makefile с основными командами (migrate, run, test) |
#### Flutter
| ID | Story | Описание |
|----|-------|----------|
| 0.7 | Инициализация Flutter-проекта | Создание проекта, структура (features/, core/, shared/), подключение основных пакетов (dio, riverpod/bloc, go_router) |
| 0.8 | Firebase Auth во Flutter | Пакеты firebase_auth, google_sign_in, sign_in_with_apple. Экраны: вход (email + Google + Apple), регистрация. Хранение JWT в secure storage |
| 0.9 | Навигация и каркас экранов | Bottom Tab Bar (5 вкладок), пустые заглушки для каждого экрана, роутинг |
| 0.10 | API-клиент | Dio-клиент с interceptors: JWT-токен, refresh, error handling. Базовые модели (User) |
### Результат итерации
- Можно зарегистрироваться через email / Google / Apple
- Войти в приложение и увидеть 5 вкладок с заглушками
- Backend отвечает на healthcheck и auth-запросы
- БД содержит таблицу users с данными зарегистрированных пользователей
---
## Итерация 1: Справочник ингредиентов и рецепты
**Цель:** наполнить БД каноническими ингредиентами и рецептами из Spoonacular. Это фундамент для всех фичей, связанных с рецептами, поиском и маппингом продуктов.
**Зависимости:** итерация 0.
### User Stories
| ID | Story | Описание |
|----|-------|----------|
| 1.1 | Таблица ingredient_mappings | Миграция: id, canonical_name, spoonacular_id, aliases (JSONB), category, default_unit, нутриенты на 100г, storage_days. Индексы: GIN по aliases, UNIQUE по spoonacular_id |
| 1.2 | Импорт ингредиентов из Spoonacular | CLI-команда / джоб: запрос Spoonacular Ingredient API → сохранение ~1000 базовых ингредиентов в ingredient_mappings |
| 1.3 | Таблица recipes | Миграция: id, source, spoonacular_id, title, description, cuisine, difficulty, prep_time_min, калории, БЖУ, servings, image_url, ingredients (JSONB), steps (JSONB), tags (JSONB), avg_rating, review_count, created_by. Индексы: GIN по ingredients, full-text по title |
| 1.4 | Импорт рецептов из Spoonacular | CLI-команда / джоб: импорт 5 00010 000 популярных рецептов. Маппинг ингредиентов рецепта на ingredient_mappings через spoonacular_id |
| 1.5 | Перевод рецептов | Batch-джоб: перевод title, description, steps через Gemini Flash-Lite. Результат сохраняется в БД (поля title_ru, description_ru или отдельная таблица переводов) |
| 1.6 | Базовая локализация aliases | Перевод aliases топ-200 ингредиентов на русский. Batch через Gemini или ручной маппинг |
### Результат итерации
- БД содержит ~1 000 ингредиентов с русскими алиасами и нутриентами
- БД содержит 5 00010 000 рецептов с переводами, ингредиентами, шагами, нутриентами
- Каждый ингредиент рецепта связан с ingredient_mappings через spoonacular_id
---
## Итерация 2: Управление продуктами
**Цель:** пользователь может вести список своих продуктов вручную — добавлять, редактировать, удалять, отслеживать сроки.
**Зависимости:** итерация 0.
### User Stories
#### Backend
| ID | Story | Описание |
|----|-------|----------|
| 2.1 | Таблица products | Миграция: id, user_id, mapping_id (FK nullable), name, quantity, unit (ENUM), category, storage_days, added_at, expires_at (computed). Индексы по user_id, expires_at |
| 2.2 | Products CRUD API | `GET /products` (фильтры: category, expiring), `POST /products`, `PUT /products/{id}`, `DELETE /products/{id}`, `DELETE /products` (очистить все) |
| 2.3 | Частичное использование | `PATCH /products/{id}/consume` — уменьшить количество. Если количество = 0, предложить удаление |
| 2.4 | Массовое добавление | `POST /products/batch` — добавление нескольких продуктов за раз (после распознавания). Обработка дубликатов: проверка по mapping_id, предложение объединить |
| 2.5 | Дефолтные сроки хранения | `GET /products/storage-defaults`, `PUT /products/storage-defaults`. Хранение в user preferences (JSONB в таблице users) |
| 2.6 | Fuzzy matching при добавлении | При добавлении продукта — поиск по ingredient_mappings.aliases. Если найдено — автозаполнение: mapping_id, category, unit, storage_days, нутриенты |
#### Flutter
| ID | Story | Описание |
|----|-------|----------|
| 2.7 | Экран «Мои продукты» | Список продуктов по категориям, поиск, chip-фильтры, badge «осталось X дней» |
| 2.8 | Добавление/редактирование продукта | Форма: название, количество, единица, категория, период хранения. Выпадающее меню добавления (+) |
| 2.9 | Частичное использование | Модалка «Сколько осталось?» при свайпе или тапе |
| 2.10 | Очистить и перезаполнить | Контекстное меню (···) → подтверждение → очистка |
| 2.11 | Настройки сроков хранения | Экран из Профиля: список категорий с редактируемыми днями |
| 2.12 | Пустое состояние | Иллюстрация + CTA «Сфотографируйте продукты или сканируйте чек» |
### Результат итерации
- Пользователь может вручную добавить продукты, указать количество и сроки
- Видит «осталось X дней» для каждого продукта
- Может частично использовать продукт, удалить, очистить всё
- При добавлении — автоматический подбор категории, единицы, срока через fuzzy match
---
## Итерация 3: AI-ядро
**Цель:** построить инфраструктуру для AI-запросов: очереди, rate limiter, budget guard, адаптер Gemini. После итерации можно отправлять AI-запросы через API с контролем расхода.
**Зависимости:** итерация 0.
### User Stories
| ID | Story | Описание |
|----|-------|----------|
| 3.1 | AI Service интерфейсы | Go-интерфейсы: FoodRecognizer, RecipeGenerator, MenuPlanner, NutritionEstimator. Структуры запросов и ответов |
| 3.2 | Gemini-адаптер | Реализация интерфейсов через Gemini API (google/generative-ai-go). Structured JSON output. Обработка ошибок, retries |
| 3.3 | Таблица ai_tasks | Миграция: id, user_id, task_type, status, priority, input/output_tokens, estimated_cost, queue/process_time_ms, created_at, completed_at. Индексы по user_id, status, created_at |
| 3.4 | Priority Queue Manager | Две очереди (chan в Go): paid (N воркеров), free (1 воркер). Распределение RPM между очередями. Горутины-воркеры |
| 3.5 | Rate Limiter (per-user) | Token bucket на горутинах. Конфигурируемые лимиты по тарифу (free: 20 req/час, paid: 100 req/час). HTTP 429 при превышении |
| 3.6 | Budget Guard | Подсчёт дневных затрат по ai_tasks. Пороги: 80% → warn, 100% → free stop, 120% → all stop. Счётчик сбрасывается в полночь |
| 3.7 | AI API эндпоинты (заглушки) | `POST /ai/recognize-receipt`, `/ai/recognize-products`, `/ai/recognize-dish`, `/ai/suggest-recipes`, `/ai/generate-menu`, `/ai/substitute`. Возвращают task_id (HTTP 202). `GET /ai/tasks/{id}` для polling |
| 3.8 | Логирование и мониторинг | Каждый AI-запрос логируется в ai_tasks с токенами и стоимостью. Эндпоинт `/admin/ai-stats` для просмотра затрат |
### Результат итерации
- AI-запросы проходят через очередь с приоритетами
- Paid-пользователи обслуживаются быстрее
- Расход бюджета контролируется, при превышении — graceful degradation
- Все запросы логируются с точной стоимостью
- API эндпоинты принимают запросы и возвращают результат через polling
---
## Итерация 4: AI-распознавание
**Цель:** пользователь может фотографировать чеки, продукты и блюда — AI распознаёт и предлагает результат для корректировки.
**Зависимости:** итерации 2 (продукты), 3 (AI-ядро).
### User Stories
#### Backend
| ID | Story | Описание |
|----|-------|----------|
| 4.1 | OCR чека | Реализация FoodRecognizer.RecognizeReceipt: фото → Gemini Flash (vision) → structured JSON (name, quantity, unit, category, price, confidence). Маппинг результатов на ingredient_mappings |
| 4.2 | Распознавание продуктов (фото) | FoodRecognizer.RecognizeProducts: фото → Gemini Flash → JSON. Поддержка мультифото (объединение результатов, дедупликация). Маппинг на ingredient_mappings |
| 4.3 | Распознавание блюда | FoodRecognizer.RecognizeDish: фото → Gemini Flash → dish_name, weight, calories, БЖУ, confidence. Full-text search по recipes.title для привязки к рецепту из БД |
| 4.4 | Авто-маппинг нераспознанных | Если fuzzy match по aliases не нашёл ингредиент → разовый запрос к Gemini: определить canonical_name → сохранить в ingredient_mappings. Следующий запрос с таким же продуктом — без AI |
| 4.5 | Загрузка фото | Эндпоинт для multipart upload фото. Сохранение в S3. Передача URL в AI-задачу |
#### Flutter
| ID | Story | Описание |
|----|-------|----------|
| 4.6 | Экран камеры (чек) | Видоискатель, кнопка съёмки, выбор из галереи. Отправка на backend |
| 4.7 | Экран камеры (еда) | Переключатель «Готовое блюдо» / «Продукты». Съёмка, отправка |
| 4.8 | Экран загрузки AI | Анимация «Распознаём...» с индикатором. Polling по task_id |
| 4.9 | Экран корректировки (чек/фото продуктов) | Список распознанных продуктов. Инлайн-редактирование: название, количество, единица, категория, срок хранения. Чекбоксы, удаление, добавление вручную, «Сделать ещё фото». Предупреждения о дубликатах. CTA «Добавить в мои продукты» |
| 4.10 | Экран результата (фото блюда) | Фото, название, калории, БЖУ. Подтверждение / корректировка. Слайдер порции. Выбор приёма пищи. CTA «Записать в дневник» |
| 4.11 | Обработка ошибок AI | Экран «Не удалось распознать» → «Переснять» / «Ввести вручную» |
### Результат итерации
- Пользователь фотографирует чек → получает список продуктов → корректирует → добавляет в запасы
- Фотографирует холодильник (несколько фото) → то же
- Фотографирует блюдо → видит калории и БЖУ → может записать в дневник
- Нераспознанные ингредиенты автоматически добавляются в справочник
---
## Итерация 5: Каталог рецептов
**Цель:** пользователь может просматривать, искать и фильтровать рецепты. Видит, какие ингредиенты есть в запасах, а каких не хватает. Может добавить рецепт в избранное.
**Зависимости:** итерации 1 (рецепты в БД), 2 (продукты для проверки наличия).
### User Stories
#### Backend
| ID | Story | Описание |
|----|-------|----------|
| 5.1 | Поиск и фильтрация рецептов | `GET /recipes` — фильтры: cuisine, difficulty, prep_time, calories_max, meal_type, diet_tags. Full-text search по title. Пагинация (cursor-based) |
| 5.2 | «Из моих продуктов» | Фильтр: сопоставление ingredients[].mapping_id с products.mapping_id пользователя. Ранжирование по доле совпадения. На каждом рецепте: «Есть всё ✓» / «-N прод.» |
| 5.3 | Карточка рецепта с наличием | `GET /recipes/{id}` — рецепт + для каждого ингредиента: есть ✅ / нет ❌ / замена 🔄. Итог: «Всё есть» / «Не хватает N» |
| 5.4 | Замены ингредиентов | При ❌ — поиск замены: сначала в таблице ingredient_substitutions, затем (если нет) — запрос к Gemini, результат кешируется |
| 5.5 | Избранное | `POST /recipes/{id}/favorite`, `DELETE /recipes/{id}/favorite`. Таблица favorites (user_id, recipe_id). `GET /recipes?favorite=true` |
| 5.6 | Дозапрос Spoonacular | Если в локальной БД мало результатов по фильтрам — запрос к Spoonacular API (findByIngredients, complexSearch). Новые рецепты сохраняются в БД |
#### Flutter
| ID | Story | Описание |
|----|-------|----------|
| 5.7 | Экран каталога рецептов | Сетка 2 колонки, поиск, chip-фильтры, кнопка «Из моих продуктов», панель фильтров (bottom sheet), бесконечный скролл |
| 5.8 | Карточка рецепта | Фото, рейтинг, метаинформация (время/сложность/кухня), калории/БЖУ, регулятор порций, список ингредиентов с ✅/❌/🔄, описание. CTA «Начать готовить», «Добавить в меню» |
| 5.9 | Замены ингредиентов | Строка «→ Замена: пармезан (есть)» под ингредиентом с 🔄 |
| 5.10 | Кнопка «Добавить в список покупок» | Недостающие ингредиенты → формирование позиций для списка покупок |
### Результат итерации
- Пользователь ищет рецепты, фильтрует по кухне/сложности/времени/калориям
- Видит, что можно приготовить из имеющихся продуктов
- Для каждого рецепта — отметки наличия ингредиентов и предложения замен
- Может добавить рецепт в избранное
---
## Итерация 6: Планирование меню
**Цель:** пользователь может составлять меню на неделю — вручную или через AI-генерацию. Формируется список покупок.
**Зависимости:** итерации 3 (AI-ядро для генерации), 5 (каталог рецептов).
### User Stories
#### Backend
| ID | Story | Описание |
|----|-------|----------|
| 6.1 | Таблицы menu_plans и menu_items | Миграции. menu_plans: id, user_id, week_start, template_name. menu_items: id, menu_plan_id, day_of_week, meal_type, recipe_id, servings |
| 6.2 | Menu CRUD | `GET /menu?week=`, `POST /menu/items`, `PUT /menu/items/{id}`, `DELETE /menu/items/{id}`. Подсчёт калорий за день/неделю |
| 6.3 | AI-генерация меню | `POST /ai/generate-menu`: backend отбирает кандидатов из БД (SQL по фильтрам + наличие ингредиентов) → формирует промпт с recipe_id → Gemini ранжирует → backend сохраняет menu_items |
| 6.4 | Шаблоны меню | `POST /menu/templates` (сохранить), `GET /menu/templates` (список), `POST /menu/from-template/{id}` (применить). История прошлых меню |
| 6.5 | Таблица shopping_lists | Миграция: id, user_id, menu_plan_id, items (JSONB). Автогенерация из меню: ингредиенты рецептов имеющиеся продукты = список |
| 6.6 | Shopping list API | `GET /shopping-list`, `POST /shopping-list/generate`, `PUT /shopping-list/items/{idx}`, `PATCH /shopping-list/items/{idx}/check`, `POST /shopping-list/items` (ручная позиция) |
#### Flutter
| ID | Story | Описание |
|----|-------|----------|
| 6.7 | Экран меню | Понедельный календарь, слоты по приёмам пищи, калорийность за день, drag-and-drop, контекстное меню (···), пустые слоты с подсказками |
| 6.8 | Добавление блюда в слот | Модалка выбора дня + приёма пищи. Переход в каталог рецептов для выбора |
| 6.9 | AI-генерация | Кнопка ⚡ → экран параметров (период, кухня, сложность, из моих продуктов, калории) → генерация → отображение результата с возможностью заменить отдельные блюда |
| 6.10 | Шаблоны и история | Выпадающее меню: сохранить как шаблон, загрузить из шаблона, из истории |
| 6.11 | Экран списка покупок | Список по категориям, чекбоксы, свайп-удаление, ручное добавление, итого, «Поделиться», «Пересчитать из меню» |
| 6.12 | Переходный экран «Составить меню?» | После добавления продуктов (чек/фото) → предложение сгенерировать меню с выбором параметров |
### Результат итерации
- Пользователь составляет меню на неделю — вручную или AI-генерацией
- AI подбирает рецепты из нашей БД с учётом продуктов, калорий, предпочтений
- Формируется список покупок (автоматически из меню запасы)
- Можно сохранять шаблоны и повторять удачные меню
- После сканирования чека — плавный переход к генерации меню
---
## Итерация 7: Дневник питания
**Цель:** пользователь ведёт учёт съеденного — записывает приёмы пищи, отслеживает калории и БЖУ, регулирует порции.
**Зависимости:** итерация 5 (рецепты для добавления из каталога).
### User Stories
#### Backend
| ID | Story | Описание |
|----|-------|----------|
| 7.1 | Таблица meal_diary | Миграция: id, user_id, date, meal_type, recipe_id (nullable), name, portions, calories, protein, fat, carbs, source (menu/photo/manual/recipe), created_at |
| 7.2 | Diary CRUD | `GET /diary?date=`, `POST /diary`, `PUT /diary/{id}`, `DELETE /diary/{id}`. Подсчёт итогов дня (калории, БЖУ) |
| 7.3 | Из меню в дневник | При отметке «съедено» на главном экране → автосоздание записи в дневнике. Списание ингредиентов из продуктов |
| 7.4 | Трекер воды | Таблица water_tracker (user_id, date, glasses). `GET /stats/water?date=`, `PUT /stats/water` |
| 7.5 | База продуктов для быстрого поиска | Endpoint для поиска по ingredient_mappings: `GET /ingredients/search?q=банан` → название + нутриенты на порцию. Для перекусов без рецепта |
#### Flutter
| ID | Story | Описание |
|----|-------|----------|
| 7.6 | Экран дневника питания | Навигация по дням, круговой прогресс калорий, прогресс-бары БЖУ, приёмы пищи, порции, «+ Добавить» |
| 7.7 | Модалка добавления | Варианты: сфотографировать, из меню, из каталога, из избранного, быстрый поиск продукта, вручную |
| 7.8 | Указание порции | Слайдер 0.5x3x при добавлении из рецепта. Пересчёт калорий/БЖУ |
| 7.9 | Быстрый поиск продукта | Поле поиска → результаты из ingredient_mappings → тап → добавить в дневник с указанием количества |
| 7.10 | Трекер воды | Ряд стаканов внизу дневника, тап = +1/-1 |
| 7.11 | Главный экран — карточка калорий | Круговой прогресс, тап → переход в дневник |
| 7.12 | Главный экран — «Сегодня в меню» | Список из menu_items на сегодня с чекбоксами «съедено» |
### Результат итерации
- Пользователь записывает приёмы пищи: из меню, каталога, фото или вручную
- Регулирует порции, видит калории и БЖУ за день
- На главном экране — прогресс калорий и чекбоксы «съедено»
- Трекер воды
---
## Итерация 8: Режим готовки
**Цель:** пользователь готовит блюдо по пошаговой инструкции с таймерами. После завершения — запись в дневник и оценка.
**Зависимости:** итерация 5 (карточка рецепта для запуска).
### User Stories
| ID | Story | Описание |
|----|-------|----------|
| 8.1 | Экран пошаговой готовки | Фото шага, заголовок, описание (крупный шрифт), навигация «Назад»/«Далее», свайп, точечный индикатор прогресса |
| 8.2 | Таймеры | Кнопка «Запустить таймер» на шагах с timer_seconds. Обратный отсчёт. Пауза, стоп. Несколько таймеров параллельно |
| 8.3 | Панель активных таймеров | Фиксирована внизу. Показывает все запущенные таймеры с оставшимся временем |
| 8.4 | Уведомления таймера | Push-уведомление + звук при завершении таймера. Модалка «Готово!» |
| 8.5 | Keep screen on | Экран не гаснет в режиме готовки (wakelock) |
| 8.6 | Закрытие с подтверждением | Кнопка ✕ → «Прервать готовку?» |
| 8.7 | Экран завершения | «Приятного аппетита!» → «Записать в дневник» (с выбором порций), «Оценить рецепт», «Поделиться фото». Автосписание ингредиентов из запасов |
### Результат итерации
- Пользователь готовит по шагам с фото и описанием
- Запускает таймеры (параллельно), получает уведомления
- По завершении — запись в дневник, оценка рецепта, списание продуктов
---
## Итерация 9: Рекомендации и статистика
**Цель:** приложение проактивно рекомендует рецепты. Пользователь видит аналитику своего питания.
**Зависимости:** итерации 6 (меню), 7 (дневник — данные для статистики).
### User Stories
#### Рекомендации
| ID | Story | Описание |
|----|-------|----------|
| 9.1 | Рекомендации на главном экране | Карусель «Рекомендуем приготовить». Алгоритм: (1) рецепты из продуктов с истекающим сроком, (2) полное совпадение ингредиентов, (3) предпочтения кухни. Endpoint: `GET /recipes/recommended` |
| 9.2 | «Готовили недавно» | Секция на главном экране и в каталоге. Endpoint: `GET /recipes/recent` — последние 5 приготовленных (из meal_diary с source=recipe) |
| 9.3 | Секция «Для вас» в каталоге | Персональные рекомендации на основе: оценок, предпочтений кухонь, истории. Endpoint: `GET /recipes/recommended?section=personal` |
| 9.4 | Подсказки в пустых слотах меню | При пустом слоте меню — рекомендация на основе оставшихся калорий + продукты с истекающим сроком |
#### Статистика
| ID | Story | Описание |
|----|-------|----------|
| 9.5 | Endpoint статистики | `GET /stats?period=week|month|3months` — калории/БЖУ по дням, средние, тренды, самые частые блюда. Агрегация по meal_diary |
| 9.6 | Экран статистики | Переключатель периода, столбчатая диаграмма калорий, stacked bar БЖУ, тренды (↑↓→), топ блюд |
| 9.7 | Переход из главного экрана | Тап по прогресс-бару калорий → дневник или статистика |
### Результат итерации
- На главном экране — рекомендации (приоритет на истекающие продукты) и «Готовили недавно»
- В каталоге — секция «Для вас»
- В меню — умные подсказки в пустых слотах
- Графики калорий и БЖУ за неделю/месяц/3 месяца
---
## Итерация 10: Полировка
**Цель:** довести приложение до продуктового качества — онбординг, пустые состояния, уведомления, отзывы, переходные экраны.
**Зависимости:** итерация 9.
### User Stories
#### Онбординг
| ID | Story | Описание |
|----|-------|----------|
| 10.1 | Экраны онбординга | 5 шагов: приветствие (свайп-карточки), параметры тела, цель + расчёт нормы, ограничения + предпочтения кухонь, предложение добавить продукты |
| 10.2 | Сохранение данных онбординга | `PUT /profile` с параметрами из онбординга. Сохранение предпочтений кухонь в preferences |
| 10.3 | Флаг прохождения онбординга | Показывать только при первом входе. Флаг в secure storage |
#### Пустые состояния и ошибки
| ID | Story | Описание |
|----|-------|----------|
| 10.4 | Пустые состояния всех экранов | Иллюстрация + текст + CTA для: продуктов, меню, дневника, статистики, рецептов (избранные) |
| 10.5 | Состояния ошибок | Нет сети (баннер + оффлайн-данные), ошибка AI (переснять / ввести вручную), ошибка сервера (повторить) |
| 10.6 | Toast с отменой | При удалении записи из дневника, продукта — toast «Удалено» + кнопка «Отменить» (5 сек) |
#### Уведомления
| ID | Story | Описание |
|----|-------|----------|
| 10.7 | Push-уведомления (FCM) | Интеграция Firebase Cloud Messaging. Flutter: запрос разрешений, обработка |
| 10.8 | Уведомления о сроках продуктов | Backend: cron-джоб утром → push «Молоко — осталось 1 день. Использовать в рецепте?» |
| 10.9 | Напоминания о приёмах пищи | По расписанию (настраиваемое): «Время обеда! В меню: ...» |
| 10.10 | Вечернее напоминание о воде | «Вы выпили 5 из 8 стаканов воды сегодня» |
#### Отзывы
| ID | Story | Описание |
|----|-------|----------|
| 10.11 | Таблица reviews | Миграция: id, user_id, recipe_id, rating, text, photo_url, created_at. Пересчёт avg_rating, review_count в recipes |
| 10.12 | API отзывов | `GET /recipes/{id}/reviews` (пагинация), `POST /recipes/{id}/reviews` |
| 10.13 | UI отзывов | Секция в карточке рецепта, модалка написания отзыва (звёзды + текст + фото), полный список отзывов |
#### Профиль
| ID | Story | Описание |
|----|-------|----------|
| 10.14 | Экран профиля | Аватар, параметры, цель, ограничения, предпочтения кухонь, ссылки (статистика, избранное, отзывы, сроки хранения, настройки) |
| 10.15 | Настройки приложения | Экран: уведомления (вкл/выкл по типам), тема (светлая/тёмная/системная), норма воды, язык |
### Результат итерации
- Новый пользователь проходит онбординг и сразу получает персонализированный опыт
- Все экраны имеют осмысленные пустые состояния
- Ошибки обрабатываются gracefully
- Push-уведомления о сроках, приёмах пищи, воде
- Можно оставлять отзывы к рецептам
- Полноценный профиль с настройками
---
## Итоги по объёму
| Итерация | Backend stories | Flutter stories | Всего |
|----------|----------------|-----------------|-------|
| 0. Фундамент | 6 | 4 | 10 |
| 1. Ингредиенты + рецепты | 6 | 0 | 6 |
| 2. Продукты | 6 | 6 | 12 |
| 3. AI-ядро | 8 | 0 | 8 |
| 4. AI-распознавание | 5 | 6 | 11 |
| 5. Каталог рецептов | 6 | 4 | 10 |
| 6. Меню + покупки | 6 | 6 | 12 |
| 7. Дневник питания | 5 | 7 | 12 |
| 8. Режим готовки | 0 | 7 | 7 |
| 9. Рекомендации + стат-ка | 4 | 3 | 7 |
| 10. Полировка | 5 | 10 | 15 |
| **Итого** | **57** | **53** | **110** |
## Приоритеты для MVP
Минимально жизнеспособный продукт — итерации **06**:
- Авторизация, продукты, AI-распознавание, рецепты, меню, список покупок
- Позволяет пройти основной пользовательский сценарий: купил продукты → сфотографировал чек → получил меню → составил список покупок
- **68 stories** из 110 (62%)
Итерации 710 — расширение до полного продукта.