# دیتابیس — تسک ۰۶ این تسک **جدول جدیدی نمی‌سازد** جز کش. مصرف‌کنندهٔ جدول‌های تسک ۰۲/۰۳ و `resource_occupancy` تسک ۰۷ است. ⚠️ **وابستگی معکوس:** موتور جستجو به `resource_occupancy` نیاز دارد ولی آن جدول در تسک ۰۷ ساخته می‌شود. راه‌حل: **مهاجرت جدول `resource_occupancy` در همین تسک انجام شود** و تسک ۰۷ فقط منطق نوشتن در آن را اضافه کند. تعریف کامل جدول در [task-07/database.md](../task-07-hold-and-book/database.md) است؛ اینجا فقط ایندکس‌های لازم برای خواندن ذکر می‌شوند. ## ایندکس‌های حیاتی خواندن ```sql -- کوئری داغ: اشغال‌های این منابع در این بازه KEY idx_occupancy_resource_range (resource_id, start_at, end_at, status) ``` پرس‌وجو: ```sql SELECT resource_id, start_at, end_at, units FROM resource_occupancy WHERE resource_id IN (?, ?, …) AND start_at < :to AND end_at > :from AND status IN ('hold', 'booked') ``` `resource_id` ستون اول است چون `IN` روی آن، محدودکننده‌ترین شرط است. اضافه کردن `entity_type` به ابتدای این ایندکس **اشتباه** است: اینجا فیلتر tenant از راه منابع (که خودشان محیط دارند) اعمال شده و ستون tenant-پیشرو فقط ایندکس را بی‌اثر می‌کند. یک ایندکس دوم tenant-پیشرو برای لیست‌های پنل جدا تعریف می‌شود (تسک ۰۷). ## تنظیمات جدید روی `weekly_schedules.setting.meta` بدون تغییر schema (ستون JSON موجود): ```json { "meta": { "booking_mode": "resource", "slot_granularity": 15, "picker_strategy": "least_gap", "buffer_minutes": 0 } } ``` `setMeta()` باید کلیدهای جدید را با اعتبارسنجی بپذیرد: - `slot_granularity` ∈ {5, 10, 15, 20, 30, 60} - `picker_strategy` ∈ کلیدهای ثبت‌شدهٔ `ResourcePickerInterface` مقدار نامعتبر → مقدار فعلی حفظ می‌شود (همان الگوی موجود `setMeta`). ## کش Redis، بدون جدول. کلیدها: ``` cp:avail:res:{resourceId}:win:{Y-m-d} → JSON بازه‌های آزاد TTL تا پایان روز cp:avail:month:{branchId}:{serviceId}:{Y-m} → JSON بولین per روز TTL 300s ``` ابطال: | رویداد | کلیدهای باطل | |---|---| | تغییر `resource_calendars` | همهٔ `win` آن منبع | | ثبت/حذف `resource_exceptions` | `win` آن منبع در بازهٔ استثنا | | تغییر `branch_working_hours` | `win` همهٔ منابع آن شعبه | | تغییر `tenant_holiday_overrides` | `win` همهٔ منابع آن محیط در آن روز | | ثبت/لغو نوبت | فقط `month` — **`win` هرگز** (اشغال کش نمی‌شود) | آخرین سطر مهم‌ترین است: اگر کسی وسوسه شد اشغال را هم کش کند، نتیجه‌اش نمایش وقتِ گرفته‌شده و شکست رزرو در مرحلهٔ آخر است. ## تست کارایی — داده مصنوعی ```bash ddev exec php bin/console app:dev:seed-availability-benchmark --force ``` می‌سازد: - ۱ شعبه · ۳ اتاق (ظرفیت ۱) · ۲ اپراتور · ۳ دستگاه در یک استخر - ۱ سرویس با ۴ بخش (سناریوی مستند) - ۵۰۰ نوبت پراکنده در ۳۰ روز آینده `AvailabilityPerformanceTest` روی همین داده اجرا می‌شود و **دو** چیز را می‌سنجد: ```php self::assertLessThan(500, $elapsedMs, 'جستجوی ۳۰ روزه باید زیر نیم ثانیه باشد'); self::assertLessThanOrEqual(5, $queryCount, 'تعداد کوئری نباید با تعداد روز رشد کند'); ``` شرط دوم مهم‌تر از اولی است: زمان روی ماشین‌های مختلف فرق می‌کند، تعداد کوئری نه.