Дублирование заказов в WooCommerce почти всегда выглядит одинаково: клиент оплатил один раз, а в админке появляется два заказа, иногда с одинаковой суммой и разными статусами. На стороне магазина это быстро превращается в путаницу с отгрузкой, возвратами и сверкой оплат. Хорошая новость в том, что причина обычно не одна, и её можно локализовать по симптомам.
Ниже разберём именно практический сценарий: как понять, где появляется дубль — в форме оформления, на стороне платёжного шлюза, из-за кэша или из-за повторной обработки вебхука — и что менять в коде и настройках.
Когда проблема действительно в дублях, а не в статусах
Сначала стоит отделить настоящий дубль заказа от нормального поведения шлюза. Некоторые платёжные системы сначала создают заказ со статусом pending, а после подтверждения меняют его на processing или completed. Это не дубль. Дубль — это два разных заказа с разными ID.
Что проверить в админке
- Одинаковая ли сумма и состав корзины у двух заказов.
- Создаются ли оба заказа в одну минуту.
- Есть ли в заказах одинаковый email, телефон и способ оплаты.
- Меняется ли статус у первого заказа после оплаты, или появляется второй отдельный заказ.
Если второй заказ создаётся после возврата клиента на сайт, проблема чаще в повторной отправке формы или в некорректной обработке редиректа. Если дубль приходит позже, уже после успешной оплаты, чаще виноват вебхук или повторный callback от шлюза.
Диагностика: где именно рождается второй заказ
Самый быстрый путь — посмотреть логи WooCommerce и логи платёжного шлюза. В WooCommerce → Статус → Журналы обычно можно выбрать лог конкретного метода оплаты, если он его пишет. Параллельно полезно включить логирование в самом шлюзе, если это предусмотрено.
Дальше проверьте три точки:
- Создаётся ли заказ сразу после нажатия кнопки оплаты.
- Появляется ли второй заказ после возврата с платёжной страницы.
- Приходит ли повторный webhook/callback от провайдера.
Если у вас есть доступ к серверным логам, ищите повторные POST-запросы на wc-api, /?wc-api= или endpoint конкретного шлюза. Для REST-ориентированных интеграций смотрите повторные запросы к /wp-json/.
Минимальная проверка на стороне кода
Иногда полезно временно добавить запись в лог при создании заказа. Это помогает понять, сколько раз реально срабатывает создание заказа.
add_action( 'woocommerce_checkout_order_processed', function( $order_id, $posted_data, $order ) {
if ( function_exists( 'wc_get_logger' ) ) {
$logger = wc_get_logger();
$logger->info(
'Order processed: ' . $order_id . ' | ' . wp_json_encode( array(
'email' => $order ? $order->get_billing_email() : '',
'total' => $order ? $order->get_total() : '',
) ),
array( 'source' => 'order-dedup-debug' )
);
}
}, 10, 3 );Если этот хук срабатывает дважды на один и тот же checkout, проблема на стороне оформления или повторной отправки запроса. Если хук срабатывает один раз, а дубль появляется позже, ищите повторный callback от шлюза.
Пошаговое решение
1. Защитите создание заказа от повторной отправки формы
Пользователь может дважды нажать кнопку оплаты, особенно если страница подвисла или шлюз отвечает медленно. В этом случае нужно не только отключать кнопку на фронтенде, но и проверять уникальность на сервере.
Один из рабочих вариантов — сохранять в сессию или мета заказа временный ключ попытки оплаты и не создавать новый заказ, если такой ключ уже использован. Для простого сценария можно использовать nonce и блокировку по корзине/сессии, но важно помнить: nonce не защищает от повторной отправки сам по себе, он лишь проверяет валидность запроса.
add_action( 'woocommerce_checkout_process', function() {
if ( ! WC()->session ) {
return;
}
$lock = WC()->session->get( 'checkout_submit_lock' );
$now = time();
if ( $lock && ( $now - (int) $lock ) < 20 ) {
wc_add_notice( 'Платёж уже отправлен. Подождите несколько секунд и не обновляйте страницу.', 'error' );
return;
}
WC()->session->set( 'checkout_submit_lock', $now );
} );Это не универсальная панацея, но в реальных магазинах часто убирает дубли из-за повторного клика.
2. Идемпотентность для вебхуков и callback-ов
Если дубль создаёт платёжный шлюз, нужно сделать обработчик идемпотентным: повторный webhook не должен создавать новый заказ или повторно менять состояние. Самый практичный способ — сохранять внешний идентификатор транзакции и проверять, обработан ли он уже.
function wpticket_mark_payment_processed( $order_id, $transaction_id ) {
if ( ! $order_id || ! $transaction_id ) {
return false;
}
$existing = get_post_meta( $order_id, '_external_transaction_id', true );
if ( $existing === $transaction_id ) {
return false;
}
update_post_meta( $order_id, '_external_transaction_id', sanitize_text_field( $transaction_id ) );
return true;
}Если ваш шлюз даёт уникальный ID транзакции, используйте его. Если нет — ищите комбинацию из суммы, email, order key и времени создания, но это уже компромисс, и он хуже, чем нормальный transaction ID.
3. Не создавайте новый заказ при возврате с платёжной страницы
Частая ошибка — логика, которая на странице успеха проверяет корзину и создаёт заказ заново, если не находит нужный ID в URL или сессии. После успешной оплаты корзина может быть очищена, а пользователь вернётся по старой ссылке. В результате код считает, что заказа нет, и создаёт его повторно.
Правильнее опираться на уже созданный order ID и order key, а не на состояние корзины. Если шлюз возвращает пользователя по callback URL, проверяйте, существует ли заказ и обработан ли он уже.
4. Отключите кэширование для checkout и thank you page
Кэш на страницах оформления заказа и подтверждения оплаты может ломать сессию WooCommerce. Это не всегда приводит к дублям напрямую, но часто провоцирует повторную отправку формы и потерю данных о текущем заказе.
Проверьте, что кэш-плагин и серверный кэш не трогают:
/checkout//cart//my-account/- страницы thank you после оплаты
Если используете плагины оптимизации, убедитесь, что они не объединяют и не откладывают скрипты WooCommerce так, что ломается checkout. Для таких задач иногда проще точечно исключить checkout-скрипты, чем искать редкие баги в минификации.
Сравнение подходов
| Подход | Когда подходит | Ограничение |
|---|---|---|
| Плагин кэширования с исключениями | Если дубль связан с кэшем или checkout-страницами | Не решает повторные webhooks |
| Код с проверкой transaction ID | Если дубль приходит от платёжного шлюза | Нужен стабильный внешний ID |
| Отключение повторной отправки формы | Если пользователь кликает кнопку оплаты несколько раз | Не защищает от серверных повторов |
Проверка результата после внедрения
После правок не ограничивайтесь одной тестовой оплатой. Проверьте сценарий с задержкой и повторным действием пользователя.
- Сделайте тестовый заказ через sandbox-режим шлюза.
- Нажмите кнопку оплаты дважды с интервалом в 1–2 секунды.
- Обновите страницу возврата после оплаты.
- Проверьте, что в WooCommerce создан только один заказ.
- Посмотрите логи шлюза: повторный callback должен игнорироваться, а не создавать новый заказ.
Если у вас есть доступ к базе, можно дополнительно проверить, не плодятся ли записи в wp_posts типа shop_order в момент одной транзакции. Для магазинов с высокой нагрузкой это особенно полезно.
Частые ошибки и как их исправить
Ставят защиту только на фронтенде
Отключение кнопки после клика помогает, но не спасает от повторного запроса, если пользователь открыл две вкладки или запрос повторил шлюз. Нужна серверная проверка.
Путают повторный webhook с новым заказом
Иногда заказ один, но шлюз несколько раз присылает уведомление о платеже. Если обработчик не идемпотентен, он может повторно создавать связанные записи, менять статусы или отправлять письма.
Не исключают checkout из кэша
Кэширование checkout и thank you page часто ломает сессию WooCommerce. Исключения должны быть настроены и в плагине, и на уровне CDN, если он используется.
Используют слишком общий ключ дедупликации
Проверка только по сумме и email ненадёжна: один и тот же клиент может сделать два разных заказа на одинаковую сумму. Лучше использовать внешний transaction ID или order key.
Что делать для безопасности и стабильности
Если вы добавляете собственную логику обработки оплаты, не храните чувствительные данные в открытых мета-полях и не доверяйте данным из $_POST без проверки. Для webhook-обработчиков проверяйте подпись запроса, если шлюз её предоставляет, и сверяйте сумму, валюту и order ID с данными заказа.
Для магазинов, где часто меняют плагины и тему, полезно держать отдельный staging-сайт и тестировать оплату там. Это дешевле, чем разбирать дубли в боевом магазине после каждого обновления.
Если вам нужно быстро навести порядок в лишних скриптах и дублях разметки на сайте, иногда помогает Clearfy Pro, но в случае дублей заказов он не заменяет проверку логики оплаты и webhook-обработчиков.
В итоге рабочая схема обычно выглядит так: фронтенд не даёт отправить форму дважды, сервер не создаёт повторный заказ по одному и тому же событию, а webhook-обработчик игнорирует уже обработанную транзакцию. Если все три слоя настроены, дубли исчезают не случайно, а по причине.