هندسة التجارة الإلكترونية · Shopify

Shopify Webhooks الموثوقة: صندوق دائم ومطابقة الحالة

تصميم إنتاجي للتحقق من Shopify Webhooks وإزالة تكرارها وحماية ترتيبها واستعادة فجوات مزامنة الطلبات والمخزون.

المسار المرتبطهندسة التجارة الإلكترونية
رسم معماري تصوري لأحداث تجارة تمر عبر صندوق وارد دائم محمي وعامل معالجة وحلقة مطابقة.
تصوّر بصري للفكرة — يتبعه شرح ومخطط تنفيذي داخل المقال.

تكامل Shopify Webhooks الموثوق لا يعتبر نجاح طلب HTTP اكتمالًا للحدث التجاري. بل يتحقق من HMAC على الجسم الخام، ويحفظ كل عملية تسليم في صندوق وارد دائم قبل الرد، ويمنع التكرار عبر معرّف التسليم، وينفذ الآثار الجانبية خارج مسار الطلب، ثم يطابق الحالة مع Admin API. هذا التصميم مهم للتطبيقات التي تزامن الطلبات أو المخزون أو الشحن؛ لأن وثائق Shopify تنص على احتمال التكرار، وعدم ضمان الترتيب، وعدم ضمان وصول كل Webhook أصلًا.

هذا دليل معماري عملي مبني على وثائق Shopify الرسمية التي راجعتها في ٥ أكتوبر ٢٠٢٦، وليس إعلانًا عن ميزة جديدة. مخطط صندوق الوارد والطوابير ومفاتيح Idempotency وضوابط الاستعادة توصيات تصميم داخل التطبيق، بينما تحدد وثائق Shopify عقد التسليم والرؤوس الرسمية.

ابدأ من عقد التسليم

Shopify Webhooks إشعارات شبه فورية تقلل الحاجة إلى Polling المتكرر. يشترك التطبيق في Topics، ثم يستقبل طلبات HTTPS عند حدوث تغييرات مطابقة. لهذا تصلح Webhooks كمحفزات سريعة، لكنها ليست سجل Change Data Capture مرتبًا. توضح Shopify صراحة أن الترتيب غير مضمون داخل Topic واحد أو بين Topics، وأن التطبيق لا ينبغي أن يعتمد عليها وحدها لأن التسليم نفسه غير مضمون.

ميزانية النقل ضيقة عمدًا. توثق Shopify مهلة ثانية واحدة لإنشاء الاتصال وخمس ثوانٍ للطلب كاملًا. وأي رد خارج نطاق 200 يعد فشلًا. النتيجة العملية: يجب أن ينفذ المستقبل أقل عمل متزامن ممكن؛ قراءة الجسم دون تعديل، والتحقق من التوقيع، وإدخال سجل واحد دائم، ثم رد 200. تحليل رسم كبير أو استدعاء API آخر أو تحديث خمس جداول تجارية قبل الرد يستهلك ميزانية الموثوقية في المكان الخطأ.

قد تعيد Shopify محاولة تسليم HTTPS الفاشل حتى ثماني مرات خلال أربع ساعات. وإذا كان الاشتراك منشأ عبر Admin API فقد تحذفه المنصة بعد استمرار الفشل. أما الاشتراكات الخاصة بالتطبيق والمعلنة في إعداداته فلها سلوك Lifecycle مختلف ولا تحذف تلقائيًا بالشرط نفسه. راقب نوع الاشتراك الفعلي بدل افتراض أن إعادة المحاولة ستحميه إلى الأبد.

تحقق من الجسم الخام قبل الثقة

توقّع Shopify طلب HTTPS عبر HMAC في الرأس `X-Shopify-Hmac-SHA256`. يجب أن يستخدم التحقق جسم الطلب الخام وClient Secret الخاص بالتطبيق. إذا حلل Framework صيغة JSON أو عدل المسافات أو الترميز قبل التحقق، فقد تختلف البصمة المحسوبة رغم صحة الطلب. ترتيب Middleware جزء من الحد الأمني، وليس تفصيلًا تنظيميًا.

قارن البصمات بعملية ثابتة زمنيًا، وارفض الطلب غير الصحيح قبل استخدام اسم المتجر أو Topic أو حقول Payload. احتفظ بالسر في مخزن أسرار مدار، وادعم التدوير المتعمد. لا تسجل الجسم الخام بلا ضوابط؛ فقد تحتوي الطلبات والعملاء على بيانات شخصية. سجل أمني مفيد يحتفظ بمعرّف الطلب وTopic وهوية المتجر بعد التحقق والنتيجة وبصمة محدودة للحمولة.

const raw = await readRawBody(request);
const supplied = request.headers.get('x-shopify-hmac-sha256');
if (!verifyShopifyHmac(raw, supplied, activeSecrets)) {
  return new Response('invalid signature', { status: 401 });
}

await inbox.insertIfAbsent({
  shop: request.headers.get('x-shopify-shop-domain'),
  topic: request.headers.get('x-shopify-topic'),
  deliveryId: request.headers.get('x-shopify-webhook-id'),
  eventId: request.headers.get('x-shopify-event-id'),
  triggeredAt: request.headers.get('x-shopify-triggered-at'),
  rawBody: encrypt(raw),
  receivedAt: now()
});
return new Response(null, { status: 200 });

هذا مثال محايد لتطبيق العقد، وليس نسخة من Shopify SDK. استخدم مكتبة Shopify الرسمية وإرشادات Runtime الخاصة بإطارك، واختبر التوقيع على تسلسل البايت نفسه الذي يصل إلى الإنتاج.

ابن صندوق وارد دائمًا قبل الطابور

الطابور ليس صندوق وارد تلقائيًا. إذا أرسل المستقبل رد 200 قبل النشر إلى Queue، فإن سقوط العملية بين الخطوتين يفقد الحدث. وإذا نشر أولًا ثم سقط قبل الرد، فقد تعيد Shopify التسليم وينشأ تكرار. الحد الآمن هو إدخال دائم واحد مع Unique Constraint على `(shop_id, delivery_id)` ثم رد 200. بعد ذلك يرسل Dispatcher مستقل الصفوف المثبتة إلى طابور العمل.

احفظ معرّف التسليم، ومعرّف الحدث إن وجد، وTopic، والمتجر بعد التحقق، وإصدار API، ووقت إنشاء الحدث، ووقت الاستلام، والحمولة المشفرة أو مرجعًا متوافقًا، وحالة المعالجة، وعدد المحاولات، وآخر خطأ. اجعل مدة الاحتفاظ متناسبة مع الحاجة إلى Replay والتدقيق، ولا تحول الصندوق إلى نسخة أبدية من بيانات العملاء. يفيد Transactional Outbox عندما تكون قاعدة صندوق الوارد وMessage Broker نظامين مختلفين.

تفرق الوثائق بين معرّفين. `X-Shopify-Webhook-Id` يعرّف عملية التسليم ويناسب إزالة التكرار. أما `X-Shopify-Event-Id` فيربط عدة رسائل Webhook ناتجة عن إجراء تاجر واحد. لا تضعهما في حقل واحد. يمكن لاشتراكين مختلفين إنتاج عمليتي تسليم مرتبطتين بمعرّف حدث واحد، بينما لا ينبغي لمعرّف التسليم نفسه تنفيذ الأثر مرتين.

تحقق من الطلب الخام، واحفظ كل تسليم قبل الرد، ونفذ الآثار Idempotent خارج مسار الطلب، ثم أصلح التغييرات المفقودة عبر المطابقة.
تحقق من الطلب الخام، واحفظ كل تسليم قبل الرد، ونفذ الآثار Idempotent خارج مسار الطلب، ثم أصلح التغييرات المفقودة عبر المطابقة. اضغط لعرض أكبر

اجعل كل أثر جانبي Idempotent

إزالة تكرار طلب HTTP ضرورية لكنها لا تكفي. قد ينهي Worker استدعاء نظام خارجي ثم يسقط قبل وضع علامة الاكتمال على سجل Inbox. تعيد المحاولة التالية الاستدعاء. لذلك ضع حد Idempotency حول كل أثر تجاري، لا حول الاستلام فقط.

في إسقاط الطلبات يمكن استعمال مفتاح مثل `(shop_id, source_order_id, source_updated_at, projection_version)`. وفي إجراء شحن خارجي، احفظ سجل عملية بمفتاح تجاري ثابت قبل استدعاء شركة الشحن أو ERP. إذا كان المزود يدعم Idempotency Key فأرسل المفتاح نفسه في كل محاولة. وإن لم يدعمه، فابحث عن الحالة الحالية قبل الإنشاء، وحوّل النتيجة الغامضة إلى مراجعة.

اجعل مراحل المعالج صريحة: تحقق من Schema، وحمّل سياسة Tenant، واقرأ نسخة المورد، واحسب الانتقال المقصود، واكتب الحالة المحلية، ونفذ الآثار الخارجية المحمية، ثم سجل الاكتمال. يجب أن تستأنف إعادة المحاولة المرحلة أو تعيدها بأمان. Exactly once ليست وعدًا من النقل؛ بل خاصية يبنيها التطبيق من القيود والمعاملات والآثار Idempotent.

احم الحالة من الأحداث غير المرتبة

ترتيب الوصول ليس ترتيب العمل. قد تصل `orders/updated` قبل `orders/create` الأقدم، وقد تتقاطع عمليتا تحديث في الطريق. توصي Shopify باستخدام `X-Shopify-Triggered-At` أو طابع المورد مثل `updated_at` لفهم وقت الحدث. عامل هذه القيم كدليل للمقارنة، لا كإذن بالكتابة العمياء.

احتفظ لكل مورد بآخر Source Version أو `updated_at` مقبول. طبّق الحدث فقط إذا قدم الإسقاط، أو اقرأ الحالة الرسمية الحالية إذا كان الانتقال غامضًا. يبقى الحدث القديم مفيدًا للتدقيق، لكنه لا ينبغي أن يعيد طلبًا مدفوعًا إلى حالة سابقة. وإذا ساهم أكثر من Topic في Aggregate واحد، فعرّف قاعدة دمج واضحة بدل الاعتماد على ترتيب عالمي لا توفره Shopify.

يمكن تقسيم Worker Stream حسب المتجر ومعرّف المورد لتقليل السباقات، لكنه لا يعيد معلومة لم تصل أصلًا. كما قد يصنع Hot Partition في المتاجر الكبيرة. استخدم الترتيب المحدود عندما يحمي Invariant واضحًا، واترك الموارد المستقلة تعمل بالتوازي.

نفذ Reconciliation لأن التسليم غير مضمون

حلقة الاستعادة ليست Script للطوارئ؛ بل جزء من التصميم الطبيعي. توصي Shopify بمهام مطابقة عندما لا يتحمل التطبيق فقد التغييرات، وباستخدام مرشحات Admin API مثل `updated_at` لجلب ما تغير منذ آخر مهمة ناجحة. احتفظ Checkpoint لكل متجر، واقرأ نافذة متداخلة لأن الساعات وPagination وظهور المعاملات قد تصنع فجوة عند الحدود.

يمكن لمهمة تعمل كل عشر دقائق أن تبدأ من `last_checkpoint - overlap`، وتتابع الصفحات حتى Safe Horizon الحالية، ثم تمرر كل نتيجة إلى مسار الإسقاط Idempotent نفسه المستخدم مع Webhooks. لا تقدم Checkpoint إلا بعد تثبيت كل الصفحات. التداخل ينتج تكرارات عمدًا، والنظام مصمم أصلًا لامتصاصها.

لا تطابق كامل الكتالوج لكل متجر في كل تشغيل. استخدم Cursors خاصة بالمورد وميزانية Rate Limit وأولوية حسب الخطر التجاري. قد تحتاج الطلبات والشحن إلى Recovery Objective أقصر من وصف المنتج. ونفذ جردًا دوريًا للاشتراكات الفعالة، خصوصًا الاشتراكات الخاصة بالمتجر المنشأة عبر Admin API والتي قد تحذف بعد فشل متكرر.

اختر الاشتراكات وإصداراتها بوعي

توصي Shopify بالاشتراكات الخاصة بالتطبيق في `shopify.app.toml` للأحداث المطلوبة في كل تثبيت. تنشر مع إعداد التطبيق وتقدم عقدًا واحدًا تحت Source Control. أما Shop-specific subscriptions المنشأة عبر GraphQL Admin API فتلائم الاحتياجات الخاصة بكل تاجر أو وقت التشغيل. سجل سبب كل اشتراك ومالكه ومجموعة Topics المتوقعة.

استخدم Filters لتقليل الأحداث غير المهمة و`include_fields` عندما تكفي حمولة أصغر فعلًا. تخفف هذه الأدوات الشبكة والتحليل، لكنها عقود بيانات أيضًا. Filter يستبعد انتقالًا مهمًا أو حقلًا تحتاجه الاستعادة قد يكسر الإسقاط بصمت. اختبر موارد نموذجية عند الحدود، واحتفظ Contract Test لكل Filter مهيأ.

تأخذ حمولة Webhook إصدار Shopify API. ثبّت الإصدار المقصود، وافحص رأس الإصدار المسلم، وتمرن على الترقية بFixtures ملتقطة بطريقة متوافقة. حدّث Parsers بإضافات متوافقة أولًا، وانشرها قبل تغيير إصدار الاشتراك، واحتفظ بنافذة توافق قصيرة. Endpoint يقبل Schema واحدًا حرفيًا يجعل ترقية إصدار المنصة أخطر من اللازم.

راقب المسار كاملًا لا أخطاء HTTP فقط

تركز إرشادات Shopify على عدد عمليات التسليم ونسبة الفشل وزمن الرد. أضف إشارات تكشف الانحراف الصامت: نسبة رفض HMAC، وزمن إدخال Inbox، ونسبة التسليم المكرر، وعمر أقدم سجل غير معالج، ومحاولات Worker، وعدد Dead Letters، وتأخر الحدث حتى الإسقاط، والفجوات التي اكتشفتها المطابقة، واختلاف جرد الاشتراكات.

قس p90 وp99 لزمن الرد تحت سقف الخمس ثوانٍ؛ فقد يخفي المتوسط ذيلًا بدأ يسبب إعادة المحاولة. أنشئ تنبيهًا منفصلًا عندما يكون المستقبل سليمًا لكن Backlog العامل ينمو. رد 200 يثبت القبول الدائم فقط إذا كان حد الإدخال صحيحًا؛ ولا يثبت وصول الطلب إلى ERP.

تتبع حدثًا واحدًا بمعرّفات داخلية بدل تفاصيل العميل. الحقول المفيدة هي معرّف التسليم والحدث والمتجر وTopic والمورد وطابع المصدر وسجل Inbox ومحاولة العامل ومفتاح الأثر. لا تستخدم معرفات المتاجر والطلبات كـMetric Labels غير محدودة؛ مكانها Log أو Trace مضبوط الوصول.

اعرف متى لا تكفي Webhooks

استخدم Webhooks للمحفزات قليلة التأخير والمزامنة التزايدية. استخدم Reconciliation للاكتمال. واستخدم Bulk Export أو Job بصفحات للتهيئة الأولى أو ترحيل Schema أو تدقيق شامل. وقد يكون Polling أبسط لتكامل صغير بمتطلبات حداثة مرنة وعدد متاجر قليل، مع احترام حدود API وCheckpoints.

لا تعتبر جسم Webhook دليلًا دائمًا على الحالة الحالية عندما يحتاج القرار إلى أحدث مورد رسمي. اجلب الحالة الحالية بعد دمج الدفعات إذا احتاج القرار Object كاملًا. وفي الاتجاه المقابل، لا تنفذ Admin API Read لكل حدث إذا احتوت الحمولة الحقول الثابتة المطلوبة؛ فهذا يضيف تأخيرًا وضغط Rate Limit بلا فائدة.

القاعدة الإنتاجية واضحة: تحقق من البايت قبل الثقة، واحفظ قبل الرد، وميز عملية التسليم من الحدث، واجعل الآثار Idempotent، وقارن نسخ المصدر بدل ترتيب الوصول، وأصلح الفجوات عبر Reconciliation. بهذه المعمارية تتحول Shopify Webhooks من Callback هش إلى إشارة مزامنة موثوقة. ولأنماط أوسع، اقرأ تصميم معالجة Webhooks الموثوقة ودليل استعادة Webhooks في سلة وزد وخدمات تكاملات Backend API.

المراجع الرسمية

تدعم هذه المراجع سلوك الأدوات المذكورة. الأمثلة وقرارات التصميم توضيحية، ويجب تكييفها مع متطلبات المشروع وإصداراته.

إعداد: Noor Yasser

من القرار إلى التنفيذ

تعمل على تحدٍ تقني مشابه؟

أساعد الفرق على تحويل القرار المعماري إلى نطاق واضح وتنفيذ يمكن تشغيله ومراجعته بثقة.

احجز لقاءً لمدة ٣٠ دقيقةالخدمة المرتبطةهندسة الأنظمة الخلفية وتكاملات APIمشروع من الأعمالMember Plus