- Removed SmsTextResolver dependency from multiple services and controllers. - Introduced dispatchTemplate method in SmsService to handle SMS sending with templates. - Updated existing SMS sending logic across various services (OtpService, PreRegistrationController, ClinicInvitationService, PaymentManager, SecretaryController, RepresentationActionController) to utilize the new dispatchTemplate method. - Enhanced SmsMessageTemplate entity to include kavenegar_template and token_map fields. - Created migration to add new fields to the sms_message_templates table and populate them with existing data. - Updated SeedSmsMessageTemplatesCommand to handle new template structure. - Added documentation for the new SMS template structure and usage.
17 KiB
تبدیل همهی پیامکهای سیستمی به VerifyLookup کاوهنگار (تمپلت نامدار)
پروژه
clinicpro (backend src/Sms + سرویسهای فرستنده + پنل ادمین assets/admin/pages/SmsPage.tsx)
زمینه
همهی پیامکهای ما «اطلاعرسانی/تراکنشی» هستند. سرویس کاوهنگار برای این نوع پیامکها VerifyLookup را الزام میکند (نه ارسال متن آزاد sms/send). VerifyLookup فقط با تمپلتهای از پیشساخته و تأییدشده در پنل کاوهنگار کار میکند و متغیرها را در جایگاههای token, token2, token3, token10, token20 جایگذاری میکند.
- مستند REST:
https://kavenegar.com/rest.html#sms-Lookup - مستند SDK:
https://kavenegar.com/sdk.html#php
وضعیت فعلی کد: KavehNegarProvider از قبل هر دو متد را دارد — send() (متن آزاد via sms/send.json) و sendTemplate() (VerifyLookup via verify/lookup.json). اما فقط OTP از lookup استفاده میکند؛ بقیهی همهی پیامکها با send() (متن آزاد) میروند. همچنین موجودیت SmsMessageTemplate (متن ویرایشپذیر هر تگ) نه نام تمپلت کاوهنگار دارد نه نگاشت متغیر→token.
مشکل / هدف
۱. همهی پیامکهای سیستمی (همهی تگها) باید از طریق VerifyLookup ارسال شوند، نه send() متنآزاد.
۲. هر تمپلت سیستمی باید یک نام تمپلت کاوهنگار داشته باشد (که کاربر در پنل کاوهنگار میسازد) + یک نگاشت متغیر→token slot.
۳. مسیر فرستنده متمرکز شود تا هر call-site فقط تگ + متغیرها بدهد و ارسال همیشه lookup باشد.
قید حیاتی VerifyLookup (حتماً رعایت شود)
- تمپلت باید از قبل در پنل کاوهنگار ساخته و تأیید شده باشد؛ متن ثابت پیام در پنل تعریف میشود، نه در دیتابیس ما. یعنی بعد از این تغییر، متن واقعی ارسالی = تمپلت کاوهنگار؛ فیلد
bodyدر DB فقط برای پیشنمایش ادمین و متنSmsLogمیماند و باید دستی با تمپلت پنل همراستا نگه داشته شود. - جایگاهها:
token,token2,token3→ فاصله (space) نمیپذیرند (تکمقدار بدون space)؛token10وtoken20→ فاصله مجازند. پس هر مقداری که ممکن است space داشته باشد (نام دکتر/بیمار/کلینیک/مالک/نام سایت) باید درtoken10/token20برود؛ مقادیر بدون space (کد، تاریخ۱۴۰۳/۰۵/۱۲، ساعت، username، password، لینک) درtoken/token2/token3. - newline/متن طولانی داخل token مجاز نیست؛ خطوط ثابت و شکست خط باید داخل خودِ تمپلت پنل باشند، فقط مقادیر متغیر token شوند.
KavehNegarProvider::sendTemplate()از قبل نگاشت صریح slot را پشتیبانی میکند (اگر کلیدهای$varsدقیقاً نام slotها باشند از همان استفاده میکند)، پس منطق provider نیاز به تغییر ندارد.
فایلهای مرتبط
| فایل | نقش |
|---|---|
src/Sms/Provider/KavehNegarProvider.php |
sendTemplate() (VerifyLookup) — آماده است، تغییر نده |
src/Sms/Service/SmsService.php |
dispatchAsync() / sendNow() — افزودن متد متمرکز dispatchTemplate() |
src/Sms/Service/SmsTextResolver.php |
resolve متن body برای log/preview |
src/Sms/Entity/SmsMessageTemplate.php |
افزودن kavenegar_template + token_map به فیلدها و DEFAULTS |
src/Sms/Command/SeedSmsMessageTemplatesCommand.php |
seed از DEFAULTS |
src/Sms/Controller/SmsMessageController.php |
GET/PATCH تمپلتهای سیستمی (ادمین) |
assets/admin/pages/SmsPage.tsx |
ویرایش تمپلتهای سیستمی |
src/Auth/Service/OtpService.php |
OTP (تنها جایی که الان lookup میکند) |
src/Auth/Controller/NotificationMobileController.php |
تگ notification_mobile |
src/Auth/Controller/PreRegistrationController.php |
تگ pre_registration |
src/Secretary/Controller/SecretaryController.php |
تگ secretary |
src/Payment/Service/PaymentManager.php |
تگهای payment و doctor_appointment |
src/ClinicInvitation/Service/ClinicInvitationService.php |
تگ clinic_invitation |
src/Representation/Controller/RepresentationActionController.php |
تگ welcome (الان sprintf inline) |
migrations/VersionXXithm.php |
migration برای دو ستون جدید |
docs/api/sms.md |
مستندسازی |
وضعیت فعلی
SmsService::sendNow — انتخاب بین lookup و متنآزاد
$success = ($msg->templateCode !== null)
? $provider->sendTemplate($msg->mobile, $msg->templateCode, $msg->templateVars)
: $provider->send($msg->mobile, $msg->message);
الگوی فعلی همهی call-siteها (بهجز OTP) — متنآزاد، بدون templateCode
// PaymentManager.php:318
$message = $this->smsText->resolve(SmsLog::TAG_PAYMENT, ['doctor' => $doctor, 'date' => $date]);
$this->smsService->dispatchAsync($mobile, $message, tag: SmsLog::TAG_PAYMENT);
// ClinicInvitationService.php:116، SecretaryController.php:133، NotificationMobileController.php:64،
// PreRegistrationController.php:158 — همگی همین شکل: resolve(...) سپس dispatchAsync(mobile, message, tag: TAG)
تنها جای درست (OTP) — که باید الگوی بقیه شود
// OtpService.php:80
$this->sms->dispatchAsync(
$mobile, $message,
templateCode: $this->otpTemplate, // نام تمپلت کاوهنگار از env
templateVars: ['token' => $code, 'token10' => $site], // نگاشت صریح slot
tag: SmsLog::TAG_OTP,
);
SmsMessageTemplate::DEFAULTS (فاقد نام تمپلت و نگاشت token)
SmsLog::TAG_PAYMENT => [
'title' => 'تأیید پرداخت و نوبت',
'body' => 'نوبت شما با {doctor} در تاریخ {date} ثبت و تأیید شد.',
'variables' => ['doctor', 'date'],
],
// ... بقیهی تگها مشابه
RepresentationActionController — welcome بهصورت inline (بدون تمپلت)
$this->smsService->dispatchAsync(
$mobile,
sprintf('دکتر %s عزیز، به %s خوش آمدید. ...', $name, $siteName),
tag: \App\Sms\Entity\SmsLog::TAG_WELCOME,
);
وظایف
۱. افزودن kavenegar_template و token_map به SmsMessageTemplate + DEFAULTS
در src/Sms/Entity/SmsMessageTemplate.php:
- دو ستون جدید:
#[ORM\Column(name: 'kavenegar_template', type: 'string', length: 100, nullable: true)]
private ?string $kavenegarTemplate = null;
// نگاشت متغیر منطقی → slot کاوهنگار: مثلاً {"code":"token","site":"token10"}
#[ORM\Column(name: 'token_map', type: 'json')]
private array $tokenMap = [];
- getter/setter، افزودن به constructor و
toArray(). - در هر آیتم
DEFAULTSدو کلید اضافه کن:kavenegar_templateوtoken_map. مقادیر پیشنهادی (با رعایت قید space):
| تگ | نام تمپلت کاوهنگار (پیشنهادی) | token_map |
|---|---|---|
otp |
clinicpro-otp |
{ "code": "token", "site": "token10" } |
notification_mobile |
clinicpro-notify-code |
{ "code": "token" } |
payment |
clinicpro-payment |
{ "doctor": "token10", "date": "token2" } |
clinic_invitation |
clinicpro-clinic-invite |
{ "clinic": "token10", "link": "token" } |
pre_registration |
clinicpro-pre-register |
{ "username": "token", "password": "token2", "link": "token3" } |
secretary |
clinicpro-secretary |
{ "owner": "token10", "username": "token", "link": "token3" } |
doctor_appointment |
clinicpro-doctor-appt |
{ "patient": "token10", "date": "token2", "time": "token3" } |
welcome |
clinicpro-welcome |
{ "name": "token10", "site": "token20" } |
نامها را نهایی با کاربر چک کن؛ همین نامها باید در پنل کاوهنگار ساخته شوند (وظیفهی ۶).
- سپس migration بساز (
ddev exec php bin/console doctrine:migrations:diff) و seed را طوری کن که رکوردهای موجود هم دو ستون جدید را بگیرند (درSeedSmsMessageTemplatesCommandعلاوه بر ساخت رکورد جدید، اگر رکورد هست ولیkavenegar_templateخالی است، ازDEFAULTSپرش کن — یا یک migration دادهایUPDATEبرای پرکردن مقادیر).
۲. متد متمرکز SmsService::dispatchTemplate(tag, mobile, vars)
یک متد واحد که همهی call-siteها از آن استفاده کنند:
/** @param array<string,string|int> $vars متغیرهای منطقی (مثل ['doctor'=>..,'date'=>..]) */
public function dispatchTemplate(string $tag, string $mobile, array $vars = []): void
{
$tpl = $this->messageTemplateRepo->findByTag($tag);
$kaveTemplate = $tpl?->getKavenegarTemplate()
?? SmsMessageTemplate::DEFAULTS[$tag]['kavenegar_template'] ?? null;
$tokenMap = $tpl?->getTokenMap()
?: (SmsMessageTemplate::DEFAULTS[$tag]['token_map'] ?? []);
// متن body برای log/preview (تمپلت واقعی سمت کاوهنگار است)
$message = $this->textResolver->resolve($tag, $vars);
if ($kaveTemplate !== null && $tokenMap !== []) {
// نگاشت متغیر منطقی → slot کاوهنگار
$slotVars = [];
foreach ($tokenMap as $logicalKey => $slot) {
if (array_key_exists($logicalKey, $vars)) {
$slotVars[$slot] = (string) $vars[$logicalKey];
}
}
$this->dispatchAsync($mobile, $message, templateCode: $kaveTemplate, templateVars: $slotVars, tag: $tag);
} else {
// fallback فقط اگر تمپلت کاوهنگار تعریف نشده باشد
$this->dispatchAsync($mobile, $message, tag: $tag);
}
}
SmsServiceبایدSmsMessageTemplateRepositoryوSmsTextResolverرا inject کند.- قید space را رعایت کن: اگر مقداری که به
token/token2/token3میرود شامل space باشد، کاوهنگار خطا میدهد. token_map درDEFAULTSطوری چیده شده که مقادیر دارای space درtoken10/token20بروند؛ هنگام افزودن تگ جدید همین را رعایت کن.
۳. مهاجرت همهی call-siteها به dispatchTemplate
هر جای زیر را از الگوی «resolve() + dispatchAsync(..., tag:)» به یک فراخوانی dispatchTemplate(tag, mobile, vars) تبدیل کن (متغیرهای منطقی همان کلیدهای {...} بدنهاند):
src/Auth/Service/OtpService.php:80—dispatchTemplate(TAG_OTP, $mobile, ['code'=>$code,'site'=>$site])(شاخهی envotpTemplateو fallback حذف شود؛ منبع نام تمپلت حالا DB/DEFAULTS است).src/Auth/Controller/NotificationMobileController.php:64src/Auth/Controller/PreRegistrationController.php:158src/Secretary/Controller/SecretaryController.php:133src/Payment/Service/PaymentManager.php:318(payment) و:328(doctor_appointment)src/ClinicInvitation/Service/ClinicInvitationService.php:116
۴. تگ welcome را از inline به تمپلت تبدیل کن
در src/Representation/Controller/RepresentationActionController.php (دو جای ~313 و ~388) بهجای sprintf(...) از dispatchTemplate(TAG_WELCOME, $mobile, ['name'=>$name,'site'=>$siteName]) استفاده کن. مطمئن شو TAG_WELCOME در DEFAULTS وجود دارد (در وظیفهی ۱ اضافه شد).
۵. پنل ادمین: نمایش/ویرایش نام تمپلت کاوهنگار
src/Sms/Controller/SmsMessageController.php: در پاسخ GET،kavenegar_templateوtoken_mapرا هم برگردان (ازtoArray())؛ در PATCH اجازهی ویرایشkavenegar_template(و در صورت لزومtoken_map) را بده.assets/admin/pages/SmsPage.tsx: فیلد «نام تمپلت کاوهنگار» را در فرم ویرایش هر تمپلت سیستمی نشان بده و ذخیره کن. یک راهنمای کوتاه بگذار که این نام باید دقیقاً با تمپلت ساختهشده در پنل کاوهنگار یکی باشد.- مستند API (
docs/api/sms.md): تغییر response/بدنهیadmin/sms/messagesرا ثبت کن.
۶. فهرست تمپلتهایی که کاربر باید در پنل کاوهنگار بسازد
در پایان، این جدول را (با نامهای نهایی) به کاربر بده تا در پنل کاوهنگار بسازد؛ متن هر تمپلت باید با body همان تگ همراستا باشد و جایگاهها با %token... مطابق token_map:
| تمپلت | نمونه متن پنل (جایگاهها با token_map) |
|---|---|
clinicpro-otp |
کد تأیید شما: %token (سایت: %token10) |
clinicpro-payment |
نوبت شما با %token10 در تاریخ %token2 ثبت و تأیید شد. |
| … | (برای هر تگ بر اساس body و token_map) |
نکات مهم
- متن واقعی ارسالی از پنل کاوهنگار میآید، نه از
bodyدیتابیس. پسbodyرا فقط برای preview/log نگهدار و در پنل هم همان متن را بساز؛ اگر ادمینbodyرا عوض کند، متن ارسالی عوض نمیشود مگر تمپلت پنل هم عوض شود — این را در UI به ادمین گوشزد کن. - قید space در token/token2/token3 مهمترین علت خطای ۴۳۱/۴۱۸ کاوهنگار است؛ مقادیر دارای فاصله را حتماً به
token10/token20بده (token_map پیشفرض این را رعایت کرده). - لینکها: بعضی تمپلتهای VerifyLookup لینک را فقط اگر تمپلت با لینک تأیید شده باشد میپذیرند؛ برای تگهای دارای
{link}(clinic_invitation, pre_registration, secretary) هنگام ساخت تمپلت در پنل، تأیید لینک را بگیر. SDKکاوهنگار (kavenegar/php) لازم نیست: provider فعلی مستقیمverify/lookup.jsonرا باHttpClientصدا میزند و درست است. اگر کاربر صراحتاً SDK بخواهد، میتوانcomposer require kavenegar/phpکرد و provider را بازنویسی کرد، ولی پیشفرض همین HttpClient بماند (بدون وابستگی جدید).TAG_USER_TEMPLATE(پنل پیامک کلینیکها،SmsController): اینها پیامکهای متنآزادِ کاربرساخته باSmsTemplate.providerCodeهستند و از قبل مسیر تمپلتدار دارند؛ در دامنهی این تغییر نیستند. اگر کاربر میخواهد آنها هم اجباری lookup شوند، جداگانه بپرس (متن آزاد کاربر با VerifyLookup سازگار نیست مگر هر متن یک تمپلت تأییدشده داشته باشد).env KAVENEGAR_OTP_TEMPLATE: بعد از انتقال نام تمپلت به DB، این env و binding$otpTemplateدرconfig/services.yaml:59را حذف یا به fallback تبدیل کن (تا جای واحدِ حقیقت، DB باشد).- بعد از تغییر Entity:
doctrine:migrations:diffسپسmigrate. بعد از تغییر API:docs/api/sms.md. تست:ddev exec php bin/console app:seed-sms-templates(یا نام واقعی seed) و بررسی ارسال با یک تگ (مثلاً OTP) در محیط تست. - edge case: اگر تگی
token_mapنداشت یاkavenegar_templateخالی بود،dispatchTemplateباید بهصورت امن fallback کند (نه crash) — ولی هدف این است که همهی تگهای سیستمی مقدار داشته باشند.