Files
food-ai/docs/plans/Iteration_4.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

14 KiB
Raw Blame History

Итерация 4: Планирование меню

Цель: пользователь запрашивает меню на неделю — Gemini генерирует полный план питания (21 приём пищи) с учётом продуктов, целей и предпочтений. Из меню автоматически формируется список покупок.

Зависимости: Итерация 2 (продукты), Итерация 1 (сохранённые рецепты).


Структура задач

4.1 Backend: таблицы меню
 ├── 4.1.1 Миграция: menu_plans, menu_items
 └── 4.1.2 Миграция: meal_diary

4.2 Backend: генерация меню
 ├── 4.2.1 POST /ai/generate-menu
 ├── 4.2.2 Промпт для Gemini (7 дней × 3 приёма)
 ├── 4.2.3 Параллельный Pexels для 21 рецепта
 └── 4.2.4 Сохранение в menu_plans + menu_items + saved_recipes

4.3 Backend: CRUD меню
 ├── 4.3.1 GET /menu?week=YYYY-WNN
 ├── 4.3.2 PUT /menu/items/{id} (заменить рецепт)
 └── 4.3.3 DELETE /menu/items/{id}

4.4 Backend: список покупок
 ├── 4.4.1 POST /shopping-list/generate (из меню)
 ├── 4.4.2 GET /shopping-list
 └── 4.4.3 PATCH /shopping-list/items/{index}/check

4.5 Backend: дневник питания
 ├── 4.5.1 GET /diary?date=YYYY-MM-DD
 ├── 4.5.2 POST /diary (добавить запись)
 └── 4.5.3 DELETE /diary/{id}

4.6 Flutter: экран меню
 ├── 4.6.1 MenuScreen (7-дневный вид)
 ├── 4.6.2 Кнопка «Сгенерировать меню»
 ├── 4.6.3 Редактирование слота (смена рецепта)
 └── 4.6.4 Skeleton на время генерации (510 сек)

4.7 Flutter: список покупок
 └── 4.7.1 ShoppingListScreen (с галочками)

4.8 Flutter: дневник питания
 ├── 4.8.1 DiaryScreen (записи за день)
 └── 4.8.2 Добавление записи (из меню / вручную)

4.1 Миграции

menu_plans и menu_items

-- migrations/005_create_menu_plans.sql

-- +goose Up
CREATE TABLE menu_plans (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id         UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    week_start      DATE NOT NULL,  -- понедельник недели
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    UNIQUE(user_id, week_start)
);

CREATE TABLE menu_items (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    menu_plan_id    UUID NOT NULL REFERENCES menu_plans(id) ON DELETE CASCADE,
    day_of_week     INT NOT NULL CHECK (day_of_week BETWEEN 1 AND 7),
    meal_type       TEXT NOT NULL CHECK (meal_type IN ('breakfast','lunch','dinner')),
    recipe_id       UUID REFERENCES saved_recipes(id) ON DELETE SET NULL,
    recipe_data     JSONB,  -- snapshot рецепта (если saved_recipe удалён)
    UNIQUE(menu_plan_id, day_of_week, meal_type)
);

-- +goose Down
DROP TABLE menu_items;
DROP TABLE menu_plans;

meal_diary

-- migrations/006_create_meal_diary.sql

-- +goose Up
CREATE TABLE meal_diary (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id     UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    date        DATE NOT NULL,
    meal_type   TEXT NOT NULL,
    name        TEXT NOT NULL,
    portions    DECIMAL(5,2) NOT NULL DEFAULT 1,
    calories    DECIMAL(8,2),
    protein_g   DECIMAL(8,2),
    fat_g       DECIMAL(8,2),
    carbs_g     DECIMAL(8,2),
    source      TEXT NOT NULL DEFAULT 'manual',  -- manual|recipe|menu|photo
    recipe_id   UUID REFERENCES saved_recipes(id) ON DELETE SET NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_meal_diary_user_date ON meal_diary(user_id, date);

-- +goose Down
DROP TABLE meal_diary;

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

4.2.1 Промпт для Gemini

Ты — диетолог-повар. Составь меню на 7 дней для пользователя.

Профиль:
- Цель: {goal_ru}
- Дневная норма калорий: {calories} ккал
- Ограничения: {restrictions или "нет"}
- Предпочтения кухни: {cuisines или "любые"}

Продукты в наличии (приоритет — скоро истекают ⚠):
{products_list}

Требования:
- 3 приёма пищи в день: завтрак (25% КБЖУ), обед (40%), ужин (35%)
- Разнообразие: не повторять рецепты
- По возможности использовать имеющиеся продукты
- КБЖУ рассчитать на 1 порцию (приблизительно)

Верни ТОЛЬКО валидный JSON без markdown:
{
  "days": [
    {
      "day": 1,
      "meals": [
        {
          "meal_type": "breakfast",
          "recipe": {
            "title": "Овсяная каша с яблоком",
            "description": "...",
            "cuisine": "european",
            "difficulty": "easy",
            "prep_time_min": 5,
            "cook_time_min": 10,
            "servings": 1,
            "image_query": "oatmeal apple breakfast bowl",
            "ingredients": [...],
            "steps": [...],
            "tags": [...],
            "nutrition_per_serving": {
              "calories": 320, "protein_g": 8,
              "fat_g": 6, "carbs_g": 58
            }
          }
        },
        { "meal_type": "lunch", "recipe": {...} },
        { "meal_type": "dinner", "recipe": {...} }
      ]
    },
    ...
  ]
}

4.2.2 Параллельный Pexels

21 Pexels-запрос выполняется параллельно (горутины). С учётом лимита 200 req/hour — для одного меню это 10% дневного бюджета. Повторяющиеся image_query (между пользователями) следует кэшировать в будущем (Redis, см. TODO.md).

4.2.3 Сохранение

// Транзакция:
// 1. INSERT menu_plans (upsert по user_id + week_start)
// 2. INSERT saved_recipes для каждого из 21 рецептов
// 3. INSERT menu_items (21 записи с recipe_id → saved_recipes)

4.3 CRUD меню

GET /menu?week=2026-W08

{
  "id": "uuid",
  "week_start": "2026-02-16",
  "days": [
    {
      "day": 1,
      "date": "2026-02-16",
      "meals": [
        {
          "id": "menu_item_uuid",
          "meal_type": "breakfast",
          "recipe": {
            "id": "saved_recipe_uuid",
            "title": "Овсяная каша с яблоком",
            "image_url": "...",
            "calories": 320,
            "nutrition_per_serving": {...}
          }
        }
      ],
      "total_calories": 1780
    }
  ]
}

PUT /menu/items/{id}

Заменить рецепт в слоте меню. Тело: { "recipe_id": "uuid" } — ID существующего сохранённого рецепта, или запрос нового от Gemini.


4.4 Список покупок

POST /shopping-list/generate

// 1. SELECT menu_items JOIN saved_recipes WHERE menu_plan_id=$1
// 2. Извлечь все ингредиенты из всех рецептов
// 3. Агрегация по canonical_name (через ingredient_mappings):
//    суммирование количества для одинаковых ингредиентов
// 4. Вычесть уже имеющееся в products (где quantity > 0)
// 5. INSERT/UPSERT shopping_lists

Gemini не участвует — чистая SQL-агрегация.

GET /shopping-list

[
  {
    "name": "Куриная грудка",
    "category": "meat",
    "amount": 1200,
    "unit": "г",
    "checked": false,
    "in_stock": 0
  },
  {
    "name": "Яйца",
    "category": "dairy",
    "amount": 12,
    "unit": "шт",
    "checked": false,
    "in_stock": 4
  }
]

4.6 Flutter: экран меню

MenuScreen

┌─────────────────────────────────────┐
│ Меню               [← Пред] [След →]│
│ Неделя 1622 февраля                │
├─────────────────────────────────────┤
│                                     │
│ Понедельник, 16 фев    1 780 ккал   │
│ ┌──────────────────────────────┐    │
│ │ 🌅 Завтрак   ≈320 ккал       │    │
│ │ [фото] Овсянка с яблоком    │    │
│ │                    [Изменить]│    │
│ ├──────────────────────────────┤    │
│ │ ☀  Обед      ≈680 ккал       │    │
│ │ [фото] Куриный суп          │    │
│ │                    [Изменить]│    │
│ ├──────────────────────────────┤    │
│ │ 🌙 Ужин      ≈780 ккал       │    │
│ │ [фото] Рис с овощами        │    │
│ │                    [Изменить]│    │
│ └──────────────────────────────┘    │
│                                     │
│ Вторник, 17 фев       1 820 ккал   │
│ ┌──────────────────────────────┐    │
│ │  ...                         │    │
│ └──────────────────────────────┘    │
│                                     │
│  ┌─────────────────────────────┐    │
│  │ ✨ Сгенерировать новое меню │    │
│  └─────────────────────────────┘    │
│  ┌─────────────────────────────┐    │
│  │ 🛒 Список покупок           │    │
│  └─────────────────────────────┘    │
│                                     │
├─────────────────────────────────────┤
│ [Главная] [Продукты] [Меню●] [Рецепты] [Профиль] │
└─────────────────────────────────────┘

Skeleton при генерации (510 сек)

┌─────────────────────────────────────┐
│ Меню                                │
├─────────────────────────────────────┤
│                                     │
│  Составляем меню на неделю...       │
│  Учитываем ваши продукты и цели     │
│                                     │
│  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   │
│  ░░░░░░░░░  ░░░░░░░░░░░░░░░░░░░    │
│  ░░░░░░░░░░░░░░░  ░░░░░░░░░░░░░░   │
│                                     │
│  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   │
│  ░░░░░░░░░  ░░░░░░░░░░░░░░░░░░░    │
│                                     │
└─────────────────────────────────────┘

4.7 Flutter: список покупок

┌─────────────────────────────────────┐
│ [←] Список покупок        [Поделиться]│
├─────────────────────────────────────┤
│                                     │
│  Мясо                               │
│  ☐ Куриная грудка      1.2 кг       │
│  ☐ Фарш говяжий         500 г       │
│                                     │
│  Молочное                           │
│  ☑ Яйца                  12 шт     │
│    (4 шт есть дома)                 │
│  ☐ Молоко                  1 л     │
│                                     │
│  Овощи                              │
│  ☐ Морковь                 3 шт    │
│  ☐ Лук репчатый            4 шт    │
│                                     │
│  ┌───────────────────────────────┐  │
│  │  + Добавить вручную           │  │
│  └───────────────────────────────┘  │
│                                     │
├─────────────────────────────────────┤
│  Осталось купить: 8 позиций         │
└─────────────────────────────────────┘

Оценка нагрузки

Действие Gemini Pexels
Генерация меню (неделя) 1 (большой промпт) до 21
Просмотр/редактирование меню 0 0
Список покупок 0 0
Дневник питания 0 0

Генерация меню — самый тяжёлый запрос по Pexels. Происходит 1 раз в неделю на пользователя. При 100 DAU × 1/7 = ~14 генераций/день × 21 Pexels = 294 Pexels-запроса/день — в пределах лимита 200 req/hour.