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
Метод POST /api/v1/gateway/upload-receipt
Документация API: Добавление продуктов через готовый снимок с ИИ-модерацией
- IAD-MIGRATION-116 Backlog — Краткое Описание задачи 1.
- IAD-MIGRATION-117 Refinement (Уточнение) — Краткое Описание задачи 2.
- GW-2 Ready for Development — Подключение эндпоинта в шлюзе.
- AID-012 Ready for Development — создать метож ProcessVoiceStream.
- AID-013 Ready for Development — создать метож ProcessUploadText.
- INV-FRONTEND-222 Refinement (Уточнение) — Экран управления B2B-интеграциями
- INV-FRONTEND-223 Refinement (Уточнение) — Экран управления B2B-интеграциями
1 Функциональное назначение
Метод предназначен для асинхронного извлечения печатного текста с фотографии чека с помощью оптического распознавания символов (OCR).
В рамках архитектуры этот метод выполняет роль поставщика текстового контента чека (OCR Context Provider):
- Разгрузка сетевых потоков: Бинарный multipart-поток фотографии сбрасывается шлюзом в S3-бакет
MinIO, избавляя подсистему от зависания UI. - Построчное чтение текста: Тяжелый графический инференс вынесен в асинхронный воркер
image-processor. Его задача — отыскать символы, нормализовать строки и превратить массив пикселей в сырой текст чека. - Переиспользование контура фильтрации: На Шаге 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--
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: BucketNotFoundMinIOException: InvalidDigest (повреждение файла при передаче)StorageFullException |
Шаг 2 (MinIO -> Gateway) |
Блочное объектное хранилище сохраняет изображение чека и возвращает API-шлюзу прямую внутреннюю URL-ссылку на объект. | S3 API PUT Object Response: HTTP Статус: 200 OKETag: "9b71d224bd62f3322d8d8a7c5cb9c111", URL: https://minio.internal |
MinIOException: ConnectionTimeoutDiskWriteError (критический сбой 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: QueueFullExceptionMessageTimedOut |
Шаг 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 ForbiddenConnectTimeout |
Шаг 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.uploadPayload: { "raw_text_input": "Текст чека с нецензурными наименованиями", "app_lang": "ru-RU", "x_request_id": "trace-ocr-censor-uuid" } |
KafkaException: CommitFailedExceptionSerializationException (ошибка десериализации 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 unreachableHTTP 500 Internal Server ErrorReadTimeout: Inference duration exceeded |
Шаг 9 (Censor -> Redis) |
Параллельно выполняется проверка по строковым словарям мата для app_lang и атомарный инкремент счетчика инцидентов пользователя. |
Redis Command Pipeline:1. SISMEMBER "dict:profanity:ru-RU" "подозрительное_слово"2. MULTI3. INCRBY "abuse:counter:user_789" 14. EXPIRE "abuse:counter:user_789" 864005. EXEC |
Redis.ConnectionError: Connection refusedRedis.TimeoutError: Command timed outRedis.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: DeliveryTimeoutKafkaException: 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: LeaderNotAvailableExceptionKafkaException: 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: BrokerNotAvailableKafkaException: MessageTimedOut |
Шаг 11 (T_Draft -> App) |
Из топика черновиков текст неразборчивого чека выводится на экран смартфона пользователя во вкладку ручной корректировки приложения. | gRPC / HTTP Stream (Вкладка модерации):Payload: { "draft_id": "uuid-draft-ocr", "raw_text_input": "Текст неразборчивого чека", "app_lang": "ru-RU" } |
DioException: connection errorHTTP 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: QueueFullExceptionFastAPI.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: DeliveryTimeoutKafkaException: 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: LeaderNotAvailableExceptionInvalidTopicException |
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."
}
}