Amirali Yaghoutiمهندس ارشد نرم‌افزار

مطالعه موردی مهندسی

پل تماس تلفن فروشگاه

تلفن پیشخوانی که به CRM وصل نیست، تلفنی است که CRM آن را نمی‌بیند. برای یک فروشگاه ساعت و جواهر با یک تلفن پیشخوان، پلی REST میان گوشی Android فروشگاه و میز اپراتور ساختم: شماره‌گیری با یک کلیک از داخل پرونده‌ی مشتری، ثبت هر تماس در همان پرونده، و رسیدن هشدار سفارش و سبد رهاشده روی گوشی. سمت سرور پیش از آنکه گوشی واقعی راه بیفتد، با یک گوشی شبیه‌سازی‌شده اثبات شد.

مسئله تجاری

اپراتورها از داخل پرونده‌ی مشتری در PWA اپراتور کار می‌کردند؛ یازده نمای کار روزانه با مشتری که هر ردیفش برگه‌ی CRM مشتری را باز می‌کند. ولی شماره را با دست روی یک گوشی جدا می‌گرفتند. پس تماس هیچ‌وقت به پرونده برنمی‌گشت: نه ثبتی از اینکه اتفاق افتاده، نه پیوندی به مشتری، و تماس ورودی هم هیچ‌چیز باز نمی‌کرد. خود گوشی روی پیشخوان در حالت Doze می‌خوابد، و کانال بدیهی برای هشدار، یعنی Web Push، در دسترس نبود: ساخت WebAPK در منطقه‌ی فروشگاه مسدود است.

نیمه‌ی دوم مسئله این بود که سایت چطور با بیرون از خودش حرف می‌زند. پیش از این کار، سرور ربات‌ها یک کلید SSH با دسترسی root به سایت داشت. هرچه کاتالوگ، داده‌ی مشتری یا گوشی را می‌خواست، از راه شل وارد می‌شد. این یعنی یک اعتبارنامه با همه‌ی مجوزها؛ دقیقاً همان چیزی که یک لایه‌ی یکپارچه‌سازی باید بی‌نیازش کند.

آنچه تحویل دادم

  • شماره‌گیری با یک کلیک از کارت مشتری. ردیف اقدام‌ها تماس، پیامک، واتس‌اپ، تلگرام و بله را پیشنهاد می‌دهد؛ فرمان شماره‌گیری برای گوشی صف می‌شود، تماس در پرونده‌ی مشتری ثبت می‌شود و تماس ورودی کارت درست را باز می‌کند.
  • صف فرمان با انقضای صریح. فرمان شماره‌گیری‌ای که گوشی ظرف دو دقیقه برنداشته باشد، به‌شکل قابل‌دیدن ناموفق علامت می‌خورد. هشداری که ظرف ده دقیقه برداشته نشود، بی‌صدا دور ریخته می‌شود؛ چون هشدار کهنه نویز است، نه خطا.
  • هشدارهای زنده روی همان صف، با نوعی دیگر. هشدار سفارش و سبد رهاشده به‌عنوان kind=alert صف می‌شود؛ در هر بازخوانی شصت‌ثانیه‌ای گوشی، پل بیست‌وپنج ردیف بالای هر تب اپلیکیشن اپراتور را تفاضل می‌گیرد و فقط تازه‌ها را می‌فرستد. صف کردن هیچ‌وقت به آنلاین بودن گوشی گره نخورده است.
  • یک همراه Android که روی پیشخوان زنده می‌ماند. سرویس با یک آلارم راه‌اندازی مجدد یک‌ثانیه‌ای و یک ضربان پانزده‌دقیقه‌ای از کنار زده شدن جان سالم به در می‌برد و موقعیت گوشی را هم گزارش می‌دهد.
  • آینه‌سازی مخاطبان در واتس‌اپ، تلگرام و بله، با یک بازپیمایش خودترمیم. وقتی معلوم شد گوشی هفت هزار مخاطب دارد و فقط دو تای آن‌ها مال ماست، همین بازپیمایش بود که کار را جمع کرد.
  • یک لایه‌ی REST با سه فضای نام: API ربات‌ها برای کاتالوگ، رسانه‌ی محصول و همگام‌سازی DNA مشتری؛ API مربوط به CRM برای مالی و مخاطبان؛ و API پل تماس. همه پشت یک هدر توکن با گزینه‌ی فهرست مجاز IP نشسته‌اند، و کلید SSH با دسترسی root سرور ربات‌ها به سایت باطل و حذف شد.

رویکرد فنی

  • سمت سرور را پیش از آنکه گوشی واقعی وجود داشته باشد، سر تا ته با یک گوشی شبیه‌سازی‌شده اثبات کردم. هر اندپوینت، صف، انقضا و تفاضل هشدار را یک کلاینت ساختگی تمرین کرد؛ گوشی واقعی وقتی رسید، در برابر سروری آزمایش شد که از قبل کار می‌کرد. با این حال یک چیز را هم پیدا کرد، که پایین‌تر آمده.
  • هشدارها به‌جای کانال push روی صف فرمان سوار می‌شوند، چون Web Push در دسترس نبود: ساخت WebAPK در منطقه‌ی فروشگاه مسدود است. استفاده‌ی دوباره از صف یعنی یک بازخوانی، یک مسیر تحویل و یک مدل انقضا برای فرمان و هشدار، و فیلد kind آن دو را از هم جدا می‌کند.
  • صف پیش از پذیرفتن کار هیچ‌وقت نمی‌پرسد گوشی آنلاین است یا نه. فرمان شماره‌گیری اپراتور در هر حال صف می‌شود؛ اگر گوشی ظرف دو دقیقه آن را برندارد، ناموفق علامت می‌خورد و اپراتور همین را می‌بیند. ضربان گوشی پانزده دقیقه است و در Doze می‌خوابد، پس «آنلاین» هیچ‌وقت واقعیتی نیست که سرور بتواند به آن اعتماد کند؛ انقضا جواب صادقانه است.
  • همان ماهی که گوشی راه افتاد، فهمیدم انقضای دودقیقه‌ای هیچ‌وقت اجرا نشده بود. صف ردیف‌ها را به وقت محلی سایت مهر می‌زد و با NOW() در MySQL مقایسه می‌کرد که UTC است؛ مقایسه بی‌صدا هیچ‌وقت درست درنمی‌آمد. در سه سال حتی یک فرمان منقضی نشده بود. همان ماه اصلاح شد.
  • لایه‌ی REST دسترسی شل را با سه فضای نام باریک پشت یک هدر توکن به‌علاوه‌ی فهرست مجاز IP جایگزین می‌کند؛ هر ربات مسیری به همان چیزی دارد که لازم دارد و نه بیشتر. با برقرار شدن این مسیرها، کلید SSH با دسترسی root سرور ربات‌ها به سایت باطل و حذف شد.

نتیجه و شواهد

سمت سرور اول سر تا ته با یک گوشی شبیه‌سازی‌شده اثبات شد، و گوشی واقعی، با Android 16، همان ماه به‌طور کامل راه‌اندازی شد: یک زنگِ صف‌شده، کارت درست مشتری را خودکار باز کرد. شواهد من همین است. حجم تماس، نرخ پاسخ یا زمان صرفه‌جویی‌شده را اندازه نگرفته‌ام و عددی را که جمع نکرده‌ام، نسبت نمی‌دهم.

اهمیت برای کارفرما

پیشخوانی که از داخل پرونده‌ی مشتری شماره می‌گیرد، هر تماس را بی‌آنکه کسی چیزی تایپ کند، بخشی از سابقه می‌کند؛ و گوشی‌ای که برای سفارش تازه یا سبد رهاشده به اپراتور زنگ می‌زند، فاصله‌ی میان رویداد و تماس را کوتاه می‌کند. ارزش کم‌سروصداتر، لایه‌ی یکپارچه‌سازی است: سایت یک فروشگاه حالا می‌تواند با ربات‌ها و گوشی‌اش حرف بزند، بی‌آنکه کلید root روی یک ماشین دیگر نشسته باشد.

خلاصه-اجرایی-پروژه

خلاصه اجرایی خوانا

خلاصه_اجرایی {
  پروژه: "پل تماس تلفن فروشگاه"
  کارفرما: "یک فروشگاه ساعت و جواهر با یک تلفن پیشخوان"
  گوشی: "همراه Android؛ با آلارم راه‌اندازی یک‌ثانیه‌ای و ضربان
         پانزده‌دقیقه‌ای از کنار زده شدن جان به در می‌برد؛ در Doze"
  شماره‌گیری: "از کارت مشتری صف می‌شود؛ ظرف دو دقیقه منقضی و
               ناموفق علامت می‌خورد؛ هرگز به آنلاین بودن گوشی گره نخورده"
  هشدار: "سفارش + سبد رهاشده به‌عنوان kind=alert روی همان صف؛
          تفاضل ۲۵ ردیف بالای هر تب اپراتور در بازخوانی ۶۰ ثانیه‌ای؛
          ظرف ده دقیقه منقضی و بی‌صدا دور ریخته می‌شود"
  REST: "سه فضای نام (ربات‌ها، CRM، پل تماس)؛ هدر توکن +
         فهرست مجاز IP؛ کلید root SSH باطل و حذف شد"
  اثبات: "گوشی شبیه‌سازی‌شده سر تا ته، بعد گوشی واقعی"
  باگ: "انقضا وقت محلی را با NOW() در MySQL (UTC) مقایسه می‌کرد؛
        سه سال هیچ‌وقت اجرا نشد؛ اصلاح شد"
}

ارزش حرفه‌ای این پروژه

چیزی که دوست دارم بازبین نگاهش کند، گوشی شبیه‌سازی‌شده است. اول نوشتن سمت گوشی یعنی هم‌زمان با دو مجهول کلنجار رفتن؛ اثبات سرور در برابر یک کلاینت ساختگی یعنی گوشی واقعی در برابر چیزی آزمایش شد که از قبل معلوم بود کار می‌کند.

انقضایی که هیچ‌وقت اجرا نشد، درسی است که نگه می‌دارم. مقایسه‌ی یک ستونِ مهرخورده به وقت محلی با NOW() یک خط است، بدیهی‌درست به نظر می‌رسد، و وقتی ساعت دیتابیس UTC است بی‌صدا هیچ‌وقت درست درنمی‌آید. ساعتِ دیتابیس هم یک مرز است، مثل هر سرویس‌دهنده‌ی بیرونی، و حالا با آن همین‌طور رفتار می‌کنم.