# ClinicPro — مستندات کامل پروژه > **آخرین به‌روزرسانی:** ۱۴۰۵/۰۳/۲۱ | **Symfony:** 7.x | **PHP:** ≥ 8.2 | **React:** 19 --- ## فهرست مطالب 1. [معرفی پروژه](#۱-معرفی-پروژه) 2. [تکنولوژی Stack](#۲-تکنولوژی-stack) 3. [راه‌اندازی محیط توسعه](#۳-راه‌اندازی-محیط-توسعه) 4. [آدرس‌های مهم](#۴-آدرس‌های-مهم) 5. [متغیرهای محیطی](#۵-متغیرهای-محیطی) 6. [دستورات Console](#۶-دستورات-console) 7. [امنیت و احراز هویت](#۷-امنیت-و-احراز-هویت) 8. [پایگاه داده — جداول و موجودیت‌ها](#۸-پایگاه-داده--جداول-و-موجودیت‌ها) 9. [API Endpoints](#۹-api-endpoints) 10. [پنل ادمین React](#۱۰-پنل-ادمین-react) 11. [Migrations](#۱۱-migrations) 12. [سرویس‌های خارجی](#۱۲-سرویس‌های-خارجی) 13. [ساختار دایرکتوری](#۱۳-ساختار-دایرکتوری) --- ## ۱. معرفی پروژه **ClinicPro** سیستم جامع مدیریت کلینیک و نوبت‌دهی پزشکی است — مهاجرت‌یافته از Drupal به **Symfony 7**. شامل یک REST API کامل و یک پنل ادمین React SPA داخل Symfony. ### ویژگی‌های اصلی - احراز هویت OTP + رمز عبور + JWT - مدیریت پزشکان با پروفایل کامل، تخصص، آدرس‌های مطب - مدیریت کلینیک‌ها با گالری، نقشه، دعوت پزشک - سیستم نوبت‌دهی با اسلات هفتگی، روزهای استثناء، تعطیلات - درگاه پرداخت (ملت + سپ) با کیف پول و تسویه‌حساب - مدیریت نمایندگان (agents) با کمیسیون - منشی با دسترسی‌های دانه‌ای (fine-grained permissions) - سیستم دعوتنامه پزشک به کلینیک با توکن ۷۲ ساعته - ارسال پیامک async از طریق KaveNegar / Rangineh - امتیاز و نظرات پزشکان - بلاگ با مدیریت کامل - داشبورد آمار real-time - Swagger UI محافظت‌شده با HTTP Basic Auth --- ## ۲. تکنولوژی Stack ### Backend | لایه | تکنولوژی | | ------------- | -------------------------------- | | Framework | Symfony 7.x | | PHP | ≥ 8.2 | | ORM | Doctrine ORM | | Auth | LexikJWT + OTP | | Queue | Symfony Messenger | | Cache | Redis | | Database | MariaDB 11.8 | | API Docs | NelmioApiDocBundle + swagger-php | | Rate Limiting | Symfony Rate Limiter | ### Frontend (Admin SPA) | لایه | تکنولوژی | | ------------- | --------------------- | | Framework | React 19 + TypeScript | | Routing | React Router DOM v7 | | Server State | TanStack Query v5 | | Client State | Zustand v5 (persist) | | Forms | React Hook Form + Zod | | Charts | Recharts | | Maps | React Leaflet 5 | | Icons | Heroicons v2 | | Notifications | Sonner | | Date | jalaali-js | | Build | Webpack Encore 6 | --- ## ۳. راه‌اندازی محیط توسعه ### پیش‌نیازها - [ddev](https://ddev.readthedocs.io/) نصب‌شده ### مراحل ```bash # ۱. clone و وارد شدن به پروژه git clone clinic-pro-symfony cd clinic-pro-symfony # ۲. راه‌اندازی ddev ddev start # ۳. نصب وابستگی‌های PHP ddev exec composer install # ۴. تولید کلیدهای JWT ddev exec php bin/console lexik:jwt:generate-keypair # ۵. اجرای migration های پایگاه داده ddev exec php bin/console doctrine:migrations:migrate --no-interaction # ۶. seed دسته‌بندی‌ها (استان، شهر، تخصص، خدمات پزشک) از data/seed/*.json ddev exec php bin/console app:seed-categories # ۷. نصب وابستگی‌های Node و بیلد فرانت‌اند ddev exec yarn install ddev exec yarn dev # ۸. ایجاد اولین ادمین (اختیاری) ddev exec php bin/console app:create-admin ``` ### دستورات روزانه ```bash # بیلد فرانت‌اند (یکبار) ddev exec yarn dev # حالت watch (هنگام توسعه) ddev exec yarn watch # پاک کردن cache بعد از تغییر backend ddev exec php bin/console cache:clear # اجرای queue worker ddev exec php bin/console messenger:consume async ``` --- ## ۴. آدرس‌های مهم | سرویس | آدرس | | ---------------- | ------------------------------------ | | **وب‌سایت** | https://clinic-pro.ddev.site | | **پنل ادمین** | https://clinic-pro.ddev.site/admin | | **Swagger UI** | https://clinic-pro.ddev.site/api/doc | | **Health Check** | https://clinic-pro.ddev.site/health | ### ورود به Swagger UI > آدرس: `https://clinic-pro.ddev.site/api/doc` | فیلد | مقدار | | -------------- | ----------- | | **نام کاربری** | `admin` | | **رمز عبور** | `clinic123` | پس از ورود، برای احراز هویت API درون Swagger ابتدا از `/api/v1/user/login` توکن بگیرید، سپس دکمه **Authorize** را بزنید و `Bearer ` وارد کنید. --- ## ۵. متغیرهای محیطی فایل `.env` (مقادیر پیش‌فرض توسعه): ```env # ── برنامه ───────────────────────────────────────────────────── APP_ENV=dev APP_SECRET=clinic_pro_secret_change_in_prod DEFAULT_URI=https://clinic-pro.ddev.site APP_BASE_URL=https://clinic-pro.ddev.site # ── پایگاه داده ────────────────────────────────────────────────── DATABASE_URL="mysql://db:db@db:3306/db?serverVersion=8.0&charset=utf8mb4" # ── JWT ───────────────────────────────────────────────────────── JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem JWT_PASSPHRASE=<رشته تصادفی> # ── CORS ──────────────────────────────────────────────────────── CORS_ALLOW_ORIGIN='^https?://(clinic-pro\.ddev\.site|localhost)(:[0-9]+)?$' ALLOWED_FRONTEND_HOSTS=clinic-pro.ddev.site,localhost # پروداکشن چند-دامنه‌ای (فرانتِ هر شهر): مقدار این دو متغیر را با اسکریپت بساز — # ddev exec php docker/gen-cors-env.php # دامنه‌ها از docker/frontend-domains.json خوانده می‌شوند؛ برای افزودن شهر، آن فایل را # ویرایش و دوباره اجرا کن و خروجی را در env سرور بگذار. # ── صف پیام و Cache ────────────────────────────────────────────── MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages REDIS_URL=redis://redis:6379 # ── توکن‌ها ────────────────────────────────────────────────────── REFRESH_TOKEN_TTL=2592000 # ۳۰ روز (ثانیه) OTP_TTL=1200 # ۲۰ دقیقه (ثانیه) # ── پیامک ──────────────────────────────────────────────────────── SMS_PROVIDER=kavenegar KAVENEGAR_API_KEY=<کلید> KAVENEGAR_SENDER=<شماره فرستنده> RANGINEH_API_KEY=<کلید> RANGINEH_SENDER=<شماره فرستنده> # ── فایل آپلود ─────────────────────────────────────────────────── MAX_FILE_SIZE_BYTES=5242880 # 5MB UPLOAD_DIR=var/uploads # ── درگاه پرداخت ───────────────────────────────────────────────── MELLAT_TERMINAL_ID=<شناسه> MELLAT_USERNAME=<کاربری> MELLAT_PASSWORD=<رمز> SEP_TERMINAL_ID=<شناسه> # ── Swagger UI ─────────────────────────────────────────────────── API_DOC_USERNAME=admin API_DOC_PASSWORD= # plain text: admin@admin ``` --- ## ۶. دستورات Console ```bash # ── Cache ──────────────────────────────────────────────────────── ddev exec php bin/console cache:clear # ── Database ───────────────────────────────────────────────────── ddev exec php bin/console doctrine:migrations:migrate --no-interaction ddev exec php bin/console doctrine:migrations:diff --no-interaction # بعد از تغییر entity ddev exec php bin/console doctrine:migrations:status # ── Debug ──────────────────────────────────────────────────────── ddev exec php bin/console debug:router | grep api ddev exec php bin/console debug:container | grep -i service # ── CORS چند-دامنه‌ای (تولید CORS_ALLOW_ORIGIN + ALLOWED_FRONTEND_HOSTS) ── ddev exec php docker/gen-cors-env.php # خروجی را در env سرور (Liara/Coolify) بگذار # ── Queue ──────────────────────────────────────────────────────── ddev exec php bin/console messenger:consume async # ── Test ───────────────────────────────────────────────────────── ddev exec php bin/phpunit ddev exec php bin/phpunit tests/SomeTest.php # ── Static Analysis ────────────────────────────────────────────── ddev exec php vendor/bin/phpstan analyse # ── Frontend ───────────────────────────────────────────────────── ddev exec yarn dev # بیلد یکبار ddev exec yarn watch # حالت watch ddev exec yarn build # پروداکشن ddev exec npx tsc --noEmit # فقط type check ``` --- ## ۷. امنیت و احراز هویت ### جریان احراز هویت ``` ۱. POST /api/v1/user/send-code → ارسال OTP به موبایل ۲. POST /api/v1/user/verify-code → تأیید OTP → دریافت uuid ۳. POST /oauth/token → ارسال uuid → دریافت JWT + refresh token ۴. Authorization: Bearer → همراه هر درخواست محافظت‌شده ``` ### جریان Login با رمز عبور ``` POST /api/v1/user/login { "mobile_number": "09...", "password": "..." } → { "access_token": "...", "refresh_token": "..." } ``` ### نقش‌های کاربری | نقش | توضیح | دسترسی | | ---------------- | ------------------ | ------------------------ | | `ROLE_USER` | پایه — همه کاربران | نوبت‌گیری، پروفایل | | `ROLE_ADMIN` | مدیر کل | پنل ادمین کامل | | `ROLE_CLINIC` | صاحب کلینیک | مدیریت کلینیک خود | | `ROLE_DOCTOR` | پزشک | مدیریت نوبت‌ها، پروفایل | | `ROLE_SECRETARY` | منشی | بر اساس permissions دکتر | ### دسترسی‌های منشی (JSON) ```json { "version": 1, "resources": { "appointments": { "view": true, "create": true, "cancel": false, "update_status": true }, "addresses": { "view": true, "create": false, "update": false, "delete": false }, "clinic_info": { "view": true, "update": false }, "insurances": { "view": true, "create": false, "update": false, "delete": false } } } ``` ### JWT - مدت اعتبار access token: **۱ ساعت** - Refresh token: **۳۰ روز** (ذخیره در Redis) - پیلود: `{ username: mobile_number, roles: [...], iat, exp }` - تجدید: `POST /oauth/token/refresh` ### Firewall های security.yaml | Firewall | Pattern | توضیح | | ------------------ | ------------------------------ | ----------------------------------- | | `api_doc` | `^/api/doc` | HTTP Basic Auth (admin / clinic123) | | `public_endpoints` | OTP، OAuth، endpoint های عمومی | بدون احراز هویت | | `payment_callback` | `/api/v1/payment/callback/` | بدون احراز هویت | | `api` | `^/(api\|oauth\|file/)` | JWT stateless | --- ## ۸. پایگاه داده — جداول و موجودیت‌ها ### جداول اصلی | جدول | موجودیت | توضیح | | --------------------------- | ---------------------- | ------------------------------------------------------------------------------------ | | `users` | User | uuid، mobile_number (unique)، roles (JSON)، status | | `doctors` | Doctor | user_id (OneToOne)، degree، doctor_rate، active_doctor_appointment | | `doctor_addresses` | DoctorAddress | doctor_id، name، latitude، longitude | | `clinics` | Clinic | user_id (owner)، name، telephone، is_24_7، latitude، longitude، images_clinic (JSON) | | `clinic_doctor_invitations` | ClinicDoctorInvitation | token (unique 96char hex)، status، expires_at (+72h)، mobile، uuid | | `appointments` | Appointment | doctor_id، user_id، slot_start، slot_end، status، price | | `weekly_schedules` | WeeklySchedule | doctor_id (OneToOne)، setting (JSON) | | `date_overrides` | DateOverride | doctor_id، date، active، setting (JSON) | | `holidays` | Holiday | doctor_id، start_date، end_date | | `payments` | Payment | order_id (unique)، amount_rials، status، gateway (mellat\|sep) | | `settlements` | Settlement | user_id، amount_rials، status، bank_account (JSON) | | `wallet_transactions` | WalletTransaction | user_id، amount_rials، type (credit\|debit)، balance_after | | `representations` | Representation | full_name، commission_percent، city_id | | `doctor_secretaries` | DoctorSecretary | doctor_id، secretary_id، permissions (JSON)، active | | `rates` | Rate | doctor_id، user_id، overall، diagnosis_accuracy، skill، behavior | | `comments` | Comment | doctor_id، user_id، body، status (approved\|pending\|rejected) | | `likes` | Like | comment_id، user_id | | `blogs` | Blog | author_id، title، slug (unique)، body، status | | `sms_templates` | SmsTemplate | name، body، provider_code، status | | `sms_logs` | SmsLog | mobile، message، provider، success | | `profiles` | UserProfile | blood_type، gender، insurance_id | | `provinces` | Province | name | | `cities` | City | name، province_id (FK) | | `specialties` | Specialty | name، parent_id (nullable برای زیر-تخصص) | | `doctor_services` | DoctorService | name | | `insurances` | Insurance | name، type (basic\|supplementary) | | `tags` | Tag | name | ### جداول رابطه‌ای (ManyToMany) | جدول | رابطه | | -------------------- | ---------------------- | | `clinic_doctors` | Clinic ↔ Doctor | | `clinic_specialties` | Clinic ↔ Specialty | | `clinic_services` | Clinic ↔ DoctorService | | `clinic_insurances` | Clinic ↔ Insurance | | `doctor_specialties` | Doctor ↔ Specialty | | `doctor_expertise` | Doctor ↔ Category | | `doctor_provinces` | Doctor ↔ Province | ### وابستگی‌های کلیدی ``` User ──OneToOne──► Doctor ──ManyToMany──► Specialty ──OneToOne──► Representation ──OneToMany──► Appointment ──OneToMany──► Clinic (owner) Doctor ──OneToMany──► DoctorAddress ──OneToMany──► Appointment ──ManyToMany──► Clinic Clinic ──ManyToOne──► User (owner) ──ManyToMany──► Doctor ──OneToMany──► ClinicDoctorInvitation ``` ### نکته timestamp همه فیلدهای تاریخ (createdAt، updatedAt، slotStart، ...) به صورت **integer Unix timestamp** ذخیره می‌شوند — نه DateTime object. --- ## ۹. API Endpoints ### احراز هویت | متد | آدرس | توضیح | دسترسی | | ---- | -------------------------- | ------------------ | ---------- | | POST | `/api/v1/user/send-code` | ارسال OTP | عمومی | | POST | `/api/v1/user/verify-code` | تأیید OTP | عمومی | | POST | `/api/v1/user/register` | ثبت‌نام | عمومی | | POST | `/api/v1/user/login` | لاگین با رمز عبور | عمومی | | POST | `/oauth/token` | دریافت JWT | عمومی | | POST | `/oauth/token/refresh` | تجدید توکن | عمومی | | GET | `/oauth/userinfo` | اطلاعات کاربر جاری | احراز هویت | | POST | `/oauth/logout` | خروج | احراز هویت | ### پزشکان | متد | آدرس | توضیح | دسترسی | | ------ | ------------------------------------------------------------ | --------------- | ---------- | | GET | `/api/v1/doctors` | لیست پزشکان | عمومی | | GET | `/api/v1/doctor/{uuid}` | جزئیات پزشک | عمومی | | POST | `/api/v1/doctor` | ایجاد پروفایل | احراز هویت | | PATCH | `/api/v1/doctor/{uuid}` | ویرایش | احراز هویت | | DELETE | `/api/v1/doctor/{uuid}` | حذف | احراز هویت | | POST | `/file/upload/clinic_pro/doctor/field_image` | آپلود تصویر | احراز هویت | | GET | `/api/v1/clinic-pro/doctor-addresses/{doctorId}` | آدرس‌های مطب | عمومی | | POST | `/api/v1/clinic-pro/doctor-address` | افزودن آدرس | احراز هویت | | PATCH | `/api/v1/clinic-pro/doctor-address/{id}` | ویرایش آدرس | احراز هویت | | DELETE | `/api/v1/clinic-pro/doctor-address/{id}` | حذف آدرس | احراز هویت | | POST | `/api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}` | ایجاد از کلینیک | احراز هویت | ### کلینیک‌ها | متد | آدرس | توضیح | دسترسی | | ----- | --------------------------------------------------- | -------------- | ---------- | | GET | `/api/v1/clinics` | لیست کلینیک‌ها | عمومی | | GET | `/api/v1/clinic/{uuid}` | جزئیات کلینیک | عمومی | | POST | `/api/v1/clinic` | ایجاد کلینیک | احراز هویت | | PATCH | `/api/v1/clinic/{uuid}` | ویرایش | احراز هویت | | GET | `/api/v1/clinic/doctor-list/{clinicUuid}` | پزشکان کلینیک | عمومی | | POST | `/file/upload/clinic_pro/clinic/field_clinic_logo` | آپلود لوگو | احراز هویت | | POST | `/file/upload/clinic_pro/clinic/field_image_clinic` | آپلود گالری | احراز هویت | ### دعوتنامه پزشک به کلینیک | متد | آدرس | توضیح | دسترسی | | ------ | -------------------------------------------------- | ----------------- | ---------- | | POST | `/api/v1/admin/clinic/{uuid}/invite-doctor` | ارسال دعوتنامه | ROLE_ADMIN | | GET | `/api/v1/admin/clinic/{uuid}/invitations` | لیست دعوتنامه‌ها | ROLE_ADMIN | | POST | `/api/v1/admin/clinic/invitation/{invUuid}/resend` | ارسال مجدد پیامک | ROLE_ADMIN | | PATCH | `/api/v1/admin/clinic/invitation/{invUuid}/status` | تغییر وضعیت | ROLE_ADMIN | | DELETE | `/api/v1/admin/clinic/invitation/{invUuid}` | حذف | ROLE_ADMIN | | GET | `/api/v1/clinic-invitation/{token}` | مشاهده دعوتنامه | عمومی | | POST | `/api/v1/clinic-invitation/{token}/accept` | پذیرش توسط دکتر | عمومی | | POST | `/api/v1/clinic-invitation/{token}/reject` | رد کردن توسط دکتر | عمومی | > توکن ۹۶ کاراکتر hex (`bin2hex(random_bytes(48))`) — مدت اعتبار ۷۲ ساعت. پیامک async از طریق Symfony Messenger ارسال می‌شود. ### نوبت‌دهی | متد | آدرس | توضیح | دسترسی | | ----- | ------------------------------------------ | -------------- | ---------- | | GET | `/api/v1/appointment-slots` | اسلات‌های خالی | عمومی | | POST | `/api/v1/appointment` | رزرو نوبت | احراز هویت | | GET | `/api/v1/appointment/{uuid}` | جزئیات نوبت | احراز هویت | | PATCH | `/api/v1/appointment/{uuid}/status` | تغییر وضعیت | احراز هویت | | GET | `/api/v1/appointments/doctor/{doctorUuid}` | نوبت‌های دکتر | احراز هویت | | GET | `/api/v1/appointments/user` | نوبت‌های کاربر | احراز هویت | ### تنظیمات نوبت‌دهی | متد | آدرس | توضیح | | --------------------- | ---------------------------------------------- | ------------ | | POST/PATCH/GET | `/api/v1/appointment-settings/weekly-schedule` | برنامه هفتگی | | POST/PATCH/DELETE/GET | `/api/v1/appointment-settings/date-override` | روز استثناء | | POST/PATCH/DELETE/GET | `/api/v1/appointment-settings/holidays` | تعطیلات | ### پرداخت | متد | آدرس | توضیح | دسترسی | | -------- | ------------------------------------ | ---------------------- | ---------- | | POST | `/api/v1/payment/appointment` | شروع پرداخت نوبت | احراز هویت | | POST/GET | `/api/v1/payment/callback/{gateway}` | callback (mellat\|sep) | عمومی | | GET | `/api/v1/payment/{uuid}` | وضعیت پرداخت | احراز هویت | | POST | `/api/v1/subscription-payment` | پرداخت اشتراک | احراز هویت | ### کیف پول و تسویه‌حساب | متد | آدرس | توضیح | دسترسی | | ---- | ----------------------------------- | ------------------- | ---------- | | GET | `/api/v1/wallet/balance` | موجودی کیف پول | احراز هویت | | GET | `/api/v1/wallet/transactions` | تاریخچه تراکنش‌ها | احراز هویت | | POST | `/api/v1/settlement` | درخواست تسویه | احراز هویت | | GET | `/api/v1/settlement` | لیست تسویه‌های خودم | احراز هویت | | POST | `/api/v1/settlement/{uuid}/approve` | تأیید | ROLE_ADMIN | | POST | `/api/v1/settlement/{uuid}/reject` | رد | ROLE_ADMIN | ### امتیاز و نظرات | متد | آدرس | توضیح | دسترسی | | ------ | ------------------------------- | -------------- | ---------- | | POST | `/api/v1/rate` | ثبت امتیاز | احراز هویت | | GET | `/api/v1/rate/{doctorUuid}` | میانگین امتیاز | عمومی | | POST | `/api/v1/comment` | ثبت نظر | احراز هویت | | GET | `/api/v1/comments/{doctorUuid}` | نظرات تأییدشده | عمومی | | DELETE | `/api/v1/comment/{uuid}` | حذف نظر | احراز هویت | | POST | `/api/v1/like/{commentUuid}` | لایک | احراز هویت | ### منشی | متد | آدرس | توضیح | دسترسی | | ------ | ---------------------------------- | ------------------ | ----------- | | POST | `/api/v1/secretary` | ایجاد منشی | ROLE_DOCTOR | | GET | `/api/v1/secretary/{uuid}` | جزئیات | احراز هویت | | PATCH | `/api/v1/secretary/{uuid}` | ویرایش permissions | ROLE_DOCTOR | | DELETE | `/api/v1/secretary/{uuid}` | حذف | ROLE_DOCTOR | | GET | `/api/v1/secretaries/{doctorUuid}` | لیست منشی‌های دکتر | احراز هویت | ### نمایندگان | متد | آدرس | توضیح | دسترسی | | --------------------- | ------------------------------------------------- | -------------- | ---------- | | POST/GET/PATCH/DELETE | `/api/v1/representation/{uuid?}` | مدیریت نماینده | احراز هویت | | GET | `/api/v1/representation/{uuid}/dashboard/monthly` | آمار ماهانه | احراز هویت | | GET | `/api/v1/representation/{uuid}/dashboard/yearly` | آمار سالانه | احراز هویت | ### پیامک | متد | آدرس | توضیح | دسترسی | | --------------------- | ------------------------------------------- | ---------------- | ---------- | | POST | `/api/v1/sms/send` | ارسال مستقیم | احراز هویت | | POST | `/api/v1/sms/send-template` | ارسال با قالب | احراز هویت | | POST/PATCH/GET/DELETE | `/api/v1/sms/template/{uuid?}` | مدیریت قالب | احراز هویت | | POST | `/api/v1/sms/template/{uuid}/submit` | ارسال برای تأیید | احراز هویت | | POST | `/api/v1/admin/sms/template/{uuid}/approve` | تأیید قالب | ROLE_ADMIN | ### تخصص، خدمات، بیمه، تگ، موقعیت ``` GET /api/v1/specialties عمومی GET /api/v1/doctor-services عمومی GET /api/v1/insurances عمومی GET /api/v1/tags عمومی GET /api/v1/provinces عمومی GET /api/v1/cities?province_id= عمومی GET /api/v1/categorys/{bundle} عمومی (legacy — bundle: state|city|specially_doctor|doctor_services|insurance_type|supplementary_insurance|tag) POST/PATCH/DELETE /api/v1/admin/specialty/{id?} ROLE_ADMIN POST/PATCH/DELETE /api/v1/admin/doctor-service/{id?} ROLE_ADMIN POST/PATCH/DELETE /api/v1/admin/insurance/{id?} ROLE_ADMIN POST/PATCH/DELETE /api/v1/admin/tag/{id?} ROLE_ADMIN POST/PATCH/DELETE /api/v1/admin/province/{id?} ROLE_ADMIN POST/PATCH/DELETE /api/v1/admin/city/{id?} ROLE_ADMIN ``` ### بلاگ | متد | آدرس | توضیح | دسترسی | | ------ | ------------------------------------------ | ---------------------- | ---------- | | GET | `/api/v1/blogs` | لیست بلاگ‌های منتشرشده | عمومی | | GET | `/api/v1/blog/{slug}` | جزئیات بلاگ | عمومی | | POST | `/api/v1/blog` | نوشتن بلاگ | احراز هویت | | PATCH | `/api/v1/blog/{uuid}` | ویرایش | احراز هویت | | DELETE | `/api/v1/blog/{uuid}` | حذف | احراز هویت | | POST | `/file/upload/clinic_pro/blog/field_image` | آپلود تصویر | احراز هویت | ### پنل ادمین API > همه endpoint های زیر نیاز به `ROLE_ADMIN` دارند. | آدرس | توضیح | | ------------------------------------------ | ------------------------------------------ | | `GET /api/v1/admin/dashboard/stats` | ۱۲ KPI (کاربران، پزشکان، نوبت، درآمد، ...) | | `GET /api/v1/admin/dashboard/charts` | نمودار ۳۰ روزه نوبت و درآمد | | `GET /api/v1/admin/dashboard/recent` | آخرین نوبت‌ها، پرداخت‌ها، کاربران | | `GET /api/v1/admin/users` | لیست کاربران (paginated) | | `GET /api/v1/admin/users/{uuid}` | جزئیات کاربر | | `PUT /api/v1/admin/users/{uuid}/role` | تغییر نقش | | `POST /api/v1/admin/users/{uuid}/status` | فعال/غیرفعال | | `DELETE /api/v1/admin/users/{uuid}` | حذف کاربر | | `GET /api/v1/admin/doctors` | لیست پزشکان | | `GET /api/v1/admin/clinics` | لیست کلینیک‌ها | | `PATCH /api/v1/admin/clinic/{uuid}/status` | تغییر وضعیت کلینیک | | `DELETE /api/v1/admin/clinic/{uuid}` | حذف کلینیک | | `GET /api/v1/admin/appointments` | لیست نوبت‌ها | | `GET /api/v1/admin/payments` | لیست پرداخت‌ها | | `GET /api/v1/admin/settlements` | لیست تسویه‌ها | | `GET /api/v1/admin/representations` | لیست نمایندگان | | `GET /api/v1/admin/secretaries` | لیست منشی‌ها | | `GET /api/v1/admin/rates` | لیست امتیازها | | `GET /api/v1/admin/comments` | لیست نظرات | | `GET /api/v1/admin/sms/logs` | لاگ پیامک‌ها | | `GET /api/v1/admin/sms/templates` | قالب‌های پیامک | ### فرمت پاسخ‌های API ```json // Single resource { "success": true, "data": { ... } } // Paginated list { "success": true, "data": [...], "meta": { "totalRecords": N, "totalPages": N, "currentPage": N } } // Error { "success": false, "data": null, "errors": [{ "code": "ERR_...", "message": "..." }] } ``` **نکته double-nested:** برخی endpoint ها پاسخ را داخل `data` اضافه می‌کنند: `$this->success(['data' => $entity->toArray()])` → فرانت باید با `data?.data?.data` استخراج کند. --- ## ۱۰. پنل ادمین React ### Route های پنل | مسیر | صفحه | توضیح | | ------------------------------ | ------------------------ | ------------------------------------ | | `/admin/login` | LoginPage | ورود با موبایل + رمز | | `/admin/dashboard` | DashboardPage | آمار KPI، نمودار، فعالیت‌های اخیر | | `/admin/users` | UsersPage | لیست + جستجو + فیلتر نقش | | `/admin/users/:uuid` | UserDetailPage | پروفایل + ویرایش نقش | | `/admin/doctors` | DoctorsPage | لیست پزشکان | | `/admin/doctors/new` | DoctorFormPage | ایجاد پزشک جدید | | `/admin/doctors/:uuid` | DoctorDetailPage | پروفایل کامل + تخصص + نوبت‌ها | | `/admin/clinics` | ClinicsPage | لیست کلینیک‌ها | | `/admin/clinics/:uuid` | ClinicDetailPage | پروفایل + نقشه + گالری + دعوتنامه‌ها | | `/admin/appointments` | AppointmentsPage | لیست نوبت‌ها + فیلتر | | `/admin/appointments/:uuid` | AppointmentDetailPage | جزئیات نوبت | | `/admin/payments` | PaymentsPage | لیست پرداخت‌ها | | `/admin/settlements` | SettlementsPage | تسویه‌حساب | | `/admin/representations` | RepresentationsPage | نمایندگان | | `/admin/representations/:uuid` | RepresentationDetailPage | جزئیات نماینده | | `/admin/comments` | CommentsPage | مدیریت نظرات | | `/admin/ratings` | RatingsPage | امتیازها | | `/admin/sms` | SmsPage | مدیریت پیامک | | `/admin/categories` | CategoriesPage | تخصص/خدمات/بیمه/تگ | | `/admin/blogs` | BlogsPage | لیست بلاگ‌ها | | `/admin/blogs/new` | BlogFormPage | نوشتن بلاگ جدید | | `/admin/secretaries` | SecretariesPage | مدیریت منشی‌ها | ### الگوی دریافت داده ```typescript // لیست paginated const { data } = useQuery({ queryKey: ["resource", page, filters], queryFn: () => api.get>(`/api/v1/admin/...?page=${page}`), }); const items = data?.data ?? []; // آرایه آیتم‌ها const total = data?.meta?.totalRecords ?? 0; // Single resource (double-nested — برخی endpoint ها) const raw = data?.data; const item = (raw as any)?.data ?? raw; // Category API (triple-nested legacy) const cats = data?.data?.data ?? []; ``` ### ویژگی‌های فنی فرانت‌اند - **Auth Store** (Zustand persist): توکن در `localStorage['clinicpro-auth']` → `state.token` - **Modal ها**: از `createPortal(modal, document.body)` استفاده می‌کنند تا از z-index Leaflet بگریزند - **CSS**: کلاس‌های template اختصاصی — `.card`، `.btn.primary/.ghost/.soft/.sm`، `.badge.green/.gray/.blue`، `.overlay`، `.seg`، `.skeleton`، `.empty` - **Avatar**: OKLCH gradient با `HUES_LIST = [256, 205, 162, 295, 272]` --- ## ۱۱. Migrations ```bash # ایجاد migration جدید بعد از تغییر entity ddev exec php bin/console doctrine:migrations:diff --no-interaction # اجرا ddev exec php bin/console doctrine:migrations:migrate --no-interaction # وضعیت ddev exec php bin/console doctrine:migrations:status ``` ### لیست migration های موجود | فایل | توضیح | | ------------------------- | ---------------------------------------------- | | Version20260609130407 | جداول پایه: users, doctors, appointments | | Version20260609131304 | پرداخت، کیف پول | | Version20260609131553 | امتیاز، نظرات | | Version20260609132009 | نمایندگان | | Version20260609132708 | منشی | | Version20260609133121 | کلینیک | | Version20260609133334 | پیامک | | Version20260609133546 | بلاگ | | Version20260609134112 | تخصص، بیمه، تگ | | Version20260609134741 | استان، شهر | | Version20260609135126 | پروفایل، آدرس مطب | | Version20260609135514 | برنامه هفتگی | | Version20260609135704 | روز استثناء، تعطیلات | | Version20260609135923 | ManyToMany: clinic_doctors, clinic_specialties | | Version20260609140223 | ایندکس‌های اضافی | | Version20260609140423 | doctor_insurances | | Version20260610062539 | فیلدهای اضافی clinic | | Version20260610175105 | فیلد is_active برای clinic | | **Version20260610183655** | **جدول clinic_doctor_invitations** | --- ## ۱۲. سرویس‌های خارجی ### پیامک **KaveNegar** (پیش‌فرض) - API: `https://api.kavenegar.com/v1/{KEY}/sms/send.json` - پشتیبانی از: send (متن آزاد) + sendTemplate (قالب) **Rangineh** - API: `https://rest.payamresan.com/api/v1/send` - پشتیبانی از: send + sendTemplate **ارسال async:** ```php $this->smsService->dispatchAsync($mobile, $message); // → Symfony Messenger → Redis → SendSmsHandler ``` ### درگاه پرداخت **بانک ملت (Mellat)** - SOAP WebService: `https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl` - تأیید: ResCode=0 + SettlePayment **سپ (SEP)** - REST API با ترمینال ID ### Redis - صف پیام: `redis://redis:6379/messages` - Cache: `redis://redis:6379` - Refresh Token: کلید `refresh_` --- ## ۱۳. ساختار دایرکتوری ``` clinic-pro-symfony/ ├── assets/ │ └── admin/ # React SPA (پنل ادمین) │ ├── App.tsx # React Router routes │ ├── pages/ # یک فایل به ازای هر صفحه │ ├── components/ │ │ ├── layout/ # AdminLayout, Sidebar, Topbar │ │ └── ui/ # DataTable, Modal, ConfirmDialog, Pagination, ... │ ├── lib/ │ │ ├── api.ts # fetch wrapper (JWT از localStorage) │ │ └── utils.ts # formatRial, formatDate, formatDateTime, formatNumber │ ├── stores/ │ │ ├── authStore.ts # Zustand — token, isAuthenticated │ │ └── uiStore.ts # sidebar state │ ├── types/index.ts # TypeScript interfaces │ └── styles.css # کلاس‌های template ├── config/ │ ├── packages/ │ │ ├── security.yaml # firewalls, access_control │ │ └── lexik_jwt_authentication.yaml │ └── services.yaml # dependency injection ├── docs/ │ └── tasks/ # مستندات فنی هر feature ├── migrations/ # Doctrine migrations ├── src/ │ ├── Admin/Controller/ # AdminApiController — همه endpoint های ادمین │ ├── Appointment/ # نوبت‌دهی، زمان‌بندی، SlotCalculatorService │ ├── Auth/ # احراز هویت، JWT، OTP، User entity │ ├── Blog/ # بلاگ │ ├── Category/ # کتگوری legacy (bundle system) │ ├── Clinic/ # کلینیک │ ├── ClinicInvitation/ # دعوتنامه پزشک به کلینیک │ ├── Doctor/ # پزشک، آدرس مطب │ ├── DoctorService/ # خدمات پزشکی │ ├── Insurance/ # بیمه │ ├── Location/ # استان، شهر │ ├── Payment/ # پرداخت، درگاه‌ها (Mellat, Sep) │ ├── Rating/ # امتیاز، نظرات، لایک │ ├── Representation/ # نمایندگان │ ├── Secretary/ # منشی │ ├── Settlement/ # تسویه‌حساب، کیف پول │ ├── Sms/ # پیامک، قالب، لاگ │ ├── Specialty/ # تخصص پزشکی │ ├── Tag/ # تگ بلاگ │ ├── UserProfile/ # پروفایل پزشکی کاربر │ └── Shared/ │ ├── Controller/BaseController.php # success() / paginated() / error() │ ├── Exception/AppException.php # domain exception با httpStatus │ ├── Constant/ErrorCodes.php # کدهای خطا به صورت const string │ └── EventSubscriber/ExceptionSubscriber.php ├── public/ │ └── build/ # webpack output (gitignore شده) ├── var/ │ └── uploads/ # فایل‌های آپلود‌شده ├── .ddev/config.yaml # PHP 8.3، MariaDB 11.8، nginx-fpm ├── .env # متغیرهای پیش‌فرض (commit نشود .env.local) └── webpack.config.js # Webpack Encore ``` --- > **یادآور توسعه:** تمام دستورات را با پیشوند `ddev exec` اجرا کنید. > CSS فرانت‌اند از کلاس‌های template اختصاصی استفاده می‌کند (نه Tailwind). > بعد از هر تغییر backend: `cache:clear` > بعد از هر تغییر entity: `migrations:diff` سپس `migrations:migrate` --- ## 🛡️ ALTCHA — کپچای خودمیزبان (ضدِ اسپم/بات) کپچای Proof-of-Work مبتنی بر [ALTCHA](https://altcha.org) — بدون تصویر، بدون تایپ، بدون سرویس خارجی (مناسب کاربران داخل ایران). فقط روی endpointهای **عمومی و بدون احراز هویت** اعمال می‌شود. ### نصب (انجام‌شده) - بک‌اند: `composer require altcha-org/altcha` (پکیج رسمی PHP، از V1 API استفاده می‌شود). - فرانت‌اند: `yarn add altcha` (web-component محلی، بدون CDN؛ Encore آن را bundle می‌کند). ### تنظیمات (`.env` / برای prod در `.env.local`) | متغیر | پیش‌فرض | توضیح | |---|---|---| | `ALTCHA_ENABLED` | `false` | در prod روی `true`؛ در dev/test غیرفعال بماند | | `ALTCHA_HMAC_KEY` | `change-me-in-env-local` | کلید امضای سرور — **حتماً در prod عوض شود** و مخفی بماند | | `ALTCHA_MAX_NUMBER` | `100000` | سقف اعداد PoW = **سختی**؛ بالاتر = سنگین‌تر برای مرورگر | | `ALTCHA_EXPIRE_SECONDS` | `300` | عمر هر challenge (ثانیه) | ### جریان 1. کلاینت `GET /api/v1/altcha/challenge` را می‌گیرد (challenge امضاشده). 2. `` در پس‌زمینه PoW را حل می‌کند. 3. مقدار حل‌شده (base64) با کلید `altcha` در بدنه‌ی درخواستِ endpoint عمومی ارسال می‌شود. 4. سرور با `CaptchaGuard::assertValid($request)` اعتبارسنجی می‌کند (امضا + انقضا + یک‌بارمصرف بودن). ### endpointهای محافظت‌شده `send-code`، `register`، `otp-login`، `reset-password`، `pre-registration`. > `rate`/`comment` پشت JWT هستند و کپچا نمی‌گیرند (بی‌فایده است). ### افزودن کپچا به endpoint عمومی جدید `CaptchaGuard` را inject کن و اولین خط handler: ```php $this->captcha->assertValid($request); // پرتاب ERR_CAPTCHA_001 (422) در صورت شکست ``` سپس مسیر را در فرانت به بدنه‌ی درخواست `altcha` وصل کن. ### تغییر سختی `ALTCHA_MAX_NUMBER` را بالا/پایین ببر (مثلاً `1000000` برای سخت‌تر). ### غیرفعال‌سازی `ALTCHA_ENABLED=false` → guard کاملاً no-op می‌شود (dev/test همیشه این‌طور است). ### Troubleshooting - **همیشه ERR_CAPTCHA_001:** کلید `ALTCHA_HMAC_KEY` بین challenge و verify باید یکسان باشد؛ اگر بعد از صدور challenge کلید عوض شود، امضا نامعتبر می‌شود. - **challenge منقضی:** ساعت سرور را چک کن؛ `ALTCHA_EXPIRE_SECONDS` خیلی کوتاه نباشد. - **replay:** هر challenge یک‌بار مصرف است؛ برای هر submit یک challenge تازه بگیر. - **خطای Redis:** pool اختصاصی `altcha.pool` روی `REDIS_URL` است؛ در دسترس بودن Redis لازم است.