diff --git a/docs/api/doctor-import.md b/docs/api/doctor-import.md index 2c468d6a..bae2475f 100644 --- a/docs/api/doctor-import.md +++ b/docs/api/doctor-import.md @@ -6,8 +6,9 @@ وارد کردن یک پزشک از سازمان نظام پزشکی (`membersearch.irimc.org`) **بدون شماره موبایل**. برخلاف `POST /api/v1/admin/doctors` (که موبایل معتبر ایرانی می‌خواهد)، این اندپوینت برای -هر پزشک یک **کاربر جانشینِ غیرفعال** با شناسهٔ مصنوعی می‌سازد و پروفایل را در وضعیت -`unclaimed` ذخیره می‌کند تا بعداً به پزشک واقعی منتقل شود. +هر پزشک یک **کاربر جانشینِ غیرفعال** با شناسهٔ مصنوعی و نقشِ اختصاصی `ROLE_UNCLAIMED_DOCTOR` +می‌سازد و پروفایل را در وضعیت `unclaimed` ذخیره می‌کند تا بعداً به پزشک واقعی منتقل شود. +این نقش، هم کاربر جانشین را قابل‌شناسایی می‌کند و هم مبنای حذفِ امنِ او پس از انتقال است. مصرف‌کنندهٔ اصلی: خزندهٔ پایتون (`clinicpro-crawler/pipeline.py`) که با کاربر «مالک سیستمی» (`0000000000`) لاگین می‌کند و هر ~۶۰ ثانیه یک پزشک را می‌فرستد. @@ -107,6 +108,42 @@ --- +## انتقال مالکیت به پزشک واقعی + +> **Endpoint:** `POST /api/v1/admin/doctors/{uuid}/transfer` — permission `ROLE_ADMIN` + +مالکیت یک پروفایلِ `unclaimed` را به پزشک واقعی منتقل می‌کند. + +```json +// Request +{ "mobile": "09120000000" } +``` + +مراحل اتمیک: + +1. کاربر واقعی بر پایهٔ `mobile` پیدا یا ساخته می‌شود (باید موبایل معتبر ایران باشد). +2. اگر کاربر واقعی از قبل صاحب پزشک دیگری باشد → `409`. +3. `user_id` پروفایل به کاربر واقعی تغییر می‌کند، `ROLE_DOCTOR` به او داده می‌شود، + `owner_status = claimed` و `claimed_at = now` و `managed_by = null` می‌شود. +4. **کاربر جانشین حذف می‌شود** — فقط اگر واقعاً `ROLE_UNCLAIMED_DOCTOR` داشته باشد و + دیگر هیچ پزشکی به او وصل نباشد (حذف امن). + +```json +// Response 200 +{ "success": true, "data": { + "uuid": "…", "owner_status": "claimed", + "transferred_to": "09120000000", "placeholder_deleted": true +} } +``` + +| کد | حالت | +|---|---| +| `404` | پزشک یافت نشد | +| `409` | قبلاً `claimed` است، یا کاربر مقصد پزشک دیگری دارد | +| `422` | موبایل نامعتبر | + +--- + ## کاربر سیستمی و چرخهٔ عمر ```bash diff --git a/docs/scenarios/ایمپورت-پزشکان-نظام-پزشکی.md b/docs/scenarios/ایمپورت-پزشکان-نظام-پزشکی.md new file mode 100644 index 00000000..2e35969c --- /dev/null +++ b/docs/scenarios/ایمپورت-پزشکان-نظام-پزشکی.md @@ -0,0 +1,138 @@ +# ایمپورت پزشکان نظام پزشکی به کلینیک‌پرو و مدیریت مالکیت پروفایل + +> سند فنی + +این سند سناریو و پیاده‌سازیِ انجام‌شده را برای خواندن آسان جمع می‌کند: از دیتای سازمان نظام پزشکی پزشکان وارد کلینیک‌پرو می‌شوند، و چون این دیتا شماره موبایل ندارد، مکانیزمی برای ذخیره‌ی «بدون‌مالک» و انتقال بعدیِ مالکیت به پزشک واقعی طراحی شده است. + +--- + +## ۱. مسئله و محدودیت‌ها + +در سیستم فعلی هر پزشک به‌صورت اجباری و یکتا به یک کاربر متصل است و هر کاربر هم شماره موبایلِ یکتا و غیرتهی می‌خواهد. اما رکوردهای نظام پزشکی موبایل ندارند؛ پس نه می‌توان کاربر ساخت و نه پزشکِ بدون کاربر ذخیره کرد. + +| محدودیت کد فعلی | پیامد | +|---|---| +| کاربر برای پزشک اجباری است (`user_id NOT NULL`) | پزشک بدون کاربر ذخیره نمی‌شود | +| رابطه پزشک↔کاربر یکتاست (`unique user_id`) | «همه به یک کاربر» ممکن نیست | +| موبایل کاربر اجباری و یکتاست | بدون موبایل کاربر ساخته نمی‌شود | +| اندپوینت ساخت پزشک موبایل معتبر می‌خواهد | برای ایمپورت انبوه مناسب نیست | + +--- + +## ۲. مدل مالکیت پروفایل + +هر پروفایل یکی از سه وضعیت را دارد: + +- **unclaimed (بدون‌مالک):** ایمپورت‌شده، هنوز به پزشک واقعی وصل نیست؛ نوبت‌دهی آنلاین خاموش. +- **pending_transfer (در انتظار انتقال):** پزشک واقعی درخواست تصاحب داده و منتظر تأیید است. +- **claimed (تصاحب‌شده):** مالکیت به کاربر واقعی پزشک منتقل شده و کنترل کامل دارد. + +--- + +## ۳. راه‌حل انتخاب‌شده و دلیل آن + +به‌جای «اتصال همه‌ی پزشکان به یک کاربر سیستمی» (که قید یکتای `user_id` آن را نقض می‌کند)، مدل «کاربر جانشین به‌ازای هر پزشک» پیاده شد: برای هر پزشکِ ایمپورت‌شده یک کاربرِ غیرفعال با شناسه‌ی مصنوعی ساخته می‌شود. این کار هیچ‌کدام از کوئری‌های موجود روی جدول `users` را نمی‌شکند. + +- این کاربر جانشین نقشِ اختصاصی `ROLE_UNCLAIMED_DOCTOR` می‌گیرد؛ این نقش هم او را قابل‌شناسایی می‌کند و هم مبنای حذفِ امنِ او پس از انتقال است. +- پس از انتقال مالکیت به پزشک واقعی، **کاربر جانشین (فیک) حذف می‌شود** — فقط اگر واقعاً همان نقش را داشته باشد و دیگر هیچ پزشکی به او وصل نباشد. +- کاربر سیستمی `0000000000` نقش «مدیر و فراخوانِ API» را دارد (در ستون `managed_by` ثبت می‌شود)، نه صاحبِ ردیف‌های پزشک. + +--- + +## ۴. تغییرات بک‌اند (clinicpro) + +ستون‌های جدید جدول `doctors` (به‌همراه migration؛ رکوردهای موجود بدون تغییر می‌مانند): + +| ستون | مقدار هنگام ایمپورت | توضیح | +|---|---|---| +| `owner_status` | `unclaimed` | وضعیت مالکیت | +| `source` | `irimc` | منبع رکورد | +| `source_ref` | `profile_url` | شناسه‌ی مبدأ برای ممیزی | +| `managed_by` | id کاربر سیستمی | مدیرِ پروفایل | +| `claimed_at` | `null` | زمان انتقال مالکیت | + +**اندپوینت جدید:** `POST /api/v1/admin/doctors/import` (فقط `ROLE_ADMIN`). + +- بدون موبایل کار می‌کند؛ برای هر پزشک کاربر جانشین می‌سازد و پروفایل را `unclaimed` ذخیره می‌کند. +- idempotent روی `(source, medical_system_code)`: اجرای مجدد رکورد را به‌روزرسانی می‌کند نه تکراری. +- پروفایلِ `claimed` را بازنویسی نمی‌کند (مالک واقعی اولویت دارد). + +**دستور کنسول:** `app:system-owner` برای ساخت/فعال/غیرفعال کردن کاربر `0000000000`. + +**اندپوینت انتقال مالکیت:** `POST /api/v1/admin/doctors/{uuid}/transfer` با بدنه‌ی `{ mobile }`: پروفایل را به کاربر واقعی می‌دهد، `ROLE_DOCTOR` اضافه می‌کند، وضعیت را `claimed` می‌کند و کاربر جانشین را حذف می‌کند. + +--- + +## ۵. جریان کرالر (pipeline.py) + +کرالر به‌جای فقط ذخیره‌ی JSON، هر ۶۰ ثانیه یک پزشک را مستقیم در کلینیک‌پرو ذخیره می‌کند. کندیِ عمدی به‌خاطر سقفِ اندپوینت عکسِ نظام پزشکی است (بعد از ~۱۰ درخواست موقتاً بلاک می‌شود). + +1. لاگین یک‌بار با کاربر `0000000000` و رمز عبور، دریافت JWT (در ۴۰۱ دوباره لاگین). +2. گرفتن یک پزشک از منبع: فایل `doctors.json` موجود، یا خزش زنده‌ی یک شهر. +3. نرمالایز و نگاشت به شناسه‌های مرجع (تخصص/استان/شهر). +4. `POST` به اندپوینت ایمپورت، دریافت `uuid` پزشک. +5. دانلود عکس پروفایل از نظام پزشکی، آپلود به کلینیک‌پرو، و اتصال به پروفایل. +6. مکث ۶۰ ثانیه و تکرار؛ کدهای انجام‌شده برای resume ذخیره می‌شوند. +7. در پایان، کاربر `0000000000` از طریق API غیرفعال می‌شود (با `--deactivate-on-finish`). + +دو ماژول اضافه‌شده: `clinicpro_client.py` (کلاینت API) و `pipeline.py` (ارکستراتور). + +--- + +## ۶. نحوه‌ی اجرا + +ساخت و فعال‌سازی کاربر سیستمی در بک‌اند: + +```bash +ddev exec php bin/console app:system-owner 0000000000 --password=SECRET --activate +``` + +اجرای migration: + +```bash +ddev exec php bin/console doctrine:migrations:migrate +``` + +اجرای کرالر از فایل، با عکس و غیرفعال‌سازی در پایان: + +```bash +CLINICPRO_PASSWORD=SECRET python pipeline.py --mode file --file output/یاسوج/doctors.json --deactivate-on-finish +``` + +یا خزش زنده‌ی یک شهر: + +```bash +CLINICPRO_PASSWORD=SECRET python pipeline.py --mode crawl --city یاسوج --province "کهگیلویه و بویراحمد" +``` + +--- + +## ۷. نکته‌ی مهم: captcha در لاگین + +مسیر لاگین `/api/v1/user/login` از `CaptchaGuard` رد می‌شود و چون `ALTCHA_ENABLED=true` است، یک payload کپچا می‌خواهد. تا وقتی یکی از این‌ها انجام نشود، لاگین کرالر با خطای `ERR_CAPTCHA_001` رد می‌شود: + +- گذاشتن `ALTCHA_ENABLED=false` در `.env.local` روی سرور کرالر (ساده‌ترین برای dev)، یا +- افزودن استثنا در `PasswordAuthenticator` که برای کاربر سیستمی کپچا را رد کند، یا +- لاگین سرویس با یک هدر سرّیِ مورد اعتماد. + +--- + +## ۸. فایل‌های ساخته/تغییر‌یافته + +| فایل | کار | +|---|---| +| `clinicpro/src/Doctor/Entity/Doctor.php` | فیلدهای مالکیت + متد انتقال | +| `clinicpro/migrations/Version20260711120000.php` | افزودن ستون‌ها و ایندکس‌ها | +| `clinicpro/src/Admin/Controller/AdminApiController.php` | اندپوینت‌های `importDoctor` و `transferDoctor` | +| `clinicpro/src/Auth/Command/SystemOwnerCommand.php` | دستور کاربر سیستمی | +| `clinicpro/docs/api/doctor-import.md` | مستند API | +| `clinicpro-crawler/clinicpro_client.py` | کلاینت API کلینیک‌پرو | +| `clinicpro-crawler/pipeline.py` | ارکستراتور ایمپورت زنده | + +--- + +## ۹. مراحل بعدی پیشنهادی + +- اجرای migration و یک ایمپورت آزمایشی با `--limit 2` روی محیط خودت برای تست سرتاسری. +- حل موضوع کپچا برای اجرای headless کرالر. +- در فاز بعد: فرایند «تصاحب پروفایل» در Nobat724 (احراز هویت پزشک + تأیید ادمین + انتقال مالکیت).