Integracja korporacyjnego sklepu WooCommerce z systemem klasy ERP (Enterprise Resource Planning) stanowi jeden z najbardziej wymagających obszarów inżynierii e-commerce. W projektach obsługujących dziesiątki tysięcy jednostek magazynowych (SKU), wysoki wolumen zamówień oraz wielokanałową sprzedaż w czasie rzeczywistym tradycyjne, synchroniczne integracje punkt-punkt stają się źródłem poważnych awarii. Bezpośrednie wywołania API prowadzą do przeciążenia serwerów, blokad baz danych, gubienia zamówień oraz nadmiernej sprzedaży towaru.
Odporna na błędy, dwukierunkowa integracja WooCommerce z systemem ERP wymaga odseparowanej, asynchronicznej architektury sterowanej zdarzeniami, opartej na pięciu filarach inżynieryjnych:
- Asynchroniczne buforowanie komunikatów: Przyjmowanie webhooków i zdarzeń zakupowych do pośredniczącej kolejki (Redis Streams lub Cloudflare Queues) ze zwrotem HTTP 202 Accepted w czasie poniżej 25 ms, co uniezależnia działanie sklepu od czasu odpowiedzi ERP.
- Gwarancja idempotencji: Wymuszenie unikalności każdego żądania za pomocą nagłówka
X-Idempotency-Keyoraz atomowych blokad w Redis, co zapobiega dublowaniu zamówień i rozliczeń finansowych. - Kontrola współbieżności bazy danych: Zastosowanie blokad wierszy silnika InnoDB (
SELECT ... FOR UPDATE) lub optymistycznej kontroli wersji, eliminując ryzyko sprzedaży niedostępnego towaru podczas pików zakupowych. - Odporna obsługa błędów: Implementacja wykładniczego wycofywania z losowym szumem (jitter) oraz przekierowywanie trwale błędnych pakietów do kolejki martwych listów (DLQ) z automatycznymi powiadomieniami.
- Dwuetapowe uzgadnianie danych: Połączenie cogodzinnej synchronizacji różnicowej z nocną weryfikacją sum kontrolnych SHA-256 dla całego katalogu towarowego w celu natychmiastowej eliminacji rozbieżności.
W przypadku zespołów inżynieryjnych planujących wdrożenie lub modernizację przepływu danych pomiędzy systemami handlowymi, nasze usługi integracji WooCommerce z ERP zapewniają bezpośrednie wsparcie wdrożeniowe. W niniejszym przewodniku szczegółowo omawiamy schematy architektoniczne, specyfikę wiodących platform ERP, produkcyjne implementacje w PHP 8.4 oraz procedury rozwiązywania sytuacji awaryjnych.
Fundament architektoniczny: asynchroniczna integracja sterowana zdarzeniami
W tradycyjnych wdrożeniach WooCommerce zdarzenia w sklepie wywołują bezpośrednie żądania HTTP do systemu magazynowo-księgowego. Kiedy klient finalizuje zamówienie, akcja woocommerce_checkout_order_processed uruchamia synchroniczne połączenie cURL do SAP S/4HANA, Comarch Optima lub Subiekta GT.
Taki synchroniczny model wprowadza krytyczne ryzyko operacyjne:
[Przeglądarka klienta]
│ (1) Kliknięcie "Kupuję i płacę"
▼
[Proces PHP-FPM WooCommerce] ──(2) Synchroniczny POST HTTP──▶ [Serwer ERP (Zajęty / Przeciążony)]
│ │
│ ◀───(3) HTTP 504 Gateway Timeout (Blokada wątku przez 60s)────┘
▼
[Klient widzi błąd na ekranie] ──▶ Ponowne kliknięcia ──▶ Blokady bazy i zdublowane zamówienia
W sytuacjach, gdy serwer ERP wykonuje nocne procedury księgowe, przetwarza duże raporty lub doświadcza opóźnień sieciowych, czas odpowiedzi wydłuża się z 200 milisekund do kilkudziesięciu sekund. Ponieważ wątki PHP-FPM są zablokowane w oczekiwaniu na odpowiedź gniazda sieciowego, pula wolnych procesów serwera WWW ulega błyskawicznemu wyczerpaniu. Kolejni użytkownicy przeglądający sklep otrzymują błędy HTTP 504 Gateway Timeout. Dodatkowo, jeśli połączenie zostanie zerwane po zapisaniu dokumentu w ERP, ale przed odebraniem potwierdzenia przez WooCommerce, mechanizmy ponawiania utworzą podwójne zamówienia i podwójnie zarezerwują stany magazynowe.
Wzorzec brokera zdarzeń
Rozwiązaniem zapewniającym pełną niezawodność jest odseparowanie procesu generowania zdarzeń od ich faktycznego przetwarzania. Sklep i system ERP komunikują się wyłącznie za pośrednictwem kolejki komunikatów.
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ ODSEPAROWANY PRZEPŁYW STEROWANY ZDARZENIAMI │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────┐ ┌─────────────────────────┐
│ Sklep WooCommerce │ │ System ERP │
│ (Obsługa zamówień B2C) │ │ (Centralny magazyn) │
└────────────┬────────────┘ └────────────▲────────────┘
│ │
(1) Szybki zapis zdarzenia (4) Kontrolowany wysył
(Czas < 25 ms) (Zgodny z limitami API)
▼ │
┌────────────────────────────────────────────────────────────┴───────────────────────────┐
│ BROKER WIADOMOŚCI I BUFOR │
│ (Redis Streams / Cloudflare Queues) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ ┌────────────────────────────┐ │
│ │ orders.incoming │ │ inventory.delta │ │ dead.letter.queue (DLQ) │ │
│ │ [Zdarzenie 1][Zdarz]│ │ [SKU 101][SKU 102] │ │ [Błędne pakiety + logi] │ │
│ └──────────┬───────────┘ └──────────┬───────────┘ └─────────────▲──────────────┘ │
└──────────────┼─────────────────────────┼────────────────────────────┼──────────────────┘
│ │ │
(2) Odczyt strumienia (5) Atomowy zapis stanów (3) Przekroczenie limitu
przez grupę workerów poprzez blokady InnoDB prób ponowienia
▼ ▼ │
┌─────────────────────────────────────────────────────────────────────┴──────────────────┐
│ PULA DAEMONÓW WP-CLI (PHP 8.4) │
│ │
│ - Obsługa sygnałów POSIX (SIGTERM/SIGINT) - Weryfikacja idempotencji (Redis SETNX) │
│ - Zarządzanie pamięcią (wp_cache_flush) - Wykładnicze wycofywanie z jitterem │
└────────────────────────────────────────────────────────────────────────────────────────┘
- Błyskawiczne utrwalenie zdarzenia: W momencie złożenia zamówienia WooCommerce zapisuje kompaktowy pakiet danych w strumieniu Redis (
orders.incoming) i natychmiast wyświetla klientowi podziękowanie za zakup w czasie poniżej 15 milisekund. - Asynchroniczne przetwarzanie w tle: Pula daemonów uruchomionych przez WP-CLI pobiera zadania ze strumienia z prędkością dopasowaną do przepustowości interfejsu ERP.
- Płynna kontrola obciążenia (backpressure): W przypadku prac konserwacyjnych lub spowolnienia ERP komunikaty bezpiecznie gromadzą się w kolejce, nie wpływając na działanie frontendu sklepu.
- Idempotentne wykonanie: Każde zdarzenie posiada unikalny klucz idempotencji. W przypadku awarii pojedynczego procesu roboczego, kolejny worker podejmuje zadanie z listy oczekujących bez ryzyka powielenia rekordów w bazie.
Macierz systemów ERP i kompatybilność protokołów
Poszczególne platformy ERP różnią się architekturą sieciową, ograniczeniami współbieżności, metodami autoryzacji oraz wydajnością interfejsów API. Skuteczna integracja wymaga dostosowania strategii komunikacji do specyfiki danego systemu.
Poniższa tabela porównuje cztery wiodące systemy ERP powszechnie integrowane z WooCommerce na rynku polskim i europejskim:
| Platforma ERP | Obsługiwane protokoły | Profil przepustowości | Model współbieżności i blokad | Główne punkty awaryjności |
|---|---|---|---|---|
| SAP S/4HANA | OData v4, IDoc przez SAP BTP, RFC, Async SOAP | Bardzo wysoka w pakietach (10k+ rekordów/min); średnia dla pojedynczych zapytań | SAP Logical Unit of Work (LUW); blokady serwera Enqueue; limity OData batch | Wyczerpanie puli połączeń przy restartach SAP Cloud Connector; przekroczenia czasu przy złożonych strukturach BOM |
| Comarch Optima / XL | Optima WebAPI, automatyzacja COM DLL, tabele staging w MS SQL | Średnia dla API (50-100 zamówień/min); bardzo wysoka dla SQL staging (50k/min) | Model Single-Threaded Apartment (STA) w COM; eskalacja blokad tabel MS SQL (sp_lock) | Wycieki pamięci w procesach COM wymagające restartów; blokady licencji przy zawieszonych wątkach |
| InsERT Subiekt GT / nexo | Subiekt GT Sfera (COM/OLE), Subiekt nexo PRO SDK (.NET / WebAPI) | GT: 30-80 zamówień/min; nexo PRO: 250+ zamówień/min | Blokowanie wierszy w SQL Server na tabelach dok__Dokument i tw__Towar | Blokowanie wątków COM przez modalne okna dialogowe w GT; fragmentacja indeksów bazy |
| Microsoft Dynamics 365 BC | Business Central REST API v2.0, OData v4, AL API Pages | Limit 600 żądań/min na środowisko chmurowe; pakiety JSON (do 100 podżądań) | Izolacja migawkowa (snapshot) w Azure SQL; blokady nagłówków zamówień przy księgowaniu | Ograniczenia przepustowości HTTP 429; gubienie powiadomień webhook przy masowym fakturowaniu |
Wzorce integracji z SAP S/4HANA
W środowiskach SAP S/4HANA komunikacja odbywa się najczęściej poprzez platformę SAP Business Technology Platform (SAP BTP) oraz SAP Cloud Connector. Synchroniczne tworzenie pojedynczych dokumentów przez standardowe usługi OData v4 (API_SALES_ORDER_SRV) generuje opóźnienie sieciowe rzędu 450-800 ms na żądanie.
Aby uzyskać odpowiednią skalę, należy wdrożyć przetwarzanie wsadowe:
- Pakiety JSON (Batching): Łączenie do stu zamówień w jedno wieloczęściowe żądanie OData. Zmniejsza to narzut negocjacji połączeń HTTP i zapewnia atomowość transakcji na poziomie pakietu.
- Kolejki IDoc: W przypadku masowej aktualizacji cenników i stanów magazynowych rekomendowane jest wykorzystanie asynchronicznych komunikatów IDoc (np.
ORDERS05,MATMAS05) przetwarzanych przez zadania wsadowe w tle SAP. - Obsługa jednostek LUW: SAP zarządza spójnością za pomocą logicznych jednostek pracy (Logical Units of Work). Klient integracyjny musi zapisać zwrócony identyfikator dokumentu SAP w dedykowanej tabeli indeksowej (
wp_wc_orders_erp_lookup), powiązując go trwale z numerem zamówienia WooCommerce.
Comarch ERP Optima oraz Comarch ERP XL
Comarch Optima jest standardem w segmencie średnich i dużych przedsiębiorstw w Polsce. Ze względu na architekturę opartą na Microsoft SQL Server, nowoczesna integracja API wymaga uwzględnienia specyfiki środowiska Windows.
- Optima WebAPI a obiekty COM: Choć Optima WebAPI udostępnia interfejs REST, operacje pod spodem tworzą instancje obiektów COM (
Optima.dll). Wymusza to inicjalizację wątków w modelu Single-Threaded Apartment (STA) poprzezCoInitialize. - Zarządzanie licencjami stanowiskowymi: Próba równoległego uruchomienia kilkunastu zapytań bez kolejkowania prowadzi do natychmiastowego wyczerpania dostępnych stanowisk licencyjnych modułu integracyjnego.
- Architektura tabel stagingowych: W sklepach B2B o bardzo wysokiej częstotliwości zmian stanów magazynowych najbardziej niezawodnym rozwiązaniem jest replikacja danych ze snapshotów bazy MS SQL do pamięci Redis, podczas gdy tworzenie dokumentów zamówień powierza się dedykowanemu daemonowi działającemu w środowisku Windows.
InsERT Subiekt GT oraz Subiekt nexo PRO
InsERT Subiekt GT korzysta z biblioteki Sfera dla Subiekta GT (interfejs COM/OLE Automation), natomiast nowocześniejszy Subiekt nexo PRO oferuje pełne SDK oparte na platformie .NET oraz usługi WebAPI.
- Ograniczenia Sfery dla Subiekta GT: Wywołania Sfery GT działają synchronicznie. Jeśli podczas przetwarzania dokumentu wystąpi nieobsłużony błąd aplikacji lub pojawi się modalny komunikat systemowy, wątek ulega zawieszeniu. Usługa integracyjna musi działać jako zarządzany serwis Windows (np. w C# lub Go) wyposażony w mechanizm watchdog oraz automatyczny restart procesu po przekroczeniu 30 sekund bezczynności.
- Zalety Subiekta nexo PRO: W nexo PRO operacje mogą być wykonywane wielowątkowo i asynchronicznie. Przy synchronizacji drzewa produktów warto korzystać z mechanizmu pobierania zmian w oparciu o znacznik czasu
Zmieniono, filtrując wyłącznie rekordy nowsze niż ostatnio zapisany punkt kontrolny.
Microsoft Dynamics 365 Business Central
Chmurowa wersja Business Central wymusza ścisłe przestrzeganie polityk dostępu do zasobów:
- Limity zapytań (Throttling): Microsoft nakłada limit 600 wywołań na minutę na pojedyncze środowisko. Przekroczenie limitu skutkuje kodem HTTP 429 z nagłówkiem
Retry-After. - Punkty końcowe $batch: Zamiast wysyłać pięćdziesiąt oddzielnych żądań HTTP dla każdego zamówienia, aplikacja wysyła jeden pakiet na adres
https://api.businesscentral.dynamics.com/v2.0/{tenant}/production/api/v2.0/$batch, zawierający tablicę sub-żądań przetwarzanych sekwencyjnie wewnątrz platformy. - Śledzenie zmian (Change Data Capture): Zamiast ciągłego odpytywania bazy o nowe stany magazynowe, należy zarejestrować subskrypcję powiadomień Webhooks w Business Central. Otrzymanie powiadomienia uruchamia precyzyjną synchronizację zmienionego rekordu.
Wzorce odporności na błędy dla transakcji o wysokim wolumenie
System integracyjny musi być projektowany przy założeniu, że połączenia sieciowe, serwery baz danych i usługi zewnętrzne ulegają cyklicznym awariom. Niezawodność WooCommerce opiera się na wdrożeniu pięciu fundamentalnych wzorców odpornościowych.
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ WZORZEC IDEMPOTENCJI I OBSŁUGI BŁĘDÓW │
└─────────────────────────────────────────────────────────────────────────────────────────┘
Przychodzące żądanie / Webhook
│
▼
┌───────────────────────────────────┐
│ Pobranie nagłówka │
│ X-Idempotency-Key │
└─────────────────┬─────────────────┘
│
▼
┌───────────────────────────────────┐
│ Atomowa blokada w Redis: │
│ SETNX lock:idempotency:{key} │
└─────────┬─────────────────────────┘
│
┌─────┴────────────────────────┐
│ Klucz pobrany (Nowe żądanie) │ Klucz istnieje (Duplikat / W toku)
▼ ▼
┌───────────────────────────┐ ┌─────────────────────────────────────────────────┐
│ Status: PROCESSING │ │ Sprawdzenie statusu: │
│ (Ważność TTL: 86400 s) │ │ - Jeśli 'PROCESSING': Zwróć HTTP 409 Conflict │
└─────────┬─────────────────┘ │ - Jeśli 'COMPLETED': Zwróć zapisaną odpowiedź │
│ └─────────────────────────────────────────────────┘
▼
┌───────────────────────────┐
│ Wykonanie logiki biznesu │
│ (Transakcja + Zapis w DB) │
└─────────┬─────────────────┘
│
┌─────┴────────────────────────┐
│ Sukces │ Awaria (Błąd / Timeout)
▼ ▼
┌───────────────────────────┐ ┌─────────────────────────────────────────────────┐
│ Aktualizacja w Redis: │ │ Wyliczenie wycofania z szumem (Full Jitter): │
│ COMPLETED + Cache wyniku │ │ sleep = min(T_max, T_base * 2^retry) + rand() │
└─────────┬─────────────────┘ └─────────────┬───────────────────────────────────┘
│ │
▼ ▼
┌───────────────────────────┐ ┌─────────────────────────────────────────────────┐
│ Odpowiedź HTTP 200/201 │ │ Jeśli próby < 5: Ponowne wrzucenie do kolejki │
└───────────────────────────┘ │ Jeśli próby >= 5: Przekierowanie do kolejki DLQ│
└─────────────────────────────────────────────────┘
1. Obsługa żądań idempotentnych (X-Idempotency-Key)
W architekturze asynchronicznej powtórzenia sieciowe i zdublowane webhooki są zjawiskiem naturalnym. Brak zabezpieczeń idempotencji przy ponownym odebraniu webhooka z informacją o opłaceniu zamówienia mógłby doprowadzić do wystawienia podwójnej faktury lub dwukrotnego zlecenia wysyłki.
Każde modyfikujące żądanie musi zawierać unikalny klucz idempotencji:
- Konstrukcja klucza: Klient przekazuje nagłówek
X-Idempotency-Key(np. UUIDv4) lub serwer wylicza deterministyczny skrót:sha256(order_id + status + timestamp). - Atomowa blokada stanu: Przed wykonaniem zadania proces wykonuje atomowy zapis w Redis:
SET lock:idempotency:{hash} "PROCESSING" NX EX 86400 - Obsługa stanów:
- Jeśli klucz został zapisany (
OK), proces przystępuje do wykonania zadania. Po zakończeniu zmienia wartość klucza na"COMPLETED:{json_odpowiedzi}". - Jeśli klucz istnieje i posiada wartość
"PROCESSING", inne zadanie jest w trakcie realizacji. Żądanie zwraca kod HTTP 409 Conflict lub oczekuje na zwolnienie blokady. - Jeśli wartość rozpoczyna się od
"COMPLETED:", proces natychmiast zwraca zapamiętaną odpowiedź z nagłówkiemX-Cache-Lookup: HIT.
- Jeśli klucz został zapisany (
2. Redis Streams i grupy konsumentów
Zwykłe listy w Redis (LPUSH/RPOP) tracą wiadomości w przypadku awarii procesu roboczego pomiędzy pobraniem zadania a zatwierdzeniem transakcji w bazie. Redis Streams eliminuje to ryzyko:
- Dopisywanie zdarzeń (
XADD): Komunikaty trafiają do trwałego logu ze znacznikami czasowymi w milisekundach. - Grupy konsumentów (
XREADGROUP): Wiele procesów roboczych pobiera zadania bez duplikowania pracy, a Redis rejestruje, który worker obsługuje dany komunikat. - Jawne potwierdzenia (
XACK): Komunikat zostaje usunięty z listy zadań oczekujących (PEL) dopiero po zatwierdzeniu zmian w bazie danych i wywołaniuXACK. - Przejmowanie osieroconych zadań (
XCLAIM): Jeśli proces konsumenta zostanie zatrzymany przez system operacyjny, pozostałe workery wykrywają przeterminowane zadania za pomocąXPENDINGi przejmują je przezXCLAIM.
3. Kolejka martwych listów (DLQ) i wycofywanie z losowym szumem
Wszystkie błędy integracji należy podzielić na dwie kategorie:
- Błędy przejściowe: Chwilowe braki łączności, błędy bramy HTTP 502/503/504, limity HTTP 429 oraz zakleszczenia transakcji w MySQL (deadlocks). Wymagają automatycznego ponawiania.
- Błędy trwałe: Błędy walidacji schematu HTTP 400, brak zasobu HTTP 404, nieobsługiwane stawki VAT lub błędy reguł biznesowych. Nie mogą być ponawiane automatycznie.
Dla błędów przejściowych stosuje się algorytm wykładniczego wycofywania z pełnym szumem losowym (Full Jitter), co zapobiega jednoczesnemu atakowi setek procesów na restartujący się serwer ERP:
$$\text{Odstęp} = \min\left(T_{\text{max}}, T_{\text{base}} \times 2^{\text{próba}}\right)$$
$$\text{Czas oczekiwania} = \text{random}\left(0, \text{Odstęp}\right)$$
Jeżeli zadanie zakończy się niepowodzeniem po określonej liczbie prób (zazwyczaj pięciu), trafia do kolejki martwych listów (dlq:erp_sync). Zapis w DLQ zawiera pełny zrzut payloadu, ślad stosu błędu, kod odpowiedzi ERP oraz historię prób.
Kontrola współbieżności bazy danych i blokowanie stanów magazynowych
Podczas intensywnych wyprzedaży błyskawicznych setki klientów próbują kupić ten sam produkt w ciągu kilkunastu sekund. Jeśli dwa procesy jednocześnie odczytają stan magazynowy, zweryfikują dostępność i zaktualizują bazę, dojdzie do sprzedaży towaru, którego fizycznie brakuje na magazynie.
Blokowanie pesymistyczne: MySQL InnoDB SELECT FOR UPDATE
WooCommerce przechowuje stany magazynowe w bazie MySQL. Wywoływanie standardowych funkcji get_stock_quantity() i wc_update_product_stock() w warunkach wysokiej współbieżności jest niebezpieczne, ponieważ wykonują one nieblokujące odczyty.
Blokowanie pesymistyczne eliminuje wyścigi danych poprzez zablokowanie rekordu na czas trwania transakcji:
Transakcja A (Klient 1) Transakcja B (Klient 2)
─────────────────────── ───────────────────────
START TRANSACTION; START TRANSACTION;
SELECT stock_quantity SELECT stock_quantity
FROM wp_wc_product_meta FROM wp_wc_product_meta
WHERE product_id = 4500 WHERE product_id = 4500
FOR UPDATE; FOR UPDATE;
──▶ Wiersz zablokowany przez Transakcję A ──▶ OCZEKIWANIE (Wątek zablokowany)
Stan: 5 sztuk.
Odliczenie: 5 - 1 = 4.
UPDATE wp_wc_product_meta
SET stock_quantity = 4
WHERE product_id = 4500;
COMMIT; ── Zwolnienie blokady ────────────────▶ Blokada przyznana Transakcji B!
Stan: 4 sztuki.
Odliczenie: 4 - 1 = 3.
UPDATE wp_wc_product_meta SET ...
COMMIT;
Kluczowa zasada: Nigdy nie wolno wykonywać zewnętrznych wywołań HTTP do API systemu ERP wewnątrz otwartej transakcji bazodanowej. Blokujemy wiersz, aktualizujemy stan w bazie, zamykamy transakcję w czasie poniżej 50 milisekund, a powiadomienie do ERP wysyłamy asynchronicznie przez kolejkę.
Optymistyczna kontrola współbieżności (OCC)
W sytuacjach, gdy liczba modyfikacji jest umiarkowana, optymistyczna kontrola współbieżności zapewnia wyższą przepustowość bez konieczności blokowania wierszy podczas odczytu.
Polega ona na dodaniu kolumny version w tabeli metadanych produktu i wykonaniu atomowej aktualizacji warunkowej:
UPDATE wp_wc_product_meta
SET stock_quantity = stock_quantity - :zakupiona_ilosc,
version = version + 1
WHERE product_id = :product_id
AND version = :oczekiwana_wersja
AND stock_quantity >= :zakupiona_ilosc;
Jeśli inny proces zmienił stan w międzyczasie, warunek wersji nie zostanie spełniony i baza zwróci informację o zaktualizowaniu zera wierszy. Aplikacja natychmiast ponawia próbę z nowym numerem wersji.
Przykłady implementacji produkcyjnej: PHP 8.4 i daemon WP-CLI
Poniższe listingi prezentują przetestowane komponenty integracyjne w standardzie PHP 8.4 przygotowane do pracy w środowisku WooCommerce.
Listing 1: Daemon konsumenta kolejki w WP-CLI (QueueConsumerCommand.php)
Komenda WP-CLI zaprojektowana do ciągłej pracy pod nadzorem Systemd lub Supervisord. Obsługuje strumienie Redis Streams, zarządza pamięcią RAM i przechwytuje sygnały systemowe.
<?php
declare(strict_types=1);
namespace WPPoland\ErpIntegration\Cli;
use WP_CLI;
use Redis;
use Throwable;
if (!defined('ABSPATH')) {
exit;
}
/**
* Nadzorowany daemon WP-CLI do asynchronicznej synchronizacji WooCommerce z ERP.
*/
class QueueConsumerCommand
{
private const STREAM_KEY = 'erp:stream:orders';
private const CONSUMER_GROUP = 'erp_sync_group';
private const BATCH_SIZE = 10;
private const BLOCK_TIMEOUT_MS = 2000;
private const MAX_MEMORY_BYTES = 134217728; // Limit 128 MB przed kontrolowanym restartem
private Redis $redis;
private string $consumerName;
private bool $shouldRun = true;
public function __construct()
{
$this->consumerName = 'worker_' . gethostname() . '_' . getmypid();
$this->initRedis();
$this->registerSignalHandlers();
}
/**
* Punkt wejścia dla: wp erp-queue consume
*/
public function __invoke(array $args, array $assocArgs): void
{
WP_CLI::line("Uruchamianie daemona kolejki ERP [{$this->consumerName}] na PHP " . PHP_VERSION);
$this->ensureConsumerGroup();
$processedCount = 0;
while ($this->shouldRun) {
// Przetwarzanie sygnałów systemowych POSIX
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}
try {
$messages = $this->redis->xReadGroup(
self::CONSUMER_GROUP,
$this->consumerName,
[self::STREAM_KEY => '>'],
self::BATCH_SIZE,
self::BLOCK_TIMEOUT_MS
);
if (empty($messages) || !isset($messages[self::STREAM_KEY])) {
$this->reclaimOrphanedMessages();
$this->checkMemoryThreshold();
continue;
}
foreach ($messages[self::STREAM_KEY] as $messageId => $payload) {
$this->processMessage((string) $messageId, $payload);
$processedCount++;
}
$this->checkMemoryThreshold();
} catch (Throwable $e) {
WP_CLI::error("Nieobsłużony wyjątek w pętli konsumenta: " . $e->getMessage(), false);
sleep(2); // Throttling po awarii infrastruktury
}
}
WP_CLI::success("Daemon zakończył pracę pomyślnie. Przetworzono {$processedCount} komunikatów.");
}
private function processMessage(string $messageId, array $payload): void
{
$orderId = isset($payload['order_id']) ? (int) $payload['order_id'] : 0;
$idempotencyKey = $payload['idempotency_key'] ?? '';
if ($orderId <= 0 || empty($idempotencyKey)) {
WP_CLI::warning("Niepoprawny pakiet danych w komunikacie {$messageId}. Przekierowanie do DLQ.");
$this->routeToDlq($messageId, $payload, 'Błąd walidacji: brak order_id lub idempotency_key');
$this->redis->xAck(self::STREAM_KEY, self::CONSUMER_GROUP, [$messageId]);
return;
}
// Weryfikacja blokady idempotencji
$lockKey = "erp:lock:idemp:{$idempotencyKey}";
$acquired = $this->redis->set($lockKey, 'PROCESSING', ['NX', 'EX' => 86400]);
if (!$acquired) {
$status = (string) $this->redis->get($lockKey);
if (str_starts_with($status, 'COMPLETED')) {
WP_CLI::line("Zdarzenie {$idempotencyKey} zostało już przetworzone. Potwierdzanie.");
$this->redis->xAck(self::STREAM_KEY, self::CONSUMER_GROUP, [$messageId]);
return;
}
WP_CLI::line("Zdarzenie {$idempotencyKey} jest w trakcie obsługi przez inny wątek. Pomijanie.");
return;
}
try {
// Wywołanie adaptera ERP
$this->syncOrderToErp($orderId, $payload);
// Zapis sukcesu i potwierdzenie w kolejce
$this->redis->set($lockKey, 'COMPLETED:' . time(), ['EX' => 86400]);
$this->redis->xAck(self::STREAM_KEY, self::CONSUMER_GROUP, [$messageId]);
WP_CLI::line("Zsynchronizowano zamówienie #{$orderId} [ID: {$messageId}]");
} catch (Throwable $e) {
WP_CLI::warning("Błąd synchronizacji zamówienia #{$orderId}: " . $e->getMessage());
$this->redis->del($lockKey); // Zwolnienie blokady dla ponowienia
$attempts = isset($payload['_retry_count']) ? ((int) $payload['_retry_count']) + 1 : 1;
if ($attempts >= 5) {
$this->routeToDlq($messageId, $payload, $e->getMessage());
$this->redis->xAck(self::STREAM_KEY, self::CONSUMER_GROUP, [$messageId]);
} else {
// Ponowne zakolejkowanie z licznikiem prób
$payload['_retry_count'] = $attempts;
$this->redis->xAdd(self::STREAM_KEY, '*', $payload);
$this->redis->xAck(self::STREAM_KEY, self::CONSUMER_GROUP, [$messageId]);
}
}
}
private function syncOrderToErp(int $orderId, array $payload): void
{
$order = wc_get_order($orderId);
if (!$order) {
throw new \RuntimeException("Zamówienie WooCommerce #{$orderId} nie istnieje w bazie.");
}
// Przykład: Wywołanie dedykowanego klienta ERP
// $this->erpAdapter->createOrderDocument($order);
}
private function reclaimOrphanedMessages(): void
{
// Przegląd listy PEL pod kątem zadań zawieszonych > 60 sekund
$pending = $this->redis->xPending(self::STREAM_KEY, self::CONSUMER_GROUP, '-', '+', 5);
if (empty($pending)) {
return;
}
$staleIds = [];
foreach ($pending as $entry) {
$messageId = $entry[0];
$idleMs = $entry[2];
if ($idleMs > 60000) {
$staleIds[] = $messageId;
}
}
if (!empty($staleIds)) {
$claimed = $this->redis->xClaim(
self::STREAM_KEY,
self::CONSUMER_GROUP,
$this->consumerName,
60000,
$staleIds,
['JUSTID']
);
WP_CLI::line("Przejęto " . count($claimed) . " osieroconych zadań z nieaktywnych workerów.");
}
}
private function routeToDlq(string $messageId, array $payload, string $reason): void
{
$dlqEntry = [
'original_id' => $messageId,
'payload' => json_encode($payload, JSON_THROW_ON_ERROR),
'failure_reason' => $reason,
'failed_at' => (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))->format(\DateTimeInterface::ATOM),
'consumer' => $this->consumerName,
];
$this->redis->xAdd('erp:stream:dlq', '*', $dlqEntry);
WP_CLI::error("Komunikat {$messageId} przeniesiony do DLQ: {$reason}", false);
}
private function checkMemoryThreshold(): void
{
// Czyszczenie pamięci podręcznej obiektów WordPress i zapytań SQL
wp_cache_flush();
global $wpdb;
$wpdb->queries = [];
if (function_exists('gc_collect_cycles')) {
gc_collect_cycles();
}
$memoryUsed = memory_get_usage(true);
if ($memoryUsed >= self::MAX_MEMORY_BYTES) {
WP_CLI::line("Osiągnięto limit pamięci (" . round($memoryUsed / 1048576, 2) . " MB). Restart procesu...");
$this->shouldRun = false;
}
}
private function registerSignalHandlers(): void
{
if (!function_exists('pcntl_signal')) {
return;
}
pcntl_signal(SIGTERM, function () {
WP_CLI::line("Odebrano sygnał SIGTERM. Dokończenie bieżącego pakietu przed wyjściem...");
$this->shouldRun = false;
});
pcntl_signal(SIGINT, function () {
WP_CLI::line("Odebrano sygnał SIGINT. Zatrzymywanie daemona...");
$this->shouldRun = false;
});
}
private function initRedis(): void
{
$this->redis = new Redis();
$this->redis->connect('127.0.0.1', 6379, 2.5);
}
private function ensureConsumerGroup(): void
{
try {
$this->redis->xGroup('CREATE', self::STREAM_KEY, self::CONSUMER_GROUP, '0', true);
} catch (Throwable) {
// Grupa już istnieje w strumieniu
}
}
}
Listing 2: Bezpieczny kontroler webhooków z weryfikacją HMAC (WebhookController.php)
Kontroler REST API przyjmujący powiadomienia z systemu ERP (np. zmiany stanów magazynowych), weryfikujący podpis kryptograficzny HMAC SHA-256 w czasie stałym oraz zapisujący zdarzenia w kolejce Redis w czasie poniżej 20 ms.
<?php
declare(strict_types=1);
namespace WPPoland\ErpIntegration\Api;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use Redis;
use Throwable;
if (!defined('ABSPATH')) {
exit;
}
class WebhookController extends WP_REST_Controller
{
protected $namespace = 'erp-sync/v1';
protected $rest_base = 'webhook';
private const WEBHOOK_SECRET_OPTION = 'erp_webhook_hmac_secret';
private const MAX_TIMESTAMP_SKEW_SECONDS = 300; // 5-minutowe okno przeciw atakom powtórzeniowym
public function register_routes(): void
{
register_rest_route($this->namespace, '/' . $this->rest_base, [
[
'methods' => 'POST',
'callback' => [$this, 'handleIncomingWebhook'],
'permission_callback' => [$this, 'validateHmacSignature'],
],
]);
}
/**
* Bezpieczna weryfikacja podpisu HMAC i świeżości znacznika czasu.
*/
public function validateHmacSignature(WP_REST_Request $request): bool|WP_Error
{
$signatureHeader = $request->get_header('x-erp-signature-256');
$timestampHeader = $request->get_header('x-erp-timestamp');
if (empty($signatureHeader) || empty($timestampHeader)) {
return new WP_Error(
'rest_forbidden',
'Brak wymaganych nagłówków autoryzacyjnych: X-ERP-Signature-256 lub X-ERP-Timestamp.',
['status' => 401]
);
}
// Odrzucenie przeterminowanych zapytań
$requestTime = (int) $timestampHeader;
$currentTime = time();
if (abs($currentTime - $requestTime) > self::MAX_TIMESTAMP_SKEW_SECONDS) {
return new WP_Error(
'rest_forbidden',
'Znacznik czasu przekracza dozwolone okno tolerancji zegara (300 s).',
['status' => 403]
);
}
$rawBody = $request->get_body();
$secret = (string) get_option(self::WEBHOOK_SECRET_OPTION, '');
if (empty($secret)) {
return new WP_Error('rest_error', 'Klucz HMAC serwera nie został skonfigurowany.', ['status' => 500]);
}
$signedPayload = "t={$timestampHeader}.{$rawBody}";
$expectedSignature = hash_hmac('sha256', $signedPayload, $secret);
// Porównanie w stałym czasie odporne na ataki czasowe (timing attacks)
if (!hash_equals($expectedSignature, $signatureHeader)) {
return new WP_Error(
'rest_forbidden',
'Nieprawidłowy podpis kryptograficzny HMAC.',
['status' => 403]
);
}
return true;
}
/**
* Nieblokujący odbiornik zdarzeń.
*/
public function handleIncomingWebhook(WP_REST_Request $request): WP_REST_Response|WP_Error
{
$params = $request->get_json_params();
if (empty($params) || !is_array($params)) {
return new WP_Error('rest_bad_request', 'Niepoprawne ciało żądania JSON.', ['status' => 400]);
}
$eventId = $request->get_header('x-idempotency-key') ?: wp_generate_uuid4();
$eventType = sanitize_text_field((string) ($params['event_type'] ?? 'inventory_delta'));
try {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379, 1.0);
// Zapis do strumienia dla asynchronicznego przetworzenia
$streamPayload = [
'event_id' => $eventId,
'event_type' => $eventType,
'received_at' => (string) microtime(true),
'payload_json' => json_encode($params, JSON_THROW_ON_ERROR),
];
$messageId = $redis->xAdd('erp:stream:incoming_webhooks', '*', $streamPayload);
return new WP_REST_Response([
'status' => 'accepted',
'message_id' => $messageId,
'event_id' => $eventId,
], 202);
} catch (Throwable $e) {
return new WP_Error(
'rest_internal_error',
'Błąd buforowania webhooka: ' . $e->getMessage(),
['status' => 500]
);
}
}
}
Listing 3: Atomowe zarządzanie stanami z SELECT FOR UPDATE (StockManager.php)
Serwis bazodanowy odpowiedzialny za transakcyjne zmniejszanie stanów magazynowych w silniku MySQL InnoDB z obsługą zakleszczeń.
<?php
declare(strict_types=1);
namespace WPPoland\ErpIntegration\Database;
use wpdb;
use RuntimeException;
use Throwable;
if (!defined('ABSPATH')) {
exit;
}
class StockManager
{
private wpdb $db;
private const MAX_DEADLOCK_RETRIES = 3;
public function __construct()
{
global $wpdb;
$this->db = $wpdb;
}
/**
* Atomowe zmniejszenie stanu magazynowego z blokadą wiersza.
*
* @param int $productId Identyfikator produktu lub wariantu
* @param int $quantityToDeduct Liczba sztuk do odjęcia
* @return int Nowy stan magazynowy
* @throws RuntimeException Przy braku towaru lub nierozwiązanym zakleszczeniu
*/
public function deductStockAtomically(int $productId, int $quantityToDeduct): int
{
if ($quantityToDeduct <= 0) {
throw new \InvalidArgumentException("Liczba sztuk musi być większa od zera.");
}
$attempt = 0;
while ($attempt < self::MAX_DEADLOCK_RETRIES) {
$attempt++;
try {
$this->db->query('START TRANSACTION');
// Blokada wyłączna na wiersz metadanych stanu
$query = $this->db->prepare(
"SELECT meta_value FROM {$this->db->postmeta}
WHERE post_id = %d AND meta_key = '_stock'
FOR UPDATE",
$productId
);
$currentStockRaw = $this->db->get_var($query);
if ($currentStockRaw === null) {
throw new RuntimeException("Brak rekordu stanu dla produktu ID {$productId}.");
}
$currentStock = (int) $currentStockRaw;
if ($currentStock < $quantityToDeduct) {
$this->db->query('ROLLBACK');
throw new RuntimeException(
"Niewystarczający stan magazynowy. Żądano: {$quantityToDeduct}, Dostępne: {$currentStock}"
);
}
$newStock = $currentStock - $quantityToDeduct;
// Aktualizacja liczby sztuk
$this->db->update(
$this->db->postmeta,
['meta_value' => (string) $newStock],
['post_id' => $productId, 'meta_key' => '_stock'],
['%s'],
['%d', '%s']
);
// Zmiana statusu dostępności przy wyczerpaniu zapasów
if ($newStock === 0) {
$this->db->update(
$this->db->postmeta,
['meta_value' => 'outofstock'],
['post_id' => $productId, 'meta_key' => '_stock_status'],
['%s'],
['%d', '%s']
);
}
$this->db->query('COMMIT');
// Czyszczenie pamięci podręcznej obiektów
wp_cache_delete($productId, 'post_meta');
if (function_exists('wc_delete_product_transients')) {
wc_delete_product_transients($productId);
}
return $newStock;
} catch (Throwable $e) {
$this->db->query('ROLLBACK');
// Przechwycenie błędu zakleszczenia MySQL Deadlock Error 1213
$isDeadlock = str_contains($e->getMessage(), 'Deadlock found') ||
($this->db->last_error && str_contains($this->db->last_error, '1213'));
if ($isDeadlock && $attempt < self::MAX_DEADLOCK_RETRIES) {
// Wykładnicze wycofanie z szumem przed ponowieniem
$backoffUs = (int) (pow(2, $attempt) * 10000 + random_int(1000, 5000));
usleep($backoffUs);
continue;
}
throw new RuntimeException(
"Błąd transakcji magazynowej [Próba {$attempt}]: " . $e->getMessage(),
0,
$e
);
}
}
throw new RuntimeException("Przekroczono limit prób rozwiązania zakleszczenia dla produktu {$productId}.");
}
}
KSeF, numer faktury zwracany przez system i spójność z JPK_V7M
Polska specyfika integracji WooCommerce z ERP sprowadza się do jednego pytania: kto jest wystawcą faktury. Odpowiedź przesądza o kierunku synchronizacji i o tym, czy miesięczne uzgodnienie będzie automatyczne, czy ręczne.
W Krajowym Systemie e-Faktur faktura nie jest dokumentem, który sklep generuje i wysyła. Jest strukturą przesyłaną do systemu, który dopiero nadaje jej identyfikator. Ten identyfikator wraca i musi zostać zapisany po stronie zamówienia, inaczej powiązanie zamówienia z dokumentem księgowym trzeba odtwarzać ręcznie.
| Element | Źródło prawdy | Typowy błąd integracji |
|---|---|---|
| Numer nadany przez KSeF | system, nie wystawca | generowany lokalnie w sklepie |
| Numeracja własna serii | ERP | licznik trzymany w WooCommerce |
| Stawka i kod GTU | ERP | wyliczane z konfiguracji sklepu |
| Powiązanie z zamówieniem | pole zwrotne w zamówieniu | brak, uzgadnianie po kwocie |
Ostatni wiersz jest źródłem największej liczby godzin straconych na uzgadnianiu. Dopasowywanie dokumentów po kwocie działa do pierwszego dnia, w którym dwa zamówienia mają tę samą wartość.
JPK_V7M zamyka obieg. Plik powstaje z ksiąg, nie ze sklepu, więc każde zamówienie musi mieć drogę powrotną do dokumentu księgowego. Jeżeli synchronizacja jest jednokierunkowa, ze sklepu do ERP, to uzgodnienie miesięczne staje się pracą ręczną, a rozbieżność wykrywa się po terminie.
Stąd reguła projektowa dla rynku polskiego, ta sama, która obowiązuje w kolejce opisanej wyżej: sklep jest źródłem zamówienia, ERP jest źródłem dokumentu, a żadna stawka podatku nie ma sklepu za wyrocznię. Kolejka przenosi zamówienie w jedną stronę i identyfikator dokumentu w drugą.
Warto też ustalić z klientem na etapie analizy, kto odpowiada za korekty. Faktura korygująca jest osobnym dokumentem o własnym identyfikatorze, więc zwrot w sklepie nie może modyfikować pierwotnego zamówienia w miejscu. Musi utworzyć zdarzenie, dokładnie jak w opisanym wcześniej strumieniu.
Procedury uzgadniania danych i zapobieganie rozbieżnościom stanów
Nawet przy perfekcyjnie zaprojektowanych kolejkach i blokadach czynniki zewnętrzne - takie jak fizyczne zwroty w magazynie stacjonarnym, ręczna korekta na kasie POS czy przywrócenie bazy danych ze snapshotu - mogą doprowadzić do powstania rozbieżności pomiędzy WooCommerce a systemem ERP.
Wdrożenie korporacyjne wymaga precyzyjnego podziału ról oraz automatycznych procedur uzgadniania stanów.
Podział ról i nadrzędność systemów
Aby uniknąć konfliktów przy dwukierunkowej synchronizacji, granice systemowe muszą być ściśle określone:
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ MODEL PODZIAŁU ODPOWIEDZIALNOŚCI (GOVERNANCE) │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ SYSTEM ERP (NADRZĘDNE ŹRÓDŁO PRAWDY) │
│ │
│ - Rzeczywiste stany magazynowe w podziale na magazyny i lokalizacje │
│ - Nadrzędne cenniki B2B i B2C, progi rabatowe, waluty bazowe │
│ - Kartoteka towarowa (kody EAN, klasyfikacje celne, stawki VAT) │
│ - Księgowość, rozrachunki z kontrahentami i fakturowanie │
└───────────────────────────────────────────┬───────────────────────────────────────────┘
│
Autorytatywny zapis do e-commerce
│
▼
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ WOOCOMMERCE (FRONTEND I OBSŁUGA KOSZYKA) │
│ │
│ - Sesje zakupowe klientów i intencja złożenia zamówienia w czasie rzeczywistym │
│ - Treści marketingowe, opisy produktów, galerie mediów, struktura kategorii │
│ - Chwilowe rezerwacje koszykowe (czas życia blokady 5 minut) │
│ - Profile klientów detalicznych i historia transakcji internetowych │
└───────────────────────────────────────────────────────────────────────────────────────┘
- Stany magazynowe: ERP jest jedynym nadrzędnym źródłem prawdy. W przypadku rozbieżności wykrytej podczas audytu stan z systemu ERP bezwzględnie nadpisuje wartość w WooCommerce.
- Ceny i rabaty: ERP jest nadrzędny. WooCommerce wylicza orientacyjne kwoty w koszyku, lecz ostateczna wartość dokumentu sprzedaży jest kalkulowana i weryfikowana przez silnik taryfowy ERP.
- Nowe zamówienia: WooCommerce jest nadrzędny w momencie rejestracji transakcji. Po opłaceniu zamówienia w sklepie pakiet jest przekazywany do ERP. W chwili gdy ERP utworzy dokument i nada mu numer (
ERP_DOC_ID), przejmuje kontrolę nad dalszymi statusami realizacji i wysyłki.
Dwuetapowa architektura uzgadniania danych
Nowoczesny mechanizm uzgadniania działa na dwóch płaszczyznach czasowych:
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ DWUETAPOWE UZGADNIANIE DANYCH (RECONCILIATION) │
└─────────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ ETAP 1: COGODZINNA SYNCHRONIZACJA RÓŻNICOWA │
│ (Wysoka częstotliwość, lekki transfer, naprawa bieżących transakcji) │
│ │
│ Zapytanie do ERP i WooCommerce: │
│ WHERE updated_at >= NOW() - INTERVAL 90 MINUTE │
│ ──▶ Wykrycie zmienionych SKU i zamówień ──▶ Zapis do szybkiej kolejki naprawczej │
└───────────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ ETAP 2: NOCNY AUDYT KRYPTOGRAFICZNYCH SUM KONTROLNYCH │
│ (Pełna weryfikacja katalogu, zero narzutu na sieć, precyzyjna naprawa segmentów) │
│ │
│ Katalog WooCommerce (Pakiet 01: SKU 00001 - 00500) ──▶ SHA-256: 7f83b165... │
│ │ │
│ [Porównanie] │
│ │ │
│ Katalog ERP (Pakiet 01: SKU 00001 - 00500) ──▶ SHA-256: 7f83b165... │
│ │
│ - Sumy ZGODNE: Pakiet poprawny. Przejście do Pakietu 02. │
│ - Sumy NIEZGODNE: Uruchomienie precyzyjnej naprawy wyłącznie dla elementów Pakietu 01.│
└───────────────────────────────────────────────────────────────────────────────────────┘
Etap 1: Cogodzinna synchronizacja różnicowa
Co sześćdziesiąt minut proces audytowy sprawdza rekordy zmodyfikowane w ciągu ostatnich 90 minut (z 30-minutowym buforem zakładkowym):
- WooCommerce wyszukuje zamówienia z
date_updated_gmt >= (NOW() - INTERVAL 90 MINUTE). - ERP weryfikuje zmiany w rejestrze dokumentów handlowych.
- System porównuje statusy par. Każde brakujące zamówienie lub rozbieżność statusu trafia do priorytetowej kolejki naprawczej.
Etap 2: Nocny audyt sum kontrolnych SHA-256
Odpytywanie bazy o sto tysięcy produktów rekord po rekordzie przez interfejs API każdej nocy powodowałoby gigantyczne obciążenie łączy i serwerów. Zamiast tego stosuje się pakiety kryptograficzne:
- Cały katalog towarowy dzielony jest alfabetycznie na pakiety po 500 SKU (np. Pakiet 001: SKU od
A0001doA0500). - Dla każdego pakietu wyliczana jest suma kontrolna SHA-256 na podstawie posortowanych ciągów: $$\text{Hash} = \text{SHA256}\left(\sum_{i=1}^{500} \text{SKU}_i + \text{Cena}_i + \text{Stan}_i\right)$$
- Serwer ERP generuje analogiczne sumy dla tych samych pakietów.
- Skrypt porównuje 200 skrótów SHA-256.
- Jeśli 198 pakietów jest identycznych, 99 000 produktów jest w pełni spójnych bez konieczności transferu ich danych.
- Jedynie 2 pakiety z rozbieżnościami (1 000 SKU) są pobierane w celu szczegółowej synchronizacji, co redukuje transfer danych o 99%.
Rozwiązywanie incydentów produkcyjnych: playbook inżynierski
W warunkach produkcyjnych zespół techniczny musi dysponować gotowymi procedurami diagnostycznymi dla najczęstszych problemów integracyjnych.
Incydent 1: Zmiana kolejności doręczania webhooków (Out-of-order delivery)
- Objawy: Administrator oznacza zamówienie w WooCommerce jako “Anulowane”, jednak po kilku minutach status powraca do “W trakcie realizacji”, ponieważ opóźniony webhook z ERP został przetworzony z opóźnieniem.
- Przyczyna: Sieci asynchroniczne nie gwarantują kolejności pakietów. Webhook z wcześniejszym statusem dotarł po webhooku z nowszym statusem.
- Procedura naprawcza:
- Każdy pakiet webhooka musi zawierać numer wersji rekordu (
revision_id) lub precyzyjny znacznik czasu ISO UTC. - Tabela mapowania zamówień przechowuje numer ostatnio przetworzonej wersji.
- Aktualizacja bazy następuje wyłącznie pod warunkiem:
UPDATE wp_wc_orders_erp_lookup SET erp_status = :nowy_status, last_event_version = :wersja_pakietu WHERE order_id = :order_id AND last_event_version < :wersja_pakietu; - Jeśli zapytanie zaktualizowało 0 wierszy, pakiet jest ignorowany i odnotowywany w logach jako przedawniony.
- Każdy pakiet webhooka musi zawierać numer wersji rekordu (
Incydent 2: Zakleszczenia bazy danych (Deadlocks) podczas wyprzedaży
- Objawy: Użytkownicy otrzymują komunikaty o błędzie finalizacji koszyka. W logach MySQL pojawia się błąd
ERROR 1213: Deadlock found when trying to get lock. - Przyczyna: Dwa równoległe koszyki zawierały te same dwa produkty (Produkt A i Produkt B), lecz zostały zablokowane w odwrotnej kolejności (Transakcja 1: A -> B, Transakcja 2: B -> A).
- Procedura naprawcza:
- Wdrożenie kanonicznego sortowania blokad. Identyfikatory produktów w koszyku wielopozycyjnym muszą być zawsze sortowane rosnąco przed wywołaniem
SELECT ... FOR UPDATE:$productIds = [920, 114, 450]; sort($productIds, SORT_NUMERIC); // Wynik: [114, 450, 920] foreach ($productIds as $id) { $stockManager->deductStockAtomically($id, $items[$id]['qty']); } - Identyczna sekwencja blokowania we wszystkich wątkach eliminuje powstawanie cyklicznych zależności i całkowicie wyklucza zakleszczenia transakcyjne.
- Wdrożenie kanonicznego sortowania blokad. Identyfikatory produktów w koszyku wielopozycyjnym muszą być zawsze sortowane rosnąco przed wywołaniem
Metryki wydajnościowe i monitoring SLA
Utrzymanie integracji klasy enterprise wymaga ciągłego monitorowania kluczowych wskaźników telemetrycznych.
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ STOS MONITORINGU TELEMETRII │
└─────────────────────────────────────────────────────────────────────────────────────────┘
[Wątki PHP WooCommerce] ──▶ Ślady OpenTelemetry ──▶ [Jaeger / Tempo / Datadog]
[Broker Redis Streams] ──▶ Eksporter Prometheus ──▶ [Serwer Prometheus]
[Daemony WP-CLI w tle] ──▶ Metryki StatsD ──▶ │
▼
[Panele Grafana]
│
[Alertmanager]
│
┌─────────────────────┴─────────────────────┐
▼ ▼
[PagerDuty / Opsgenie] [Kanał Slack]
Kluczowe metryki operacyjne
| Nazwa metryki | Opis | Docelowe SLA | Próg ostrzegawczy | Próg incydentu krytycznego |
|---|---|---|---|---|
erp_queue_consumer_lag | Liczba nieprzetworzonych wiadomości w kolejce orders.incoming | < 100 komunikatów | > 500 przez 5 minut | > 2 500 lub wiek > 15 minut |
webhook_ingest_p95_ms | Czas weryfikacji podpisu i przyjęcia webhooka do kolejki | < 25 milisekund | > 75 milisekund | > 250 milisekund |
order_sync_latency_p99 | Czas od opłacenia zamówienia do utworzenia dokumentu w ERP | < 30 sekund | > 120 sekund | > 600 sekund |
dlq_occupancy_count | Liczba błędnych komunikatów w kolejce martwych listów | 0 komunikatów | > 10 komunikatów | > 50 komunikatów |
db_deadlock_rate | Częstotliwość zakleszczeń bazy na 1000 transakcji | 0,00% | > 0,10% (1 na 1000) | > 1,00% (10 na 1000) |
Podsumowanie i rekomendacje wdrożeniowe
Budowa stabilnej integracji WooCommerce z systemem ERP wymaga asynchronicznego odseparowania procesów, rygorystycznej kontroli idempotencji, transakcyjnego blokowania stanów magazynowych oraz automatycznego uzgadniania rozbieżności.
Jeśli planujesz wdrożenie dedykowanej integracji lub optymalizację istniejącej infrastruktury, skonsultuj się z naszym zespołem realizującym zaawansowane integracje WooCommerce z ERP oraz poznaj doświadczenie naszych programistów WooCommerce w WPPoland.




