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 — ذخیره وزن انتخاب‌شده در Cart
  • woocommerce_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