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>
This commit is contained in:
644
docs/Flow.md
Normal file
644
docs/Flow.md
Normal file
@@ -0,0 +1,644 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user