Get a quote

بناء نظام إشعارات متعدد القنوات في SaaS: واتساب والبريد الإلكتروني والرسائل النصية

مستخدمو MENA يفضلون واتساب على البريد الإلكتروني بفارق كبير. بناء نظام إشعارات يدعم قنوات متعددة مع تتبع التسليم والمحاولة التلقائية هو متطلب أساسي لأي SaaS في المنطقة.

لماذا قناة الإشعارات الواحدة لا تكفي في MENA

في لبنان وعموم منطقة MENA، تجاوز واتساب البريد الإلكتروني والرسائل النصية ليصبح القناة الافتراضية للرسائل التحويلية. سلسلة مطاعم في بيروت ترسل تأكيدات الطلبات عبر البريد الإلكتروني وحده تخسر نسبة كبيرة من معدلات القراءة. المستخدمون في هذا السوق يتحققون من واتساب خلال دقائق من استلام الرسالة.

هذا ليس تفضيلاً يمكن لمنتجات SaaS تجاهله. بناء نظام إشعارات يوجّه إلى القناة الصحيحة وفقاً لتفضيل المستخدم هو متطلب هندسي أساسي لأي منتج يعمل في هذه المنطقة.

تصميم طبقة التجريد: interface للمزودين في Go

الأساس هو interface واحدة تُلبّيها جميع تطبيقات القنوات:

type Message struct {
    To             string
    Subject        string
    Body           string
    TemplateID     string
    TemplateParams map[string]string
}

type Notifier interface {
    Send(ctx context.Context, msg Message) (DeliveryResult, error)
    Channel() string
}

كل قناة (واتساب، بريد إلكتروني، رسائل نصية) تُطبّق هذه الواجهة. الموجّه يحتفظ بخريطة تطبيقات القنوات وترتيب الاحتياط:

func (r *Router) Notify(ctx context.Context, userID uuid.UUID, msg Message, preferred string) error {
    chain := r.buildChain(preferred)
    for _, channel := range chain {
        provider := r.providers[channel]
        result, err := provider.Send(ctx, msg)
        r.logAttempt(ctx, userID, msg, channel, result, err)
        if err == nil {
            return nil
        }
    }
    return ErrAllChannelsFailed
}

دعم واتساب API (Meta Business API)

يستخدم RTYLR Cloud API من Meta للواتساب. القيد الأساسي هو الرسائل القائمة على القوالب للإشعارات الصادرة. القوالب تُعرَّف في Business Manager، تُراجَع من Meta، وتُعتمد قبل الاستخدام:

func (w *WhatsAppProvider) Send(ctx context.Context, msg Message) (DeliveryResult, error) {
    payload := WhatsAppMessage{
        MessagingProduct: "whatsapp",
        To:               msg.To,
        Type:             "template",
        Template: &WATemplate{
            Name:     msg.TemplateID,
            Language: WALanguage{Code: "ar"},
            Components: buildComponents(msg.TemplateParams),
        },
    }
    // إرسال الطلب واستخراج معرف الرسالة للتتبع
}

إيصالات التسليم تصل عبر webhook. RTYLR يُسجّل نقطة webhook يستدعيها Meta مع تحديثات الحالة (أُرسل، وُصّل، قُرئ، فشل).

دعم البريد الإلكتروني (Mailgun)

func (m *MailgunProvider) Send(ctx context.Context, msg Message) (DeliveryResult, error) {
    message := m.client.NewMessage(m.from, msg.Subject, "", msg.To)
    message.SetHtml(msg.Body)
    message.SetTracking(true)
    _, id, err := m.client.Send(ctx, message)
    if err != nil {
        return DeliveryResult{}, err
    }
    return DeliveryResult{ProviderMessageID: id, Status: "queued"}, nil
}

Mailgun يوفر webhooks لأحداث الارتداد، وإلغاء الاشتراك، وتأكيدات التسليم.

دعم الرسائل النصية (Twilio و Unifonic)

لـ SMS، قيّم فريق RTYLR مزوّدَين: Twilio للتغطية العالمية و Unifonic للتوجيه الخاص بـ MENA. لدى Unifonic اتصالات مباشرة مع شركات الاتصالات في السعودية والإمارات وأسواق الخليج الأخرى، مما يقلل الكمون ويحسن معدلات التسليم.

اختيار المزود قابل للتكوين لكل مستأجر: عميل في السعودية يستخدم Unifonic، بينما عميل يحتاج تغطية عالمية يستخدم Twilio. كلاهما يُطبّق نفس واجهة Notifier.

جدول notification_logs في PostgreSQL

CREATE TABLE notification_logs (
    id                   UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id            UUID NOT NULL REFERENCES tenants(id),
    user_id              UUID NOT NULL REFERENCES users(id),
    channel              TEXT NOT NULL,
    template_id          TEXT,
    provider_message_id  TEXT,
    status               TEXT NOT NULL DEFAULT 'queued',
    error_message        TEXT,
    sent_at              TIMESTAMPTZ,
    delivered_at         TIMESTAMPTZ,
    failed_at            TIMESTAMPTZ,
    created_at           TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_notif_logs_tenant ON notification_logs(tenant_id, created_at DESC);
CREATE INDEX idx_notif_logs_provider_msg ON notification_logs(provider_message_id)
    WHERE provider_message_id IS NOT NULL;

فهرس provider_message_id يُستخدم للبحث عن السجلات عندما تصل webhooks إيصالات التسليم. انتقالات الحالة تتبع دورة حياة محددة: من queued إلى sent (قبلت الواجهة الرسالة) إلى delivered (أكّد المزود التسليم) أو failed.

المحاولة التلقائية مع التراجع الأسي

func (r *Router) sendWithRetry(ctx context.Context, provider Notifier, msg Message, maxAttempts int) (DeliveryResult, error) {
    backoff := 500 * time.Millisecond
    var lastErr error
    for attempt := 0; attempt < maxAttempts; attempt++ {
        result, err := provider.Send(ctx, msg)
        if err == nil {
            return result, nil
        }
        lastErr = err
        if !isTransient(err) {
            return DeliveryResult{}, err
        }
        select {
        case <-ctx.Done():
            return DeliveryResult{}, ctx.Err()
        case <-time.After(backoff):
            backoff = min(backoff*2, 30*time.Second)
        }
    }
    return DeliveryResult{}, fmt.Errorf("تجاوز الحد الأقصى للمحاولات: %w", lastErr)
}

isTransient ترجع true لأخطاء الشبكة، وHTTP 429 (تجاوز الحد)، واستجابات 5xx. ترجع false لأخطاء 4xx التي تشير إلى مشكلة دائمة.

خبرة من RTYLR: إشعارات طلبات المطاعم

في نشر RTYLR الإنتاجي لسلاسل المطاعم اللبنانية، يعمل تدفق إشعار تأكيد الطلب كالتالي: عند تقديم طلب، تنشر خدمة الطلبات حدثاً. عامل الإشعارات يلتقط الحدث، يحمّل تفضيلات قناة المستخدم، ويستدعي الموجّه. لمعظم العملاء اللبنانيين، القناة المفضلة هي واتساب.

عملية اعتماد قالب واتساب استغرقت أسبوعين للمجموعة الأولى من قوالب RTYLR. كل قالب يُقدَّم بالعربية والإنجليزية. التخطيط لهذا الوقت الإضافي جزء من عملية إعداد العميل.

هل تحتاج إلى بنية تحتية لإشعارات متعددة القنوات لمستخدمي MENA؟ فريق هندسة Voxire يمكنه تصميم وبناء هذا النظام. ابدأ المحادثة على https://voxire.com/get-a-quote/

العودة إلى المدونة
Chat on WhatsApp