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
- INVENTORY-2XX Backlog — Краткое Описание задачи 1.
- INVENTORY-1XX Refinement (Уточнение) — Краткое Описание задачи 2.
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 Расшифровка шагов
- Шаг 1 (
User -> APP): Пользователь переходит на экран умного подбора рецептов. Приложение автоматически инициирует поиск без необходимости ввода текста — триггером выступает сам факт открытия экрана на основе текущего содержимого холодильника. - Шаг 2 (
APP -> API): Flutter-приложение отправляет запросHTTP GET /api/v1/cook/recipes/search. В заголовках передаетсяAuthorization: Bearer <JWT>иX-Request-IDдля замера скорости выполнения тяжелого поискового запроса. - Шаг 3 (
API -> API): БэкендFastAPIдекодирует токен пользователя и извлекает параметрhome_group_id. На основе этого идентификатора будет производиться фильтрация продуктов конкретной семьи или офиса. - Шаг 4 (
API -> DB): Бэкенд отправляет вPostgreSQLоптимизированный SQL-запрос, использующий обобщенные табличные выражения (CTE) для параллельного подсчета совпадений. - Шаг 5 (
DB -> DB): Внутри СУБД на первом этапе изолируется текущий срез холодильника: собираются все уникальные канонические имена продуктов (product_name) для данногоhome_group_id, у которых физический объем строго больше нуля (quantity > 0). Отрицательные виртуальные минусы на этом этапе игнорируются, чтобы не предлагать рецепты из полностью отсутствующих продуктов. - Шаг 6 (
DB -> DB): На втором этапе СУБД производит соединение (JOIN) полученного набора продуктов со справочником ингредиентов рецептов. Срабатывает важнейший критерий: рецепт попадает в промежуточную выборку, если зафиксировано пересечение минимум по одному наименованию продукта. - Шаг 7 (
DB -> DB): СУБД группирует данные поrecipe_idи рассчитывает метрики: общее количество необходимых ингредиентов для блюда, количество совпавших позиций в холодильнике и коэффициентmatch_percentage(отношение имеющихся к требуемым). К выборке применяется жесткая сортировкаORDER BY match_percentage DESC(самые готовые рецепты идут вверх) и системный ограничительLIMIT 500. - Шаг 8 (
DB -> API): База данных отдает бэкенду отрендеренный и отсортированный массив данных, не превышающий 500 записей. Благодаря переносу расчетов на уровень индексов СУБД, время выполнения запроса составляет менее 40 мс. - Шаг 9 (
API -> APP):FastAPIупаковывает результат в Pydantic-модель и возвращает клиенту статусHTTP 200 OK. Мобильное приложение Flutter мгновенно отрисовывает карточки рецептов. Пользователь сразу видит, что он может приготовить (например, пирог с совпадением 75%, где не хватает только одного яйца).
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
)
-- Шаг 7: Фильтруем (минимум 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; -- Защита от перегрузки оперативной памяти Flutter5.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: []. Flutter перехватывает этот стейт и вместо падения рендерит заглушку: «В вашем холодильнике нет доступных продуктов. Пожалуйста, импортируйте ресурсы (сканируйте чек или добавьте фото), чтобы ИИ подобрал рецепты».
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 перехватывает ошибку, не ломая интерфейс, и предлагает пользователю нажать кнопку «Повторить поиск».