Метод PUT /api/v1/recipes/custom/{id}

Документация API: Полная перезапись авторского рецепта (Update) с каскадным замещением строк технологической карты

Published

July 2, 2026

1 Функциональное назначение

Метод предназначен для полного обновления (перезаписи) существующего авторского рецепта пользователем в рамках своей группы (home_group_id).

Метод реализует паттерн полного замещения состояния (Idempotent Replace) и решает следующие задачи: 1. Каскадное обновление структуры (Связь 1:M): Бэкенд атомарно заменяет старое название, описание, массив текстовых шагов приготовления и полностью вычищает старую матрицу ингредиентов, записывая вместо неё новый массив продуктов. 2. Защита от сиротских записей (Orphan Removal): Гарантирует, что старые строки ингредиентов, не упомянутые в новом запросе, будут физически удалены из дочерней таблицы user_recipe_ingredients, предотвращая загрязнение базы данных. 3. Инвалидация распределенного кэша ИИ: После успешного коммита изменений генерирует событие в Apache Kafka для обновления графов данных на ИИ-сервере аналитики КБЖУ и перестройки NLP-эмбеддингов.

2 Протокол взаимодействия (HTTP Контракт)

  • Метод: PUT
  • Маршрут: /api/v1/recipes/custom/{id}
  • Формат данных: application/json

2.1 Спецификация параметров пути (Path Parameters)

Параметр Тип Обязательный Описание Пример значения
id Integer Да Уникальный ID перезаписываемого кастомного рецепта пользователя 412

2.2 Спецификация заголовков (HTTP Headers)

Заголовок Обязательный Описание Пример значения
Content-Type Да Указывает на передачу строго типизированного JSON-пакета application/json
Authorization Да Токен авторизации (Access Token) для проверки прав владения кулинарной книгой Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID Да Сквозной ID запроса для связывания логов операции обновления req-rec-put-88bb

2.3 Спецификация тела запроса (Request Body)

При использовании метода PUT клиент обязан передать всю структуру объекта целиком. Если какое-то поле или ингредиент не будут переданы в JSON, система расценит это как команду на их удаление.

Поле Тип Обязательный Описание Пример значения
title String Да Новое полное название рецепта "Бабушкин яблочный пирог (Праздничный)"
cooking_steps Array Да Полный новый массив текстовых шагов приготовления ["Шаг 1...", "Шаг 2..."]
ingredients Array Да Полный новый список канонических продуктов и порций [..._name: "Яблоки", amount: 0.7]

2.3.1 Пример сырого JSON-запроса (Payload):

{
  "title": "Бабушкин яблочный пирог (Праздничный)",
  "description": "Обновленный рецепт с добавлением корицы",
  "cooking_steps": [
    "Шаг 1: Мелко нарезать яблоки, смешать с сахаром и добавить 1 ч.л. корицы.",
    "Шаг 2: Выложить тесто в форму, засыпать начинку и выпекать 45 минут при 180 градусах."
  ],
  "ingredients": [
    {
      "ingredient_name": "Яблоки",
      "amount": 0.7000,
      "unit": "кг"
    },
    {
      "ingredient_name": "Мука пшеничная",
      "amount": 0.3000,
      "unit": "кг"
    },
    {
      "ingredient_name": "Корица молотая",
      "amount": 1.0000,
      "unit": "шт"
    }
  ]
}

3 Схема обработки запроса пользователя (Mermaid)

На диаграмме представлена логика полной перезаписи (PUT) кастомного рецепта. СУБД внутри атомарной транзакции проверяет права доступа текущей группы, обновляет шапку рецепта и текстовый массив шагов, полностью вычищает старую технологическую карту ингредиентов (DELETE) и вставляет новые позиции (INSERT).

sequenceDiagram
    autonumber
    actor User as Пользователь (Экран Редактирования)
    participant APP as Мобильное приложение (Flutter)
    participant RS as Сервис Рецептов (FastAPI)
    participant DB as База Данных (PostgreSQL)
    participant KAFKA as Брокер событий (Kafka)

    User->>APP: Перезаписывает состав рецепта и жмет "Сохранить изменения"
    APP->>RS: HTTP PUT /api/v1/recipes/custom/412 (Полный JSON-пакет)
    activate RS
    
    note over RS: Шаг 3: Декодирование JWT (Извлечение home_group_id)
    note over RS: Шаг 4: Валидация лимита элементов (макс. 30 позиций)
    
    Note over RS, DB: Старт атомарной SQL-транзакции
    RS->>DB: SELECT home_group_id FROM user_custom_recipes WHERE id = 412 FOR UPDATE
    activate DB
    DB-->>RS: Возвращает home_group_id существующего рецепта
    deactivate DB
    
    note over RS: Шаг 6: Проверка соответствия home_group_id группы
    
    RS->>DB: UPDATE user_custom_recipes SET title = \$1, cooking_steps = \$2 WHERE id = 412
    RS->>DB: DELETE FROM user_recipe_ingredients WHERE recipe_id = 412 (Очистка ингредиентов)
    
    loop По каждому новому продукту из массива ingredients
        RS->>DB: INSERT INTO user_recipe_ingredients (recipe_id, ingredient_name, ...)
    end
    
    Note over RS, DB: Коммит транзакции (COMMIT)
    
    RS->>KAFKA: send_to_kafka_async("masterdata.recipes.mutated.v1")
    RS-->>APP: HTTP 200 OK (status: "REPLACED")
    deactivate RS
    
    note over APP: Кулинарная книга обновляется, старый состав полностью заменяется праздничной версией с корицей

4 Расшифровка шагов

  • Шаг 1 (User -> APP): Пользователь в интерфейсе кулинарной книги полностью переписывает рецепт (например, заменяет базовый «Бабушкин яблочный пирог» на «Бабушкин яблочный пирог (Праздничный)», добавляя корицу и изменяя шаги выпекания) и нажимает кнопку «Сохранить изменения».
  • Шаг 2 (APP -> RS): Мобильное приложение отправляет запрос HTTP PUT /api/v1/recipes/custom/{id}. В теле запроса передается монолитный JSON-пакет, содержащий новое название, описание, текстовый массив шагов и полную новую матрицу ингредиентов. В заголовках прокидывается Access Token и X-Request-ID.
  • Шаг 3 (RS -> RS): Микросервис Рецептов (FastAPI) декодирует JWT-токен и извлекает параметр home_group_id для проверки уровня доступа к приватной кулинарной книге.
  • Шаг 4 (RS -> RS): Валидация бизнес-лимитов: Срабатывает проверка длины нового входящего массива ingredients. Если технологическая карта содержит более 30 позиций, бэкенд отсекает запрос с ошибкой HTTP 400 Bad Request, защищая СУБД от избыточной нагрузки.
  • Шаг 5 (RS -> DB): Сервис Рецептов открывает атомарную SQL-транзакцию в PostgreSQL и блокирует строку шаблона: SELECT home_group_id FROM user_custom_recipes WHERE id = $1 FOR UPDATE. Это защищает запись от параллельного удаления.
  • Шаг 6 (RS -> RS): Проверка контекста безопасности (Multi-Tenancy): Сервис сверяет home_group_id из базы данных с home_group_id, зашитым в токене пользователя. Если идентификаторы не совпадают (попытка перезаписать рецепт чужой семьи), транзакция откатывается (ROLLBACK) со статусом HTTP 403 Forbidden.
  • Шаг 7 (RS -> DB): Обновление шапки и шагов: Сервис выполняет перезапись родительской структуры, включая массив строк cooking_steps: UPDATE user_custom_recipes SET title = $1, description = $2, cooking_steps = $3, updated_at = NOW() WHERE id = $4.
  • Шаг 8 (RS -> DB): *Полная очистка старой карты (Orphan Removal):** Чтобы предотвратить появление сиротских строк ингредиентов, которые были в старой версии, но отсутствуют в новом запросе, СУБД выполняет команду: DELETE FROM user_recipe_ingredients WHERE recipe_id = $1. Старая матрица продуктов полностью вычищается.
  • Шаг 9 (RS -> DB): Запись нового состояния (Bulk Insert): Бэкенд итерирует новый входящий массив ingredients и для каждого продукта выполняет команду вставки: INSERT INTO user_recipe_ingredients (recipe_id, ingredient_name, amount, unit) VALUES ($1, $2, $3, $4).
  • Шаг 10 (RS -> API): После успешного завершения всех вставок транзакция фиксируется в базе данных командой COMMIT. Новое состояние технологической карты становится единственно верным.
  • Шаг 11 (RS -> KAFKA): Сервис Рецептов отправляет асинхронное бизнес-событие в топик Kafka masterdata.recipes.mutated.v1 с типом "RECIPE_REPLACED". Это дает сигнал ИИ-сервису аналитики полностью сбросить старый кэш структуры этого рецепта и перестроить NLP-эмбеддинги под праздничную версию с корицей.
  • Шаг 12 (RS -> APP): Сервер возвращает мобильному приложению статус HTTP 200 OK. Во Flutter-приложении кулинарная книга обновляется, старый состав полностью заменяется новым.

5 Спецификация ответов сервера (Response Body) и ошибок

5.1 1. Успешный ответ (Success Response)

5.1.1 HTTP 200 OK (Ответ на Шаге 12)

Возвращается мобильному приложению после успешного коммита транзакции полной перезаписи. В теле ответа фиксируются новые метаданные и статус замещения технологической карты рецепта.

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "RECIPE_FULLY_REPLACED",
  "data": {
    "recipe_id": 412,
    "home_group_id": "group-772-alpha",
    "new_recipe_title": "Бабушкин яблочный пирог (Праздничный)",
    "inserted_ingredients_count": 3,
    "timestamp": "2026-07-02T01:05:00Z"
  }
}

5.2 2. Вилки исключений и обработка ошибок

5.2.1 Ошибка: Рецепт не найден при попытке перезаписи (HTTP 404 Not Found — Шаг 5)

Выбрасывается на Шаге 5, если переданный в URL id кастомного рецепта полностью отсутствует в таблице user_custom_recipes. Метод PUT не создает новую запись, если целевой ID не существует.

{
  "error_code": "ERR-RECIPE-PUT-TARGET-NOT-FOUND",
  "message": "Целевой рецепт для перезаписи не найден в системе.",
  "details": {
    "requested_id": 412,
    "reason": "Select for update returned 0 rows. Entity cannot be replaced."
  }
}

5.2.2 Ошибка: Превышение максимального лимита ингредиентов (HTTP 400 Bad Request — Шаг 4)

Выбрасывается на Шаге 4, если новый массив ingredients содержит более 30 элементов. Архитектурный лимит защищает систему от перегрузки на этапе каскадного удаления и повторного Bulk Insert.

{
  "error_code": "ERR-RECIPE-INGREDIENTS-LIMIT-EXCEEDED",
  "message": "Превышен лимит позиций. Новая технологическая карта рецепта не может содержать более 30 ингредиентов.",
  "details": {
    "current_ingredients_count": 35,
    "max_allowed_limit": 30
  }
}

5.2.3 Ошибка: Попытка перезаписи системного рецепта (HTTP 403 Forbidden — Шаг 6)

Выбрасывается, если пользователь пытается принудительно отправить PUT-запрос на ID, принадлежащий таблице глобальных общесистемных рецептов (master_recipes), которые доступны только для чтения.

{
  "error_code": "ERR-SYSTEM-RECIPE-IMMUTABLE",
  "message": "Операция отклонена: глобальные рецепты экосистемы защищены от изменений.",
  "details": {
    "requested_id": 12,
    "action": "Ensure UI routing isolates personal recipes from system master-data."
  }
}