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: Кулинарная книга обновляется, старый состав полностью заменяется праздничной версией с корицей
Метод PUT /api/v1/recipes/custom/{id}
Документация API: Полная перезапись авторского рецепта (Update) с каскадным замещением строк технологической карты
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).
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): Сервис Рецептов отправляет асинхронное бизнес-событие в топик Kafkamasterdata.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."
}
}