Skip to content

Repository files navigation

GoZon — минималистичный магазин

Локальный repo с четырьмя приложениями (API Gateway, Orders, Payments, Frontend) и инфраструктурой (Kafka + Zookeeper, два PostgreSQL). Основная цель — показать, как простые outbox/inbox-воркеры, идемпотентность и атомарные SQL-операции дают effectively exactly-once семантику без тяжёлых фреймворков.

Структура

  • api-gateway/src/ApiGateway — минимальный proxy (Minimal API + HttpClient).
  • order-service/src/OrderService.Api + OrderService.Infrastructure — Web API, Swagger, EF Core, outbox, consumer результатов оплат.
  • payments-service/src/PaymentsService.Api + PaymentsService.Infrastructure — счета, атомарные списания, inbox/outbox, Kafka consumer/producer.
  • frontend — Vite + React SPA, nginx проксирует /api на gateway.
  • docker-compose.yml — Kafka, Zookeeper, 2×Postgres, все сервисы и фронт.
  • tests/GoZon.Tests — пара unit-тестов на маппинг статусов/сообщений.

Запуск

  1. Установите Docker и Docker Compose.
  2. Выполните docker compose up --build.
  3. Через ~1–2 минуты сервисы будут доступны:

Основные запросы (через API Gateway)

Маршрут Метод Назначение
/api/payments/accounts POST Создать счёт для user_id (409, если уже есть).
/api/payments/accounts/{userId}/topup POST Пополнение, внутри UPDATE ... WHERE balance >=.
/api/payments/accounts/{userId}/balance GET Узнать баланс и updated_at.
/api/orders POST Создать заказ → в транзакции orders + outbox (order.payment.requests).
/api/orders GET Список заказов, фильтр user_id через query.
/api/orders/{orderId} GET Текущий статус (NEW/FINISHED/CANCELLED) + failureReason.

Фронтенд вызывает только эти маршруты (через nginx-прокси /api → gateway). Внутренние сервисы напрямую извне недоступны.

Swagger и ручное тестирование

  • Payments: http://localhost:55001/swagger — создайте счёт (POST /api/accounts), затем POST /api/accounts/{userId}/topup, затем GET /api/accounts/{userId}/balance.
  • Orders: http://localhost:55000/swaggerPOST /api/orders запускает оплату; GET /api/orders/{orderId} позволяет следить за статусом.
  • API Gateway (http://localhost:58080/health) покажет суммарное состояние Orders/Payments.

Фронтенд

SPA на http://localhost:53000 разделена на два блокa: управление счётом (создание, пополнение, просмотр баланса) и создание заказа. После отправки заказа клиент автоматически poll-ит /api/orders/{id} (через gateway) каждую секунду в течение 30 секунд и отображает итоговый статус/причину. Для локальной разработки npm install && npm run dev (Vite проксирует /api на http://localhost:58080).

Архитектура и надежность (Outbox/Inbox)

Проект реализует Effectively Exactly-Once семантику обработки сообщений с помощью паттернов Outbox и Inbox и идемпотентных операций.

Схема взаимодействия

sequenceDiagram
    participant C as Client (Frontend)
    participant O as Order Service
    participant K as Kafka
    participant P as Payments Service

    C->>O: POST /api/orders (Создать заказ)
    Note over O: DB Transaction Start
    O->>O: Сохранить Order (Status: NEW)
    O->>O: Записать Outbox (order.payment.requests)
    Note over O: DB Transaction Commit
    O-->>C: 201 Created (Order ID)

    loop Outbox Dispatcher
        O->>O: Найти не отправленные Outbox сообщения
        O->>K: Опубликовать в Kafka (order.payment.requests)
        O->>O: Пометить Outbox как отправленный
    end

    loop Payment Consumer
        K->>P: Сообщение: Списать X у User Y
        Note over P: DB Transaction Start
        P->>P: Записать Inbox (message_id) - Защита от дублей
        P->>P: Атомарный UPDATE Account (WHERE balance >= X)
        P->>P: Записать Payment (order_id UNIQUE) - Защита от double-charge
        P->>P: Записать Outbox (order.payment.results)
        Note over P: DB Transaction Commit
        P->>K: Подтвердить смещение (Manual Commit Offset)
    end

    loop Payment Result Consumer
        K->>O: Сообщение: Оплата OK/Fail
        Note over O: DB Transaction Start
        O->>O: Записать Inbox (message_id)
        O->>O: Обновить Order Status (FINISHED/CANCELLED)
        Note over O: DB Transaction Commit
    end

    C->>O: GET /api/orders/{id} (Polling статуса)
    O-->>C: Статус заказа
Loading

Ключевые механизмы:

  1. Transactional Outbox: Каждое изменение в базе (создание заказа, списание денег) происходит одновременно с записью события в таблицу outbox в рамках одной SQL-транзакции. Это гарантирует, что мы никогда не забудем уведомить другие сервисы.
  2. Inbox Pattern (Deduplication): Каждый консьюмер сначала записывает message_id в таблицу inbox. Если придет дубликат сообщения из Kafka, вставка в inbox упадет по уникальному ключу, и сообщение будет проигнорировано.
  3. Атомарные списания: В PaymentsService используется сырой SQL UPDATE accounts SET balance = balance - @amount WHERE user_id = @userId AND balance >= @amount RETURNING. Это предотвращает "овердрафт" (уход в минус) даже при параллельных запросах.
  4. Идемпотентность БД: Таблица payments имеет уникальный ключ на order_id. Это гарантирует, что даже при сбое логики мы никогда не спишем деньги за один и тот же заказ дважды.
  5. Manual Ack: Мы подтверждаем прочтение сообщения в Kafka (Commit()) только после того, как транзакция в базе успешно завершена.

Тесты

dotnet test

Покрывают маппинг статусов в Orders и формирование ответных сообщений Payments (reason/status), что является ключевой частью идемпотентности.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages