ویرگول
ورودثبت نام
سید محمد مهدی حسینی
سید محمد مهدی حسینی
سید محمد مهدی حسینی
سید محمد مهدی حسینی
خواندن ۲۶ دقیقه·۴ ماه پیش

معرفی و بررسی معماری سه نرم‌افزار فروشگاهی «متن‌باز» SockShop، OpenCart و Saleor

در این نوشتار تلاش می‌گردد تا از روی مطالعه و تحلیل کد مشخصات معماری در سه نرم‌افزار فروشگاهی اوپن سورس  به نامهای sockshop و opencart  و saleor شناسایی و در صورت امکان با هم مقایسه گردند. هرچند در ابتدا باید اشاره کرد همانطور که سیمون براون می‌گوید:

«کد به تنهایی نمی‌تواند تمام داستان را تعریف نماید..»

در این پروژه اصلی‌ترین منبع اطلاعاتی ما برای بررسی دو نرم‌افزار قطعه کدهای داخل دو نرم‌افزار است .البته در لابلای مباحث نیز از مستندات مربوط به نرم‌افزارهای فوق نیز استفاده شده است.

اهداف :

در این پژوهش تلاش گردیده است معماری نرم‌افزارها از نظر ویژگیهای زیر بررسی گردند.

1-     رویکرد و الگوی طراحی معماری نرم‌افزار

2-     معماری داده نرم‌افزار

3-     معماری ارتباطات و پیام‌رسانی بین مدولهای مختلف نرم افزار

4- سایر ویژگیهای معماری نرم‌افزارها و مقایسه تطبیقی آنها با یکدیگر

نرم افزار sockhop

نرم افزار sockshop یک فروشگاه انلاین غیرحرفه‌ای برای فروش جوراب هست که با هدف نمایش و آزمایش معماری میکروسرویس طراحی شده است. نرم افزار دارای 15 نود است. ولی میکروسرویسهای اصلی آن به شرح زیر هستند.

میکروسرویسهای نرم افزار sockshop (مرجع: مستندات رسمی نرم افزار)
میکروسرویسهای نرم افزار sockshop (مرجع: مستندات رسمی نرم افزار)
مشخصات میکروسرویسهای نرم افزار sockshop
مشخصات میکروسرویسهای نرم افزار sockshop

نصب و استقرار نرم‌افزار Sockshop:

نرم افزار sockshop یک نرم افزار اوپن سورس هست که بر روی گیتهاب نسخه‌های مختلفی از آن موجود است. برای استقرار نرم‌افزار فعلی از گیتهاب به آدرس ocp-power-demos/sock-shop-demo: A multiarchitecture port of the Sock Shop Microservices application   شاخه master بر روی سیستم کلون گردید. سپس در پوشه deploy از نرم افزار که فایل docker.compose.yml موجود بود ابتدا دستور docker compose build و سپس دستور docker compose up اجرا شد تا نرم افزار نصب گردد. در همان پوشه نیز یک فایل docker.compose.monitoring.yml هست که از طریق آن سیستم مونیتورینگ نرم افزار با دستور docker compose -f docker-compose.monitoring.yml up -d  اجرا گردید.

تصویری از رابط کاربری نرم افزار sockshop
تصویری از رابط کاربری نرم افزار sockshop

چالشهای نصب نرم‌افزار sockshop:

برای نصب نرم‌افزار باید پورتهای 80 و 8080 آزادسازی گردد. برای اینکار می‌توان با دستور netstat -ano در ترمینال برنامه‌هایی که آن پورت را اشغال نموده‌اند جستجو و سپس با استفاده از دستور taskkill در ترمینال در حالت administrator پورت مذکور را آزاد نمود. همچنین برخی وابستگیهای موردنیاز پروژه از سایتهایی تامین می‌گردند که برای IPهای ایران تحریم هستند و با استفاده از dns های سایتهایی نظیر شکن در حالت غیررایگان تنها امکان دانلود آنها فراهم می‌گردد. برخی خطاها و تنظیمات جزئی نیز با وبرایش فایل docker.compose.yml برطرف گردید.

اجرای نرم‌افزار sockshop:

بپس از اجرای کانینر برنامه بر روی داکر نرم افزار از طریق مرورگر و از آدرس a.localhost رابط کاربری نرم‌افزار در دسترس خواهد بود. البته به جای کلمه a هر کلمه دیگری می‌تواند جایگزین گردد؛ ولی عبارت localhost خالی ما را به صفحه‌ای که مربوط به راهنمای داکر هست می‌برد. در صورت نصب مونیتورینگ هم آدرس localhost:9090 صفحه Prometheus و آدرس localhost:3000 صفحه Grafana را باز می‌کند. آدرس localhost:8080 نیز صفحه traefik را باز می‌کند. به منظور مشاهده چگونگی عملکرد نرم‌افزار در برابر بارهای ناشی از ترافیک شبکه‌ای بارهای ترافیکی به صورت مصنوعی با locust ایجاد گردید.

ویژگیهای معماری نرم‌افزار sockshop:

  • استفاده از یک Reverse Proxy به عنوان سرور واسطه‌ای بین کاربران و سرورهای اصلی که درخواستهای کاربران را به سرویسهای مناسب هدایت می‌کند و ضمن افزایش امنیت، مدیریت بار و کنترل ترافیک بین سرویس‌ها را بر عهده دارد.

  • استفاده از CI travis به عنوان سرویس یکپارچه‌سازی مداوم

  • وجود پوشه‌های مربوط به تست نرم‌‌افزار در کنار هر میکروسرویس

  • استفاده از دیتابیسهای جداگانه برای هر میکروسرویس

نمایش مدلهای ارتباطی و پایگاه داده برای هر میکروسرویس (مرجع: صفحه رسمی نرم‌افزار در گیتهاب)
نمایش مدلهای ارتباطی و پایگاه داده برای هر میکروسرویس (مرجع: صفحه رسمی نرم‌افزار در گیتهاب)

مدلهای ارتباطی بین سرویس‌ها در نرم افزار Sock Shop

در معماری میکروسرویس‌ها، هر سرویس به طور مجزا و مستقل عمل می‌کند، اما برای انجام وظایف پیچیده باید با سایر سرویس‌ها ارتباط داشته باشد.  Sock Shop از سه روش اصلی برای ارتباط بین سرویس‌ها استفاده می‌کند:

 1-     ارتباط هم‌زمان به طریق RESTful API مبتنی بر HTTP  :  در این روش نرم‌افزار وقتی بدان می‌رسد منتظر می‌ماند تا پاسخ را دریافت نماید و خطهای بعدی برنامه را پردازش نمی‌نماید. در برنامه sockshop کلیه درخواست‌ها ابتدا با استفاده از future غیرهمزمان ارسال می‌گردند. ولی وقنی نرم‌افزار به پاسخ آن نیاز پیدا می‌کند با دستور get درخواست‌ها همزمان می‌گردد. برای مثال به  بخشی از کد فایل orderscontroller.java  از میکروسرویس  orders به شرح زیر اشاره می‌گردد.

در خطوط 65 تا 73 از تصویر بالا که بخشی از فایل مذکور است ابتدا سه درخواست غیرهم‌زمان (Asynchronous) برای دریافت اطلاعات مشتری، کارت و محصول از یک سرویس خارجی ارسال می‌کند. Item.customer  آدرس سرویس مشتری است و getResource()  یک درخواست HTTP GET است. به علت استفاده از Future پردازش ابتدا منتظر دریافت پاسخ نمی‌ماند. اما وقتی به خط 76 می‌رسد چون  متغیر amount باید مقداردهی شود و به جواب آن نیاز دارد برنامه متوقف می‌گردد و منتظر پاسخ می‌ماند. لذا درخواست همزمان می‌گردد و درخواستهای زیاد می‌تواند بار سنگینی روی سرویسهای مقصد ایجاد کند.

چرا نیاز به ارتباط همزمان داریم: به طور مثال برای  پردازش سفارش تا زمانی که مبلغ آن مشخص نگردد نمی‌تواند وارد مرحله پرداخت شود. برنامه باید متوقف شود تا وارد مرحله بعدی که shipment است گردد.

2-    ارتباط غیر همزمان HTTP بین سرویس‌ها:  در این حالت درخواست‌ها به سرویس مقصد ارسال می‌شوند اما پردازش منتظر دریافت پاسخ فوری نمی‌ماند.  برای این منظور از Future  و @Async  در جاوا استفاده شده است. برای مثال می‌توان به قطعه کد زیر از همان فایل قبلی اشاره کرد:

زمانی که سرویس Orders  یک سفارش جدید ثبت می‌کند، به جای تماس مستقیم و انتظار برای پاسخ از سرویس Shipping، یک درخواست غیرهمزمان HTTP در خطوط 100 تا 103 قطعه کد فوق ارسال می‌شود. درخواست به سرویس Shipping ارسال می‌شود، اما تا زمان دریافت پاسخ، پردازش متوقف نمی‌شود. پس از پردازش، سرویس Shipping نتیجه را برمی‌گرداند و پاسخ دریافت می‌شود. در این حالت پردازش‌های دیگر در سیستم متوقف نمی‌شوند و ادامه پیدا می‌کنند. اما برای مدیریت خطاهای شبکه نیاز به مدیریت زمان انتظار و خطاهای مربوط به تاخیر در پاسخگویی سرویس‌ها وجود دارد. این کار در خطوط 108 تا 113 قطعه کد فوق  با درنظر گرفتن یک مقدار زمانی برای انقضای دستور پیش‌بینی شده است.

3. ارتباط غیر همزمان با RabbitMQ: در سرویس shipment درخواستهای حمل و نقل به صورت کاملاً غیر همزمان و با RabbitMQ  انجام می‌شود، به این معنی که پردازش منتظر پاسخ از سرویس نمی‌ماند و درخواست‌ها به صف پیام‌محور ارسال می‌شوند تا پردازش در زمان مناسب انجام گیرد. این کار از طریق RabbitTemplate  انجام می‌شود که یک کلاس در SPRING AMQP است که ارتباط با RabbitMQ را ساده می‌کند. در این شیوه هیچ Future  یا get(timeout, TimeUnit.SECONDS)  وجود ندارد که باعث توقف پردازش می‌شد، اینجا هیچ توقفی وجود ندارد و درخواست‌ها کاملاً غیرهم‌زمان باقی می‌مانند. قطعه کد زیر بخشی از فایل shippingcontroller.java در میکروسرویس shipping است.

در قطعه کد فوق درخواست را  به یک صف مشخص، مانند shipping-task، اضافه می‌کند. در لحظه‌ی ارسال پیام، پردازش ادامه می‌یابد بدون اینکه منتظر تأیید یا پاسخ باشد. در نتیجه، این مدل باعث افزایش مقیاس‌پذیری و عملکرد بهینه سیستم می‌شود. برای بررسی وضعیت RabbitMQ، این نرم‌افزار از یک مکانیسم Health Check  استفاده می‌کند که با اجرای rabbitTemplate.execute(ChannelCallback<String>)  اطلاعات سرور RabbitMQ را دریافت می‌کند تا اطمینان حاصل شود که پیام‌ها به درستی پردازش خواهند شد. همچنین، اگر صف پیام در دسترس نباشد، سیستم به‌طور خودکار وضعیت را مدیریت می‌کند و خطای AmqpException  را کنترل می‌کند. این معماری باعث می‌شود که فرآیند حمل‌ونقل بدون توقف پردازش و به‌صورت کاملاً غیرهم‌زمان انجام شود، به طوری که پیام‌ها ارسال می‌شوند و در زمان مناسب توسط مصرف‌کنندگان RabbitMQ پردازش خواهند شد. این مدل به‌خصوص در معماری میکروسرویس بسیار کارآمد است، زیرا مانع از ایجاد وابستگی‌های هم‌زمان بین سرویس‌های مختلف می‌شود و اجازه می‌دهدRabbitMQ  به عنوان واسطه‌ای برای مدیریت صف‌های پردازش سفارشات عمل می‌کند.

Queue-Master  مسئول مدیریت صف‌ها در RabbitMQ است.  این سرویس نظارت می‌کند که پیام‌ها پردازش شوند و از ازدحام در صف‌ها جلوگیری شود.  همچنین، زمانی که بار پردازشی بالا باشد، می‌تواند پیام‌ها را مدیریت کند تا سرویس Shipping با فشار زیاد مواجه نشود.

معماری داده‌های برنامه:

با توجه به طراحی نرم‌افزار بر مبنای میکروسرویس هر سرویس دارای یک دیتابیس جداگانه هست. برای سرویس کاربر، سبد خرید و سفارشات از دیتابیس MongoDB استفاده شده و برای سرویس کاتالوگ که سرویس نمایش کالاهای موجود در فروشگاه است از دیتابیس MySQL استفاده شده است.

دیتابیسهای انتخاب شده چه ویژگیهایی دارند؟

علت انتخاب نوع دیتابیسهای فعلی برای فروشگاه چیست؟

نحوه فراخوانی اطلاعات از دبتابیس چگونه است؟

معرفی سیستم مدیریت پایگاه داده MONGODB برای میکروسرویسهای سفارشات، کاربران و سبد خرید: برای مشاهده ساختار دیتابیس در این میکروسرویس‌ها با توجه به نگارش استفاده شده از پایگاه داده که یک نگارش نسبتاً قدیمی بود از نرم‌افزار MongoDB Compass نگارش 1.18 استفاده گردید. در تصویر زیر دیتابیسهای موجود برای میکروسرویس کاربران و جداول آن که توسط MongoDb Compass کاوش شده است نشان داده شده است.

در تصویر زیر نیز محتویات جدول آدرس ها نشان داده شده است که همانطور که مشاهده می‌شود ساختار آن مشابه فایلهای JSON است که در این معماری BSON نامیده می‌شود.

نحوه ذخیره سازی آدرس در جدول آدرس ها
نحوه ذخیره سازی آدرس در جدول آدرس ها

برای ارتباط با پایگاه داده یک فایل اینترفیس تهیه شده است. در این اینترفیس که db.go نام دارد متدهای دسترسی به پایگاه داده تعریف شده است. مثلا برای میکروسرویس user   متدهای  ایجاد یک کاربر جدید، خواندن یک کاربر با نام، ایجاد آدرس، خواندن آدرس و ...... تعریف شده است. همچنین یک فایل دیگر با نام mongodb.go وظیفه مقداردهی اولیه و مشخصات پایگاه داده مانند hostname  و password و همچنین اسکیمای دیتابیس و مدیریت خطاها تعریف شده است.

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

در خط 206 دستور قید شده وابسته به نوع پایگاه داده است که MongoDB است.

استفاده از این نوع معماری که یک نوع پایگاه داده NoSQL است سرعت خواندن و نوشتن را بالا می‌برد. ضمن آنکه مقیاس‌پذیری افقی را ممکن می‌سازد و در بارهای کاری سنگین به خوبی مقیاس‌پذیر است. اما در صورتی که جداول بخواهند به یکدیگر JOIN گردند با پیچیدگیهای بیشتری روبرو است و امکان استفاده از کلید خارجی مشابه پایگاه داده‌های پشتیبانی‌کننده از  دستورهای SQL وجود ندارد.

استفاده از سیستم مدیریت پایگاه داده MySQL برای میکروسرویس catalogue: در این میکروسرویس از سه جدول استفاده شده است. جدول اول مشخصات کالا است. جدول دوم مشخصات برچسب کالا است و جدول سوم حاصل join شدن دو جدول مذکور است. برای مشاهده جداول مذکور از طریق CMD وارد کانتینر مربوط به دیتابیس مذکور شده و از طریق دستور show databases و show tables  جداول این کانتینر طبق تصویر زیر استخراج شده است.

در معماری این دیتابیس با توجه به آنکه نیازمند اتصال دو جدول به یکدیگر و استفاده از کلید خارجی بوده است از معماری MySQL  استفاده شده است تا امکان مذکور فراهم گردد. این ساختار ضمن خفظ سازگاری و پشتیبانی از ویژگیهای ACID امکان ایجاد کوئریهای پیچیده‌تر با دستورات SQL را فراهم می‌سازد. اما سرعت خواندن و نوشتن پایین‌تری دارد و فاقد مقیاس‌پذیری افقی است.

با توجه به نوع معماری نرم‌افزار که بر مبنای میکروسرویس است امکان join شدن جداول صفحه خرید به سفارشات و همین‌طور مشخصات محصول وجود ندارد. پس در هر بار که یک محصول برای سبد خرید انتخاب می‌شود باید مشخصات کاربر و محصول و قیمت آن برای میکروسرویس سبد خرید ارسال می‌گردد و این باعث افزونگی داده می‌شود که از تلعات انتخاب معماری میکروسرویس است. یعنی اطلاعاتی که در یک جدول دیگر است مجددا باید در جدول دیگری ذخیره شود. حال آنکه در صورتی که جداول به یکدیگر join می‌گردیدند کافی بود که تنها شناسه کاربر و محصول ارسال گردد. اما چون سرویس سبد خرید مستقل از سرویس سفارش و سرویس کاتالوگ محصول است چنین امری ممکن نیست. لذا حجم اطلاعات ارسالی بین مدولهای برنامه افزایش می‌یابد و نیازمند پایگاه داده‌ای هستیم که سرعت خواندن و نوشتن بالایی دارد. ویژگیی که پایگاه داده Mongo از آن برخوردار است.

معماری نرم افزار opencart:

این نرم افزار بر خلاف نرم افزار sockshop یک نرم افزار حرفه‌ای است که به صورت مونولیت البته در دو بخش فرانت و بکند به زبان php طراحی شده است. در بخش رابط کاربری از زبان جاوا اسکریپت نیز استفاده شده است  و دارای دو رابط کاربری برای ادمین و کاربری عادی می‌باشد. به منظور نصب آن نرم‌افزار از روی گیتهاب بر روی هارد دستگاه کلون گردید و سپس با داکر طبق دستورالعمل داده شده نصب گردید. اطلاعات کاربری نظیر نام کاربری و رمز عبور برای دسترسی به پایگاه داده و ادمین نرم‌افزار نیز از طریق فایل ‌docker.compose.yml در دسترس است. در نهایت پس از اجرای کانتینرها نرم‌افزار بر روی 6 کانتینر و سه پورت 80، 8080 و 3306  از طریق آدرس a.localhost برای کاربر عادی و localhost/admin برای ادمین برنامه در دسترس است.  در ادمین نرم‌افزار ضمن مونیتورینگ برنامه و رصد مشاهدات و معاملات کاربران امکان تعریف کالاها و ویرایش رابط کاربری نرم افزار مهیاست.

صفحه واسط کاربری نرم‌افزار opencart
صفحه واسط کاربری نرم‌افزار opencart
تصویری از صفحه داشبورد مدیریتی فروشگاه opencart
تصویری از صفحه داشبورد مدیریتی فروشگاه opencart

معماری نرم‌افزار:

فایلهای اصلی مربوط به نرم‌افزار در مسیر upload/catalog و در داخل چهار پوشه language، model، view و control قرار گرفته است که نشان دهنده بهره‌گیری آن از الگوی طراحی معماری MVC است.

1. مدل (model):

  •    مسئول ارتباط با پایگاه داده است.

  •    داده‌ها را پردازش و بازیابی می‌کند.

  •    اطلاعاتی مانند محصولات، دسته‌بندی‌ها، کاربران و سفارش‌ها را از دیتابیس دریافت می‌کند.

  2. کنترل (controller):

  •    درخواست‌های کاربر را دریافت کرده و پردازش می‌کند.

  •    داده‌ها را از مدل (model) می‌گیرد و به view (template) ارسال می‌کند.

 3. دید (view):

  •    اطلاعات پردازش‌شده را در قالبی که توسط فایلهای با پسوند twig تعریف شده است به صورت HTML نمایش می‌دهد.  

    برای مثال زمانی که کاربر روی محصولی کلیک می‌کند :

⬅ controller/product.phpاجرا می‌شود 

⬅ model/catalog/product.php` اطلاعات محصول را دریافت می‌کند

⬅ view/template/product.twig قالب داده‌ها را نمایش می‌دهد.

 یا برای مثال زمانی که کاربر یک سفارش ایجاد می‌نماید مراحل زیر طی می‌شود:

 لایه کنترلر: دریافت داده‌های مورد نیاز سفارش از سبد خرید و آدرس و ... و ارسال به مدل برای ذخیره از طریق قطعه کد زیر در فایل  catalog/controller/checkout/confirm.php

لایه مدل: ذخیره‌سازی کامل اطلاعات سفارش در پایگاه داده از طریق قطعه کد زیر در فایل catalog/model/checkout/order.php

لایه ویو: نمایش خلاصه‌ی سفارش به کاربر برای تایید نهایی از طریق فایل catalog/view/template/checkout/confirm.twig

بررسی پایگاه داده برنامه  opencart

واکاوی پایگاه داده برنامه نشان میدهد بر خلاف برنامه sockshop که دارای دیتابیسهای متعدد بود دارای تنها یک مرکز پایگاه داده است که در کانتینر mySQL-1 گردآوری شده است. بررسی داخل آن نشان می‌دهد که دارای 149 جدول در دیتابیس opencart است که اتصالات و روابط بسیار پیچیده‌ای دارند که تصویر شمای آنها در پیوست آمده است. دیتابیسهای داخل این پایگاه داده و برخی از مهمترین جداول آن به شرح زیر است:

یکی از مکانیسمهای به کار برده شده در این برنامه برای کاهش بار روی پایگاه داده و افزایش سرعت پاسخ‌دهی کش‌کردن (cashing) است. برای مثال در تعریف تابع   getProducts که شامل یک کوئری برای اخذ اطلاعات از پایگاه داده است بدین ترتیب از کش کردن دیتا استفاده شده است.

در تابع getProducts  از فایل product.php در مسیر  opencart\upload\catalog\model\catalog نوشته شده است:

ابتدا در خط 243 از کل متن SQL یک تابع از نوع هش (Hash) انجام می‌شود تا کلیدی یکتا برای ذخیره یا بازیابی کش ساخته شود. این کار تضمین می‌کند که هر کوئری متفاوت، کلید منحصر به‌فردی در کش داشته باشد. سپس در خط 245 تلاش می‌شود داده محصولات با این کلید از کش خوانده شود. اگر قبلاً کوئری مشابهی اجرا شده باشد، نتیجه آن در کش ذخیره شده و این خط آن را بازیابی می‌کند. شرط if بررسی می‌کند که آیا داده‌ای از کش گرفته شده است یا خیر. اگر کش وجود نداشته باشد یعنی $product_data  تهی باشد، نشان می‌دهد یا کش پاک شده است یا اولین بار است که این کوئری اجرا می‌شود.   در این صورت، کوئری SQL اجرا می‌شود و نتیجه‌اش از پایگاه داده گرفته می‌شود.

بررسی تکنیک  به کار گرفته شده برای کش:

 کش بر اساس یک کلید یکتا ساخته می‌شود. این کلید از ترکیب شناسه فروشگاه، زبان، دسته‌بندی و هش پارامترهای ورودی ساخته می‌شود. سپس داده با کلید product.{key}  ذخیره یا بازیابی می‌شود. کش در مسیر system/storage/cache/  به صورت فایل‌هایی با قالب cache.product.{hash}.<timestamp> ذخیره می‌شود. محتوای این فایل‌ها معمولاً داده‌ی serialize شده یا json  از آرایه‌های PHP هستند. هیچ مکانیزم invalidation (بی‌اعتبار کردن کش) در این کد مشاهده نمی‌شود. یعنی اگر محصول جدید اضافه شود یا یکی ویرایش شود، ممکن است کش قدیمی باشد. به‌صورت معمول، این invalidate باید هنگام تغییر داده (مثل ویرایش محصول یا موجودی) انجام شود.مثلاً در تابع editQuantity  هیچ پاک‌سازی کش انجام نشده است. بهتر است پس از تغییر داده، کش مرتبط با آن محصول یا لیست محصولات پاک شود. لذا اگر فایل کش وجود داشته باشد، از آن استفاده می‌شود حتی اگر قدیمی باشد.

نرم‌افزار  Saleor

Saleor یک پلتفرم تجارت الکترونیک متن‌باز است که برای ساخت فروشگاه‌های آنلاین طراحی شده است. این پلتفرم به صورت بدون واسط کاربری (Headless) عمل می‌کند، به این معنی که بخش بک‌اند (backend) و بخش فرانت‌اند (frontend) آن از یکدیگر جدا هستند. این جداسازی به توسعه‌دهندگان امکان می‌دهد تا از هر فناوری دلخواهی برای ساخت رابط کاربری فروشگاه (وب‌سایت، اپلیکیشن موبایل، و غیره) استفاده کنند، زیرا تمام داده‌ها و عملکردهای اصلی از طریق یک رابط برنامه‌نویسی کاربردی (API) در دسترس هستند.

Saleor تمامی عملکردهای مورد نیاز یک فروشگاه آنلاین، از جمله مدیریت محصولات، سفارشات، پرداخت‌ها و موجودی انبار را فراهم می‌آورد.

فحه رابط کاربری نرم افزار Saleor
فحه رابط کاربری نرم افزار Saleor

صفحه پنل مدیریتی فروشگاه Saleor
صفحه پنل مدیریتی فروشگاه Saleor

نظام Saleor در بخش‌های اصلی مدیریت فروشگاه مثل سفارش، سبد خرید، کاربران و ..... بر خلاف دو نرم افزار قبلی که در قالب سرویسهای متفاوتی تهیه شده بودند یه صورت یکپارچه (monolithic) است. هرچند تمام آنها در قالب مدولهای جداگانه و به صورت یک مولفه تعریف شده‌اند. ولی ارتباط آنها بر خلاف موارد قبلی که از طریق روشهای متنی مثل http تعریف می‌شد با import سایر بخشها و دسترسی به متغیرهای آنها انجام می‌گردد. این امر امکان آنکه مدولهای مختلف برنامه در زبانهای مختلف نوشته شود را از نرم‌افزار سلب نموده است. لذا تمام بخش ‌بک‌اند برنامه با زبان پایتون نوشته شده است. البته saleor همچنان در بخش ارتباط بین فرانت و بک‌اند نرم‌افزار و همچنین افزونه‌هایی نظیر مکانیسمهای پرداخت از طریق API و به روش GraphQL ارتباط برقرار می‌کند.

هسته اصلی Saleor یک برنامه واحد است که تمام منطق کسب‌وکار را در خود جای داده است. این بخش شامل موارد زیر است:

  • مدل‌های داده‌ای (Data Models): تمام داده‌های مربوط به محصولات، سفارشات، مشتریان، و انبارداری در یک پایگاه داده (PostgreSQL) ذخیره می‌شوند. این مدل‌ها به صورت یکپارچه با چارچوب جنگو (Django) مدیریت می‌شوند.

  • منطق کسب‌وکار (Business Logic): فرآیندهای اصلی مانند ثبت سفارش، مدیریت پرداخت، محاسبه قیمت و موجودی، همگی در یک کدبیس واحد قرار دارند.

  • هسته API: GraphQL API که به عنوان تنها نقطه ورودی برای ارتباط با هسته نرم‌افزار است.

نصب و استقرار

برای نصب فروشگاه  saleor دو برنامه باید نصب شود. برنامه اول بخش داشبورد مدیریتی  پروژه است که از طریق داکر  و طبق راهنمایی که در سایت رسمی پروژه به آدرس http://docs.saleor.io/quickstart/running-locally نیز آمده است نصب گردید.  برنامه دوم بخش فرانت نرم‌افزار است که پیچیدگی بیشتری دارد. چون وقتی برنامه‌ای بر روی داکر نصب می‌گردد در یک حالت انزوا قرار گرفته و با دیگر برنامه‌های نصب شده بر روی همان دستگاه از طریق localhost ارتباط برقرار نمی‌کند. در نهایت داشبورد مدیریتی از آدرس localhost:9000  و نرم‌افزار فروشگاهی از آدرس http://172.19.0.1:3000  قابل دسترس گشت.

معماری نرم‌افزار Saleor

پلتفرم تجارت الکترونیک Saleor یک سیستم API-محور و Headless است که بر پایه معماری GraphQL و با استفاده از زبان برنامه‌نویسی پایتون و فریم‌ورک جنگو (Django) توسعه یافته است. این پلتفرم از پایگاه داده PostgreSQL برای ذخیره‌سازی اطلاعات استفاده می‌کند و از Redis و Celery برای مدیریت عملیات ناهمگام بهره می‌برد. معماری آن به صورت ماژولار طراحی شده و از طریق سیستم پلاگین و وب‌هوک، قابلیت‌ ادغام با سایر سرویس‌ها را فراهم می‌آورد. Saleor از قابلیت‌های چندکاناله (Multi-channel) پشتیبانی می‌کند که امکان مدیریت چندین کانال فروش مستقل را در یک سامانه فراهم می‌سازد. در ادامه این بخش موارد فوق شرح داده می‌شود.

نمایش مدول‌بندی و مولفه های نرم افزار saleor (مرجع: deepwiki.com/saleor/saleor/1-overview)
نمایش مدول‌بندی و مولفه های نرم افزار saleor (مرجع: deepwiki.com/saleor/saleor/1-overview)

رویکردهای معماری نرم‌افزار Saleor

Saleor  از یک رویکرد ترکیبی در معماری خود بهره می‌برد که شامل هر مدلهای API-first Architecture و Event‑driven Architecture  و معماری پلاگین است.

در معماری  API-first، تمامی قابلیت‌های Saleor، از مدیریت محصولات و سفارشات گرفته تا قیمت‌گذاری و مشتریان، در وهله اول از طریق یک رابط برنامه‌نویسی کاربردی (API) در دسترس قرار می‌گیرند

. Saleor  به طور خاص از GraphQL API استفاده می‌کند که به توسعه‌دهندگان امکان دریافت دقیق داده‌های مورد نیاز خود را می‌دهد. این رویکرد، مفهوم «تجارت الکترونیک بدون رابط کاربری گرافیکی» (Headless Commerce) را پیاده‌سازی می‌کند و به توسعه‌دهندگان اجازه می‌دهد تا بخش فرانت‌اند (نمایشی) را از بخش بک‌اند (منطقی) جدا کرده و با استفاده از هر فناوری دلخواهی، فروشگاه آنلاین خود را بسازند. این امر، انعطاف‌پذیری را در یکپارچه‌سازی با سرویس‌های خارجی نظیر سیستم‌های مدیریت محتوا (CMS)، سیستم‌های برنامه‌ریزی منابع سازمانی (ERP) و ابزارهای بازاریابی فراهم می‌آورد.

علاوه بر این، Saleor یک معماری Event-driven  نیز دارد.

در معماری Event-driven هنگامی که یک رویداد مهم در سیستم رخ می‌دهد (مانند ایجاد یک سفارش جدید یا به‌روزرسانی موجودی انبار)، یک رویداد منتشر می‌شود. سرویس‌های خارجی می‌توانند به این رویدادها گوش فرا دهند و به آن‌ها واکنش نشان دهند.

به عنوان مثال، به جای اینکه یک سرویس خارجی به طور مداوم از طریق API وضعیت سفارشات را بررسی کند، Saleor با انتشار یک رویداد به نام order_created، آن سرویس را به صورت لحظه‌ای از وقوع رویداد مطلع می‌سازد. Saleor این فرآیند را از طریق وب‌هوک‌ها (webhooks) پیاده‌سازی می‌کند؛ وب‌هوک‌ها URLهایی هستند که Saleor هنگام وقوع یک رویداد خاص، داده‌های مربوط به آن را به صورت خودکار به آن‌ها ارسال می‌کند. تفاوت وب هوک با صفهای پیام‌رسان در مکانیسم آنهاست. وب‌هوک به عنوان یک مکانیسم "فشاری" (Push) عمل می‌کند. در این روش، سرور فرستنده بلافاصله پس از وقوع یک رویداد مشخص (مانند ثبت یک سفارش جدید)، اطلاعات مربوط به آن رویداد را به صورت خودکار و لحظه‌ای به یک آدرس اینترنتی (URL) از پیش تعریف‌شده ارسال می‌کند. این فرآیند شبیه به یک تماس تلفنی است؛ با وقوع یک اتفاق، سیستم به صورت خودکار و بدون نیاز به درخواست از سوی گیرنده، آن را مطلع می‌سازد. صف پیام‌رسان (Message Queue) در مقابل، صف پیام‌رسان به عنوان یک مکانیسم "کششی" (Pull) عمل می‌کند. در این روش، سرویس دریافت‌کننده (گیرنده) باید به صورت مداوم یا در فواصل زمانی مشخص، به صف پیام مراجعه کرده و وجود پیام جدید را بررسی کند. این فرآیند شبیه به بررسی صندوق پستی است؛ گیرنده به صورت دوره‌ای برای دریافت پیام‌های احتمالی مراجعه می‌کند.

به عبارت ساده‌تر مکانیسم فشاری مثل خبردادن از طریق زنگ زدن با تلفن است و مکانیسم کششی مثل یک صندوق پستی است که نامه‌ها را داخل آ ن می‌گذاریم.

معماری پلاگین، یک الگوی طراحی است که به یک سیستم مرکزی اجازه می‌دهد تا به صورت ماژولار و بدون تغییر در کد اصلی، توسعه یابد. در Saleor، این معماری به عنوان یک ستون فقرات برای انعطاف‌پذیری سیستم عمل می‌کند. هسته‌ی Saleor یک پلتفرم تجارت الکترونیک جامع است که وظایف اصلی مانند مدیریت محصولات و سفارشات را انجام می‌دهد. پلاگین‌ها، در نقش ماژول‌های جداگانه، مسئول پیاده‌سازی قابلیت‌های تخصصی و خاص هستند.

برای مثال، یک پلتفرم تجارت الکترونیک نیاز به چندین روش پرداخت (مانند کارت اعتباری، PayPal و...) دارد. به جای کدنویسی تمام این روش‌ها در هسته اصلی برنامه، هر کدام به عنوان یک پلاگین مجزا توسعه داده می‌شوند. این رویکرد دو مزیت کلیدی دارد:

  • تفکیک مسئولیت‌ها: هر پلاگین مسئول یک وظیفه مشخص است (مثلاً یک پلاگین فقط پرداخت را مدیریت می‌کند و دیگری فقط مالیات را). این امر کد را تمیز، قابل نگهداری و آسان برای عیب‌یابی می‌کند.

  • قابلیت اتصال و جدا شدن: پلاگین‌ها می‌توانند به راحتی به سیستم اضافه یا از آن حذف شوند. این قابلیت به توسعه‌دهندگان اجازه می‌دهد تا با توجه به نیازهای خود، پلاگین‌های دلخواه را فعال یا غیرفعال کنند، بدون اینکه به عملکرد کل سیستم آسیبی وارد شود.

کلاس PluginsManager نقش مدیر این سیستم را بر عهده دارد. این کلاس به عنوان یک واسط بین هسته و پلاگین‌ها عمل کرده و هماهنگی بین آن‌ها را تضمین می‌کند. PluginsManager خودش منطق پرداخت یا مالیات را انجام نمی‌دهد، بلکه فقط می‌داند که کدام پلاگین‌ها فعال هستند و چگونه باید متدهای آن‌ها را در زمان مناسب فراخوانی کند. این الگو به Saleor امکان می‌دهد که به آسانی با سرویس‌ها و ابزارهای خارجی ادغام شود و به سرعت به نیازهای جدید بازار واکنش نشان دهد.

معماری سیستم های مدیریت داده‌ای در نرم‌افزار Saleor: Saleor  برای سازماندهی اطلاعات، از یک طراحی مبتنی بر حوزه‌های اصلی (Core Domain Areas) استفاده می‌کند. این حوزه‌ها هر کدام بخش مهمی از عملکرد تجارت الکترونیک را پوشش می‌دهند، مانند:

  • مدل‌های محصولات (Product Models): شامل اطلاعاتی درباره محصولات، قیمت‌ها، تصاویر و ویژگی‌ها.

  • مدل‌های سفارشات (Order Models): مربوط به داده‌های سفارش مشتریان، وضعیت پرداخت و جزئیات ارسال.

  • مدل‌های پرداخت (Checkout Models): شامل اطلاعات مربوط به فرآیند پرداخت و سبد خرید.

  • مدل‌های تخفیف و پیشبرد خرید (Discount and Promotion Models): برای مدیریت تخفیف‌ها، کدهای تخفیف و کمپین‌های تبلیغاتی.

در تمام این داده‌ها برای ذخیره سازی به صورت پیش‌فرض از postgresql استفاده شده است اما یک تفاوت اساسی نسبت به موارد معمول نرم‌افزارهای دیگر دارد و آن اینکه به جای استفاده از SQL برای ارتباط با پایگاه داده از قراردادهای Django ORM استفاده می‌کند.

ORM  ابزاری است که به توسعه‌دهندگان اجازه می‌دهد با استفاده از کدهای پایتون با پایگاه داده ارتباط برقرار کنند، بدون اینکه نیاز به نوشتن مستقیم دستورات SQL داشته باشند. این کار توسعه را ساده‌تر و سریع‌تر می‌کند.

یکی از نکات کلیدی در معماری Saleor، پیاده‌سازی یک معماری چندمستاجری (Multi-tenant Architecture) است که از طریق طراحی مبتنی بر کانال (Channel-aware design) انجام می‌شود.

در یک معماری چندمستاجری، یک نمونه (instance) از نرم‌افزار به چندین سازمان یا "مستاجر" خدمت‌رسانی می‌کند. به عبارت دیگر، یک پایگاه کد و یک دیتابیس مشترک، به چندین فروشگاه یا کانال فروش خدمات می‌دهد.

Saleor  این مفهوم را با ایده کانال‌ها پیاده‌سازی می‌کند. یک کانال می‌تواند یک فروشگاه آنلاین، یک اپلیکیشن موبایل، یک فروشگاه فیزیکی یا حتی یک بازارچه(marketplace)  باشد. هر کانال می‌تواند:

  • قیمت‌های مخصوص به خود را داشته باشد: یک محصول می‌تواند در کانال A با قیمت متفاوت از کانال B فروخته شود.

  • موجودی انبار متفاوتی داشته باشد: برای هر کانال می‌توان موجودی انبار خاصی تعریف کرد.

  • واحد پول متفاوتی را پشتیبانی کند: کانال‌ها می‌توانند برای کشورهای مختلف و با ارزهای متفاوت تنظیم شوند.

این طراحی کانال‌محور، به کسب‌وکارها امکان می‌دهد تا چندین فروشگاه یا برند را از طریق یک پلتفرم و یک پنل مدیریت کنند، که باعث کاهش هزینه‌ها و پیچیدگی‌های مدیریتی می‌شود.

معماری تست در نرم افزار Saleor

معماری تست در نرم‌افزار Saleor بر پایه‌ی یک ساختار مدولار و سلسله‌مراتبی بنا شده است. هر اپلیکیشن جنگو در این پروژه (مانند account، product و checkout) دارای یک پوشه اختصاصی به نام tests است که به صورت مستقل قابل تست هستند. این رویکرد، تفکیک‌پذیری تست‌ها را تضمین می‌کند و به توسعه‌دهندگان اجازه می‌دهد تا هر بخش را به صورت جداگانه ارزیابی کنند. این معماری از سطوح مختلفی از تست، شامل تست‌های واحد (Unit Tests) برای بررسی کوچک‌ترین واحدهای کد، تست‌های یکپارچه‌سازی (Integration Tests) برای ارزیابی تعامل بین ماژول‌های مختلف و تست‌های سرتاسری (E2E Tests) برای شبیه‌سازی کامل فرآیندهای کاربری، بهره می‌برد. برای ایزوله کردن تست‌ها از وابستگی‌های خارجی، از ابزارهایی مانند fixtures (داده‌های نمونه) و cassettes (برای ضبط و پخش مجدد درخواست‌های شبکه) استفاده می‌شود. این روش، اجرای سریع‌تر تست‌ها را امکان‌پذیر می‌سازد و نیاز به اتصال به سرویس‌های واقعی خارجی را از بین می‌برد. در مجموع، این معماری تست جامع و دقیق، Saleor را به یک پروژه بسیار قابل اعتماد و پایدار تبدیل کرده است.

GraphQL چیست و در نرم‌افزار Saleor چطور پیاده سازی شده است؟

GraphQL یک زبان پرس‌وجو برای API است. برخلاف REST که در آن سرور تصمیم می‌گیرد چه داده‌هایی را برگرداند، در GraphQL کلاینت (مرورگر) کنترل کامل دارد که دقیقاً چه داده‌هایی را می‌خواهد. این امکان باعث می‌شود داده‌های اضافی (over-fetching) دریافت نشود و ارتباط بین بخش‌های مختلف داده به آسانی برقرار شود.

مزایای GraphQL

  • کنترل دقیق بر روی واکشی داده‌ها: GraphQSaleor با استفاده از GraphQL، امکان دسترسی به داده‌ها را با انعطاف‌پذیری و کارایی بالا فراهم می‌کند. این فناوری به توسعه‌دهندگان اجازه می‌دهد تا با یک درخواست واحد به هر تعداد فیلد مورد نیاز خود دسترسی پیدا کنند و با ترکیب چندین درخواست، تمامی داده‌ها را به صورت یکجا واکشی کنند. این رویکرد به ویژه در پلتفرم‌های تجارت الکترونیک که مدل‌های داده‌ای پیچیده‌ای دارند، بسیار سودمند است.

واکشی بیش از حد (Over-fetching): در این حالت، سرور داده‌های بیشتری از آنچه نیاز است، ارسال می‌کند. برای مثال برای نمایش نام و قیمت یک محصول، سرور تمام نظرات کاربران، اطلاعات انبار و تاریخچه‌ی فروش آن محصول را هم می‌فرستد. این اطلاعات اضافی باعث اتلاف پهنای باند و کاهش سرعت می‌شود.

واکشی ناقص (Under-fetching): در این حالت، سرور داده‌های کافی را در یک درخواست ارسال نمی‌کند و شما مجبور می‌شوید چندین درخواست جداگانه به سرور بفرستید. برای مثال برای نمایش اطلاعات یک محصول، ابتدا یک درخواست برای دریافت نام و قیمت فرستاده شود. سپس یک درخواست دیگر برای دریافت تصاویر، و یک درخواست سوم برای نظرات. این کار باعث افزایش تعداد تبادلات با سرور و تأخیر در بارگذاری صفحه می‌شود.

GraphQL مشکلاتی مانند "واکشی بیش از حد" (over-fetching) و "واکشی ناقص" (under-fetching) را برطرف می‌کند. Saleor تمامی APIهای خود، از جمله ارتباطات وب‌هوک، را بر پایه GraphQL بنا نهاده است، تا یکپارچه‌سازی‌های خارجی نیز از همان سطح کنترل و انعطاف‌پذیری بهره‌مند شوند.

  • گویا بودن پیام و تسهیل در تولید خودکار کد: طرح GraphQL به صورت خودتوضیح عمل می‌کند و تشخیص عملیات‌ها را برای توسعه‌دهنده ساده‌تر می‌سازد. سیستم نوع قوی در GraphQL یک قرارداد محکم بین کلاینت و سرور برقرار می‌کند که به توسعه‌دهندگان امکان استفاده از ابزارهایی مانند تولید خودکار کد و بررسی نوع ایستا را می‌دهد. این قابلیت به ویژه برای توسعه‌دهندگان TypeScript امنیت نوعی (type safety) را فراهم می‌آورد.

  • نگهداری و تکامل: یکی از مزایای کلیدی GraphQL سهولت نگهداری آن است. از آنجایی که پرس‌وجوها صراحتاً فیلدهای مورد نیاز خود را مشخص می‌کنند، افزودن فیلدهای جدید به API یک تغییر غیرمخرب محسوب می‌شود. همچنین، امکان منسوخ کردن فیلدها با استفاده از علامت‌گذاری، به ابزارها اجازه می‌دهد که به توسعه‌دهندگان درباره نیاز به به‌روزرسانی کد هشدار دهند. این ویژگی‌ها تکامل API را بدون تأثیر منفی بر کلاینت‌های موجود ممکن می‌سازد و نیاز به نسخه‌بندی صریح API را از بین می‌برد.

تغییر غیرمخرب (Non-breaking Change): در REST API های سنتی، افزودن یک فیلد جدید به پاسخ سرور می‌تواند برای کلاینت‌های قدیمی که انتظار ساختار داده‌ای متفاوتی دارند، مشکل‌ساز باشد. اما در GraphQL، از آنجایی که هر کلاینت تنها فیلدهایی را که صریحاً درخواست کرده دریافت می‌کند، افزودن فیلدهای جدید بر عملکرد کلاینت‌های موجود هیچ تأثیری نمی‌گذارد.

منسوخ کردن فیلدها (Deprecation): اگر نیاز به حذف یک فیلد قدیمی وجود داشته باشد، می‌توان آن را با علامت‌گذاری به عنوان "منسوخ" (deprecated) در اسکیمای آن، به تدریج مدیریت کرد. با این کار، ابزارهای توسعه به توسعه‌دهندگان هشدار می‌دهند که استفاده از این فیلد توصیه نمی‌شود، اما فیلد منسوخ تا زمان به‌روزرسانی همه کلاینت‌ها به کار خود ادامه می‌دهد. این روند تدریجی به تیم‌ها اجازه می‌دهد فیلدهای قدیمی را بدون ایجاد اختلال ناگهانی حذف کنند.

حذف نیاز به نسخه‌بندی صریح (Versioning): این ویژگی‌ها باعث می‌شوند که GraphQL نیاز به نسخه‌بندی صریح API را از بین ببرد. در REST، توسعه‌دهندگان اغلب مجبورند برای هر تغییر عمده‌ای، یک نسخه‌ی جدید از API (مانند api.com/v1 به api.com/v2) ایجاد کنند که این کار نگهداری API را بسیار پیچیده می‌کند. GraphQL با روش‌های خود، این پیچیدگی را به شدت کاهش می‌دهد و به تیم‌ها اجازه می‌دهد تا API را به صورت روان و مداوم تکامل دهند.


نمونه ای از یک پیام ارسالی از بخش فرانت به بک اند در قالب GraphQL که توسط ابزار developer tools در مرورگر هنگام اجرای نرم‌افزار رصد شده است.
نمونه ای از یک پیام ارسالی از بخش فرانت به بک اند در قالب GraphQL که توسط ابزار developer tools در مرورگر هنگام اجرای نرم‌افزار رصد شده است.

نرم‌افزار Saleor یک رابط کاربری هم برای اجرای دستورات GraphQL ارائه داده است که برای محیط توسعه نرم‌افزار مناسب است. تصویر زیر گویای یک درخواست و پاسخی است که در همین محیط انجام شده است.

در تصویر بالا سمت چپ یک پرس‌وجو به نام MyQuery است که به شرح زیر توضیح داده می‌شود

  • products: یعنی لیست products را بگیرد.

  • (first: 4, channel: "default-channel"): فقط ۴ محصول اول را از کانالی به نام "default-channel" درخواست کرده است.

  • edges و node: در ساختار GraphQL، داده‌های لیست‌ها معمولاً درون edges و node قرار می‌گیرند. این یک الگوی رایج برای صفحه‌بندی (pagination) و مدیریت ارتباطات است.

  • name: از هر محصول، فقط نام آن را درخواست کرده است.

پاسخ سمت راست نیز داده‌هایی است که سرور GraphQL پس از اجرای پرس‌وجوی برگردانده است:

  • "data": این بخش شامل تمام داده‌های درخواستی است.

  • "products": این همان لیست محصولاتی است که در پرس‌وجو درخواست شده بود.

  • "edges": یک آرایه (array) شامل ۴ شیء است که هر کدام نماینده یک محصول هستند.

  • "node": در هر شیء از edges، یک node وجود دارد که اطلاعات واقعی محصول را نگه می‌دارد.

  • "name": نام هر محصول (مثل "Apple Juice", "Monospace Tee" و...). این دقیقاً همان فیلدی است که در پرس‌وجو درخواست کرده‌اید.

این مثال به خوبی نشان می‌دهد که چگونه GraphQL اجازه می‌دهد تا دقیقاً همان داده‌ای که نیاز است دریافت گردد. وقتی فقط نام محصولات درخواست می‌شود، پاسخ هم فقط شامل نام محصولات است، نه اطلاعات اضافی مثل قیمت، توضیحات یا تصاویر. این کار باعث می‌شود تا حجم داده‌های منتقل شده کمتر و سرعت برنامه شما بالاتر باشد. بخش extensions هم اطلاعاتی درباره هزینه اجرای پرس‌وجو ارائه می‌دهد. "requestedQueryCost": 4 به این معنی است که اجرای این پرس‌وجو ۴ واحد هزینه داشته است.

خلاصه ارزیابی سه نرم‌افزار و مقایسه آنها با یکدیگر

مقایسه تطبیقی هر سه نرم‌افزار
مقایسه تطبیقی هر سه نرم‌افزار

منابع و مراجع

  • https://github.com/ocp-power-demos/sock-shop-demo, Accessed May 2025

  • https://github.com/opencart/opencart, Accessed May 2025

  • https://github.com/saleor/saleor, Accessed May 2025

  • https://deepwiki.com/saleor/saleor, Accessed August 2025

  • برای تحلیل قطعات کد نرم‌افزاری از ابزارهای هوش مصنوعی Chatgpt و Gemini استفاده شده است.

معماری نرم‌افزارنرم افزارتجارت الکترونیکیفروشگاه اینترنتی
۲
۱
سید محمد مهدی حسینی
سید محمد مهدی حسینی
شاید از این پست‌ها خوشتان بیاید