Get a quote

كيف تبني نظام تتبع التوصيل والأسطول بالوقت الفعلي باستخدام Go

شركات التوصيل والخدمات اللوجستية في لبنان والمنطقة تواجه تحدياً تشغيلياً واضحاً: سائقون في الميدان، عملاء ينتظرون، ومدراء يحتاجون رؤية فورية. Go وWebSockets وPostgreSQL يُقدمون أساساً تقنياً ممتازاً لنظام تتبع مخصص.

لماذا يحتاج التتبع الفوري نظاماً خاصاً

شركات التوصيل والخدمات اللوجستية في لبنان والمنطقة تواجه تحدياً تشغيلياً واضحاً: سائقون في الميدان، عملاء ينتظرون التحديثات، ومدراء عمليات يحتاجون صورة فورية دقيقة. المنصات الجاهزة كـ Google Fleet Management تحل المشكلة للشركات الكبيرة بأسعار مرتفعة وقيود على التخصيص.

بناء نظام تتبع خاص بـ Go وWebSockets يمنحك تحكماً كاملاً في التكلفة، الخصوصية، والمنطق التشغيلي الخاص بعملياتك.

البنية الأساسية لنظام التتبع الفوري

نظام التتبع الفوري يتكون من ثلاثة طبقات متمايزة:

طبقة الاستقبال: تقبل تحديثات الموقع من تطبيق الهاتف عبر WebSocket أو HTTP POST. تتحقق من الهوية وتوجه للـ hub.

Hub التوزيع: يحتفظ بسجل السائقين النشطين والمشتركين في لوحة التحكم، يوجه التحديثات للمستأجرين الصحيحين، ويخزن آخر موقع معروف في Redis.

طبقة الحفظ: تكتب سجل الحركة في PostgreSQL باستخدام إدراج مجمّع مؤجل يخفض تكلفة الكتابة 80-90% مقارنة بالإدراج الفردي.

type TrackingHub struct {
    mu          sync.RWMutex
    subscribers map[string][]chan LocationUpdate
    lastKnown   map[string]LocationUpdate
}

func (h *TrackingHub) Broadcast(update LocationUpdate) {
    h.mu.Lock()
    h.lastKnown[update.DriverID] = update
    h.mu.Unlock()

    h.mu.RLock()
    subs := h.subscribers[update.TenantID]
    h.mu.RUnlock()

    for _, ch := range subs {
        select {
        case ch <- update:
        default:
            // المشترك بطيء، تخطَّ التحديث بدلاً من الحجب
        }
    }
}

الإرسال غير المحجوب (select default) ضروري. عميل لوحة تحكم بطيء لا يجب أن يحجب التحديثات عن باقي المشتركين.

مخطط PostgreSQL لسجل الحركة

تخزين كل تحديث موقع في جدول واحد يُشكّل مشكلة أداء وصيانة مع النمو. الجدول المقسّم زمنياً هو النهج الآمن للإنتاج:

CREATE TABLE driver_locations (
    id          BIGSERIAL,
    driver_id   UUID NOT NULL,
    tenant_id   UUID NOT NULL,
    lat         DOUBLE PRECISION NOT NULL,
    lng         DOUBLE PRECISION NOT NULL,
    speed       REAL,
    recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
) PARTITION BY RANGE (recorded_at);

CREATE INDEX ON driver_locations (driver_id, recorded_at DESC);
CREATE INDEX ON driver_locations (tenant_id, recorded_at DESC);

الأقسام الشهرية تعني أن حذف البيانات القديمة هو DROP TABLE على قسم واحد، لا DELETE كبير يُقفل الجدول.

الكتابة المجمّعة المؤجلة

مع 200 سائق نشط يرسل كل منهم تحديثاً كل 8 ثوانٍ، لديك 25 كتابة في الثانية. هذا قابل للإدارة لكنه ينمو بسرعة. الكتابة المجمّعة تخفض هذا بشكل كبير:

type LocationBuffer struct {
    mu      sync.Mutex
    pending []LocationUpdate
}

func (b *LocationBuffer) Flush(ctx context.Context, db *pgxpool.Pool) error {
    b.mu.Lock()
    batch := b.pending
    b.pending = b.pending[:0]
    b.mu.Unlock()

    if len(batch) == 0 {
        return nil
    }
    return bulkInsertLocations(ctx, db, batch)
}

مؤقت زمني يُنفّذ Flush كل 5 ثوانٍ. 200 سائق يولّدون 1600 تحديث خلال 5 ثوانٍ يُصبحون إدراجاً مجمّعاً واحداً.

Redis للموقع الأخير المعروف

مستخدمو لوحة التحكم يحتاجون الموقع الحالي لكل سائق نشط لحظة فتح الواجهة:

func (s *TrackingService) GetDriverLocation(ctx context.Context, driverID string) (*LocationUpdate, error) {
    key := fmt.Sprintf("driver:loc:%s", driverID)
    data, err := s.redis.Get(ctx, key).Bytes()
    if err == redis.Nil {
        return s.db.GetLastDriverLocation(ctx, driverID)
    }
    var loc LocationUpdate
    return &loc, json.Unmarshal(data, &loc)
}

Redis يتعامل مع القراءات الحساسة للتأخير. PostgreSQL يتعامل مع المتانة وسجل الحركة القابل للاستعلام.

حساب وقت الوصول المتوقع بدون تكاليف API خارجية

لحساب ETA تقديري بدون استدعاء Google Maps في كل طلب:

func haversineKm(lat1, lng1, lat2, lng2 float64) float64 {
    const R = 6371.0
    dLat := (lat2 - lat1) * math.Pi / 180
    dLng := (lng2 - lng1) * math.Pi / 180
    a := math.Sin(dLat/2)*math.Sin(dLat/2) +
        math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*
            math.Sin(dLng/2)*math.Sin(dLng/2)
    return R * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
}

للتطبيقات التي تحتاج دقة أعلى في أحوال سير المدن كبيروت والرياض، HERE Maps وTomTom يوفران بيانات حركة المرور الإقليمية بأسعار مناسبة.

الدروس الأساسية من الإنتاج

نمط الإرسال غير المحجوب ضروري. مشترك بطيء واحد لا يجب أن يُسبب تأخراً لجميع مستخدمي لوحة التحكم في نفس الـ tenant.

Redis للموقع الأخير المعروف وPostgreSQL للسجل هو التقسيم المعماري الصحيح. لا تحاول خدمة طلبات لوحة التحكم الفورية من PostgreSQL مباشرة.

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


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

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