نظام تتبع الطلبات في الوقت الفعلي ليس مجرد ميزة في SaaS، بل هو متطلب تشغيلي يحدد تجربة العميل وكفاءة الفريق. هذا المقال يشرح كيفية بناء نظام متكامل باستخدام آلة الحالات وقاعدة بيانات PostgreSQL مع التحديثات الفورية.
نظام تتبع الطلبات في الوقت الفعلي ليس مجرد ميزة في منصة SaaS، بل هو متطلب تشغيلي يحدد تجربة العميل وكفاءة الفريق. المشكلة ليست في وجود حالات للطلبات، فكل نظام يمتلكها. المشكلة هي في إدارة انتقالات الحالة بطريقة صحيحة، وإيصال التحديثات للعملاء في الوقت الفعلي دون إهدار الموارد بـ polling متكرر.
ما هو نظام تتبع الطلبات في الوقت الفعلي ولماذا يهم؟
النمط الأكثر شيوعاً في الفشل: تخزين حالة الطلب الحالية كعمود نص عادي في جدول قاعدة البيانات بدون قواعد انتقال مُطبَّقة في طبقة التطبيق. مطور يكتب استعلام تحديث حالة مباشرة لتغيير صف من "ملغي" إلى "قيد التحضير" للتعافي من خطأ. الاستعلام ينجح لأن قاعدة البيانات لا تعرف أن "ملغي" حالة نهائية.
النتيجة: تاريخ طلبات فاسد، أحداث downstream غير متسقة، وبطاقات دعم حيث الطلب يظهر كمكتمل وقيد التنفيذ في آن واحد.
آلة الحالات: الأساس الذي يمنع الفوضى
الحل هو تعريف آلة الحالات بشكل صريح في كود الـ backend يتحقق من كل انتقال قبل أي كتابة في قاعدة البيانات:
var allowedTransitions = map[OrderStatus][]OrderStatus{
StatusPending: {StatusConfirmed, StatusCancelled},
StatusConfirmed: {StatusPreparing, StatusCancelled},
StatusPreparing: {StatusReady},
StatusReady: {StatusDelivering, StatusCompleted},
StatusDelivering: {StatusCompleted},
StatusCompleted: {},
StatusCancelled: {},
}
func (s OrderStatus) CanTransitionTo(next OrderStatus) bool {
for _, a := range allowedTransitions[s] {
if a == next {
return true
}
}
return false
}
قبل أي تغيير في الحالة، تتحقق طبقة الخدمة من صحة الانتقال. إذا كان الانتقال غير مسموح به، تُرجع خطأً مكتوباً يظهر للمتصل كاستجابة 422.
حفظ الحالة في PostgreSQL مع سجل الأحداث
لا يكفي حفظ الحالة الحالية فقط. نحتاج إلى سجل كامل بكل انتقال حدث:
CREATE TABLE order_status_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id UUID NOT NULL REFERENCES orders(id),
tenant_id UUID NOT NULL,
from_status TEXT,
to_status TEXT NOT NULL,
changed_by UUID,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
metadata JSONB
);
كل انتقال حالة يكتب صفاً واحداً في هذا الجدول بشكل ذري مع التحديث على جدول الطلبات (معاملة واحدة، الكتابتان معاً أو لا شيء). هذا السجل يُمكّن من تتبعات التدقيق الكاملة وتحليل SLA لكل مستأجر.
التحديثات الفورية بـ Server-Sent Events
الطريقة الساذجة: العميل يسأل API كل ثانيتين عن حالة الطلب. مع 1000 طلب نشط متزامن، هذا يعني 500 استعلام قاعدة بيانات في الثانية لمجرد إيصال معلومة أن الطلب ما زال في نفس الحالة.
Server-Sent Events تُلغي حمل الـ polling. الخادم يحتفظ باتصال HTTP مفتوح مع العميل ويُرسل التحديثات فقط عندما تحدث:
func (h *Handler) StreamOrderStatus(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
orderID := chi.URLParam(r, "orderID")
updates := h.hub.Subscribe(orderID)
defer h.hub.Unsubscribe(orderID, updates)
for {
select {
case event := <-updates:
fmt.Fprintf(w, "data: %s\n\n", event)
w.(http.Flusher).Flush()
case <-r.Context().Done():
return
}
}
}
عند تغيير حالة الطلب في قاعدة البيانات، تُنشر إلى Hub بعد إتمام المعاملة. Hub ينشر التحديث لجميع اتصالات SSE المشتركة في هذا الطلب.
التعامل مع إعادة الاتصال والأحداث الفائتة
اتصالات SSE تنقطع عندما ينتقل العملاء بين الشبكات. آلية إعادة الاتصال الأصلية في SSE تُرسل آخر معرف حدث استقبله العميل في رأس Last-Event-ID عند إعادة الاتصال.
أدرج معرف حدث في كل رسالة SSE:
id: 1748358060-abc123
event: status_update
data: {"order_id":"abc123","status":"ready","updated_at":"2026-05-28T12:34:00Z"}
عند إعادة الاتصال، استعلم عن جدول order_status_events عن كل الأحداث الأحدث من timestamp معرف Last-Event-ID لهذا الطلب وبثّها قبل استئناف تدفق الأحداث المباشرة.
الاعتبارات متعددة المستأجرين
في بيئة SaaS متعددة المستأجرين، يجب تحديد نطاق اتصالات SSE للمستأجر المُصادق عليه. طبّق هذا في طبقة اشتراك Hub، وليس فقط على مستوى middleware المصادقة HTTP.
الدروس الأساسية من الإنتاج
حدّد انتقالات حالة الطلب بشكل صريح في الكود. سجّل كل انتقال في جدول أحداث. استخدم SSE بدلاً من polling للتسليم في الوقت الفعلي. أدرج معرفات الأحداث في رسائل SSE لدعم اللحاق عند إعادة الاتصال. حدّد نطاق اشتراكات Hub للمستأجر على مستوى الاشتراك.


