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: Сверху экрана покупок отрисовывается карусель пресетов быстрой вставки
[Underconstruction] Метод GET /api/v1/affinity/suggestions
Документация API: Извлечение персонализированных рекомендаций для закупок на основе разреженной матрицы Affinity и продуктов в холодильнике
- AFF-MIGRATION-101 Backlog — Таблица In-Memory Redis.
- AFF-MIGRATION-102 Backlog — Реализация скрипта Decay Strategy.
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) симулятора и предназначен для вывода умных подсказок на экране «Импорт ресурсов / Покупки». Метод избавляет пользователя от необходимости вручную вбивать списки частых покупок, предугадывая его потребности. В симуляторе вектор параметров полученных методом используется в числе прочих для определения вероятности события.
Метод решает следующие задачи:
- Экстраполяция разреженной матрицы (Sparse Matrix Optimization): Так как система хранит аффинити-скоры только для тех продуктов, которые пользователь реально покупал (200–250 уникальных SKU из 20 000 глобального справочника), метод выполняет
LEFT JOINс мастер-данными. Если личной записи нет, система налету подставляет общесистемный дефолтный вес продукта (например, Хлеб = 99, Авокадо = 15). - Динамическая фильтрация по остаткам (Inventory Cross-Check): Метод сопоставляет аффинити-скоры с текущим живым холодильником группы (
home_group_id). Продукты с высоким приоритетом, но имеющие физический остаток на складеquantity > 1.0(или выше критического порога), автоматически исключаются из выдачи, чтобы не плодить дубликаты. - Ранжирование дефицита: Формирует упорядоченный список из Топ-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"
}
]
}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 дефицитных товаров.
В настоящей публичной документации отображены не все шаги и сценарии для приложения в частности и для системы цифровых симуляторов бизнес-процессов в общем.
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.templatedPayload: { "receipt_id": "rcpt-777-uuid", "user_id": "user-888-uuid", "items": [{"product_id": "prod-milk-uuid"}, {"product_id": "prod-bread-uuid"}] } |
KafkaException: CommitFailedExceptionSerializationException (ошибка десериализации пакета транзакции покупок) |
Шаг 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 refusedRedis.TimeoutError: Command timed outRedis.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-uuidHeaders: Authorization: Bearer <JWT>, Query: product_id="prod-milk-uuid" |
DioException: send timeoutHTTP 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 refusedRedis.TimeoutError: Command timed out |
Шаг 8 (Affinity -> App) |
Сервис собирает DTO предложений, и шлюз транслирует пользователю текстовые подсказки перекрестных продаж (Cross-Sell). | HTTP Статус на выходе: 200 OKPayload ( 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 перегружен, сообщение сбрасывается или улетает во внутренний повтор) |
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 лишними элементами.