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

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

Published

June 11, 2026

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

Метод предназначен для асинхронного извлечения печатного текста с фотографии чека с помощью оптического распознавания символов (OCR).

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

  1. Разгрузка сетевых потоков: Бинарный multipart-поток фотографии сбрасывается шлюзом в S3-бакет MinIO, избавляя подсистему от зависания UI.
  2. Построчное чтение текста: Тяжелый графический инференс вынесен в асинхронный воркер image-processor. Его задача — отыскать символы, нормализовать строки и превратить массив пикселей в сырой текст чека.
  3. Переиспользование контура фильтрации: На Шаге 11 воркер отправляет извлеченный текст в тот же самый топик bpds.inventory.in.receipt.upload, который мы спроектировали для ручного ввода текста. Это позволяет повторно использовать воркер цензуры (censorship-control-worker) без дублирования кода.

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

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

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

Заголовок Обязательный Описание Пример значения
Content-Type Да Формат передачи составных данных формы (мультипарт) multipart/form-data; boundary=----WebKitFormBoundary...
Authorization Да Токен пользователя для Multi-Tenancy изоляции (user_id, home_group_id) Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Request-ID Да Сквозной ID запроса для трассировки (FastAPI \(\rightarrow\) MinIO \(\rightarrow\) Kafka \(\rightarrow\) OCR) req-8c2a-4b9d-99ef

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

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

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

Поле Тип Обязательный Описание Пример значения
file Binary (File) Да Бинарный файл фотографии бумажного чека (image/jpeg, image/png). Лимит: до 10 МБ. receipt_photo.jpg

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

POST /api/v1/purchase/upload-receipt?app_lang=ru HTTP/1.1
Host: foodlifecycle.com
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
X-Request-ID: req-8c2a-4b9d-99ef
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary9XzR2YwkTrZu0gW

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

[БИНАРНЫЕ ДАННЫЕ ИЗОБРАЖЕНИЯ ЧЕКА]
------WebKitFormBoundary9XzR2YwkTrZu0gW--

sequenceDiagram
    autonumber
    
    actor App as APP (📱 Мобильный клиент)
    participant GW as GATEWAY (backend-api)
    participant S3 as MinIO Object Storage
    participant K as Брокер: Apache Kafka
    participant OCR as image-processor (OCR)
    participant Censor as censorship-control-worker

    %% ШАГ 1: ЗАГРУЗКА ИЗОБРАЖЕНИЯ И ПУШ ЗАДАЧИ
    App->>GW: Шаг 1: POST /api/v1/purchase/upload-receipt (Multipart Photo, app_lang)
    activate GW
    GW->>S3: Шаг 2: PutObject: Сохранение бинарного фото чека
    activate S3
    S3-->>GW: Шаг 3: Возврат image_file_url
    deactivate S3
    
    note over GW: Шаг 4: Генерация X-Request-ID (trace_id)<br/>Сборка payload с app_lang
    
    GW->>K: Шаг 5: Пуш в топик: bpds.inventory.in.receipt.image
    GW-->>App: Шаг 6: HTTP 202 Accepted (Шлюз свободен)
    deactivate GW

    %% ШАГ 2: OCR ОБРАБОТКА
    K->>OCR: Шаг 7: Handler: ProcessReceiptImage()
    activate OCR
    OCR->>S3: Шаг 8: GetObject: Скачивание бинарного файла по URL
    activate S3
    S3-->>OCR: Шаг 9: Бинарный графический поток
    deactivate S3
    
    note over OCR: Шаг 10: Инференс OCR-движка<br/>Построчное извлечение текста с фото чека
    
    OCR->>K: Шаг 11: Пуш в топик: bpds.inventory.in.receipt.upload
    deactivate OCR

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

Процесс загрузки фотографии чека, построчного OCR-распознавания и асинхронного цензурирования

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

Шаг Действие Параметры / Запросы / DTO Ошибки (Исключения / Статусы)
Шаг 1 (Gateway -> MinIO) API-шлюз принимает бинарный файл фотографии чека (.jpg или .png) со смартфона пользователя, проводит валидацию MIME-типа и загружает объект в S3. S3 API PUT Object Request:
Bucket: "receipt-images", Key: "uploads/2026/07/receipt_photo_101.png"
Headers: Content-Type: image/png, Content-Length: 2097152
MinIOException: BucketNotFound
MinIOException: InvalidDigest (повреждение файла при передаче)
StorageFullException
Шаг 2 (MinIO -> Gateway) Блочное объектное хранилище сохраняет изображение чека и возвращает API-шлюзу прямую внутреннюю URL-ссылку на объект. S3 API PUT Object Response:
HTTP Статус: 200 OK
ETag: "9b71d224bd62f3322d8d8a7c5cb9c111", URL: https://minio.internal
MinIOException: ConnectionTimeout
DiskWriteError (критический сбой RAID-массива хранилища)
Шаг 3 (Gateway -> T_Receipt) Шлюз генерирует задачу на OCR-распознавание, добавляет метаданные языка локали приложения, сквозной trace-id и отправляет сообщение в брокер Кафки. Kafka Message (Topic: bpds.inventory.in.receipt.image):
Payload: { "image_file_url": "https://minio.internal", "app_lang": "ru-RU", "x_request_id": "trace-ocr-censor-uuid" }
FastAPI.ValidationError (передан некорректный URL схемы)
KafkaException: QueueFullException
MessageTimedOut
Шаг 4 (T_Receipt -> ReceiptAI) Специализированный микросервис image-processor, подписанный на топик изображений чеков, вычитывает очередную задачу для извлечения символов. Kafka Consumer Poll Request:
Group_ID: "receipt-ocr-processors", Получен payload шага 3.
Partition: 1, Offset: 55410
KafkaException: CommitFailedException (зависание обработчика изображений чеков)
SerializationException
Шаг 5 (ReceiptAI -> MinIO) OCR-сервис обращается к объектному хранилищу по полученному URL-адресу для загрузки бинарной матрицы картинки в локальную память видеокарты/процессора. HTTP GET /receipt-images/uploads/2026/07/receipt_photo_101.png:
Headers: Internal Auth Tokens. На выходе: бинарный поток байт графического файла.
HTTP 404 Not Found (файл был удален из S3 до начала чтения воркером)
HTTP 403 Forbidden
ConnectTimeout
Шаг 6 (ReceiptAI -> T_Results) Локальный OCR-движок считывает текстовые строки с фото чека. Весь распознанный массив данных вместе с trace-id отправляется в общий топик загрузок. Kafka Message (Topic: bpds.inventory.in.receipt.upload):
Payload: { "raw_text_input": "ФН 9999 ИНН 12345 ТОВАР С КОРРЕКТНЫМ ИЛИ С ПРОВОКАЦИОННЫМ НАЗВАНИЕМ", "app_lang": "ru-RU", "x_request_id": "trace-ocr-censor-uuid" }
OCRError: ImageUnreadable (не удалось сегментировать текст или смазано фото)
KafkaException: DeliveryTimeout
Шаг 7 (T_Results -> Censor) Воркер цензуры считывает извлеченный с фотографии чека текст для проведения лингвистической и контекстной фильтрации. 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-ocr-censor-uuid" }
KafkaException: CommitFailedException
SerializationException (ошибка десериализации JSON после OCR-этапа)
Шаг 8 (Censor -> Ollama) Цензор вызывает локальный LLM-контейнер для проверки семантики распознанного текста чека на предмет скрытого спама, мата или оскорблений. HTTP POST /api/generate (Ollama API):
{ "model": "llama3", "prompt": "Analyze the following text from OCR receipt 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_789" 1
4. EXPIRE "abuse:counter:user_789" 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-ocr-censor-uuid" }
KafkaException: DeliveryTimeout
KafkaException: NotCoordinatorForException
Шаг 10б (Censor -> T_Draft) Ветка «Сомнение»: Если текст чека неразборчив, смазан или ИИ не уверен в контексте, воркер изолирует запись в топик черновиков продуктов. Kafka Message (Topic: bpds.mdm.in.product.process):
Payload: { "draft_data": { "raw_id": "uuid-ocr", "text": "Текст неразборчивого чека" }, "app_lang": "ru-RU", "x_request_id": "trace-ocr-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_789", "violation_count": 1, "app_lang": "ru-RU", "notification_type": "RECEIPT_PROFANITY_WARNING" }
KafkaException: BrokerNotAvailable
KafkaException: MessageTimedOut
Шаг 11 (T_Draft -> App) Из топика черновиков текст неразборчивого чека выводится на экран смартфона пользователя во вкладку ручной корректировки приложения. gRPC / HTTP Stream (Вкладка модерации):
Payload: { "draft_id": "uuid-draft-ocr", "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-ocr-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-ocr-censor-uuid" }
KafkaException: DeliveryTimeout
KafkaException: ConcurrentModificationException
Шаг 12б (Abuse_Check -> T_Notif) Повторный мат при модерации: Если в отредактированном черновике чека снова обнаружен мат, метод фиксирует Счетчик >= 2 и пушит сообщение для блокировки пользователя. Kafka Message (Topic: bpds.aid.out.profanity.violate):
Payload: { "user_id": "user_789", "violation_count": 2, "app_lang": "ru-RU", "notification_type": "RECEIPT_PROFANITY_BAN" }
KafkaException: LeaderNotAvailableException
InvalidTopicException

4 Примеры ответов сервера

4.1 Успешный асинхронный ответ (HTTP 202 Accepted)

{
  "status": "PROCESSING",
  "data": {
    "trace_id": "req-8c2a-4b9d-99ef",
    "message": "Файл успешно загружен. Запущен асинхронный контур ИИ-модерации и построчного OCR-чтения чека.",
    "processed_at": "2026-07-21T02:15:00Z"
  }
}

4.2 Исключение: Низкое качество фото чека / Размытый текст (Асинхронный Исход 10б)

Формируется воркером, если средний коэффициент уверенности распознавания символов движка OCR упал ниже 0.85.

{
  "error_code": "ERR-LOW-OCR-CONFIDENCE",
  "message": "Текст чека распознан частично из-за низкого качества фотографии. Требуется ручная модерация.",
  "details": {
    "ocr_score": 0.61,
    "receipt_snapshot_url": "s3://receipt-images/uuid-77.jpg",
    "action": "Render interactive draft receipt table on UI for manual correction fallback."
  }
}