12 KiB
Doctor Import (IRIMC) API
Endpoint:
POST /api/v1/admin/doctors/importPermission:ROLE_ADMINیاROLE_IMPORTER(نقش حداقلی کاربر سیستمی کرالر) Controller:App\Doctor\Controller\DoctorImportController::import— منطق دامنه درApp\Doctor\Service\DoctorImportService
وارد کردن یک پزشک از سازمان نظام پزشکی (membersearch.irimc.org) بدون شماره موبایل.
برخلاف POST /api/v1/admin/doctors (که موبایل معتبر ایرانی میخواهد)، این اندپوینت برای
هر پزشک یک کاربر جانشینِ غیرفعال با شناسهٔ مصنوعی و نقشِ اختصاصی ROLE_UNCLAIMED_DOCTOR
میسازد و پروفایل را در وضعیت unclaimed ذخیره میکند تا بعداً به پزشک واقعی منتقل شود.
این نقش، هم کاربر جانشین را قابلشناسایی میکند و هم مبنای حذفِ امنِ او پس از انتقال است.
مصرفکنندهٔ اصلی: خزندهٔ پایتون (clinicpro-crawler/pipeline.py) که با کاربر «مالک
سیستمی» (0000000000) لاگین میکند و هر ~۶۰ ثانیه یک پزشک را میفرستد.
مدل مالکیت (ستونهای جدید doctors)
| ستون | مقدار هنگام ایمپورت | توضیح |
|---|---|---|
owner_status |
unclaimed |
claimed | unclaimed | pending_transfer |
source |
irimc |
manual (پیشفرض رکوردهای قدیمی) | irimc |
source_ref |
profile_url |
شناسهٔ رکورد مبدأ برای ممیزی |
managed_by |
id کاربر ادمینِ فراخوان | کاربری که پروفایل را مدیریت میکند |
claimed_at |
null |
زمان انتقال مالکیت (هنگام claim پر میشود) |
رکوردهای موجود در migration مقدار manual + claimed میگیرند تا رفتارشان تغییر نکند.
idempotency
کلید یکتای (source, medical_system_code) — از این نسخه در سطح دیتابیس هم unique است
(uniq_doctors_source_code، migration Version20260711150000)؛ درخواست همزمانِ همان پزشک
با retry داخلی به مسیر update میرود و هرگز رکورد تکراری نمیسازد.
- اگر پزشکی با همان
source+medical_system_codeوجود نداشته باشد → ساخته میشود (201). - اگر وجود داشته باشد و
owner_status != claimed→ بهروزرسانی میشود (200). - اگر وجود داشته باشد و
owner_status == claimed→ رد میشود (200,skipped: "claimed") تا دادهٔ مالک واقعی بازنویسی نشود. - تکرار بین منابع: اگر پزشکی با همان
medical_system_codeولیsourceمتفاوت (مثلاً ثبت دستی در پنل) وجود داشته باشد → رد میشود (200,skipped: "duplicate")؛ نه رکورد جدیدی ساخته میشود و نه رکورد موجود بازنویسی میشود.uuidهمان رکورد موجود برگردانده میشود (تست:DoctorImportTest::testManualDoctorWithSameCodeIsNeverDuplicated).
Request
{
"name": "دکتر فرخنده حسینی",
"medical_system_code": "145657",
"source": "irimc",
"source_ref": "https://membersearch.irimc.org/member/profile?id=…",
"gender": "woman",
"degree": "general",
"info": "دکترای حرفهای پزشکی",
"specialties": [1],
"states": [23],
"cities": [123]
}
| فیلد | الزامی | توضیح |
|---|---|---|
name |
✅ | نام کامل پزشک — پیشوند «دکتر» هنگام ذخیره حذف میشود (کنوانسیون: نام بدون عنوان؛ UI خودش «دکتر» را جلو میگذارد). ورودی میتواند با یا بدون «دکتر» باشد. |
medical_system_code |
✅ | کد نظام پزشکی (کلید idempotency) |
source |
— | پیشفرض irimc |
source_ref |
— | profile_url یا شناسهٔ مبدأ |
gender |
— | man | woman |
degree |
— | general | expert | specialist | subspecialistplus |
info |
— | متن تخصص/توضیح |
specialties / states / cities |
— | آرایهٔ شناسههای مرجع (id) |
gender/degreeباید با ثابتهایDoctor::GENDERS/Doctor::DEGREESسازگار باشند.specialties/states/citiesاگر داده شوند مجموعهٔ فعلی را جایگزین میکنند.
آدرس پیشفرض (مطب)
هر پزشک ایمپورتشده همیشه دستکم یک آدرس (doctor_addresses، type=personal) دارد:
- اگر پزشک هیچ آدرسی نداشته باشد، یک آدرس با نام «مطب دکتر {نام}» ساخته میشود و
شهر/استان آن از اولین عنصر آرایههای
cities/statesهمان درخواست پر میشود. - در ایمپورت مجدد آدرس تکراری ساخته نمیشود؛ فقط فیلدهای خالیِ آدرس موجود (نام/شهر/استان) backfill میشوند — مقادیر ویرایششده توسط کاربر بازنویسی نمیشوند.
- اگر
cities/statesدر درخواست نباشند، آدرس فقط با نام ساخته میشود (کرالر در حالت auto همیشه هر دو را میفرستد).
(تستها: DoctorImportTest::testImportCreatesDefaultOfficeAddressWithCityAndProvince،
testImportWithoutLocationStillCreatesOfficeAddress)
Response
201 Created (ساخته شد)
{ "success": true, "data": { "uuid": "…", "created": true } }
200 OK (بهروزرسانی شد)
{ "success": true, "data": { "uuid": "…", "created": false } }
200 OK (رد بهدلیل تصاحبشده)
{ "success": true, "data": { "uuid": "…", "created": false, "skipped": "claimed" } }
200 OK (رد بهدلیل کد نظام پزشکی تکراری با منبع دیگر)
{ "success": true, "data": { "uuid": "…", "created": false, "skipped": "duplicate" } }
422 (اعتبارسنجی)
{ "success": false, "data": null, "errors": [ { "code": "…", "message": "کد نظام پزشکی الزامی است", "field": "medical_system_code" } ] }
عکس پروفایل
پس از ایمپورت، عکس با دو مرحله اضافه میشود (همان مسیر پزشکِ عادی):
POST /file/upload/clinic_pro/doctor/field_image— بدنه: باینری خام؛ هدرContent-Disposition: attachment; filename="145657.jpg". پاسخ:{ fid, uuid, url, filename, filemime, filesize }.PATCH /api/v1/doctor/{uuid}با بدنهٔ{ "image_data": <پاسخ مرحلهٔ ۱> }تا به آرایهٔimagesپروفایل افزوده شود.
انتقال مالکیت به پزشک واقعی
Endpoint:
POST /api/v1/admin/doctors/{uuid}/transfer— permissionROLE_ADMIN
مالکیت یک پروفایلِ unclaimed را به پزشک واقعی منتقل میکند.
// Request
{ "mobile": "09120000000" }
مراحل اتمیک:
- کاربر واقعی بر پایهٔ
mobileپیدا یا ساخته میشود (باید موبایل معتبر ایران باشد). - اگر کاربر واقعی از قبل صاحب پزشک دیگری باشد →
409. user_idپروفایل به کاربر واقعی تغییر میکند،ROLE_DOCTORبه او داده میشود،owner_status = claimedوclaimed_at = nowوmanaged_by = nullمیشود.- کاربر جانشین حذف میشود — فقط اگر واقعاً
ROLE_UNCLAIMED_DOCTORداشته باشد و دیگر هیچ پزشکی به او وصل نباشد (حذف امن).
// Response 200
{ "success": true, "data": {
"uuid": "…", "owner_status": "claimed",
"user_mobile": "09120000000",
"claim": { "uuid": "…" }
} }
این اندپوینت در
App\Doctor\Controller\DoctorClaimController::transferاست و همانDoctorClaimService::transferByAdminرا صدا میزند؛ هر انتقال یک رکورد ممیزی درdoctor_claim_requestsباverification_method = "admin_manual"میسازد. جریان self-claim پزشک (با احراز هویت API.ir) درdocs/api/doctor-claim.mdمستند است.
| کد | حالت |
|---|---|
404 |
پزشک یافت نشد |
409 |
قبلاً claimed است، یا کاربر مقصد پزشک دیگری دارد |
422 |
موبایل نامعتبر |
کاربر سیستمی و چرخهٔ عمر
# ساخت/فعالسازی کاربر مالک سیستمی (کرالر با این لاگین میکند)
php bin/console app:system-owner 0000000000 --password=<secret> --activate
# غیرفعالسازی پس از پایان کار (کرالر هم با --deactivate-on-finish این را از طریق API انجام میدهد)
php bin/console app:system-owner 0000000000 --deactivate
کاربر سیستمی least privilege است: فقط ROLE_USER,ROLE_IMPORTER میگیرد (اجرای مجدد
دستور، ROLE_ADMIN قدیمی را هم حذف میکند). ROLE_IMPORTER فقط به همین اندپوینت ایمپورت
دسترسی دارد و به هیچ اندپوینت /api/v1/admin/* دیگری راه ندارد (تست: DoctorImportTest::testImporterRoleCanImportButNothingElse).
لاگین سرویسی (captcha)
مسیر /api/v1/user/login کپچای ALTCHA دارد. برای لاگین headless کرالر، هدر سرّی
تعریف شده است:
X-Service-Token: <مقدار env CRAWLER_SERVICE_TOKEN>
- فقط کپچا دور زده میشود؛ rate limit و اعتبارسنجی رمز دستنخورده میمانند.
- اگر env خالی/تعریفنشده باشد هیچ bypass وجود ندارد (secure by default).
- چرخش credential: تغییر
CRAWLER_SERVICE_TOKEN+ تغییر رمز باapp:system-owner … --password=…؛ ابطال فوری:--deactivate.
backfill نقش جانشینهای قدیمی
جانشینهای ساختهشده قبل از افزودن نقش marker:
php bin/console app:doctors:backfill-surrogate-role --dry-run # فقط گزارش
php bin/console app:doctors:backfill-surrogate-role # اعمال
اصلاح نام رکوردهای قدیمی (حذف پیشوند «دکتر»)
رکوردهای IRIMC که پیش از این تغییر با پیشوند «دکتر» ذخیره شده بودند:
php bin/console app:doctors:fix-irimc-names --dry-run # فقط گزارش
php bin/console app:doctors:fix-irimc-names # اعمال (فقط source='irimc')
پاکسازی کامل برای دیتابیس تست
حذف همهٔ پزشکان + دادههای وابسته (FK-safe) برای شروع تمیز:
php bin/console app:doctors:purge # dry-run: فقط گزارش تعداد هر جدول
php bin/console app:doctors:purge --force # حذف واقعی + کاربران جانشین یتیم
⚠️ مخرب —
appointments/comments/ratesرا هم پاک میکند. روی prod نیازمند--i-know-this-is-prodاست و پیشفرض متوقف میشود.