技術 6 分鐘閱讀

從零開始:用 Laravel + React 建一個訂位系統

從零開始:用 Laravel + React 建一個訂位系統

前言

最近幫一個客戶做了一個餐廳訂位系統,從需求訪談到上線大約花了六週。這篇分享整個開發過程中的架構設計與踩過的坑。

需求分析

客戶是一間中型餐廳,主要需求:

  • 顧客可以線上選擇日期、時段、人數來訂位
  • 後台可以管理訂位、查看每日訂位狀況
  • 自動發送訂位確認信和提醒通知
  • 處理 No-Show 問題

資料庫設計

// 餐廳時段設定
Schema::create('time_slots', function (Blueprint $table) {
    $table->id();
    $table->time('start_time');
    $table->time('end_time');
    $table->integer('max_capacity');
    $table->boolean('is_active')->default(true);
    $table->timestamps();
});

// 訂位記錄
Schema::create('reservations', function (Blueprint $table) {
    $table->id();
    $table->date('date');
    $table->foreignId('time_slot_id')->constrained();
    $table->string('customer_name');
    $table->string('customer_phone');
    $table->string('customer_email')->nullable();
    $table->integer('party_size');
    $table->enum('status', ['pending', 'confirmed', 'cancelled', 'completed', 'no_show']);
    $table->text('note')->nullable();
    $table->timestamps();
});

核心邏輯:可用時段計算

class ReservationService
{
    public function getAvailableSlots(string $date): Collection
    {
        $slots = TimeSlot::where('is_active', true)->get();

        return $slots->map(function ($slot) use ($date) {
            $reserved = Reservation::where('date', $date)
                ->where('time_slot_id', $slot->id)
                ->whereNotIn('status', ['cancelled'])
                ->sum('party_size');

            return [
                'id' => $slot->id,
                'start_time' => $slot->start_time,
                'end_time' => $slot->end_time,
                'remaining' => $slot->max_capacity - $reserved,
                'available' => ($slot->max_capacity - $reserved) > 0,
            ];
        });
    }
}

前端:日期選擇器元件

import { useState } from 'react';
import { router } from '@inertiajs/react';

function BookingForm({ availableSlots }: { availableSlots: TimeSlot[] }) {
  const [form, setForm] = useState({
    date: '',
    time_slot_id: '',
    party_size: 2,
    customer_name: '',
    customer_phone: '',
  });

  const handleSubmit = () => {
    router.post('/reservations', form);
  };

  return (
    <div className="mx-auto max-w-lg space-y-4">
      <input
        type="date"
        min={new Date().toISOString().split('T')[0]}
        value={form.date}
        onChange={e => setForm({ ...form, date: e.target.value })}
        className="w-full rounded-md border p-2"
      />

      <div className="grid grid-cols-3 gap-2">
        {availableSlots.map(slot => (
          <button
            key={slot.id}
            disabled={!slot.available}
            onClick={() => setForm({ ...form, time_slot_id: String(slot.id) })}
            className={`rounded-md p-2 ${
              slot.available ? 'bg-green-100 hover:bg-green-200' : 'bg-gray-100 text-gray-400'
            }`}
          >
            {slot.start_time}
            <span className="block text-xs">剩 {slot.remaining} 位</span>
          </button>
        ))}
      </div>

      <button onClick={handleSubmit} className="w-full rounded-md bg-blue-600 py-3 text-white">
        確認訂位
      </button>
    </div>
  );
}

通知功能

使用 Laravel Notification 發送確認信和提醒:

// 訂位確認後排程提醒
$reservation->notify(new ReservationConfirmed());

// 排程:訂位前一天提醒
Schedule::command('reservations:send-reminders')->dailyAt('10:00');

學到的經驗

  • 時段計算要考慮「用餐時間」,不能讓兩組客人重疊
  • 搭配 Seeder 產生假資料 來測試各種滿桌情境很重要
  • 手機號碼驗證要做好,台灣手機格式 09xx-xxx-xxx

如果你需要類似的系統,歡迎聯繫討論。

#Laravel #React #訂位系統
Thousand Lab

Thousand Lab