Метод POST /api/v1/recipes/custom

Документация API: Создание нового авторского рецепта (Create) с привязкой к домашней кулинарной книге

Published

July 2, 2026

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

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

В отличие от неизменяемых общесистемных рецептов, данный метод решает следующие задачи: 1. Формирование личной кулинарной книги: Регистрирует текстовые шаги приготовления, описание и массив ингредиентов, привязанных к каноническим именам справочников. 2. Multi-Tenancy изоляция данных: Автоматически привязывает создаваемый рецепт к home_group_id текущей семьи или офиса, извлеченному из JWT-токена. Рецепт становится приватным и скрывается от остальных участников платформы. 3. NLP-индексация и кэширование: После коммита транзакции отправляет асинхронное уведомление в Apache Kafka. ИИ-сервис перехватывает этот сигнал и фоном строит векторные эмбеддинги для текстового микросервиса CheckProduct, чтобы система мгновенно распознавала кастомное блюдо при последующем ручном вводе.

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

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

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

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

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

В массиве ingredients пользователь передает канонические имена продуктов из мастер-словарей и их точные пропорции (тип float/double для весовых товаров).

Поле Тип Обязательный Описание Пример значения
title String Да Пользовательское название рецепта "Бабушкин яблочный пирог"
cooking_steps Array Да Пошаговый текстовый алгоритм приготовления ["Замесить тесто", "Выпекать"]
ingredients Array Да Список необходимых продуктов и их объемов [..._name: "Яблоки", amount: 0.5]

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

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

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

На диаграмме представлена логика создания нового кастомного рецепта. Происходит параллельное сохранение метаданных, текстовых шагов и матрицы ингредиентов в PostgreSQL в рамках единой транзакции СУБД с последующей публикацией события в Apache Kafka.

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 POST /api/v1/recipes/custom (JSON-пакет структуры)
    activate RS
    
    note over RS: Шаг 3: Декодирование JWT (Извлечение home_group_id)
    note over RS: Шаг 4: Валидация лимита (макс. 30 ингредиентов на блюдо)
    
    Note over RS, DB: Старт атомарной SQL-транзакции
    
    RS->>DB: INSERT INTO user_custom_recipes (home_group_id, title, cooking_steps)
    activate DB
    DB-->>RS: Возвращает сгенерированный recipe_id
    deactivate DB
    
    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 201 Created (JSON с ID нового рецепта)
    deactivate RS
    
    note over APP: Экран закрывается, кастомный рецепт добавляется в личную книгу пользователя

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

  • Шаг 1 (User -> APP): Пользователь открывает конструктор рецептов во Flutter-приложении, вводит название («Бабушкин яблочный пирог»), добавляет пошаговое текстовое описание процесса выпекания, указывает канонические наименования продуктов, их веса и нажимает «Сохранить рецепт».
  • Шаг 2 (APP -> RS): Мобильное приложение упаковывает данные в монолитное JSON-тело и отправляет запрос HTTP POST /api/v1/recipes/custom. В заголовках передается стандартный Access Token и X-Request-ID.
  • Шаг 3 (RS -> RS): Микросервис Рецептов (FastAPI) декодирует JWT-токен сессии и извлекает параметр home_group_id. Данный метод универсален и доступен как для домашних профилей HOME, так и для корпоративных OFFICE.
  • Шаг 4 (RS -> RS): Бизнес-валидация лимитов: Срабатывает проверка длины массива ingredients. Если пользователь попытался сохранить более 30 ингредиентов в рамках одного блюда, бэкенд отсекает запрос с ошибкой HTTP 400 Bad Request, защищая СУБД от избыточной нагрузки на этапах будущего парсинга и gRPC-сериализации.
  • Шаг 5 (RS -> DB): Сервис Рецептов открывает атомарную SQL-транзакцию в PostgreSQL. Первым действием выполняется вставка базовых метаданных и текстового массива шагов в родительскую таблицу: INSERT INTO user_custom_recipes (home_group_id, title, description, cooking_steps) VALUES ($1, $2, $3, $4) RETURNING id.
  • Шаг 6 (DB -> RS): База данных генерирует последовательный числовой идентификатор recipe_id и возвращает его на бэкенд.
  • Шаг 7 (RS -> DB): Запускается цикл формирования технологической карты ингредиентов блюда (связь 1:M). Бэкенд итерирует массив ingredients и для каждого продукта выполняет DML-команду множественной вставки: INSERT INTO user_recipe_ingredients (recipe_id, ingredient_name, amount, unit) VALUES ($1, $2, $3, $4).
  • Шаг 8 (RS -> API): После успешной записи всех дочерних строк транзакция фиксируется в базе данных командой COMMIT. Рецепт становится доступен для использования.
  • Шаг 9 (RS -> KAFKA): Сервис Рецептов выступает в роли продюсера (Producer) и отправляет асинхронное системное уведомление в топик Kafka masterdata.recipes.mutated.v1 с типом события "RECIPE_CREATED". Это дает сигнал ИИ-серверу аналитики (MLOps контуру) запустить фоновый пересчет векторных эмбеддингов для текстового микросервиса CheckProduct, чтобы система точнее распознавала кастомное блюдо при последующем ручном вводе.
  • Шаг 10 (RS -> APP): Сервер возвращает мобильному приложению статус HTTP 201 Created с подтверждением генерации сущности. Во Flutter-интерфейсе форма закрывается, а новый рецепт появляется в общем списке доступных для готовки блюд этой семьи или офиса.

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

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

5.1.1 HTTP 201 Created (Ответ на Шаге 10)

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

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

5.2 2. Спецификация структуры таблиц PostgreSQL (Связь 1:M)

Для хранения кастомных рецептов пользователей используется реляционная схема с поддержкой массивов для текстовых шагов приготовления.

-- Таблица 1: Шапки кастомных рецептов пользователей (Родительская структура)
CREATE TABLE user_custom_recipes (
    id SERIAL PRIMARY KEY,
    home_group_id VARCHAR(50) NOT NULL,    -- UUID или строковый ID семьи/офиса
    title VARCHAR(255) NOT NULL,          -- Название («Бабушкин яблочный пирог»)
    description TEXT,                     -- Описание рецепта
    cooking_steps TEXT[] NOT NULL,        -- Массив текстовых шагов приготовления в PostgreSQL
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Таблица 2: Ингредиенты кастомных рецептов (Дочерняя структура)
CREATE TABLE user_recipe_ingredients (
    id SERIAL PRIMARY KEY,
    recipe_id INT NOT NULL REFERENCES user_custom_recipes(id) ON DELETE CASCADE,
    ingredient_name VARCHAR(255) NOT NULL,   -- Каноническое имя продукта из словаря
    amount NUMERIC(10, 4) NOT NULL,          -- Вес или штуки (дробный float/numeric)
    unit VARCHAR(20) NOT NULL                -- "кг", "шт", "л"
);

-- Индекс для изоляции приватных кулинарных книг по группам (Multi-Tenancy)
CREATE INDEX idx_user_recipes_group ON user_custom_recipes(home_group_id);
-- Индекс для мгновенной сборки технологической карты при gRPC вызовах контура Cook
CREATE INDEX idx_user_ingredients_recipe ON user_recipe_ingredients(recipe_id);

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

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

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

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

5.3.2 Ошибка валидации структуры (HTTP 422 Unprocessable Entity)

Выбрасывается на Шаге 3, если Pydantic-модель обнаружила отрицательное количество ингредиентов или пустое название рецепта.

{
  "error_code": "ERR-VALIDATION-FAILED",
  "message": "Переданный JSON-пакет содержит некорректные типы данных или пустые поля.",
  "details": [
    {
      "loc": ["body", "ingredients", 0, "amount"],
      "msg": "Значение количества ингредиента должно быть строго больше 0.0000",
      "type": "value_error.number.not_gt"
    }
  ]
}