sequenceDiagram
autonumber
actor User as Пользователь (Экран Обеда / Перекуса)
participant APP as Мобильное приложение (Flutter)
participant API as Бэкенд-шлюз (FastAPI)
participant DB as Реляционная СУБД (PostgreSQL)
participant KAFKA as Брокер событий (Kafka)
User->>APP: Выбирает 1/8 пирога и йогурт, нажимает "Подтвердить прием пищи"
APP->>API: HTTP POST /api/v1/consume/session (JSON-массив с fridge_item_id и portion)
activate API
note over API: Шаг 3: Декодирование JWT (Выделение контекста HOME или OFFICE)
note over API: Шаг 4: Проверка интеграции с сервисом Menu (menu_template_id is null)
Note over API, DB: Старт атомарной SQL-транзакции
loop По каждому элементу из массива consume_items
API->>DB: SELECT quantity FROM fridge_inventory WHERE item_id = fridge_item_id FOR UPDATE
activate DB
DB-->>API: Возвращает текущий float-баланс (например, пирог: 1.0, йогурт: 1.0)
deactivate DB
note over API: Шаг 7: Математическое вычитание порции (quantity - quantity_to_consume)
alt Остатка ХВАТАЕТ
API->>DB: UPDATE fridge_inventory SET quantity = quantity - quantity_to_consume
else Остатка НЕ ХВАТАЕТ
alt Контекст аккаунта == OFFICE
API-->>APP: HTTP 400 Bad Request (ERR-CONSUME-SHORTAGE)
else Контекст аккаунта == HOME
note over API: Шаг 10: Скрытый уход в виртуальный минус (Запись скрывается в общем списке)
API->>DB: UPDATE fridge_inventory SET quantity = quantity - quantity_to_consume (quantity < 0)
end
end
end
Note over API, DB: Коммит атомарной транзакции (COMMIT)
API->>KAFKA: send_to_kafka_async("bupar.fridge.events.consumed")
API-->>APP: HTTP 200 OK (status: "CONSUME_SESSION_RECORDED")
deactivate API
note over APP: Экран обновляется: йогурт исчез, у пирога остаток стал 0.875
Метод POST /api/v1/consume/session
Документация API: Пакетное и одиночное списание продуктов (перекус / комплексный обед) с поддержкой динамической порционности и хука Меню
1 Функциональное назначение
Метод является главной точкой фиксации потребления ресурсов в системе. Он обрабатывает как легкие одиночные перекусы (одно съеденное яблоко или lovely cup of tea), так и комплексные корпоративные приемы пищи («1-е, 2-е и компот»).
Метод решает четыре ключевые задачи: 1. Атомарный пакетный инпут: Принимает массив выбранных позиций, выполняя списание всех продуктов в рамках одной транзакции СУБД. 2. Динамическое порционирование (Дробление): Поддерживает списание готовых сложных блюд долями (float-коэффициентами). Если пользователь делит пирог на 8 частей и съедает одну, Flutter рассчитывает шаг на клиенте (1 / 8 = 0.125) и шлет команду уменьшить баланс блюда строго на 0.125. 3. Изоляция фабричных партий (Magnum / Small): Запрещает автоматическое схлопывание готовых продуктов из супермаркетов для сохранения контроля сроков годности. Списание происходит строго по целевому item_id конкретной упаковки. 4. Интеграционный хук Меню: Архитектура метода подготовлена к подключению внешнего микросервиса Menu (для предприятий). При передаче параметра menu_template_id система автоматически разворачивает шаблон комплексного обеда (пирог, салат, котлеты) в массив списания.
2 Протокол взаимодействия (HTTP Контракт)
- Метод:
POST - Маршрут:
/api/v1/consume/session - Формат данных:
application/json
2.1 Спецификация заголовков (HTTP Headers)
| Заголовок | Обязательный | Описание | Пример значения |
|---|---|---|---|
Content-Type |
Да | Указывает на передачу строго типизированного JSON-пакета | application/json |
Authorization |
Да | Токен авторизации (Access Token). Выделяет home_group_id и контекст HOME / OFFICE. |
Bearer eyJhbGciOiJIUzI1Ni... |
X-Request-ID |
Да | Сквозной ID запроса для связывания логов сессии питания | req-consume-44aa |
2.2 Спецификация тела запроса (Request Body)
| Поле | Тип | Обязательный | Описание | Пример значения |
|---|---|---|---|---|
menu_template_id |
Integer | Нет | Идентификатор шаблона меню для интеграции с сервисом предприятий | 102 (или null) |
consume_items |
Array | Да | Список списываемых товаров (может содержать от 1 до N элементов) | [..._id: 5012, portion: 0.125] |
2.2.1 Пример JSON-запроса (Сценарий: перекус 1/8 частью яблочного пирога и фабричным йогуртом):
{
"menu_template_id": null,
"consume_items": [
{
"fridge_item_id": 5012,
"product_name": "Яблочный пирог классический",
"quantity_to_consume": 0.1250,
"unit": "шт"
},
{
"fridge_item_id": 8841,
"product_name": "Йогурт Danone Клубника",
"quantity_to_consume": 1.0000,
"unit": "шт"
}
]
}3 Схема обработки запроса пользователя (Mermaid)
На диаграмме представлена логика пакетного списания сессии потребления. СУБД обрабатывает как фабричные штучные товары, так и составные блюда, уменьшая их float-баланс на указанную порцию (например, 0.125 для 1/8 пирога).
4 Расшифровка шагов
- Шаг 1 (
User -> APP): Пользователь на экране потребления формирует свой прием пищи. Это может быть как одиночный перекус (ввод одного яблока), так и комплексный обед из нескольких позиций. При выборе пирога пользователь указывает, что берет 1 кусок из 8 — интерфейс Flutter мгновенно переводит это в дробное значение0.1250. Пользователь нажимает кнопку подтверждения. - Шаг 2 (
APP -> API): Приложение через клиентDioотправляет запросHTTP POST /api/v1/consume/session. В теле запроса передается структурированный JSON-массив с точными идентификаторами записей поставок (fridge_item_id) и вычисленными float-коэффициентами порций. - Шаг 3 (
API -> API): Бэкенд-шлюзFastAPIизвлекает метаданные из JWT-токена авторизации. Параметрaccount_typeопределяет, по какому сценарию пойдет обработка в случае нехватки продуктов на складе: жесткая блокировка дляOFFICEили скрытый уход в минус дляHOME. - Шаг 4 (
API -> API): Системный шаг проверки интеграционного хука: бэкенд проверяет полеmenu_template_id. Если бы запрос пришел от корпоративного модуляMenu(предприятия), шлюз предварительно развернул бы шаблон комплексного обеда (пирог, салат, котлеты) в массив списания по технологическим картам. В данном кейсе перекус ручной, поле равноnull, шаг пропускается. - Шаг 5 (
API -> DB): Бэкенд открывает атомарную SQL-транзакцию вPostgreSQL. Запускается цикл обработки корзины потребления. Бэкенд выполняет точечный запросSELECT quantity FROM fridge_inventory WHERE item_id = $1 FOR UPDATEдля первой позиции (яблочного пирога), блокируя строку от параллельных изменений. - Шаг 6 (
DB -> API): База данных возвращает текущий float-баланс записи. Для свежеприготовленного пирога он равен базовому значению1.0000 шт. - Шаг 7 (
API -> API): Алгоритм бэкенда производит математическое вычитание порции:1.0000 - 0.1250 = 0.8750. - Шаг 8 (
API -> DB): Так как остатка пирога хватает, бэкенд выполняет обновление строки в СУБД:UPDATE fridge_inventory SET quantity = 0.8750 WHERE item_id = 5012. - Шаг 9 (
API -> API): Цикл переходит к следующему элементу — фабричному йогурту сitem_id = 8841. Если обнаруживается, что продукт полностью отсутствует на балансе или его количества не хватает для списания, система активирует вилку бизнес-контекстов. - Шаг 10 (
API -> DB):- Ветка OFFICE: Бэкенд немедленно вызывает
ROLLBACK, полностью отменяя списание пирога, и возвращает мобильному приложению ошибкуHTTP 400(запрет дефицита в офисе). - Ветка HOME: Срабатывает логика скрытого ухода в виртуальный минус. Бэкенд вычитает объем, уводя баланс конкретной фабричной записи в минус. В общем списке продуктов на главном экране холодильника эта запись больше не показывается (её видимость блокируется фильтром
quantity > 0), но сама строка долга перемещается в скрытый реестр задолженностей (Метод 5) для последующего слияния или закрытия.
- Ветка OFFICE: Бэкенд немедленно вызывает
- Шаг 11 (
API -> KAFKA): Атомарная транзакция фиксируется в PostgreSQL (COMMIT). Бэкенд асинхронно отправляет бизнес-событие в топик Kafkabupar.fridge.events.consumed. Сообщение содержит массив съеденныхitem_idи их точные float-порции, что необходимо для работы команды MLOps при расчете калорийности и КБЖУ. - Шаг 12 (
API -> APP):FastAPIвозвращает успешный ответHTTP 200 OK. Во Flutter-приложении мгновенно обновляется состояние склада: карточка йогурта исчезает из видимости, а у карточки пирога остаток пересчитывается в0.875 шт.
5 Спецификация ответов сервера (Response Body) и ошибок
5.1 1. Успешный ответ (Success Response)
5.1.1 HTTP 200 OK (Ответ на Шаге 12)
Возвращается мобильному приложению после успешного завершения атомарной транзакции списания. Все указанные в массиве фабричные товары и дробные порции блюд уменьшают свой баланс в базе данных.
- Заголовки ответа (Response Headers):
Content-Type: application/json
- Тело ответа (Response Body):
{
"status": "CONSUME_SESSION_RECORDED",
"data": {
"session_id": "sess-meal-99aa-11ef",
"account_context": "HOME",
"total_items_mutated": 2,
"mutated_items": [
{
"fridge_item_id": 5012,
"product_name": "Яблочный пирог классический",
"remaining_quantity": 0.8750,
"unit": "шт"
},
{
"fridge_item_id": 8841,
"product_name": "Йогурт Danone Клубника",
"remaining_quantity": 0.0000,
"unit": "шт"
}
],
"timestamp": "2026-07-01T23:45:00Z"
}
}5.2 2. Спецификация ошибок и вилок исключений (Error Responses)
5.2.1 Ошибка дефицита при списании (HTTP 400 Bad Request — Шаг 10)
Выбрасывается строго в контексте аккаунта OFFICE, если в процессе циклической проверки на Шаге 5 обнаружилось, что физический остаток товара или порции на складе меньше, чем указано в запросе в поле quantity_to_consume. Транзакция полностью откатывается (ROLLBACK).
{
"error_code": "ERR-CONSUME-SHORTAGE",
"message": "Списание отклонено: в корпоративном контуре запрещено потребление отсутствующих позиций.",
"details": {
"reason": "Physical partition volume underflow during transactional batch consumption.",
"failed_item_id": 8841,
"product_name": "Йогурт Danone Клубника",
"requested_quantity": 1.0000,
"available_quantity": 0.0000,
"unit": "шт"
}
}5.2.2 Ошибка валидации структуры float-коэффициентов (HTTP 422 Unprocessable Entity)
Выбрасывается на Шаге 3, если Pydantic-модель на FastAPI обнаружила отрицательные или нулевые значения порций, либо если точность float превышает 4 знака после запятой.
{
"error_code": "ERR-VALIDATION-FAILED",
"message": "Переданный JSON-пакет содержит некорректные коэффициенты порций или невалидные UUID.",
"details": [
{
"loc": ["body", "consume_items", 0, "quantity_to_consume"],
"msg": "Значение порции списания должно быть строго больше 0.0000",
"type": "value_error.number.not_gt"
}
]
}