Przejdź do treści
🛒 Duplikaty zamówień w WooCommerce: 3 skrypty do blokowania na poziomie koszyka

🛒 Duplikaty zamówień w WooCommerce: 3 skrypty do blokowania na poziomie koszyka

Klient kliknął „Zamów" dwa razy i już w panelu administracyjnym widnieją dwa identyczne zamówienia z różnicą kilku sekund. Pieniądze pobrano podwójnie, klient jest wściekły, a Pan/Pani zajmuje się refundacją.

Duplikaty zamówień to nie hipotetyczna sytuacja. Spotyka się z nimi mniej więcej co trzeci sklep na WooCommerce, szczególnie przy niestabilnym hostingu lub wolnych bramkach płatniczych. Ale nawet na szybkim serwerze użytkownik może ponownie dodać produkt przez URL lub kupić bezpłatny produkt cyfrowy dziesiątki razy.

Zebraliśmy trzy działające skrypty, które pokrywają różne scenariusze duplikacji: od wymuszenia sprzedaży po jednej sztuce po blokowanie ponownych dodawań przez URL. Poniżej pełna analiza z kodem.

Dlaczego zamówienia w WooCommerce się duplikują

💡 Szybki przegląd:

  • Dowiedzą się Państwo, dlaczego zamówienia duplikują się na poziomie koszyka, URL i ponownych zakupów
  • Zainstalują Państwo skrypt wymuszający sprzedaż produktu tylko po jednej sztuce przez meta pola
  • Zablokują Państwo ponowny zakup bezpłatnych wariantów (produkty cyfrowe, kupony)
  • Usuną Państwo duplikację przez URL z add-to-cart podczas odświeżania strony
  • Sprawdzą Państwo zabezpieczenie i dowiedzą się, co jeszcze może powodować duplikaty na poziomie serwera

Duplikaty zamówień powstają w trzech głównych scenariuszach.

Pierwszy: użytkownik kilka razy klika „Zamów". Dzieje się tak, gdy strona działa wolno, a osoba nie widzi reakcji i klika przycisk ponownie. Na wolnym hostingu to regularna historia.

Drugi scenariusz: bezpłatne produkty. Jeśli mają Państwo bezpłatny produkt cyfrowy, nic nie stoi na przeszkodzie, by jeden użytkownik „kupił" go dziesięć razy z rzędu. WooCommerce domyślnie nie śledzi, czy ten klient brał już wcześniej ten sam bezpłatny produkt.

Trzeci: bezpośredni link z parametrem ?add-to-cart=123. Odświeżenie strony koszyka powoduje ponowne dodanie produktu. Dwukrotne przejście przez link z newslettera skutkuje dwoma egzemplarzami w koszyku.

Wszystkie trzy scenariusze rozwiązuje się po stronie serwera za pomocą hooków WooCommerce. Żadnych wtyczek, tylko kod.

Sposób 1: Wymuszenie sprzedaży po jednej sztuce przez meta pola

Pierwszy skrypt zmusza WooCommerce do uznania konkretnego produktu za „sprzedawany na sztuki", nawet jeśli to ustawienie nie jest zaznaczone w konfiguracji produktu. Sterowanie odbywa się przez meta pole.

Gdy meta pole maximum_allowed_quantity ma wartość 1, filtr woocommerce_is_sold_individually zwraca true, a WooCommerce sam blokuje dodanie drugiej sztuki do koszyka. Działa zarówno dla produktów prostych, jak i wariantów.

Proszę dodać kod do functions.php motywu potomnego lub przez wtyczkę Code Snippets:

1function sd_woocommerce_is_sold_individually( $return, $product ) {
2 switch ( $product->get_type() ) {
3 case 'variation':
4 if ( $product->get_meta( 'variation_maximum_allowed_quantity' ) == 1 ) {
5 return true;
6 }
7 break;
8 case 'simple':
9 if ( $product->get_meta( 'maximum_allowed_quantity' ) == 1 ) {
10 return true;
11 }
12 break;
13 }
14 return $return;
15}
16add_filter( 'woocommerce_is_sold_individually', 'sd_woocommerce_is_sold_individually', 10, 2 );

Co tu się dzieje: hook woocommerce_is_sold_individually uruchamia się za każdym razem, gdy WooCommerce sprawdza, czy można dodać produkt do koszyka więcej niż raz. Funkcja zagląda do meta pól produktu i jeśli widzi maximum_allowed_quantity = 1, wymusza włączenie trybu „Sold Individually". Dla wariantów pole nazywa się variation_maximum_allowed_quantity.

Jak zastosować. Proszę wejść w edycję dowolnego produktu lub wariantu, dodać pole niestandardowe o nazwie maximum_allowed_quantity (lub variation_maximum_allowed_quantity dla wariantu) i wartości 1. Zapisać. To wszystko, teraz tego produktu nie można włożyć do koszyka dwa razy.

Sposób 2: Blokada ponownego zakupu bezpłatnych wariantów

Bezpłatne produkty cyfrowe to główna luka w zabezpieczeniu przed duplikatami. Użytkownik jest zalogowany, cena produktu wynosi zero, WooCommerce przepuszcza zamówienie bez pytań. Dziesięć takich zamówień od jednego klienta to dziesięć identycznych wpisów w panelu administracyjnym.

Poniższy skrypt sprawdza przez wc_customer_bought_product(), czy ten użytkownik kupował już wcześniej ten sam produkt. Jeśli tak, dodanie do koszyka jest blokowane z czytelnym komunikatem.

1function sd_disable_repeat_purchase_for_free_products( $passed, $product_id, $quantity, $variation_id = 0 ) {
2 $current_user = wp_get_current_user();
3 $product = $variation_id > 0 ? wc_get_product( $variation_id ) : wc_get_product( $product_id );
4 $target_id = $variation_id > 0 ? $variation_id : $product_id;
5
6 if ( floatval( $product->get_price() ) == 0 ) {
7 if ( wc_customer_bought_product( $current_user->user_email, $current_user->ID, $target_id ) ) {
8 wc_add_notice(
9 sprintf( __( 'You have already purchased "%s" and cannot purchase it again.', 'sd' ), $product->get_name() ),
10 'error'
11 );
12 return false;
13 }
14 }
15 return $passed;
16}
17add_filter( 'woocommerce_add_to_cart_validation', 'sd_disable_repeat_purchase_for_free_products', 10, 4 );

Po instalacji użytkownik widzi komunikat o błędzie przy próbie dodania do koszyka już kupionego bezpłatnego produktu. Samo zamówienie nie jest tworzone.

Dwa ważne warunki. Po pierwsze, klient musi być zalogowany. Dla gości wp_get_current_user() zwraca pusty obiekt i sprawdzenie nie zadziała. Po drugie, cena produktu musi być równa zero (floatval eliminuje problemy z formatowaniem). Produktów z ceną wyższą od zera skrypt nie ruszy.

Sposób 3: Ochrona przed duplikacją przez URL z add-to-cart

WooCommerce pozwala dodać produkt do koszyka przez bezpośredni link: /cart/?add-to-cart=123. Wygodne dla marketingu, ale jeśli użytkownik odświeży stronę lub przejdzie przez link ponownie, produkt zostanie dodany znowu.

Ten skrypt przechwytuje ładowanie strony koszyka lub checkoutu i jeśli produkt z parametru URL już znajduje się w koszyku, po cichu usuwa parametry zapytania i przekierowuje na czysty URL.

1function sd_prevent_duplicate_add_to_cart_redirect() {
2 if ( ( is_cart() || is_checkout() ) && isset( $_GET['add-to-cart'] ) ) {
3 $current_url = home_url( add_query_arg( null, null ) );
4 $product_id = intval( $_GET['add-to-cart'] );
5 $variation_id = isset( $_GET['variation_id'] ) ? intval( $_GET['variation_id'] ) : 0;
6
7 foreach ( WC()->cart->get_cart() as $cart_item ) {
8 if ( $cart_item['product_id'] == $product_id
9 && ( $variation_id == 0 || $cart_item['variation_id'] == $variation_id ) ) {
10 wp_redirect( remove_query_arg( array( 'add-to-cart', 'variation_id' ), $current_url ) );
11 exit;
12 }
13 }
14 }
15}
16add_action( 'template_redirect', 'sd_prevent_duplicate_add_to_cart_redirect' );

Hook template_redirect uruchamia się zanim WordPress zacznie renderować stronę. Skrypt sprawdza, czy w URL jest parametr add-to-cart, szuka tego produktu w zawartości koszyka (WC()->cart->get_cart()) i jeśli znajdzie, wykonuje wp_redirect na tę samą stronę bez parametrów zapytania.

Ponowne dodanie przez URL jest wykluczone. Przy tym pierwsze dodanie (gdy produktu w koszyku jeszcze nie ma) działa normalnie.

Łączymy wszystko razem

Wszystkie trzy skrypty nie kolidują ze sobą i działają niezależnie:

  • Pierwszy ogranicza liczbę sztuk jednego produktu do 1, ochrona przed wielokrotnym dodaniem.
  • Drugi blokuje ponowny zakup bezpłatnych produktów, ochrona przed „wyklikaniem" gratisów.
  • Trzeci usuwa duplikację przez URL, ochrona przed przypadkowymi ponownymi dodaniami.

Proszę dodawać kod przez wtyczkę Code Snippets na WP.org, a nie bezpośrednio do functions.php motywu. Wtyczka pozwala włączać i wyłączać snippety pojedynczo, nie psując strony w razie błędu, i nie traci kodu przy zmianie motywu. Proszę utworzyć trzy oddzielne snippety, tak łatwiej debugować.

Co jeszcze sprawdzić, jeśli duplikaty nadal występują

Powyższe skrypty eliminują duplikaty na poziomie koszyka i składania zamówienia. Istnieją jednak jeszcze dwa źródła problemu, których nie da się naprawić kodem.

Buforowanie stron koszyka i checkoutu. Wtyczki buforujące, takie jak WP Rocket lub LiteSpeed Cache, domyślnie wykluczają te strony, ale po aktualizacji ustawień wykluczenie może się zresetować. Proszę sprawdzić: cart, checkout i my-account nie powinny być buforowane ani na poziomie wtyczki, ani serwera, ani w CDN.

Ponowienia ze strony bramki płatniczej. Jeśli bramka (Stripe, PayPal) nie otrzyma od serwera potwierdzenia na czas, wysyła powiadomienie ponownie, a WooCommerce tworzy drugie zamówienie. Przyczyną jest zwykle wolny hosting lub firewall blokujący żądania callback. Rozwiązanie: proszę sprawdzić logi bramki pod kątem powtarzanych wysyłek webhook i upewnić się, że URL callback jest dostępny z zewnątrz.

Jak przetestować zabezpieczenie

Po zainstalowaniu skryptów koniecznie proszę sprawdzić każdy scenariusz na stronie testowej.

Sposób 1: proszę utworzyć produkt z meta polem maximum_allowed_quantity = 1 i spróbować dodać dwie sztuki do koszyka. Powinna dodać się tylko jedna.

Sposób 2: proszę się zalogować, kupić bezpłatny produkt, a następnie spróbować kupić go ponownie. Powinni Państwo zobaczyć komunikat o błędzie.

Sposób 3: proszę dodać produkt do koszyka przez bezpośredni link z ?add-to-cart=ID, a następnie odświeżyć stronę. Liczba pozycji w koszyku nie powinna się zmienić.

Proszę testować na osobnej instalacji lub na stronie stagingowej. Na działającym sklepie tylko po pełnym sprawdzeniu.

⁉️🤔 Często zadawane pytania

Dlaczego zamówienia się duplikują, jeśli przycisk „Zamów" został kliknięty raz?

Przyczyną jest prawie zawsze wolna odpowiedź serwera. Użytkownik klika przycisk, żądanie idzie, ale strona nie odświeża się przez kilka sekund. Osoba myśli, że nie kliknęła, i klika ponownie. Oba żądania docierają do WooCommerce i tworzone są dwa zamówienia. Skrypty z tego artykułu nie wyłapują takiej sytuacji, tutaj pomaga buforowanie po stronie klienta (disable button after click) lub serwerowe sprawdzenie unikalności zamówienia za pomocą tokena nonce.

Czy bramka płatnicza może sama tworzyć duplikaty?

Tak. Jeśli bramka nie otrzyma od Państwa serwera odpowiedzi HTTP 200 na IPN/webhook, ponawia wysyłkę. Przy każdym ponowieniu WooCommerce może utworzyć nowe zamówienie. Problem rozwiązuje się przez przyspieszenie hostingu i sprawdzenie, czy URL callback bramki nie jest zablokowany przez firewall i nie wymaga autoryzacji HTTP.

Czy to zabezpieczenie blokuje prawdziwe ponowne zamówienia od klienta?

Nie. Skrypty działają na etapie dodawania do koszyka, a nie na etapie tworzenia zamówienia. Jeśli klient świadomie złożył jedno zamówienie, a tydzień później drugie, skrypty nie zainterweniują. Wyjątek: sposób 2, który zabrania ponownego zakupu bezpłatnego produktu przez tego samego użytkownika.

Czy koniecznie trzeba używać Code Snippets, czy można w functions.php?

Można i w functions.php, ale zalecamy Code Snippets. W przypadku literówki w kodzie w functions.php otrzymają Państwo biały ekran i dostęp tylko przez FTP. Wtyczka Code Snippets wyłapuje błędy krytyczne i automatycznie wyłącza problematyczny snippet, strona pozostaje na chodzie.

Czy wtyczki do zapobiegania duplikatom pomagają?

Specjalistyczne wtyczki, takie jak WooCommerce Duplicate Order Blocker, działają na innym poziomie, śledzą duplikaty po IP, emailu lub odcisku przeglądarki na etapie składania zamówienia. To pokrywa scenariusz podwójnego kliknięcia przycisku, którego nasze skrypty nie obejmują. Jednocześnie wtyczki nie rozwiązują problemu duplikatów przez URL add-to-cart i ponownych zakupów bezpłatnego produktu. Idealne rozwiązanie to skrypty z tego artykułu plus wtyczka blokująca na checkoucie.

Podsumowanie: trzy poziomy ochrony przed duplikatami

Trzy skrypty pokrywają trzy konkretne scenariusze duplikacji zamówień. Proszę zainstalować wszystkie od razu, każdy odpowiada za swój obszar i razem dają szczelną ochronę na poziomie koszyka.

Jeśli po instalacji duplikaty wciąż się pojawiają, proszę spojrzeć w stronę hostingu i bramki płatniczej. Podwójnego kliknięcia przycisku „Zamów" nie da się wyleczyć kodem na poziomie koszyka, tutaj potrzebna jest albo wtyczka blokująca, albo niestandardowe zabezpieczenie przycisku przez JavaScript.

Proszę sprawdzić zabezpieczenie na stronie testowej i przenieść na produkcyjną. Piętnaście minut na instalację trzech snippetów oszczędza godziny rozpatrywania refundacji.