Files
clinicpro/.claude/prompt/doctors-list-city-and-limit.md
T
hamedandClaude Opus 4.8 e78b0c4b4c chore(prompt): split SEO audit follow-up into backend task prompts
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>
2026-07-19 08:09:00 +03:30

109 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>