sequenceDiagram
autonumber
actor User as Пользователь (Экран Закупок)
participant APP as Мобильное приложение (Flutter)
participant AS as Сервис Аффинити (FastAPI)
participant DB as База Данных (PostgreSQL)
User->>APP: Открывает экран "Импорт ресурсов / Покупки"
APP->>AS: HTTP GET /api/v1/affinity/suggestions (JWT в Header)
activate AS
note over AS: Шаг 3: Извлечение home_group_id и контекста из JWT-токена
AS->>DB: Вызов SQL-запроса кросс-валидации матриц и остатков
activate DB
note over DB: Шаг 5: LEFT JOIN разреженной таблицы пользователя и глобальных SKU
note over DB: Шаг 6: LEFT JOIN с fridge_inventory (Срез текущих живых остатков)
note over DB: Шаг 7: Фильтрация (Остаток == 0 ИЛИ < 10%), сортировка по score и LIMIT 20
DB-->>AS: Возвращает упорядоченную матрицу рекомендаций (макс. 20 строк)
deactivate DB
AS-->>APP: HTTP 200 OK (JSON с ранжированными подсказками закупок)
deactivate AS
note over APP: Сверху экрана покупок отрисовывается карусель пресетов быстрой вставки
Метод GET /api/v1/affinity/suggestions
Документация API: Извлечение персонализированных рекомендаций для закупок на основе разреженной матрицы Affinity и продуктов в холодильнике
- INVENTORY-2XX Backlog — Краткое Описание задачи 1.
- INVENTORY-1XX Refinement (Уточнение) — Краткое Описание задачи 2.
1 Функциональное назначение
Метод является ключевым элементом рекомендательного контура (Recall Engine) приложения, а также важной частью вероятностного контура (Stohastic Engine) симулятора и предназначен для вывода умных подсказок на экране «Импорт ресурсов / Покупки». Метод избавляет пользователя от необходимости вручную вбивать списки частых покупок, предугадывая его потребности. В симуляторе вектор параметров полученных методом используется в числе прочих для определения вероятности события.
Метод решает следующие архитектурные задачи:
- Экстраполяция разреженной матрицы (Sparse Matrix Optimization): Так как система хранит аффинити-скоры только для тех продуктов, которые пользователь реально покупал (200–250 уникальных SKU из 20 000 глобального справочника), метод выполняет
LEFT JOINс мастер-данными. Если личной записи нет, система налету подставляет общесистемный дефолтный вес продукта (например, Хлеб = 99, Авокадо = 15). - Динамическая фильтрация по остаткам (Inventory Cross-Check): Метод сопоставляет аффинити-скоры с текущим живым холодильником группы (
home_group_id). Продукты с высоким приоритетом, но имеющие физический остаток на складеquantity > 1.0(или выше критического порога), автоматически исключаются из выдачи, чтобы не плодить дубликаты. - Ранжирование дефицита: Формирует упорядоченный список из Топ-20 рекомендаций, где на самом верху находятся любимые продукты пользователя, которые прямо сейчас полностью закончились (
quantity == 0) или находятся в критическом минимуме (на исходе).
2 Протокол взаимодействия (HTTP Контракт)
- Метод:
GET - Маршрут:
/api/v1/affinity/suggestions - Формат данных:
application/json(только ответ)
2.1 Спецификация заголовков (HTTP Headers)
| Заголовок | Обязательный | Описание | Пример значения |
|---|---|---|---|
Authorization |
Да | Токен авторизации (Access Token). Бэкенд извлекает из него home_group_id и account_type. |
Bearer eyJhbGciOiJIUzI1Ni... |
X-Request-ID |
Да | Сквозной ID запроса для замера скорости аналитического джойна матриц | req-aff-suggest-11aa |
2.2 Спецификация структуры ответа (Response Body)
Метод возвращает упорядоченный массив подсказок. Поле trigger_reason сообщает интерфейсу Flutter, почему товар попал в рекомендации, для вывода красивых текстовых плашек (баннеров).
| Поле | Тип | Описание | Пример значения |
|---|---|---|---|
master_product_id |
Integer | Глобальный ID товара в справочнике Master Data | 1024 |
affinity_score |
Integer | Итоговый вычисленный балл предпочтения (от 0 до 100) | 94 |
trigger_reason |
String | Маркер причины рекомендации для UI | "STOCK_EMPTY" |
2.2.1 Перечень допустимых маркеров trigger_reason:
STOCK_EMPTY— продукт полностью закончился в холодильнике (баланс = 0.0), но имеет высокий affinity-скор.STOCK_CRITICAL_LOW— продукт на исходе (остался критический float-минимум, например, менее 10% от стандартной закупки).
2.2.2 Пример JSON-ответа (Payload):
{
"home_group_id": "group-772-alpha",
"suggestions_count": 2,
"suggestions": [
{
"master_product_id": 1024,
"product_name": "Молоко 3.2% пастеризованное",
"affinity_score": 94,
"current_stock": 0.0,
"unit": "л",
"trigger_reason": "STOCK_EMPTY"
},
{
"master_product_id": 2048,
"product_name": "Яблоки Гренни Смит",
"affinity_score": 82,
"current_stock": 0.150,
"unit": "кг",
"trigger_reason": "STOCK_CRITICAL_LOW"
}
]
}3 Схема обработки запроса пользователя (Mermaid)
На диаграмме представлена логика формирования пресетов «Умного Аффинити». База данных сопоставляет личные скоры пользователя из разреженной матрицы с дефолтными мастер-данными, отсекает позиции, которые уже есть в достаточном количестве в холодильнике, и возвращает во Flutter топ-20 дефицитных товаров.
В настоящей публичной документации отображены не все шаги и сценарии для приложения в частности и для системы цифровых симуляторов бизнес-процессов в общем.
4 Расшифровка шагов
- Шаг 1 (
User -> APP): Пользователь открывает раздел закупок или планирования корзины (экран «Импорт ресурсов») во Flutter-приложении. Интерфейс автоматически, без дополнительных кликов со стороны пользователя, инициирует фоновый запрос для генерации персональных рекомендаций. - Шаг 2 (
APP -> AS): Мобильное приложение отправляет запросHTTP GET /api/v1/affinity/suggestions. Сквозной логирующий заголовокX-Request-IDи Access Token авторизации передаются в стандартном виде. - Шаг 3 (
AS -> AS): Микросервис (FastAPI) декодирует JWT Claims токена и выделяетhome_group_id. Логика аффинити используется как для домашнего аккаунтаHOME, так и для аккаунта компанииOFFICE, метод работает для любого типа аккаунта. - Шаг 4 (
AS -> DB): Сервис отправляет в базу данныхPostgreSQLаналитический SQL-запрос, реализующий кросс-проверку трех независимых таблиц для бесшовного развертывания разреженной матрицы. - Шаг 5 (
DB -> DB): На уровне СУБД запускается алгоритмLEFT JOIN. Базовая таблица глобальных продуктов (master_products, 20 000 строк) соединяется с таблицей личных предпочтений пользователя (user_product_affinity) по составному ключу группы. Благодаря индексам, СУБД мгновенно вытаскивает кастомный балл для тех 200 позиций, которые семья покупала, а для остальных 19 800 строк налету подставляет системный дефолтный балл с помощью функцииCOALESCE(). - Шаг 6 (
DB -> DB): Полученная матрица предпочтений повторно соединяется черезLEFT JOINс таблицей текущего холодильника (fridge_inventory) по совпадению имен продуктов, чтобы получить текущие остатки. - Шаг 7 (
DB -> DB): Фильтрация и ранжирование: СУБД отсекает товары, которых в холодильнике прямо сейчас достаточно. В выборку пропускаются только те продукты, у которых баланс на складе равен0.0(полный дефицит) или упал ниже критического float-порога (на исходе). Строки сортируются по убыванию affinity score (ORDER BY affinity_score DESC) и ограничиваются жестким лимитомLIMIT 20. - Шаг 8 (
DB -> AS): База данных отдает сервису отранжированный и очищенный от дубликатов массив из 20 целевых подсказок. Время выполнения сложной выборки составляет менее 35 мс. - Шаг 9 (
AS -> APP):FastAPIупаковывает результат в Pydantic-модель и возвращает статусHTTP 200 OK. Мобильное приложение Flutter принимает JSON-датасет. Стейт-менеджер мгновенно отрисовывает горизонтальную карусель быстрых пресетов сверху экрана покупок. Пользователь видит любимое молоко и яблоки с плашками «Закончилось» и может в одинтапдобавить их в планируемый список, не прибегая к ручному поиску по буквам.
5 Спецификация серверной логики (SQL) и вилок исключений
5.1 Архитектурный SQL-запрос кросс-валидации Sparse Matrix (Шаги 5–7)
Для реализации бизнес-логики (бесшовное развертывание разреженной матрицы, LEFT JOIN с текущими остатками холодильника и жесткий LIMIT 20) на уровне PostgreSQL используется аналитический запрос со слиянием через COALESCE.
WITH current_fridge_stock AS (
-- Агрегируем текущие остатки холодильника пользователя по каноническим SKU
SELECT LOWER(product_name) AS food_name, SUM(quantity) AS total_qty, unit
FROM fridge_inventory
WHERE home_group_id = \$1
GROUP BY LOWER(product_name), unit
)
SELECT
mp.id AS master_product_id,
mp.product_name,
-- Развертывание Sparse Matrix: если личного скора нет, берем системный дефолт
COALESCE(upa.score, mp.default_system_affinity) AS affinity_score,
COALESCE(cfs.total_qty, 0.0)::FLOAT AS current_stock,
mp.unit,
-- Динамическое определение триггера для интерфейса Flutter
CASE
WHEN COALESCE(cfs.total_qty, 0.0) = 0.0 THEN 'STOCK_EMPTY'
ELSE 'STOCK_CRITICAL_LOW'
END AS trigger_reason
FROM master_products mp
-- Шаг 5: LEFT JOIN с разреженной таблицей личного аффинити пользователя
LEFT JOIN user_product_affinity upa
ON upa.master_product_id = mp.id AND upa.home_group_id = \$1
-- Шаг 6: LEFT JOIN со срезом текущих остатков склада
LEFT JOIN current_fridge_stock cfs
ON cfs.food_name = LOWER(mp.product_name) AND cfs.unit = mp.unit
-- Шаг 7: Фильтрация дефицита и ранжирование по силе предпочтения
WHERE
COALESCE(cfs.total_qty, 0.0) = 0.0 -- Продукт полностью закончился
OR (COALESCE(cfs.total_qty, 0.0) > 0.0 AND COALESCE(cfs.total_qty, 0.0) < mp.critical_min_threshold) -- Продукт на исходе
ORDER BY affinity_score DESC, current_stock ASC
LIMIT 20; -- Защита от перегрузки экрана карусели пресетов во Flutter5.2 Спецификация структуры таблиц PostgreSQL (Sparse Matrix Architecture)
Схема базы данных поддерживает концепцию разреженной матрицы: записи в user_product_affinity генерируются точечно строго в моменты транзакций.
-- Таблица 1: Глобальный справочник продуктов (Мастер-данные, 20 000+ строк)
CREATE TABLE master_products (
id SERIAL PRIMARY KEY,
product_name VARCHAR(255) NOT NULL UNIQUE,
default_system_affinity INT NOT NULL DEFAULT 15, -- Дефолтный скор (Хлеб=99, Авокадо=15)
critical_min_threshold NUMERIC(10, 4) NOT NULL DEFAULT 0.1000, -- Порог дефицита (например, < 0.1 кг)
unit VARCHAR(20) NOT NULL -- "кг", "шт", "л"
);
-- Таблица 2: Разреженная матрица личного сходства пользователя (Sparse Matrix)
CREATE TABLE user_product_affinity (
id BIGSERIAL PRIMARY KEY,
home_group_id VARCHAR(50) NOT NULL,
master_product_id INT NOT NULL REFERENCES master_products(id) ON DELETE CASCADE,
score INT NOT NULL CHECK (score >= 0 AND score <= 100), -- Балл предпочтения от 0 до 100
last_interaction TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- Дата для Крон-затухания
CONSTRAINT unique_group_product_affinity UNIQUE (home_group_id, master_product_id)
);
-- Составной индекс для мгновенных LEFT JOIN операций
CREATE INDEX idx_user_affinity_composite ON user_product_affinity(home_group_id, master_product_id);5.3 Вилки исключений и обработка ошибок
5.3.1 Сценарий А: Новая семья / Чистый профиль (HTTP 200 OK на базе системных дефолтов)
- Условие: Новая группа пользователей только что зарегистрировалась, в таблице
user_product_affinityнет ни одной строки (абсолютно пустая Sparse Matrix). - Действие системы: SQL-запрос отработает корректно за счет
COALESCE. СУБД подтянет топ-20 продуктов, имеющих наивысшийdefault_system_affinity(Хлеб, Молоко, Яйца). Семья получит базовый качественный набор пресетов первой необходимости.
5.3.2 Сценарий Б: Переполнение холодильника (HTTP 200 OK с пустым массивом suggestions)
- Условие: Пользователь забил холодильник продуктами до отказа, физический объем абсолютно всех SKU на складе строго выше порога
critical_min_threshold. - Действие системы: Фильтр
WHEREотсечет все строки. Метод вернет статус200 OKс пустым массивомsuggestions: []. Интерфейс Flutter скроет горизонтальную карусель пресетов с экрана покупок, не перегружая UI лишними элементами.