Метод POST /api/v1/gateway/upload-product-photo

Документация API: Добавление продуктов через готовый снимок с автоматической gRPC ИИ-модерацией

Published

June 11, 2026

WarningОграничение публичной документации

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

  • Полная спецификация метода: Будет доступна только во внутреннем контуре разработки (Confluence / Swagger Enterprise).

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

Метод предназначен для асинхронной обработки и распознавания изображений продуктов (или чеков) с использованием компьютерного зрения (YOLO).

В рамках новой архитектуры этот метод выполняет роль поставщика медиа-контекста (Vision Context Provider):

  1. Изоляция бинарного трафика: Шлюз FastAPI не держит изображение в оперативной памяти во время инференса, а мгновенно сбрасывает его в распределенное объектное хранилище MinIO (S3), освобождая сетевые потоки Uvicorn.
  2. Асинхронная детекция (YOLO): Тяжелый инференс сверточных нейросетей вынесен из API-слоя в изолированный воркер product-vision-processor, который транслирует графические пиксели в текстовый канонический класс предмета.
  3. Переиспользование контура защиты: После детекции текстовая строка автоматически направляется в уже существующий воркер цензуры (censorship-control-worker), что гарантирует соблюдение единых политик безопасности периметра (Fail-Close через Redis).

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

  • Метод: POST
  • Маршрут: /api/v1/purchase/upload-product-photo
  • Формат данных: multipart/form-data

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

Заголовок Обязательный Описание Пример значения
Content-Type Да Указывает на передачу составных данных формы (мультипарт) multipart/form-data; boundary=----WebKitFormBoundary...
Authorization Да Токен авторизации для извлечения user_id и Multi-Tenancy клейма home_group_id Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Request-ID Да Сквозной ID запроса для трассировки сессии (FastAPI \(\rightarrow\) MinIO \(\rightarrow\) Kafka \(\rightarrow\) Workers) req-5f9c-2b1a-88ef
Accept Нет Ожидаемый формат ответа от бэкенд-шлюза application/json

2.2 Спецификация параметров запроса (Query Parameters)

Параметр Тип Обязательный Описание Пример значения
app_lang String Да Целевой язык приложения для адаптации системного промпта на этапе цензуры ru

2.3 Спецификация тела запроса (Request Body — Multipart FormData)

Поле Тип Обязательный Описание Пример значения
file Binary (File) Да Бинарный файл фотографии продукта (image/jpeg, image/png). Ограничение: до 10 МБ. snapshot.jpg
crop_type String Нет Режим кадрирования и предобработки матрицы для ИИ-воркера (default, square, raw) "square"

2.3.1 Пример сырого HTTP-запроса (Payload):

POST /api/v1/purchase/upload-product-photo?app_lang=ru HTTP/1.1
Host: foodlifecycle.com
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID: req-5f9c-2b1a-88ef
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="snapshot.jpg"
Content-Type: image/jpeg

[БИНАРНЫЕ ДАННЫЕ ИЗОБРАЖЕНИЯ]
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="crop_type"

square
------WebKitFormBoundary7MA4YWxkTrZu0gW--

3 Схема обработки запроса пользователя (Mermaid)

sequenceDiagram
    autonumber
    
    actor App as APP (📱 Клиент)
    participant GW as GATEWAY (FastAPI)
    participant S3 as MinIO (S3 Storage)
    participant K as Брокер: Kafka
    participant Vision as product-vision (YOLO)
    participant Censor as censorship-worker

    %% ЧАСТЬ А: ЗАГРУЗКА И ПУШ В ОЧЕРЕДЬ
    App->>GW: POST /upload-product-photo (Multipart, lang)
    activate GW
    GW->>S3: PutObject: Сохранение файла изображения
    activate S3
    S3-->>GW: Возврат image_file_url
    deactivate S3
    note over GW: trace_id Generation & Payload Assembly
    GW->>K: Пуш в топик: bpds.inventory.in.product.image
    GW-->>App: HTTP 202 Accepted (Шлюз свободен)
    deactivate GW

    %% ЧАСТЬ Б: YOLO ДЕТЕКЦИЯ
    K->>Vision: Handler: ProcessProductImage()
    activate Vision
    Vision->>S3: GetObject: Скачивание по URL
    activate S3
    S3-->>Vision: Бинарный графический поток
    deactivate S3
    note over Vision: YOLO Inference:<br/>Локализация контуров и детекция класса
    Vision->>K: Пуш в топик: bpds.inventory.in.receipt.upload
    deactivate Vision

    %% ЧАСТЬ В: ЦЕНЗУРА И ВЕТВЛЕНИЕ
    K->>Censor: Handler: ProcessUploadText()
    activate Censor
    note over Censor: Lua Script Security Check (Fail-Close)
    
    alt Исход 10а: Чисто + ИИ уверен (score >= 0.85)
        Censor->>K: Пуш в топик: bpds.inventory.out.receipt.parsed
    else Исход 10б: Чисто + ИИ НЕ уверен (score < 0.85)
        Censor->>K: Пуш в топик: bpds.mdm.in.product.process
        K-->>App: Трансляция черновика на UI (через WS)
    else Исход 10в: Обнаружен спам / инъекция
        Censor->>K: Пуш в топик: bpds.aid.out.profanity.violate
    end
    deactivate Censor

Процесс загрузки изображения, YOLO-детекции и асинхронного цензурирования продукта

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

Шаг Действие Параметры / Запросы / DTO Ошибки (Исключения / Статусы)
Шаг 1 (Gateway -> MinIO) API-шлюз принимает бинарный файл фотографии продукта (.jpg или .png) со смартфона пользователя, выполняет валидацию размера и сигнатуры медиафайла, после чего загружает его в S3-бакет. S3 API PUT Object Request:
Bucket: "product-images", Key: "uploads/2026/07/product_photo_202.jpg"
Headers: Content-Type: image/jpeg, Content-Length: 3145728
MinIOException: BucketNotFound
MinIOException: AccessDenied (неверные или просроченные учетные данные API Gateway)
StorageFullException
Шаг 2 (MinIO -> Gateway) Персистентное хранилище MinIO сохраняет изображение на диски и возвращает API-шлюзу внутреннюю URL-ссылку на загруженный графический объект. S3 API PUT Object Response:
HTTP Статус: 200 OK
ETag: "4a28c319ba27d3322d8d8a7c5cb9c402", URL: https://minio.internal
MinIOException: ConnectionTimeout
DiskWriteError (критическая ошибка ввода-вывода или размонтирование RAID-массива)
Шаг 3 (Gateway -> T_Photo) Шлюз формирует стартовое DTO задачи компьютерного зрения, внедряет метаданные языка локали приложения, сквозной trace-id и отправляет сообщение в брокер Kafka. Kafka Message (Topic: bpds.inventory.in.product.image):
Payload: { "image_file_url": "https://minio.internal", "app_lang": "ru-RU", "x_request_id": "trace-cv-censor-uuid" }
FastAPI.ValidationError (передан некорректный URL или отсутствуют обязательные поля)
KafkaException: QueueFullException
MessageTimedOut
Шаг 4 (T_Photo -> ProductAI) Микросервис product-vision-processor, подписанный на топик изображений продуктов, вычитывает очередную задачу для проведения объектного анализа. Kafka Consumer Poll Request:
Group_ID: "product-cv-processors", Получен payload шага 3.
Partition: 2, Offset: 99412
KafkaException: CommitFailedException (зависание потока детекции, консьюмер выпал из группы по таймауту)
SerializationException
Шаг 5 (ProductAI -> MinIO) Сервис компьютерного зрения обращается к объектному хранилищу S3 для скачивания бинарной матрицы картинки во временный буфер памяти GPU/CPU. HTTP GET /product-images/uploads/2026/07/product_photo_202.jpg:
Headers: Internal Auth Tokens. На выходе: бинарный поток байт графического файла.
HTTP 404 Not Found (файл удален из бакета до начала обработки воркером)
HTTP 403 Forbidden
ConnectTimeout
Шаг 6 (ProductAI -> T_Results) Локальная нейросеть YOLO определяет границы объекта и транслирует его в строковое имя класса (например, ‘Груша’). Результат вместе с trace-id пушится в общий топик. Kafka Message (Topic: bpds.inventory.in.receipt.upload):
Payload: { "raw_text_input": "Груша Конференция", "app_lang": "ru-RU", "x_request_id": "trace-cv-censor-uuid" }
YOLOInferenceError (низкая уверенность модели или сбой CUDA ядра)
KafkaException: DeliveryTimeout
KafkaException: RecordTooLargeException
Шаг 7 (T_Results -> Censor) Воркер цензуры считывает извлеченный нейросетью YOLO текстовый класс или метаданные объекта для проведения лингвистической фильтрации. Kafka Consumer Poll Request:
Group_ID: "censorship-workers", Topic: bpds.inventory.in.receipt.upload
Payload: { "raw_text_input": "Груша Конференция", "app_lang": "ru-RU", "x_request_id": "trace-cv-censor-uuid" }
KafkaException: CommitFailedException
SerializationException (ошибка десериализации JSON после CV-этапа)
Шаг 8 (Censor -> Ollama) Цензор вызывает локальный LLM-контейнер для проверки семантики полученного имени класса на предмет инъекций, спама или провокационных подмен. HTTP POST /api/generate (Ollama API):
{ "model": "llama3", "prompt": "Analyze the following object class text for profanity. Return exact JSON: { 'is_clean': boolean, 'confidence': float }. Text: 'Груша Конференция'", "stream": false }
ConnectTimeout: Ollama unreachable
HTTP 500 Internal Server Error
ReadTimeout: Inference duration exceeded
Шаг 9 (Censor -> Redis) Параллельно выполняется сверка по локальным словарям мата для app_lang и атомарный инкремент счетчика инцидентов пользователя. Redis Command Pipeline:
1. SISMEMBER "dict:profanity:ru-RU" "проверяемое_слово"
2. MULTI
3. INCRBY "abuse:counter:user_321" 1
4. EXPIRE "abuse:counter:user_321" 86400
5. EXEC
Redis.ConnectionError: Connection refused
Redis.TimeoutError: Command timed out
Redis.ClusterDownException
Шаг 10а (Censor -> T_Clean_Text) Ветка «Успех»: Если имя класса признано чистым и ИИ полностью уверен в результате, текстовое описание продукта отправляется в топик для дальнейшего парсинга. Kafka Message (Topic: bpds.inventory.out.receipt.parsed):
Payload: { "clean_text_input": "Груша Конференция", "app_lang": "ru-RU", "x_request_id": "trace-cv-censor-uuid" }
KafkaException: DeliveryTimeout
KafkaException: NotCoordinatorForException
Шаг 10б (Censor -> T_Draft) Ветка «Сомнение»: Если фото неразборчиво, объект перекрыт или ИИ не уверен в контексте класса, воркер изолирует запись в топик черновиков. Kafka Message (Topic: bpds.mdm.in.product.process):
Payload: { "draft_data": { "raw_id": "uuid-cv", "text": "Неразборчивый класс объекта" }, "app_lang": "ru-RU", "x_request_id": "trace-cv-censor-uuid" }
KafkaException: LeaderNotAvailableException
KafkaException: RecordTooLargeException
Шаг 10в (Censor -> Abuse_Check -> T_Notif) Первичное нарушение на фото: Зафиксирован спам или мат в метаданных. Разветвитель видит в Redis Счетчик == 1 и пушит событие первого предупреждения в шину алармов. Kafka Message (Topic: bpds.aid.out.profanity.violate):
Payload: { "user_id": "user_321", "violation_count": 1, "app_lang": "ru-RU", "notification_type": "CV_PROFANITY_WARNING" }
KafkaException: BrokerNotAvailable
KafkaException: MessageTimedOut
Шаг 11 (T_Draft -> App) Из топика черновиков текст неразборчивого или сомнительного фото выводится на экран смартфона модератора во вкладку ручной корректировки приложения. gRPC / HTTP Stream (Вкладка модерации):
Payload: { "draft_id": "uuid-draft-cv", "raw_text_input": "Сомнительный класс объекта", "app_lang": "ru-RU" }
DioException: connection error
HTTP 401 Unauthorized (истек JWT-токен модератора)
Шаг 12 (App -> T_Results) Модератор вручную корректирует текстовое наименование продукта по фотографии и отправляет чистую версию на повторный цикл цензуры. Kafka Message (Topic: bpds.inventory.in.receipt.upload):
Payload: { "raw_text_input": "Исправленное вручную имя продукта модератором", "app_lang": "ru-RU", "x_request_id": "trace-cv-censor-uuid" }
KafkaException: QueueFullException
FastAPI.ValidationError (текст превысил допустимые лимиты схемы)
Шаг 12а (Censor -> T_Clean_Text) Успешный исход модерации фото: Повторная автоматическая проверка цензором подтверждает чистоту текста. Данные публикуются в топик успешно разобранных строк. Kafka Message (Topic: bpds.inventory.out.receipt.parsed):
Payload: { "clean_text_input": "Исправленное вручную имя продукта модератором", "app_lang": "ru-RU", "x_request_id": "trace-cv-censor-uuid" }
KafkaException: DeliveryTimeout
KafkaException: ConcurrentModificationException
Шаг 12б (Abuse_Check -> T_Notif) Повторный мат при модерации: Если в отредактированном черновике фото снова обнаружен мат, разветвитель фиксирует Счетчик >= 2 и пушит аларм для блокировки пользователя. Kafka Message (Topic: bpds.aid.out.profanity.violate):
Payload: { "user_id": "user_321", "violation_count": 2, "app_lang": "ru-RU", "notification_type": "CV_PROFANITY_BAN" }
KafkaException: LeaderNotAvailableException
InvalidTopicException

5 Спецификация ответов сервера (Response Body) и вилок исключений

5.1 1. Успешные ответы (Success Responses)

5.1.1 HTTP 202 Accepted (Успешный асинхронный ответ — Шаг 6)

Возвращается мобильному приложению шлюзом FastAPI Gateway мгновенно после успешного сохранения бинарного multipart-потока в MinIO S3 и публикации задачи в топик брокера сообщений Kafka.

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "PHOTO_UPLOAD_ACCEPTED",
  "data": {
    "trace_id": "req-5f9c-2b1a-88ef",
    "message": "Фотография успешно загружена в S3-хранилище и принята в пайплайн ИИ-детекции YOLO.",
    "processed_at": "2026-07-20T23:21:00Z"
  }
}

5.1.2 HTTP 201 Created (Асинхронный Исход 10а / Финальное авто-подтверждение)

Генерируется на финальном этапе обработки чека или фото после успешного прохождения цензуры, NER-распознавания и параллельной фиксации (Fan-Out) транзакций в инвентарных базах данных.

  • Заголовки ответа (Response Headers):
    • Content-Type: application/json
  • Тело ответа (Response Body):
{
  "status": "CONFIRMED",
  "product_name": "ГРУША",
  "applied_quantity": 1.0,
  "unit": "шт",
  "timestamp": "2026-07-20T23:21:00Z"
}

5.2 2. Спецификация ошибок и вилок исключений (Error Responses)

Все ошибки бэкенда возвращаются в едином стандартизированном формате RFC 7807 (Problem Details) или стандартном JSON-формате FastAPI HTTPException. Это позволяет Flutter-клиенту (Dio) однозначно парсить код ошибки и выводить корректный локализованный текст на основе динамического справочника.

5.2.1 Исключение: Низкое качество изображения / Сбой распознавания (Асинхронный Исход 10б)

Формируется ИИ-воркером, если модель YOLO вернула score уверенности ниже порогового значения 0.85. Пайплайн инвентаризации приостанавливается, система генерирует черновик и отправляет пуш-контекст на UI через WebSocket.

{
  "error_code": "ERR-LOW-VISION-CONFIDENCE",
  "message": "Не удалось однозначно распознать продукт на фотографии. Требуется ручное подтверждение.",
  "details": {
    "yolo_score": 0.54,
    "image_snapshot_url": "s3://raw-images/uuid-99.jpg",
    "action": "Render interactive draft card on UI with user text-input field fallback."
  }
}

5.2.2 Ошибка загрузки и персистентности файла в бакет (HTTP 500 Internal Server Error — Шаг 2)

Выбрасывается шлюзом FastAPI, если внутренний кластер MinIO S3 недоступен, пул соединений переполнен или хранилище выдало отказ на прием бинарного потока put_object().

{
  "error_code": "ERR-BUCKET",
  "message": "Не удалось сохранить или верифицировать медиафайл во внутреннем объектном хранилище.",
  "details": {
    "reason": "MinIO S3 storage cluster rejected input stream or partition disk is full."
  }
}

5.2.3 Ошибка ИИ-цензурирования и детекции (Асинхронный Исход 10в — Шаг 14в)

Формируется воркером цензуры, если в процессе распознавания или анализа текстового класса модели YOLO локальная нейросеть Ollama через gRPC зафиксировала попытку инъекции кода в метаданные, мат, спам или деструктивный контент (is_profane == true).

{
  "error_code": "ERR-CENSOR",
  "message": "Изображение или распознанный текст класса не прошли автоматическую проверку политик безопасности.",
  "details": {
    "reason": "Spam, explicit content, or prompt injection pattern detected by Ollama Guardrails."
  }
}

5.2.4 Ошибка валидации структуры контракта (HTTP 422 Unprocessable Entity — Шаг 1.1)

Выбрасывается шлюзом FastAPI, если размер переданного файла превышает лимит в 10 МБ, либо нарушена multipart-структура тела запроса формы.

{
  "error_code": "IAD-VAL-422",
  "message": "Переданные данные формы или файл не соответствуют ожидаемой схеме валидации Pydantic.",
  "details": [
    {
      "loc": ["body", "file"],
      "msg": "Размер загружаемой фотографии продукта не должен превышать 10 МБ.",
      "type": "value_error.file.size_limit"
    }
  ]
}

5.2.5 Ошибка авторизации периметра (HTTP 401 Unauthorized — Триггер Interceptor)

Выбрасывается API-шлюзом на самом верхнем уровне, если Access Token просрочен, подпись заголовка изменена, либо токен заблокирован в черном списке.

{
  "error_code": "IAD_JWT_ACCESS_EXPIRED",
  "message": "Срок действия сессии авторизации истек. Доступ к ИИ-пайплайну отклонен.",
  "details": {
    "action": "Trigger queued token refresh mechanism using refresh_token underneath Dio Interceptor."
  }
}

5.2.6 Ошибка недоступности шины данных и периметра защиты (HTTP 503 Service Unavailable)

Выбрасывается шлюзом FastAPI (Шаг 5) при попытке пуша в топик bpds.inventory.in.product.image, если брокер Kafka лежит по таймауту, а также в рамках жесткой стратегии Fail-Close, если упала In-Memory Redis база проверки черных списков.

{
  "error_code": "ERR-INFRASTRUCTURE-OFFLINE",
  "message": "Сервис временно недоступен. Пожалуйста, повторите попытку позже.",
  "details": {
    "reason": "Kafka broker cluster connection timeout or Redis security check failure (Fail-Close enforced)."
  }
}