feat(docs): add scenario documentation for importing doctors from IRIMC and managing profile ownership

This commit is contained in:
hamed
2026-07-11 10:16:14 +03:30
parent 744a40c0f6
commit 7868577c57
2 changed files with 177 additions and 2 deletions
+39 -2
View File
@@ -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
@@ -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 (احراز هویت پزشک + تأیید ادمین + انتقال مالکیت).