# Flow: взаимодействие пользователя → бэкенда → сторонних API ## Содержание 1. [Архитектура системы](#1-архитектура-системы) 2. [Ключевой принцип: сторонние API только там где нужны](#2-ключевой-принцип) 3. [Flow 1: Аутентификация](#3-flow-1-аутентификация) 4. [Flow 2: Обновление токена](#4-flow-2-обновление-токена) 5. [Flow 3: Профиль пользователя](#5-flow-3-профиль-пользователя) 6. [Flow 4: Рекомендации рецептов](#6-flow-4-рекомендации-рецептов) 7. [Flow 5: Сохранённые рецепты](#7-flow-5-сохранённые-рецепты) 8. [Flow 6: Управление продуктами (Итерация 2)](#8-flow-6-управление-продуктами-итерация-2) 9. [Flow 7: Распознавание продуктов (Итерация 3)](#9-flow-7-распознавание-продуктов-итерация-3) 10. [Flow 8: Планирование меню (Итерация 4)](#10-flow-8-планирование-меню-итерация-4) 11. [Анализ потребления сторонних API](#11-анализ-потребления-сторонних-api) 12. [Количество запросов к бэкенду по сценариям](#12-количество-запросов-к-бэкенду-по-сценариям) 13. [Сводная таблица](#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 } ``` **Запросов на бэкенд:** 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 | ### Ключевые выводы 1. **Критические пути требуют Gemini + Pexels:** рекомендации (2–4 сек), распознавание продуктов (1–3 сек), генерация меню (5–10 сек). Во всех случаях нужна 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.