sequenceDiagram
autonumber
actor User as Пользователь (Flutter)
participant APP as Мобильное приложение (Dio)
participant NGINX as Прокси-сервер (Nginx)
participant API as Бэкенд-шлюз (FastAPI)
participant S3 as Хранилище (MinIO S3)
participant DB as Реляционная СУБД (PostgreSQL)
participant GRPC as ИИ-сервис (PyTorch / gRPC)
participant KAFKA as Брокер событий (Kafka)
%% ЭТАП 1: ЗАГРУЗКА И ФИКСАЦИЯ ЧЕРНОВИКА
User->>APP: Нажимает "ПАКЕТ ЧЕКА v1" и выбирает фото
APP->>NGINX: HTTP POST /product-photo (multipart бинарник)
NGINX->>API: Проксирует HTTP POST /product-photo
activate API
API->>S3: s3_client.upload_fileobj() -> Сохраняет картинку апельсина/чека
API->>DB: INSERT INTO pending_receipts_buffer (status='PROCESSING') -> Фиксирует UUID черновика
API-->>NGINX: Возвращает HTTP 202 Accepted (draft_id = UUID)
NGINX-->>APP: Транслирует HTTP 202 (Экран свободен для пользователя)
%% ЭТАП 2: АСИНХРОННЫЙ ИИ-ИНФЕРЕНС И МОДЕРАЦИЯ
Note over API, GRPC: Запуск фонового фокус-воркера async_bupar_ai_worker
API->>GRPC: gRPC CheckProduct(draft_id, s3_key, crop_type)
deactivate API
activate GRPC
GRPC->>S3: Скачивает бинарные байты картинки по s3_key
GRPC->>GRPC: Определение является ли это продуктом или чеком
GRPC->>GRPC: PyTorch (MobileNetV2) -> Расчет тензора весов
Note over GRPC: Выделен ID класса 950 (Авто-маппинг в имя "ORANGE")
GRPC-->>API: gRPC ImageResponse(status='PENDING', info='ORANGE')
deactivate GRPC
activate API
Note over API: Проверка типа ответа (Строка, не JSON) -> Запуск вилки исключений
API->>DB: INSERT INTO product_moderation_tickets (status='PENDING') -> Создает тикет
API->>KAFKA: send_to_kafka_async("analytics.dlq.v1") -> Лог инцидента SKU для MLOps
API->>APP: WebSocket /ws/push-stream/{id} -> Сигнал "NEW_MODERATION_TICKET"
Note over APP: Вкладка "Модерация ИИ" мгновенно обновляется и выводит карточку ORANGE
deactivate API
%% ЭТАП 3: РУЧНОЕ ИСПРАВЛЕНИЕ И ЗАЧИСЛЕНИЕ НА СКЛАД
User->>APP: Стирает ORANGE, вводит "АПЕЛЬСИНЫ" и жмет "Утвердить"
APP->>NGINX: HTTP POST /receipt-draft/{UUID}/confirm (С чистым double "qty": 1.0)
NGINX->>API: Проксирует HTTP POST /confirm
activate API
API->>DB: SELECT FROM product_moderation_tickets (Проверка статуса)
Note over API, DB: Старт атомарной SQL-транзакции
API->>DB: INSERT INTO bupar_event_log (activity='PURCHASE') -> Лог для Process Mining в R
API->>DB: INSERT INTO fridge_inventory ON CONFLICT DO UPDATE -> Начисление продуктов
API->>DB: UPDATE product_moderation_tickets (status='CONFIRMED') -> Закрытие тикета
API->>KAFKA: send_to_kafka_async("bupar.fridge.events.add") -> Сигнал изменения остатков
API-->>NGINX: Возвращает HTTP 201 Created
deactivate API
NGINX-->>APP: Транслирует HTTP 201 Created
APP->>DB: Авто-запрос GET /fridge/stock -> Перечитывает актуальный склад
Note over APP: Карточка тикета исчезает, на складе появляются реальные апельсины!
Метод POST /api/v1/fridge/add-by-photo
Документация API: Добавление продуктов через готовый снимок с автоматической gRPC ИИ-модерацией
1 Функциональное назначение
Метод реализует логику быстрого добавления продуктов питания в «цифровой холодильник» через отправку снимка (jpg, png). Фотография продукта либо чек со списком продуктов.
2 Протокол взаимодействия (HTTP Контракт)
- Метод:
POST - Маршрут:
/api/v1/fridge/add-by-photo - Формат данных:
multipart/(jpeg)
2.1 Спецификация заголовков (HTTP Headers)
Обязательные заголовки для передачи бинарного файла, авторизации и сквозного логирования асинхронного трейсинга.
| Заголовок | Обязательный | Описание | Пример значения |
|---|---|---|---|
Content-Type |
Да | Указывает на передачу составных данных формы (мультипарт) | multipart/form-data; boundary=----WebKitFormBoundary... |
Authorization |
Да | Токен авторизации пользователя для определения user_id и home_group_id |
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... |
X-Request-ID |
Да | Сквозной ID запроса. Генерируется на Flutter (Dio) или на Nginx для трейсинга всей цепочки (FastAPI -> S3 -> gRPC -> Kafka) | req-5f9c-2b1a-88ef |
Accept |
Нет | Ожидаемый формат ответа от бэкенд-шлюза | application/json |
2.2 Спецификация тела запроса (Request Body)
| Поле | Тип | Обязательный | Описание | Пример значения |
|---|---|---|---|---|
file |
Binary (File) | Да | Бинарный файл фотографии продукта или товарного чека (image/jpeg, image/png) |
snapshot.jpg |
crop_type |
String | Нет | Режим кадрирования для ИИ-воркера (default, square, raw) |
"square" |
3 Схема обработки запроса пользователя (Mermaid)
4 Расшифровка шагов
4.1 Шаги 1–6: Загрузка файла и первичная фиксация
- Шаг 1 (
User -> APP): Пользователь во Flutter-интерфейсе нажимает кнопку «ПАКЕТ ЧЕКА v1» и выбирает фотографию из галереи или камеры. - Шаг 2 (
APP -> NGINX): Приложение через клиентDioотправляет бинарный файл изображения методомHTTP POST /product-photo(форматmultipart/form-data) на прокси-сервер. - Шаг 3 (
NGINX -> API):Nginxпринимает запрос и перенаправляет (проксирует) его на бэкенд-шлюзFastAPI. В бэкенде активируется контекст обработки (activate API). - Шаг 4 (
API -> S3):FastAPIвызывает асинхронный методs3_client.upload_fileobj(), отправляя бинарный поток фотографии апельсина/чека на сохранение в бакетMinIO S3.- Вилка исключений: Если метод возвращает статус
REJECTEDили хранилище недоступно, бэкенд прерывает транзакцию и выбрасывает ошибку цензурыERR-BUCKET(HTTP 400). Ошибка маппится с текстом для модального окна на клиенте.
- Вилка исключений: Если метод возвращает статус
- Шаг 5 (
API -> DB): Бэкенд делает запись в базу данныхPostgreSQL:INSERT INTO pending_receipts_buffer (status='PROCESSING'). Этим он резервирует буфер и генерирует уникальныйUUID(draft_id) для черновика.- Вилка исключений: Если база данных недоступна, система возвращает ошибку, прерывая операцию.
- Шаг 6 (
API -> NGINX): Бэкенд мгновенно, не дожидаясь ИИ, возвращает прокси-серверу ответHTTP 202 Acceptedвместе со сгенерированнымdraft_id = UUID.
4.2 Шаги 7–14: Асинхронный ИИ-инференс, генерация тикета и Push-уведомление
- Шаг 7 (
NGINX -> APP):Nginxтранслирует ответHTTP 202в мобильное приложение. Экран смартфона освобождается для пользователя, он может идти дальше по приложению. - Шаг 8 (
API -> GRPC): На бэкенде фоновый воркерasync_bupar_ai_workerинициирует удаленный вызов к ИИ-сервису через методgRPC CheckProduct(draft_id, s3_key, crop_type). ШлюзFastAPIвременно деактивируется, а gRPC-сервер активируется (activate GRPC).- Стратегия Fail-Safe: Если gRPC-сервер оффлайн, система пишет предупреждение
[Moderation Warning]в системный лог и пропускает запрос по сценарию ограниченного доверия, чтобы не блокировать действия реального человека.
- Стратегия Fail-Safe: Если gRPC-сервер оффлайн, система пишет предупреждение
- Шаг 9 (
GRPC -> S3): ИИ-сервис на базеPyTorchобращается к хранилищуMinIO S3и скачивает сырые байты картинки по переданному адресуs3_key. - Шаг 10 (
GRPC -> GRPC): Внутренний шаг ИИ-сервиса: нейросетьPyTorch (MobileNetV2)рассчитывает тензор весов. На выходе определяется ID класса950, который автоматически маппится в строку"ORANGE". - Шаг 11 (
GRPC -> API): ИИ-сервис возвращает структурированный gRPC-ответImageResponse(status='PENDING', info='ORANGE')обратно на бэкендFastAPI. Работа gRPC-сервера завершается.- Вилка исключений: Если ИИ-модератор возвращает статус
REJECTED, бэкенд прерывает транзакцию и выбрасывает ошибку цензурыERR-BUPAR-CENSOR(HTTP 400).
- Вилка исключений: Если ИИ-модератор возвращает статус
- Шаг 12 (
API -> DB):FastAPIснова активен. Происходит проверка типа ответа (ожидается строка, не JSON) — в случае несовпадения запускается вилка исключений. Затем создаётся запись в БД:INSERT INTO product_moderation_tickets (status='PENDING')для ручного подтверждения. - Шаг 13 (
API -> KAFKA): Бэкенд отправляет асинхронное сообщениеsend_to_kafka_async("analytics.dlq.v1"). Этот инцидент SKU логируется в брокер сообщений для команды MLOps. - Шаг 14 (
API -> APP): Бэкенд через открытый WebSocket-канал/ws/push-stream/{id}отправляет клиенту push-сигнал"NEW_MODERATION_TICKET". У пользователя во Flutter мгновенно обновляется вкладка «Модерация ИИ» и выводится карточкаORANGE. Контекст API засыпает.
4.3 Шаги 15–22: Ручное исправление, транзакция в БД и обновление остатков
- Шаг 15 (
User -> APP): Пользователь видит карточкуORANGE, вручную стирает её, пишет"АПЕЛЬСИНЫ"и нажимает кнопку «Утвердить». Чистый текст разбивается по разделителям (пробелам). Первое слово интерпретируется как наименование продукта, второе — преобразуется в типfloatкак количество (дефолтное значение = 1.0). - Шаг 16 (
APP -> NGINX): Приложение отправляет запросHTTP POST /receipt-draft/{UUID}/confirmс телом"qty": 1.0(тип double) на прокси-сервер. - Шаг 17 (
NGINX -> API):Nginxперенаправляет этот POST-запрос на бэкендFastAPI. Бэкенд снова активируется. - Шаг 18 (
API -> DB): Бэкенд идет в базу данных с запросомSELECT FROM product_moderation_tickets, чтобы проверить статус тикета (защита от повторного подтверждения). В этот момент стартует атомарная SQL-транзакция, внутри которой последовательно выполняются еще три операции:INSERT INTO bupar_event_log (activity='PURCHASE')— логирование для Process Mining в R.INSERT INTO fridge_inventory ON CONFLICT DO UPDATE— фиксация в базе данных. В случае коллизии (если такой продукт уже добавлен в рамках этой семьи /home_group_id), срабатывает логика инкремента остатков:DO UPDATE SET quantity = fridge_inventory.quantity + EXCLUDED.quantity.UPDATE product_moderation_tickets (status='CONFIRMED')— закрытие тикета.
- Шаг 19 (
API -> KAFKA): После успешного коммита транзакции в БД, бэкенд отправляет событиеsend_to_kafka_async("bupar.fridge.events.add"), сигнализируя всей системе, что остатки продуктов на складе изменились. - Шаг 20 (
API -> NGINX): Бэкенд сообщает об успехе, возвращая статусHTTP 201 Createdпрокси-серверу. - Шаг 21 (
NGINX -> APP):Nginxтранслирует статусHTTP 201 Createdв мобильное приложение. Контекст API завершается. - Шаг 22 (
APP -> DB): Приложение лочит статус201и делает автоматический фоновый запросGET /fridge/stock(для вычитывания актуального склада), карточка тикета исчезает, и на экране пользователя появляются реальные апельсины.
5 Спецификация ответов сервера (Response Body)
5.1 Успешное добавление (201 Created)
{
"status": "SUCCESS",
"message": "Продукт 'Яблоки' успешно отмодерирован и добавлен текстовым контуром."
}5.2 Ошибка модерации (400 Bad Request)
Возвращается, если ИИ-контур зафиксировал мат или спам в текстовом вводе реального пользователя.
{
"code": "ERR-BUPAR-CENSOR",
"detail": "Текст не прошел модерацию: обнаружена ненормативная лексика или спам."
}6 Спецификация ответов сервера (Response Body) и ошибок
6.1 1. Успешные ответы (Success Responses)
6.1.1 HTTP 202 Accepted (Ответ на Шаге 6 — Загрузка фото)
Возвращается мгновенно после успешного сохранения бинарного файла в MinIO S3 и фиксации черновика в PostgreSQL.
- Заголовки ответа (Response Headers):
Content-Type: application/json
- Тело ответа (Response Body):
{
"status": "PROCESSING",
"draft_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"message": "Файл успешно загружен. Запущен асинхронный контур ИИ-модерации."
}6.1.2 HTTP 201 Created (Ответ на Шаге 21 — Подтверждение)
Возвращается после успешного завершения атомарной транзакции в PostgreSQL и отправки бизнес-события в Kafka.
- Заголовки ответа (Response Headers):
Content-Type: application/json
- Тело ответа (Response Body):
{
"status": "CONFIRMED",
"product_name": "АПЕЛЬСИНЫ",
"applied_quantity": 1.0,
"unit": "шт",
"timestamp": "2026-07-01T20:21:00Z"
}6.2 2. Спецификация ошибок и вилок исключений (Error Responses)
Все ошибки бэкенда возвращаются в едином стандартизированном формате RFC 7807 (Problem Details) или стандартном JSON-формате FastAPI HTTPException. Это позволяет Flutter-клиенту (Dio) однозначно парсить код ошибки и выводить корректный текст в модальных окнах.
6.2.1 Ошибка цензуры бакета (HTTP 400 Bad Request — Шаг 4)
Выбрасывается, если контур загрузки в S3 заблокировал файл или S3 вернул отказ.
{
"error_code": "ERR-BUCKET",
"message": "Не удалось сохранить или верифицировать медиафайл во внутреннем хранилище.",
"details": {
"reason": "S3 bucket storage rejected input stream or content validation failed."
}
}6.2.2 Ошибка ИИ-цензурирования (HTTP 400 Bad Request — Шаг 11)
Выбрасывается воркером, если ИИ-модератор через gRPC вернул статус REJECTED (обнаружен спам, ненормативная лексика или изображение не содержит продуктов/чеков).
{
"error_code": "ERR-BUPAR-CENSOR",
"message": "Изображение не прошло автоматическую проверку ИИ-модератором.",
"details": {
"reason": "Spam, explicit content, or invalid SKU pattern detected by MobileNetV2."
}
}6.2.3 Ошибка валидации структуры (HTTP 422 Unprocessable Entity — Шаг 12 или 17)
Выбрасывается FastAPI, если gRPC-сервер вернул некорректный тип данных (JSON вместо строки), либо если клиент передал невалидный UUID / отрицательный double qty на Шаге 17.
{
"error_code": "ERR-VALIDATION-FAILED",
"message": "Переданные данные не соответствуют ожидаемой схеме валидации Pydantic.",
"details": [
{
"loc": ["body", "qty"],
"msg": "Value must be greater than 0.0",
"type": "value_error.number.not_gt"
}
]
}