Sklep generował zduplikowane zamówienia – klient kliknął „Zamów” raz, ale powstawały dwa identyczne zamówienia z podwójną płatnością. Problem dotyczył ~2% zamówień głównie przy wolnym połączeniu internetowym. Czas naprawy: 6 godzin.
Objawy
- W panelu admina pojawiają się pary identycznych zamówień w odstępie sekund
- Klienci skarżą się na podwójne obciążenie karty
- Problem częstszy przy wolnym połączeniu i na urządzeniach mobilnych
- Logi pokazują dwa requesty POST do
/rest/V1/carts/mine/payment-informationw tym samym czasie
Przyczyna
Przycisk „Zamów” nie był blokowany po pierwszym kliknięciu. Przy wolnym połączeniu użytkownik klikał ponownie myśląc że nic się nie stało. Dwa równoległe requesty trafiały do serwera – oba kończyły się sukcesem bo nie było mechanizmu idempotentności.
Rozwiązanie
<?php
// Plugin blokujący duplikaty przez klucz idempotentności
class PlaceOrderIdempotencyPlugin
{
private const LOCK_TTL = 30; // sekund
public function __construct(
private \Magento\Framework\Cache\FrontendInterface $cache,
) {}
public function aroundSavePaymentInformationAndPlaceOrder(
\Magento\Checkout\Api\PaymentInformationManagementInterface $subject,
callable $proceed,
int $cartId,
\Magento\Quote\Api\Data\PaymentInterface $paymentMethod,
?\Magento\Quote\Api\Data\AddressInterface $billingAddress = null
): int {
$lockKey = 'order_lock_' . $cartId;
if ($this->cache->load($lockKey)) {
throw new \Magento\Framework\Exception\LocalizedException(
__('Your order is being processed. Please wait.')
);
}
$this->cache->save('1', $lockKey, [], self::LOCK_TTL);
try {
$result = $proceed($cartId, $paymentMethod, $billingAddress);
return $result;
} finally {
$this->cache->remove($lockKey);
}
}
}
Wnioski
Każda operacja która powinna wykonać się dokładnie raz wymaga mechanizmu idempotentności. W checkout: blokada przycisku po stronie JS + klucz idempotentności po stronie serwera. Redis z TTL to idealny mechanizm do krótkotrwałych blokad.
