feat: Implement SMS sending functionality with KavehNegar and Rangineh providers
- Add SendSmsMessage class for encapsulating SMS message data. - Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS. - Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates. - Develop SendSmsHandler for handling SMS sending messages. - Create SmsService to manage SMS dispatching and logging. - Add UserProfileController for managing user profiles with CRUD operations. - Implement UserProfile entity and repository for user profile data management. - Update symfony.lock and bootstrap.php for project dependencies and environment setup.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# معماری — تسک ۱۲: ماژول امتیاز و نظرات
|
||||
|
||||
## ساختار فایلها
|
||||
```
|
||||
src/Module/Rating/
|
||||
├── Controller/
|
||||
│ ├── RatingController.php
|
||||
│ └── CommentController.php
|
||||
├── Service/
|
||||
│ ├── RatingService.php ← آپدیت average_rating دکتر
|
||||
│ └── CommentService.php
|
||||
├── Repository/
|
||||
│ ├── RatingRepository.php
|
||||
│ └── CommentRepository.php
|
||||
├── Entity/
|
||||
│ ├── Rating.php
|
||||
│ └── Comment.php
|
||||
├── DTO/
|
||||
│ ├── Request/
|
||||
│ │ ├── CreateRatingRequest.php
|
||||
│ │ ├── CreateCommentRequest.php
|
||||
│ │ └── ConfirmCommentRequest.php
|
||||
│ └── Response/
|
||||
│ ├── RatingResponse.php
|
||||
│ └── CommentResponse.php
|
||||
└── Voter/
|
||||
├── RatingVoter.php
|
||||
└── CommentVoter.php
|
||||
```
|
||||
|
||||
## Entity: Rating
|
||||
```php
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'ratings')]
|
||||
class Rating
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
|
||||
private int $id;
|
||||
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
private Uuid $uuid;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
private User $patient;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Doctor::class)]
|
||||
private Doctor $doctor;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $score; // 1 تا 5
|
||||
|
||||
#[ORM\OneToOne(targetEntity: Appointment::class, nullable: true)]
|
||||
private ?Appointment $appointment;
|
||||
|
||||
// TimestampableTrait
|
||||
}
|
||||
```
|
||||
|
||||
## Entity: Comment
|
||||
```php
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'comments')]
|
||||
class Comment
|
||||
{
|
||||
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
|
||||
private int $id;
|
||||
|
||||
#[ORM\Column(type: UuidType::NAME, unique: true)]
|
||||
private Uuid $uuid;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
private User $author;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Doctor::class)]
|
||||
private Doctor $doctor;
|
||||
|
||||
#[ORM\Column(type: 'text')]
|
||||
private string $text;
|
||||
|
||||
// pending, approved, rejected
|
||||
#[ORM\Column(length: 20, default: 'pending')]
|
||||
private string $status;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Rating::class, nullable: true)]
|
||||
private ?Rating $rating;
|
||||
|
||||
// TimestampableTrait
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
# پایگاه داده — تسک ۱۲: ماژول امتیاز و نظرات
|
||||
|
||||
## مهم: نام فیلد از DB
|
||||
فیلد ستاره در Drupal **`field_starts`** است (نه `field_stars`!) — از config تأیید شد.
|
||||
|
||||
## جدول: ratings
|
||||
_(entity_type=clinic_pro_comment, bundle=rate)_
|
||||
|
||||
| ستون | نوع | نام Drupal | توضیح |
|
||||
|------|-----|-----------|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
|
||||
| user_id | INT FK → users.id NOT NULL | uid | امتیازدهنده |
|
||||
| doctor_id | INT FK → doctors.id NOT NULL | field_doctor_id | entity ref → clinic_pro |
|
||||
| doctor_behavior | INT NOT NULL | field_doctor_behavior | برخورد مناسب (0-100) |
|
||||
| accuracy_of_diagnosis | INT NOT NULL | field_accuracy_of_diagnosis | تشخیص درست (0-100) |
|
||||
| waiting_time_at_clinic | INT NOT NULL | field_waiting_time_at_clinic | زمان انتظار (0-100) |
|
||||
| doctor_expertise | INT NOT NULL | field_doctor_expertise | مهارت (0-100) |
|
||||
| clinic_cleanliness | INT NOT NULL | field_clinic_cleanliness | نظافت (0-100) |
|
||||
| starts | DECIMAL(10,2) NOT NULL | field_starts | ستاره محاسبهشده (0-5) — ⚠️ `starts` نه `stars`! |
|
||||
| percent | FLOAT NOT NULL | field_percent | درصد محاسبهشده (0-100) |
|
||||
| created_at | INT NOT NULL | created | Unix timestamp |
|
||||
| updated_at | INT NOT NULL | changed | Unix timestamp |
|
||||
|
||||
## جدول: comments
|
||||
_(entity_type=clinic_pro_comment, bundle=comments)_
|
||||
|
||||
| ستون | نوع | نام Drupal | توضیح |
|
||||
|------|-----|-----------|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
|
||||
| user_id | INT FK → users.id NOT NULL | uid | نویسنده |
|
||||
| doctor_id | INT FK → doctors.id NOT NULL | field_doctor_id | دکتر |
|
||||
| comment | LONGTEXT NOT NULL | field_comment | متن نظر |
|
||||
| approved | TINYINT(1) DEFAULT 0 | field_approved | تأیید شده (نه ENUM بلکه boolean) |
|
||||
| parent_id | INT FK → comments.id NULL | field_parent | نظر پدر (پاسخ به نظر) |
|
||||
| created_at | INT NOT NULL | created | Unix timestamp |
|
||||
| updated_at | INT NOT NULL | changed | Unix timestamp |
|
||||
|
||||
## جدول: likes
|
||||
_(entity_type=clinic_pro_comment, bundle=like)_
|
||||
|
||||
| ستون | نوع | نام Drupal | توضیح |
|
||||
|------|-----|-----------|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
|
||||
| user_id | INT FK → users.id NOT NULL | uid | کاربر |
|
||||
| comment_id | INT FK → comments.id NOT NULL | field_comment_id | نظر مورد لایک |
|
||||
| is_like | TINYINT(1) NOT NULL | field_like | لایک (1) یا دیسلایک (0) |
|
||||
| created_at | INT NOT NULL | created | Unix timestamp |
|
||||
| updated_at | INT NOT NULL | changed | Unix timestamp |
|
||||
|
||||
## ایندکسها
|
||||
```sql
|
||||
-- هر کاربر فقط یک امتیاز برای هر دکتر
|
||||
CREATE UNIQUE INDEX idx_ratings_user_doctor ON ratings(user_id, doctor_id);
|
||||
|
||||
-- هر کاربر فقط یک لایک/دیسلایک برای هر نظر
|
||||
CREATE UNIQUE INDEX idx_likes_user_comment ON likes(user_id, comment_id);
|
||||
|
||||
CREATE INDEX idx_ratings_doctor ON ratings(doctor_id);
|
||||
CREATE INDEX idx_comments_doctor_approved ON comments(doctor_id, approved);
|
||||
CREATE INDEX idx_comments_parent ON comments(parent_id);
|
||||
```
|
||||
|
||||
## نکتههای مهم
|
||||
- فیلد ستاره `starts` است (نه `stars`) — همانطور که در config تأیید شد
|
||||
- `approved` boolean است (0/1)، نه ENUM
|
||||
- `percent` نوع FLOAT است (نه DECIMAL) — از config تأیید شد
|
||||
- `starts` نوع DECIMAL(10,2) است — از config تأیید شد
|
||||
- `comment.status` در جدول پایه Drupal وجود دارد (tinyint) اما از `field_approved` استفاده میشود
|
||||
@@ -0,0 +1,164 @@
|
||||
# نکات پیادهسازی — تسک ۱۲: ماژول امتیاز و نظرات
|
||||
|
||||
## ⚠ تناقض نام فیلدها: API vs DB (بسیار مهم!)
|
||||
|
||||
فیلدهایی که **کلاینت ارسال میکند** با نام فیلدهای **پایگاه داده** متفاوت هستند:
|
||||
|
||||
| نام در Request (API) | نام در DB (Drupal field) | توضیح |
|
||||
|---------------------|------------------------|-------|
|
||||
| `correct_diagnosis` | `accuracy_of_diagnosis` | دقت تشخیص |
|
||||
| `doctor_skill` | `doctor_expertise` | مهارت پزشک |
|
||||
| `behavior_doctor` | `doctor_behavior` | برخورد پزشک |
|
||||
| `office_cleaning` | `clinic_cleanliness` | نظافت مطب |
|
||||
| `time_in_office` | `waiting_time_at_clinic` | زمان انتظار |
|
||||
| `doctor` | `doctor_id` | شناسه دکتر (integer) |
|
||||
| `rate` | `starts` | امتیاز ستارهای (DECIMAL 10,2) |
|
||||
|
||||
**در Symfony باید:**
|
||||
- ورودی را با نامهای API دریافت کن (`correct_diagnosis`, ...)
|
||||
- در Entity و DB با نامهای Drupal ذخیره کن (`accuracy_of_diagnosis`, ...)
|
||||
|
||||
## نمونه واقعی Request — POST /api/v1/clinicpro/rate
|
||||
```json
|
||||
{
|
||||
"correct_diagnosis": 100,
|
||||
"doctor_skill": 100,
|
||||
"behavior_doctor": 100,
|
||||
"office_cleaning": 100,
|
||||
"time_in_office": 100,
|
||||
"doctor": 1,
|
||||
"rate": 2
|
||||
}
|
||||
```
|
||||
|
||||
## نمونه واقعی Request — PATCH /api/v1/clinicpro/rate/{uuid}
|
||||
```json
|
||||
{
|
||||
"correct_diagnosis": 50,
|
||||
"doctor_skill": 60,
|
||||
"behavior_doctor": 70,
|
||||
"office_cleaning": 80,
|
||||
"time_in_office": 90,
|
||||
"doctor": 1,
|
||||
"rate": 2
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## سیستم امتیازدهی وزنی
|
||||
|
||||
Rating در Drupal **5 معیار جداگانه** دارد که هر کدام مقدار 0-100 میگیرند
|
||||
و با وزنهای متفاوت محاسبه میشوند:
|
||||
|
||||
```php
|
||||
$weights = [
|
||||
"doctor_behavior" => 1.5, // behavior_doctor در API
|
||||
"accuracy_of_diagnosis" => 3.0, // correct_diagnosis در API
|
||||
"waiting_time_at_clinic" => 1.0, // time_in_office در API
|
||||
"doctor_expertise" => 2.0, // doctor_skill در API
|
||||
"clinic_cleanliness" => 1.0, // office_cleaning در API
|
||||
];
|
||||
|
||||
// فرمول محاسبه:
|
||||
$weightedAverage = SUM(value * weight) / SUM(weights); // از 100
|
||||
$stars = ($weightedAverage / 100) * 5; // از 5
|
||||
```
|
||||
|
||||
### پیادهسازی calculateDoctorRating در Symfony
|
||||
```php
|
||||
public function calculateRating(array $apiScores): array
|
||||
{
|
||||
// نگاشت نامهای API به نامهای DB
|
||||
$mapped = [
|
||||
'accuracy_of_diagnosis' => $apiScores['correct_diagnosis'] ?? 0,
|
||||
'doctor_expertise' => $apiScores['doctor_skill'] ?? 0,
|
||||
'doctor_behavior' => $apiScores['behavior_doctor'] ?? 0,
|
||||
'clinic_cleanliness' => $apiScores['office_cleaning'] ?? 0,
|
||||
'waiting_time_at_clinic' => $apiScores['time_in_office'] ?? 0,
|
||||
];
|
||||
|
||||
$weights = [
|
||||
'doctor_behavior' => 1.5,
|
||||
'accuracy_of_diagnosis' => 3.0,
|
||||
'waiting_time_at_clinic' => 1.0,
|
||||
'doctor_expertise' => 2.0,
|
||||
'clinic_cleanliness' => 1.0,
|
||||
];
|
||||
|
||||
$totalScore = 0.0;
|
||||
$totalWeight = 0.0;
|
||||
|
||||
foreach ($weights as $key => $weight) {
|
||||
$totalScore += $mapped[$key] * $weight;
|
||||
$totalWeight += $weight;
|
||||
}
|
||||
|
||||
$weightedAverage = $totalWeight > 0 ? $totalScore / $totalWeight : 0;
|
||||
$stars = ($weightedAverage / 100) * 5;
|
||||
|
||||
return [
|
||||
'percent' => round($weightedAverage, 1),
|
||||
'starts' => round(min(5.0, max(0.0, $stars)), 2),
|
||||
// ⚠️ نام فیلد DB: "starts" است نه "stars"!
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## آمار دکتر — GET /api/v1/clinicpro-comment/doctor-rate/{doctorUuid}
|
||||
|
||||
```
|
||||
URL: /api/v1/clinicpro-comment/doctor-rate/{uuid_دکتر}
|
||||
Auth: عمومی (بدون احراز هویت)
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"average_stars": 4.3,
|
||||
"total_rates": 87,
|
||||
"averages": {
|
||||
"average_doctor_behavior": 82.1,
|
||||
"average_accuracy_of_diagnosis": 88.5,
|
||||
"average_waiting_time_at_clinic": 65.3,
|
||||
"average_doctor_expertise": 90.2,
|
||||
"average_clinic_cleanliness": 78.4
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## تأیید نظرات
|
||||
نظرات با `approved=0` ذخیره میشوند.
|
||||
ادمین آنها را از `GET /api/v1/clinicpro/unverified-comments/{doctorId}` میبیند.
|
||||
سپس با `PATCH /api/v1/clinicpro/unverified-comments/{commentId}` تأیید میکند.
|
||||
|
||||
---
|
||||
|
||||
## اعتبارسنجی مقادیر Rating
|
||||
هر معیار باید بین 0 تا 100 باشد:
|
||||
```php
|
||||
#[Assert\Range(min: 0, max: 100)]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## مجوزها
|
||||
```
|
||||
POST /api/v1/clinicpro/rate → احراز هویتشده
|
||||
PATCH /api/v1/clinicpro/rate/{uuid} → owner (هر کاربر فقط یک امتیاز برای هر دکتر)
|
||||
DELETE /api/v1/rate/doctor/{uuid} → ROLE_ADMIN
|
||||
GET /api/v1/clinicpro/rate/{uuid} → owner (امتیاز کاربر برای دکتر مشخص)
|
||||
GET /api/v1/clinicpro-comment/doctor-rate/{uuid} → عمومی (آمار کلی دکتر)
|
||||
POST /api/v1/clinicpro/comment → احراز هویتشده
|
||||
PATCH /api/v1/clinicpro/comment/{uuid} → owner یا ROLE_ADMIN
|
||||
DELETE /api/v1/clinicpro/comment/{uuid} → owner یا ROLE_ADMIN
|
||||
GET /api/v1/clinicpro/comment/{uuid} → احراز هویتشده
|
||||
GET /api/v1/clinicpro/comments/{doctorId} → احراز هویتشده (فقط approved)
|
||||
GET /api/v1/clinicpro/unverified-comments/{doctorId} → ROLE_ADMIN
|
||||
PATCH /api/v1/clinicpro/unverified-comments/{id} → ROLE_ADMIN (تأیید/رد نظر)
|
||||
POST /api/v1/clinicpro/like → احراز هویتشده
|
||||
PATCH /api/v1/clinicpro/like/{uuid} → owner
|
||||
```
|
||||
@@ -0,0 +1,99 @@
|
||||
# تسک ۱۲: ماژول امتیاز و نظرات
|
||||
|
||||
## توضیح
|
||||
سیستم امتیازدهی (rate) و نظرات (comment) و لایک کاربران برای دکترها،
|
||||
شامل تأیید نظرات توسط ادمین.
|
||||
|
||||
## Endpoint ها (واقعی از Drupal)
|
||||
|
||||
### امتیازدهی (Rate)
|
||||
|
||||
| متد | مسیر | توضیح | نیاز به Auth |
|
||||
|-----|------|-------|-------------|
|
||||
| POST | `/api/v1/clinicpro/rate` | ثبت امتیاز جدید | بله |
|
||||
| PATCH | `/api/v1/clinicpro/rate/{uuid}` | ویرایش امتیاز | بله (Owner) |
|
||||
| DELETE | `/api/v1/rate/doctor/{uuid}` | حذف امتیاز | بله (Admin) |
|
||||
| GET | `/api/v1/clinicpro/rate/{uuid}` | امتیاز من برای دکتر | بله |
|
||||
| GET | `/api/v1/clinicpro-comment/doctor-rate/{doctor_uuid}` | آمار کلی امتیازهای دکتر | خیر |
|
||||
|
||||
### نظرات (Comment)
|
||||
|
||||
| متد | مسیر | توضیح | نیاز به Auth |
|
||||
|-----|------|-------|-------------|
|
||||
| POST | `/api/v1/clinicpro/comment` | ثبت نظر جدید | بله |
|
||||
| PATCH | `/api/v1/clinicpro/comment/{uuid}` | ویرایش نظر | بله (Owner/Admin) |
|
||||
| DELETE | `/api/v1/clinicpro/comment/{uuid}` | حذف نظر | بله (Owner/Admin) |
|
||||
| GET | `/api/v1/clinicpro/comment/{uuid}` | دریافت یک نظر | بله |
|
||||
| GET | `/api/v1/clinicpro/comments/{doctorId}` | لیست نظرات دکتر (با page/limit) | بله |
|
||||
| GET | `/api/v1/clinicpro/unverified-comments/{doctorId}` | نظرات تأییدنشده (با page/limit) | بله (Admin) |
|
||||
| PATCH | `/api/v1/clinicpro/unverified-comments/{commentId}` | تأیید/رد نظر | بله (Admin) |
|
||||
|
||||
### لایک (Like)
|
||||
|
||||
| متد | مسیر | توضیح | نیاز به Auth |
|
||||
|-----|------|-------|-------------|
|
||||
| POST | `/api/v1/clinicpro/like` | ثبت لایک/دیسلایک | بله |
|
||||
| PATCH | `/api/v1/clinicpro/like/{uuid}` | ویرایش لایک | بله (Owner) |
|
||||
|
||||
## پیشنیازها
|
||||
- تسک ۰۱، ۰۲، ۰۵ (Doctor)
|
||||
|
||||
## زمان تخمینی
|
||||
۸ تا ۱۰ ساعت
|
||||
|
||||
---
|
||||
|
||||
## ⚠ نام فیلدهای Request (متفاوت از DB!)
|
||||
|
||||
| فیلد در Request | معادل در DB | توضیح |
|
||||
|----------------|------------|-------|
|
||||
| `correct_diagnosis` | `accuracy_of_diagnosis` | دقت تشخیص (0-100) |
|
||||
| `doctor_skill` | `doctor_expertise` | مهارت پزشک (0-100) |
|
||||
| `behavior_doctor` | `doctor_behavior` | برخورد پزشک (0-100) |
|
||||
| `office_cleaning` | `clinic_cleanliness` | نظافت مطب (0-100) |
|
||||
| `time_in_office` | `waiting_time_at_clinic` | زمان انتظار (0-100) |
|
||||
| `doctor` | `doctor_id` | شناسه دکتر |
|
||||
| `rate` | `starts` | امتیاز ستاره (ذخیره محاسبهشده) |
|
||||
|
||||
---
|
||||
|
||||
## نمونه واقعی Request — POST /api/v1/clinicpro/rate
|
||||
```json
|
||||
{
|
||||
"correct_diagnosis": 100,
|
||||
"doctor_skill": 100,
|
||||
"behavior_doctor": 100,
|
||||
"office_cleaning": 100,
|
||||
"time_in_office": 100,
|
||||
"doctor": 1,
|
||||
"rate": 2
|
||||
}
|
||||
```
|
||||
|
||||
## نمونه واقعی Request — PATCH /api/v1/clinicpro/rate/{uuid}
|
||||
```json
|
||||
{
|
||||
"correct_diagnosis": 50,
|
||||
"doctor_skill": 60,
|
||||
"behavior_doctor": 70,
|
||||
"office_cleaning": 80,
|
||||
"time_in_office": 90,
|
||||
"doctor": 1,
|
||||
"rate": 2
|
||||
}
|
||||
```
|
||||
|
||||
## نمونه واقعی Request — PATCH /api/v1/clinicpro/unverified-comments/{uuid}
|
||||
_(تأیید نظر — بدنه خالی یا فقط `approved`)_
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## نکات مهم
|
||||
- هر کاربر فقط **یک امتیاز** برای هر دکتر میتواند ثبت کند (UNIQUE user_id + doctor_id)
|
||||
- نظرات با `approved=0` ذخیره میشوند و باید توسط ادمین تأیید شوند
|
||||
- لایک فقط برای **نظرات** است (نه بلاگ یا دکتر)
|
||||
- هر کاربر فقط **یک لایک** برای هر نظر میتواند ثبت کند
|
||||
- URL کامنتهای تأییدنشده: `{doctorId}` در URL است اما فقط ROLE_ADMIN بررسی میشود
|
||||
Reference in New Issue
Block a user