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:
250
docs/Description.md
Normal file
250
docs/Description.md
Normal 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. Приветствие и краткое описание возможностей (2–3 карточки с иллюстрациями).
|
||||
2. Указание базовых параметров: пол, возраст, рост, вес, уровень активности.
|
||||
3. Выбор цели: похудение, поддержание, набор массы. Расчёт рекомендуемой калорийности.
|
||||
4. Указание ограничений и предпочтений (аллергии, диеты).
|
||||
5. Предпочтения по кухням (выбрать 2–3 из списка).
|
||||
6. Предложение добавить первые продукты: сфотографировать холодильник, сканировать чек или пропустить.
|
||||
7. Переход на главный экран.
|
||||
|
||||
## Экраны приложения
|
||||
|
||||
### Главный экран
|
||||
- Сводка на сегодня: запланированные приёмы пищи, текущий баланс калорий.
|
||||
- Быстрые действия: сфотографировать чек, сфотографировать еду, найти рецепт.
|
||||
- **Рекомендации:** блок «Рекомендуем приготовить» — 2–3 карточки рецептов на основе имеющихся продуктов (приоритет — продукты с истекающим сроком).
|
||||
- **Быстрый повтор:** секция «Готовили недавно» — последние 3–5 приготовленных рецептов для быстрого повтора.
|
||||
|
||||
### Мои продукты
|
||||
- Список имеющихся продуктов, сгруппированных по категориям.
|
||||
- Кнопка добавления: вручную, через фото, через сканирование чека.
|
||||
- **Кнопка сброса/перезаполнения** в контекстном меню.
|
||||
- Срок годности отображается как «осталось 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
1735
docs/Design.md
Normal file
File diff suppressed because it is too large
Load Diff
835
docs/Tech.md
Normal file
835
docs/Tech.md
Normal 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, название, калории, основные ингредиенты):
|
||||
[список из 50–100 кандидатов из БД]
|
||||
|
||||
Задача: составь меню на 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 000–10 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 | $50–150 | $500–1 500 | $2 500–7 500 |
|
||||
| Spoonacular | $29 | $149 | $149 |
|
||||
| Хостинг (Cloud Run / Fly.io) | $10–20 | $50–100 | $200–500 |
|
||||
| PostgreSQL (managed) | $15 | $50 | $150–300 |
|
||||
| S3 (фото) | $5 | $20 | $50–100 |
|
||||
| **Итого** | **$110–220** | **$770–1 820** | **$3 050–8 550** |
|
||||
|
||||
### Оптимизация затрат на AI
|
||||
|
||||
- **Кеширование результатов:** замены ингредиентов, распознавание типовых блюд.
|
||||
- **Batch-запросы:** при генерации меню на неделю — один запрос вместо семи.
|
||||
- **Снижение detail для фото:** Gemini поддерживает `low` / `high` detail. Для чеков `high`, для фото продуктов `low` достаточно.
|
||||
- **Мониторинг:** дашборд затрат по `ai_tasks` — какие задачи дорогие, где можно оптимизировать промпты.
|
||||
1884
docs/plans/Iteration_0.md
Normal file
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
499
docs/plans/Summary.md
Normal 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 000–10 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 000–10 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.5x–3x при добавлении из рецепта. Пересчёт калорий/БЖУ |
|
||||
| 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
|
||||
|
||||
Минимально жизнеспособный продукт — итерации **0–6**:
|
||||
|
||||
- Авторизация, продукты, AI-распознавание, рецепты, меню, список покупок
|
||||
- Позволяет пройти основной пользовательский сценарий: купил продукты → сфотографировал чек → получил меню → составил список покупок
|
||||
- **68 stories** из 110 (62%)
|
||||
|
||||
Итерации 7–10 — расширение до полного продукта.
|
||||
Reference in New Issue
Block a user