Get a quote

بناء نظام إدارة الطاولات والحجوزات للمطاعم باستخدام Go

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

كيف تبني نظام إدارة الطاولات والحجوزات للمطاعم باستخدام Go

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

نموذج البيانات الأساسي

ثلاثة كيانات رئيسية: الطاولات، الحجوزات، وتعيينات الطاولات.

CREATE TABLE tables (
    id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id  UUID NOT NULL,
    name       TEXT NOT NULL,
    capacity   INT NOT NULL,
    section    TEXT,
    is_active  BOOLEAN NOT NULL DEFAULT true
);

CREATE TABLE reservations (
    id             UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id      UUID NOT NULL,
    guest_name     TEXT NOT NULL,
    guest_phone    TEXT,
    party_size     INT NOT NULL,
    reserved_for   TIMESTAMPTZ NOT NULL,
    duration_mins  INT NOT NULL DEFAULT 90,
    status         TEXT NOT NULL DEFAULT 'confirmed'
);

CREATE TABLE table_assignments (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id       UUID NOT NULL,
    reservation_id  UUID REFERENCES reservations(id),
    table_id        UUID NOT NULL REFERENCES tables(id),
    starts_at       TIMESTAMPTZ NOT NULL,
    ends_at         TIMESTAMPTZ NOT NULL,
    assignment_type TEXT NOT NULL DEFAULT 'reservation'
);

جدول table_assignments هو مصدر الحقيقة لما هو مشغول خلال أي نافذة زمنية. كل من الحجوزات والزبائن بدون حجز ينشئون تعيينات.

الكشف عن التعارضات

الاستعلام الحرج: قبل تعيين طاولة، تحقق مما إذا كان أي تعيين موجود يتداخل مع النافذة الزمنية المطلوبة.

func (r *TableRepository) HasConflict(
    ctx context.Context,
    tenantID, tableID uuid.UUID,
    startsAt, endsAt time.Time,
) (bool, error) {
    query := `
        SELECT EXISTS (
            SELECT 1 FROM table_assignments
            WHERE tenant_id = $1
              AND table_id  = $2
              AND starts_at < $4
              AND ends_at   > $3
        )
    `
    var exists bool
    err := r.db.QueryRowContext(ctx, query, tenantID, tableID, startsAt, endsAt).Scan(&exists)
    return exists, err
}

شرط تداخل النافذة الزمنية starts_at < ends_at_new AND ends_at > starts_at_new هو الفحص القياسي لتداخل الفترات. يجب أن يعمل هذا الفحص داخل معاملة مع الإدراج، باستخدام SELECT ... FOR UPDATE على سجل الطاولة لمنع الطلبات المتزامنة من الاثنتين من تجاوز فحص التعارض.

التعامل مع الزبائن بدون حجز (Walk-in)

الزبائن بدون حجز هم تعيينات بدون حجز. يُنشئون عندما يصل ضيف بدون حجز. المدة تُقدَّر (عادةً 60-90 دقيقة):

func (s *TableService) SeatWalkIn(
    ctx context.Context,
    tenantID, tableID uuid.UUID,
    estimatedDurationMins int,
) (*TableAssignment, error) {
    now := time.Now()
    a := TableAssignment{
        TenantID: tenantID,
        TableID:  tableID,
        StartsAt: now,
        EndsAt:   now.Add(time.Duration(estimatedDurationMins) * time.Minute),
        Type:     "walk_in",
    }
    return &a, s.tables.AssignTable(ctx, a)
}

الاستعلام عن الطاولات المتاحة

عند أخذ حجز أو فحص التوفر لزبون بدون حجز، تحتاج جميع الطاولات ذات السعة الكافية التي لا تحتوي على تعيينات متعارضة:

SELECT t.id, t.name, t.capacity, t.section
FROM tables t
WHERE t.tenant_id = $1
  AND t.capacity  >= $2
  AND t.is_active = true
  AND NOT EXISTS (
      SELECT 1 FROM table_assignments a
      WHERE a.table_id  = t.id
        AND a.starts_at < $4
        AND a.ends_at   > $3
  )
ORDER BY t.capacity ASC

الترتيب تصاعدياً حسب السعة يُرجع أصغر طاولة تناسب الحفلة أولاً، وهو مهم لتحسين استخدام الطاولات.

عرض الطوابق في الوقت الفعلي

عرض الطابق يُظهر جميع الطاولات مع حالتها الحالية. طاولة إما متاحة، أو مشغولة، أو محجوزة قريباً (تعيين قادم خلال 30 دقيقة)، أو محجوزة مستقبلاً.

للتحديثات الفورية، نستخدم PostgreSQL LISTEN/NOTIFY لدفع تغييرات حالة الطاولة إلى العملاء الويب المتصلين بدلاً من الاستطلاع المستمر. عند إنشاء تعيين أو تحديثه أو حذفه، تُطلق دالة مُشغِّلة إشعاراً تستمع إليه goroutine في Go وتُعيده عبر WebSocket.

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

  • استخدم جدول table_assignments منفصلاً كمصدر حقيقة للإشغال، وليس عموداً للحالة على جدول الطاولات.
  • يجب أن يعمل الكشف عن التعارضات داخل معاملة مع قفل صف لضمان السلامة من حالات السباق.
  • الزبائن بدون حجز والحجوزات كلاهما سجل table_assignments بنوع حقل مختلف.
  • ادفع تغييرات الحالة عبر WebSocket بدلاً من الاستطلاع.

نبني أنظمة مطاعم تشغيلية للعملاء في لبنان وعبر الشرق الأوسط. إذا كنت تبني ميزات POS أو حجوزات وتريد الحصول على البنية الصحيحة، تواصل معنا عبر https://voxire.com/get-a-quote/

Voxire

تطوير منتجات SaaS

من الفكرة إلى المنتج المطلوق - استراتيجية وبنية ومطوّر كامل الخدمات.

تعرف على المزيد
العودة إلى المدونة
Chat on WhatsApp