Куда отправлять клиента после оплаты заказа если нельзя выполнять команды в CQRS?
Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Отличный и очень практичный вопрос, который затрагивает архитектурные паттерны и их правильное применение в реальных проектах.
Если в системе строго запрещено выполнять команды (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 с разделением команд и запросов.