feat: implement doctor import completion feature and crawler enhancements
- Added the remaining components for the doctor import feature in the backend, including role management, ownership transfer endpoint, and captcha bypass for crawler service login. - Created detailed scenarios for the doctor claim process, ensuring proper identity verification and mobile validation. - Established a crawler interface for token management and state tracking using SQLite, enabling a resume capability for the crawling process.
This commit is contained in:
@@ -0,0 +1,201 @@
|
|||||||
|
# تکمیل فیچر ایمپورت پزشکان نظام پزشکی (قطعات باقیمانده)
|
||||||
|
|
||||||
|
## پروژه
|
||||||
|
|
||||||
|
`clinicpro` (backend) + یک تغییر کوچک در `clinicpro-crawler/clinicpro_client.py`
|
||||||
|
|
||||||
|
> **برنچ:** تغییرات backend روی برنچ جدید در repo خود clinicpro: `git -C clinicpro checkout -b feature/irimc-doctor-import`
|
||||||
|
> (تغییر کرالر در repo والد است — همانجا commit شود.)
|
||||||
|
|
||||||
|
## زمینه
|
||||||
|
|
||||||
|
سند سناریو: [docs/scenarios/ایمپورت-پزشکان-نظام-پزشکی.md](../docs/scenarios/ایمپورت-پزشکان-نظام-پزشکی.md).
|
||||||
|
بخش عمدهٔ فیچر **قبلاً پیاده شده و در کد موجود است** — دوباره نساز:
|
||||||
|
|
||||||
|
| قطعه | وضعیت |
|
||||||
|
|------|-------|
|
||||||
|
| ستونهای مالکیت `doctors` (`owner_status`, `source`, `source_ref`, `managed_by`, `claimed_at`) + متد `transferOwnershipTo()` | ✅ `src/Doctor/Entity/Doctor.php:81-98,390` |
|
||||||
|
| Migration | ✅ `migrations/Version20260711120000.php` (اعمالشده) |
|
||||||
|
| `POST /api/v1/admin/doctors/import` — idempotent، کاربر جانشین `imp_<hash>`، skip روی claimed | ✅ `src/Admin/Controller/AdminApiController.php:483` |
|
||||||
|
| دستور `app:system-owner` | ✅ `src/Auth/Command/SystemOwnerCommand.php` |
|
||||||
|
| مستند | ✅ `docs/api/doctor-import.md` |
|
||||||
|
| کرالر (`clinicpro_client.py`, `pipeline.py`) | ✅ `clinicpro-crawler/` |
|
||||||
|
|
||||||
|
**چهار قطعه از سند هنوز پیاده نشده** — این پرامپت فقط همانهاست:
|
||||||
|
|
||||||
|
1. نقش `ROLE_UNCLAIMED_DOCTOR` برای کاربر جانشین (§۳ سند) — الان جانشین فقط `ROLE_USER` میگیرد.
|
||||||
|
2. اندپوینت انتقال مالکیت `POST /api/v1/admin/doctors/{uuid}/transfer` (§۴) — متد entity هست، کنترلر **نیست**.
|
||||||
|
3. حذف امن کاربر جانشین بعد از انتقال (§۳) — وابسته به ۱ و ۲.
|
||||||
|
4. رد شدن کپچا برای لاگین سرویسیِ کرالر (§۷) — الان لاگین headless با `ERR_CAPTCHA_001` میشکند.
|
||||||
|
|
||||||
|
## فایلهای مرتبط
|
||||||
|
|
||||||
|
| فایل | نقش |
|
||||||
|
|------|-----|
|
||||||
|
| `src/Admin/Controller/AdminApiController.php` | `importDoctor` خط ۴۸۳ (اصلاح نقش) + اندپوینت transfer جدید |
|
||||||
|
| `src/Doctor/Entity/Doctor.php` | `transferOwnershipTo(User)` خط ۳۹۰ — آماده، فقط صدا بزن |
|
||||||
|
| `src/Auth/Entity/User.php` | `addRole()` خط ۱۰۵، `hasRole()` خط ۱۱۴، `setStatus()` |
|
||||||
|
| `src/Auth/Security/PasswordAuthenticator.php` | خط ۴۹: `$this->captcha->assertValid($request)` — نقطهٔ bypass |
|
||||||
|
| `src/Shared/Captcha/CaptchaGuard.php` | گارد کپچا (برای فهم امضا) |
|
||||||
|
| `docs/api/doctor-import.md` | باید transfer + هدر سرویس مستند شود |
|
||||||
|
| `clinicpro-crawler/clinicpro_client.py` | افزودن هدر سرویس به لاگین |
|
||||||
|
|
||||||
|
## وضعیت فعلی
|
||||||
|
|
||||||
|
ساخت کاربر جانشین در `importDoctor` (خط ~۵۱۴) — **بدون نقش اختصاصی**:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$synthetic = 'imp_' . substr(md5($source . ':' . $code), 0, 14);
|
||||||
|
$user = $userRepo->findOneBy(['mobileNumber' => $synthetic]);
|
||||||
|
if ($user === null) {
|
||||||
|
$user = new User($synthetic);
|
||||||
|
$user->setRealName($name);
|
||||||
|
$user->setStatus(0); // جانشین: هرگز لاگین نمیکند
|
||||||
|
$this->em->persist($user);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
متد آمادهٔ entity:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Doctor.php:388-396 — user_id را پر میکند، مدیریت سیستمی را برمیدارد و وضعیت را claimed میکند.
|
||||||
|
public function transferOwnershipTo(User $user): self
|
||||||
|
{
|
||||||
|
...
|
||||||
|
$this->ownerStatus = 'claimed';
|
||||||
|
$this->claimedAt = time();
|
||||||
|
```
|
||||||
|
|
||||||
|
کپچا (بدون استثنا):
|
||||||
|
|
||||||
|
```php
|
||||||
|
// PasswordAuthenticator.php:49 — ابتدای authenticate()
|
||||||
|
$this->captcha->assertValid($request);
|
||||||
|
```
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
### ۱. نقش `ROLE_UNCLAIMED_DOCTOR` برای کاربر جانشین
|
||||||
|
|
||||||
|
در `importDoctor`، هنگام ساخت کاربر جانشین:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$user = new User($synthetic);
|
||||||
|
$user->setRealName($name);
|
||||||
|
$user->setStatus(0);
|
||||||
|
$user->addRole('ROLE_UNCLAIMED_DOCTOR');
|
||||||
|
$this->em->persist($user);
|
||||||
|
```
|
||||||
|
|
||||||
|
- نقش را بهصورت رشته اضافه کن (الگوی موجود `addRole('ROLE_DOCTOR')` در پروژه).
|
||||||
|
- **ایمپورتهای قبلی** (جانشینهای موجود بدون این نقش): چون idempotent است، در همان `importDoctor` وقتی `$doctor !== null && unclaimed` است هم نقش را به کاربر فعلیاش تضمین کن (`if (!$user->hasRole(...)) addRole(...)`) — کاربر جانشین از `$doctor->getUser()` در دسترس است.
|
||||||
|
|
||||||
|
### ۲. اندپوینت انتقال مالکیت
|
||||||
|
|
||||||
|
در `AdminApiController` (کنار `importDoctor`، همان الگوی OA + `$this->success/error`):
|
||||||
|
|
||||||
|
```php
|
||||||
|
#[Route('/api/v1/admin/doctors/{uuid}/transfer', methods: ['POST'])]
|
||||||
|
public function transferDoctor(string $uuid, Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$data = json_decode($request->getContent(), true) ?? [];
|
||||||
|
$mobile = trim((string) ($data['mobile'] ?? ''));
|
||||||
|
if (!preg_match('/^09\d{9}$/', $mobile)) {
|
||||||
|
return $this->error(ErrorCodes::VALIDATION, 'شماره موبایل نامعتبر است', 422, 'mobile');
|
||||||
|
}
|
||||||
|
|
||||||
|
$doctor = $this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]);
|
||||||
|
if ($doctor === null) {
|
||||||
|
return $this->error(ErrorCodes::NOT_FOUND, 'پزشک یافت نشد', 404);
|
||||||
|
}
|
||||||
|
if ($doctor->getOwnerStatus() === 'claimed') {
|
||||||
|
return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این پروفایل قبلاً تصاحب شده است', 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
$userRepo = $this->em->getRepository(User::class);
|
||||||
|
$target = $userRepo->findOneBy(['mobileNumber' => $mobile]);
|
||||||
|
if ($target === null) {
|
||||||
|
$target = new User($mobile);
|
||||||
|
$target->setRealName($doctor->getName());
|
||||||
|
$target->setStatus(1);
|
||||||
|
$this->em->persist($target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// قید یکتای user_id: کاربر هدف نباید از قبل پزشک دیگری داشته باشد
|
||||||
|
$already = $this->em->getRepository(Doctor::class)->findOneBy(['user' => $target]);
|
||||||
|
if ($already !== null && $already->getId() !== $doctor->getId()) {
|
||||||
|
return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این کاربر قبلاً پروفایل پزشک دیگری دارد', 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
$surrogate = $doctor->getUser();
|
||||||
|
$target->addRole('ROLE_DOCTOR');
|
||||||
|
$doctor->transferOwnershipTo($target);
|
||||||
|
|
||||||
|
// حذف امن جانشین: فقط اگر واقعاً جانشین است و هیچ پزشک دیگری به او وصل نیست
|
||||||
|
if ($surrogate !== null
|
||||||
|
&& $surrogate->getId() !== $target->getId()
|
||||||
|
&& $surrogate->hasRole('ROLE_UNCLAIMED_DOCTOR')
|
||||||
|
&& $this->em->getRepository(Doctor::class)->count(['user' => $surrogate]) === 0) {
|
||||||
|
$this->em->remove($surrogate);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'uuid' => $doctor->getUuid(),
|
||||||
|
'owner_status' => $doctor->getOwnerStatus(),
|
||||||
|
'user_mobile' => $mobile,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
نکتهها:
|
||||||
|
- **ترتیب**: اول `transferOwnershipTo` (که `user_id` را عوض میکند)، بعد شمارش پزشکانِ جانشین — دقت کن Doctrine تا `flush` تغییر را به DB نمیبرد، پس `count(['user' => $surrogate])` ممکن است هنوز همین پزشک را بشمارد. یا اول flush کن بعد حذف در flush دوم، یا شرط را `count === 0 || (count === 1 && همین doctor)` بگذار. سناریوی سادهٔ امن: دو مرحله — `flush()` بعد از transfer، سپس شمارش و `remove($surrogate)` و `flush()` دوم.
|
||||||
|
- امضای دقیق فیلد رابطهٔ `Doctor::user` را قبل از `findOneBy(['user' => ...])` از entity تأیید کن.
|
||||||
|
- ثابتهای `ErrorCodes` موجود (`VALIDATION`, `NOT_FOUND`, `ERR_CONFLICT_001`) — چیز جدید نساز.
|
||||||
|
- بلوک OA (سواگر) مثل `importDoctor` بنویس: body `{ mobile }`، پاسخهای 200/404/409/422.
|
||||||
|
|
||||||
|
### ۳. رد شدن کپچا برای لاگین سرویسی کرالر
|
||||||
|
|
||||||
|
راه انتخابی سند (§۷، گزینهٔ «هدر سرّی مورد اعتماد»)، امنتر از خاموشکردن ALTCHA:
|
||||||
|
|
||||||
|
در `PasswordAuthenticator::authenticate()`، قبل از `assertValid`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$serviceToken = $_ENV['CRAWLER_SERVICE_TOKEN'] ?? '';
|
||||||
|
$sentToken = (string) $request->headers->get('X-Service-Token', '');
|
||||||
|
$isServiceLogin = $serviceToken !== '' && hash_equals($serviceToken, $sentToken);
|
||||||
|
|
||||||
|
if (!$isServiceLogin) {
|
||||||
|
$this->captcha->assertValid($request);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **فقط کپچا** دور زده میشود؛ rate-limit و اعتبارسنجی رمز سر جای خود میمانند.
|
||||||
|
- اگر env خالی باشد هیچ bypass وجود ندارد (پیشفرض امن).
|
||||||
|
- env جدید را به `.env` (خالی) و `.env.example` اضافه کن + ذکر در مستند.
|
||||||
|
- ترجیحاً env را از طریق constructor bind کن (الگوی `services.yaml` مثل `$appUrl: '%env(APP_BASE_URL)%'`) نه `$_ENV` مستقیم — با الگوی موجود فایل هماهنگ شو.
|
||||||
|
|
||||||
|
### ۴. کرالر: ارسال هدر سرویس
|
||||||
|
|
||||||
|
در `clinicpro-crawler/clinicpro_client.py`، متد لاگین: اگر env `CLINICPRO_SERVICE_TOKEN` ست بود، هدر `X-Service-Token` را به درخواست لاگین اضافه کن (فقط لاگین کافی است). به `.env.example` کرالر هم اضافه کن.
|
||||||
|
|
||||||
|
### ۵. مستند + تست
|
||||||
|
|
||||||
|
- `docs/api/doctor-import.md`: بخش transfer (method/path/permission/body/پاسخها/خطاها با مثال JSON) + توضیح `X-Service-Token` برای لاگین سرویسی + نقش `ROLE_UNCLAIMED_DOCTOR`.
|
||||||
|
- تست e2e مطابق §۹ سند:
|
||||||
|
```bash
|
||||||
|
ddev exec php bin/console app:system-owner 0000000000 --password=test123 --activate
|
||||||
|
cd ../clinicpro-crawler && CLINICPRO_PASSWORD=test123 .venv/bin/python pipeline.py \
|
||||||
|
--mode file --file output/یاسوج/doctors.json --no-photos --limit 2 --interval 2
|
||||||
|
```
|
||||||
|
سپس یک transfer دستی با curl و بررسی: `owner_status=claimed`، کاربر جانشین حذفشده، کاربر واقعی `ROLE_DOCTOR` دارد.
|
||||||
|
- تستهای موجود `tests/Admin`/`tests/Doctor` را اجرا کن؛ اگر تست importDoctor وجود دارد، case انتقال را کنارش اضافه کن.
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
- `transferOwnershipTo` از قبل `claimed_at`/`owner_status`/`managed_by` را هندل میکند — منطق را در کنترلر تکرار نکن.
|
||||||
|
- پروفایل `pending_transfer` در این فاز فقط یک مقدار enum است؛ جریان درخواست تصاحب از سمت Nobat724 فاز بعدی است (§۹) — نساز.
|
||||||
|
- حذف جانشین باید **دقیقاً** سه شرط سند را داشته باشد: نقش `ROLE_UNCLAIMED_DOCTOR` + هیچ پزشک متصل + غیر از کاربر هدف. کاربر واقعی را هرگز حذف نکن.
|
||||||
|
- `activeDoctorAppointment` بعد از transfer دست نزن — روشنکردن نوبتدهی با مالک جدید است.
|
||||||
|
- بعد از تغییر API، بهروزرسانی `docs/api/doctor-import.md` در همین session الزامی است (قانون پروژه).
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
حتماً، متن کامل پرامپت را در محیط قابل کپی میگذارم:
|
||||||
|
|
||||||
|
یک قابلیت کامل برای Claim Doctor Profile یا «احراز مالکیت پروفایل پزشک» در سیستم Clinic Pro و سایت نوبت 724 پیادهسازی کن.
|
||||||
|
|
||||||
|
قبل از هرگونه تغییر، ابتدا ساختار فعلی Backend و Frontend را کامل بررسی کن و مشخص کن وضعیت پزشکان ایمپورتشده در حال حاضر چگونه مدیریت میشود.
|
||||||
|
|
||||||
|
در سیستم، پزشک ایمپورتشده میتواند یکی از وضعیتهای زیر را داشته باشد:
|
||||||
|
|
||||||
|
- unclaimed
|
||||||
|
- pending_transform
|
||||||
|
- claimed
|
||||||
|
|
||||||
|
نام دقیق enum، field و statusهای موجود در پروژه را بررسی کن و بدون ایجاد ساختار تکراری، از معماری فعلی استفاده کن.
|
||||||
|
|
||||||
|
سناریوی اصلی
|
||||||
|
|
||||||
|
زمانی که یک پزشک از دیتای سازمان نظام پزشکی Import میشود و هنوز به کاربر یا پزشک واقعی در Clinic Pro متصل نشده است، وضعیت آن باید unclaimed باشد.
|
||||||
|
|
||||||
|
تا زمانی که پروفایل پزشک Claim نشده است:
|
||||||
|
|
||||||
|
- نوبتدهی آنلاین پزشک باید غیرفعال باشد.
|
||||||
|
- در سایت اصلی نوبت 724 و تمام سایتهای زیرمجموعه و دامنههای نمایندگان، وضعیت پروفایل باید مشخص باشد.
|
||||||
|
- کاربر باید متوجه شود که این پروفایل هنوز توسط پزشک تأیید و مدیریت نشده است.
|
||||||
|
|
||||||
|
در صفحه پروفایل پزشک، دقیقاً زیر بخش «نوبتدهی»، یک بخش برای احراز مالکیت پروفایل نمایش داده شود.
|
||||||
|
|
||||||
|
متن مناسب فارسی مانند زیر نمایش داده شود:
|
||||||
|
|
||||||
|
«آیا شما این پزشک هستید؟»
|
||||||
|
|
||||||
|
و یک دکمه با عنوان:
|
||||||
|
|
||||||
|
«تأیید و مدیریت این پروفایل»
|
||||||
|
|
||||||
|
این بخش فقط برای پزشکانی نمایش داده شود که وضعیت آنها unclaimed است.
|
||||||
|
|
||||||
|
Modal احراز مالکیت پروفایل
|
||||||
|
|
||||||
|
با کلیک روی دکمه، یک Modal باز شود.
|
||||||
|
|
||||||
|
فرم باید با UI فعلی پروژه، RTL، فارسی و کاملاً Responsive طراحی شود.
|
||||||
|
|
||||||
|
اطلاعات موردنیاز فرم:
|
||||||
|
|
||||||
|
- شماره موبایل
|
||||||
|
- کد ملی
|
||||||
|
- تاریخ تولد به شمسی
|
||||||
|
- نام
|
||||||
|
- نام خانوادگی
|
||||||
|
|
||||||
|
ارتباط با API.ir
|
||||||
|
|
||||||
|
تمام فرآیند احراز هویت باید از Backend انجام شود.
|
||||||
|
|
||||||
|
هیچ Request مستقیمی از Frontend به API.ir ارسال نشود.
|
||||||
|
|
||||||
|
Token مربوط به API.ir باید در Environment Variable نگهداری شود و هرگز به Frontend ارسال نشود.
|
||||||
|
|
||||||
|
از API زیر برای دریافت اطلاعات هویتی شخص استفاده کن:
|
||||||
|
|
||||||
|
curl https://s.api.ir/api/sw1/PersonInfo
|
||||||
|
–request POST
|
||||||
|
–header ‘Content-Type: application/json’
|
||||||
|
–header ‘Authorization: Bearer [API_TOKEN]’
|
||||||
|
–data ‘{
|
||||||
|
“nationalCode”: “0010007700”,
|
||||||
|
“birthDate”: “1371/1/1”
|
||||||
|
}’
|
||||||
|
|
||||||
|
نمونه Response:
|
||||||
|
|
||||||
|
{
|
||||||
|
“data”: {
|
||||||
|
“nationalCode”: “0010007700”,
|
||||||
|
“firstName”: “محسن”,
|
||||||
|
“lastName”: “اکبری”,
|
||||||
|
“fatherName”: “علی”,
|
||||||
|
“gender”: 1,
|
||||||
|
“alive”: true
|
||||||
|
},
|
||||||
|
“success”: false,
|
||||||
|
“code”: 0,
|
||||||
|
“message”: null
|
||||||
|
}
|
||||||
|
|
||||||
|
طبق نمونه API، ورودی PersonInfo شامل nationalCode و birthDate است.
|
||||||
|
|
||||||
|
ابتدا بررسی کن که آیا API.ir Endpoint دیگری برای تطبیق شماره موبایل و کد ملی دارد یا خیر.
|
||||||
|
|
||||||
|
اگر چنین Endpointی در ساختار فعلی پروژه یا مستندات موجود تعریف شده است، از آن برای تأیید مالکیت شماره موبایل استفاده کن.
|
||||||
|
|
||||||
|
در غیر این صورت، شماره موبایل باید با OTP تأیید شود.
|
||||||
|
|
||||||
|
OTP بهتنهایی به معنی مالکیت پروفایل پزشک نیست.
|
||||||
|
|
||||||
|
Claim فقط زمانی موفق باشد که هویت شخص با اطلاعات پزشک Importشده تطبیق داده شده باشد.
|
||||||
|
|
||||||
|
فرآیند احراز هویت
|
||||||
|
|
||||||
|
کاربر فرم را تکمیل و ارسال میکند.
|
||||||
|
|
||||||
|
Backend باید اطلاعات را دریافت و Validation کند.
|
||||||
|
|
||||||
|
سپس با استفاده از nationalCode و birthDate به API.ir درخواست PersonInfo ارسال شود.
|
||||||
|
|
||||||
|
اطلاعات برگشتی شامل موارد زیر بررسی شود:
|
||||||
|
|
||||||
|
- nationalCode
|
||||||
|
- firstName
|
||||||
|
- lastName
|
||||||
|
- alive
|
||||||
|
|
||||||
|
اگر alive برابر false بود، فرآیند Claim متوقف شود.
|
||||||
|
|
||||||
|
نام و نام خانوادگی برگشتی از API.ir باید با اطلاعات پزشک Importشده از سازمان نظام پزشکی مقایسه شود.
|
||||||
|
|
||||||
|
همچنین نام و نام خانوادگی واردشده توسط کاربر باید با اطلاعات تأییدشده مقایسه شود.
|
||||||
|
|
||||||
|
قبل از Compare نامهای فارسی، عملیات Normalize انجام شود.
|
||||||
|
|
||||||
|
موارد زیر مدیریت شوند:
|
||||||
|
|
||||||
|
- تفاوت ي و ی
|
||||||
|
- تفاوت ك و ک
|
||||||
|
- فاصلههای اضافه
|
||||||
|
- نیمفاصله
|
||||||
|
- فاصله ابتدا و انتهای متن
|
||||||
|
- Unicode normalization
|
||||||
|
|
||||||
|
از Compare ساده و مستقیم String استفاده نکن.
|
||||||
|
|
||||||
|
در صورت امکان، یک Service مشترک برای Persian Name Normalization ایجاد یا از Service موجود استفاده کن.
|
||||||
|
|
||||||
|
تأیید شماره موبایل
|
||||||
|
|
||||||
|
شماره موبایل کاربر باید تأیید شود.
|
||||||
|
|
||||||
|
اگر API.ir امکان تطبیق شماره موبایل با کد ملی را دارد، از همان API استفاده کن.
|
||||||
|
|
||||||
|
در غیر این صورت، از OTP موجود در سیستم استفاده کن.
|
||||||
|
|
||||||
|
اگر سیستم در حال حاضر OTP Service دارد، Service جدید و تکراری ایجاد نکن.
|
||||||
|
|
||||||
|
از زیرساخت فعلی Authentication و OTP استفاده کن.
|
||||||
|
|
||||||
|
پس از تأیید OTP، شماره موبایل Verified در نظر گرفته شود.
|
||||||
|
|
||||||
|
اما Claim نهایی فقط بعد از موفقیت احراز هویت PersonInfo انجام شود.
|
||||||
|
|
||||||
|
اتصال پروفایل پزشک به کاربر
|
||||||
|
|
||||||
|
اگر احراز هویت موفق بود:
|
||||||
|
|
||||||
|
- User مربوط به پزشک را بر اساس معماری فعلی سیستم پیدا کن.
|
||||||
|
- اگر Flow فعلی سیستم اجازه ایجاد User را میدهد، User مناسب ایجاد شود.
|
||||||
|
- پروفایل Doctor ایمپورتشده به User مربوطه متصل شود.
|
||||||
|
- وضعیت Doctor از unclaimed به claimed تغییر کند.
|
||||||
|
- زمان Claim ذخیره شود.
|
||||||
|
- روش احراز هویت ذخیره شود.
|
||||||
|
- در صورت وجود Audit Log، عملیات Claim ثبت شود.
|
||||||
|
- از Claim مجدد Doctor جلوگیری شود.
|
||||||
|
|
||||||
|
تمام عملیات نهایی Claim باید داخل Transaction انجام شود.
|
||||||
|
|
||||||
|
اگر سیستم فعلی از وضعیت pending_transform در این Flow استفاده میکند، ابتدا منطق آن را بررسی کن.
|
||||||
|
|
||||||
|
مشخص کن pending_transform دقیقاً در چه شرایطی استفاده میشود و Flow جدید را با همان معماری هماهنگ کن.
|
||||||
|
|
||||||
|
Status جدید و تکراری ایجاد نکن مگر اینکه واقعاً ضروری باشد.
|
||||||
|
|
||||||
|
جلوگیری از Race Condition
|
||||||
|
|
||||||
|
ممکن است دو Request همزمان برای Claim یک Doctor ارسال شوند.
|
||||||
|
|
||||||
|
Backend باید از Race Condition جلوگیری کند.
|
||||||
|
|
||||||
|
قبل از نهایی کردن Claim، وضعیت Doctor مجدداً بررسی شود.
|
||||||
|
|
||||||
|
در صورت نیاز از Transaction، Database Lock یا مکانیزم مناسب معماری فعلی استفاده کن.
|
||||||
|
|
||||||
|
یک Doctor تحت هیچ شرایطی نباید به دو User مختلف متصل شود.
|
||||||
|
|
||||||
|
پیام موفقیت
|
||||||
|
|
||||||
|
بعد از Claim موفق، پیام خوشآمدگویی نمایش داده شود:
|
||||||
|
|
||||||
|
«دکتر [نام پزشک]، به نوبت 724 خوش آمدید 🎉
|
||||||
|
|
||||||
|
پروفایل شما با موفقیت تأیید شد و اکنون میتوانید اطلاعات پروفایل و تنظیمات نوبتدهی خود را مدیریت کنید.»
|
||||||
|
|
||||||
|
سپس کاربر بر اساس Flow فعلی Authentication سیستم به پنل مناسب هدایت شود.
|
||||||
|
|
||||||
|
مدیریت خطاها
|
||||||
|
|
||||||
|
برای حالتهای زیر Error Handling مناسب ایجاد کن:
|
||||||
|
|
||||||
|
- اطلاعات هویتی اشتباه است.
|
||||||
|
- تاریخ تولد اشتباه است.
|
||||||
|
- نام شخص با پزشک تطبیق ندارد.
|
||||||
|
- کد ملی نامعتبر است.
|
||||||
|
- شماره موبایل تأیید نشده است.
|
||||||
|
- OTP نامعتبر یا منقضی شده است.
|
||||||
|
- API.ir در دسترس نیست.
|
||||||
|
- API.ir Timeout شده است.
|
||||||
|
- Response API نامعتبر است.
|
||||||
|
- Doctor قبلاً Claim شده است.
|
||||||
|
- Doctor در وضعیت قابل Claim نیست.
|
||||||
|
- User دیگری همزمان Doctor را Claim کرده است.
|
||||||
|
- Rate Limit رد شده است.
|
||||||
|
|
||||||
|
پیامهای Frontend باید فارسی، واضح و قابل فهم باشند.
|
||||||
|
|
||||||
|
اطلاعات حساس API یا جزئیات Internal Error در Frontend نمایش داده نشود.
|
||||||
|
|
||||||
|
امنیت
|
||||||
|
|
||||||
|
Endpoint مربوط به Claim باید Rate Limit داشته باشد.
|
||||||
|
|
||||||
|
برای جلوگیری از Brute Force، محدودیت تعداد تلاش بر اساس موارد مناسب مانند موارد زیر اعمال شود:
|
||||||
|
|
||||||
|
- IP
|
||||||
|
- Doctor ID
|
||||||
|
- National Code Hash
|
||||||
|
- User ID
|
||||||
|
- Mobile Number Hash
|
||||||
|
|
||||||
|
کد ملی و تاریخ تولد را در Logهای معمولی بهصورت Plain Text ثبت نکن.
|
||||||
|
|
||||||
|
شماره موبایل کامل را در Log ثبت نکن.
|
||||||
|
|
||||||
|
API Token را Log نکن.
|
||||||
|
|
||||||
|
Request و Response کامل API.ir را در Production Log ذخیره نکن.
|
||||||
|
|
||||||
|
در صورت نیاز برای Audit، فقط اطلاعات ضروری و Mask شده ذخیره شود.
|
||||||
|
|
||||||
|
در صورت وجود CAPTCHA یا ALTCHA در پروژه، بررسی کن که Claim Endpoint باید تحت محافظت ALTCHA قرار بگیرد.
|
||||||
|
|
||||||
|
از زیرساخت فعلی ALTCHA استفاده کن و سیستم CAPTCHA جدید ایجاد نکن.
|
||||||
|
|
||||||
|
Frontend
|
||||||
|
|
||||||
|
این قابلیت باید در موارد زیر کار کند:
|
||||||
|
|
||||||
|
- سایت اصلی نوبت 724
|
||||||
|
- تمام دامنههای زیرمجموعه
|
||||||
|
- سایتهای نمایندگان
|
||||||
|
|
||||||
|
از Component مشترک استفاده کن.
|
||||||
|
|
||||||
|
منطق Claim را برای هر Domain جداگانه Duplicate نکن.
|
||||||
|
|
||||||
|
Tenant و Domain Logic فعلی پروژه را بررسی کن و از همان ساختار استفاده کن.
|
||||||
|
|
||||||
|
UI باید با Design System فعلی پروژه هماهنگ باشد.
|
||||||
|
|
||||||
|
Modal باید:
|
||||||
|
|
||||||
|
- RTL باشد.
|
||||||
|
- فارسی باشد.
|
||||||
|
- Responsive باشد.
|
||||||
|
- روی موبایل UX مناسبی داشته باشد.
|
||||||
|
- Loading State داشته باشد.
|
||||||
|
- Error State داشته باشد.
|
||||||
|
- Success State داشته باشد.
|
||||||
|
- از Double Submit جلوگیری کند.
|
||||||
|
|
||||||
|
برای تاریخ تولد از Date Picker شمسی موجود در پروژه استفاده کن.
|
||||||
|
|
||||||
|
Library جدید برای تاریخ شمسی نصب نکن، مگر اینکه پروژه هیچ راهکار فعلی برای تاریخ شمسی نداشته باشد.
|
||||||
|
|
||||||
|
تست
|
||||||
|
|
||||||
|
تستهای Backend و Frontend لازم را اضافه کن.
|
||||||
|
|
||||||
|
حداقل سناریوهای زیر تست شوند:
|
||||||
|
|
||||||
|
- Claim موفق
|
||||||
|
- کد ملی اشتباه
|
||||||
|
- تاریخ تولد اشتباه
|
||||||
|
- عدم تطبیق نام پزشک
|
||||||
|
- Doctor قبلاً Claim شده
|
||||||
|
- Doctor با Status غیرمجاز
|
||||||
|
- API.ir Timeout
|
||||||
|
- API.ir Error
|
||||||
|
- Response نامعتبر API
|
||||||
|
- تلاش همزمان برای Claim یک Doctor
|
||||||
|
- Normalize صحیح نام فارسی
|
||||||
|
- OTP نامعتبر
|
||||||
|
- OTP منقضیشده
|
||||||
|
- Rate Limit
|
||||||
|
- نمایش Claim Form فقط برای unclaimed
|
||||||
|
- عدم نمایش Claim Form برای claimed
|
||||||
|
- عدم امکان Claim یک Doctor توسط دو User
|
||||||
|
|
||||||
|
API.ir را در تستها Mock کن.
|
||||||
|
|
||||||
|
تستها تحت هیچ شرایطی نباید به سرویس واقعی API.ir Request ارسال کنند.
|
||||||
|
|
||||||
|
خروجی مورد انتظار
|
||||||
|
|
||||||
|
قابلیت Claim Doctor Profile را بهصورت End-to-End پیادهسازی کن.
|
||||||
|
|
||||||
|
ابتدا معماری موجود پروژه را کامل بررسی کن.
|
||||||
|
|
||||||
|
سپس موارد زیر را پیادهسازی کن:
|
||||||
|
|
||||||
|
- Backend Claim Flow
|
||||||
|
- API.ir Integration
|
||||||
|
- Identity Verification
|
||||||
|
- Mobile Verification
|
||||||
|
- OTP Integration
|
||||||
|
- Security
|
||||||
|
- Rate Limiting
|
||||||
|
- Doctor/User Relation
|
||||||
|
- Transaction Handling
|
||||||
|
- Race Condition Protection
|
||||||
|
- Frontend Claim Section
|
||||||
|
- Claim Modal
|
||||||
|
- Success Flow
|
||||||
|
- Error Handling
|
||||||
|
- Tests
|
||||||
|
|
||||||
|
از ایجاد کد تکراری یا معماری موازی خودداری کن.
|
||||||
|
|
||||||
|
در پایان گزارشی ارائه بده که شامل موارد زیر باشد:
|
||||||
|
|
||||||
|
- فایلهای تغییرکرده
|
||||||
|
- Flow نهایی Claim
|
||||||
|
- نحوه ارتباط با API.ir
|
||||||
|
- نحوه تطبیق هویت
|
||||||
|
- نحوه تأیید شماره موبایل
|
||||||
|
- نحوه اتصال Doctor به User
|
||||||
|
- کاربرد دقیق pending_transform در Flow
|
||||||
|
- تغییرات Database
|
||||||
|
- Environment Variableهای جدید
|
||||||
|
- تستهای اضافهشده
|
||||||
|
- نکات امنیتی پیادهسازیشده
|
||||||
|
|
||||||
|
مهم:
|
||||||
|
|
||||||
|
بدون بررسی کد فعلی پروژه، درباره Entityها، Tableها، Fieldها، Routeها، Authentication Flow، OTP Service یا معماری سیستم فرض نساز.
|
||||||
|
|
||||||
|
ابتدا ساختار موجود را بررسی کن و قابلیت را کاملاً با معماری فعلی پروژه هماهنگ کن.
|
||||||
|
|
||||||
|
اگر خواستی، نسخه مخصوص Claude Code / Cursor Agent همین پرامپت را هم با دستورهای اجرایی دقیقتر برات تنظیم میکنم.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
در Crawler، برای اینکه بتواند اطلاعات را Crawl کرده و در Clinic DataYar یک Doctor ایجاد کند، نیاز به Token احراز هویت دارد.
|
||||||
|
|
||||||
|
در حال حاضر میتوانیم Token را بهصورت دستی ایجاد کرده و در اختیار Crawler قرار دهیم، اما ترجیح من این است که خود Crawler یک Web Interface ساده داشته باشد.
|
||||||
|
|
||||||
|
این وبسایت باید احراز هویت داخلی سادهای داشته باشد. احراز هویت میتواند Static باشد، چون این پنل استفاده عمومی ندارد و قرار نیست سیستم پیچیدهای برای مدیریت کاربران داشته باشیم.
|
||||||
|
|
||||||
|
من باید بتوانم وارد پنل Crawler شوم و سپس Username و Password مربوط به Clinic DataYar را وارد کنم.
|
||||||
|
|
||||||
|
Crawler باید با استفاده از این Username و Password به Clinic DataYar درخواست Login ارسال کند، Token را دریافت کند و آن را بهصورت امن ذخیره کند.
|
||||||
|
|
||||||
|
در نتیجه، نیازی به ساخت و وارد کردن دستی Token نباشد و بتوانم Token مورد استفاده Crawler را از طریق پنل مدیریت کنم.
|
||||||
|
|
||||||
|
همچنین Crawler باید یک Database داخلی داشته باشد.
|
||||||
|
|
||||||
|
من نمیخواهم برای Crawler یک Database Server مانند MySQL، MariaDB یا PostgreSQL نصب کنم.
|
||||||
|
|
||||||
|
ترجیح من استفاده از یک دیتابیس Local و File-Based مانند SQLite است.
|
||||||
|
|
||||||
|
هدف Database فقط این است که Crawler بتواند State و Progress عملیات Crawl را نگهداری کند و بداند:
|
||||||
|
|
||||||
|
- چه استانهایی پردازش شدهاند.
|
||||||
|
- چه شهرهایی پردازش شدهاند.
|
||||||
|
- در حال حاضر کدام شهر در حال پردازش است.
|
||||||
|
- چه Doctorهایی Crawl شدهاند.
|
||||||
|
- چه Doctorهایی با موفقیت به Clinic DataYar ارسال شدهاند.
|
||||||
|
- چه مواردی Failed شدهاند.
|
||||||
|
- چه کارهایی هنوز باقی مانده است.
|
||||||
|
|
||||||
|
Crawler باید فرآیند Crawl را بر اساس لیست استانها و شهرهای موجود در Clinic DataYar انجام دهد.
|
||||||
|
|
||||||
|
ابتدا باید لیست استانها و شهرها را از Clinic DataYar دریافت کند.
|
||||||
|
|
||||||
|
سپس پردازش باید بهصورت مرحلهای و ترتیبی انجام شود.
|
||||||
|
|
||||||
|
برای مثال:
|
||||||
|
|
||||||
|
ابتدا استان کهگیلویه و بویراحمد انتخاب شود.
|
||||||
|
|
||||||
|
سپس شهرهای این استان یکییکی پردازش شوند.
|
||||||
|
|
||||||
|
اگر شهر یاسوج در حال Crawl شدن است، Crawler باید تمام فرآیند مربوط به شهر یاسوج را کامل کند.
|
||||||
|
|
||||||
|
تا زمانی که Crawl و پردازش شهر یاسوج بهطور کامل تمام نشده است، Crawler نباید به شهر بعدی برود.
|
||||||
|
|
||||||
|
بعد از اتمام کامل یک شهر، وضعیت آن در Database بهعنوان Completed ذخیره شود و سپس پردازش شهر بعدی آغاز شود.
|
||||||
|
|
||||||
|
همین Flow برای تمام شهرهای یک استان انجام شود و بعد از اتمام کامل استان، Crawler به استان بعدی برود.
|
||||||
|
|
||||||
|
ترتیب کلی پردازش باید به شکل زیر باشد:
|
||||||
|
|
||||||
|
Province → City → Crawl Doctors → Process Doctors → Send Doctors to Clinic DataYar → Complete City → Next City → Complete Province → Next Province
|
||||||
|
|
||||||
|
Crawler باید قابلیت Resume داشته باشد.
|
||||||
|
|
||||||
|
یعنی اگر Process متوقف شد، Server Restart شد یا Crawler Crash کرد، بعد از اجرای مجدد نباید عملیات را از ابتدا شروع کند.
|
||||||
|
|
||||||
|
Crawler باید State ذخیرهشده در Database را بررسی کند و دقیقاً از آخرین مرحلهای که متوقف شده است، ادامه دهد.
|
||||||
|
|
||||||
|
هدف این است که Crawler یک سیستم State-Based و قابل Resume باشد و همیشه مشخص باشد چه کاری انجام شده، چه کاری در حال انجام است و چه کاری هنوز باقی مانده است.
|
||||||
|
|
||||||
|
اگر بخواهی، میتوانم همین متن را به یک پرامپت فنی دقیق برای Agent جهت پیادهسازی Crawler با NestJS + SQLite تبدیل کنم.
|
||||||
@@ -1,385 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fa" dir="rtl">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>سناریو: ایمپورت پزشکان نظام پزشکی و مدیریت مالکیت پروفایل</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
|
||||||
<style>
|
|
||||||
:root{
|
|
||||||
--bg:#f7f8fa; --card:#ffffff; --ink:#1f2933; --muted:#5b6673;
|
|
||||||
--line:#e3e8ee; --accent:#0f6fbf; --accent-soft:#eaf3fb; --code:#f2f4f7;
|
|
||||||
}
|
|
||||||
*{box-sizing:border-box}
|
|
||||||
html,body{margin:0;padding:0}
|
|
||||||
body{
|
|
||||||
font-family:'Vazirmatn',system-ui,sans-serif;
|
|
||||||
background:var(--bg); color:var(--ink);
|
|
||||||
direction:rtl; text-align:right;
|
|
||||||
line-height:1.95; font-size:16px; font-weight:400;
|
|
||||||
}
|
|
||||||
.wrap{max-width:900px;margin:0 auto;padding:40px 22px 80px}
|
|
||||||
.card{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:38px 42px;box-shadow:0 1px 3px rgba(16,24,40,.04)}
|
|
||||||
h1{font-weight:800;font-size:1.9rem;line-height:1.5;margin:.2em 0 .6em;border-bottom:3px solid var(--accent);padding-bottom:.4em}
|
|
||||||
h2{font-weight:700;font-size:1.35rem;margin:2em 0 .7em;color:var(--accent);border-right:4px solid var(--accent);padding-right:12px}
|
|
||||||
h3{font-weight:600;font-size:1.1rem;margin:1.5em 0 .5em}
|
|
||||||
p{margin:.7em 0}
|
|
||||||
a{color:var(--accent);text-decoration:none}
|
|
||||||
a:hover{text-decoration:underline}
|
|
||||||
strong{font-weight:700;color:#0b3d5c}
|
|
||||||
blockquote{margin:1.2em 0;padding:.6em 16px;background:var(--accent-soft);border-right:4px solid var(--accent);border-radius:8px;color:#124a6b}
|
|
||||||
blockquote p{margin:.3em 0}
|
|
||||||
ul,ol{padding-right:1.4em;margin:.6em 0}
|
|
||||||
li{margin:.35em 0}
|
|
||||||
hr{border:none;border-top:1px solid var(--line);margin:2em 0}
|
|
||||||
table{border-collapse:collapse;width:100%;margin:1.2em 0;font-size:.94rem}
|
|
||||||
th,td{border:1px solid var(--line);padding:9px 12px;text-align:right;vertical-align:top}
|
|
||||||
th{background:var(--accent-soft);font-weight:700;color:#0b3d5c}
|
|
||||||
tr:nth-child(even) td{background:#fafbfc}
|
|
||||||
code{font-family:'Vazirmatn',ui-monospace,monospace;background:var(--code);padding:2px 6px;border-radius:5px;font-size:.9em;direction:ltr;unicode-bidi:embed}
|
|
||||||
pre{background:#0f1b26;color:#e6edf3;padding:18px 20px;border-radius:12px;overflow-x:auto;direction:ltr;text-align:left;line-height:1.7}
|
|
||||||
pre code{background:transparent;color:inherit;padding:0;font-size:.88rem}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="wrap"><div class="card">
|
|
||||||
<h1 id="_1">سناریو: ایمپورت پزشکان سازمان نظام پزشکی و مدیریت مالکیت پروفایل</h1>
|
|
||||||
<blockquote>
|
|
||||||
<p>نسخه: ۱.۰ — تاریخ: ۱۴۰۵/۰۴/۱۹ (۲۰۲۶-۰۷-۱۰)
|
|
||||||
دامنه: <code>clinicpro</code> (بکاند + پنل ادمین) · <code>nobat724_front</code> (سایت عمومی) · <code>clinic-pro-tauri</code> (اپ دسکتاپ)
|
|
||||||
وضعیت: پیشنویس طراحی برای پیادهسازی</p>
|
|
||||||
</blockquote>
|
|
||||||
<hr />
|
|
||||||
<h2 id="_2">۱. خلاصه اجرایی</h2>
|
|
||||||
<p>یک دیتاست ۱۶۰ نفره از پزشکان از سامانه استعلام اعضای سازمان نظام پزشکی (<code>membersearch.irimc.org</code>) استخراج شده است. هدف، وارد کردن این پزشکان به Clinic Pro است تا در سایت عمومی Nobat724 نمایش داده شوند، <strong>پیش از آنکه پزشک واقعی در سیستم ثبتنام کرده باشد</strong>.</p>
|
|
||||||
<p>مشکل محوری: در مدل دادهی فعلی، هر پزشک (<code>Doctor</code>) بهصورت اجباری و <strong>یکبهیک و یکتا</strong> به یک کاربر (<code>User</code>) متصل است، و هر کاربر نیز الزاماً یک <strong>شماره موبایل یکتا و غیرتهی</strong> دارد. اما رکوردهای سازمان نظام پزشکی فاقد شماره موبایل هستند (<code>mobileNumber: null</code>). بنابراین نه میتوان کاربر ساخت (چون موبایل لازم است) و نه میتوان یک پزشک را بدون کاربر ذخیره کرد.</p>
|
|
||||||
<p>این سند یک مدل «مالکیت پروفایل» (Profile Ownership) طراحی میکند که در آن پزشکان ایمپورتشده در حالت <strong>«بدونمالک» (unclaimed)</strong> ذخیره و مدیریت میشوند، و بعداً از طریق یک فرایند احراز هویتشده در Nobat724 به پزشک واقعی <strong>منتقل (claim/transfer)</strong> میشوند.</p>
|
|
||||||
<hr />
|
|
||||||
<h2 id="_3">۲. مسئله و محدودیتهای سیستم فعلی</h2>
|
|
||||||
<p>پیش از طراحی راهحل، محدودیتهای واقعی کد فعلی مستند میشوند (منبع: <code>src/Doctor/Entity/Doctor.php</code>, <code>src/Auth/Entity/User.php</code>, <code>src/Doctor/Controller/DoctorController.php</code>).</p>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>محدودیت</th>
|
|
||||||
<th>جزئیات کد فعلی</th>
|
|
||||||
<th>پیامد برای ایمپورت</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td>کاربر برای پزشک اجباری است</td>
|
|
||||||
<td><code>Doctor::$user</code> → <code>OneToOne</code>، <code>JoinColumn(nullable: false, onDelete: RESTRICT)</code></td>
|
|
||||||
<td>نمیتوان پزشک بدون کاربر ذخیره کرد.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>رابطه پزشک↔کاربر یکتاست</td>
|
|
||||||
<td><code>UniqueConstraint idx_doctors_user (user_id)</code></td>
|
|
||||||
<td><strong>نمیتوان چند پزشک را به یک کاربر مشترک وصل کرد</strong> — ایدهی «همه به یک کاربر سیستمی» با این قید نقض میشود.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>موبایل کاربر اجباری و یکتاست</td>
|
|
||||||
<td><code>User::$mobileNumber</code> → <code>NOT NULL</code>، <code>UniqueConstraint uniq_mobile</code></td>
|
|
||||||
<td>بدون موبایل نمیتوان <code>User</code> ساخت؛ دادهی نظام پزشکی موبایل ندارد.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>ساخت پزشک به کاربر لاگینشده گره خورده</td>
|
|
||||||
<td><code>DoctorController::create()</code> از <code>#[CurrentUser] User $user</code> استفاده میکند و اگر همان کاربر پزشک داشته باشد خطای ۴۰۹ میدهد</td>
|
|
||||||
<td>مسیر فعلی ساخت پزشک برای ایمپورت انبوه مناسب نیست.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>medical_system_code</code> یکتا نیست</td>
|
|
||||||
<td><code>Doctor::$medicalSystemCode</code> → <code>nullable</code>, بدون <code>unique</code></td>
|
|
||||||
<td>برای جلوگیری از ایمپورت تکراری و برای تطبیق هنگام claim، باید کلید طبیعی یکتا شود.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<h3 id="_4">نتیجهگیری کلیدی طراحی</h3>
|
|
||||||
<p>خواستهی اولیه («همهی پزشکان ایمپورتشده به یک کاربر سیستمی اختصاص یابند») بهدلیل قید یکتای <code>user_id</code> روی جدول <code>doctors</code> <strong>مستقیماً قابل اجرا نیست</strong>. بنابراین یکی از دو مسیر زیر لازم است، و این سند <strong>گزینه A</strong> را توصیه میکند:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>گزینه A (توصیهشده): جداسازی «مالکیت» از «کاربر».</strong> ستون <code>Doctor.user</code> اختیاری (<code>nullable</code>) میشود. یک کاربر سیستمی بهنام «مالک سیستمی» (System Owner) صرفاً بهعنوان <strong>مدیرِ منطقیِ</strong> پزشکان بدونمالک عمل میکند (نه از طریق ستون <code>user_id</code>، بلکه از طریق فیلد جدید <code>managed_by</code>). این کار قید یکتا را نقض نمیکند و مدل تمیزتری میسازد.</li>
|
|
||||||
<li><strong>گزینه B (جایگزین کمتغییر): کاربر جانشین (Placeholder User) بهازای هر پزشک.</strong> برای هر پزشک یک <code>User</code> غیرفعال با شناسهی مصنوعی (مثلاً موبایل رزروشدهی <code>IRIMC-<code></code>) ساخته میشود. اسکیمای <code>doctors</code> تقریباً دستنخورده میماند اما جدول <code>users</code> با ۱۶۰ کاربر جعلی شلوغ میشود و هنگام claim باید ادغام (merge) انجام شود.</li>
|
|
||||||
</ul>
|
|
||||||
<p>مقایسه و تصمیم نهایی در بخش ۱۳ آمده است.</p>
|
|
||||||
<hr />
|
|
||||||
<h2 id="ownership-model">۳. مدل مفهومی مالکیت (Ownership Model)</h2>
|
|
||||||
<p>هر پروفایل پزشک یکی از این وضعیتهای مالکیت را دارد:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong><code>unclaimed</code> (بدونمالک):</strong> ایمپورتشده از نظام پزشکی، هنوز به پزشک واقعی وصل نشده. توسط «مالک سیستمی» مدیریت میشود. در Nobat724 نمایش داده میشود اما قابل ویرایش توسط عموم نیست و نوبتدهی آنلاین آن پیشفرض <strong>غیرفعال</strong> است.</li>
|
|
||||||
<li><strong><code>pending_transfer</code> (در انتظار انتقال):</strong> پزشک واقعی درخواست تصاحب داده و در حال احراز هویت / انتظار تأیید ادمین است.</li>
|
|
||||||
<li><strong><code>claimed</code> (تصاحبشده):</strong> مالکیت به پزشک واقعی منتقل شده؛ پروفایل به کاربر واقعی او متصل است و او کنترل کامل دارد.</li>
|
|
||||||
</ul>
|
|
||||||
<p>منبع پروفایل نیز ثبت میشود:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong><code>source</code></strong>: <code>irimc</code> (نظام پزشکی) یا <code>manual</code> (ساخت دستی/ثبتنام عادی — رفتار فعلی).</li>
|
|
||||||
<li><strong><code>source_ref</code></strong>: شناسهی یکتای رکورد مبدأ (<code>profile_url</code> id یا <code>medicalSystemCode</code>) برای idempotency و ممیزی.</li>
|
|
||||||
</ul>
|
|
||||||
<hr />
|
|
||||||
<h2 id="_5">۴. بخش اول — ایمپورت پزشکان نظام پزشکی</h2>
|
|
||||||
<h3 id="_6">۴.۱ کاربر «مالک سیستمی»</h3>
|
|
||||||
<p>یک کاربر ویژه یکبار ساخته میشود (از طریق دستور کنسول، همسبک <code>CreateAdminCommand</code>):</p>
|
|
||||||
<ul>
|
|
||||||
<li>موبایل رزروشده و ثابت، مثلاً <code>0000000000</code> (خارج از فضای شمارههای واقعی ایران، ۱۱ رقمی نامعتبر).</li>
|
|
||||||
<li>نقشها: <code>['ROLE_USER', 'ROLE_ADMIN']</code> یا نقش اختصاصی <code>ROLE_SYSTEM_OWNER</code>.</li>
|
|
||||||
<li><code>status = 0</code> (غیرفعال برای لاگین) تا امکان ورود با آن وجود نداشته باشد.</li>
|
|
||||||
<li><code>real_name = 'مالک سیستمی نوبت۷۲۴'</code>.</li>
|
|
||||||
</ul>
|
|
||||||
<p>این کاربر <strong>صاحب <code>user_id</code> پزشکان نیست</strong> (چون یکتاست)؛ بلکه شناسهاش در ستون جدید <code>Doctor.managed_by</code> قرار میگیرد تا مشخص باشد این پزشکان توسط پلتفرم مدیریت میشوند و بعداً قابل واگذاریاند.</p>
|
|
||||||
<h3 id="doctorsjson">۴.۲ نگاشت فیلدها از <code>doctors.json</code></h3>
|
|
||||||
<p>هر رکورد ورودی به این شکل به موجودیت <code>Doctor</code> نگاشت میشود:</p>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>فیلد ورودی (JSON)</th>
|
|
||||||
<th>مقصد در <code>Doctor</code></th>
|
|
||||||
<th>توضیح</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><code>name</code></td>
|
|
||||||
<td><code>name</code></td>
|
|
||||||
<td>مثلاً «دکتر فرخنده حسینی»</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>gender</code> (<code>woman</code>/<code>man</code>)</td>
|
|
||||||
<td><code>gender</code></td>
|
|
||||||
<td>با <code>Doctor::GENDERS</code> سازگار است</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>medicalSystemCode</code></td>
|
|
||||||
<td><code>medical_system_code</code> + <code>source_ref</code></td>
|
|
||||||
<td>کلید طبیعی یکتا برای dedup</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>mobileNumber</code> (<code>null</code>)</td>
|
|
||||||
<td><code>mobile_number</code> = <code>null</code></td>
|
|
||||||
<td>اجازه دارد null بماند</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>degree</code> (<code>general</code>)</td>
|
|
||||||
<td><code>degree</code></td>
|
|
||||||
<td>با <code>Doctor::DEGREES</code> سازگار است</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>info</code></td>
|
|
||||||
<td><code>info</code></td>
|
|
||||||
<td>«دکترای حرفهای پزشکی»</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>specialty_id</code> / <code>specialty_uuid</code></td>
|
|
||||||
<td>رابطه <code>specialties</code></td>
|
|
||||||
<td>تطبیق با جدول <code>specialties</code> (fallback با uuid)</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>state_id</code> / <code>state_uuid</code></td>
|
|
||||||
<td>رابطه <code>provinces</code></td>
|
|
||||||
<td>استان محل فعالیت</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>city_id</code> / <code>city_uuid</code></td>
|
|
||||||
<td>رابطه <code>cities</code></td>
|
|
||||||
<td>شهر محل فعالیت</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>images</code> (<code>[]</code>)</td>
|
|
||||||
<td><code>images</code></td>
|
|
||||||
<td>خالی → <code>null</code></td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>socialMedia</code></td>
|
|
||||||
<td><code>social_media</code></td>
|
|
||||||
<td>نگاشت به کلیدهای مجاز</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><code>profile_url</code> / <code>source_url</code></td>
|
|
||||||
<td>متادیتای ایمپورت</td>
|
|
||||||
<td>برای ممیزی و لینک بازبینی</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p>فیلدهای ثابت هنگام ایمپورت: <code>owner_status = 'unclaimed'</code>، <code>source = 'irimc'</code>، <code>managed_by = <systemOwnerId></code>، <code>active_doctor_appointment = false</code> (تا وقتی مالک واقعی برنامهی کاری تعریف کند نوبتدهی روشن نشود).</p>
|
|
||||||
<h3 id="idempotency">۴.۳ قواعد Idempotency و اعتبارسنجی</h3>
|
|
||||||
<ul>
|
|
||||||
<li>کلید یکتای ایمپورت: <code>(source = 'irimc', medical_system_code)</code>. اجرای مجدد ایمپورت رکورد موجود را <strong>بهروزرسانی</strong> میکند نه تکراریسازی.</li>
|
|
||||||
<li>رکوردهای بدون <code>medicalSystemCode</code> رد و در گزارش ایمپورت لاگ میشوند.</li>
|
|
||||||
<li>تطبیق تخصص/استان/شهر ابتدا با <code>*_id</code> و در صورت نبود، با <code>*_uuid</code> انجام میشود؛ عدم تطبیق باعث رد کل رکورد نمیشود بلکه فقط آن رابطه خالی میماند و در گزارش ثبت میشود.</li>
|
|
||||||
<li>خروجی دستور ایمپورت: تعداد ساختهشده / بهروزشده / ردشده + مسیر فایل گزارش.</li>
|
|
||||||
</ul>
|
|
||||||
<h3 id="_7">۴.۴ روش اجرا</h3>
|
|
||||||
<p>دستور کنسول اختصاصی (همسبک <code>SeedDemoDataCommand</code> و <code>SeedCategoriesCommand</code>):</p>
|
|
||||||
<pre><code class="language-bash">ddev exec php bin/console app:doctors:import-irimc var/import/doctors.json --dry-run
|
|
||||||
ddev exec php bin/console app:doctors:import-irimc var/import/doctors.json
|
|
||||||
</code></pre>
|
|
||||||
<p><code>--dry-run</code> فقط گزارش میدهد و چیزی ذخیره نمیکند. ایمپورت درون یک تراکنش دیتابیس و بهصورت دستهای (batch/flush هر ۵۰ رکورد) انجام میشود.</p>
|
|
||||||
<hr />
|
|
||||||
<h2 id="_8">۵. طراحی فنی دیتابیس</h2>
|
|
||||||
<p>تغییرات روی موجودیت <code>Doctor</code> (بههمراه یک migration در <code>migrations/</code>):</p>
|
|
||||||
<pre><code>doctors:
|
|
||||||
user_id INT NULL -- تغییر از NOT NULL به NULL (گزینه A)
|
|
||||||
managed_by INT NULL -- FK به users.id؛ کاربر «مالک سیستمی»
|
|
||||||
owner_status VARCHAR(20) NOT NULL DEFAULT 'claimed'
|
|
||||||
-- unclaimed | pending_transfer | claimed
|
|
||||||
source VARCHAR(20) NOT NULL DEFAULT 'manual' -- irimc | manual
|
|
||||||
source_ref VARCHAR(100) NULL -- شناسه رکورد مبدأ
|
|
||||||
claimed_at INT NULL
|
|
||||||
medical_system_code VARCHAR(25) NULL -- (موجود) + ایندکس یکتای جزئی
|
|
||||||
</code></pre>
|
|
||||||
<p>قیود و ایندکسها:</p>
|
|
||||||
<ul>
|
|
||||||
<li>حذف/تعدیل <code>UniqueConstraint idx_doctors_user</code>: یکتایی فقط باید برای پزشکانِ <strong>دارای کاربر</strong> اعمال شود. چون MariaDB از partial unique index پشتیبانی مستقیم ندارد، یکتایی <code>user_id</code> در سطح اپلیکیشن (هنگام claim) تضمین میشود و ایندکس دیتابیس به <code>INDEX</code> ساده تبدیل میشود.</li>
|
|
||||||
<li>ایندکس یکتای طبیعی: <code>UNIQUE (source, medical_system_code)</code> برای idempotency ایمپورت.</li>
|
|
||||||
<li>ایندکس <code>owner_status</code> برای فیلتر سریع «پزشکان بدونمالک».</li>
|
|
||||||
</ul>
|
|
||||||
<p>سازگاری با دادهی موجود: تمام پزشکان فعلی هنگام migration مقدار <code>owner_status = 'claimed'</code> و <code>source = 'manual'</code> میگیرند تا رفتارشان تغییر نکند.</p>
|
|
||||||
<blockquote>
|
|
||||||
<p>نکته سازگاری: طبق <code>CLAUDE.md</code>، <code>medical_system_code</code> تا الان <code>nullable</code> و بدون یکتایی بوده؛ پیش از افزودن ایندکس یکتا باید دادهی موجود از نظر تکراری بودن پاکسازی شود.</p>
|
|
||||||
</blockquote>
|
|
||||||
<hr />
|
|
||||||
<h2 id="api-clinicpro">۶. طراحی API (بکاند clinicpro)</h2>
|
|
||||||
<p>پاسخها از پوشش <code>BaseController</code> پیروی میکنند: <code>{ success, data }</code> / <code>{ success, errors }</code> / صفحهبندی <code>{ data, meta }</code>. مطابق قانون پروژه، هر تغییر کنترلر باید در <code>docs/api/*</code> هم مستند شود.</p>
|
|
||||||
<h3 id="_9">۶.۱ ایمپورت (داخلی / ادمین)</h3>
|
|
||||||
<p>معمولاً از طریق دستور کنسول انجام میشود؛ در صورت نیاز به تریگر از پنل ادمین:</p>
|
|
||||||
<pre><code>POST /api/v1/admin/doctors/import-irimc [ROLE_ADMIN]
|
|
||||||
body: { source_url?, dry_run?: bool, records: [...] }
|
|
||||||
→ 200 { success, data: { created, updated, skipped, report_url } }
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="_10">۶.۲ فهرست پزشکان بدونمالک</h3>
|
|
||||||
<p>اندپوینت موجود <code>GET /api/v1/doctors</code> با فیلتر جدید <code>owner_status</code> توسعه مییابد تا هم برای پنل ادمین و هم برای صفحهی «تصاحب پروفایل» در Nobat724 قابلاستفاده باشد:</p>
|
|
||||||
<pre><code>GET /api/v1/doctors?owner_status=unclaimed&search=&city_id=&specialty_id=
|
|
||||||
→ 200 { success, data: [...], meta }
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="claim">۶.۳ درخواست تصاحب (Claim) — عمومی و احراز هویتشده</h3>
|
|
||||||
<pre><code>POST /api/v1/doctor/{uuid}/claim [IS_AUTHENTICATED_FULLY]
|
|
||||||
body: { national_code, medical_system_code, activity_time? }
|
|
||||||
قواعد:
|
|
||||||
- پروفایل باید owner_status = 'unclaimed' باشد، وگرنه 409.
|
|
||||||
- medical_system_code ورودی باید با رکورد پزشک مطابقت کند، وگرنه 422.
|
|
||||||
- کاربر لاگینشده (که موبایلش قبلاً با OTP تأیید شده) نباید از قبل پزشکِ دیگری داشته باشد.
|
|
||||||
- رکورد به pending_transfer میرود و یک ClaimRequest ثبت میشود.
|
|
||||||
→ 202 { success, data: { claim_id, status: 'pending_transfer' } }
|
|
||||||
</code></pre>
|
|
||||||
<h3 id="_11">۶.۴ تأیید/رد توسط ادمین و نهاییسازی انتقال</h3>
|
|
||||||
<pre><code>GET /api/v1/admin/doctor-claims?status=pending [ROLE_ADMIN]
|
|
||||||
POST /api/v1/admin/doctor-claims/{claimId}/approve [ROLE_ADMIN]
|
|
||||||
POST /api/v1/admin/doctor-claims/{claimId}/reject [ROLE_ADMIN] { reason }
|
|
||||||
</code></pre>
|
|
||||||
<p>هنگام approve، عملیات انتقال مالکیت (بخش ۷) بهصورت اتمیک اجرا میشود.</p>
|
|
||||||
<blockquote>
|
|
||||||
<p>امکان «انتقال خودکار» (بدون ادمین) نیز قابل تعریف است: اگر <code>national_code</code> کاربر تأییدشده باشد و <code>medical_system_code</code> و نام کاملاً منطبق باشند، سیستم میتواند مستقیماً claim را تأیید کند. تصمیم پیشفرض این سند: <strong>تأیید ادمین اجباری برای فاز اول</strong> (بخش ۱۳).</p>
|
|
||||||
</blockquote>
|
|
||||||
<hr />
|
|
||||||
<h2 id="_12">۷. بخش دوم — مکانیزم انتقال مالکیت</h2>
|
|
||||||
<p>جریان کامل تصاحب پروفایل توسط پزشک واقعی:</p>
|
|
||||||
<ol>
|
|
||||||
<li><strong>کشف:</strong> پزشک در Nobat724 نام خود را میبیند (پروفایل <code>unclaimed</code>) و روی «این پروفایل من است» کلیک میکند.</li>
|
|
||||||
<li><strong>احراز هویت پایه:</strong> اگر لاگین نیست، با موبایل + OTP ثبتنام/ورود میکند. در این مرحله یک <code>User</code> واقعی با موبایل واقعی ساخته میشود (مسیر عادی auth موجود).</li>
|
|
||||||
<li><strong>تطبیق هویت:</strong> فرم تصاحب، <code>national_code</code> و <code>medical_system_code</code> را میگیرد و با رکورد پزشک تطبیق میدهد (<code>POST .../claim</code>). پروفایل به <code>pending_transfer</code> میرود.</li>
|
|
||||||
<li><strong>بازبینی:</strong> ادمین در پنل، درخواست را با <code>profile_url</code> سازمان نظام پزشکی بازبینی و approve/reject میکند.</li>
|
|
||||||
<li><strong>نهاییسازی انتقال (اتمیک):</strong></li>
|
|
||||||
<li><code>Doctor.user</code> = کاربر واقعی پزشک (پر شدن ستونی که تا الان null بود).</li>
|
|
||||||
<li><code>Doctor.managed_by</code> = <code>null</code>؛ <code>owner_status = 'claimed'</code>؛ <code>claimed_at = time()</code>.</li>
|
|
||||||
<li>افزودن <code>ROLE_DOCTOR</code> به کاربر واقعی (همان منطق موجود در <code>DoctorController::create</code>).</li>
|
|
||||||
<li>از این پس ویرایش پروفایل توسط خود پزشک از طریق <code>PATCH /api/v1/doctor/{uuid}</code> مجاز است (چک مالکیت فعلی <code>getUser()->getId() === user</code> اکنون درست کار میکند).</li>
|
|
||||||
<li><strong>اطلاعرسانی:</strong> پیامک/نوتیف تأیید به پزشک (همسبک <code>Sms</code> موجود).</li>
|
|
||||||
</ol>
|
|
||||||
<p>قواعد یکتایی هنگام انتقال: چون یک کاربر واقعی نباید صاحب دو پزشک شود، پیش از اتصال باید بررسی شود که <code>user_id</code> مقصد در جدول <code>doctors</code> تکراری نشود (تضمین در سطح اپلیکیشن، جایگزین قید یکتای حذفشده).</p>
|
|
||||||
<hr />
|
|
||||||
<h2 id="nobat724-nobat724_front">۸. اتصال با Nobat724 (<code>nobat724_front</code>)</h2>
|
|
||||||
<p>مصرفکنندهی API از طریق <code>services/response.js</code> است (که هماکنون <code>getDoctors</code>, <code>postDoctor</code>, ... را دارد). تغییرات لازم:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>نمایش پزشکان بدونمالک:</strong> فهرست فعلی پزشکان (<code>api/v1/doctors</code>) بهطور خودکار پزشکان <code>unclaimed</code> را هم شامل میشود؛ در کارت پزشک، بهجای دکمهی «رزرو نوبت»، دکمهی «این پروفایل من است / تکمیل پروفایل» نمایش داده میشود چون <code>active=false</code> است.</li>
|
|
||||||
<li><strong>صفحه/فرم تصاحب:</strong> فراخوانی <code>POST api/v1/doctor/{uuid}/claim</code> پس از ورود با OTP. نگهداری <code>access_token</code>/<code>refresh_token</code> در کوکی (مطابق الگوی فعلی Nobat724).</li>
|
|
||||||
<li><strong>زبان/تقویم:</strong> تمام رشتههای جدید فارسی و تاریخها جلالی (شمسی) بمانند.</li>
|
|
||||||
<li>رعایت قرارداد: تغییر قالب پاسخ در بکاند باید در <code>nobat724_front/services/response.js</code> هم منعکس شود، چون در زمان build خطا نمیدهد.</li>
|
|
||||||
</ul>
|
|
||||||
<hr />
|
|
||||||
<h2 id="clinic-pro-tauri">۹. اثر بر <code>clinic-pro-tauri</code></h2>
|
|
||||||
<p>اپ دسکتاپ نیز کلاینت همان API است (<code>src/service/response.js</code>) و از CASL برای نقشها استفاده میکند (<code>clinic</code>, <code>doctor</code>, <code>clinic_doctor</code>, <code>secretary</code>).</p>
|
|
||||||
<ul>
|
|
||||||
<li>اگر لیست پزشکان در اپ نمایش داده میشود، باید فیلد <code>owner_status</code> و رفتار <code>active=false</code> را مدیریت کند (پزشک بدونمالک قابل رزرو آنلاین نیست).</li>
|
|
||||||
<li>مرز sync آفلاین/آنلاین این اپ هنوز کامل نگاشت نشده؛ پیش از فرض همترازی، رفتار <code>owner_status</code> در دیتابیس محلی SQLite باید بررسی شود (طبق هشدار <code>AGENTS.md</code>).</li>
|
|
||||||
<li>برای فاز اول، تغییر در Tauri <strong>اختیاری</strong> است؛ فقط در صورتی که این اپ پزشکان <code>unclaimed</code> را نشان دهد لازم میشود.</li>
|
|
||||||
</ul>
|
|
||||||
<hr />
|
|
||||||
<h2 id="_13">۱۰. جریان کاربری (خلاصهی گامبهگام)</h2>
|
|
||||||
<pre><code>[نظام پزشکی JSON] → دستور ایمپورت → پزشکِ unclaimed (managed_by = System Owner)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
نمایش در Nobat724 (active=false، بدون نوبت آنلاین)
|
|
||||||
│ پزشک واقعی: «این پروفایل من است»
|
|
||||||
▼
|
|
||||||
ورود با موبایل + OTP → ساخت User واقعی
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
فرم تصاحب (کد ملی + کد نظام پزشکی) → POST /claim → pending_transfer
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
بازبینی ادمین (approve) → انتقال اتمیک:
|
|
||||||
user_id=واقعی، owner_status=claimed، +ROLE_DOCTOR
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
پزشک پروفایل و برنامهی کاری را کامل میکند → active=true → نوبتدهی آنلاین فعال
|
|
||||||
</code></pre>
|
|
||||||
<hr />
|
|
||||||
<h2 id="_14">۱۱. حالات مرزی و قواعد کسبوکار</h2>
|
|
||||||
<ul>
|
|
||||||
<li><strong>درخواست تصاحب همزمان دو نفر برای یک پروفایل:</strong> فقط اولین <code>pending_transfer</code> پذیرفته میشود؛ بقیه با ۴۰۹ رد میشوند تا تعیین تکلیف قبلی روشن شود.</li>
|
|
||||||
<li><strong>کاربری که قبلاً پزشک دارد:</strong> نمیتواند پروفایل دوم را تصاحب کند (قید یکتای منطقی <code>user_id</code>).</li>
|
|
||||||
<li><strong>عدم تطابق کد نظام پزشکی:</strong> رد با ۴۲۲ و بدون تغییر وضعیت.</li>
|
|
||||||
<li><strong>رد توسط ادمین:</strong> پروفایل به <code>unclaimed</code> بازمیگردد و برای تصاحب مجدد آزاد میشود.</li>
|
|
||||||
<li><strong>حذف پزشک بدونمالک:</strong> مجاز برای ادمین (مسیر فعلی <code>DELETE</code>); اما پزشکِ <code>claimed</code> طبق رفتار فعلی محافظت میشود.</li>
|
|
||||||
<li><strong>ایمپورت مجدد یک پزشکِ از قبل claimed:</strong> فیلدهای هویتی بهروز نمیشوند (مالک واقعی اولویت دارد)؛ فقط در گزارش «skipped/claimed» ثبت میشود.</li>
|
|
||||||
<li><strong>نوبتدهی:</strong> تا زمانی که پروفایل <code>unclaimed</code> است، <code>active_doctor_appointment=false</code> و برنامهی کاری وجود ندارد؛ لذا در <code>toListArray</code> مقدار <code>active=false</code> میشود و رزرو ممکن نیست.</li>
|
|
||||||
</ul>
|
|
||||||
<hr />
|
|
||||||
<h2 id="_15">۱۲. مراحل پیادهسازی (بهترتیب و بهتفکیک ریپو)</h2>
|
|
||||||
<p>مطابق <code>CLAUDE.md</code>: ابتدا بکاند <code>clinicpro</code>، سپس مستندسازی API، سپس کلاینتها.</p>
|
|
||||||
<p><strong>الف) <code>clinicpro</code> (بکاند):</strong>
|
|
||||||
1. افزودن فیلدهای <code>managed_by</code>, <code>owner_status</code>, <code>source</code>, <code>source_ref</code>, <code>claimed_at</code> و nullable کردن <code>user_id</code> در <code>Doctor</code> + migration در <code>migrations/</code>.
|
|
||||||
2. پاکسازی داده و افزودن ایندکس یکتای <code>(source, medical_system_code)</code>.
|
|
||||||
3. دستور کنسول <code>app:doctors:import-irimc</code> (با <code>--dry-run</code>، گزارش، تراکنش).
|
|
||||||
4. دستور/سیدر ساخت کاربر «مالک سیستمی».
|
|
||||||
5. موجودیت/جدول <code>DoctorClaim</code> + اندپوینتهای claim و approve/reject.
|
|
||||||
6. توسعهی فیلتر <code>owner_status</code> در <code>GET /api/v1/doctors</code> و بهروزرسانی چکهای مالکیت.
|
|
||||||
7. بهروزرسانی <code>docs/api/*</code> (طبق قانون استاندارد پروژه) و افزودن این سند به مستندات.</p>
|
|
||||||
<p><strong>ب) <code>nobat724_front</code>:</strong>
|
|
||||||
8. همترازی <code>services/response.js</code> با قالبهای جدید.
|
|
||||||
9. دکمهی «این پروفایل من است» روی کارت پزشکِ <code>unclaimed</code> + صفحهی فرم تصاحب (فارسی، جلالی، RTL).</p>
|
|
||||||
<p><strong>ج) <code>clinic-pro-tauri</code> (در صورت نیاز):</strong>
|
|
||||||
10. مدیریت <code>owner_status</code>/<code>active=false</code> در لیست پزشکان و بررسی مرز sync محلی.</p>
|
|
||||||
<p><strong>د) بازبینی نهایی:</strong>
|
|
||||||
11. تست ایمپورت روی نمونهی ۱۶۰ رکورد، تست جریان claim سرتاسری، و بازسازی کاربران تست (<code>ddev exec php create_test_users.php</code>).</p>
|
|
||||||
<hr />
|
|
||||||
<h2 id="_16">۱۳. تصمیمات باز و ریسکها</h2>
|
|
||||||
<ul>
|
|
||||||
<li><strong>گزینه A در برابر B:</strong> این سند گزینه A (nullable کردن <code>user_id</code> + <code>managed_by</code>) را توصیه میکند چون جدول <code>users</code> را با کاربران جعلی آلوده نمیکند و مدل مالکیت را صریح میسازد. هزینهاش: از دست رفتن قید یکتای دیتابیسی روی <code>user_id</code> و انتقال آن به سطح اپلیکیشن.</li>
|
|
||||||
<li><strong>تأیید ادمین در برابر انتقال خودکار:</strong> پیشفرض فاز اول تأیید دستی ادمین است (امنتر برای هویت پزشک). خودکارسازی بعداً با اتکا به تأیید کد ملی افزوده میشود.</li>
|
|
||||||
<li><strong>کیفیت دادهی نظام پزشکی:</strong> برخی رکوردها ممکن است فاقد <code>specialty_id</code>/<code>city_id</code> معتبر باشند؛ گزارش ایمپورت باید اینها را شفاف کند.</li>
|
|
||||||
<li><strong>حریم خصوصی:</strong> نمایش عمومی نام و کد نظام پزشکی پیش از رضایت پزشک، ملاحظهی حقوقی دارد و باید با سیاست پلتفرم بررسی شود.</li>
|
|
||||||
<li><strong>یکتایی موبایل مالک سیستمی:</strong> مقدار رزروشده باید تضمیناً هرگز با موبایل واقعی کاربر تداخل نکند.</li>
|
|
||||||
</ul>
|
|
||||||
<hr />
|
|
||||||
<h2 id="_17">۱۴. مرجع نمونهی داده</h2>
|
|
||||||
<p>نمونهی یک رکورد ورودی از <code>doctors.json</code> (۱۶۰ رکورد، همگی <code>mobileNumber: null</code>):</p>
|
|
||||||
<pre><code class="language-json">{
|
|
||||||
"name": "دکتر فرخنده حسینی",
|
|
||||||
"gender": "woman",
|
|
||||||
"medicalSystemCode": "145657",
|
|
||||||
"mobileNumber": null,
|
|
||||||
"degree": "general",
|
|
||||||
"info": "دکترای حرفهای پزشکی",
|
|
||||||
"specialty_id": 1,
|
|
||||||
"specialty_name": "پزشک عمومی",
|
|
||||||
"state_id": 23,
|
|
||||||
"state_name": "کهگیلویه و بویراحمد",
|
|
||||||
"city_id": 123,
|
|
||||||
"city_name": "یاسوج",
|
|
||||||
"profile_url": "https://membersearch.irimc.org/member/profile?id=02058131-...",
|
|
||||||
"source_url": "https://membersearch.irimc.org"
|
|
||||||
}
|
|
||||||
</code></pre>
|
|
||||||
</div></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user