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: Экран закрывается, кастомный рецепт добавляется в личную книгу пользователя
Метод POST /api/v1/recipes/custom
Документация API: Создание нового авторского рецепта (Create) с привязкой к домашней кулинарной книге
- INVENTORY-2XX Backlog — Краткое Описание задачи 1.
- INVENTORY-1XX Refinement (Уточнение) — Краткое Описание задачи 2.
1 Функциональное назначение
Метод предназначен для проектирования и первичного сохранения нового авторского (кастомного) рецепта пользователем в семейном или корпоративном контуре.
В отличие от неизменяемых общесистемных рецептов, данный метод решает следующие задачи:
- Формирование личной кулинарной книги: Регистрирует текстовые шаги приготовления, описание и массив ингредиентов, привязанных к каноническим именам справочников.
- Multi-Tenancy изоляция данных: Автоматически привязывает создаваемый рецепт к
home_group_idтекущей семьи или офиса, извлеченному из JWT-токена. Рецепт становится приватным и скрывается от остальных участников платформы. - 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.
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) и отправляет асинхронное системное уведомление в топик Kafkamasterdata.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"
}
]
}