graph TD
subgraph Transport_Layer ["1. Edge & Transport Layer (Входные контракты)"]
App["Мобильный Клиент / Стрим"]
Partner["B2B Ритейлер / Магнум"]
gRPC_Srv["grpc.ReceiptServer (Handlers)"]
end
subgraph Core_Layer_Step1 ["2. Service Core (Инициация транзакции)"]
KClient["kafka.Client (franz-go Producer)"]
S3Client["minio.Client (MinIO SDK)"]
Repo["postgres.ReceiptRepository (pgxpool)"]
end
subgraph Storage_Layer_Step1 ["3. Storage & Persistence (Первичный сброс)"]
MinIO["MinIO S3 Object Storage"]
Postgres["PostgreSQL DB"]
T_Img["topic: receipt-images-topic"]
T_Audio["topic: receipt-audio-topic"]
T_Clean["topic: receipt-clean-products-topic"]
end
subgraph External_Layer_Step1 ["4. External Dependencies"]
AuthSvc["auth-service (ValidateToken)"]
end
App -->|gRPC клиентский стрим| gRPC_Srv
Partner -->|gRPC контракт| gRPC_Srv
gRPC_Srv -->|Валидация сессии B2C| AuthSvc
gRPC_Srv -->|Сохранение чанков медиа-файла| S3Client
S3Client -->|PutObject| MinIO
gRPC_Srv -->|Фиксация accepted транзакции| Repo
Repo --> Postgres
gRPC_Srv -->|Инициирующий пуш чека| KClient
gRPC_Srv -->|Инициирующий пуш аудио| KClient
KClient -->|ProduceSync| T_Img
KClient -->|ProduceSync| T_Audio
gRPC_Srv -.->|B2B прямой пуш минуя ИИ| T_Clean
style gRPC_Srv fill:#2B6CB0,stroke:#fff,stroke-width:2px,color:#fff
style MinIO fill:#10B981,stroke:#fff,stroke-width:2px,color:#fff
style Postgres fill:#D69E2E,stroke:#fff,stroke-width:2px,color:#fff
style T_Img fill:#E53E3E,stroke:#fff,stroke-width:1px,color:#fff
style T_Audio fill:#E53E3E,stroke:#fff,stroke-width:1px,color:#fff
Microservice: Сервис receipt-processor многоканальный асинхронный шлюз
1 Общее назначение и архитектурная роль компонента
Компонент receipt-processor функционирует как многоканальный асинхронный шлюз (Ingestion Transport Gateway & Router) в Core Backend Layer. Его основная задача — изолировать синхронный транспортный gRPC-слой мобильных клиентов и B2B-партнеров от «тяжелых» ИИ-вычислений. Сервис принимает потоковое медиа (картинки, аудио), сохраняет его в объектное хранилище MinIO, регистрирует транзакции в PostgreSQL и асинхронно распределяет задачи по топикам Kafka для последующего анализа ИИ-моделями.
2 Схема архитектуры компонента (Входной контур: Шаг 1)
3 Системная спецификация: Ingestion API Gateway (Write Path)
- Изоляция и сборка gRPC-стримов: Сервис реализует неблокирующий прием бинарных данных через механизмы клиентского стриминга (
stream.Recv). Кусочки файлов (чанки) собираются в оперативной памяти (bytes.Buffer) и атомарно отгружаются в объектное хранилище MinIO S3 с MIME-типомimage/jpegили дефолтным аудио-форматом. Это разгружает дисковую подсистему и обеспечивает стабильный p99 времени ответа для мобильных приложений. - Внешняя gRPC-аутентификация (B2C): Перед началом сборки стрима интерцептор извлекает токен
access_tokenиз входящего gRPC-контекста метаданных (metadata.FromIncomingContext) и выполняет синхронный RPC-вызов вauth-service. Успешный ответ обогащает контекст идентификатором домашней группы пользователя (GroupId), который пробрасывается дальше по всему пайплайну в структуреmodel.KafkaMessage. - Прямой B2B-контур (Паттерн Reply-To): Для крупных ритейл-партнеров (например, торговой сети «Магнум») gRPC-сервер предоставляет выделенный метод
RegisterStoreReceipt. Чек, уже распознанный на стороне кассы магазина, минует ресурсоемкие фазы ИИ-анализа (OCR и Whisper) и мгновенно публикуется напрямую в топикreceipt-clean-products-topic. При этом в заголовки Kafka-сообщения принудительно внедряются метаданныеreply_to="store-callbacks-topic"и уникальныйcorrelation_idдля асинхронного трекинга.
4 Спецификация Входных gRPC-интерфейсов (Транспортный контракт)
Транспортный слой обрабатывает данные в рамках спецификации ReceiptProcessorService. При возникновении инфраструктурных сбоев (например, падение auth-service или MinIO S3) или ошибок валидации, внутренние исключения Go преобразуются в канонические коды ошибок пакета google.golang.org/grpc/codes.
4.1 Метод UploadReceipt (Клиентский стрим графического чека)
Принимает бинарные чанки фотографии чека, загружает собранный файл в MinIO и отправляет событие-триггер в Kafka.
- Входные параметры (stream pb.UploadReceiptRequest): Поток чанков, содержащий структуру
oneof Data(Metadataс указанием расширения файла илиChunkс массивом байт). Если стрим пустой, сервер возвращает статусcodes.InvalidArgument. - Выходные параметры (pb.UploadReceiptResponse): После успешной публикации метаданных в брокер возвращается статус
"processing"и сгенерированный уникальный строковый идентификаторreceipt_id(с префиксомBILL-).
4.2 Метод UploadAudio (Клиентский стрим голосовой команды)
Принимает чанки аудиозаписи речи пользователя, добавляющего продукты голосом, и отправляет метаданные в топик receipt-audio-topic.
- Входные параметры (stream pb.UploadAudioRequest): Поток данных аудио-файла. При невалидном или отсутствующем токене в контексте метаданных gRPC возвращает статус
codes.Unauthenticated. - Выходные параметры (pb.UploadAudioResponse): Возвращает статус
"processing"и сгенерированный идентификаторaudio_id(с префиксомAUDIO-).
4.3 Метод RegisterStoreReceipt (B2B Импорт чеков)
Принимает готовый структурированный состав позиций от внешних торговых сетей по токену авторизации.
- Входные параметры (pb.StoreReceiptRequest): Содержит уникальный токен магазина
StoreToken, номер чекаStoreReceiptNo, массив позицийItemsи идентификатор целевой группыTargetGroupId. При невалидном токене возвращает статусcodes.PermissionDenied.
5 Сценарий взаимодействия при публикации команд (Sequence Diagram)
Ниже представлена диаграмма сквозного жизненного цикла чеков и голосовых сообщений. Схема охватывает потоковую gRPC-загрузку через интерцепторы, неблокирующее хранение в MinIO S3, оркестрацию в ReceiptHandler, многоэтапный ИИ-анализ (Whisper, Llama Vision, Qwen) с фильтрацией аномалий и асинхронный обратный вызов партнеров по паттерну Reply-To.
%%{init: {
'theme': 'base',
'themeVariables': {
'actorBkg': '#E3F2FD',
'actorBorder': '#546E7A',
'actorTextColor': '#0D47A1',
'rectBkg': '#FFF9C4',
'rectBorder': '#FBC02D',
'noteBkgColor': '#F3E5F5',
'noteBorderColor': '#7E57C2',
'noteTextColor': '#311B92',
'signalColor': '#2C3E50',
'signalLineColor': '#2C3E50'
}
}}%%
sequenceDiagram
autonumber
actor Client as Внешний Клиент / Магнум
participant Server as gRPC ReceiptServer
participant KC as kafka.Client (franz-go)
participant H as ReceiptHandler (Router)
participant S3 as minio.Client (S3 Storage)
participant AI as ai.Client (Ollama/Whisper)
participant DB as postgres.ReceiptRepository
%% ВЕТКА 1: Загрузка файлов (B2C)
Client->>Server: Стриминг чанков gRPC (UploadReceipt / UploadAudio) с access_token
activate Server
note over Server: Шаг 2: Валидация access_token через auth-service.<br/>Извлечение GroupID сессии.
Server->>S3: UploadFile(bucket, filename, payloadBytes)
S3-->>Server: Возврат s3Path
Server->>KC: PublishEvent(topic, jsonBytes) [model.KafkaMessage]
KC-->>Server: Сетевой ACK подтвержден
Server-->>Client: Срочный ответ (Status: "processing", ID)
deactivate Server
%% ВЕТКА 2: Асинхронный Поллинг очередей (Воркеры)
activate KC
KC->>KC: PollFetches(ctx) из бесконечного цикла Start()
KC->>H: Вызов ProcessMessage(ctx, topic, value)
activate H
alt Топик == "receipt-images-topic" ( Фаза 1)
H->>S3: DownloadFile(bucket, s3Link)
S3-->>H: imgBytes (image/jpeg)
note over H: base64.StdEncoding.EncodeToString(imgBytes)
H->>AI: ExtractEntitiesFromImage(ctx, b64Img) [Llama Vision:11435]
AI-->>H: rawJsonProducts
note over H: Шаг 13: Обогащение промежуточной структуры group_id
H->>KC: PublishEvent("receipt-raw-products-topic", rawJsonProducts)
else Топик == "receipt-audio-topic" (Воркер 3)
H->>S3: DownloadFile(bucket, s3Link)
S3-->>H: audioBytes
H->>AI: RecognizeSpeech(ctx, audioBytes) [Whisper:11436]
AI-->>H: speechText
H->>AI: ConvertSpeechToProductsJSON(ctx, speechText) [Qwen:11437]
AI-->>H: aiJsonBytes (С семантическим флагом validationCheck.Status)
alt ИИ-Статус == "invalid" ( Dead Letter Path)
H->>KC: PublishEvent("receipt-moderation-topic", aiJsonBytes)
note over KC: Перенаправление в voice_moderation_queue
else ИИ-Статус == "valid"
H->>KC: PublishEvent("receipt-raw-products-topic", aiJsonBytes)
end
else Топик == "receipt-raw-products-topic" ( Фаза 2)
H->>AI: FilterProfanityAndSpam(ctx, value) [Qwen:11438]
AI-->>H: cleanJsonProducts (Мат заменен на '*', спам удален)
H->>KC: PublishEvent("receipt-clean-products-topic", cleanJsonProducts)
end
deactivate H
deactivate KC
%% ВЕТКА 3: Финализация и Обратный вызов (B2B Webhooks)
activate KC
KC->>KC: PollFetches в StoreCallbackWorker ("store-callbacks-topic")
KC->>DB: GetWebhookURLByCorrelationID(ctx, correlationID)
DB-->>KC: WebhookURL ритейлера + Номер чека
KC->>Client: HTTP POST Webhook (clean payload + status: "processed")
note over Client: Жесткий таймаут ожидания ответа — 10 секунд
Client-->>KC: HTTP Status 200 OK
KC->>DB: UpdateTransactionStatus(ctx, correlationID, "processed", "")
deactivate KC
5.1 Таблица расшифровки шагов сценария асинхронного логирования команд
| Шаг | Действие | Параметры / Запросы / DTO | Код ошибки (canonical_code) |
HTTP / gRPC статус |
|---|---|---|---|---|
| 1-2 | Клиент инициирует gRPC-стрим, передавая токен в заголовках метаданных. | metadata.MD: {"access_token": ["jwt"]} |
METADATA_MISSING |
codes.Unauthenticated (401) |
| 3-4 | Потоковая сборка файла в буфер и неблокирующая отправка в объектное хранилище. | minio.PutObject(ctx, "receipts", fileName, reader) |
MINIO_UPLOAD_FAILURE |
codes.Internal (500) |
| 5 | Публикация инициирующего сообщения с метаданными во входные очереди. | model.KafkaMessage JSON-строка |
KAFKA_PRODUCE_SYNC_ERR |
codes.Internal (500) |
| 8-9 | Воркер вычитывает картинку и запрашивает бинарный объект из S3 без удержания блокировок БД. | s3Client.DownloadFile(ctx, bucket, path) |
MINIO_DOWNLOAD_FAILURE |
Внутренний перезапуск |
| 11 | Вызов мультимодального OCR-слоя инференса LLM на выделенном Ubuntu-сервере. | PromptRequest: {model: "llama3.2-vision:11b", images: [b64]} |
AI_INFERENCE_TIMEOUT |
Внутренний перезапуск |
| 15-16 | STT-транскрибация голоса и последующий семантический парсинг полноты данных. | RecognizeSpeech(ctx, audioBytes) [Whisper] |
WHISPER_STT_CRASH |
Внутренний перезапуск |
| 18 | Маршрутизация аномалий: ИИ выявил нехватку метрик (нет веса/количества). Пуш в DLQ топик. | validationCheck.Status == "invalid" |
VOICE_METRIC_INVALID |
DLQ_ROUTING (Лог) |
| 21-22 | Вызов SLM Qwen для санитизации контента, удаления спама и посимвольного маскирования мата. | FilterProfanityAndSpam(ctx, rawJson) [Qwen] |
AI_MODEL_UNAVAILABLE |
Внутренний перезапуск |
| 26-28 | Поллинг результатов, поиск партнера по correlation_id в БД и отправка уведомления. | http.MethodPost к WebhookURL с таймаутом 10с. |
STORE_WEBHOOK_TIMEOUT |
HTTP 504 / 500 Error |
| 29 | Фиксация успешного завершения бизнес-транзакции в локальной СУБД. | UpdateTransactionStatus(ctx, corrID, "processed", "") |
POSTGRES_EXEC_FAILURE |
ClickHouse Alerting |
6 Справочник Обсервабилити (Структура логов Producer/Consumer-контура для ClickHouse)
Каждая строка лога, сбрасываемая компонентами ReceiptServer, ReceiptHandler и StoreCallbackWorker в поток os.Stdout, представляет собой структурированную запись в формате JSON Lines (Structured Logging через slog).
Сборщик Vector в реальном времени перехватывает данный поток, выполняет парсинг и осуществляет атомарную отгрузку пакетов в аналитическую СУБД ClickHouse DBMS. Эти данные служат базовым источником (Event Logs) для восстановления сквозных цепочек поставок и анализа узких мест бизнес-процессов с помощью аналитического движка PM4Py (Process Mining Engine).
При возникновении штатных событий или аварийных инфраструктурных инцидентов в ClickHouse фиксируются следующие типизированные метрики и метаданные:
1. Код (canonical_code) |
2. Уровень (log_level) |
3. Статусы (transport_statuses) |
4. Получатель (error_target) |
5. JSON для Фронтенда (ui_payload) |
6. Метрики для ClickHouse (observability_json) |
|---|---|---|---|---|---|
METADATA_MISSING |
WARN |
{"grpc": 16, "http": 401} |
EXTERNAL_SYSTEM |
{"error": "Авторизационные метаданные gRPC отсутствуют"} |
{"metric": "grpc_metadata_err", "labels": {"layer": "transport"}} |
AUTH_SERVICE_UNREACHABLE |
ERROR |
{"grpc": 13, "auth_alive": 0} |
INTERNAL_SYSTEM |
{"error": "Не удалось связаться с сервисом авторизации"} |
{"metric": "auth_rpc_fail", "labels": {"client": "auth_service"}} |
UNAUTHORIZED_STORE_TOKEN |
WARN |
{"grpc": 7, "http": 403} |
EXTERNAL_SYSTEM |
{"error": "Отказано в доступе: невалидный токен ритейлера"} |
{"metric": "b2b_auth_fail", "labels": {"handler": "store_grpc"}} |
POSTGRES_EXEC_FAILURE |
ERROR |
{"grpc": 13, "db_connected": 1} |
INTERNAL_SYSTEM |
{"error": "Ошибка сохранения данных транзакции в СУБД"} |
{"metric": "db_write_err", "labels": {"table": "store_transactions"}} |
KAFKA_POLL_FETCH_FAILURE |
ERROR |
{"kafka_connected": 0} |
INFRASTRUCTURE |
null |
{"metric": "kafka_fetch_fail", "labels": {"client": "franz_go"}} |
MINIO_DOWNLOAD_FAILURE |
ERROR |
{"s3_status": 404} |
INFRASTRUCTURE |
null |
{"metric": "s3_not_found", "labels": {"bucket": "receipts"}} |
WHISPER_STT_CRASH |
WARN |
{"grpc": 14, "gpu_util": 0} |
INTERNAL_SYSTEM |
null |
{"metric": "ai_inference_fail", "labels": {"engine": "whisper"}} |
OLLAMA_VISION_TIMEOUT |
WARN |
{"grpc": 14, "llama_status": 0, "duration_ms": 45000} |
INTERNAL_SYSTEM |
null |
{"metric": "ai_timeout", "labels": {"model": "llama3.2_vision"}} |
VOICE_METRIC_INVALID |
INFO |
{"kafka_produced": 1, "dlq": 1} |
EXTERNAL_SYSTEM |
{"warn": "Недостаточно данных в голосе, команда отправлена на модерацию"} |
{"metric": "dlq_routing", "labels": {"reason": "missing_weight"}} |
STORE_WEBHOOK_TIMEOUT |
ERROR |
{"http_status": 504, "webhook_ack": 0} |
EXTERNAL_SYSTEM |
{"error": "Превышено время ожидания ответа сервера ритейлера"} |
{"metric": "partner_webhook_timeout", "labels": {"tx": "outbound"}} |
PARTNER_WEBHOOK_BAD_STATUS |
ERROR |
{"http_status": 500, "webhook_ack": 0} |
EXTERNAL_SYSTEM |
{"error": "Сервер партнера вернул внутреннюю ошибку"} |
{"metric": "webhook_http_err", "labels": {"partner": "magnum"}} |
6.1 Регламент разбора инцидентов и аномалий ИИ-слоя
- Многоканальный аудит по ключам трассировки: Поле
observability_jsonпринудительно обогащается сквозными идентификаторамиrequest_id(для B2C gRPC-стримов) илиcorrelation_id(для B2B партнерских вебхуков). При возникновении аномалииVOICE_METRIC_INVALIDаналитик выполняет поиск в ClickHouse поaudio_id, восстанавливая исходный текст (speechText) и причину отбраковки моделью Qwen (moderation_reason). - Сегрегация зон ответственности (
error_target):- Инциденты с маркером
INFRASTRUCTUREсигнализируют о деградации внутренних кластеров (Kafka, MinIO) и требуют немедленного реагирования DevOps-команды. - Ошибки
EXTERNAL_SYSTEMфиксируют недоступность или некорректное поведение интеграционных шлюзов третьих сторон (серверы «Магнум», клиентские gRPC-таймауты) и переводятся на уровень клиентской поддержки.
- Инциденты с маркером