Files
clinicpro/.claude/prompt/fix-server-error-logs-20260711.md
T
hamed 9f56f4aa08 feat(migrations): add ownership fields to doctors table for IRIMC import
- Introduced new columns: owner_status, source, source_ref, managed_by, and claimed_at to the doctors table.
- Created indexes for owner_status and source to optimize queries related to unclaimed doctors.

feat(auth): implement SystemOwnerCommand for managing system-owner user

- Added command to create, activate, and deactivate a system-owner user for IRIMC crawler.
- Ensured the user has ROLE_ADMIN to access import endpoints.
- Handled password setting and user status management within the command.
2026-07-11 08:59:36 +03:30

13 KiB
Raw Blame History

رفع خطاهای لاگ سرور (production) — ۱۴۰۵/۰۴/۲۰

پروژه

clinicpro (backend فقط)

زمینه

لاگ ارور production (فایل logs-20260711-083844.csv) بررسی شد. پنج دسته خطا شناسایی شد. بیشترشان error سطح ۵۰۰ هستند که باید یا رفع شوند یا از سطح error خارج شوند تا لاگ کثیف نشود. ریشهٔ هر کدام در کد پیدا شده و در ادامه با راه‌حل دقیق آمده است.

خلاصهٔ خطاها و اولویت

# خطا تعداد در لاگ ریشه نوع
۱ SMS sendTemplate failed (kavenegar): HTTP 431 Request Header Fields Too Large ۲ توکن دعوت ۹۶ کاراکتری + URL کامل به‌عنوان توکن کاوه‌نگار باگ کد
۲ Payment initiate failed (mellat): Class "SoapClient" not found ۱ ایمیج prod قدیمی؛ افزونهٔ soap نصب نیست deploy + گارد کد
۳ ForeignKeyConstraintViolationException هنگام حذف پزشک ۱ حذف پزشک بدون بررسی نوبت‌های وابسته → ۵۰۰ باگ کد
۴ MethodNotAllowedHttpException: POST/OPTIONS https://clinic-pro.ir/ ~۳۰ ربات/اسکنر روی /؛ در fallback عمومی error+۵۰۰ لاگ می‌شود نویز لاگ
۵ SMS Idle timeout / login_failed چند گذرا/عادی بدون اقدام

خطای ۵ اقدام لازم ندارد (idle timeout قبلاً retry دارد؛ login_failed warning عادی است).


فایل‌های مرتبط

فایل نقش
src/ClinicInvitation/Entity/ClinicDoctorInvitation.php تولید توکن دعوت (خط ۷۷ و ۹۷: bin2hex(random_bytes(48)))
src/ClinicInvitation/Service/ClinicInvitationService.php ساخت لینک و ارسال پیامک دعوت (خط ۱۱۳، sendSms)
src/Sms/Entity/SmsMessageTemplate.php token_map قالب‌ها؛ CLINIC_INVITATION'link' => 'token'
src/Sms/Provider/KavehNegarProvider.php ساخت GET به کاوه‌نگار؛ محدودیت slotها
src/Payment/Gateway/MellatGateway.php خط ۷۰: new \SoapClient(...)
src/Doctor/Controller/DoctorController.php خط ۳۶۹–۳۸۱: متد delete
src/Shared/EventSubscriber/ExceptionSubscriber.php fallback عمومی که همه‌چیز را error+۵۰۰ می‌کند
Dockerfile خط ۶۷: docker-php-ext-install ... soap (از قبل هست)

وظیفه ۱ — رفع 431 پیامک دعوت (و welcome)

ریشه

توکن دعوت این‌گونه تولید می‌شود:

// ClinicDoctorInvitation.php:77 و :97
$this->token = bin2hex(random_bytes(48));   // ۹۶ کاراکتر hex

سپس لینک کامل ساخته و به‌عنوان توکن کاوه‌نگار فرستاده می‌شود:

// ClinicInvitationService.php:113
$link = rtrim($this->appUrl, '/') . '/clinic-invitation/' . $inv->getToken();
// link ≈ https://clinic-pro.ir/clinic-invitation/<۹۶ کاراکتر> ≈ ۱۳۶ کاراکتر

و در token_map، link روی slot token می‌نشیند:

// SmsMessageTemplate.php — TAG_CLINIC_INVITATION
'token_map' => ['clinic' => 'token10', 'link' => 'token'],

دو مشکل:

  1. کل URL (۱۳۶ کاراکتر) در query stringِ GET کاوه‌نگار → طول request line از حد edge کاوه‌نگار رد می‌شود → 431 Request Header Fields Too Large.
  2. slot token/token2/token3 کاراکترهای خاص (/, :) و فاصله را رد می‌کند؛ URL پر از / است. باید slot token10/token20 باشد (طبق کامنت خود provider).

راه‌حل

الف) توکن دعوت را کوتاه کن (هم برای SMS، هم index دیتابیس سبک‌تر). ۹۶ کاراکتر بیش از حد است؛ random_bytes(16) → ۳۲ hex کافیِ امن است:

// ClinicDoctorInvitation.php — خط ۷۷ و ۹۷ (هر دو محل)
$this->token = bin2hex(random_bytes(16));   // ۳۲ کاراکتر، همچنان کریپتوگرافیک امن

ب) slot لینک را به token10 منتقل کن (فاصله/URL را می‌پذیرد):

// SmsMessageTemplate.php — TAG_CLINIC_INVITATION
'token_map' => ['clinic' => 'token20', 'link' => 'token10'],

نکته مهم: کاوه‌نگار قالب‌های verify/lookup را از پیش تأیید می‌کند. اگر متن قالب تأییدشدهٔ clinicpro-clinic-invite به %token%/%token10% خاصی بسته شده، تغییر slot باید با پنل کاوه‌نگار هماهنگ شود. اول بررسی کن قالب فعلی کدام tokenها را انتظار دارد؛ اگر تغییر slot در پنل ممکن نیست، حداقل وظیفهٔ (الف) — کوتاه‌کردن توکن — را انجام بده که به‌تنهایی طول را از آستانهٔ 431 پایین می‌آورد.

ج) بررسی welcome (خطای ۹۴۹۴ روی clinicpro-welcome، token_map: ['name' => 'token', 'site' => 'token10']): اگر name می‌تواند طولانی/دارای فاصله باشد، آن هم باید token10 شود. متن قالب welcome در پنل کاوه‌نگار را چک کن و در صورت نیاز name را به slot طولانی‌تر ببر.

edge cases

  • توکن‌های دعوتِ قبلی (۹۶ کاراکتری) در دیتابیس باقی می‌مانند؛ تغییر فقط روی دعوت‌های جدید اثر دارد — نیازی به migration نیست (طول ستون token محدودیت‌شکن نمی‌شود؛ فقط index).
  • مطمئن شو random_bytes(16) هنوز به‌اندازهٔ کافی یکتاست (هست — ۱۲۸ بیت).

وظیفه ۲ — رفع SoapClient not found (درگاه ملت)

ریشه

Dockerfile:67 از قبل soap را نصب می‌کند:

&& docker-php-ext-install pdo_mysql intl opcache soap \

پس کد درست است؛ ایمیج در حال اجرا در production قدیمی است (قبل از افزوده‌شدن soap ساخته شده) یا build ناموفق بوده. php -m | grep soap روی کانتینر prod خالی است.

راه‌حل

الف) redeploy با rebuild کامل (بدون کش) — قدم اصلی. بعد از deploy تأیید:

# روی کانتینر prod
php -m | grep -i soap    # باید soap چاپ شود

ب) گارد نرم در کد تا اگر باز هم soap نبود، پرداخت خطای فارسی تمیز بدهد نه Error: Class "SoapClient" not found با ۵۰۰:

// MellatGateway.php — داخل soap() قبل از new \SoapClient(...)
private function soap(): \SoapClient
{
    if (!class_exists(\SoapClient::class)) {
        throw new \App\Shared\Exception\AppException(
            'ERR_PAYMENT_GATEWAY_001',
            'درگاه پرداخت موقتاً در دسترس نیست. لطفاً بعداً تلاش کنید',
            503
        );
    }
    if ($this->soap === null) {
        $this->soap = new \SoapClient($this->wsdlUrl(), [ /* ... بدون تغییر ... */ ]);
    }
    return $this->soap;
}

کد ارور ERR_PAYMENT_GATEWAY_001 را با الگوی موجود ErrorCodes/AppException هماهنگ کن؛ اگر ثابت مشابهی برای درگاه هست از همان استفاده کن.


وظیفه ۳ — رفع ۵۰۰ هنگام حذف پزشکِ دارای نوبت

ریشه

// DoctorController.php:369-381
#[Route('/api/v1/doctor/{uuid}', methods: ['DELETE'])]
public function delete(string $uuid): JsonResponse
{
    $doctor = $this->doctorRepo->findByUuid($uuid);
    if ($doctor === null) { return $this->error(...404); }

    $this->insuranceCleanup->purgeForEntity(TenantInsurance::TYPE_DOCTOR, $doctor->getId());
    $this->doctorRepo->remove($doctor);   // ← اگر appointment وابسته باشد: FK 1451 → ۵۰۰
    return $this->success(['message' => 'دکتر با موفقیت حذف شد']);
}

appointments.doctor_id FK دارد؛ حذف پزشکِ دارای نوبت → ForeignKeyConstraintViolationException → fallback عمومی ۵۰۰.

راه‌حل

قبل از remove تعداد نوبت‌های پزشک را بررسی کن و خطای ۴۰۹ فارسی بده:

// تزریق AppointmentRepository در constructor (اگر نیست)
$appointmentCount = $this->appointmentRepo->count(['doctor' => $doctor]);
if ($appointmentCount > 0) {
    return $this->error(
        ErrorCodes::ERR_VALIDATION_002,   // یا کد conflict مناسب
        'این پزشک نوبت ثبت‌شده دارد و قابل حذف نیست. ابتدا نوبت‌ها را مدیریت کنید',
        409
    );
}

بررسی کن: constructor فعلی DoctorController (خط ۳۳) چه repositoryهایی تزریق می‌کند؛ اگر AppointmentRepository نیست اضافه کن. نام دقیق فیلد رابطه در Appointment (doctor) را از entity تأیید کن.

جایگزین: اگر منطق محصول اجازه می‌دهد، به‌جای بلاک‌کردن، حذف را به soft-delete تبدیل کن — ولی راه‌حل بالا (۴۰۹) کمترین ریسک است. تصمیم را با الگوی موجود بقیهٔ deleteها (مثل deleteAddress) هماهنگ کن.


وظیفه ۴ — کاهش نویز لاگِ MethodNotAllowedHttpException روی /

ریشه

حدود ۳۰ خطا از نوع POST/OPTIONS روی https://clinic-pro.ir/ (ربات/اسکنر و preflight). این‌ها در ExceptionSubscriber به هیچ‌کدام از شاخه‌های خاص نمی‌خورند و به fallback عمومی می‌رسند که:

// ExceptionSubscriber.php — انتهای onKernelException
$this->logger->error(sprintf('Unhandled exception: %s ...'));   // ← error سطح ۵۰۰
$event->setResponse(new JsonResponse([...'ERR_INTERNAL_001'...], 500));  // ← اشتباه: باید ۴۰۵

یعنی خطای کلاینت ۴۰۵ به‌اشتباه به‌عنوان error داخلی ۵۰۰ لاگ و پاسخ داده می‌شود.

راه‌حل

یک شاخهٔ اختصاصی برای MethodNotAllowedHttpException قبل از fallback اضافه کن — پاسخ ۴۰۵ و لاگ در سطح notice (نه error):

use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;

// قبل از بلاک fallback عمومی
if ($exception instanceof MethodNotAllowedHttpException) {
    $this->logger->notice('Method not allowed', [
        'path'   => $event->getRequest()->getPathInfo(),
        'method' => $event->getRequest()->getMethod(),
    ]);
    $event->setResponse(new JsonResponse(
        ['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_METHOD_NOT_ALLOWED_001', 'message' => 'متد درخواستی مجاز نیست']]],
        405
    ));
    return;
}

نکته دربارهٔ OPTIONS

بعضی خطاها OPTIONS https://clinic-pro.ir/ هستند = preflight CORS. اگر کلاینتی واقعاً به ریشهٔ دامنه preflight می‌زند، احتمالاً base URL اشتباه در فرانت‌اند است — ولی چون فقط چند مورد است و probeهای ربات هم OPTIONS می‌فرستند، برای الان همین کاهش نویز کافی است. اگر config CORS جداگانه OPTIONS را قبل از router هندل می‌کند، بررسی کن که این تغییر با آن تداخل ندارد.


نکات مهم کلی

  • همهٔ تغییرها backend clinicpro هستند؛ کلاینت (nobat724_front) قرارداد API را برای این موارد مصرف نمی‌کند به‌جز کد خطای جدید ۴۰۵/۴۰۹/۵۰۳ — پیام‌ها فارسی‌اند و ساختار envelope حفظ می‌شود.
  • بعد از تغییرِ رفتار endpoint حذف پزشک و درگاه پرداخت، فایل مربوطه در clinicpro/docs/api/ را به‌روز کن (قانون استاندارد پروژه: docs در همان session).
  • برای وظیفهٔ ۲ (soap) قدم اصلی redeploy است؛ گارد کد فقط شبکهٔ ایمنی است.
  • تست: بعد از تغییرات، ddev exec php bin/console lint:container و در صورت وجود، تست‌های SMS/Doctor delete را اجرا کن.
  • کدهای خطای جدید (ERR_METHOD_NOT_ALLOWED_001, ERR_PAYMENT_GATEWAY_001) را در ErrorCodes (اگر enum/const مرکزی دارد) ثبت کن تا با الگوی موجود یکدست بماند.