[Underconstruction] Метод GET /api/v1/affinity/suggestions

Документация API: Извлечение персонализированных рекомендаций для закупок на основе разреженной матрицы Affinity и продуктов в холодильнике

Author

Application & Simulation Services Framework Documentation

Published

July 2, 2026

ImportantDocumentation Under Development

This documentation section is currently under development and may contain incomplete data. Some technical descriptions, parameters, and system operation scenarios for the digital business process simulator are subject to change.

  • Stable documentation version: Will be published upon final completion and validation of the method code.

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

Метод является ключевым элементом рекомендательного контура (Recall Engine) приложения, а также важной частью вероятностного контура (Stohastic Engine) симулятора и предназначен для вывода умных подсказок на экране «Импорт ресурсов / Покупки». Метод избавляет пользователя от необходимости вручную вбивать списки частых покупок, предугадывая его потребности. В симуляторе вектор параметров полученных методом используется в числе прочих для определения вероятности события.

Метод решает следующие задачи:

  1. Экстраполяция разреженной матрицы (Sparse Matrix Optimization): Так как система хранит аффинити-скоры только для тех продуктов, которые пользователь реально покупал (200–250 уникальных SKU из 20 000 глобального справочника), метод выполняет LEFT JOIN с мастер-данными. Если личной записи нет, система налету подставляет общесистемный дефолтный вес продукта (например, Хлеб = 99, Авокадо = 15).
  2. Динамическая фильтрация по остаткам (Inventory Cross-Check): Метод сопоставляет аффинити-скоры с текущим живым холодильником группы (home_group_id). Продукты с высоким приоритетом, но имеющие физический остаток на складе quantity > 1.0 (или выше критического порога), автоматически исключаются из выдачи, чтобы не плодить дубликаты.
  3. Ранжирование дефицита: Формирует упорядоченный список из Топ-20 рекомендаций, где на самом верху находятся любимые продукты пользователя, которые прямо сейчас полностью закончились (quantity == 0) или находятся в критическом минимуме (на исходе).

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

  • Метод: GET
  • Маршрут: /api/v1/affinity/suggestions
  • Формат данных: application/json (только ответ)

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

Заголовок Обязательный Описание Пример значения
Authorization Да Токен авторизации (Access Token). Бэкенд извлекает из него home_group_id и account_type. Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID Да Сквозной ID запроса для замера скорости аналитического джойна матриц req-aff-suggest-11aa

Спецификация структуры ответа (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"

Перечень допустимых маркеров trigger_reason:

  • STOCK_EMPTY — продукт полностью закончился в холодильнике (баланс = 0.0), но имеет высокий affinity-скор.
  • STOCK_CRITICAL_LOW — продукт на исходе (остался критический float-минимум, например, менее 10% от стандартной закупки).

Пример 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"
    }
  ]
}
ImportantDocumentation Under Development

This documentation section is currently under development and may contain incomplete data. Some technical descriptions, parameters, and system operation scenarios for the digital business process simulator are subject to change.

  • Stable documentation version: Will be published upon final completion and validation of the method code.

Диаграмма последовательности (Mermaid)

На диаграмме представлена логика формирования пресетов «Умного Аффинити». База данных сопоставляет личные скоры пользователя из разреженной матрицы с дефолтными мастер-данными, отсекает позиции, которые уже есть в достаточном количестве в холодильнике, и возвращает во Flutter топ-20 дефицитных товаров.

WarningВажное примечание

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

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: Сверху экрана покупок отрисовывается карусель пресетов быстрой вставки

ImportantDocumentation Under Development

This documentation section is currently under development and may contain incomplete data. Some technical descriptions, parameters, and system operation scenarios for the digital business process simulator are subject to change.

  • Stable documentation version: Will be published upon final completion and validation of the method code.

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

Шаг Действие Параметры / Запросы / DTO Ошибки (Исключения / Статусы)
Шаг – (T_Cooking -> Helper) Бизнес-правило фильтрации: Регистратор интеракций отсекает события приготовления еды, так как они не отражают покупательские паттерны. Поток событий из топика bpds.inventory.out.meal_item.create сбрасывается на уровне фильтра Consumer-кода. Ошибки отсутствуют (срабатывает предикат if event_type == 'COOKING': return)
Шаг 1 (T_Clean_JSON -> Helper) Регистратор интеракций вычитывает валидный JSON-пакет новых чеков покупок из топика дистрибуции для анализа продуктового соседства. Kafka Consumer Poll Request:
Group_ID: "affinity-interaction-registrars", Topic: bpds.mdm.out.product.templated
Payload: { "receipt_id": "rcpt-777-uuid", "user_id": "user-888-uuid", "items": [{"product_id": "prod-milk-uuid"}, {"product_id": "prod-bread-uuid"}] }
KafkaException: CommitFailedException
SerializationException (ошибка десериализации пакета транзакции покупок)
Шаг 2 (Helper -> Affinity) Регистратор транслирует извлеченный список идентификаторов продуктов в менеджер рекомендаций по gRPC-каналу для расчета матрицы совпадений. gRPC Запрос:
RegisterInteraction(InteractionRequest)
Payload: user_id: "user-888-uuid", items_list: ["prod-milk-uuid", "prod-bread-uuid"]
gRPC Status: UNAVAILABLE (сервис affinity-service перегружен или недоступен)
gRPC Status: DEADLINE_EXCEEDED
Шаг 3 (Affinity -> Redis) Алгоритм совпадений: Сервис декомпозирует массив продуктов на уникальные пары и атомарно инкрементирует вес связи между ними в Redis. Redis Команды (Запись связей графа):
1. ZINCRBY "affinity:user-888-uuid:prod-milk-uuid" 1.0 "prod-bread-uuid"
2. ZINCRBY "affinity:user-888-uuid:prod-bread-uuid" 1.0 "prod-milk-uuid"
3. EXPIRE "affinity:user-888-uuid:prod-milk-uuid" 2592000
Redis.ConnectionError: Connection refused
Redis.TimeoutError: Command timed out
Redis.ClusterDownException
Шаг 4 (Cron -> Affinity) Системный планировщик Cron раз в сутки генерирует событие-триггер для запуска регламентного процесса угасания устаревающих интересов. Внутреннее расписание Cron-демона: 0 0 * * * (Каждую полночь). Запуск фонового обработчика triggerInterestDecay(). CronExecutionException (сбой планировщика, пропуск запуска регламентного окна)
Шаг 5 (Affinity -> Redis) Логика Decay: Менеджер рекомендаций вычисляет новые веса неактивных связей по формуле Score * 0.95 для понижения приоритета старых трат. Redis Lua-скрипт (Массовое снижение весов):
local keys = redis.call('KEYS', 'affinity:*') for _, key in ipairs(keys) do local items = redis.call('ZRANGE', key, 0, -1, 'WITHSCORES') for i=1, #items, 2 do redis.call('ZADD', key, items[i+1] * 0.95, items[i]) end end
Redis.ResponseError: Script time limit exceeded (блокировка Redis при слишком большом количестве ключей связей)
Шаг 6 (App -> Affinity) Пользователь открывает экран корзины, и приложение отправляет GET-запрос для получения сопутствующих товарных рекомендаций на основе текущего контекста. HTTP GET /api/v1/affinity/suggestions?product_id=prod-milk-uuid
Headers: Authorization: Bearer <JWT>, Query: product_id="prod-milk-uuid"
DioException: send timeout
HTTP 401 Unauthorized (истек сессионный JWT-токен пользователя)
HTTP 400 Bad Request
Шаг 7 (Redis -> Affinity) Менеджер рекомендаций запрашивает из упорядоченного множества Redis топ ассоциированных продуктов с наивысшими весами. Redis Команда:
ZREVRANGE "affinity:user-888-uuid:prod-milk-uuid" 0 4 WITHSCORES (Извлечение топ-5 связанных товаров)
Redis.ConnectionError: Connection refused
Redis.TimeoutError: Command timed out
Шаг 8 (Affinity -> App) Сервис собирает DTO предложений, и шлюз транслирует пользователю текстовые подсказки перекрестных продаж (Cross-Sell). HTTP Статус на выходе: 200 OK
Payload (AffinitySuggestionsResponseDTO):
{ "source_product_id": "prod-milk-uuid", "suggestions": [{ "product_name": "Хлеб", "product_id": "prod-bread-uuid", "confidence_score": 0.95 }] }
DioException: receive timeout (клиент потерял соединение, зашел в лифт или оборвал TCP-сессию до завершения загрузки)
Шаг 9 (Affinity -> Bupar) Менеджер рекомендаций отправляет асинхронное уведомление о пересчете матрицы связей в контур сквозного аудита процессов. gRPC Fire-and-Forget / Async RPC:
LogAffinityRecalculation(AffinityLogRequest)
Payload: user_id: "user-888-uuid", trace_id: "trace-push-delivery-uuid", activity: "AFFINITY_RECALCULATED"
gRPC Status: UNAVAILABLE (модуль bupar-audit-service перегружен, сообщение сбрасывается или улетает во внутренний повтор)
ImportantDocumentation Under Development

This documentation section is currently under development and may contain incomplete data. Some technical descriptions, parameters, and system operation scenarios for the digital business process simulator are subject to change.

  • Stable documentation version: Will be published upon final completion and validation of the method code.

Спецификация серверной логики (SQL) и вилок исключений

Архитектурный 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; -- Защита от перегрузки экрана карусели пресетов во Flutter

Спецификация структуры таблиц 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);

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

Сценарий А: Новая семья / Чистый профиль (HTTP 200 OK на базе системных дефолтов)

  • Условие: Новая группа пользователей только что зарегистрировалась, в таблице user_product_affinity нет ни одной строки (абсолютно пустая Sparse Matrix).
  • Действие системы: SQL-запрос отработает корректно за счет COALESCE. СУБД подтянет топ-20 продуктов, имеющих наивысший default_system_affinity (Хлеб, Молоко, Яйца). Семья получит базовый качественный набор пресетов первой необходимости.

Сценарий Б: Переполнение холодильника (HTTP 200 OK с пустым массивом suggestions)

  • Условие: Пользователь забил холодильник продуктами до отказа, физический объем абсолютно всех SKU на складе строго выше порога critical_min_threshold.
  • Действие системы: Фильтр WHERE отсечет все строки. Метод вернет статус 200 OK с пустым массивом suggestions: []. Интерфейс Flutter скроет горизонтальную карусель пресетов с экрана покупок, не перегружая UI лишними элементами.

Сценарий В: Таймаут СУБД при тяжелой аналитической сборке (HTTP 503 Service Unavailable)

  • Условие: База данных перегружена параллельными транзакциями списания, и JOIN-операция по 20 000 строкам превысила таймаут пула соединений в 1500 мс.
  • Действие FastAPI: Шлюз перехватывает исключение и возвращает код ошибки ERR-AFFINITY-TIMEOUT. Flutter-приложение гасит карусель рекомендаций, позволяя пользователю продолжить покупки через ручной ввод, не блокируя основной процесс импорта ресурсов.