The operator opens the session, treats each body area on its own device and records what that device was set to. Readings are validated against the resource type's field schema, so a laser form and an RF form each enforce their own rules without this code naming either. Finishing is allowed with areas still open — the operator is standing in front of a patient and must not be trapped by the software — but the count comes back so the panel can warn. Session state mirrors onto the appointment (salon, then completed) while its slot times are never rewritten: those are the reservation's promise and the input to occupancy, whereas how long it actually took belongs to the session. Overwriting them would destroy the comparison between the two. Who performed it is recorded on the session rather than inferred from the appointment's planned staff: when a colleague covers a sick operator, the medical record must say who actually held the device. Endpoints live under /api/v1/dashboard/staff because StaffRouteGuardSubscriber closes everything else to staff-only users. Opening a second door through its allowlist would put the access boundary in two places. 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 لازم است.