8.9 KiB
افزودن اسم سایت شهر به پیامک کد تأیید (OTP) — بهصورت اختیاری
پروژه
clinicpro (Backend) — cross-repo؛ پرامپت همتای frontend: nobat724_front/.claude/prompt/otp-sms-site-name.md
زمینه
سایت عمومی چند-دامنهای است (هر شهر یک دامنه: yasuj-nobat.ir، ahvaz-nobat.ir، ...). هر شهر در جدول cities فیلد site_name دارد (مثل «یاسوج نوبت»). الان پیامک کد تأیید ورود ثابت است: کد تأیید شما: {code} و اسم سایت شهر ندارد.
هدف: بتوان اختیاری اسم سایتِ شهرِ درخواستکننده را در پیامک OTP نمایش داد — مثلاً «کد تأیید شما در یاسوج نوبت: ۱۲۳۴۵». اختیاریبودن از طریق قالب قابلویرایش پنل SMS کنترل میشود: اگر ادمین {site} را در متن قالب بگذارد نمایش داده میشود، نگذارد نمیشود.
مشکل / هدف
- درخواست
send-codeاز دامنهی شهر به API (api.clinic-pro.ir) میرسد →Host= دامنهی API، نه شهر. پس backend خودش شهر را نمیفهمد. - راهحل: frontend دامنهی خودش را در body میفرستد؛ backend از روی
domainدر جدولcities→site_nameرا پیدا میکند و بهعنوان متغیر{site}به قالب پیامک تزریق میکند. اگر شهری پیدا نشد یاdomainنیامد → fallback به برند پیشفرض «نوبت ۷۲۴». - متغیر
{site}همیشه مقداردهی میشود؛ استفاده یا عدماستفادهاش به قالب بستگی دارد (اختیاری).
فایلهای مرتبط
| فایل | نقش |
|---|---|
src/Auth/Controller/AuthController.php |
endpoint POST /api/v1/user/send-code — خواندن domain اختیاری از body |
src/Auth/Service/OtpService.php |
ساخت و ارسال پیامک OTP — resolve اسم سایت و تزریق {site} |
src/Location/Repository/CityRepository.php |
lookup شهر بر اساس domain |
src/Location/Entity/City.php |
دارای getDomain() و getSiteName() (آماده، تغییر نمیخواهد) |
src/Sms/Entity/SmsMessageTemplate.php |
ثبت {site} بهعنوان متغیر مجاز قالب OTP |
docs/api/auth.md, docs/api/sms.md |
مستندسازی |
وضعیت فعلی
OtpService::sendCode (src/Auth/Service/OtpService.php):
public function sendCode(string $mobile): string
{
$uuid = Uuid::v4()->toRfc4122();
$code = $this->appEnv === 'dev' ? '12345' : str_pad((string) random_int(10000, 99999), 5, '0', STR_PAD_LEFT);
// ... cache set ...
if ($this->appEnv !== 'dev') {
$message = $this->smsText->resolve(SmsLog::TAG_OTP, ['code' => $code]);
$this->sms->dispatchAsync($mobile, $message, tag: SmsLog::TAG_OTP);
}
return $uuid;
}
AuthController::sendCode (src/Auth/Controller/AuthController.php:136):
$data = json_decode($request->getContent(), true) ?? [];
$mobile = trim($data['mobile'] ?? '');
// ... validation + rate limit ...
$uuid = $this->otpService->sendCode($mobile);
قالب پیشفرض OTP (src/Sms/Entity/SmsMessageTemplate.php):
SmsLog::TAG_OTP => [
'title' => 'کد تأیید ورود',
'body' => 'کد تأیید شما: {code}',
'variables' => ['code'],
],
resolver (src/Sms/Service/SmsTextResolver.php) هر {key} را با str_replace جایگزین میکند — پس {site} بدون تغییر resolver کار میکند.
وظایف
۱. OtpService — resolve اسم سایت و تزریق {site}
CityRepositoryرا به constructor تزریق کن.- امضای متد را به
sendCode(string $mobile, ?string $domain = null)تغییر بده (پارامتر اختیاری → سازگاری عقبرو). - اسم سایت را resolve کن و به متغیرهای قالب اضافه کن:
public function sendCode(string $mobile, ?string $domain = null): string
{
// ... code + cache بدون تغییر ...
if ($this->appEnv !== 'dev') {
$site = $this->resolveSiteName($domain);
$message = $this->smsText->resolve(SmsLog::TAG_OTP, ['code' => $code, 'site' => $site]);
$this->sms->dispatchAsync($mobile, $message, tag: SmsLog::TAG_OTP);
}
return $uuid;
}
private function resolveSiteName(?string $domain): string
{
$default = 'نوبت ۷۲۴';
if (!$domain) {
return $default;
}
$domain = strtolower(trim(preg_replace('#^https?://#', '', $domain)));
$domain = preg_replace('#/.*$#', '', $domain); // strip path
$city = $this->cityRepo->findOneBy(['domain' => $domain]);
return $city?->getSiteName() ?: $default;
}
- نکته:
{site}همیشه مقدار دارد؛ اگر قالب فعلی{site}نداشته باشد، این متغیر بیاثر است (اختیاریبودن حفظ میشود).
۲. AuthController::sendCode — خواندن domain اختیاری از body
$mobile = trim($data['mobile'] ?? '');
$domain = isset($data['domain']) ? trim((string) $data['domain']) : null;
// ... validation + rate limit بدون تغییر ...
$uuid = $this->otpService->sendCode($mobile, $domain ?: null);
domainاختیاری است؛ اگر نیامد، رفتار قبلی حفظ میشود.- روی
domainvalidation سختگیرانه لازم نیست (فقط برای lookup استفاده میشود، نه اجرا)، اما trim و طول محدود (مثلاً حداکثر ۲۵۳ کاراکتر) اعمال کن.
۳. SmsMessageTemplate — ثبت {site} بهعنوان متغیر مجاز OTP
قالب پیشفرض را بدون {site} نگه دار (opt-in)، اما site را به لیست variables اضافه کن تا در پنل ویرایش قالب بهعنوان متغیر قابلاستفاده نمایش داده شود:
SmsLog::TAG_OTP => [
'title' => 'کد تأیید ورود',
'body' => 'کد تأیید شما: {code}', // پیشفرض بدون site
'variables' => ['code', 'site'], // site بهعنوان متغیر اختیاری در پنل
],
- اینطوری ادمین در پنل SMS میتواند body را به
کد تأیید شما در {site}: {code}تغییر دهد تا اسم سایت نمایش داده شود — یا ندهد.
۴. مستندسازی
docs/api/auth.md: درPOST /api/v1/user/send-code، فیلد اختیاریdomainرا به request body اضافه کن (نوع string، توضیح: دامنهی شهرِ درخواستکننده برای شخصیسازی متن پیامک؛ اگر نیامد از برند پیشفرض استفاده میشود). نمونه request/response را بهروز کن.docs/api/sms.md: متغیر جدید{site}را برای قالبOTPمستند کن (مقدار =site_nameشهرِ دامنه یا «نوبت ۷۲۴»).
نکات مهم
- backward-compatible: هر دو
domain(body) و پارامترsendCodeاختیاریاند؛ کلاینتهای فعلی (پنل ادمین clinicpro، لاگین بدون domain) بدون تغییر کار میکنند و برند پیشفرض را میگیرند. - محیط
dev: پیامک ارسال نمیشود (کد ثابت12345) — این مسیر دستنخورده بماند. {site}هرگز خالی نباشد (fallback «نوبت ۷۲۴») تا اگر ادمین{site}را در قالب گذاشت، متن ناقص «... در : ...» تولید نشود.- lookup دامنه: مقدار ارسالی frontend ممکن است
yasuj-nobat.irیا باwww./https://باشد — درresolveSiteNamenormalize کن (حذف پروتکل و path). اگر لازم شدwww.را هم strip کن، اما مقادیرcities.domainبدونwwwذخیره شدهاند (مثلyasuj-nobat.ir). - پیامک OTP با پترن اپراتور: provider پترن
{site}را پشتیبانی میکند (تأییدشده). مطمئن شو ترتیب/نام متغیرها با پترن مصوب سامانهی پیامکی هماهنگ است. - تست:
ddev exec php -l src/Auth/Service/OtpService.php ddev exec php -l src/Auth/Controller/AuthController.php ddev exec php bin/console cache:clear # ارسال با domain و بدون domain را در محیط staging بررسی کن (dev پیامک نمیفرستد) - بعد از تغییر API،
docs/api/auth.mdوdocs/api/sms.mdرا در همین session بهروز کن (قانون استاندینگ پروژه).