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>
37 KiB
Flow: взаимодействие пользователя → бэкенда → сторонних API
Содержание
- Архитектура системы
- Ключевой принцип: сторонние API только там где нужны
- Flow 1: Аутентификация
- Flow 2: Обновление токена
- Flow 3: Профиль пользователя
- Flow 4: Рекомендации рецептов
- Flow 5: Сохранённые рецепты
- Flow 6: Управление продуктами (Итерация 2)
- Flow 7: Распознавание продуктов (Итерация 3)
- Flow 8: Планирование меню (Итерация 4)
- Анализ потребления сторонних API
- Количество запросов к бэкенду по сценариям
- Сводная таблица
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 }
Запросов на бэкенд: 3–5 (поиск, 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 Распознавание фото продуктов (холодильник/стол)
Аналогичен чеку, но без цены. Поддерживает несколько фото:
Пользователь делает 1–3 фото холодильника
│
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: 1–3 (по числу фото, параллельно)
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
Запросов к бэкенду: 1–3 | Gemini: 0 | SQL: 1–2
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 токены (промпт + продукты) | ~500–800 |
| Output токены (5 рецептов JSON) | ~1 500–2 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 | 0–1 |
| Открыть рекомендации | GET /recommendations | 1 |
| Сохранить рецепт | POST /saved-recipes | 1 |
| Открыть сохранённые | GET /saved-recipes | 1 |
| Итого | 3–4 | 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 (~1–3 сек)
4. Pexels.Search(image_query) × 5 (параллельно) → 5 Pexels req (параллельно)
5. Формирование ответа и отдача → 0 SQL
Итого: 1–2 SQL + 1 Gemini + 5 Pexels
Время ответа: ~2–4 секунды (доминирует 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 | 1–3 (фото) | — |
| 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 | 1–3 | 0 |
| Распознавание блюда | 2 | 0 | 1 | 0 |
| Генерация меню (неделя) | 1 | 0 | 1 | до 21 |
| Просмотр меню | 1 | 0 | 0 | 0 |
| Список покупок из меню | 1 | 0 | 0 | 0 |
Ключевые выводы
-
Критические пути требуют Gemini + Pexels: рекомендации (2–4 сек), распознавание продуктов (1–3 сек), генерация меню (5–10 сек). Во всех случаях нужна skeleton-загрузка в UI.
-
Pexels — потенциальный bottleneck при масштабировании (200 req/hour). Особенно при генерации меню (до 21 вызова). Решается кэшированием image_url по query-строке в Redis.
-
Всё остальное работает без внешних зависимостей — отказ Gemini/Pexels не роняет авторизацию, профиль, сохранённые рецепты, меню (просмотр/редактирование).
-
КБЖУ приблизительные — Gemini генерирует оценочные значения. Для MVP этого достаточно; точные данные требуют интеграции с верифицированной базой (USDA FoodData Central, см. TODO.md).
-
Gemini Free tier (1 500 req/day): распознавание продуктов (3 AI-операции) + рекомендации (1) + меню (1) = ~5 Gemini-запросов на активного пользователя. Free tier хватает на 300 DAU.