Files
food-ai/docs/Flow.md
dbastrikin e57ff8e06c feat: implement Iteration 1 — AI recipe recommendations
Backend:
- Add Groq LLM client (llama-3.3-70b) for recipe generation with JSON
  retry strategy (retries only on parse errors, not API errors)
- Add Pexels client for parallel photo search per recipe
- Add saved_recipes table (migration 004) with JSONB fields
- Add GET /recommendations endpoint (profile-aware prompt building)
- Add POST/GET/GET{id}/DELETE /saved-recipes CRUD endpoints
- Wire gemini, pexels, recommendation, savedrecipe packages in main.go

Flutter:
- Add Recipe, SavedRecipe models with json_serializable
- Add RecipeService (getRecommendations, getSavedRecipes, save, delete)
- Add RecommendationsNotifier and SavedRecipesNotifier (Riverpod)
- Add RecommendationsScreen with skeleton loading and refresh FAB
- Add RecipeDetailScreen (SliverAppBar, nutrition tooltip, steps with timer)
- Add SavedRecipesScreen with Dismissible swipe-to-delete and empty state
- Update RecipesScreen to TabBar (Recommendations / Saved)
- Add /recipe-detail route outside ShellRoute (no bottom nav)
- Extend ApiClient with getList() and deleteVoid()

Project:
- Add CLAUDE.md with English-only rule for comments and commit messages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 22:43:29 +02:00

37 KiB
Raw Permalink Blame History

Flow: взаимодействие пользователя → бэкенда → сторонних API

Содержание

  1. Архитектура системы
  2. Ключевой принцип: сторонние API только там где нужны
  3. Flow 1: Аутентификация
  4. Flow 2: Обновление токена
  5. Flow 3: Профиль пользователя
  6. Flow 4: Рекомендации рецептов
  7. Flow 5: Сохранённые рецепты
  8. Flow 6: Управление продуктами (Итерация 2)
  9. Flow 7: Распознавание продуктов (Итерация 3)
  10. Flow 8: Планирование меню (Итерация 4)
  11. Анализ потребления сторонних API
  12. Количество запросов к бэкенду по сценариям
  13. Сводная таблица

1. Архитектура системы

┌────────────────────────────────────────────────────────────────────┐
│                      Flutter Client                                │
│               (Android / iOS / Web)                                │
│   - Firebase Auth (Google / Apple / Email)                        │
│   - Dio HTTP Client + Auth Interceptor                            │
│   - FlutterSecureStorage (токены)                                 │
│   - Riverpod (состояние)                                          │
└────────────────────────┬───────────────────────────────────────────┘
                         │ HTTPS, Bearer JWT
                         │
┌────────────────────────▼───────────────────────────────────────────┐
│                      Go Backend (chi v5)                           │
│   - JWT middleware (HS256, верификация без Firebase в рантайме)   │
│   - Gemini API (генерация рекомендаций рецептов)                  │
│   - Pexels API (подбор фотографий к рецептам)                     │
│   - Firebase Admin SDK (только при логине)                        │
│   - Калькулятор КБЖУ (Mifflin-St Jeor, локально)                 │
└──────┬─────────────────────────────────────────────────────────────┘
       │
┌──────▼──────────────────────────────────────────────────────────────┐
│                     PostgreSQL 15                                    │
│  users · saved_recipes · products (Iter.2) · ingredient_mappings   │
└─────────────────────────────────────────────────────────────────────┘

Сторонние API:
  ┌────────────────────┐   ┌─────────────────────────────────────┐
  │   Firebase Auth    │   │   Google Gemini 2.0 Flash           │
  │   только логин     │   │   генерация рецептов-рекомендаций   │
  └────────────────────┘   └─────────────────────────────────────┘
  ┌────────────────────┐
  │   Pexels API       │
  │   фото к рецептам  │
  └────────────────────┘

2. Ключевой принцип

Сторонние API вызываются только при конкретных пользовательских действиях, а не фоновых задачах:

API Когда вызывается Частота
Firebase Auth Только POST /auth/login 1 раз за сессию
Gemini GET /recommendations, POST /ai/recognize-*, POST /ai/generate-menu По запросу пользователя
Pexels Внутри рекомендаций и генерации меню 1 вызов на рецепт

Все токены, профили и сохранённые рецепты хранятся в PostgreSQL и раздаются без внешних вызовов.


3. Flow 1: Аутентификация

3.1 Первый вход (Google OAuth)

Пользователь          Flutter              Firebase           Go Backend         PostgreSQL
     │                   │                    │                    │                  │
     │── тапает "Войти" ─►│                    │                    │                  │
     │                   │── signInWithGoogle()►│                    │                  │
     │                   │◄── idToken ─────────│                    │                  │
     │                   │                    │                    │                  │
     │                   │── POST /auth/login ─────────────────────►│                  │
     │                   │   { firebase_token: idToken }            │                  │
     │                   │                    │◄── VerifyIDToken() ─│                  │
     │                   │                    │──► { uid, email } ──►│                  │
     │                   │                    │                    │── UPSERT users ──►│
     │                   │                    │                    │◄── User{id, plan}─│
     │                   │                    │                    │── генерирует JWT   │
     │                   │                    │                    │── UPDATE refresh ─►│
     │                   │◄── { access_token, refresh_token, user } ─│                  │
     │                   │── сохраняет в SecureStorage             │                  │
     │◄── Home Screen ───│                    │                    │                  │

Запросы на бэкенд: 1 Вызовов Firebase: 1 (VerifyIDToken — серверная верификация) SQL: 2 (UPSERT users + UPDATE refresh_token)

3.2 Последующие запросы (с JWT)

Flutter  ──  GET /profile  ──►  middleware.Auth
                                └── ValidateAccessToken() — локально, HMAC HS256
                                    Firebase НЕ вызывается
                                ──► handler ──► SELECT users WHERE id=$1

Вызовов Firebase: 0 (JWT верифицируется локально по секрету)


4. Flow 2: Обновление токена

Происходит автоматически в AuthInterceptor при 401 или истечении токена (15 мин TTL).

Flutter (AuthInterceptor)        Go Backend               PostgreSQL
         │                            │                        │
         │── POST /auth/refresh ──────►│                        │
         │   { refresh_token }        │── SELECT users WHERE ──►│
         │                            │   refresh_token=$1 AND │
         │                            │   token_expires_at>now()│
         │                            │◄── User ───────────────│
         │                            │── новый JWT + UUID      │
         │                            │── UPDATE users ────────►│
         │◄── { access_token,         │                        │
         │     refresh_token }        │                        │
         │── повторяет исходный запрос│                        │

Запросов к Firebase: 0 SQL: 2 (SELECT + UPDATE)


5. Flow 3: Профиль пользователя

Просмотр

Flutter  →  GET /profile  →  SELECT * FROM users WHERE id=$1

Запросов на бэкенд: 1 | SQL: 1

Обновление (онбординг)

Flutter  →  PUT /profile  →  Go Backend
            { height_cm, weight_kg,  │
              age, gender,           │── Mifflin-St Jeor (локально):
              activity, goal }       │   BMR = 10W + 6.25H - 5A + offset
                                     │   TDEE = BMR × activity_factor
                                     │   Calories = TDEE ± goal_delta
                                     │── UPDATE users SET daily_calories=...

Запросов к сторонним API: 0 (всё считается на Go) Запросов на бэкенд: 1 | SQL: 1


6. Flow 4: Рекомендации рецептов

Центральный flow приложения. Вызывается когда пользователь открывает экран рекомендаций.

6.1 Полный flow

Пользователь  Flutter        Go Backend         Gemini         Pexels        PostgreSQL
     │            │               │                 │              │               │
     │── открывает │               │                 │              │               │
     │  экран ───►│               │                 │              │               │
     │            │── GET /recommendations ─────────►│              │               │
     │            │   ?count=5                       │              │               │
     │            │                                  │              │               │
     │            │               │── SELECT user ──────────────────────────────── ►│
     │            │               │   profile + products (Iter.2) ◄────────────────│
     │            │               │                 │              │               │
     │            │               │── GenerateContent(prompt) ────►│               │
     │            │               │   prompt содержит:             │               │
     │            │               │   - цель пользователя          │               │
     │            │               │   - дневные калории            │               │
     │            │               │   - список продуктов (Iter.2) │               │
     │            │               │   - N=5 рецептов               │               │
     │            │               │◄── JSON: [Recipe×5] ───────────│               │
     │            │               │    каждый с image_query        │               │
     │            │               │                 │              │               │
     │            │               │  для каждого рецепта:          │               │
     │            │               │── GET /v1/search?query=... ─────────────────── ►│
     │            │               │   Authorization: Pexels key    │              │
     │            │               │◄── { photos[0].src.medium } ───────────────────│
     │            │               │                 │              │               │
     │◄── [Recipe×5 c image_url] ─│                 │              │               │

6.2 Структура промпта для Gemini

Ты — диетолог-повар. Предложи {N} рецептов на русском языке.

Профиль пользователя:
- Цель: похудение
- Дневная норма калорий: 1800 ккал
- Ограничения: без глютена (если есть в preferences)

[Итерация 2+] Доступные продукты:
- куриная грудка (500г)
- помидоры (3 шт)
- ...

Требования к каждому рецепту:
- Калорийность на порцию: не более 600 ккал
- Время приготовления: до 40 минут
- Укажи КБЖУ на порцию (приблизительно)

Верни ТОЛЬКО валидный JSON массив без markdown:
[{
  "title": "Название рецепта",
  "description": "Краткое описание (2-3 предложения)",
  "cuisine": "mediterranean",
  "difficulty": "easy|medium|hard",
  "prep_time_min": 10,
  "cook_time_min": 20,
  "servings": 2,
  "image_query": "grilled chicken breast vegetables mediterranean",
  "ingredients": [
    { "name": "Куриная грудка", "amount": 300, "unit": "г" }
  ],
  "steps": [
    { "number": 1, "description": "Нарежьте курицу...", "timer_seconds": null }
  ],
  "tags": ["без глютена", "высокий белок"],
  "nutrition_per_serving": {
    "calories": 420,
    "protein_g": 48,
    "fat_g": 12,
    "carbs_g": 18
  }
}]

6.3 Кэширование рекомендаций

Рекомендации не сохраняются автоматически. При каждом открытии экрана генерируются заново. Это дает:

  • Свежесть контента
  • Учёт изменившихся продуктов (Итерация 2)

Возможная оптимизация в будущем: кэшировать последний набор рекомендаций в Redis/памяти на 30 минут, инвалидировать при обновлении продуктов.


7. Flow 5: Сохранённые рецепты

7.1 Сохранить рекомендацию

Пользователь тапает ❤️ на рецепте

Flutter  →  POST /saved-recipes  →  PostgreSQL
            { полный JSON рецепта }   └── INSERT INTO saved_recipes
                                          (user_id, title, steps,
                                           ingredients, nutrition,
                                           image_url, ...)
                                      ← { id, saved_at }

Запросов на бэкенд: 1 Вызовов Gemini/Pexels: 0 (данные уже есть в клиенте) SQL: 1

7.2 Список сохранённых

Flutter  →  GET /saved-recipes  →  SELECT * FROM saved_recipes
                                   WHERE user_id=$1
                                   ORDER BY saved_at DESC

Запросов на бэкенд: 1 | SQL: 1

7.3 Удалить из сохранённых

Flutter  →  DELETE /saved-recipes/{id}  →  DELETE FROM saved_recipes
                                           WHERE id=$1 AND user_id=$2

Запросов на бэкенд: 1 | SQL: 1


8. Flow 6: Управление продуктами (Итерация 2)

8.1 Открытие списка продуктов

Flutter  →  GET /products  →  SELECT * FROM products
                              WHERE user_id=$1
                              ORDER BY expires_at ASC

Запросов на бэкенд: 1

8.2 Добавление продукта с автодополнением

Пользователь вводит "кур" (debounce 300мс)
     │
Flutter  →  GET /ingredients/search?q=кур  →  PostgreSQL
                                               ├── ILIKE на canonical_name_ru
                                               ├── GIN на aliases
                                               └── pg_trgm similarity

Пользователь выбирает "Куриная грудка"
     │  поля автозаполняются локально

Flutter  →  POST /products  →  INSERT INTO products
            { mapping_id,       expires_at GENERATED ALWAYS AS
              name, quantity,   (added_at + storage_days days)
              unit, category,
              storage_days }

Запросов на бэкенд: 35 (поиск, debounce) + 1 (создание) Вызовов сторонних API: 0

8.3 Связь продуктов с рекомендациями (Итерация 2+)

После того как у пользователя есть продукты, GET /recommendations включает их в промпт для Gemini. Рекомендации становятся персонализированными: "что приготовить из того, что есть".


9. Flow 7: Распознавание продуктов (Итерация 3)

Пользователь фотографирует чек, холодильник или готовое блюдо — Gemini Vision распознаёт содержимое и заполняет список продуктов.

9.1 Распознавание чека

Пользователь  Flutter            Go Backend            Gemini          PostgreSQL
     │            │                   │                    │                │
     │─ фото чека►│                   │                    │                │
     │            │── POST /ai/recognize-receipt ─────────►│                │
     │            │   multipart/form-data: image           │                │
     │            │                   │── GenerateContent ►│                │
     │            │                   │   prompt: OCR чека │                │
     │            │                   │◄── JSON: [{name,   │                │
     │            │                   │    qty, unit,      │                │
     │            │                   │    category,       │                │
     │            │                   │    confidence}]    │                │
     │            │                   │                    │                │
     │            │                   │  fuzzy match по ingredient_mappings►│
     │            │                   │◄── mapping_id ─────────────────────│
     │            │                   │                    │                │
     │◄── [{name, mapping_id, qty,    │                    │                │
     │     unit, storage_days}] ──────│                    │                │
     │            │                   │                    │                │
     │─ подтверждает список ─────────►│                    │                │
     │  POST /products/batch          │── INSERT products ►│                │

Запросов на бэкенд: 2 (recognize + batch insert) Gemini: 1 (vision) SQL: 1 SELECT (fuzzy match) + 1 INSERT batch

9.2 Распознавание фото продуктов (холодильник/стол)

Аналогичен чеку, но без цены. Поддерживает несколько фото:

Пользователь делает 13 фото холодильника
     │
Flutter  →  POST /ai/recognize-products  →  Gemini Vision
            (multipart, несколько фото)      └── анализирует каждое фото
                                             └── объединяет результаты
                                                 (дедупликация по canonical_name)

Backend:
  1. Для каждого фото → 1 Gemini-запрос (параллельно)
  2. Объединение списков, дедупликация (суммирование количества)
  3. Fuzzy match по ingredient_mappings
  4. Возврат клиенту для подтверждения
  5. POST /products/batch → INSERT

Gemini: 13 (по числу фото, параллельно)

9.3 Распознавание блюда (фото → калории)

Пользователь  Flutter            Go Backend            Gemini          PostgreSQL
     │            │                   │                    │                │
     │─ фото блюда►│                  │                    │                │
     │            │── POST /ai/recognize-dish ────────────►│                │
     │            │                   │── GenerateContent ►│                │
     │            │                   │   prompt: распознай│                │
     │            │                   │   блюдо, КБЖУ      │                │
     │            │                   │◄── {dish_name,     │                │
     │            │                   │    weight_g,       │                │
     │            │                   │    calories,       │                │
     │            │                   │    protein, fat,   │                │
     │            │                   │    carbs,          │                │
     │            │                   │    confidence}     │                │
     │            │                   │                    │                │
     │            │                   │  Опционально: поиск│                │
     │            │                   │  в saved_recipes   │                │
     │            │                   │  по dish_name      │                │
     │            │                   │                    │                │
     │◄── {dish, calories, КБЖУ≈,    │                    │                │
     │     matched_recipe?} ──────────│                    │                │
     │            │                   │                    │                │
     │─ добавить в дневник? ─────────►│                    │                │
     │  POST /diary                   │── INSERT meal_diary►│               │

Запросов на бэкенд: 2 (recognize + diary) Gemini: 1 (vision)


10. Flow 8: Планирование меню (Итерация 4)

Пользователь запрашивает меню на неделю — Gemini генерирует полный план питания с рецептами на основе продуктов, профиля и целей пользователя.

10.1 Генерация меню

Пользователь  Flutter            Go Backend            Gemini         Pexels        PostgreSQL
     │            │                   │                    │              │               │
     │─ «Составить │                   │                    │              │               │
     │   меню» ──►│                   │                    │              │               │
     │            │── POST /ai/generate-menu ─────────────►│              │               │
     │            │   { period: "week",                    │              │               │
     │            │     meals_per_day: 3 }                 │              │               │
     │            │                   │                    │              │               │
     │            │               SELECT user profile + products ────────────────────────►│
     │            │               ◄── {goal, КБЖУ, products list} ───────────────────────│
     │            │                   │                    │              │               │
     │            │                   │── GenerateContent ►│              │               │
     │            │                   │   промпт:          │              │               │
     │            │                   │   - профиль юзера  │              │               │
     │            │                   │   - продукты       │              │               │
     │            │                   │   - период 7 дней  │              │               │
     │            │                   │   - 3 приёма/день  │              │               │
     │            │                   │◄── JSON: 21 recipe │              │               │
     │            │                   │    каждый с        │              │               │
     │            │                   │    image_query     │              │               │
     │            │                   │                    │              │               │
     │            │                   │  для каждого рецепта (параллельно):              │
     │            │                   │── GET /v1/search?query=... ──────────────────────►│
     │            │                   │◄── photo_url ────────────────────────────────────│
     │            │                   │                    │              │               │
     │            │                   │── INSERT menu_plans + menu_items ───────────────►│
     │            │                   │── INSERT saved_recipes (рецепты меню) ──────────►│
     │            │                   │                    │              │               │
     │◄── {menu_plan_id,              │                    │              │               │
     │     days: [{day, meals: [      │                    │              │               │
     │       {meal_type, recipe}      │                    │              │               │
     │     ]}]} ──────────────────────│                    │              │               │

Gemini: 1 (большой промпт, ~21 рецепт) Pexels: до 21 (параллельно; на практике повторяющиеся query кешируются) SQL: 1 SELECT + batch INSERT menu_plans/items + batch INSERT saved_recipes

10.2 Просмотр и редактирование меню

Flutter  →  GET /menu?week=2026-W08  →  SELECT menu_plans, menu_items WHERE user_id=$1 AND week_start=$2
                                         LEFT JOIN saved_recipes ON menu_items.recipe_id

Flutter  →  PUT /menu/items/{id}     →  UPDATE menu_items SET recipe_id=$1

Flutter  →  DELETE /menu/items/{id}  →  DELETE menu_items WHERE id=$1 AND user_id=$2

Запросов к бэкенду: 13 | Gemini: 0 | SQL: 12

10.3 Список покупок из меню

Flutter  →  POST /shopping-list/generate  →  Go Backend
            { menu_plan_id }                  ├── SELECT menu_items JOIN saved_recipes
                                              │   WHERE menu_plan_id=$1
                                              ├── Агрегация ингредиентов:
                                              │   суммирование по canonical_name
                                              │   вычитание того, что уже есть в products
                                              └── INSERT/UPDATE shopping_lists

Gemini НЕ участвует. Чистая SQL-агрегация.

Gemini: 0 | SQL: 3


11. Анализ потребления сторонних API

9.1 Firebase Auth

Сценарий Вызовов
Логин 1 (VerifyIDToken)
Обычный запрос с JWT 0
Refresh токена 0

9.2 Gemini (Google Gemini 2.0 Flash)

Тарифы Free tier:

Параметр Значение
RPM 15 (Flash) / 30 (Flash-Lite)
Запросов/день 1 500
Токенов/минуту 1 000 000

Расход на запрос рекомендаций (5 рецептов):

Метрика Значение
Input токены (промпт + продукты) ~500800
Output токены (5 рецептов JSON) ~1 5002 500
Gemini-запросов 1
Стоимость (Flash, платный) ~$0.0003

Дневное потребление при 100 активных пользователях (каждый открывает рекомендации 3 раза/день):

  • 100 × 3 = 300 Gemini-запросов/день
  • Free tier: 1 500/день → хватает на ~5× текущую нагрузку
  • Платный Flash: ~$0.09/день = ~$2.7/мес

9.3 Pexels API

Тарифы:

Тариф Запросов/час Запросов/мес
Free 200 20 000

Расход:

  • 1 рекомендация = 5 рецептов = 5 Pexels-запросов
  • 100 пользователей × 3 рекомендации = 300 запросов/час пик
  • ⚠️ 200 req/hour лимит может стать узким местом при пиковой нагрузке

Стратегия: кэшировать image_url в saved_recipes, для несохранённых рекомендаций — запрашивать при генерации. При росте нагрузки — кэшировать по image_query в Redis (большинство запросов повторяются: "grilled chicken", "pasta carbonara", etc.).


12. Количество запросов к бэкенду по сценариям

Первый запуск (новый пользователь)

Шаг Endpoint Сторонний API
Вход через Google POST /auth/login Firebase (1×)
Загрузка профиля GET /profile
Онбординг PUT /profile
Первые рекомендации GET /recommendations Gemini (1×) + Pexels (5×)
Итого 4 Firebase×1, Gemini×1, Pexels×5

Обычная сессия

Шаг Endpoint Кол-во
Refresh токена (если истёк) POST /auth/refresh 01
Открыть рекомендации GET /recommendations 1
Сохранить рецепт POST /saved-recipes 1
Открыть сохранённые GET /saved-recipes 1
Итого 34 Gemini×1, Pexels×5

Сценарий: пользователь не взаимодействует с рекомендациями

Открывает приложение → просматривает сохранённые рецепты

Запросов: 1 GET /saved-recipes
Сторонних API: 0
SQL: 1

Детальный breakdown: GET /recommendations

1. SELECT users WHERE id=$1                        → 1 SQL
2. [Iter.2+] SELECT products WHERE user_id=$1      → 1 SQL
3. Gemini.GenerateContent(prompt)                  → 1 Gemini req (~13 сек)
4. Pexels.Search(image_query) × 5 (параллельно)   → 5 Pexels req (параллельно)
5. Формирование ответа и отдача                    → 0 SQL

Итого: 12 SQL + 1 Gemini + 5 Pexels
Время ответа: ~24 секунды (доминирует Gemini latency)

13. Сводная таблица

Сторонние API в рантайме

API Trigger Вызовов на запрос Free tier (день)
Firebase Auth POST /auth/login 1 Без ограничений
Gemini Flash GET /recommendations 1 1 500
Gemini Flash POST /ai/recognize-receipt 1
Gemini Flash POST /ai/recognize-products 13 (фото)
Gemini Flash POST /ai/recognize-dish 1
Gemini Flash POST /ai/generate-menu 1
Pexels GET /recommendations 5 (параллельно) ~667 рекомендаций
Pexels POST /ai/generate-menu до 21 (параллельно)

Запросы к бэкенду

Сценарий Бэкенд Firebase Gemini Pexels
Первый вход 1 1 0 0
Просмотр профиля 1 0 0 0
Обновление профиля 1 0 0 0
Рекомендации 1 0 1 5
Сохранить рецепт 1 0 0 0
Список сохранённых 1 0 0 0
Удалить из сохранённых 1 0 0 0
Refresh токена 1 0 0 0
Распознавание чека 2 0 1 0
Распознавание фото продуктов 2 0 13 0
Распознавание блюда 2 0 1 0
Генерация меню (неделя) 1 0 1 до 21
Просмотр меню 1 0 0 0
Список покупок из меню 1 0 0 0

Ключевые выводы

  1. Критические пути требуют Gemini + Pexels: рекомендации (24 сек), распознавание продуктов (13 сек), генерация меню (510 сек). Во всех случаях нужна skeleton-загрузка в UI.

  2. Pexels — потенциальный bottleneck при масштабировании (200 req/hour). Особенно при генерации меню (до 21 вызова). Решается кэшированием image_url по query-строке в Redis.

  3. Всё остальное работает без внешних зависимостей — отказ Gemini/Pexels не роняет авторизацию, профиль, сохранённые рецепты, меню (просмотр/редактирование).

  4. КБЖУ приблизительные — Gemini генерирует оценочные значения. Для MVP этого достаточно; точные данные требуют интеграции с верифицированной базой (USDA FoodData Central, см. TODO.md).

  5. Gemini Free tier (1 500 req/day): распознавание продуктов (3 AI-операции) + рекомендации (1) + меню (1) = ~5 Gemini-запросов на активного пользователя. Free tier хватает на 300 DAU.