مجله ایرانیکاسرورآموزش سرور، هاست، وردپرس و شبکه
آموزشی

ساخت سیستم تیکت برای ووکامرس؛ پشتیبانی مشتری متصل به سفارش‌ها

وقتی مشتری یک فروشگاه اینترنتی بعد از خرید با مشکلی روبه‌رو می‌شود، تیم پشتیبانی معمولاً به چند اطلاعات هم‌زمان نیاز دارد: مشتری چه کسی است، کدام سفارش را ثبت کرده، چه محصولی خریده، وضعیت…

ساخت سیستم تیکت برای ووکامرس؛ پشتیبانی مشتری متصل به سفارش‌ها
خلاصه سریع این آموزش

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

۲۹ دقیقه مطالعه ۰ دیدگاه ۱۹ مهر ۱۴۰۵ آخرین به‌روزرسانی

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

در این آموزش یک Help Desk یا «مرکز پشتیبانی تیکتی» اختصاصی برای WooCommerce می‌سازیم. مشتری از داخل حساب کاربری خودش تیکت ثبت می‌کند، در صورت نیاز سفارش مرتبط را انتخاب می‌کند، پاسخ پشتیبان را می‌بیند و فایل پیوست می‌فرستد. سمت مدیریت نیز اپراتور می‌تواند تیکت‌ها را براساس وضعیت و اولویت بررسی کند، مسئول رسیدگی تعیین کند، پاسخ بدهد و سابقه کامل تغییرات را ببیند.

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

در پایان چه چیزی خواهیم داشت؟ یک افزونه اختصاصی WooCommerce با بخش «پشتیبانی» در My Account، جدول مستقل تیکت و پیام، اتصال امن تیکت به سفارش، نقش اپراتور پشتیبانی، پاسخ مشتری و Agent، فایل پیوست خصوصی، اعلان ایمیلی، صف پردازش، SLA، لاگ فعالیت و روش حذف یا بازگشت امن تغییرات.

فهرست مطالب

Help Desk چیست و چه تفاوتی با فرم تماس دارد؟

فرم تماس معمولاً یک پیام را برای مدیر سایت یا یک ایمیل مشخص ارسال می‌کند. بعد از ارسال پیام، ادامه گفت‌وگو ممکن است خارج از سایت انجام شود و وضعیت درخواست نیز مشخص نباشد. Help Desk متفاوت است: هر درخواست یک پرونده با شناسه یکتا دارد و تمام پیام‌ها، وضعیت‌ها، مسئول رسیدگی و زمان‌های مهم در همان پرونده ذخیره می‌شوند.

تفاوت فرم تماس ساده با Help Desk
قابلیت فرم تماس سیستم تیکت
شناسه پیگیری معمولاً ندارد دارد
وضعیت درخواست مشخص نیست باز، منتظر پاسخ، حل‌شده و بسته
تاریخچه گفت‌وگو ممکن است بین ایمیل‌ها پراکنده شود در یک پرونده ذخیره می‌شود
اتصال به سفارش ووکامرس معمولاً دستی مستقیم و قابل کنترل
تخصیص به اپراتور محدود قابل پیاده‌سازی
SLA و گزارش زمان پاسخ معمولاً ندارد قابل اندازه‌گیری است

آیا WooCommerce خودش Help Desk دارد؟

WooCommerce در صفحه My Account بخش‌هایی مثل سفارش‌ها، دانلودها، آدرس‌ها و روش‌های پرداخت را به‌صورت Endpoint ارائه می‌کند، اما در هسته اصلی یک سامانه کامل تیکت شامل Agent، Priority، صف پاسخ و تاریخچه مستقل گفت‌وگو وجود ندارد. به همین دلیل افزونه‌های تخصصی Help Desk نیز در اکوسیستم WooCommerce عرضه شده‌اند.

اگر نیاز شما کاملاً عمومی است، استفاده از یک افزونه آماده می‌تواند منطقی‌تر باشد. توسعه اختصاصی زمانی ارزش بیشتری دارد که بخواهید تیکت را به سفارش، CRM، حسابداری، سرویس فنی، SLA داخلی، دپارتمان‌های اختصاصی یا پنل دیگری متصل کنید.

مقاله مرتبط: قبل از تغییر دیتابیس فروشگاه، مسیر بازگشت داشته باشید

این آموزش جدول جدید و Endpoint جدید به وردپرس اضافه می‌کند. قبل از اجرای آن روی سایت اصلی، از دیتابیس و فایل‌های فروشگاه نسخه پشتیبان بگیرید. راهنمای ساخت بکاپ خودکار WordPress روی S3 برای ایجاد نسخه پشتیبان خارج از هاست اصلی مفید است.

چرا تیکت را داخل Order Note ذخیره نمی‌کنیم؟

Order Note برای ثبت رویدادها و یادداشت‌های همان سفارش مناسب است، اما Ticket موجودیت مستقلی است. یک مشتری ممکن است قبل از خرید سؤال داشته باشد، درباره یک سفارش چند مشکل جدا ثبت کند یا چند ماه بعد برای همان محصول یک درخواست جدید بفرستد.

پس تیکت را در جدول مستقل نگه می‌داریم و فقط یک order_id اختیاری به آن متصل می‌کنیم. برای خواندن اطلاعات سفارش نیز از wc_get_order() استفاده می‌کنیم؛ در نتیجه سیستم Help Desk مجبور نیست بداند WooCommerce سفارش را در چه جدول فیزیکی ذخیره کرده است.

پیش‌نیازهای این آموزش

برای اجرای نسخه پایه به یک سایت WordPress دارای WooCommerce فعال، دسترسی مدیر وردپرس و امکان ساخت یک افزونه اختصاصی نیاز دارید. کدها را داخل قالب قرار نمی‌دهیم؛ چون با تغییر یا به‌روزرسانی قالب، سیستم پشتیبانی نباید حذف شود.

قبل از شروع این موارد را آماده کنید
مورد کاربرد
WooCommerce فعال دسترسی به سفارش و My Account
دسترسی فایل File Manager، SFTP یا SSH برای ساخت افزونه
Staging تست بدون اثر روی مشتری واقعی
بکاپ Rollback در صورت خطای دیتابیس یا PHP

ساختار افزونه‌ای که می‌سازیم

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

wp-content/plugins/iranica-woo-helpdesk/
├── iranica-woo-helpdesk.php
├── includes/
│   ├── install.php
│   ├── account.php
│   ├── rest.php
│   ├── admin.php
│   ├── attachments.php
│   └── notifications.php
└── assets/
    └── helpdesk.js

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

مرحله ۱: فایل اصلی افزونه و بررسی WooCommerce

در مسیر wp-content/plugins/ پوشه iranica-woo-helpdesk را بسازید. سپس فایل iranica-woo-helpdesk.php را ایجاد کنید:

<?php
/**
 * Plugin Name: Iranica Woo Help Desk
 * Description: Help Desk و Ticket متصل به سفارش‌های WooCommerce
 * Version: 1.0.0
 */

defined( 'ABSPATH' ) || exit;

define( 'IRHD_VERSION', '1.0.0' );
define( 'IRHD_FILE', __FILE__ );
define( 'IRHD_DIR', plugin_dir_path( __FILE__ ) );

require_once IRHD_DIR . 'includes/install.php';
require_once IRHD_DIR . 'includes/account.php';
require_once IRHD_DIR . 'includes/rest.php';
require_once IRHD_DIR . 'includes/admin.php';
require_once IRHD_DIR . 'includes/attachments.php';
require_once IRHD_DIR . 'includes/notifications.php';

add_action(
    'plugins_loaded',
    function() {
        if ( ! class_exists( 'WooCommerce' ) ) {
            add_action(
                'admin_notices',
                function() {
                    echo '<div class="notice notice-error">';
                    echo '<p>Iranica Woo Help Desk به WooCommerce فعال نیاز دارد.</p>';
                    echo '</div>';
                }
            );
        }
    }
);

نتیجه مورد انتظار: بعد از ساخت فایل‌ها، افزونه باید در بخش «افزونه‌ها» دیده شود. فعلاً آن را فعال نکنید؛ ابتدا فایل نصب و جدول‌ها را می‌سازیم.

اگر خطای Fatal Error گرفتید: معمولاً نام فایل include اشتباه است یا یکی از فایل‌های پوشه includes وجود ندارد. فایل اصلی را موقتاً از پوشه Plugins خارج کنید تا وردپرس دوباره بالا بیاید، سپس مسیرها را بررسی کنید.

مرحله ۲: جدول‌های Ticket، Message و Audit Log را بسازید

در فایل includes/install.php ساختار دیتابیس را قرار دهید. برای Ticket پرتعداد، جدول اختصاصی Query و Index قابل کنترل‌تری نسبت به انباشتن همه پیام‌ها در Post Meta ایجاد می‌کند.

<?php

defined( 'ABSPATH' ) || exit;

function irhd_install_tables() {
    global $wpdb;

    $tickets = $wpdb->prefix . 'irhd_tickets';
    $messages = $wpdb->prefix . 'irhd_messages';
    $attachments = $wpdb->prefix . 'irhd_attachments';
    $events = $wpdb->prefix . 'irhd_events';

    $charset = $wpdb->get_charset_collate();

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';

    $sql_tickets = "CREATE TABLE {$tickets} (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        public_id char(36) NOT NULL,
        user_id bigint(20) unsigned NOT NULL,
        order_id bigint(20) unsigned NULL,
        subject varchar(190) NOT NULL,
        department varchar(80) NOT NULL DEFAULT 'general',
        status varchar(30) NOT NULL DEFAULT 'open',
        priority varchar(20) NOT NULL DEFAULT 'normal',
        assigned_user_id bigint(20) unsigned NULL,
        first_response_at datetime NULL,
        first_response_due_at datetime NULL,
        created_at datetime NOT NULL,
        updated_at datetime NOT NULL,
        closed_at datetime NULL,
        PRIMARY KEY  (id),
        UNIQUE KEY public_id (public_id),
        KEY user_id (user_id),
        KEY order_id (order_id),
        KEY status (status),
        KEY assigned_user_id (assigned_user_id),
        KEY updated_at (updated_at)
    ) {$charset};";

    $sql_messages = "CREATE TABLE {$messages} (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        ticket_id bigint(20) unsigned NOT NULL,
        author_user_id bigint(20) unsigned NOT NULL,
        author_type varchar(20) NOT NULL,
        body longtext NOT NULL,
        is_internal tinyint(1) NOT NULL DEFAULT 0,
        created_at datetime NOT NULL,
        PRIMARY KEY  (id),
        KEY ticket_id (ticket_id),
        KEY author_user_id (author_user_id),
        KEY created_at (created_at)
    ) {$charset};";

    $sql_attachments = "CREATE TABLE {$attachments} (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        message_id bigint(20) unsigned NOT NULL,
        stored_name varchar(190) NOT NULL,
        original_name varchar(190) NOT NULL,
        mime_type varchar(100) NOT NULL,
        size_bytes bigint(20) unsigned NOT NULL DEFAULT 0,
        created_at datetime NOT NULL,
        PRIMARY KEY  (id),
        KEY message_id (message_id)
    ) {$charset};";

    $sql_events = "CREATE TABLE {$events} (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        ticket_id bigint(20) unsigned NOT NULL,
        actor_user_id bigint(20) unsigned NULL,
        event_type varchar(50) NOT NULL,
        event_data longtext NULL,
        created_at datetime NOT NULL,
        PRIMARY KEY  (id),
        KEY ticket_id (ticket_id),
        KEY event_type (event_type),
        KEY created_at (created_at)
    ) {$charset};";

    dbDelta( $sql_tickets );
    dbDelta( $sql_messages );
    dbDelta( $sql_attachments );
    dbDelta( $sql_events );

    update_option(
        'irhd_db_version',
        IRHD_VERSION
    );
}

Role و Capability مخصوص پشتیبانی

Capability یعنی مجوز انجام یک کار. به‌جای اینکه هر اپراتور را Administrator کنیم، مجوز manage_irhd_tickets می‌سازیم:

function irhd_install_role() {
    add_role(
        'irhd_agent',
        'اپراتور پشتیبانی',
        array(
            'read' => true,
            'manage_irhd_tickets' => true,
        )
    );

    $admin = get_role( 'administrator' );

    if ( $admin ) {
        $admin->add_cap(
            'manage_irhd_tickets'
        );
    }

    $manager = get_role( 'shop_manager' );

    if ( $manager ) {
        $manager->add_cap(
            'manage_irhd_tickets'
        );
    }
}

Activation و Deactivation

function irhd_register_account_endpoint() {
    add_rewrite_endpoint(
        'support-tickets',
        EP_ROOT | EP_PAGES
    );
}

function irhd_activate() {
    irhd_install_tables();
    irhd_install_role();
    irhd_register_account_endpoint();

    flush_rewrite_rules();
}

function irhd_deactivate() {
    if (
        function_exists(
            'as_unschedule_all_actions'
        )
    ) {
        as_unschedule_all_actions(
            'irhd_sla_scan',
            array(),
            'iranica-helpdesk'
        );
    }

    flush_rewrite_rules();
}

register_activation_hook(
    IRHD_FILE,
    'irhd_activate'
);

register_deactivation_hook(
    IRHD_FILE,
    'irhd_deactivate'
);

نتیجه مورد انتظار: با فعال‌سازی افزونه، چهار جدول با پیشوند سایت ساخته می‌شوند و نقش «اپراتور پشتیبانی» نیز در WordPress ظاهر می‌شود.

Rollback: غیرفعال‌کردن افزونه جدول‌ها و Ticketها را حذف نمی‌کند؛ این رفتار عمدی است تا با یک Deactivate ساده اطلاعات مشتری از بین نرود. حذف دائمی داده باید فقط در Uninstall و با تصمیم صریح مدیر انجام شود.

مرحله ۳: بخش «پشتیبانی» را به My Account اضافه کنید

My Account صفحه حساب مشتری در WooCommerce است. WooCommerce برای بخش‌هایی مثل Orders و Downloads از Endpoint استفاده می‌کند؛ یعنی یک قسمت اضافه در انتهای URL مشخص می‌کند چه محتوایی نمایش داده شود.

فایل includes/account.php:

<?php

defined( 'ABSPATH' ) || exit;

add_action(
    'init',
    'irhd_register_account_endpoint'
);

add_filter(
    'woocommerce_account_menu_items',
    function( $items ) {
        $logout = $items[
            'customer-logout'
        ] ?? null;

        unset( $items['customer-logout'] );

        $items['support-tickets'] =
            'پشتیبانی';

        if ( $logout ) {
            $items[
                'customer-logout'
            ] = $logout;
        }

        return $items;
    }
);

add_action(
    'woocommerce_account_support-tickets_endpoint',
    'irhd_render_account_page'
);

بعد از فعال‌سازی، به My Account بروید. باید گزینه «پشتیبانی» در منوی حساب مشتری دیده شود.

اگر صفحه 404 شد: وارد «تنظیمات ← پیوندهای یکتا» شوید و یک بار «ذخیره تغییرات» را بزنید. این کار Rewrite Rules را بازسازی می‌کند. اگر مشکل باقی ماند، بررسی کنید افزونه در Activation واقعاً flush_rewrite_rules() را بعد از ثبت Endpoint اجرا کرده باشد.

مرحله ۴: لیست تیکت‌ها و فرم ثبت درخواست را نمایش دهید

همان Endpoint هم لیست Ticketهای کاربر را نمایش می‌دهد و هم فرم ثبت درخواست جدید را. اگر بعد از Endpoint یک UUID قرار داشته باشد، صفحه جزئیات همان Ticket را باز می‌کنیم.

function irhd_render_account_page(
    $value = ''
) {
    if ( ! is_user_logged_in() ) {
        return;
    }

    $ticket_uuid =
        sanitize_text_field(
            (string) $value
        );

    if ( $ticket_uuid ) {
        irhd_render_ticket_detail(
            $ticket_uuid
        );
        return;
    }

    irhd_render_ticket_list();
    irhd_render_new_ticket_form();
}

خواندن سفارش‌های خود مشتری برای فرم

کاربر نباید شماره سفارش مشتری دیگری را دستی وارد کند. سفارش‌های خودش را با API ووکامرس می‌خوانیم و در Select نشان می‌دهیم:

$orders = wc_get_orders(
    array(
        'customer_id' =>
            get_current_user_id(),
        'limit'   => 20,
        'orderby' => 'date',
        'order'   => 'DESC',
    )
);

استفاده از wc_get_orders() مهم است؛ Query مستقیم روی wp_posts برای سفارش‌ها با ساختار جدید WooCommerce آینده‌نگر نیست.

فرم پایه

<form id="irhd-new-ticket">
    <p>
        <label for="irhd-subject">
            موضوع
        </label>
        <input
            id="irhd-subject"
            name="subject"
            type="text"
            maxlength="190"
            required
        >
    </p>

    <p>
        <label for="irhd-order">
            سفارش مرتبط
        </label>
        <select
            id="irhd-order"
            name="order_id"
        >
            <option value="">
                بدون سفارش
            </option>
            <!-- گزینه‌های سفارش از PHP -->
        </select>
    </p>

    <p>
        <label for="irhd-message">
            توضیح مشکل
        </label>
        <textarea
            id="irhd-message"
            name="message"
            rows="8"
            maxlength="10000"
            required
        ></textarea>
    </p>

    <button type="submit">
        ثبت تیکت
    </button>
</form>

مرحله ۵: REST API امن برای ساخت Ticket

REST API مسیر استانداردی است که JavaScript صفحه می‌تواند از طریق HTTP با WordPress صحبت کند. برای Route خصوصی، فقط داشتن Nonce کافی نیست؛ Nonce برای مقابله با درخواست جعلی کاربرد دارد و کنترل دسترسی باید جداگانه انجام شود.

فایل includes/rest.php:

<?php

defined( 'ABSPATH' ) || exit;

function irhd_customer_permission() {
    return is_user_logged_in();
}

function irhd_agent_permission() {
    return current_user_can(
        'manage_irhd_tickets'
    );
}

add_action(
    'rest_api_init',
    function() {
        register_rest_route(
            'iranica-helpdesk/v1',
            '/tickets',
            array(
                'methods' =>
                    WP_REST_Server::CREATABLE,
                'callback' =>
                    'irhd_rest_create_ticket',
                'permission_callback' =>
                    'irhd_customer_permission',
            )
        );

        register_rest_route(
            'iranica-helpdesk/v1',
            '/tickets/(?P<uuid>[a-f0-9-]{36})/reply',
            array(
                'methods' =>
                    WP_REST_Server::CREATABLE,
                'callback' =>
                    'irhd_rest_reply',
                'permission_callback' =>
                    'irhd_customer_permission',
            )
        );

        register_rest_route(
            'iranica-helpdesk/v1',
            '/tickets/(?P<uuid>[a-f0-9-]{36})',
            array(
                'methods' =>
                    WP_REST_Server::EDITABLE,
                'callback' =>
                    'irhd_rest_update_ticket',
                'permission_callback' =>
                    'irhd_agent_permission',
            )
        );
    }
);

ساخت Ticket با Transaction

می‌خواهیم Ticket و اولین Message با هم ثبت شوند. چون جدول‌ها InnoDB هستند می‌توان عملیات را داخل Transaction انجام داد تا Ticket بدون پیام اولیه باقی نماند.

function irhd_rest_create_ticket(
    WP_REST_Request $request
) {
    global $wpdb;

    $user_id =
        get_current_user_id();

    if ( irhd_rate_limited( $user_id ) ) {
        return new WP_Error(
            'rate_limited',
            'تعداد درخواست‌ها بیش از حد مجاز است.',
            array( 'status' => 429 )
        );
    }

    $subject = sanitize_text_field(
        (string) $request->get_param(
            'subject'
        )
    );

    $message = sanitize_textarea_field(
        (string) $request->get_param(
            'message'
        )
    );

    $order_id = absint(
        $request->get_param(
            'order_id'
        )
    );

    if (
        '' === $subject ||
        '' === $message
    ) {
        return new WP_Error(
            'missing_fields',
            'موضوع و متن پیام الزامی است.',
            array( 'status' => 400 )
        );
    }

    if ( mb_strlen( $message ) > 10000 ) {
        return new WP_Error(
            'message_too_long',
            'متن پیام بیش از حد مجاز است.',
            array( 'status' => 400 )
        );
    }

    if ( $order_id ) {
        $order = wc_get_order(
            $order_id
        );

        if (
            ! $order instanceof WC_Order ||
            (int) $order->get_user_id()
                !== $user_id
        ) {
            return new WP_Error(
                'invalid_order',
                'این سفارش برای حساب شما قابل استفاده نیست.',
                array( 'status' => 403 )
            );
        }
    }

    $tickets =
        $wpdb->prefix . 'irhd_tickets';

    $messages =
        $wpdb->prefix . 'irhd_messages';

    $uuid = wp_generate_uuid4();

    $now = current_time(
        'mysql',
        true
    );

    $due = gmdate(
        'Y-m-d H:i:s',
        time() + 4 * HOUR_IN_SECONDS
    );

    $wpdb->query(
        'START TRANSACTION'
    );

    $ok = $wpdb->insert(
        $tickets,
        array(
            'public_id' => $uuid,
            'user_id' => $user_id,
            'order_id' =>
                $order_id ?: null,
            'subject' => $subject,
            'department' => 'general',
            'status' => 'waiting_support',
            'priority' => 'normal',
            'first_response_due_at' =>
                $due,
            'created_at' => $now,
            'updated_at' => $now,
        )
    );

    if ( false === $ok ) {
        $wpdb->query( 'ROLLBACK' );

        return new WP_Error(
            'ticket_insert_failed',
            'ثبت تیکت انجام نشد.',
            array( 'status' => 500 )
        );
    }

    $ticket_id =
        (int) $wpdb->insert_id;

    $ok = $wpdb->insert(
        $messages,
        array(
            'ticket_id' => $ticket_id,
            'author_user_id' => $user_id,
            'author_type' => 'customer',
            'body' => $message,
            'is_internal' => 0,
            'created_at' => $now,
        )
    );

    if ( false === $ok ) {
        $wpdb->query( 'ROLLBACK' );

        return new WP_Error(
            'message_insert_failed',
            'پیام اولیه ثبت نشد.',
            array( 'status' => 500 )
        );
    }

    $wpdb->query( 'COMMIT' );

    irhd_log_event(
        $ticket_id,
        'ticket_created',
        array(
            'order_id' => $order_id,
        )
    );

    irhd_queue_notification(
        'new_ticket',
        $ticket_id
    );

    return new WP_REST_Response(
        array(
            'success' => true,
            'ticket_id' => $uuid,
        ),
        201
    );
}

نکته درباره SLA: عدد ۴ ساعت در این نمونه فقط برای نشان‌دادن منطق فنی است و نباید به‌عنوان وعده پشتیبانی ایرانیکاسرور یا فروشگاه شما تلقی شود. مقدار واقعی باید از تنظیمات Help Desk خوانده شود و فقط اگر کسب‌وکار واقعاً متعهد به آن است به مشتری نمایش داده شود.

نتیجه مورد انتظار: ارسال فرم باید HTTP 201 و یک UUID برگرداند. در جدول Ticket یک ردیف و در جدول Messages یک ردیف ایجاد می‌شود.

اگر 403 دریافت کردید: بررسی کنید کاربر Login است، Cookie وردپرس ارسال می‌شود و JavaScript هدر X-WP-Nonce را دارد.

مرحله ۶: Nonce را به JavaScript بدهید

در درخواست‌های REST همان دامنه، WordPress می‌تواند از Cookie ورود کاربر استفاده کند. برای جلوگیری از CSRF، یک Nonce با Action استاندارد wp_rest نیز می‌فرستیم.

add_action(
    'wp_enqueue_scripts',
    function() {
        if ( ! is_account_page() ) {
            return;
        }

        wp_enqueue_script(
            'irhd-helpdesk',
            plugins_url(
                'assets/helpdesk.js',
                IRHD_FILE
            ),
            array(),
            IRHD_VERSION,
            true
        );

        wp_localize_script(
            'irhd-helpdesk',
            'IRHD',
            array(
                'restUrl' =>
                    esc_url_raw(
                        rest_url(
                            'iranica-helpdesk/v1/'
                        )
                    ),
                'nonce' =>
                    wp_create_nonce(
                        'wp_rest'
                    ),
            )
        );
    }
);

در assets/helpdesk.js:

async function irhdRequest(
    path,
    options = {}
) {
    const response = await fetch(
        IRHD.restUrl + path,
        {
            credentials: 'same-origin',
            ...options,
            headers: {
                'Content-Type':
                    'application/json',
                'X-WP-Nonce':
                    IRHD.nonce,
                ...(options.headers || {})
            }
        }
    );

    const data = await response.json();

    if (!response.ok) {
        throw new Error(
            data.message ||
            'خطا در ارتباط با سرور'
        );
    }

    return data;
}

Nonce جای Permission را نمی‌گیرد. Route باید همچنان مالکیت Ticket یا Capability اپراتور را بررسی کند.

مرحله ۷: نمایش امن جزئیات Ticket

اگر URL یک Ticket چیزی شبیه /my-account/support-tickets/UUID/ باشد، دانستن UUID به‌تنهایی نباید مجوز مشاهده پرونده باشد.

function irhd_get_ticket_for_user(
    string $uuid,
    int $user_id
) {
    global $wpdb;

    $table =
        $wpdb->prefix . 'irhd_tickets';

    return $wpdb->get_row(
        $wpdb->prepare(
            "
            SELECT *
            FROM {$table}
            WHERE public_id = %s
              AND user_id = %d
            LIMIT 1
            ",
            $uuid,
            $user_id
        )
    );
}

در پنل Agent شرط متفاوت است: کاربر باید manage_irhd_tickets داشته باشد. هیچ Route مدیریتی را صرفاً با Login بودن باز نکنید.

مرحله ۸: پاسخ مشتری و اپراتور

هر پاسخ یک ردیف جدید در جدول Messages است. متن قبلی را بازنویسی نمی‌کنیم؛ چون Help Desk باید تاریخچه واقعی گفت‌وگو را حفظ کند.

function irhd_rest_reply(
    WP_REST_Request $request
) {
    global $wpdb;

    $uuid = sanitize_text_field(
        $request['uuid']
    );

    $body = sanitize_textarea_field(
        (string) $request->get_param(
            'message'
        )
    );

    if ( '' === $body ) {
        return new WP_Error(
            'empty_message',
            'متن پاسخ خالی است.',
            array( 'status' => 400 )
        );
    }

    $ticket = irhd_get_ticket_for_user(
        $uuid,
        get_current_user_id()
    );

    $is_agent = current_user_can(
        'manage_irhd_tickets'
    );

    if ( ! $ticket && $is_agent ) {
        $ticket = irhd_get_ticket_by_uuid(
            $uuid
        );
    }

    if ( ! $ticket ) {
        return new WP_Error(
            'ticket_not_found',
            'تیکت پیدا نشد.',
            array( 'status' => 404 )
        );
    }

    if (
        'closed' === $ticket->status &&
        ! $is_agent
    ) {
        return new WP_Error(
            'ticket_closed',
            'این تیکت بسته شده است.',
            array( 'status' => 409 )
        );
    }

    $author_type =
        $is_agent ? 'agent' : 'customer';

    $new_status =
        $is_agent
        ? 'waiting_customer'
        : 'waiting_support';

    $messages =
        $wpdb->prefix . 'irhd_messages';

    $tickets =
        $wpdb->prefix . 'irhd_tickets';

    $now = current_time(
        'mysql',
        true
    );

    $wpdb->insert(
        $messages,
        array(
            'ticket_id' => $ticket->id,
            'author_user_id' =>
                get_current_user_id(),
            'author_type' =>
                $author_type,
            'body' => $body,
            'is_internal' => 0,
            'created_at' => $now,
        )
    );

    $update = array(
        'status' => $new_status,
        'updated_at' => $now,
    );

    if (
        $is_agent &&
        empty(
            $ticket->first_response_at
        )
    ) {
        $update[
            'first_response_at'
        ] = $now;
    }

    $wpdb->update(
        $tickets,
        $update,
        array(
            'id' => $ticket->id,
        )
    );

    irhd_log_event(
        (int) $ticket->id,
        'reply_added',
        array(
            'author_type' =>
                $author_type,
        )
    );

    irhd_queue_notification(
        'reply_added',
        (int) $ticket->id
    );

    return rest_ensure_response(
        array(
            'success' => true,
            'status' => $new_status,
        )
    );
}

وضعیت‌های Ticket را ساده و قابل فهم نگه دارید

چرخه پیشنهادی وضعیت
وضعیت معنی آخرین اقدام از طرف
waiting_support تیم پشتیبانی باید پاسخ دهد مشتری
waiting_customer منتظر پاسخ مشتری اپراتور
resolved مشکل حل شده ولی قابل بازگشایی است اپراتور
closed پرونده پایان یافته است سیستم یا اپراتور

مرحله ۹: پنل اپراتور در مدیریت WordPress

فایل includes/admin.php یک زیرمنو زیر WooCommerce ایجاد می‌کند. فقط کاربری که Capability پشتیبانی دارد آن را می‌بیند.

<?php

add_action(
    'admin_menu',
    function() {
        add_submenu_page(
            'woocommerce',
            'تیکت‌های پشتیبانی',
            'تیکت‌های پشتیبانی',
            'manage_irhd_tickets',
            'irhd-tickets',
            'irhd_render_admin_page'
        );
    }
);

برای لیست مدیریت همه Ticketها را یک‌جا بارگذاری نکنید. Pagination داشته باشید و فقط مثلاً ۵۰ ردیف در هر صفحه بخوانید.

$per_page = 50;

$page = max(
    1,
    absint(
        $_GET['paged'] ?? 1
    )
);

$offset =
    ( $page - 1 ) * $per_page;

$tickets = $wpdb->get_results(
    $wpdb->prepare(
        "
        SELECT *
        FROM {$wpdb->prefix}irhd_tickets
        ORDER BY updated_at DESC
        LIMIT %d OFFSET %d
        ",
        $per_page,
        $offset
    )
);

پنل نهایی بهتر است فیلتر Status، Priority، Agent، Department و Order ID داشته باشد. Indexهایی که در جدول ساختیم دقیقاً برای کاهش هزینه همین Queryها هستند.

مرحله ۱۰: وضعیت، اولویت و مسئول Ticket را از Route مدیریتی تغییر دهید

هر مقدار دریافت‌شده از Browser را مستقیم در دیتابیس ذخیره نکنید. Status و Priority باید از فهرست مجاز انتخاب شوند.

function irhd_rest_update_ticket(
    WP_REST_Request $request
) {
    global $wpdb;

    $ticket = irhd_get_ticket_by_uuid(
        sanitize_text_field(
            $request['uuid']
        )
    );

    if ( ! $ticket ) {
        return new WP_Error(
            'ticket_not_found',
            'تیکت پیدا نشد.',
            array( 'status' => 404 )
        );
    }

    $allowed_status = array(
        'waiting_support',
        'waiting_customer',
        'resolved',
        'closed',
    );

    $allowed_priority = array(
        'low',
        'normal',
        'high',
        'urgent',
    );

    $status = sanitize_key(
        (string) $request->get_param(
            'status'
        )
    );

    $priority = sanitize_key(
        (string) $request->get_param(
            'priority'
        )
    );

    $agent_id = absint(
        $request->get_param(
            'assigned_user_id'
        )
    );

    $data = array();

    if (
        $status &&
        in_array(
            $status,
            $allowed_status,
            true
        )
    ) {
        $data['status'] = $status;
    }

    if (
        $priority &&
        in_array(
            $priority,
            $allowed_priority,
            true
        )
    ) {
        $data['priority'] =
            $priority;
    }

    if ( $agent_id ) {
        $agent = get_user_by(
            'id',
            $agent_id
        );

        if (
            ! $agent ||
            ! user_can(
                $agent,
                'manage_irhd_tickets'
            )
        ) {
            return new WP_Error(
                'invalid_agent',
                'اپراتور انتخاب‌شده مجوز پشتیبانی ندارد.',
                array( 'status' => 400 )
            );
        }

        $data['assigned_user_id'] =
            $agent_id;
    }

    $data['updated_at'] =
        current_time(
            'mysql',
            true
        );

    $wpdb->update(
        $wpdb->prefix . 'irhd_tickets',
        $data,
        array(
            'id' => $ticket->id,
        )
    );

    irhd_log_event(
        (int) $ticket->id,
        'ticket_updated',
        $data
    );

    return rest_ensure_response(
        array(
            'success' => true,
        )
    );
}

مرحله ۱۱: اطلاعات سفارش را کنار Ticket نشان دهید

اگر Ticket به سفارش وصل است، اپراتور نباید برای دیدن وضعیت خرید به صفحه دیگری برود. اما فقط اطلاعاتی را نشان دهید که برای پشتیبانی لازم است.

$order = $ticket->order_id
    ? wc_get_order(
        $ticket->order_id
    )
    : false;

if ( $order ) {
    $summary = array(
        'number' =>
            $order->get_order_number(),
        'status' =>
            $order->get_status(),
        'total' =>
            $order->get_formatted_order_total(),
        'payment' =>
            $order->get_payment_method_title(),
        'date' =>
            $order->get_date_created(),
    );
}

محصولات سفارش را نیز می‌توانید با $order->get_items() نمایش دهید. Secret درگاه، Metaهای داخلی افزونه‌ها و اطلاعاتی که به حل Ticket ربط ندارند را در پنل Agent نمایش ندهید.

مرحله ۱۲: فایل پیوست را واقعاً خصوصی نگه دارید

Screenshot، PDF یا Log می‌تواند حاوی اطلاعات خصوصی باشد. اگر فایل را مثل تصویر معمولی داخل Media Library قرار دهید، ممکن است URL مستقیمی داشته باشد که بدون Login قابل بازشدن است.

برای نسخه Production بهتر است فایل‌های Help Desk در مسیری خارج از Web Root نگهداری شوند. مثلاً در wp-config.php مسیر خصوصی سرور را تعریف کنید:

define(
    'IRHD_PRIVATE_STORAGE_DIR',
    '/path/outside/public_html/iranica-helpdesk'
);

این مسیر نمونه است و باید با مسیر واقعی سرور جایگزین شود. اگر هاست شما اجازه پوشه خصوصی خارج از Public Root نمی‌دهد، قبل از فعال‌کردن Attachment معماری دانلود امن را با مدیر سرور بررسی کنید.

اعتبارسنجی فایل

function irhd_store_private_file(
    array $file
) {
    if (
        ! defined(
            'IRHD_PRIVATE_STORAGE_DIR'
        )
    ) {
        return new WP_Error(
            'private_storage_missing',
            'مسیر ذخیره خصوصی تنظیم نشده است.'
        );
    }

    if (
        ! isset( $file['tmp_name'] ) ||
        ! is_uploaded_file(
            $file['tmp_name']
        )
    ) {
        return new WP_Error(
            'invalid_upload',
            'فایل آپلود معتبر نیست.'
        );
    }

    if (
        (int) $file['size'] >
        5 * MB_IN_BYTES
    ) {
        return new WP_Error(
            'file_too_large',
            'حجم فایل بیش از حد مجاز است.'
        );
    }

    $allowed = array(
        'jpg'  => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'png'  => 'image/png',
        'webp' => 'image/webp',
        'pdf'  => 'application/pdf',
        'txt'  => 'text/plain',
    );

    $check =
        wp_check_filetype_and_ext(
            $file['tmp_name'],
            $file['name'],
            $allowed
        );

    if (
        empty( $check['ext'] ) ||
        empty( $check['type'] )
    ) {
        return new WP_Error(
            'file_type_denied',
            'نوع فایل مجاز نیست.'
        );
    }

    $dir =
        rtrim(
            IRHD_PRIVATE_STORAGE_DIR,
            DIRECTORY_SEPARATOR
        );

    if (
        ! is_dir( $dir ) &&
        ! wp_mkdir_p( $dir )
    ) {
        return new WP_Error(
            'storage_create_failed',
            'پوشه خصوصی ساخته نشد.'
        );
    }

    $stored_name =
        wp_generate_uuid4() .
        '.' .
        $check['ext'];

    $destination =
        $dir .
        DIRECTORY_SEPARATOR .
        $stored_name;

    if (
        ! move_uploaded_file(
            $file['tmp_name'],
            $destination
        )
    ) {
        return new WP_Error(
            'move_failed',
            'ذخیره فایل ناموفق بود.'
        );
    }

    return array(
        'stored_name' => $stored_name,
        'original_name' =>
            sanitize_file_name(
                $file['name']
            ),
        'mime_type' => $check['type'],
        'size_bytes' =>
            (int) $file['size'],
    );
}

عدد ۵ مگابایت فقط سقف نمونه است. برای فروشگاه واقعی آن را از تنظیمات قابل تغییر کنید.

دانلود امن فایل

برای Download یک REST Route یا Controller بسازید که ابتدا Attachment را به Message و Message را به Ticket وصل کند؛ سپس مالکیت مشتری یا Capability Agent را بررسی کند. فقط بعد از این بررسی فایل با PHP خوانده شود. هیچ URL مستقیم فایل خصوصی به Browser ندهید.

فایل‌های خطرناک: PHP، PHAR، executable، HTML ناشناخته و فرمت‌هایی که برای پشتیبانی لازم نیستند را قبول نکنید. فقط تغییر نام فایل کافی نیست؛ نوع فایل نیز باید بررسی شود.

مرحله ۱۳: اعلان ایمیلی را از درخواست اصلی جدا کنید

ارسال ایمیل ممکن است کند باشد. کاربر نباید هنگام ثبت Ticket منتظر ارتباط SMTP بماند. WooCommerce از Action Scheduler برای کارهای پس‌زمینه استفاده می‌کند و می‌توان اعلان Help Desk را نیز به Queue سپرد.

فایل includes/notifications.php:

<?php

defined( 'ABSPATH' ) || exit;

function irhd_queue_notification(
    string $type,
    int $ticket_id
) {
    if (
        function_exists(
            'as_enqueue_async_action'
        )
    ) {
        as_enqueue_async_action(
            'irhd_send_notification',
            array(
                $type,
                $ticket_id,
            ),
            'iranica-helpdesk',
            true
        );

        return;
    }

    do_action(
        'irhd_send_notification',
        $type,
        $ticket_id
    );
}

add_action(
    'irhd_send_notification',
    'irhd_send_notification_email',
    10,
    2
);

ایمیل بهتر است فقط موضوع، شماره Ticket و لینک ورود را داشته باشد؛ اطلاعات حساس یا کل متن Log را وارد ایمیل نکنید.

برای بررسی Queue به مسیر WooCommerce ← Status ← Scheduled Actions بروید. اگر Actionهای Help Desk در وضعیت Failed هستند، روی Log همان Action کلیک کنید.

اگر بعدها Worker یا Endpoint پشتیبانی را روی VPS جدا اجرا کردید، آموزش نصب Uptime Kuma روی VPS می‌تواند برای مانیتور کردن در دسترس‌بودن آن سرویس مکمل مناسبی باشد.

مرحله ۱۴: SLA و تیکت‌های عقب‌افتاده

SLA در این مقاله یعنی هدف زمانی پاسخ یا حل Ticket. این عدد باید از سیاست واقعی کسب‌وکار بیاید. برای نمونه فنی، ستون first_response_due_at را در جدول ساختیم.

یک Job دوره‌ای می‌تواند Ticketهایی را که هنوز پاسخ اول نگرفته‌اند و Deadline آن‌ها گذشته است پیدا کند:

function irhd_schedule_sla_scan() {
    if (
        ! function_exists(
            'as_has_scheduled_action'
        ) ||
        ! function_exists(
            'as_schedule_recurring_action'
        )
    ) {
        return;
    }

    if (
        as_has_scheduled_action(
            'irhd_sla_scan',
            array(),
            'iranica-helpdesk'
        )
    ) {
        return;
    }

    as_schedule_recurring_action(
        time() + 300,
        300,
        'irhd_sla_scan',
        array(),
        'iranica-helpdesk',
        true
    );
}

add_action(
    'action_scheduler_init',
    'irhd_schedule_sla_scan'
);

Job می‌تواند Ticket overdue را در داشبورد برجسته کند یا برای Supervisor اعلان بسازد. بهتر است SLA را خودکار «حل‌شده» نکنید؛ عبور از زمان هدف به معنی حل مشکل نیست.

مرحله ۱۵: Audit Log؛ چه کسی چه چیزی را تغییر داد؟

اگر Priority از Normal به Urgent تغییر کرد یا Ticket از Agent اول به Agent دوم منتقل شد، باید بدانید چه کسی این کار را انجام داده است. جدول Events برای همین منظور است.

function irhd_log_event(
    int $ticket_id,
    string $type,
    array $data = array()
) {
    global $wpdb;

    $wpdb->insert(
        $wpdb->prefix . 'irhd_events',
        array(
            'ticket_id' => $ticket_id,
            'actor_user_id' =>
                get_current_user_id()
                ?: null,
            'event_type' =>
                sanitize_key( $type ),
            'event_data' =>
                wp_json_encode(
                    $data,
                    JSON_UNESCAPED_UNICODE
                ),
            'created_at' =>
                current_time(
                    'mysql',
                    true
                ),
        )
    );
}

برای امنیت، Secret، Password، Token، Cookie یا اطلاعات کارت را وارد Event Data نکنید.

مرحله ۱۶: Rate Limit و مقابله با Spam

Login بودن کاربر به معنی امن‌بودن کامل Endpoint نیست. اگر حسابی هک شود یا یک اسکریپت خودکار بنویسد، می‌تواند هزاران Ticket بسازد.

برای نسخه ساده می‌توانید تعداد ساخت Ticket در یک بازه کوتاه را با Transient کنترل کنید:

function irhd_rate_limited(
    int $user_id
): bool {
    $key =
        'irhd_rate_' . $user_id;

    $state = get_transient(
        $key
    );

    if ( ! is_array( $state ) ) {
        set_transient(
            $key,
            array(
                'count' => 1,
            ),
            10 * MINUTE_IN_SECONDS
        );

        return false;
    }

    $count =
        (int) $state['count'];

    if ( $count >= 5 ) {
        return true;
    }

    $state['count'] =
        $count + 1;

    set_transient(
        $key,
        $state,
        10 * MINUTE_IN_SECONDS
    );

    return false;
}

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

تیکت مهمان؛ چرا در نسخه پایه فعالش نمی‌کنیم؟

کاربر Loginشده هویت مشخصی دارد و می‌توان Ticket را به User ID او وصل کرد. Guest Ticket پیچیده‌تر است؛ اگر فقط Ticket ID و ایمیل را داخل URL قرار دهید، امکان حدس یا افشای پرونده وجود دارد.

نسخه مهمان حرفه‌ای باید Token تصادفی قوی، ذخیره Hash توکن در دیتابیس، تاریخ انقضا و تأیید ایمیل داشته باشد. تا زمانی که این مسیر کامل نشده، بهتر است برای Ticket خصوصی از مشتری بخواهید وارد حساب شود.

Reply by Email چگونه ساخته می‌شود؟

Help Deskهای حرفه‌ای اجازه می‌دهند مشتری مستقیماً به ایمیل پاسخ دهد و پاسخ او به Ticket اضافه شود. این قابلیت را نباید با یک Inbox ساده و Password ذخیره‌شده داخل گزینه WordPress پیاده کرد.

دو مسیر متداول وجود دارد: دریافت ایمیل با OAuth/IMAP در یک Worker امن، یا استفاده از Webhook ورودی سرویس ایمیل. هر پیام باید به یک Ticket معتبر Match شود، فرستنده بررسی شود و Attachmentها همان قوانین امنیتی فایل را طی کنند. افزونه‌های Help Desk آماده WooCommerce نیز معمولاً برای تبدیل ایمیل به Ticket از IMAP یا OAuth استفاده می‌کنند.

این قابلیت را بهتر است بعد از پایدارشدن نسخه وب اضافه کنید؛ چون Parsing ایمیل، Threading و جلوگیری از Loop ایمیلی خود یک زیرسیستم مستقل است.

اتصال Help Desk به CRM چه زمانی مفید است؟

اگر CRM دارید، لازم نیست همه Messageها را دوباره داخل CRM ذخیره کنید. بهتر است رویدادهای مهم مانند «Ticket جدید»، «Ticket فوری»، «مشکل حل شد» یا «نارضایتی مشتری» به Timeline مشتری فرستاده شوند.

Help Desk باید منبع اصلی جزئیات گفت‌وگو باقی بماند و CRM فقط Context لازم برای تیم فروش را دریافت کند. این کار از ساخت دو دیتابیس موازی برای یک مکالمه جلوگیری می‌کند.

آیا Ticketها را با Delete افزونه پاک کنیم؟

خیر، نه به‌صورت پیش‌فرض. Deactivate ممکن است موقت باشد. حذف خودکار Ticketهای مشتری هنگام Deactivate رفتار خطرناکی است.

اگر می‌خواهید Uninstall کامل امکان‌پذیر باشد، یک گزینه مدیریتی مثل «حذف داده‌ها هنگام Uninstall» بسازید و فقط در صورت فعال‌بودن آن، جدول‌ها را در uninstall.php حذف کنید.

<?php

if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    exit;
}

if (
    'yes' !== get_option(
        'irhd_purge_on_uninstall',
        'no'
    )
) {
    return;
}

global $wpdb;

$tables = array(
    $wpdb->prefix . 'irhd_attachments',
    $wpdb->prefix . 'irhd_messages',
    $wpdb->prefix . 'irhd_events',
    $wpdb->prefix . 'irhd_tickets',
);

foreach ( $tables as $table ) {
    $wpdb->query(
        "DROP TABLE IF EXISTS {$table}"
    );
}

delete_option(
    'irhd_db_version'
);

delete_option(
    'irhd_purge_on_uninstall'
);

قبل از Purge: Ticketها را Export و بکاپ کنید. حذف جدول‌ها برگشت‌پذیر نیست مگر نسخه پشتیبان دیتابیس داشته باشید.

خطاهای رایج و روش رفع آن‌ها

عیب‌یابی مرحله‌ای
خطا علت محتمل راه‌حل
صفحه Support 404 Rewrite Rules قدیمی است Permalink را یک بار Save کنید و Activation Hook را بررسی کنید.
REST API خطای 403 Nonce یا Session معتبر نیست هدر X-WP-Nonce، Login کاربر و Cookie را بررسی کنید.
کاربر سفارش دیگران را می‌بیند Ownership Check ناقص است get_user_id سفارش را با User ID فعلی مقایسه کنید.
اعلان ایمیل ارسال نمی‌شود Action Scheduler یا SMTP مشکل دارد WooCommerce → Status → Scheduled Actions و لاگ Mail را بررسی کنید.
Actionها Pending مانده‌اند WP-Cron یا Loopback مشکل دارد Cron، Loopback و محدودیت PHP را بررسی کنید.
فایل Upload نمی‌شود نوع، حجم یا مسیر خصوصی نامعتبر است MIME، upload_max_filesize و IRHD_PRIVATE_STORAGE_DIR را بررسی کنید.
dbDelta جدول را درست نمی‌سازد قالب SQL مورد انتظار dbDelta رعایت نشده PRIMARY KEY، KEYها، طول varchar و ساختار خطوط SQL را با مستندات WordPress تطبیق دهید.

چک‌لیست امنیت نسخه Production

🔹 هر Ticket فقط برای صاحب آن یا Agent مجاز قابل مشاهده باشد.

🔹 Nonce در کنار Permission و Ownership Check استفاده شود؛ نه به‌جای آن‌ها.

🔹 اپراتور Help Desk Administrator نباشد.

🔹 فایل خصوصی URL عمومی مستقیم نداشته باشد.

🔹 Upload فقط MIMEهای ضروری و حجم محدود را بپذیرد.

🔹 متن Ticket قبل از ذخیره Sanitized و هنگام خروج Escape شود.

🔹 Endpoint ساخت Ticket Rate Limit داشته باشد.

🔹 Token، Password و Secret داخل Audit Log نوشته نشوند.

🔹 حذف داده‌ها در Deactivate انجام نشود.

🔹 قبل از Upgrade ساختار جدول، DB Version و Migration داشته باشید.

Help Desk چه زمانی به VPS جدا نیاز پیدا می‌کند؟

اگر سیستم فقط چند Ticket در روز دارد، اجرای آن داخل WordPress معمولاً ساده‌تر است. اما در پروژه‌ای با فایل‌های زیاد، Email Piping، جست‌وجوی سنگین، اتصال CRM، Workerهای متعدد یا چند فروشگاه، جداکردن سرویس‌های پس‌زمینه از هاست اصلی می‌تواند Checkout و صفحات فروشگاه را از پردازش پشتیبانی مستقل‌تر کند.

تماس با پشتیبانی ایرانیکاسرور: 021-91302467

پرسش‌های متداول

آیا WooCommerce خودش سیستم تیکت دارد؟

هسته WooCommerce بخش حساب مشتری و سفارش‌ها را دارد، اما Help Desk کامل با Ticket مستقل، Agent، Priority، Queue و تاریخچه پاسخ جزو Endpointهای اصلی هسته نیست. برای این قابلیت می‌توان از افزونه آماده یا توسعه اختصاصی استفاده کرد.

آیا هر تیکت باید به سفارش متصل باشد؟

خیر. اتصال سفارش باید اختیاری باشد. مشتری ممکن است قبل از خرید، درباره حساب کاربری یا درباره موضوعی که به Order مشخصی مربوط نیست سؤال داشته باشد.

برای Ticket از Custom Post Type استفاده کنیم یا جدول اختصاصی؟

برای سیستم کوچک هر دو روش قابل اجرا هستند. در Help Desk پرترافیک با Message زیاد، فیلتر، Assignment، SLA و گزارش، جدول اختصاصی معمولاً Query و Index قابل کنترل‌تری ایجاد می‌کند.

آیا مشتری مهمان می‌تواند Ticket بزند؟

بله، اما نسخه مهمان به Token امن، تاریخ انقضا و تأیید ایمیل نیاز دارد. نسخه پایه این آموزش برای کاهش ریسک افشای Ticket فقط کاربران Loginشده را می‌پذیرد.

چطور فایل پیوست Ticket را خصوصی نگه داریم؟

فایل را خارج از مسیر عمومی وب نگه دارید و Download را از Controllerی عبور دهید که قبل از ارسال فایل، مالکیت Ticket یا مجوز Agent را بررسی می‌کند.

آیا می‌توان Ticket را از ایمیل ساخت؟

بله. برای نسخه حرفه‌ای می‌توان از IMAP/OAuth یا Webhook سرویس ایمیل استفاده کرد، اما باید Threading، شناسایی فرستنده، Attachment و جلوگیری از Loop ایمیلی نیز طراحی شوند.

آیا این روش با HPOS ووکامرس سازگار است؟

Ticketها مستقل از دیتابیس سفارش ذخیره می‌شوند و اطلاعات Order با APIهای WooCommerce مانند wc_get_order و wc_get_orders خوانده می‌شود؛ بنابراین کد به Query مستقیم جدول‌های قدیمی سفارش وابسته نیست.

منابع رسمی و فنی

🔹 WooCommerce Endpoints — توضیح Endpointهای My Account و ساختار URLهای حساب مشتری.

🔹 wc_get_orders و WC_Order_Query — روش پیشنهادی WooCommerce برای Query سفارش بدون وابستگی مستقیم به دیتابیس.

🔹 WordPress register_rest_route — مرجع Routeهای REST و الزام permission_callback.

🔹 WordPress REST Authentication — Cookie Authentication و X-WP-Nonce برای درخواست‌های داخل سایت.

🔹 Creating Tables with Plugins — مرجع dbDelta و ساخت/ارتقای جدول‌های افزونه.

🔹 WordPress Roles and Capabilities — مرجع ساخت Role و کنترل دسترسی اپراتور.

🔹 Action Scheduler API — مرجع Queue و Scheduled Action برای اعلان و SLA.

🔹 WooCommerce Scheduled Actions — مشاهده Queue، Actionهای Failed و Logهای پردازش پس‌زمینه.

🔹 WSDesk در WooCommerce Marketplace — نمونه یک Help Desk آماده با Agent، Ticket، Email-to-Ticket و ارتباط با اطلاعات سفارش.

🔹 WooCommerce 11.2.1 Release Notes — نسخه پایدار بررسی‌شده هنگام بازنگری این آموزش.

جمع‌بندی

برای ساخت Help Desk قابل اتکا در WooCommerce، Ticket را مستقل از Order طراحی کنید و فقط در صورت نیاز شناسه سفارش را به آن متصل کنید. پیام‌ها، فایل‌ها و Audit Log باید ساختار جدا داشته باشند؛ My Account با Endpoint اختصاصی گسترش پیدا کند؛ مالکیت Ticket و Order در هر درخواست بررسی شود؛ Agent مجوز محدود خودش را داشته باشد؛ فایل خصوصی از URL عمومی قابل دریافت نباشد؛ اعلان و SLA با Queue اجرا شوند و حذف دائمی داده فقط در Uninstall آگاهانه انجام شود. این معماری از یک فرم تماس ساده پیچیده‌تر است، اما پایه مناسبی برای اتصال بعدی به CRM، ایمیل، گزارش عملکرد و چند دپارتمان فراهم می‌کند.

برای اجرای Help Desk سنگین یا Workerهای پشتیبانی کمک فنی می‌خواهید؟

اگر سیستم شما Email Piping، Queue، فایل‌های زیاد، CRM یا چند فروشگاه دارد و می‌خواهید پردازش‌های پشتیبانی را از هاست فروشگاه جدا کنید، می‌توانید زیرساخت را روی VPS مستقل اجرا کنید یا برای بررسی PHP، Nginx، Worker و Cron درخواست کانفیگ ثبت کنید.

تماس با پشتیبانی ایرانیکاسرور: 021-91302467

اشتراک‌گذاری مقاله
گفت‌وگو درباره آموزش

دیدگاه‌ها (۰)

هنوز دیدگاهی ثبت نشده است.

دیدگاه یا سؤال خود را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *