# دیتابیس — تسک ۰۷ ## `resource_occupancy` — مهم‌ترین جدول سیستم | ستون | نوع | توضیح | |---|---|---| | `id` | BIGINT PK AI | BIGINT چون پرحجم‌ترین جدول می‌شود | | `uuid` | VARCHAR(36) UNIQUE | | | `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | | | `resource_id` | INT NOT NULL | FK → `clinic_resources.id` ON DELETE RESTRICT | | `appointment_id` | INT NULL | FK → `appointments.id` ON DELETE CASCADE؛ NULL = مسدودسازی دستی | | `appointment_segment_id` | INT NULL | FK ON DELETE CASCADE | | `start_at` | INT NOT NULL | **شامل `setup_minutes` منبع** | | `end_at` | INT NOT NULL | **شامل `cleanup_minutes` منبع** | | `units` | SMALLINT NOT NULL DEFAULT 1 | | | `occupancy_kind` | VARCHAR(10) NOT NULL | `exclusive`\|`shared`\|`passive` | | `status` | VARCHAR(10) NOT NULL | `hold`\|`booked`\|`released` | | `expires_at` | INT NULL | فقط برای `hold` | | `created_at` | INT NOT NULL | | ```sql KEY idx_occ_resource_range (resource_id, start_at, end_at, status) -- کوئری داغ تسک ۰۶ KEY idx_occ_tenant_range (entity_type, entity_id, start_at) -- لیست‌های پنل KEY idx_occ_appointment (appointment_id) KEY idx_occ_expiry (status, expires_at) -- cron انقضا ``` ## `resource_occupancy_slot` — تضمین یکتایی ```sql CREATE TABLE resource_occupancy_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, resource_id INT NOT NULL, bucket INT NOT NULL, -- floor(timestamp / 300) unit_index SMALLINT NOT NULL, -- 0 .. capacity-1 occupancy_id BIGINT NOT NULL, UNIQUE KEY uniq_occ_slot (resource_id, bucket, unit_index), -- ← کل تضمین اینجاست KEY idx_occ_slot_occupancy (occupancy_id), CONSTRAINT fk_occ_slot_occupancy FOREIGN KEY (occupancy_id) REFERENCES resource_occupancy(id) ON DELETE CASCADE ) ENGINE=InnoDB; ``` `BUCKET_SECONDS = 300` در `OccupancyWriter::BUCKET_SECONDS` ثابت است. تغییرش بعد از تولید داده، migration کامل می‌خواهد — در کد کامنت هشدار بگذار. سطل‌های یک بازه: ```php // [start, end) نیم‌باز → سطل آخر شامل نمی‌شود اگر دقیقاً روی مرز باشد $first = intdiv($start, self::BUCKET_SECONDS); $last = intdiv($end - 1, self::BUCKET_SECONDS); ``` بدون `-1` نوبت ۱۰:۰۰-۱۰:۳۰ و نوبت ۱۰:۳۰-۱۱:۰۰ سطل مشترک می‌گیرند و دومی بی‌دلیل رد می‌شود. ## `appointment_segments` | ستون | نوع | توضیح | |---|---|---| | `id` | INT PK AI | | | `uuid` | VARCHAR(36) UNIQUE | | | `entity_type` / `entity_id` | VARCHAR(10) / INT NOT NULL | | | `appointment_id` | INT NOT NULL | FK ON DELETE CASCADE | | `sequence` | SMALLINT NOT NULL | | | `name` | VARCHAR(150) NOT NULL | snapshot نام بخش در لحظهٔ ثبت | | `segment_type` | VARCHAR(40) NOT NULL | | | `start_at` | INT NOT NULL | مطلق | | `end_at` | INT NOT NULL | | | `patient_present` | TINYINT(1) NOT NULL DEFAULT 1 | | | `created_at` | INT NOT NULL | | ```sql KEY idx_appt_seg_appointment (appointment_id, sequence) KEY idx_appt_seg_tenant (entity_type, entity_id, start_at) ``` `name` و `segment_type` عمداً کپی می‌شوند نه FK: قانون پنجم مستند — «هر چیزی که ثبت شد باید همان‌طور بماند». اگر کلینیک فردا الگو را عوض کند، نوبت دیروز نباید تغییر معنا دهد. ## تغییر `appointments` ```sql ALTER TABLE appointments ADD COLUMN branch_id INT NULL, ADD COLUMN plan_total_minutes SMALLINT NULL, ADD COLUMN patient_facing_minutes SMALLINT NULL, ADD CONSTRAINT fk_appointments_branch FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE SET NULL, ADD KEY idx_appointments_branch (branch_id, slot_start); ``` `slot_start` / `slot_end` **می‌مانند** و در حالت `resource` برابر شروع اولین بخش و پایان آخرین بخش‌اند. دلیل: `AppointmentRepository`، لیست‌های پنل، سایت عمومی و اپ دسکتاپ همه روی این دو ستون کوئری می‌زنند. برداشتنشان یعنی بازنویسی همه‌جا. وضعیت جدید: ```php public const STATUS_RESCHEDULED = 'rescheduled'; // ALLOWED_TRANSITIONS: confirmed → rescheduled ; pending → rescheduled ``` ## Migration ```bash ddev exec php bin/console doctrine:migrations:diff --no-interaction ddev exec php bin/console doctrine:migrations:migrate --no-interaction ``` ⚠️ **دو ایندکس را دستی در migration بنویس** — `doctrine:migrations:diff` ترتیب ستون‌های ایندکس ترکیبی را گاهی متفاوت تولید می‌کند و ترتیب اینجا حیاتی است (`resource_id` اول در `idx_occ_resource_range`). ## backfill نوبت‌های موجود ```bash ddev exec php bin/console app:occupancy:backfill --force ``` برای هر نوبت `pending`/`confirmed` آینده در حالت `slot`/`service`: - یک `appointment_segment` واحد بساز (کل بازه) - یک `resource_occupancy` روی منبع `type=doctor` همان پزشک - ردیف‌های `resource_occupancy_slot` متناظر اگر منبع `type=doctor` وجود ندارد (backfill تسک ۰۲ اجرا نشده)، آن نوبت رد شود و در خروجی گزارش شود — نه خطا. بدون این backfill، اولین رزرو در حالت جدید ممکن است روی نوبت قدیمی بنشیند. ## نگهداشت ```bash ddev exec php bin/console app:occupancy:prune --older-than=90d --force ``` ردیف‌های `resource_occupancy_slot` مربوط به بازه‌های گذشته را حذف می‌کند. `resource_occupancy` می‌ماند (آدیت و گزارش بهره‌وری تسک ۱۴). ## طبقه‌بندی tenant | جدول | وضعیت | |---|---| | `resource_occupancy` | جفت tenant | | `appointment_segments` | جفت tenant (uuid ممکن است از request بیاید) | | `resource_occupancy_slot` | `AGGREGATE_CHILDREN` → ریشه `ResourceOccupancy` — **هرگز مستقیم کوئری نشود** |