sequenceDiagram
autonumber
actor User as Пользователь (Экран Поиска)
participant APP as Мобильное приложение (Flutter)
participant API as Бэкенд-шлюз (FastAPI)
participant DB as Реляционная СУБД (PostgreSQL)
User->>APP: Открывает вкладку "Подбор рецептов"
APP->>API: HTTP GET /api/v1/cook/recipes/search (С JWT токеном в Header)
activate API
note over API: Шаг 3: Извлечение home_group_id из JWT-токена
API->>DB: Вызов сложного SQL-запроса (CTE) по home_group_id
activate DB
note over DB: Шаг 5: Сбор уникальных SKU из fridge_inventory (quantity > 0)
note over DB: Шаг 6: Пересечение со словарем рецептов (Совпадение >= 1)
note over DB: Шаг 7: Расчет match_percentage и применение LIMIT 500
DB-->>API: Возвращает плоский массив подходящих рецептов (макс. 500 строк)
deactivate DB
API-->>APP: HTTP 200 OK (JSON с ранжированным списком рецептов)
deactivate API
note over APP: Экран мгновенно отрисовывает карточки: сверху рецепты с максимумом совпадений
Метод GET /api/v1/cook/recipes/search
Документация API: Умный подбор и ранжирование рецептов на основе пересечения остатков холодильника со справочником Master Data
- MDM-MIGRATION-118 Backlog — Таблица хранения инцидентов цензуры и черновиков в СУБД.
- GW-44 Ready for Development — Подключение эндпоинта в шлюзе.
- MDM-010 Ready for Development — создать метод RecipeSearch.
- MDM-011 Ready for Development — создать метод validateAndFetchRecipe.
В открытом доступе представлена демонстрационная версия метода. В настоящей публичной документации отображены не все шаги, технические сценарии и приватные эндпоинты для системы цифровых симуляторов бизнес-процессов.
- Полная спецификация метода: Будет доступна только во внутреннем контуре разработки (Confluence / Swagger Enterprise).
1 Функциональное назначение
Метод предназначен для автоматического подбора кулинарных рецептов, которые семья или сотрудники офиса могут приготовить прямо сейчас, исходя из текущего состава продуктов в холодильнике (home_group_id).
Метод решает три ключевые задачи:
- Полнотекстовое и семантическое пересечение: Сверяет список уникальных канонических имен продуктов, находящихся на балансе пользователя, со списками ингредиентов в Мастер-базе рецептов.
- Гибкий порог вхождения: Возвращает рецепт в поисковую выдачу, если есть совпадение минимум по одному продукту. Рецепты, для которых не хватает части ингредиентов, не отсекаются, а ранжируются ниже.
- Защита от перегрузки (Performance Limit): Устанавливает жесткое ограничение на стороне СУБД — в ответ попадает максимум 500 рецептов, отсортированных по коэффициенту максимального совпадения. Это защищает оперативную память мобильного приложения (Flutter) от переполнения тяжелыми JSON-пакетами.
2 Протокол взаимодействия (HTTP Контракт)
- Метод:
GET - Маршрут:
/api/v1/cook/recipes/search - Формат данных:
application/json(только ответ)
2.1 Спецификация параметров запроса (Query Parameters)
Поскольку метод использует метод GET, параметры фильтрации передаются внутри URL.
| Параметр | Тип | Обязательный | Описание | Пример значения |
|---|---|---|---|---|
limit |
Integer | Нет | Ограничение размера страницы выдачи (Максимум = 500) |
50 |
offset |
Integer | Нет | Смещение для постраничной навигации | 0 |
2.2 Спецификация заголовков (HTTP Headers)
| Заголовок | Обязательный | Описание | Пример значения |
|---|---|---|---|
Authorization |
Да | Токен авторизации (Access Token). Бэкенд извлекает из него home_group_id для получения состава холодильника. |
Bearer eyJhbGciOiJIUzI1Ni... |
X-Request-ID |
Да | Сквозной ID запроса для профилирования скорости тяжелого SQL-запроса | req-rec-search-99aa |
2.3 Спецификация структуры ответа (Response Body)
Метод возвращает массив рецептов. Для каждого рецепта рассчитывается match_percentage (сколько процентов ингредиентов рецепта уже есть в наличии).
| Поле | Тип | Описание | Пример значения |
|---|---|---|---|
recipe_id |
Integer | Уникальный ID рецепта в мастер-базе | 412 |
title |
String | Название блюда | "Яблочный пирог классический" |
match_percentage |
Float | Процент совпадения имеющихся продуктов с рецептом | 0.7500 (75%) |
total_ingredients |
Integer | Всего ингредиентов требуется в рецепте | 4 |
matched_count |
Integer | Сколько ингредиентов из требуемых есть в холодильнике | 3 |
2.3.1 Пример JSON-ответа (Payload):
{
"total_found": 142,
"limit": 500,
"offset": 0,
"recipes": [
{
"recipe_id": 412,
"title": "Яблочный пирог классический",
"match_percentage": 0.75,
"total_ingredients": 4,
"matched_count": 3,
"matched_items": ["Мука", "Масло сливочное 82%", "Апельсины"],
"missing_items": ["Яйцо куриное"]
}
]
}3 Схема обработки запроса пользователя (Mermaid)
На диаграмме представлен асинхронный процесс извлечения текущих остатков холодильника, сопоставления их со справочником рецептов на уровне СУБД с помощью агрегатных функций и выдачи упорядоченного списка во Flutter с жестким лимитом.
4 Расшифровка шагов
| Шаг | Действие | Параметры / Запросы / DTO | Ошибки (Исключения / Статусы) |
|---|---|---|---|
Шаг 1 (APP -> Nginx) |
Клиент инициирует поиск рецептов в MDM-базе по конкретному выбранному продукту. | HTTP GET /api/v1/recipes/search?product_id=XYZHeaders: Authorization: Bearer <JWT>, Query: product_id="uuid-v4-string" |
DioException: send timeoutHTTP 400 Bad Request (передан некорректный текстовый формат вместо UUID) |
Шаг 2 (Nginx -> GW) |
Прокси-сервер выполняет базовую маршрутизацию и генерирует сквозной трассировочный идентификатор сессии. | HTTP GET (Внутреннее проксирование на FastAPI Gateway)Added Headers: X-Request-ID: "trace-recipe-search-uuid" |
HTTP 502 Bad Gateway (контейнер шлюза API Gateway недоступен / перегружен) |
Шаг 3 (GW -> Recipes) |
Шлюз валидирует типы данных через Pydantic-схему RecipeSearchQueryValidator и перенаправляет запрос в мастер-сервис. |
HTTP GET /internal/v1/recipes/search?product_id=XYZHeaders: X-Request-ID: "trace-recipe-search-uuid" |
HTTP 422 Unprocessable EntityPayload: ValidationErrorResponseDTOerror_code: "QUERY_VALIDATION_FAILED" |
Шаг 4 (Recipes -> СУБД) |
MDM-сервис рецептов выполняет выборку технологических карт из индексированной таблицы кулинарной книги. | SQL-запрос:SELECT r.* FROM recipe_book r JOIN recipe_ingredients i ON r.id = i.recipe_id WHERE i.product_id = 'XYZ' LIMIT 500; |
PostgreSQL Exception: ConnectionTimeoutPostgreSQL Exception: DiskReadError (сбой сектора диска СУБД) |
Шаг 5 (Recipes -> GW -> APP) |
Сервис возвращает список подходящих рецептов, шлюз транслирует их на экран смартфона пользователя. | HTTP Статус на выходе шлюза: 200 OKPayload ( RecipeListResponseDTO):{ "recipes": [{ "recipe_id": "uuid", "title": "Борщ", "ingredients": [...] }] } |
DioException: receive timeout (клиент разорвал сессию или ушел в авиарежим, не дождавшись рендеринга) |
5 Спецификация серверной логики (SQL) и вилок исключений
5.1 1. Архитектурный SQL-запрос подбора рецептов (Шаги 5–7)
Для реализации бизнес-логики (совпадение минимум по 1 ингредиенту, расчет процента совпадения и жесткий лимит в 500 строк) на уровне PostgreSQL используется оптимизированный запрос с обобщенными табличными выражениями (CTE) и агрегацией.
WITH user_fridge AS (
-- Шаг 5: Выделяем уникальный профиль доступных продуктов пользователя (без минусов)
SELECT DISTINCT DISTINCT ON (LOWER(product_name)) LOWER(product_name) AS food_name
FROM fridge_inventory
WHERE home_group_id = \$1 AND quantity > 0
),
recipe_matching AS (
-- Шаг 6: Сопоставляем ингредиенты рецептов с продуктами в холодильнике
SELECT
ri.recipe_id,
COUNT(ri.ingredient_name) AS total_needed,
SUM(CASE WHEN uf.food_name IS NOT NULL THEN 1 ELSE 0 END) AS matched_count,
ARRAY_AGG(ri.ingredient_name) FILTER (WHERE uf.food_name IS NOT NULL) AS matched_items,
ARRAY_AGG(ri.ingredient_name) FILTER (WHERE uf.food_name IS NULL) AS missing_items
FROM recipe_ingredients ri
LEFT JOIN user_fridge uf ON LOWER(ri.ingredient_name) = uf.food_name
GROUP BY ri.recipe_id
)
-- Фильтруем (минимум 1 совпадение), ранжируем и ставим лимит
SELECT
r.id AS recipe_id,
r.title,
(rm.matched_count::FLOAT / rm.total_needed::FLOAT) AS match_percentage,
rm.total_needed AS total_ingredients,
rm.matched_count,
rm.matched_items,
rm.missing_items
FROM recipe_matching rm
JOIN recipes r ON r.id = rm.recipe_id
WHERE rm.matched_count >= 1 -- Критерий: совпадение минимум по одному продукту
ORDER BY match_percentage DESC, rm.matched_count DESC
LIMIT 500; -- Защита от перегрузки оперативной памяти App5.2 2. Спецификация ответов сервера (Response Body)
5.2.1 HTTP 200 OK (Ответ на Шаге 9)
Возвращается при успешной выборке рецептов. Массивы matched_items и missing_items позволяют Flutter-приложению наглядно красить ингредиенты на экране (зеленый/красный цвет).
{
"total_found": 3,
"limit": 500,
"offset": 0,
"recipes": [
{
"recipe_id": 412,
"title": "Яблочный пирог классический",
"match_percentage": 0.75,
"total_ingredients": 4,
"matched_count": 3,
"matched_items": ["Мука", "Масло сливочное 82%", "Яблоки"],
"missing_items": ["Яйцо куриное"]
},
{
"recipe_id": 105,
"title": "Утренний омлет с зеленью",
"match_percentage": 0.3333,
"total_ingredients": 3,
"matched_count": 1,
"matched_items": ["Масло сливочное 82%"],
"missing_items": ["Яйцо куриное", "Молоко 3.2%"]
}
]
}5.3 3. Вилки исключений и стратегии обработки ошибок
5.3.1 Сценарий А: Пустой холодильник (HTTP 200 OK с пустым массивом)
- Условие: У данной семьи (
home_group_id) в таблицеfridge_inventoryнет ни одной записи сquantity > 0(холодильник пуст или все продукты в виртуальном минусе). - Действие системы: SQL-запрос отработает корректно и вернет 0 строк. Бэкенд возвращает статус
200 OKс пустым массивомrecipes: []. APP перехватывает этот стейт и вместо падения рендерит заглушку: «В вашем холодильнике нет доступных продуктов. Пожалуйста, импортируйте ресурсы (сканируйте чек или добавьте фото), чтобы ИИ подобрал рецепты».
5.3.2 Сценарий Б: Превышение системного лимита запроса (HTTP 400 Bad Request)
- Условие: Клиентское приложение попыталось передать через Query-параметры кастомный
limitбольше, чем зафиксировано в архитектурном стандарте системы (limit=1000). - Действие FastAPI: Срабатывает валидация Pydantic на бэкенд-шлюзе. Запрос отсекается до похода в БД.
{
"error_code": "ERR-MAX-LIMIT-EXCEEDED",
"message": "Передан некорректный параметр страницы. Максимальный лимит выдачи составляет 500 рецептов.",
"details": {
"requested_limit": 1000,
"allowed_max_limit": 500
}
}5.3.3 Сценарий В: Таймаут СУБД из-за высокой параллельной нагрузки (HTTP 503)
- Условие (Шаг 4): Пул соединений к PostgreSQL перегружен, и тяжелый аналитический
JOINне успевает выполниться за установленный таймаут в 2000 мс. - Действие FastAPI: Шлюз возвращает ошибку
ERR-DATABASE-OFFLINEсо статусомHTTP 503 Service Unavailable. На клиенте Dio перехватывает ошибку, не ломая интерфейс, и предлагает пользователю нажать кнопку «Повторить поиск».