feat(tenant): enforce environment isolation in the ORM layer
Phase 4 of the tenant-marking series. Until now isolation depended on every query remembering its own WHERE clause. With 82 entities and 844 tests, that is not a guarantee — it is a hope. MariaDB has no row-level security, so the backstop has to live in Doctrine. TenantFilter appends (entity_type, entity_id) to every DQL query on a tenant-owning entity. It ships disabled and TenantFilterSubscriber turns it on per request. The filter engages only for a **chosen** environment — an explicit clinic_uuid on the request, or a stored UserActiveContext. EntityContext now records which of the two produced it. Locking a user to the role fallback instead would hide data they are entitled to: a clinic-member doctor who never switched context lost every appointment belonging to that clinic. Five tests caught exactly that before the gate was added. Admins and unauthenticated marketplace traffic stay outside the filter by design. Two findings from running it rather than reasoning about it: - Dereferencing a lazy proxy whose target the filter excluded raises EntityNotFoundException, which surfaced as 500 on four patient endpoints. ExceptionSubscriber now maps it to 404: outside your environment means it does not exist for you. It is logged at info level so a genuinely broken FK is still visible. - EntityManager::find() by primary key IS filtered in Doctrine ORM 3, contrary to the limitation carried over from older versions. The stronger guarantee is pinned by a test so a future regression is noticed, and the documented table was corrected. The filter also caught a real leak: a clinic secretary's appointment list filtered by doctor id alone, so a doctor's personal-practice booking appeared in the clinic list. The test had been asserting that behaviour. GlobalTables classifies all 82 entities into four states — carries a tenant, deliberately global, aggregate child, or recorded debt — and TenantSchemaCoverageTest fails on anything unclassified. Aggregate children declare their root explicitly, because several attach through a scalar FK rather than a Doctrine association and cannot be inferred from metadata; the test walks each chain to a tenant-owning root. Financial tables stay in DEFERRED with a ceiling assertion so the list cannot grow quietly. Deliberately not built: the prePersist assignment listener from the plan. The tenant columns are NOT NULL without a default, so a missing assignTenant() already fails loudly at flush — phase 2 surfaced 123 such failures. A listener would add silent auto-assignment where the current behaviour is an explicit crash. EXPLAIN with the filter's conditions still picks idx_appointments_tenant_slot and uniq_patient_record. Tests: 844 passing. PHPStan unchanged at its 17 pre-existing errors, none in files touched here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# جداسازی محیط (tenancy)
|
||||
|
||||
هر دادهٔ عملیاتی در ClinicPro به یک **محیط** تعلق دارد: یا مطب شخصی یک پزشک، یا یک کلینیک. این سند میگوید آن محیط چطور تعیین میشود، کجا اجبار میشود، و — مهمتر — **کجا اجبار نمیشود**.
|
||||
|
||||
---
|
||||
|
||||
## هویت محیط
|
||||
|
||||
جفت `(entity_type, entity_id)` روی خودِ جدول:
|
||||
|
||||
| ستون | مقدار |
|
||||
|---|---|
|
||||
| `entity_type` | `doctor` یا `clinic` — `VARCHAR(10)` در هر ۲۰ جدول tenant-دار |
|
||||
| `entity_id` | شناسهٔ همان پزشک یا کلینیک |
|
||||
|
||||
موجودیتها این جفت را از trait مشترک میگیرند:
|
||||
|
||||
```php
|
||||
use App\Shared\Tenant\TenantOwnedTrait;
|
||||
|
||||
$appointment->assignTenant(EntityContext::forBooking($doctor, $clinic));
|
||||
// یا وقتی جفت از قبل بهصورت اسکالر حل شده:
|
||||
$rule->assignTenantPair('clinic', $clinicId);
|
||||
```
|
||||
|
||||
ستونها `NOT NULL` بدون مقدار پیشفرضاند. اگر سازندهای `assignTenant()` را فراموش کند، `flush` میشکند — عمدی است: ردیفِ بیمحیط بیصدا از دید همه پنهان میشود.
|
||||
|
||||
---
|
||||
|
||||
## تعیین محیط جاری
|
||||
|
||||
`App\Shared\Context\EntityContextResolver` تنها نقطهٔ تصمیم است. اولویت:
|
||||
|
||||
```
|
||||
clinic_uuid صریحِ درخواست > UserActiveContext ذخیرهشده > fallback نقش
|
||||
```
|
||||
|
||||
دو مورد اول **انتخابِ کاربر**اند و `EntityContext::$chosen` را `true` میکنند. سومی حدس است.
|
||||
|
||||
### ماتریس نقشها
|
||||
|
||||
| نقش | محیط مؤثر |
|
||||
|---|---|
|
||||
| پزشک مستقل | همیشه `('doctor', id)` |
|
||||
| پزشک عضو یک یا چند کلینیک | طبق محیط فعال؛ بیرون از آن، مطب شخصی |
|
||||
| پزشکِ مالک کلینیک | طبق محیط فعال؛ در محیط کلینیک بدون محدودیت `ClinicDoctorPermission` |
|
||||
| مدیر/مالک کلینیک | همیشه `('clinic', id)` |
|
||||
| منشی | فقط از محیط فعال؛ بدون آن `unknown` |
|
||||
|
||||
`user_active_context` هم `db_uuid` دارد هم `db_type`، پس حل محیط یک lookup است نه دو تا.
|
||||
|
||||
---
|
||||
|
||||
## اجبار: `TenantFilter`
|
||||
|
||||
یک `SQLFilter` که به هر کوئری DQL روی موجودیتهای tenant-دار شرط `(entity_type, entity_id)` اضافه میکند.
|
||||
|
||||
**پیشفرض خاموش است.** `TenantFilterSubscriber` آن را در `kernel.request` روشن میکند و فقط وقتی که:
|
||||
|
||||
- کاربر احراز شده باشد، **و**
|
||||
- `ROLE_ADMIN` نداشته باشد (پنل ادمین ذاتاً cross-tenant است)، **و**
|
||||
- محیطش **انتخابشده** باشد (`$context->chosen`)
|
||||
|
||||
> **چرا فقط محیط انتخابشده؟** کاربری که هنوز محیطی برنگزیده در هیچ محیطی «نیست». قفلکردنش روی حدسِ نقش، دادهٔ کلینیکیای را که قانوناً حقش است پنهان میکند — مثلاً پزشکِ عضوی که هرگز محیط عوض نکرده، نوبتهای کلینیکش را از دست میداد. برای این کاربران، دسترسی را همان checkerهای دامنه تعیین میکنند (رفتار پیش از فاز ۴).
|
||||
|
||||
### چه تضمین میدهد و چه نمیدهد
|
||||
|
||||
| مسیر | فیلتر اعمال میشود؟ |
|
||||
|---|---|
|
||||
| DQL و QueryBuilder | ✅ |
|
||||
| `findBy` / `findOneBy` | ✅ |
|
||||
| `EntityManager::find()` با کلید اصلی | ✅ — در Doctrine ORM 3 (تثبیتشده در `TenantFilterLeakTest`) |
|
||||
| بارگذاری تنبل کالکشنها | ✅ |
|
||||
| entity که از قبل در identity map است | ❌ دوباره کوئری نمیشود |
|
||||
| `getReference()` | ❌ |
|
||||
| SQL خام DBAL | ❌ |
|
||||
| فرزندان aggregate | ❌ — همیشه از ریشه JOIN کن |
|
||||
|
||||
**فیلتر جایگزین authorization نیست.** `AppointmentAccessChecker`، `ClinicDoctorAccessChecker`، `SecretaryAccessChecker` و `PatientRecordScopeResolver` سر جایشان میمانند: آنها «چه کاری مجاز است» را جواب میدهند، فیلتر فقط «کدام ردیفها».
|
||||
|
||||
### تغییر رفتار: ۴۰۴ بهجای ۴۰۳
|
||||
|
||||
با فیلتر روشن، رکوردی که مال محیط دیگری است **وجود ندارد**، نه اینکه «ممنوع» باشد:
|
||||
|
||||
- کاربر با محیط انتخابشدهٔ A که رکورد محیط B را باز کند → `404`
|
||||
- مقداردهیِ proxy به موجودیتی که فیلتر کنارش گذاشته → `EntityNotFoundException` که `ExceptionSubscriber` به `404` با `ERR_NOT_FOUND_001` نگاشت میکند (و در سطح `info` لاگ میشود تا FK واقعاً شکسته هم دیده شود)
|
||||
|
||||
---
|
||||
|
||||
## طبقهبندی جدولها — `App\Shared\Tenant\GlobalTables`
|
||||
|
||||
هر entity دقیقاً در یکی از چهار وضعیت است، و `TenantSchemaCoverageTest` این را اجبار میکند:
|
||||
|
||||
| وضعیت | یعنی | نمونه |
|
||||
|---|---|---|
|
||||
| جفت tenant دارد | فیلتر پوششش میدهد | `appointments`، `patient_records`، `service_sections` |
|
||||
| `ENTITIES` | عمداً سراسری | `cities`، `specialties`، `users`، `blogs` |
|
||||
| `AGGREGATE_CHILDREN` | محیط را از ریشه به ارث میبرد | `patient_notes` → `patient_records` |
|
||||
| `DEFERRED` | بدهی ثبتشده، هنوز طبقهبندی نشده | جدولهای مالی |
|
||||
|
||||
### ⚠️ فرزندان aggregate تور ایمنی ندارند
|
||||
|
||||
فیلتر روی آنها اعمال نمیشود. کوئری مستقیم روی `patient_attachments` بدون JOIN به `patient_records`، cross-tenant است. تست فقط تضمین میکند زنجیرهٔ اعلامشده به ریشهای با جفت tenant میرسد — نه اینکه کوئریها واقعاً از ریشه شروع میشوند.
|
||||
|
||||
### بدهی باقیمانده
|
||||
|
||||
جدولهای مالی (`payments`، `settlements`، `financial_breakdowns`، `wallet_transactions`، `secretary_earnings`، `bank_accounts`، `pos_devices`) در `DEFERRED` ثبت شدهاند. مالکیتشان دوگانه است — پرداختکننده در برابر دریافتکننده — و تصمیم دربارهشان تحلیل جدا میخواهد. `testDeferredDebtDoesNotGrow` جلوی رشد بیصدای این فهرست را میگیرد.
|
||||
|
||||
---
|
||||
|
||||
## entity جدید میسازی؟
|
||||
|
||||
۱. اگر به یک محیط تعلق دارد → `use TenantOwnedTrait;` و در نقطهٔ ساخت `assignTenant()` را صدا بزن
|
||||
۲. اگر ندارد → با **دلیل** در `GlobalTables::ENTITIES` ثبتش کن
|
||||
۳. اگر فرزند یک aggregate است → در `GlobalTables::AGGREGATE_CHILDREN` با ریشهٔ صریح
|
||||
۴. تست را اجرا کن: `ddev exec php bin/phpunit tests/Shared/TenantSchemaCoverageTest.php`
|
||||
|
||||
ایندکسها: `entity_type, entity_id` باید **ستونهای اول** هر ایندکس ترکیبیِ لیست باشند، وگرنه MariaDB برای شرط فیلتر از آن استفاده نمیکند.
|
||||
|
||||
---
|
||||
|
||||
## تستها
|
||||
|
||||
| فایل | چه چیزی را تضمین میکند |
|
||||
|---|---|
|
||||
| `tests/Shared/EntityContextResolverTest.php` | ماتریس ۵ نقش |
|
||||
| `tests/Shared/TenantIsolationMatrixTest.php` | هر نقش فقط دادهٔ محیط خودش را از API میگیرد |
|
||||
| `tests/Shared/TenantFilterLeakTest.php` | فیلتر **بهتنهایی** cross-tenant را میبندد؛ مسیر عمومی و ادمین باز میمانند |
|
||||
| `tests/Shared/TenantSchemaCoverageTest.php` | هیچ entity طبقهبندینشده نمیماند |
|
||||
| `tests/Appointment/BookingTenantTest.php` | نوبت در محیط درست ثبت میشود |
|
||||
| `tests/Secretary/SecretaryMultiClinicScopeTest.php` | یک منشی، یک پزشک، چند کلینیک |
|
||||
@@ -0,0 +1,726 @@
|
||||
# مستند طراحی موتور نوبتدهی Clinic Pro
|
||||
|
||||
**نسخه ۲ — نگارش ساده**
|
||||
|
||||
---
|
||||
|
||||
## این مستند برای چیست؟
|
||||
|
||||
میخواهیم یک سیستم نوبتدهی بسازیم که برای همه نوع کلینیک کار کند. کلینیک زیبایی، دندانپزشکی، فیزیوتراپی، مرکز تصویربرداری و هر مرکز خدماتی دیگر.
|
||||
|
||||
نکته اصلی این است: برای هر کلینیک جدید نباید کد جدید بنویسیم. باید بتوانیم فقط تنظیمات را عوض کنیم و همان سیستم کار کند.
|
||||
|
||||
این مستند میگوید این سیستم چطور باید ساخته شود.
|
||||
|
||||
---
|
||||
|
||||
## ۱. چه چیزهایی در این مستند هست و چه چیزهایی نیست
|
||||
|
||||
**هست:**
|
||||
تعریف خدمات، محاسبه زمان هر نوبت، مدیریت منابع کلینیک، قوانین اختصاصی هر کلینیک، پیدا کردن وقت آزاد، ثبت نوبت، محاسبه قیمت، مدیریت دورههای درمان.
|
||||
|
||||
**نیست:**
|
||||
پرونده پزشکی بیمار، حسابداری، انبار، پیامک تبلیغاتی، گزارشهای مدیریتی.
|
||||
|
||||
اینها سیستمهای جداگانهای هستند. سیستم نوبتدهی فقط به آنها خبر میدهد که چه اتفاقی افتاده و کاری به کارشان ندارد.
|
||||
|
||||
---
|
||||
|
||||
## ۲. پنج قانون طلایی
|
||||
|
||||
هر تصمیمی که در این مستند گرفته شده، از این پنج قانون میآید:
|
||||
|
||||
**یک) تقویم مال منبع است، نه مال پزشک.**
|
||||
خیلی از سیستمها فقط برای پزشک تقویم میسازند. این اشتباه است. دستگاه لیزر هم تقویم دارد. اتاق هم تقویم دارد. تخت هم. پزشک فقط یکی از اینهاست.
|
||||
|
||||
**دو) یک نوبت، یک تکه زمان پیوسته نیست.**
|
||||
یک نوبت لیزر از چند بخش تشکیل شده. در هر بخش، منابع متفاوتی درگیرند. این را مفصل توضیح میدهیم چون مهمترین نکته این مستند است.
|
||||
|
||||
**سه) اطلاعات را با قانون قاطی نکنیم.**
|
||||
«فلان اپراتور میتواند با فلان دستگاه کار کند» یک اطلاعات است. باید در جدول ذخیره شود.
|
||||
«اگر بیمار VIP بود ده درصد تخفیف بده» یک قانون است.
|
||||
اگر این دو را قاطی کنیم، سیستم پر از قانونهای الکی میشود و کند میشود.
|
||||
|
||||
**چهار) قانونها باید ساده و از قبل مشخص باشند.**
|
||||
نباید اجازه بدهیم کاربر هر کد دلخواهی به عنوان قانون بنویسد. چرا؟ چون وقتی میخواهیم وقتهای آزاد یک ماه آینده را نشان بدهیم، سیستم باید هزاران حالت را بررسی کند. اگر هر قانون یک برنامه دلخواه باشد، این کار چند ثانیه طول میکشد و کاربر منتظر میماند.
|
||||
|
||||
**پنج) هر چیزی که ثبت شد، باید همانطور بماند.**
|
||||
اگر فردا قیمت لیزر را عوض کردیم، نوبتی که دیروز ثبت شده نباید تغییر کند. اگر قانونی را عوض کردیم، نوبتهای قبلی نباید خراب شوند.
|
||||
|
||||
---
|
||||
|
||||
## ۳. سیستم از چه لایههایی تشکیل شده
|
||||
|
||||
سیستم مثل یک خط تولید کار میکند. هر لایه کار خودش را میکند و نتیجه را به لایه بعدی میدهد:
|
||||
|
||||
```
|
||||
۱. کاتالوگ → چه خدماتی داریم؟
|
||||
۲. انتخاب → بیمار چه چیزی خواسته؟
|
||||
۳. برنامهریزی → این کار چقدر طول میکشد و چه منابعی میخواهد؟
|
||||
۴. قوانین → قوانین این کلینیک چه تغییری در برنامه میدهند؟
|
||||
۵. زمانیابی → چه ساعتهایی آزاد است؟
|
||||
۶. ثبت نوبت → رزرو کن و مطمئن شو کس دیگری همان وقت را نگرفته
|
||||
۷. قیمتگذاری → چقدر باید پرداخت شود؟
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۴. کلینیک، شعبه، اتاق
|
||||
|
||||
سیستم قرار است به شکل سرویس اشتراکی (SaaS) فروخته شود. یعنی چند کلینیک مختلف روی یک سیستم کار میکنند و هیچکدام نباید اطلاعات دیگری را ببیند.
|
||||
|
||||
سه سطح داریم:
|
||||
|
||||
| سطح | یعنی چه |
|
||||
|---|---|
|
||||
| کلینیک (Tenant) | مشتری ما — کسی که اشتراک خریده |
|
||||
| شعبه (Branch) | یک ساختمان واقعی با منابع و ساعت کاری خودش |
|
||||
| اتاق (Room) | فضای داخل شعبه |
|
||||
|
||||
**نکته مهم:** شناسه کلینیک باید از همان روز اول در تمام جدولها باشد. این چیزی نیست که بعداً اضافه شود. اگر اول سیستم را تککلینیکی بسازیم، بعداً باید همه چیز را از نو بنویسیم.
|
||||
|
||||
خدمات و قیمتها را میشود در سطح کلینیک تعریف کرد و در شعبهها تغییر داد. مثلاً لیزر در شعبه مرکزی گرانتر باشد.
|
||||
|
||||
منابع همیشه مال شعبه هستند، چون فیزیکیاند.
|
||||
|
||||
---
|
||||
|
||||
## ۵. تعریف خدمات
|
||||
|
||||
### دستهبندی
|
||||
|
||||
فقط برای مرتب کردن و نمایش است. مثلاً:
|
||||
|
||||
```
|
||||
زیبایی
|
||||
لیزر
|
||||
تزریق
|
||||
دندانپزشکی
|
||||
ترمیم
|
||||
جراحی
|
||||
```
|
||||
|
||||
هیچ منطق نوبتدهی به دستهبندی وابسته نیست. فقط قوانین میتوانند به آن اشاره کنند، مثلاً «همه خدمات دسته جراحی به جراح نیاز دارند».
|
||||
|
||||
### سرویس
|
||||
|
||||
هر چیزی که بیمار میتواند رزرو کند. مثلاً «لیزر کندلا» یا «ترمیم دندان».
|
||||
|
||||
هر سرویس اینها را دارد:
|
||||
|
||||
- عنوان و دستهبندی
|
||||
- تکجلسه است یا دورهای؟ (لیزر معمولاً هشت جلسه است)
|
||||
- الگوی بخشهای نوبت (بخش ۷ توضیح میدهد)
|
||||
- قیمت پایه
|
||||
- گروههای آیتم قابل انتخاب
|
||||
- توضیحات آمادهسازی بیمار
|
||||
- فعال یا غیرفعال
|
||||
|
||||
### گروه آیتم
|
||||
|
||||
اینجا یک اشکال مهم در طراحی قبلی بود. قبلاً آیتمها یک لیست ساده بودند. اما در واقعیت، انتخاب آیتم قانون دارد:
|
||||
|
||||
- «حتماً یک سطح انرژی انتخاب کن، فقط یکی»
|
||||
- «بین یک تا هشت دندان انتخاب کن»
|
||||
- «اگر بیکینی را انتخاب کردی، نمیتوانی فولبادی را هم انتخاب کنی»
|
||||
|
||||
اینها را نباید به موتور قوانین سپرد. باید در خود تعریف آیتم باشند.
|
||||
|
||||
```
|
||||
گروه: دندانهای موردنظر
|
||||
حداقل انتخاب: ۱
|
||||
حداکثر انتخاب: ۸
|
||||
|
||||
گروه: سطح انرژی
|
||||
حداقل انتخاب: ۱
|
||||
حداکثر انتخاب: ۱
|
||||
```
|
||||
|
||||
### آیتم
|
||||
|
||||
هر آیتم اینها را دارد:
|
||||
|
||||
| فیلد | یعنی چه |
|
||||
|---|---|
|
||||
| عنوان | مثلاً «صورت» یا «دندان شماره ۵» |
|
||||
| زمان تنها | اگر تنها آیتم انتخابی باشد چقدر طول میکشد |
|
||||
| زمان اضافه | اگر همراه آیتمهای دیگر باشد چقدر به کل اضافه میکند |
|
||||
| قیمت | |
|
||||
| منابع اضافه | اگر منبع خاصی میخواهد |
|
||||
| ناسازگار با | آیتمهایی که با آن جمع نمیشوند |
|
||||
| پیشنیاز | آیتمهایی که باید قبلش انتخاب شوند |
|
||||
|
||||
**چرا دو تا فیلد زمان؟**
|
||||
|
||||
لیزر صورت به تنهایی پانزده دقیقه است. اما این پانزده دقیقه شامل آماده کردن بیمار، تنظیم دستگاه و توضیح دادن است.
|
||||
|
||||
حالا اگر بیمار صورت و بیکینی را با هم بخواهد، آمادهسازی دو بار انجام نمیشود. بیکینی فقط هشت دقیقه به کل زمان اضافه میکند، نه پانزده دقیقه.
|
||||
|
||||
فرمول قدیمی که میگفت «زمان پایه + جمع زمان آیتمها» همیشه زمان را بیشتر از واقعیت حساب میکند. یعنی ظرفیت کلینیک را الکی پر میکند و درآمد از دست میرود.
|
||||
|
||||
---
|
||||
|
||||
## ۶. منابع کلینیک
|
||||
|
||||
### منبع چیست
|
||||
|
||||
هر چیزی که برای انجام کار لازم است و ممکن است اشغال باشد: پزشک، اپراتور، دستیار، دستگاه، اتاق، تخت، یونیت دندانپزشکی.
|
||||
|
||||
نوع منابع را خود کلینیک تعریف میکند. ما در کد چیزی را ثابت نمیکنیم.
|
||||
|
||||
هر منبع اینها را دارد:
|
||||
|
||||
- متعلق به کدام شعبه
|
||||
- چه نوعی است
|
||||
- ظرفیت همزمان چقدر است
|
||||
- چه مهارتهایی دارد
|
||||
- ویژگیهای آزاد: جنسیت، مدل دستگاه، طبقه
|
||||
- تقویم کاری
|
||||
- زمان آمادهسازی و تمیزکاری بین دو بیمار
|
||||
|
||||
### ظرفیت همزمان
|
||||
|
||||
اتاق تزریق با سه تخت را چطور مدل کنیم؟
|
||||
|
||||
**غلط:** سه منبع جداگانه بسازیم.
|
||||
**درست:** یک منبع با ظرفیت سه.
|
||||
|
||||
این کار مدلسازی را خیلی سادهتر میکند.
|
||||
|
||||
### زمان تمیزکاری
|
||||
|
||||
بین دو بیمار، یونیت دندانپزشکی ده دقیقه ضدعفونی میخواهد. این ده دقیقه جزو نوبت بیمار نیست، اما یونیت در این مدت اشغال است.
|
||||
|
||||
طراحی قبلی این را در نظر نگرفته بود.
|
||||
|
||||
### استخر منابع
|
||||
|
||||
کلینیک سه دستگاه لیزر مشابه دارد. وقتی بیمار نوبت میگیرد، فرقی نمیکند کدام دستگاه.
|
||||
|
||||
پس باید بتوانیم بگوییم «هر کدام از دستگاههای استخر لیزر آلکساندرایت». طراحی قبلی فقط میتوانست به یک دستگاه مشخص اشاره کند که در عمل جواب نمیدهد.
|
||||
|
||||
### جدول مهارتها
|
||||
|
||||
کدام اپراتور مجاز است با کدام دستگاه کار کند؟ این یک جدول ساده است.
|
||||
|
||||
طراحی قبلی این را به موتور قوانین سپرده بود. مشکل: اگر کلینیک پنجاه اپراتور و دویست سرویس داشته باشد، ده هزار قانون لازم میشود. غیرقابل مدیریت است.
|
||||
|
||||
### نیازمندی منبع
|
||||
|
||||
قلب انعطاف سیستم اینجاست. برای هر بخش از کار میگوییم چه منابعی لازم است:
|
||||
|
||||
```
|
||||
نیاز اول:
|
||||
نقش: اپراتور
|
||||
تعداد: ۱
|
||||
شرط: باید مهارت لیزر آلکساندرایت داشته باشد
|
||||
قید: همجنس با بیمار
|
||||
نوع اشغال: انحصاری
|
||||
|
||||
نیاز دوم:
|
||||
نقش: دستگاه
|
||||
تعداد: ۱
|
||||
شرط: هر عضو استخر لیزرهای آلکساندرایت
|
||||
نوع اشغال: انحصاری
|
||||
|
||||
نیاز سوم:
|
||||
نقش: اتاق
|
||||
تعداد: ۱
|
||||
شرط: اتاقی که نوعش لیزر باشد
|
||||
نوع اشغال: انحصاری
|
||||
```
|
||||
|
||||
**قید جنسیت** در کلینیکهای زیبایی ایران یک الزام جدی است، نه یک ترجیح. باید در هسته سیستم باشد.
|
||||
|
||||
**نوع اشغال** سه حالت دارد:
|
||||
- انحصاری: منبع کامل اشغال است
|
||||
- اشتراکی: یک واحد از ظرفیت منبع مصرف میشود
|
||||
- منفعل: منبع رزرو است ولی کسی فعالانه کار نمیکند
|
||||
|
||||
---
|
||||
|
||||
## ۷. مهمترین بخش: نوبت از چند تکه تشکیل شده
|
||||
|
||||
### مشکل چیست
|
||||
|
||||
طراحی قبلی فکر میکرد یک نوبت، یک بازه زمانی است که همه منابعش را در کل آن بازه اشغال میکند.
|
||||
|
||||
بیایید یک نوبت لیزر واقعی را نگاه کنیم:
|
||||
|
||||
| بخش | مدت | اتاق | اپراتور | دستگاه |
|
||||
|---|---|---|---|---|
|
||||
| مالیدن کرم بیحسی | ۵ دقیقه | اشغال | اشغال | آزاد |
|
||||
| منتظر ماندن تا کرم اثر کند | ۳۰ دقیقه | اشغال | **آزاد** | آزاد |
|
||||
| خود لیزر | ۲۰ دقیقه | اشغال | اشغال | اشغال |
|
||||
| مراقبت بعد از لیزر | ۵ دقیقه | اشغال | اشغال | آزاد |
|
||||
|
||||
با طراحی قبلی، اپراتور شصت دقیقه قفل میشود. در حالی که واقعاً فقط سی دقیقه کار میکند.
|
||||
|
||||
یعنی نصف ظرفیت اپراتور الکی هدر میرود. برای یک کلینیک زیبایی، این تفاوت بین سود و ضرر است.
|
||||
|
||||
### راهحل
|
||||
|
||||
هر نوبت را به چند بخش تقسیم میکنیم. هر بخش مدت خودش، منابع خودش و ترتیب خودش را دارد:
|
||||
|
||||
```
|
||||
بخش: انتظار اثر بیحسی
|
||||
مدت: ۳۰ دقیقه
|
||||
ترتیب: بعد از بخش قبلی
|
||||
منابع: فقط اتاق
|
||||
بیمار حاضر است: بله
|
||||
قابل ادغام با: بخشهای انتظار دیگر
|
||||
```
|
||||
|
||||
### چطور برنامه نوبت ساخته میشود
|
||||
|
||||
```
|
||||
ورودی: سرویس، آیتمهای انتخابی، بیمار، شعبه
|
||||
|
||||
۱. بخشهای پایه سرویس را بردار
|
||||
۲. بخشهای اضافه هر آیتم انتخابی را اضافه کن
|
||||
۳. بخشهای همنوع را ادغام کن
|
||||
(آمادهسازی یک بار انجام میشود، نه به تعداد آیتمها)
|
||||
۴. مدت هر بخش را حساب کن
|
||||
- اولین آیتم هر گروه: زمان تنها
|
||||
- بقیه: زمان اضافه
|
||||
۵. بخشها را به ترتیب درست بچین
|
||||
۶. قوانین مربوط به زمان را اعمال کن
|
||||
۷. زمان آمادهسازی و تمیزکاری هر منبع را اضافه کن
|
||||
|
||||
خروجی: برنامه نوبت
|
||||
```
|
||||
|
||||
نتیجه چیزی شبیه این میشود:
|
||||
|
||||
```
|
||||
کل مدت: ۶۰ دقیقه
|
||||
|
||||
بخش ۱ — دقیقه ۰ تا ۵ — اتاق، اپراتور
|
||||
بخش ۲ — دقیقه ۵ تا ۳۵ — فقط اتاق
|
||||
بخش ۳ — دقیقه ۳۵ تا ۵۵ — اتاق، اپراتور، دستگاه
|
||||
بخش ۴ — دقیقه ۵۵ تا ۶۰ — اتاق، اپراتور
|
||||
```
|
||||
|
||||
حالا اپراتور در آن سی دقیقه انتظار، آزاد است و میتواند بیمار دیگری را ببیند.
|
||||
|
||||
---
|
||||
|
||||
## ۸. قوانین اختصاصی
|
||||
|
||||
### چرا طراحی قبلی جواب نمیداد
|
||||
|
||||
طراحی قبلی همه قوانین را در یک کیسه ریخته بود. سه اشکال داشت:
|
||||
|
||||
**اول:** قانونها جنسهای کاملاً متفاوتی دارند و در زمانهای مختلفی باید اجرا شوند.
|
||||
|
||||
**دوم:** معلوم نبود اگر دو قانون با هم تناقض داشتند، کدام برنده است. مثلاً اگر دو تخفیف همزمان اعمال شوند، جمع میشوند یا ضرب؟ نتیجه غیرقابل پیشبینی میشد.
|
||||
|
||||
**سوم:** اگر قانون بتواند هر کاری بکند، پیدا کردن وقت آزاد کند میشود.
|
||||
|
||||
### شش دسته قانون
|
||||
|
||||
| دسته | کی اجرا میشود | نمونه |
|
||||
|---|---|---|
|
||||
| انتخاب | موقع انتخاب آیتم | «اگر سرویس الف را انتخاب کردی، ب هم لازم است» |
|
||||
| صلاحیت بیمار | قبل از جستجوی وقت | «بیمار زیر ۱۸ سال بدون رضایت والدین نمیشود» |
|
||||
| منبع | موقع ساخت برنامه | «خدمات جراحی به جراح نیاز دارند» |
|
||||
| زمان | موقع ساخت برنامه | «حداقل مدت درمان پیچیده یک ساعت است» |
|
||||
| فاصله زمانی | موقع جستجوی وقت | «حداقل هفت روز از جلسه قبلی فاصله باشد» |
|
||||
| قیمت | بعد از نهایی شدن برنامه | «بیمار VIP ده درصد تخفیف» |
|
||||
|
||||
هر دسته موتور خودش را دارد و در نقطه مشخصی اجرا میشود.
|
||||
|
||||
### شکل یک قانون
|
||||
|
||||
```
|
||||
شناسه: جراحی نیاز به جراح دارد
|
||||
کلینیک: ۰۰۱
|
||||
شعبه: همه شعب
|
||||
دسته: منبع
|
||||
اولویت: ۱۰۰
|
||||
از تاریخ: ۱۴۰۵/۰۱/۰۱
|
||||
نسخه: ۳
|
||||
|
||||
شرط:
|
||||
دستهبندی سرویس شامل «جراحی» باشد
|
||||
|
||||
نتیجه:
|
||||
یک منبع با نقش جراح و مهارت جراحی عمومی اضافه کن
|
||||
```
|
||||
|
||||
### قانونها به چه چیزهایی دسترسی دارند
|
||||
|
||||
فقط به این فهرست مشخص. هر چیز جدیدی باید صریحاً اضافه شود:
|
||||
|
||||
```
|
||||
سرویس: شناسه، دستهبندی، برچسبها
|
||||
آیتمها: شناسه، گروه، تعداد
|
||||
بیمار: سن، جنسیت، برچسبها، تاریخ آخرین جلسه، تعداد جلسات انجامشده
|
||||
رزرو: تاریخ و ساعت، روز هفته، شعبه، کانال رزرو
|
||||
برنامه: کل مدت، کل قیمت
|
||||
```
|
||||
|
||||
عملگرها هم مشخص است: مساوی، نامساوی، بزرگتر، کوچکتر، عضو مجموعه، بین دو مقدار، تعداد روز گذشته.
|
||||
|
||||
**نوشتن کد دلخواه ممنوع است.** این محدودیت عمدی است. قوانین مربوط به فاصله زمانی باید قابل تبدیل به کوئری دیتابیس باشند، وگرنه جستجوی وقت آزاد کند میشود.
|
||||
|
||||
### وقتی چند قانون با هم تناقض دارند
|
||||
|
||||
ترتیب انتخاب:
|
||||
|
||||
۱. قانون با اولویت بالاتر برنده است
|
||||
۲. اگر اولویت مساوی بود، قانون اختصاصیتر برنده است (شعبه بر کلینیک، سرویس بر دستهبندی)
|
||||
۳. اگر باز هم مساوی بود، قانون قدیمیتر برنده است
|
||||
|
||||
و وقتی چند قانون همنوع همزمان اعمال میشوند:
|
||||
|
||||
| نوع اثر | قاعده |
|
||||
|---|---|
|
||||
| حداقل مدت | بیشترین مقدار برنده است |
|
||||
| اضافه کردن زمان | همه با هم جمع میشوند |
|
||||
| اضافه کردن منبع | همه اضافه میشوند، تکراریها حذف |
|
||||
| محدود کردن منبع | اشتراک محدودیتها |
|
||||
| تخفیف درصدی | به ترتیب اولویت پشت سر هم، با یک سقف قابل تنظیم |
|
||||
| ممنوعیت | یکی کافی است — کل عملیات رد میشود |
|
||||
|
||||
### نسخهبندی
|
||||
|
||||
قانونها ویرایش نمیشوند. هر تغییر، یک نسخه جدید با تاریخ شروع میسازد.
|
||||
|
||||
هر نوبت، فهرست قانونهایی که رویش اعمال شده را ذخیره میکند. پس اگر فردا قانون عوض شد، نوبت دیروز دست نخورده میماند.
|
||||
|
||||
### محیط آزمایش
|
||||
|
||||
قبل از اینکه یک قانون واقعاً فعال شود، باید بشود آن را روی داده واقعی اجرا کرد و نتیجه را دید، بدون اینکه چیزی ثبت شود.
|
||||
|
||||
---
|
||||
|
||||
## ۹. تقویم و ساعتهای آزاد
|
||||
|
||||
### چطور حساب میشود
|
||||
|
||||
ساعتهای آزاد یک منبع، از کم کردن لایهها به دست میآید:
|
||||
|
||||
```
|
||||
ساعت کاری شعبه
|
||||
منهای ساعتهای خارج از شیفت منبع
|
||||
منهای تعطیلات رسمی
|
||||
منهای مرخصی و غیبت
|
||||
منهای زمان سرویس دورهای دستگاه
|
||||
منهای نوبتهای ثبتشده
|
||||
منهای رزروهای موقت
|
||||
منهای زمان آمادهسازی و تمیزکاری
|
||||
────────────────────────────────
|
||||
= ساعتهای واقعاً آزاد
|
||||
```
|
||||
|
||||
### تقویم شمسی
|
||||
|
||||
- ذخیرهسازی همیشه میلادی و به وقت جهانی است
|
||||
- تبدیل به شمسی فقط در لحظه نمایش انجام میشود
|
||||
- جدول تعطیلات رسمی کشور داریم، ولی هر کلینیک میتواند تغییرش بدهد (بعضی کلینیکها پنجشنبهها کار میکنند)
|
||||
- شیفتهای تکرارشونده با الگوی استاندارد تعریف میشوند
|
||||
|
||||
---
|
||||
|
||||
## ۱۰. پیدا کردن وقت آزاد
|
||||
|
||||
### مراحل
|
||||
|
||||
```
|
||||
ورودی: برنامه نوبت، بازه تاریخ، شعبه
|
||||
|
||||
۱. برای هر نیازمندی، منابع مناسب را پیدا کن
|
||||
منابعی که شرط را دارند، قوانین اجازه میدهند و قیدها را دارند
|
||||
اگر برای یک نیازمندی هیچ منبعی پیدا نشد،
|
||||
خطای واضح بده: «هیچ اپراتور خانمی با مهارت لیزر در این شعبه نیست»
|
||||
|
||||
۲. ساعتهای آزاد هر منبع را در بازه تاریخ حساب کن
|
||||
|
||||
۳. نقطههای شروع ممکن را بساز (پیشفرض: هر ۱۵ دقیقه)
|
||||
نقطههایی که از همان اول قطعاً جواب نمیدهند را حذف کن
|
||||
|
||||
۴. برای هر نقطه شروع:
|
||||
برای هر بخش از برنامه:
|
||||
ببین در آن بازه، منابع لازم آزاد هستند یا نه
|
||||
قوانین فاصله زمانی را چک کن
|
||||
اگر همه چیز درست بود، این یک وقت معتبر است
|
||||
|
||||
۵. وقتهای پیدا شده را مرتب کن
|
||||
|
||||
خروجی: لیست وقتهای آزاد به همراه پیشنهاد اینکه کدام منبع استفاده شود
|
||||
```
|
||||
|
||||
### کدام منبع را انتخاب کنیم
|
||||
|
||||
وقتی چند منبع آزاد است، انتخاب مهم است. کلینیک میتواند یکی از اینها را تنظیم کند:
|
||||
|
||||
- **کمترین شکاف:** منبعی که کمترین وقت خالی بلااستفاده ایجاد کند (پیشفرض)
|
||||
- **توزیع متعادل:** کار بین منابع پخش شود
|
||||
- **حفظ متخصصها:** اپراتور ماهر برای کارهای ساده مصرف نشود
|
||||
- **همان منبع قبلی:** در دورههای درمان، همان اپراتور جلسات قبل
|
||||
|
||||
### سرعت
|
||||
|
||||
- ایندکس مخصوص بازه زمانی روی جدول رزروها
|
||||
- ذخیره موقت ساعتهای آزاد هر روز و هر منبع
|
||||
- محدود کردن جستجو به نود روز آینده
|
||||
- **هدف: جستجوی یک ماهه زیر نیم ثانیه پاسخ بدهد**
|
||||
|
||||
---
|
||||
|
||||
## ۱۱. ثبت نوبت
|
||||
|
||||
### سه مرحله
|
||||
|
||||
```
|
||||
جستجو ← رزرو موقت ← ثبت نهایی
|
||||
```
|
||||
|
||||
طراحی قبلی فقط «ثبت نوبت» داشت. مشکل: دو نفر همزمان یک ساعت را انتخاب میکنند، هر دو میروند پرداخت کنند، و نفر دوم موقع پرداخت با خطا مواجه میشود.
|
||||
|
||||
### رزرو موقت
|
||||
|
||||
- به محض اینکه بیمار ساعتی را انتخاب کرد، آن ساعت برای همه منابعش قفل میشود
|
||||
- قفل ده دقیقه اعتبار دارد (برای پرداخت آنلاین بیشتر)
|
||||
- تضمین این کار در سطح دیتابیس انجام میشود، نه در کد برنامه
|
||||
|
||||
این نکته فنی مهم است: اگر جلوگیری از رزرو تکراری را در کد برنامه بگذاریم، همیشه یک شکاف زمانی میماند که دو درخواست همزمان از آن رد میشوند. دیتابیس باید این را تضمین کند.
|
||||
|
||||
### ثبت نهایی
|
||||
|
||||
همه این کارها با هم و یکجا انجام میشوند. اگر یکی شکست بخورد، همه لغو میشوند:
|
||||
|
||||
۱. چک کن رزرو موقت هنوز معتبر است
|
||||
۲. قوانین صلاحیت و فاصله زمانی را دوباره چک کن
|
||||
۳. نوبت و بخشهایش را ثبت کن
|
||||
۴. قفل موقت را به رزرو دائمی تبدیل کن
|
||||
۵. قیمت و قوانین اعمالشده را ذخیره کن
|
||||
۶. به بقیه سیستم خبر بده
|
||||
|
||||
### وضعیتهای یک نوبت
|
||||
|
||||
```
|
||||
پیشنویس → رزرو موقت → ثبتشده → پذیرش → در حال انجام → تمامشده
|
||||
↓ ↓
|
||||
لغو عدم حضور
|
||||
↓
|
||||
جابجا شده
|
||||
```
|
||||
|
||||
هر تغییر وضعیت ثبت میشود تا بعداً بشود فهمید چه اتفاقی افتاده.
|
||||
|
||||
### لغو و عدم حضور
|
||||
|
||||
هر کلینیک تنظیم میکند: تا چند ساعت قبل لغو رایگان است، جریمه چقدر است، بیعانه برمیگردد یا نه، بعد از چند بار عدم حضور بیمار پرریسک علامت بخورد.
|
||||
|
||||
---
|
||||
|
||||
## ۱۲. قیمتگذاری
|
||||
|
||||
### چرا لایه جداست
|
||||
|
||||
قیمت زندگی خودش را دارد: تاریخ اعتبار، مالیات، بیمه، پکیج، بیعانه. طراحی قبلی قیمت را داخل موتور قوانین گذاشته بود که هر دو را خراب میکند.
|
||||
|
||||
### مراحل محاسبه
|
||||
|
||||
```
|
||||
۱. قیمت پایه سرویس (از لیست قیمت معتبر در تاریخ رزرو)
|
||||
۲. جمع قیمت آیتمهای انتخابی
|
||||
۳. اعمال قوانین قیمت به ترتیب اولویت
|
||||
۴. کسر از اعتبار پکیج (اگر بیمار پکیج دارد)
|
||||
۵. محاسبه مالیات و سهم بیمه
|
||||
۶. تعیین مبلغ بیعانه
|
||||
۷. ذخیره فاکتور تفکیکشده
|
||||
```
|
||||
|
||||
### لیست قیمت
|
||||
|
||||
- هر لیست قیمت تاریخ شروع و پایان دارد
|
||||
- هر شعبه میتواند قیمت خودش را داشته باشد
|
||||
- **تغییر قیمت هرگز نوبتهای ثبتشده را عوض نمیکند**
|
||||
|
||||
### پکیج
|
||||
|
||||
خیلی رایج در کلینیک زیبایی: «پکیج شش جلسه لیزر». بیمار یکجا پول میدهد و بعداً جلساتش را رزرو میکند.
|
||||
|
||||
```
|
||||
خرید پکیج → اعتبار جلسه ثبت میشود
|
||||
ثبت نوبت → یک واحد اعتبار مصرف میشود
|
||||
لغو نوبت → اعتبار برمیگردد (طبق سیاست لغو)
|
||||
```
|
||||
|
||||
**نکته فنی:** اعتبار را به صورت دفتر حساب نگه میداریم، نه یک عدد شمارنده. یعنی هر تراکنش ثبت میشود و مانده از جمع آنها حساب میشود. اگر فقط یک عدد نگه داریم، اولین اشتباه هرگز قابل ردیابی نیست.
|
||||
|
||||
---
|
||||
|
||||
## ۱۳. دوره درمان
|
||||
|
||||
لیزر معمولاً شش تا هشت جلسه است. طراحی قبلی فقط نوبت تکی میشناخت، در حالی که این حالت اصلی کسبوکار است.
|
||||
|
||||
### پروتکل دوره
|
||||
|
||||
```
|
||||
سرویس: لیزر فولبادی
|
||||
تعداد جلسات: ۸
|
||||
فاصله بین جلسات:
|
||||
حداقل: ۲۱ روز
|
||||
ایدهآل: ۲۸ روز
|
||||
حداکثر: ۴۵ روز
|
||||
سطح انرژی هر جلسه: ۱۲، ۱۴، ۱۶، ۱۸، ۲۰، ۲۰، ۲۲، ۲۲
|
||||
```
|
||||
|
||||
### رفتار سیستم
|
||||
|
||||
- بیمار میتواند کل دوره را یکجا رزرو کند، یا جلسه به جلسه پیش برود
|
||||
- بعد از هر جلسه، تاریخ جلسه بعدی پیشنهاد داده میشود
|
||||
- اگر بیمار از حداکثر فاصله عبور کرد، هشدار داده میشود
|
||||
- پیشرفت دوره ردیابی میشود: جلسه چندم از چند، پارامترهای هر جلسه
|
||||
- به صورت پیشفرض سعی میشود همان اپراتور قبلی انتخاب شود
|
||||
|
||||
---
|
||||
|
||||
## ۱۴. جدولهای دیتابیس
|
||||
|
||||
فرض: PostgreSQL
|
||||
|
||||
```
|
||||
کلینیک و شعبه
|
||||
tenant, branch, room
|
||||
|
||||
خدمات
|
||||
service_category دستهبندی درختی
|
||||
service سرویس
|
||||
service_branch_override قیمت و مدت اختصاصی شعبه
|
||||
item_group گروه آیتم با حداقل و حداکثر انتخاب
|
||||
service_item آیتم با دو نوع زمان
|
||||
service_item_relation ناسازگاری و پیشنیاز
|
||||
segment_template الگوی بخشهای نوبت
|
||||
segment_requirement نیازمندی منبع هر بخش
|
||||
|
||||
منابع
|
||||
resource_type نوع منبع
|
||||
resource منبع با ظرفیت و ویژگیها
|
||||
resource_pool استخر منابع قابل جایگزینی
|
||||
skill / resource_skill جدول مهارتها
|
||||
work_calendar تقویم کاری با الگوی تکرار
|
||||
resource_exception مرخصی و سرویس دورهای
|
||||
holiday تعطیلات
|
||||
|
||||
قوانین
|
||||
policy قانون با شرط و نتیجه
|
||||
policy_version_log تاریخچه نسخهها
|
||||
|
||||
نوبتها
|
||||
appointment نوبت اصلی
|
||||
appointment_item آیتمهای انتخابی
|
||||
appointment_segment بخشهای نوبت
|
||||
resource_occupancy اشغال منابع ← مهمترین جدول
|
||||
appointment_audit تاریخچه تغییرات
|
||||
|
||||
قیمت
|
||||
price_list لیست قیمت تاریخدار
|
||||
price_snapshot فاکتور ثبتشده
|
||||
package پکیج
|
||||
session_credit_ledger دفتر اعتبار جلسات
|
||||
payment / deposit پرداخت و بیعانه
|
||||
|
||||
دوره درمان
|
||||
course_protocol پروتکل دوره
|
||||
treatment_course دوره بیمار
|
||||
course_session جلسات دوره
|
||||
```
|
||||
|
||||
**نکته کلیدی:** جدول `resource_occupancy` تنها مرجع حقیقت برای اشغال منابع است. هم رزرو موقت و هم نوبت نهایی در همین جدول ثبت میشوند و فقط وضعیتشان فرق میکند.
|
||||
|
||||
با این طراحی، جلوگیری از رزرو تکراری به یک محدودیت ساده دیتابیسی تبدیل میشود و دیگر لازم نیست در کد نگرانش باشیم.
|
||||
|
||||
---
|
||||
|
||||
## ۱۵. مسیر کامل یک رزرو
|
||||
|
||||
```
|
||||
۱. بیمار شعبه را انتخاب میکند
|
||||
۲. دستهبندی و سرویس را انتخاب میکند
|
||||
۳. آیتمها را انتخاب میکند
|
||||
سیستم قیدهای گروه را چک میکند
|
||||
قوانین انتخاب اجرا میشوند
|
||||
۴. برنامه نوبت ساخته میشود
|
||||
تقسیم به بخشها، ادغام، محاسبه مدت
|
||||
قوانین منبع و زمان اعمال میشوند
|
||||
۵. صلاحیت بیمار بررسی میشود
|
||||
اگر رد شد، دلیل قابل فهم نشان داده میشود
|
||||
۶. قیمت اولیه محاسبه میشود
|
||||
۷. وقتهای آزاد پیدا میشوند
|
||||
۸. وقتها به بیمار نشان داده میشوند
|
||||
۹. بیمار وقت را انتخاب میکند و رزرو موقت ایجاد میشود
|
||||
۱۰. پرداخت یا بیعانه (اگر لازم باشد)
|
||||
۱۱. ثبت نهایی همراه با ذخیره قیمت و قوانین
|
||||
۱۲. اطلاعرسانی به بیمار و بقیه سیستم
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۱۶. خبرهایی که سیستم منتشر میکند
|
||||
|
||||
سیستمهای دیگر (پیامک، حسابداری، گزارش) به این خبرها گوش میدهند:
|
||||
|
||||
```
|
||||
رزرو موقت ایجاد شد نوبت ثبت شد
|
||||
نوبت لغو شد نوبت جابجا شد
|
||||
بیمار نیامد نوبت تمام شد
|
||||
منبع مسدود شد منبع آزاد شد
|
||||
دوره درمان شروع شد یک جلسه دوره تمام شد
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ۱۷. ریسکها
|
||||
|
||||
| ریسک | چه اتفاقی میافتد | چه کنیم |
|
||||
|---|---|---|
|
||||
| کند شدن جستجوی وقت با زیاد شدن منابع | کاربر منتظر میماند و میرود | ساده نگه داشتن قوانین، ذخیره موقت، محدود کردن بازه جستجو، اندازهگیری سرعت از فاز اول |
|
||||
| کاربر غیرفنی نمیتواند قانون درست تعریف کند | قانونهای اشتباه، رفتار عجیب | ساخت قانون با فرم آماده، الگوهای از پیش تعریفشده، آزمایش اجباری قبل از فعال شدن |
|
||||
| کلینیک بخشهای نوبت را اشتباه تعریف کند | ظرفیت غلط حساب میشود | الگوی آماده برای هر نوع کلینیک، گزارش بهرهوری منابع برای پیدا کردن اشکال |
|
||||
| رقابت روی ساعتهای پرتقاضا | رزرو شکست میخورد | رزرو موقت کوتاه، پیشنهاد خودکار ساعت جایگزین |
|
||||
| اضافه کردن چندکلینیکی بعد از راهاندازی | بازنویسی کامل دیتابیس | از همان روز اول در تمام جدولها |
|
||||
|
||||
---
|
||||
|
||||
## ۱۸. ترتیب ساخت
|
||||
|
||||
**مرحله اول — هسته**
|
||||
کلینیک و شعبه، تعریف کامل خدمات، منابع و مهارتها، مدل بخشهای نوبت، تقویم، جستجوی وقت، رزرو موقت و ثبت، قیمت پایه.
|
||||
|
||||
**مرحله دوم — قوانین**
|
||||
موتور قوانین با شش دسته، فرم ساخت قانون، محیط آزمایش، نسخهبندی.
|
||||
|
||||
**مرحله سوم — کسبوکار**
|
||||
دوره درمان، پکیج، بیعانه، سیاست لغو، لیست انتظار.
|
||||
|
||||
**مرحله چهارم — بهینهسازی**
|
||||
استراتژی انتخاب منبع، پیشنهاد هوشمند وقت، گزارش بهرهوری، پیشبینی عدم حضور.
|
||||
|
||||
---
|
||||
|
||||
## ۱۹. جمعبندی
|
||||
|
||||
سیستم از شش قدم تشکیل شده:
|
||||
|
||||
```
|
||||
تعریف خدمات → ساخت برنامه نوبت → اعمال قوانین
|
||||
→ پیدا کردن وقت → ثبت → قیمتگذاری
|
||||
```
|
||||
|
||||
اگر بخواهیم سه چیز را از این مستند به یاد نگه داریم:
|
||||
|
||||
**۱. نوبت از چند بخش تشکیل شده، نه یک تکه.**
|
||||
بدون این، ظرفیت واقعی کلینیک نصف نشان داده میشود.
|
||||
|
||||
**۲. قوانین باید ساده، دستهبندیشده و اولویتدار باشند.**
|
||||
بدون این، جستجوی وقت کند میشود و رفتار سیستم غیرقابل پیشبینی.
|
||||
|
||||
**۳. جلوگیری از رزرو تکراری کار دیتابیس است، نه کار کد.**
|
||||
بدون این، دیر یا زود دو نفر یک ساعت را میگیرند.
|
||||
|
||||
کلینیک زیبایی فقط یکی از کاربردهای این سیستم است. همین موتور بدون تغییر کد، دندانپزشکی، فیزیوتراپی و تصویربرداری را هم پوشش میدهد.
|
||||
Reference in New Issue
Block a user