The wiring is fixed and browser-verified, but components/appointment/ has no dark: utilities at all, so the booking flow still renders identically in either theme. That is design work, and the row says so rather than claiming done. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ClinicPro — مستندات کامل پروژه
آخرین بهروزرسانی: ۱۴۰۵/۰۳/۲۱ | Symfony: 7.x | PHP: ≥ 8.2 | React: 19
فهرست مطالب
- معرفی پروژه
- تکنولوژی Stack
- راهاندازی محیط توسعه
- آدرسهای مهم
- متغیرهای محیطی
- دستورات Console
- امنیت و احراز هویت
- پایگاه داده — جداول و موجودیتها
- API Endpoints
- پنل ادمین React
- Migrations
- سرویسهای خارجی
- ساختار دایرکتوری
۱. معرفی پروژه
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 نصبشده
مراحل
# ۱. clone و وارد شدن به پروژه
git clone <repo-url> 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
دستورات روزانه
# بیلد فرانتاند (یکبار)
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 <token> وارد کنید.
۵. متغیرهای محیطی
فایل .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=<bcrypt hash> # plain text: admin@admin
۶. دستورات Console
# ── 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 <jwt> → همراه هر درخواست محافظتشده
جریان 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)
{
"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
// 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 | مدیریت منشیها |
الگوی دریافت داده
// لیست paginated
const { data } = useQuery({
queryKey: ["resource", page, filters],
queryFn: () =>
api.get<PaginatedResponse<T>>(`/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
# ایجاد 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:
$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_<sha256>
۱۳. ساختار دایرکتوری
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 — بدون تصویر، بدون تایپ، بدون سرویس خارجی (مناسب کاربران داخل ایران). فقط روی 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 (ثانیه) |
جریان
- کلاینت
GET /api/v1/altcha/challengeرا میگیرد (challenge امضاشده). <altcha-widget>در پسزمینه PoW را حل میکند.- مقدار حلشده (base64) با کلید
altchaدر بدنهی درخواستِ endpoint عمومی ارسال میشود. - سرور با
CaptchaGuard::assertValid($request)اعتبارسنجی میکند (امضا + انقضا + یکبارمصرف بودن).
endpointهای محافظتشده
send-code، register، otp-login، reset-password، pre-registration.
rate/commentپشت JWT هستند و کپچا نمیگیرند (بیفایده است).
افزودن کپچا به endpoint عمومی جدید
CaptchaGuard را inject کن و اولین خط handler:
$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 لازم است.