← Назад к вопросам

Куда отправлять клиента после оплаты заказа если нельзя выполнять команды в CQRS?

1.8 Middle🔥 162 комментариев
#JavaScript Core

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI4 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отличный и очень практичный вопрос, который затрагивает архитектурные паттерны и их правильное применение в реальных проектах.

Если в системе строго запрещено выполнять команды (Commands) в контексте запросов (Query), что является одним из фундаментальных принципов CQRS, то прямой вызов команды CreateOrder или ConfirmPayment из обработчика успешной оплаты — это нарушение архитектуры. Нам нужно отделить инициацию бизнес-процесса (оплата) от его выполнения внутри системы.

В этом случае, после успешной оплаты на стороне платежного шлюза, клиента следует перенаправить не на команду, а на специальный endpoint, который является частью модели запросов (Query-side). Этот endpoint запустит асинхронный процесс обработки оплаты.

Вот развернутый и практический ответ, как это реализовать.

Стратегия перенаправления и обработки в CQRS

Основная идея: платежный шлюз (внешняя система) отправляет вебхук или пользователь возвращается на наш URL успеха. Наша задача — принять факт оплаты, записать его как событие или команду в очередь, и немедленно показать пользователю статус "в обработке".

1. Создание Endpoint'а для приема подтверждения оплаты

Это не команда, а обычный HTTP-метод в вашем контроллере (например, PaymentController). Его задача:

  • Верифицировать уведомление от платежки (подпись, идентификатор).
  • Создать и отправить команду в очередь (Message Bus/Queue).
  • Немедленно ответить клиенту, не дожидаясь обработки бизнес-логики.
// Пример на TypeScript + Node.js (концептуально)
import { Request, Response } from 'express';
import { commandBus } from '../infrastructure/command-bus';
import { VerifyPaymentNotification } from '../payment/verify-payment'; // Валидатор
import { ConfirmPaymentCommand } from '../order/commands/confirm-payment.command';

export class PaymentController {
    async onPaymentSuccess(req: Request, res: Response): Promise<void> {
        // 1. Верификация данных от платежного шлюза
        const isValid = await new VerifyPaymentNotification().verify(req.body);
        if (!isValid) {
            res.sendStatus(400);
            return;
        }

        // 2. Извлечение ID заказа из платежных данных (обычно передается в metadata)
        const orderId = req.body.metadata.orderId;
        const paymentId = req.body.id;

        // 3. СОЗДАНИЕ КОМАНДЫ и отправка ее в шину команд.
        // Важно: мы не выполняем команду здесь и сейчас, а лишь ставим в очередь.
        const confirmPaymentCommand = new ConfirmPaymentCommand(orderId, paymentId);
        commandBus.publish(confirmPaymentCommand); // Асинхронная операция

        // 4. Немедленный ответ клиенту или платежному шлюзу.
        // Для платежного шлюза — часто просто 200 OK.
        // Для браузера клиента — редирект на страницу статуса заказа.
        res.redirect(`/order/${orderId}/status?processing=true`);
    }
}

2. Страница статуса заказа (Query-side)

Клиент попадает на страницу, которая принадлежит к модели запросов. Она:

  • Не меняет состояние.
  • Опрашивает (через API или из Read Database) актуальный статус заказа.
  • Может использовать Server-Sent Events (SSE) или WebSocket для получения обновлений в реальном времени, когда фоновая команда ConfirmPaymentCommand будет обработана и состояние заказа обновится.
// Пример фронтенд-кода (React) для страницы статуса
import { useEffect, useState } from 'react';
import { useOrderStatus } from '../api/order-queries';

function OrderStatusPage({ orderId }) {
    const { data: order, isLoading } = useOrderStatus(orderId);
    const [pollingInterval, setPollingInterval] = useState(2000);

    useEffect(() => {
        if (order?.status === 'PAID' || order?.status === 'FAILED') {
            // Остановить опрос при конечном статусе
            setPollingInterval(0);
        }
    }, [order?.status]);

    if (isLoading) return <div>Получаем информацию о заказе...</div>;

    return (
        <div>
            <h2>Статус вашего заказа #{orderId}</h2>
            <p><strong>Текущий статус:</strong> {translateStatus(order.status)}</p>
            {order.status === 'PROCESSING' && (
                <p>✅ Оплата получена. Обрабатываем ваш заказ...</p>
            )}
        </div>
    );
}

3. Архитектурная схема потока данных

[Платежный шлюз] --(вебхук/редирект)--> [Наш Endpoint (Query-side Controller)]
                                                  |
                                                  v (публикация, не выполнение)
                                          [Шина Команд (Command Bus)]
                                                  |
                                                  v (обработка в фоне)
                                  [Обработчик Команды (Command Handler)]
                                                  |
                                                  v (выполнение бизнес-логики)
                                  [Обновление записи в Write Database]
                                                  |
                                                  v (синхронизация, если нужно)
                                  [Проекция в Read Database (Denormalized View)]
                                                  |
                                                  v (опрос)
                                  [Страница статуса заказа (Query-side UI)]

Ключевые выводы и почему это правильно

  • Соблюдение CQRS: Команда ConfirmPaymentCommand инициируется асинхронно, но выполняется в своем контексте, отделенном от слоя запросов. Контроллер, принимающий платеж, не знает бизнес-логики заказа.
  • Отказоустойчивость: Даже если обработка команды временно не работает (ошибка в базе, зависание сервиса), факт оплаты уже сохранен в надежной очереди (например, RabbitMQ, Kafka, AWS SQS) и будет обработан при восстановлении. Пользователь уже получил положительный ответ.
  • Производительность: HTTP-ответ клиенту или платежному шлюзу происходит мгновенно, так как не требует длительной бизнес-логики. Это критически важно для юзабилити и интеграций.
  • Масштабируемость: Обработчики команд могут работать в отдельном пуле сервисов, масштабироваться независимо от веб-серверов, обслуживающих запросы.
  • Четкий пользовательский опыт: Клиент видит понятный интерфейс: "Оплата прошла → Заказ обрабатывается → Заказ готов". Не возникает ситуации "страница зависла, непонятно, что происходит".

Таким образом, правильный путь — это редирект на страницу статуса (Query) с параллельной асинхронной публикацией команды в шину. Это классический и надежный подход для систем, построенных на чистых принципах CQRS с разделением команд и запросов.

Куда отправлять клиента после оплаты заказа если нельзя выполнять команды в CQRS? | PrepBro