> Как исправлять застрявшие платежи вручную при наличии реконсайл-джобы (Go)
Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы
Компании: InDrive
Стек: Go
> Пример ответа
Короткий ответ
Реконсайл-джоба автоматически исправляет застрявшие платежи, но бывают случаи, когда требуется ручное вмешательство: джоба не может разрешить конфликт, данные не сходятся, или есть подозрение на баг. Основной подход - сначала разобраться, почему джоба не справилась, затем либо скорректировать данные и перезапустить обработку, либо выполнить компенсирующее действие (refund, повторный charge) через админ-интерфейс или скрипт. Важно не дублировать логику джобы и не менять статусы напрямую в обход бизнес-правил.
Подробное объяснение
Реконсайл-джоба - это фоновый процесс, который сверяет локальные платежи с данными платежного провайдера и приводит их к консистентному состоянию. Она обрабатывает типовые кейсы: pending слишком долго, статус не совпадает, нет подтверждения от провайдера.
Ручное исправление нужно, когда:
- джоба не может определить корректный статус (например, провайдер вернул неоднозначный ответ);
- данные в БД повреждены или устарели (например, нет внешнего ID);
- джоба пропустила платеж из-за бага или ретраев;
- требуется бизнес-решение (например, форс-мажор, клиент звонит и говорит, что оплата прошла, а у нас pending).
Алгоритм ручного исправления:
- Найти платеж по ID или внешнему ID.
- Проверить текущий статус, историю переходов, логи джобы.
- Определить, почему джоба не обработала: ошибка, отсутствие данных, конфликт статусов.
- Исправить первопричину (обновить внешний ID, поправить данные провайдера).
- Выполнить повторный запуск реконсайла для этого платежа (через админ-эндпоинт или ручной триггер).
- Если повторный запуск не помогает - выполнить компенсирующее действие вручную: refund, завершить как failed, или вручную выставить confirmed, если есть подтверждение от провайдера.
Критично: ручное вмешательство должно логироваться, идемпотентно, и не должно конфликтовать с джобой (например, если джоба сейчас обрабатывает этот платеж - нужно подождать или заблокировать его).
На практике
В Go-сервисе обычно есть админ-эндпоинт или CLI-утилита для таких операций. Типичный flow:
POST /admin/payments/{id}/reconcile- принудительный запуск реконсайла для одного платежа;POST /admin/payments/{id}/force-status- установка статуса в обход джобы, но с проверкой прав и записью в audit log;- скрипт, который находит платежи в статусе
pendingстарше N часов и выводит причины, почему джоба их не обработала.
Важно: force-status не должен вызываться напрямую из БД (UPDATE ... SET status = 'confirmed'), потому что это ломает инварианты (например, не обновляется updated_at, не пишется событие, не отправляется вебхук). Лучше использовать сервисный слой с методами ConfirmPayment, FailPayment, RefundPayment.
Также стоит предусмотреть механизм блокировки: если джоба сейчас обрабатывает платеж, ручная операция должна либо подождать, либо отменить текущую обработку (через distributed lock или версионирование строки).
Пример кода
GOtype PaymentService struct {repo PaymentRepositoryreconciler Reconcileraudit AuditLogger}// ForceReconcile запускает реконсайл для конкретного платежа вручную.func (s *PaymentService) ForceReconcile(ctx context.Context, paymentID string) error {payment, err := s.repo.GetByID(ctx, paymentID)if err != nil {return fmt.Errorf("get payment: %w", err)}// Проверяем, что платеж не обрабатывается джобой прямо сейчас.if payment.LockedBy != "" {return ErrPaymentLocked}// Запускаем реконсайл через общий интерфейс - ту же логику, что и джоба.if err := s.reconciler.ReconcileOne(ctx, payment); err != nil {return fmt.Errorf("reconcile: %w", err)}s.audit.Log(ctx, "manual_reconcile", payment.ID, map[string]string{"triggered_by": "admin",})return nil}// ForceStatus устанавливает статус вручную, но через сервисный метод.func (s *PaymentService) ForceStatus(ctx context.Context, paymentID string, newStatus Status, reason string) error {payment, err := s.repo.GetByID(ctx, paymentID)if err != nil {return err}if !canTransition(payment.Status, newStatus) {return ErrInvalidTransition}// Вызываем доменный метод, а не пишем в БД напрямую.if err := payment.ChangeStatus(ctx, newStatus, reason); err != nil {return err}if err := s.repo.Save(ctx, payment); err != nil {return err}s.audit.Log(ctx, "manual_status_change", payment.ID, map[string]string{"from": string(payment.Status),"to": string(newStatus),"reason": reason,})return nil}
Как отвечать на собеседовании
Начни с того, что реконсайл-джоба - это основной механизм, и ручное вмешательство - исключение. Опиши процесс диагностики: сначала смотрим логи джобы, потом данные платежа, потом провайдера. Подчеркни, что нельзя менять статус напрямую в БД - только через сервисный слой. Упомяни идемпотентность и аудит. Если спросят про конкретный кейс - приведи пример: платеж завис в pending, джоба не может получить ответ от провайдера, мы вручную запускаем реконсайл через админ-эндпоинт, и если провайдер подтверждает - статус становится confirmed, иначе - failed.
Что проверяет интервьюер
- Понимание разницы между автоматической и ручной обработкой.
- Умение проектировать безопасные админ-операции (аудит, блокировки, идемпотентность).
- Знание доменных инвариантов платежной системы.
- Умение работать с конкурентностью (джоба и ручное действие одновременно).
- Понимание, когда ручное вмешательство оправдано, а когда это костыль.
Типичные ошибки
- Прямое обновление статуса через SQL - ломает события, вебхуки, аудит.
- Не проверять, обрабатывает ли джоба платеж в данный момент - гонка.
- Дублировать логику реконсайла вручную вместо вызова общего метода.
- Не логировать причину ручного изменения - потом невозможно разобраться.
- Считать, что ручное исправление - это норма, а не исключение; интервьюер ждет, что ты сначала попытаешься понять, почему джоба не сработала.
> Похожие задачи по Go
Как хранится история переходов статусов платежей?
Вы используете один кластер или геораспределенную систему?
Как следить за работой оркестратора и какие метрики использовать для проверки корректности
Как задаются стратегии для каждого платежа: через API или иначе
> Похожие задачи по backend
Как хранится история переходов статусов платежей?
Вы используете один кластер или геораспределенную систему?
Как следить за работой оркестратора и какие метрики использовать для проверки корректности
Как задаются стратегии для каждого платежа: через API или иначе
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью