eight
پاسخ سوالات فنی
محمد رحمان زاده
در قدم اول تعداد ۱۰ هزار بازدید روزانه عدد زیادی نیست. معمولاً در سایتهای وردپرسی، تعداد ریکوئستهای لود شده در صفحه اصلی به ازای هر کاربر تقریباً ۱۰۰ ریکوئست است که در صورتیکه صرفاً ۱۰۰ کاربر به سایت مراجعه کنند، این تعداد بازدید ثبت میشود. اما ممکن است بخش زیادی از درخواستها در یک بازه کوتاه، مثلاً هنگام اجرای کمپین یا ارسال اعلان، وارد شوند. بنابراین ظرفیت سیستم را بر اساس Peak Traffic باید طراحی کنیم.
همچنین همه درخواستها را یکسان پردازش نمیکنیم.
دستهبندی پیشنهادی
| نوع درخواست | روش پردازش | اولویت |
|---|---|---|
| ورود و احراز هویت | همزمان | بحرانی |
| بررسی سطح دسترسی | همزمان، همراه Cache | بحرانی |
| اعتبارسنجی کد تخفیف | همزمان، همراه Cache | بالا |
| ثبت یا تولید کد تخفیف گروهی | غیرهمزمان | بالا |
| اعلان تراکنشی، مانند تایید سفارش | غیرهمزمان | بالا |
| خطای بحرانی سیستم | صف مجزا و Alert فوری | بالا |
| اعلان تبلیغاتی | غیرهمزمان | پایین |
| لاگهای عادی سیستم | پردازش دستهای | پایین |
معماری پیشنهادی
Website > Webserver > Authentication + Rate Limiting > Request Classifier
/ \
Sync Requests Async Requests
| |
Auth / Access Redis / Cache Queue
|
Critical / High / Normal / Bulk
|
Workers
|
Database / External APIs
در لایه ورودی وبسرور برای موارد زیر استفاده میکنیم:
- Rate Limiting بر اساس IP، کاربر و API Key
- محدود کردن اندازه Request Body
- جلوگیری از ورود درخواستهای غیرمجاز
- Load Balancing بین چند Instance
- ثبت Request ID برای Trace کردن درخواست
درخواستهای همزمان مثل Login سریعاً پردازش میشوند. درخواستهای غیرهمزمان مثل Notification وارد Queue میشوند و Workerها آنها را پردازش میکنند.
Idempotency
ممکن است اپلیکیشن موبایل به دلیل قطعی اینترنت یک درخواست را چند بار ارسال کند. برای جلوگیری از اجرای چندباره عملیات، از Idempotency-Key استفاده میکنیم. مثلاً اگر درخواست صدور کد تخفیف دوبار ارسال شد، سیستم نباید دو کد ایجاد کند.
Retry
اگر یک درخواست به دلیل خطای موقت شکست خورد:
- Retry 1: بعد از ۱ ثانیه
- Retry 2: بعد از ۲ ثانیه
- Retry 3: بعد از ۴ ثانیه
- Retry 4: بعد از ۸ ثانیه
بعد از چند شکست، درخواست وارد Dead Letter Queue میشود تا بررسی شود.
Cache
موارد پرتکرار مانند سطح دسترسی کاربر، اطلاعات کمپین و وضعیت کد تخفیف را میتوان برای مدت کوتاهی در Redis نگه داشت. این کار تعداد Queryهای دیتابیس را کاهش میدهد.
Observability
حداقل این شاخصها را مانیتور میکنم:
- مدت پاسخ API
- تعداد درخواستهای صف
- تعداد Jobهای شکستخورده
- نرخ خطای Login
- مصرف CPU و RAM
- تعداد Connectionهای دیتابیس
- زمان انتظار هر Queue
برای خطاها میتوان از Sentry یا سیستم اختصاصی لاگر و برای Metrics از Prometheus و Grafana استفاده کرد.
نمونه اسکریپت مدیریت درخواستها
<?php
$queues = [
"critical" => [],
"high" => [],
"normal" => [],
"bulk" => []
];
function classifyRequest($type, $payload = [])
{
switch ($type) {
case "login":
case "feature_access":
return ["mode" => "sync"];
case "system_error":
return [
"mode" => "async",
"queue" => ($payload["severity"] ?? "") === "critical"
? "critical" : "normal"
];
case "notification":
return [
"mode" => "async",
"queue" => ($payload["transactional"] ?? false)
? "high" : "bulk"
];
case "discount":
if (($payload["operation"] ?? "") === "validate") {
return ["mode" => "sync"];
}
return ["mode" => "async", "queue" => "high"];
}
return ["mode" => "async", "queue" => "normal"];
}
function handleRequest($request)
{
global $queues;
$decision = classifyRequest(
$request["type"],
$request["payload"] ?? []
);
if ($decision["mode"] === "sync") {
return [
"status" => "processed",
"flow" => "direct-service-call",
"type" => $request["type"]
];
}
$job = [
"id" => uniqid(),
"type" => $request["type"],
"payload" => $request["payload"] ?? []
];
$queues[$decision["queue"]][] = $job;
return [
"status" => "queued",
"queue" => $decision["queue"],
"jobId" => $job["id"]
];
}
$requests = [
["type" => "login", "payload" => ["userId" => 10]],
["type" => "system_error", "payload" => ["severity" => "critical"]],
["type" => "notification", "payload" => ["transactional" => false]]
];
foreach ($requests as $request) {
print_r(handleRequest($request));
}
این کد یک نسخه سادهشده است. در محیط Production، Authentication، Schema Validation، Database Transaction، Logging ساختاریافته و تستهای Integration نیز اضافه میکنم.
نکته مهم: درخواست Login را نباید صرفاً برای اولویتبندی وارد Queue کرد؛ چون کاربر منتظر پاسخ است. Queue بیشتر برای عملیاتهایی مناسب است که نتیجه آنها لازم نیست در همان لحظه به کاربر تحویل داده شود.
سوال خیلی واضح نیست و برداشت من این بود که صفحات مختلفی وجود دارد برای نمایش محصول و قیمتش و یک محصول ممکن است مثلاً دو وزنش در دو صفحه متفاوت دو قیمت داشته باشد.
مشکل اصلی این سیستم، صرفاً اشتباه بودن نمایش قیمت نیست؛ مشکل این است که قیمت در چند محل مختلف ذخیره یا محاسبه میشود. راهحل من ایجاد یک Single Source of Truth برای قیمت است.
اگر فروشگاه از WordPress و WooCommerce استفاده کند، یک پلاگین WooCommerce طراحی میکنم که قیمت را بر اساس قیمت پایه و وزن محاسبه کند.
مدل قیمتگذاری
برای هر محصول این اطلاعات ثبت میشود:
- قیمت پایه هر گرم یا هر کیلوگرم
- وزنهای قابل فروش
- هزینه بستهبندی هر وزن
- تخفیف احتمالی
- قانون گرد کردن قیمت
- نسخه قیمت
- زمان آخرین تغییر
مثلاً قیمت هر گرم زعفران: ۱۲۰,۰۰۰ ریال
- وزن ۱ گرم: ۱۲۰,۰۰۰ + ۲۰,۰۰۰ هزینه بستهبندی
- وزن ۲ گرم: ۲۴۰,۰۰۰ + ۲۵,۰۰۰ هزینه بستهبندی
- وزن ۵ گرم: ۶۰۰,۰۰۰ + ۳۵,۰۰۰ هزینه بستهبندی
فرمول اصلی: قیمت نهایی پایه = قیمت هر واحد وزن × وزن انتخابی + هزینه بستهبندی
مالیات و Coupon را ترجیح میدهم به WooCommerce واگذار کنم تا منطق استاندارد Checkout حفظ شود.
فلو پلاگین
مدیر فروشگاه >
ویرایش محصول در WooCommerce
|
v
ثبت قیمت پایه + وزنها + هزینه بستهبندی
|
v
اعتبارسنجی اطلاعات
|
v
محاسبه قیمت تمام وزنها
|
v
ذخیره در Product Meta یا جدول اختصاصی
|
v
پاک کردن Cache محصول
|
v
نمایش یکسان در سایت، سبد خرید، Checkout و اپلیکیشن
رابط مدیریت
در صفحه ویرایش محصول یک Tab با عنوان «قیمتگذاری وزنی» اضافه میکنم.
| وزن | هزینه بستهبندی | فعال |
|---|---|---|
| ۵۰ گرم | ۲۰,۰۰۰ تومان | بله |
| ۱۰۰ گرم | ۲۵,۰۰۰ تومان | بله |
| ۲۵۰ گرم | ۳۵,۰۰۰ تومان | بله |
| ۵۰۰ گرم | ۵۰,۰۰۰ تومان | خیر |
اگر وزنها ثابت و محدود باشند، استفاده از Variable Product خود WooCommerce انتخاب مناسبی است. پلاگین میتواند Variationها را بهصورت خودکار بسازد و قیمت آنها را از روی قیمت پایه بهروزرسانی کند.
اگر کاربر بتواند وزن دلخواه وارد کند، مثلاً ۱۳۵ گرم، باید از Custom Cart Item و محاسبه Server-side استفاده شود.
Hookهای اصلی WooCommerce
woocommerce_product_data_tabs— اضافه کردن Tab تنظیماتwoocommerce_product_data_panels— محتوای Tab تنظیماتwoocommerce_process_product_meta— ذخیره قیمت پایه و وزنهاwoocommerce_get_price_html— نمایش بازه قیمت صحیحwoocommerce_add_cart_item_data— ذخیره وزن انتخابشده در Cartwoocommerce_before_calculate_totals— محاسبه مجدد قیمت در سمت سرورwoocommerce_checkout_create_order_line_item— ثبت وزن و قیمت در سفارش
نکته امنیتی
قیمت ارسالشده از Front-end را قبول نمیکنم. کاربر میتواند JavaScript یا Request را تغییر دهد. Front-end فقط وزن انتخابشده را ارسال میکند و قیمت نهایی دوباره در سرور محاسبه میشود.
Front-end sends: product_id = 123 weight = 250 Server calculates: price = calculatePrice(product_id, 250)
برای این کسبوکار، ابتدا بررسی میکنم قیمتها از کجا تهیه میشوند. بهترین روش استفاده از API رسمی تامینکننده، ایرلاین، GDS، هتل یا Partner Feed است.
Scraping را فقط زمانی استفاده میکنم که API در دسترس نباشد و شرایط استفاده سایت اجازه دهد؛ چون Scraping شکننده است و با تغییر HTML سایت ممکن است از کار بیفتد.
معماری پیشنهادی
Scheduler > Flight / Hotel APIs > Normalizer > Database > Pricing Rules > Validation + Anomaly Detection > Change Detector > Telegram Bot > Telegram Group / Channel
روش اجرایی پیشنهادی
برای نسخه اولیه از n8n استفاده میکنم، چون سرعت توسعه بالایی دارد و تیم غیرتکنیکال هم میتواند Flow را مشاهده کند.
در صورتی که منطق پیچیده شود، بخش اصلی را با Node.js یا Python به یک سرویس مستقل منتقل میکنم.
فلو n8n
Cron Trigger > Read Search Configurations > Call Flight and Hotel APIs > Normalize Response > Validate Price and Availability > Apply Markup and Business Rules > Compare with Previous Snapshot > Generate Telegram Message > Send to Telegram > Store Message ID and Audit Log