The nobat724_front SEO audit produced three backend-side tasks that the frontend work is blocked on or that it only masks: - doctors-list-city-and-limit: implemented in the previous commit - blog-city-field: blog has no city column, so the city-scoped blog work on the public site is written but inert - cleanup-polluted-doctor-clinic-records: records named after a phone number or "test" reach public results; the site currently hides them with a noindex filter, which masks rather than fixes the data Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
6.6 KiB
Markdown
109 lines
6.6 KiB
Markdown
<div dir="rtl" markdown="1">
|
||
|
||
# افزودن شهر به لیست پزشکان + رفع سقف خاموش limit
|
||
|
||
## پروژه
|
||
|
||
`clinicpro` (backend)
|
||
|
||
**cross-repo:** خروجی این endpoint را `nobat724_front/app/sitemap.js` مصرف میکند.
|
||
پرامپت همتا (بعد از این اجرا شود): `nobat724_front/.claude/prompt/sitemap-simplify-with-city.md`
|
||
|
||
## زمینه
|
||
|
||
در ممیزی SEO سایت عمومی دو محدودیت این endpoint باعث دو باگ در sitemap شد:
|
||
|
||
۱. **پاسخ لیست پزشکان فیلد شهر ندارد.** سایت عمومی چند-دامنهای است (۳۵ دامنهٔ شهری) و باید بداند هر پزشک به کدام دامنه تعلق دارد. چون شهر در پاسخ نیست، sitemap دامنهٔ اصلی مجبور است **۳۵ بار جداگانه** لیست را با `city_id` بگیرد و از کل کم کند تا بفهمد کدام پزشک شهر اختصاصی ندارد. تولید sitemap ریشه ~۱۳ ثانیه طول میکشد.
|
||
|
||
۲. **پارامتر `limit` بیصدا به ۵۰ سقف میخورد.** کلاینت `limit=500` میفرستد، پاسخ ۵۰ رکورد است و هیچ نشانهای از سقفخوردن در پاسخ نیست. این باعث شد sitemap ماهها روی ۵۰ پزشک بریده بماند (حلقهٔ صفحهبندی وقتی `items.length < limit` بود متوقف میشد). سمت فرانت با تکیه بر `meta.totalPages` رفع شد، ولی رفتار خاموشِ API همچنان تله است.
|
||
|
||
## فایلهای مرتبط
|
||
|
||
| فایل | نقش |
|
||
|------|-----|
|
||
| `clinicpro/src/Doctor/Entity/Doctor.php` | `toListArray()` — شکل پاسخ لیست |
|
||
| `clinicpro/src/Doctor/Repository/DoctorRepository.php` | سقف `limit` در خطوط ۴۵ و ۱۴۲ |
|
||
| `clinicpro/src/Doctor/Controller/DoctorController.php` | route `/api/v1/doctors` خط ۲۵۲ |
|
||
| `clinicpro/docs/api/doctor.md` | مستندات — الزاماً بهروز شود |
|
||
|
||
## وضعیت فعلی
|
||
|
||
`src/Doctor/Entity/Doctor.php:540` — بدون شهر:
|
||
|
||
```php
|
||
public function toListArray(array $schedules = []): array
|
||
{
|
||
$sf = $this->computeScheduleFields($schedules);
|
||
return [
|
||
'id' => (string) $this->id,
|
||
'uuid' => $this->uuid,
|
||
'name' => $this->name,
|
||
'gender' => $this->gender,
|
||
'degree' => $this->degree,
|
||
'img' => $this->images ?? [],
|
||
'specialties' => array_map(fn(Specialty $s) => [...], $this->specialties->toArray()),
|
||
'satisfaction' => $this->hasPublicRating() ? (string) $this->doctorRatePercentage : null,
|
||
'point' => $this->hasPublicRating() ? (string) $this->doctorRate : null,
|
||
'free_turn' => $sf['free_turn'],
|
||
'hours_of_work' => $sf['hours_of_work'],
|
||
'active' => $this->activeDoctorAppointment && $sf['has_schedule'],
|
||
'owner_status' => $this->ownerStatus,
|
||
];
|
||
}
|
||
```
|
||
|
||
`src/Doctor/Repository/DoctorRepository.php:45` و `:142` — سقف خاموش:
|
||
|
||
```php
|
||
$limit = min(50, max(1, (int) ($filters['limit'] ?? 10)));
|
||
```
|
||
|
||
> نکته: در پاسخ **جزئیات** پزشک (`toDetailArray`) شهر داخل `address[].city` هست، ولی `city` و `state` سطحبالا آرایهٔ خالی برمیگردند. سایت عمومی برای همین از `address[].city.id` استخراج میکند (`nobat724_front/lib/domainHelpers.js` → `extractEntityCityId`). این پرامپت آن رفتار را تغییر نمیدهد.
|
||
|
||
## وظایف
|
||
|
||
### ۱. افزودن شهر به `toListArray()`
|
||
|
||
شهرِ پزشک از آدرسهایش میآید. شهر **اولین آدرس** (یا آدرس اصلی، اگر مفهوم آدرس اصلی وجود دارد) بهعنوان شهر پزشک برگردد — چون سایت عمومی هم برای canonical دقیقاً همین قاعده («یک شهر اصلی برای پزشک چند-شهری») را اعمال میکند.
|
||
|
||
```php
|
||
'city' => $primaryAddress?->getCity() ? [
|
||
'id' => (string) $primaryAddress->getCity()->getId(),
|
||
'name' => $primaryAddress->getCity()->getName(),
|
||
] : null,
|
||
'state' => $primaryAddress?->getProvince() ? [
|
||
'id' => (string) $primaryAddress->getProvince()->getId(),
|
||
'name' => $primaryAddress->getProvince()->getName(),
|
||
] : null,
|
||
```
|
||
|
||
- شکل `{ id, name }` باشد تا با `city` در پاسخ لیست کلینیکها یکسان باشد (آنجا آرایهای از همین شکل است).
|
||
- `id` رشته باشد — همراستا با بقیهٔ فیلدهای این متد.
|
||
- پزشک بدون آدرس → `null` (نه آرایهٔ خالی، تا با «شهر ندارد» تفکیکپذیر بماند).
|
||
- **N+1 نساز:** آدرس/شهر/استان در همان کوئری `findWithFilters` با `JOIN`/`addSelect` بارگذاری شود، نه lazy per-doctor.
|
||
|
||
### ۲. شفافکردن سقف `limit`
|
||
|
||
سقف ۵۰ حفظ شود (محافظت از دیتابیس)، ولی دیگر خاموش نباشد:
|
||
|
||
- مقدار مؤثر `limit` در `meta` برگردد (اگر الان برنمیگردد) تا کلاینت بفهمد درخواستش کوتاه شده.
|
||
- در `docs/api/doctor.md` صریح نوشته شود: «`limit` حداکثر ۵۰؛ مقادیر بزرگتر بیصدا به ۵۰ کاهش مییابند».
|
||
|
||
اگر تصمیم گرفتی سقف را برای مصرفکنندهٔ sitemap بالاتر ببری، آن را بهصورت یک حد جداگانه و مستند انجام بده — نه با حذف `min()`.
|
||
|
||
### ۳. بهروزرسانی مستندات
|
||
|
||
`clinicpro/docs/api/doctor.md` برای `GET /api/v1/doctors`:
|
||
- فیلدهای جدید `city` و `state` با مثال واقعی JSON
|
||
- رفتار و سقف `limit`
|
||
|
||
## نکات مهم
|
||
|
||
- `toListArray()` را مصرفکنندگان دیگری هم دارند (پنل ادمین، اپ Tauri). **فیلد اضافه میکنیم، فیلد موجود را تغییر نام یا حذف نمیکنیم** — تغییر افزایشی و backward-compatible باشد.
|
||
- بعد از تغییر، پاسخ واقعی را با یک پزشک دارای آدرس تست کن:
|
||
`curl -s "https://clinic-pro.ir/api/v1/doctors?page=1&limit=5" | jq '.data.data[0] | {name, city, state}'`
|
||
- پزشک `beaca548-816f-4613-937d-360db01bd7c8` شهرش یاسوج (`city_id: 123`) است — نمونهٔ خوبی برای تأیید.
|
||
- Entity تغییر نمیکند (فقط متد سریالسازی) ⇒ migration لازم نیست.
|
||
|
||
</div>
|