הזמנות כפולות הן כאב ראש מתמיד עבור בעלי שירותים עם הזמנות מקוונות. כאשר שני לקוחות מזמינים בו-זמנית את אותו משבצת זמן, נגרמים אובדן כסף ואמון. תוספי CMS סטנדרטיים פותרים רק חצי מהבעיה: בדיקות בצד האפליקציה לא יכולות להתמודד עם גישה מקבילה. אנו בונים מערכות הזמנות מותאמות אישית שמבטלות הזמנות כפולות ברמת מסד הנתונים באמצעות PostgreSQL אילוצי exclusion. הלקוחות שלנו — מרפאות, מכוני יופי, חללי עבודה משותפים ומרכזי שירות — מקבלים פתרון שמתאים לאלפי משבצות ומסתגל לכל כללי עסקיים: מפגשים קבוצתיים, משאבים מרובים לשירות, לוחות זמנים מורכבים עם הפסקות. ביותר מ-15 פרויקטים שהושלמו, מעולם לא ראינו תקלה אחת של הזמנה כפולה. עומס ממוצע — עד 10,000 הזמנות ביום, זמן תגובת API מתחת ל-200 אלפיות השנייה. מנהלים חוסכים עד 70% מזמנם בניהול לוח הזמנים, ותזכורות אוטומטיות מפחיתות את שיעור אי-ההתייצבות ב-25%.
למה אילוץ exclusion עדיף על פני גישות אחרות?
נשווה שלוש גישות למניעת הזמנות כפולות:
| גישה | אמינות | ביצועים | מורכבות יישום |
|---|---|---|---|
| בדיקה בצד האפליקציה | בינונית: תנאי מרוץ אפשריים | גבוהה | נמוכה |
| נעילה ברמת מסד הנתונים (SELECT FOR UPDATE) | גבוהה | בינונית: חסימת שורות | בינונית |
אילוץ exclusion (EXCLUDE USING gist) |
מקסימלית: אטומי | גבוהה: בדיקה אחת | גבוהה: דורש btree_gist |
אילוץ exclusion אמין פי 100 מאשר בדיקות בצד האפליקציה. החיסרון היחיד הוא הצורך בהרחבת btree_gist וטיפול בחריגה 23P01 בקוד. במשך שנים של ניסיון, סיפקנו 15+ פרויקטי הזמנות וראינו אפס תקלות של הזמנות כפולות.
איך פועלת יצירת הזמנה אטומית?
הפתרון מבוסס על ארבע ישויות: משאב (רופא, שולחן, חדר), לוח זמנים (שעות פעילות + חריגים), משבצת (זמן פנוי) והזמנה. הסכימה כוללת אילוץ EXCLUDE USING gist שבודק באופן אטומי חפיפת טווחי זמן עבור כל משאב. בהכנסות מקבילות, מסד הנתונים דוחה הזמנות מתנגשות, והאפליקציה מטפלת בחריגה ומודיעה ללקוח.
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE bookable_resources (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
type VARCHAR(50) NOT NULL, -- staff | room | equipment | table
capacity INTEGER NOT NULL DEFAULT 1,
is_active BOOLEAN NOT NULL DEFAULT true,
meta JSONB NOT NULL DEFAULT '{}'
);
CREATE TABLE resource_schedules (
id BIGSERIAL PRIMARY KEY,
resource_id BIGINT NOT NULL REFERENCES bookable_resources(id),
day_of_week SMALLINT,
date DATE,
is_working BOOLEAN NOT NULL DEFAULT true,
opens_at TIME NOT NULL,
closes_at TIME NOT NULL,
slot_duration INTEGER NOT NULL DEFAULT 60
);
CREATE TABLE bookings (
id BIGSERIAL PRIMARY KEY,
resource_id BIGINT NOT NULL REFERENCES bookable_resources(id),
service_id BIGINT REFERENCES services(id),
user_id BIGINT REFERENCES users(id),
client_name VARCHAR(255) NOT NULL,
client_phone VARCHAR(50),
client_email VARCHAR(255),
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'confirmed',
notes TEXT,
cancel_reason TEXT,
reminder_sent BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
EXCLUDE USING gist (
resource_id WITH =,
tsrange(starts_at, ends_at, '[)') WITH &&
) WHERE (status NOT IN ('cancelled', 'no_show'))
); יצירת משבצות פנויות
class SlotGenerator {
public function getAvailableSlots(BookableResource $resource, int $serviceDurationMinutes, Carbon $date): Collection {
$schedule = $this->getScheduleForDate($resource, $date);
if (!$schedule || !$schedule->is_working) {
return collect();
}
$slots = collect();
$current = $date->copy()->setTimeFromTimeString($schedule->opens_at);
$closes = $date->copy()->setTimeFromTimeString($schedule->closes_at);
$duration = CarbonInterval::minutes($serviceDurationMinutes);
while ($current->copy()->add($duration)->lte($closes)) {
$slots->push($current->copy());
$current->addMinutes($schedule->slot_duration);
}
$existingBookings = Booking::where('resource_id', $resource->id)
->whereDate('starts_at', $date)
->whereNotIn('status', ['cancelled', 'no_show'])
->get();
return $slots->filter(function (Carbon $slot) use ($existingBookings, $duration) {
$slotEnd = $slot->copy()->add($duration);
return $existingBookings->every(function (Booking $booking) use ($slot, $slotEnd) {
return $slotEnd->lte($booking->starts_at) || $slot->gte($booking->ends_at);
});
})->values();
}
} יצירת הזמנה אטומית
class BookingService {
public function create(array $data): Booking {
try {
return DB::transaction(function () use ($data) {
$booking = Booking::create([
'resource_id' => $data['resource_id'],
'starts_at' => $data['starts_at'],
'ends_at' => Carbon::parse($data['starts_at'])
->addMinutes($data['duration']),
'client_name' => $data['client_name'],
'client_phone' => $data['client_phone'],
'client_email' => $data['client_email'],
'service_id' => $data['service_id'] ?? null,
'status' => 'confirmed',
]);
BookingConfirmed::dispatch($booking);
return $booking;
});
} catch (QueryException $e) {
if (str_contains($e->getMessage(), '23P01')) {
throw new SlotAlreadyBookedException($data['starts_at']);
}
throw $e;
}
}
} ניהול לוח זמנים עם היררכיה
לוח הזמנים פועל לפי העיקרון: חריגת תאריך גוברת על תבנית שבועית. זה מקל על הגדרת ימי אי-עבודה, חופשות או חגים. האלגוריתם בודק תחילה תאריכים ספציפיים, ולאחר מכן יום בשבוע.
private function getScheduleForDate(BookableResource $resource, Carbon $date): ?ResourceSchedule {
$specific = $resource->schedules()
->whereDate('date', $date)
->first();
if ($specific) {
return $specific;
}
return $resource->schedules()
->where('day_of_week', $date->dayOfWeek)
->whereNull('date')
->first();
} משאבים טיפוסיים והפרמטרים שלהם
| סוג משאב | דוגמה | מאפיינים |
|---|---|---|
| צוות (מומחה) | רופא, ספר | יכול לספק מספר שירותים באורכים שונים |
| חדר | חדר ישיבות, אולם | מוזמן לעיתים קרובות לפי שעה, עם חיוב שעתי |
| ציוד | מכשיר MRI, מכונת חריטה | דורש כיול בין מפגשים |
| שולחן | מסעדה, חלל עבודה משותף | מאפשר הזמנה חלקית של שולחן (קיבולת) |
מהן הטעויות הנפוצות ביותר בפיתוח מערכת הזמנות?
הנפוצה ביותר — הסתמכות רק על בדיקות בצד האפליקציה. תחת בקשות מקבילות, זה מוביל להזמנות כפולות. השנייה בשכיחותה — התעלמות מאזורי זמן: אם השרת והלקוח באזורים שונים, המשבצות זזות. השלישית — אחסון לוחות זמנים רק כתאריכים ללא סדירות: צריך להזין ידנית את אותן שעות כל שבוע. הפתרון שלנו משתמש בתבניות ימי חול עם חריגים, ומפחית את מאמץ הניהול פי עשרה.
מה כלול בפיתוח סוהר?
- ניתוח כללים עסקיים: משך שירות, קיבולת משאב, מדיניות ביטולים וקנסות.
- עיצוב סכימת מסד נתונים עם אילוץ exclusion.
- יישום REST API עבור ווידג'ט ופאנל ניהול (backend של Laravel).
- פיתוח ווידג'ט הזמנות תלת-שלבי ב-React.
- לוח שנה ניהולי הבנוי על FullCalendar.
- תזכורות אוטומטיות (SMS/דוא"ל), המפחיתות אי-התייצבויות עד 25%.
- תיעוד API מלא ומדריך תפעול.
- בדיקות ופריסה לאחסון שלך.
איך מתקדם הפיתוח?
- ניתוח כללים עסקיים (1–2 ימים).
- עיצוב מודל נתונים (יום אחד).
- יישום API ולוגיקת הזמנות (4–6 ימים).
- פרונטאנד לווידג'ט ופאנל ניהול (5–7 ימים).
- שילוב התראות ותזכורות (1–2 ימים).
- בדיקות, פריסה והעברת תיעוד (2–3 ימים).
משך זמן כולל — בין 2 ל-4 שבועות תלוי במורכבות. פתרון מותאם אישית משתלם דרך גמישות ומהירות. אנו מבטיחים אפס הזמנות כפולות ומספקים קוד מקור מלא.
קבל ייעוץ לפרויקט שלך — צור קשר. הזמן פיתוח סוהר: מסכימת מסד נתונים ועד ווידג'ט באתר שלך.







