feat(doctor): expose city/state in public doctor list
The public doctor list had no location field, so multi-domain consumers could not tell which city domain owns a doctor. nobat724_front's sitemap worked around this by fetching the list once per city (35 sweeps) and subtracting, costing ~13s to build the root sitemap. Location is resolved in bulk by DoctorRepository::findLocationsByDoctors using the same rule the city_id/state_id filter applies: the doctor's own address first, falling back to the address of a clinic they belong to. Without the clinic fallback a doctor could match city_id=X yet report no city, which would break the sitemap's per-domain partitioning. city/state are arrays with at most one entry, matching the shape already used by the doctor detail response and the clinic list. A doctor with no address reports [] rather than null. Multi-location doctors get a single primary city, mirroring the canonical rule on the public site. Also surface the applied page size as meta.limit. Repositories silently clamp limit to 50, which previously made clients believe pagination had ended early — this is what truncated the sitemap to 50 doctors. The clinic doctor-list endpoint gets the same location data so both endpoints agree. Location resolution costs at most 2 queries regardless of page size, asserted directly against the repository rather than through the endpoint, since the endpoint carries a pre-existing specialties N+1 in findWithFilters that is unrelated to this change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+3
-1
@@ -31,12 +31,14 @@ Authorization: Bearer <JWT_TOKEN>
|
||||
{ "success": true, "data": { ... } }
|
||||
|
||||
// Paginated
|
||||
{ "success": true, "data": [...], "meta": { "totalRecords": 100, "totalPages": 5, "currentPage": 1 } }
|
||||
{ "success": true, "data": [...], "meta": { "totalRecords": 100, "totalPages": 5, "currentPage": 1, "limit": 20 } }
|
||||
|
||||
// Error
|
||||
{ "success": false, "data": null, "errors": [{ "code": "ERR_XXX_000", "message": "..." }] }
|
||||
```
|
||||
|
||||
> `meta.limit` اندازهٔ صفحهٔ **واقعاً اعمالشده** است. ریپازیتوریها `limit` درخواستی را به سقف خودشان کاهش میدهند (مثلاً لیست پزشکان: سقف ۵۰)، پس برای پیمایش کامل به `meta.totalPages` تکیه کن — نه به این فرض که «تعداد آیتم کمتر از limit درخواستی یعنی صفحهٔ آخر».
|
||||
|
||||
---
|
||||
|
||||
## Persian digit normalization (global)
|
||||
|
||||
+8
-1
@@ -289,7 +289,13 @@ Get doctors associated with a clinic.
|
||||
"point": "4.8",
|
||||
"free_turn": "پنجشنبه 09:00–13:00",
|
||||
"hours_of_work": "شنبه تا چهارشنبه | پنجشنبه",
|
||||
"active": true
|
||||
"active": true,
|
||||
"city": [
|
||||
{ "uuid": "7bfb989e-...", "id": "123", "name": "یاسوج", "parent": "23" }
|
||||
],
|
||||
"state": [
|
||||
{ "uuid": "7bfb5705-...", "id": "23", "name": "کهگیلویه و بویراحمد" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": { "totalRecords": 3, "totalPages": 1, "currentPage": 1 }
|
||||
@@ -302,6 +308,7 @@ Get doctors associated with a clinic.
|
||||
| `free_turn` | string | Next available appointment (e.g. `پنجشنبه 09:00–13:00`), or `نوبت آزادی موجود نیست` if the doctor has no active weekly schedule |
|
||||
| `hours_of_work` | string | Working-days summary, or `برنامه کاری تنظیم نشده` when unscheduled |
|
||||
| `active` | boolean | `true` only when appointments are enabled **and** the doctor has an active schedule |
|
||||
| `city` / `state` | array | مکان خودِ پزشک (آدرس شخصی، و در نبودش آدرس کلینیک). آرایه با حداکثر یک عضو؛ پزشک بدون آدرس `[]`. جزئیات و قاعدهٔ انتخاب در [doctor.md](doctor.md#city--state-در-پاسخ-لیست) |
|
||||
|
||||
> `free_turn`/`hours_of_work`/`active` are computed from each doctor's `WeeklySchedule` (loaded in bulk by the endpoint). Without a schedule they fall back to the "not set" values.
|
||||
|
||||
|
||||
+22
-3
@@ -204,7 +204,7 @@ List doctors with pagination and filters.
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `page` | integer | ❌ | Default: 1 |
|
||||
| `limit` | integer | ❌ | Default: 20 |
|
||||
| `limit` | integer | ❌ | Default: 10. **حداکثر ۵۰** — مقادیر بزرگتر بیصدا به ۵۰ کاهش مییابند. مقدار واقعاً اعمالشده در `meta.limit` برمیگردد؛ برای پیمایش کامل به `meta.totalPages` تکیه کن، نه به «تعداد آیتم کمتر از limit درخواستی» |
|
||||
| `search` | string | ❌ | Search in title |
|
||||
| `specialty_id` | integer | ❌ | Filter by specialty ID |
|
||||
| `city_id` | integer | ❌ | Filter by city ID — شامل دکترهایی که آدرس شخصیشان (`doctor_addresses.city_id`, با `doctor_id` مقداردار) در آن شهر است یا از طریق کلینیکی که آدرس آن در آن شهر است (`doctor_addresses.clinic_id`) |
|
||||
@@ -228,19 +228,38 @@ List doctors with pagination and filters.
|
||||
"free_turn": "دوشنبه 09:00–13:00",
|
||||
"hours_of_work": "شنبه: 09:00–13:00 و 14:00–18:00 | یکشنبه: 09:00–13:00",
|
||||
"active": true,
|
||||
"owner_status": "claimed"
|
||||
"owner_status": "claimed",
|
||||
"city": [
|
||||
{ "uuid": "7bfb989e-...", "id": "123", "name": "یاسوج", "parent": "23" }
|
||||
],
|
||||
"state": [
|
||||
{ "uuid": "7bfb5705-...", "id": "23", "name": "کهگیلویه و بویراحمد" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"totalRecords": 50,
|
||||
"totalPages": 3,
|
||||
"currentPage": 1
|
||||
"currentPage": 1,
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> ℹ️ `point` و `satisfaction` فقط برای `owner_status="claimed"` مقدار دارند؛ برای `unclaimed`/`pending_transfer` هر دو `null` هستند.
|
||||
|
||||
### `city` / `state` در پاسخ لیست
|
||||
|
||||
آرایه با حداکثر یک عضو — همشکل با `city`/`state` در پاسخ جزئیات پزشک و پاسخ لیست کلینیکها.
|
||||
|
||||
- منبع مکان **دقیقاً همان قاعدهای است که فیلتر `city_id`/`state_id` اعمال میکند**: اول آدرس شخصی پزشک (`doctor_addresses` با `doctor_id` مقداردار)، و اگر نداشت آدرس کلینیکی که عضو آن است (`doctor_addresses` با `clinic_id` مقداردار و `doctor_id` تهی). یعنی هر پزشکی که با `city_id=X` برگردد، در پاسخ هم همان شهر را اعلام میکند.
|
||||
- پزشک چند-مطبی **یک شهر اصلی** میگیرد (اولین مکان یافتشده) — نه فهرست همهٔ شهرها.
|
||||
- پزشک بدون هیچ آدرس: `"city": []` و `"state": []` (آرایهٔ خالی، نه `null`).
|
||||
- `city[].parent` شناسهٔ استان است.
|
||||
- استخراج مکان دستهای انجام میشود (`DoctorRepository::findLocationsByDoctors`) — حداکثر دو کوئری ثابت، مستقل از تعداد پزشکان در صفحه.
|
||||
|
||||
> 🔗 مصرفکننده: `nobat724_front/app/sitemap.js` با این فیلد تشخیص میدهد هر پزشک به کدام دامنهٔ شهری تعلق دارد (canonical). تغییر شکل این فیلد قرارداد آن را میشکند.
|
||||
|
||||
---
|
||||
|
||||
## PATCH `/api/v1/doctor/{uuid}`
|
||||
|
||||
Reference in New Issue
Block a user