[WordPress] WP-Cron 排程任務開發教學

本篇文章更新時間:2026/03/02
如有資訊過時或語誤之處,歡迎使用 Contact 功能通知或向一介資男的 LINE 社群反應。
如果本站內容對你有幫助,歡迎贊助支持


本系列文參考自 WordPress.org 官方外掛開發文件 - Cron 的繁體中文版本,並加入作者實務開發經驗補充。

WordPress WP-Cron 是 WordPress 內建的排程任務系統,讓外掛開發者能夠安排程式碼在特定時間或固定間隔自動執行。無論是定期清理過期資料、定時發送電子郵件通知、同步外部 API 資料,還是產生報表,WP-Cron 都是實現這些自動化需求的標準方式。然而,WP-Cron 的運作機制與伺服器層級的系統 Cron 有根本性的差異——它並非由作業系統在精確的時間點觸發,而是依賴網站的頁面訪問來驅動。這個特性帶來了便利性,同時也帶來了限制。本文將從 WP-Cron 的核心原理開始,逐步介紹如何建立重複排程、單次排程、自訂排程間隔,以及在外掛停用時正確清除排程的最佳實踐。

WP-Cron 與系統 Cron 的差異

在深入 API 之前,必須先理解 WP-Cron 和傳統系統 Cron(如 Linux 的 crontab)之間的根本差異,因為這直接影響你如何設計排程邏輯。

系統 Cron 是作業系統層級的排程服務。你設定一個時間規則(例如「每天凌晨 3 點」),作業系統會精確地在該時間點觸發指定的命令。它不依賴任何應用程式的狀態,只要伺服器在運行,排程就會準時執行。

WP-Cron 則完全不同。它的觸發機制是:每次有訪客載入 WordPress 頁面時,WordPress 會檢查是否有到期的排程任務需要執行。這意味著:

  • 低流量網站的排程可能延遲:如果你設定了一個每小時執行的任務,但網站在凌晨 2 點到早上 8 點之間完全沒有訪客,那麼原本應該在凌晨 3 點執行的任務,會延遲到早上 8 點有人造訪時才被觸發。
  • 高流量網站可能產生效能問題:每次頁面載入都會檢查 Cron 排程,在高流量情況下可能造成不必要的資料庫查詢負擔。
  • 不保證精確時間:WP-Cron 只保證任務會在預定時間之後執行,不保證恰好在那個時間點執行。
  • 不需要伺服器存取權限:這是 WP-Cron 最大的優勢——在共享主機等無法存取系統 crontab 的環境中,依然能使用排程功能。

WordPress 預設提供以下排程間隔:

// WordPress 內建的排程間隔
$schedules = array(
    'hourly'     => array(
        'interval' => HOUR_IN_SECONDS,    // 3600 秒
        'display'  => 'Once Hourly',
    ),
    'twicedaily' => array(
        'interval' => 12 * HOUR_IN_SECONDS, // 43200 秒
        'display'  => 'Twice Daily',
    ),
    'daily'      => array(
        'interval' => DAY_IN_SECONDS,     // 86400 秒
        'display'  => 'Once Daily',
    ),
    'weekly'     => array(
        'interval' => WEEK_IN_SECONDS,    // 604800 秒
        'display'  => 'Once Weekly',
    ),
);

你可以透過 wp_get_schedules() 函式取得所有可用的排程間隔,包括其他外掛註冊的自訂間隔。

排程重複事件 - wp_schedule_event()

wp_schedule_event() 是建立重複排程任務的核心函式。它會在指定的時間開始,按照設定的間隔持續重複執行你掛載到指定 Hook 的回呼函式。

/**
 * 排程一個重複事件
 *
 * @param int    $timestamp  首次執行的 Unix 時間戳
 * @param string $recurrence 重複間隔(hourly, twicedaily, daily, weekly 或自訂)
 * @param string $hook       要觸發的 Action Hook 名稱
 * @param array  $args       傳遞給 Hook 回呼函式的參數(選用)
 * @return bool|WP_Error 成功回傳 true,失敗回傳 false 或 WP_Error
 */
wp_schedule_event( $timestamp, $recurrence, $hook, $args );

以下是一個完整的實務範例——建立一個每小時自動清理過期暫存資料的排程任務:

/**
 * 在外掛啟用時註冊排程任務
 *
 * 重要:wp_schedule_event() 應該只在外掛啟用時呼叫一次,
 * 而不是在每次頁面載入時呼叫。重複呼叫會建立重複的排程事件。
 */
function myplugin_activate() {
    // 檢查排程是否已經存在,避免重複註冊
    if ( ! wp_next_scheduled( 'myplugin_cleanup_expired_data' ) ) {
        wp_schedule_event( time(), 'hourly', 'myplugin_cleanup_expired_data' );
    }
}
register_activation_hook( __FILE__, 'myplugin_activate' );

/**
 * 排程任務的實際執行邏輯
 *
 * 這個函式會被 WP-Cron 在每次排程觸發時自動呼叫。
 */
function myplugin_do_cleanup() {
    global $wpdb;

    // 刪除超過 30 天的暫存記錄
    $expiration = time() - ( 30 * DAY_IN_SECONDS );

    $deleted = $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM {$wpdb->prefix}myplugin_logs WHERE created_at < %d",
            $expiration
        )
    );

    if ( false !== $deleted ) {
        error_log( sprintf( 'MyPlugin: 已清除 %d 筆過期記錄', $deleted ) );
    }
}
add_action( 'myplugin_cleanup_expired_data', 'myplugin_do_cleanup' );

這個範例展示了幾個重要的實務原則:

  • 在啟用 Hook 中註冊排程:使用 register_activation_hook() 確保排程只在外掛啟用時建立一次。關於啟用 Hook 的詳細用法,請參考「WordPress 外掛基礎架構教學」。
  • 使用 wp_next_scheduled() 防止重複:在註冊前先檢查同名排程是否已存在。
  • Hook 名稱加上外掛前綴:使用 myplugin_ 前綴避免與其他外掛的 Hook 衝突。
  • 回呼函式獨立定義:將排程觸發的邏輯寫在 add_action() 掛載的函式中,保持程式碼的可讀性與可測試性。

特別注意 $timestamp 參數使用的是 Unix 時間戳(UTC 時間)。如果你想讓任務在特定的本地時間開始執行,需要考慮時區轉換:

// 在每天凌晨 2 點(網站設定的時區)開始執行每日任務
$timezone   = wp_timezone();
$local_time = new DateTime( 'tomorrow 02:00:00', $timezone );
$timestamp  = $local_time->getTimestamp();

if ( ! wp_next_scheduled( 'myplugin_daily_report' ) ) {
    wp_schedule_event( $timestamp, 'daily', 'myplugin_daily_report' );
}

排程單次事件 - wp_schedule_single_event()

有些任務只需要執行一次,而非重複執行。例如:在使用者完成某個操作後延遲發送通知信、在特定時間後自動刪除暫存檔案,或是延遲處理較耗資源的運算。這時候就要使用 wp_schedule_single_event()

/**
 * 排程一個單次事件
 *
 * @param int    $timestamp 執行的 Unix 時間戳
 * @param string $hook      要觸發的 Action Hook 名稱
 * @param array  $args      傳遞給 Hook 回呼函式的參數(選用)
 * @return bool|WP_Error 成功回傳 true,失敗回傳 false 或 WP_Error
 */
wp_schedule_single_event( $timestamp, $hook, $args );

以下是一個實務範例——使用者下單後 30 分鐘自動發送滿意度調查信:

/**
 * 在訂單完成後排程延遲發送調查信
 *
 * @param int $order_id 訂單 ID
 */
function myplugin_schedule_survey( $order_id ) {
    // 30 分鐘後發送
    $timestamp = time() + ( 30 * MINUTE_IN_SECONDS );

    wp_schedule_single_event(
        $timestamp,
        'myplugin_send_survey_email',
        array( $order_id )
    );
}
add_action( 'woocommerce_order_status_completed', 'myplugin_schedule_survey' );

/**
 * 實際發送調查信的邏輯
 *
 * @param int $order_id 訂單 ID
 */
function myplugin_do_send_survey( $order_id ) {
    $order = wc_get_order( $order_id );
    if ( ! $order ) {
        return;
    }

    $email = $order->get_billing_email();
    $name  = $order->get_billing_first_name();

    $subject = sprintf( '%s,感謝您的訂購!請填寫滿意度調查', $name );
    $message = sprintf(
        '親愛的 %s,\n\n您的訂單 #%d 已完成。\n請點擊以下連結填寫滿意度調查:\n%s',
        $name,
        $order_id,
        home_url( '/survey/?order=' . $order_id )
    );

    wp_mail( $email, $subject, $message );
}
add_action( 'myplugin_send_survey_email', 'myplugin_do_send_survey' );

使用 wp_schedule_single_event() 時有一個重要的防重複機制需要注意:如果在未來 10 分鐘內已經有一個相同 Hook 和相同參數的單次事件被排程,新的排程會被忽略。這是 WordPress 為了避免同一事件被重複排程而設計的保護機制。如果你確實需要在短時間內排程多個同名事件,必須傳入不同的參數來區分它們。

// ❌ 第二次呼叫會被忽略(相同 Hook、相同參數、間隔不到 10 分鐘)
wp_schedule_single_event( time() + 60, 'myplugin_process', array( 'task_a' ) );
wp_schedule_single_event( time() + 120, 'myplugin_process', array( 'task_a' ) );

// ✅ 傳入不同參數可以成功排程多次
wp_schedule_single_event( time() + 60, 'myplugin_process', array( 'task_a', 1 ) );
wp_schedule_single_event( time() + 120, 'myplugin_process', array( 'task_a', 2 ) );

自訂排程間隔 - cron_schedules Filter

WordPress 內建的排程間隔(hourly、twicedaily、daily、weekly)不一定能滿足所有需求。例如你可能需要每 5 分鐘檢查一次外部服務狀態,或是每 3 天執行一次資料庫優化。透過 cron_schedules Filter,你可以註冊自訂的排程間隔。

/**
 * 註冊自訂排程間隔
 *
 * @param array $schedules 現有的排程間隔陣列
 * @return array 加入自訂間隔後的陣列
 */
function myplugin_custom_cron_intervals( $schedules ) {
    // 每 5 分鐘
    $schedules['myplugin_every_five_minutes'] = array(
        'interval' => 5 * MINUTE_IN_SECONDS, // 300 秒
        'display'  => esc_html__( 'Every Five Minutes', 'myplugin' ),
    );

    // 每 3 天
    $schedules['myplugin_every_three_days'] = array(
        'interval' => 3 * DAY_IN_SECONDS, // 259200 秒
        'display'  => esc_html__( 'Every Three Days', 'myplugin' ),
    );

    return $schedules;
}
add_filter( 'cron_schedules', 'myplugin_custom_cron_intervals' );

註冊完自訂間隔後,就可以在 wp_schedule_event() 中使用這些間隔名稱:

// 使用自訂的 5 分鐘間隔建立排程
function myplugin_activate() {
    if ( ! wp_next_scheduled( 'myplugin_health_check' ) ) {
        wp_schedule_event( time(), 'myplugin_every_five_minutes', 'myplugin_health_check' );
    }
}
register_activation_hook( __FILE__, 'myplugin_activate' );

function myplugin_do_health_check() {
    $response = wp_remote_get( 'https://api.example.com/status', array(
        'timeout' => 5,
    ) );

    if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
        // 服務異常,發送通知
        wp_mail(
            get_option( 'admin_email' ),
            '外部服務健康檢查失敗',
            '外部服務 API 回應異常,請儘速檢查。'
        );
    }
}
add_action( 'myplugin_health_check', 'myplugin_do_health_check' );

自訂間隔的命名也應該加上外掛前綴(如 myplugin_),避免與其他外掛的自訂間隔衝突。另外,設定過短的間隔(如每分鐘一次)在 WP-Cron 架構下意義不大,因為排程只在有訪客時觸發,而且 WordPress 也有最短間隔為 60 秒的內建限制。

清除排程 - 在外掛停用時正確清理

這是許多開發者容易忽略的重要步驟:當外掛被停用時,必須清除所有已註冊的排程任務。如果不這麼做,停用的外掛所排程的 Hook 仍然會被 WP-Cron 嘗試觸發,但因為對應的回呼函式已不存在(外掛程式碼已經被停用),這些排程會變成無效的「孤兒排程」,徒增資料庫負擔。

/**
 * 外掛停用時清除所有排程任務
 */
function myplugin_deactivate() {
    // 方法一:使用 wp_clear_scheduled_hook() 清除特定 Hook 的所有排程
    // 這會清除該 Hook 下所有排程(不論參數)
    wp_clear_scheduled_hook( 'myplugin_cleanup_expired_data' );
    wp_clear_scheduled_hook( 'myplugin_daily_report' );
    wp_clear_scheduled_hook( 'myplugin_health_check' );
}
register_deactivation_hook( __FILE__, 'myplugin_deactivate' );

如果你的排程任務帶有特定的參數,且你需要更精細地控制要清除哪些排程,可以使用 wp_unschedule_event()

/**
 * 清除帶有特定參數的排程事件
 *
 * wp_unschedule_event() 需要精確的時間戳和參數才能匹配到特定的排程。
 * 通常搭配 wp_next_scheduled() 取得下一次預定執行的時間戳。
 */
function myplugin_unschedule_specific_event() {
    $timestamp = wp_next_scheduled( 'myplugin_sync_data', array( 'source_a' ) );
    if ( $timestamp ) {
        wp_unschedule_event( $timestamp, 'myplugin_sync_data', array( 'source_a' ) );
    }
}

/**
 * 在外掛停用時清除所有排程
 *
 * 對於有多個排程 Hook 的外掛,建議將所有需要清除的 Hook
 * 集中管理在一個陣列中,以確保不會遺漏。
 */
function myplugin_deactivate() {
    $hooks_to_clear = array(
        'myplugin_cleanup_expired_data',
        'myplugin_daily_report',
        'myplugin_health_check',
        'myplugin_sync_data',
    );

    foreach ( $hooks_to_clear as $hook ) {
        wp_clear_scheduled_hook( $hook );
    }
}
register_deactivation_hook( __FILE__, 'myplugin_deactivate' );

最佳做法是將外掛中所有的 Cron Hook 名稱統一定義為常數或集中在一個方法中管理,這樣在停用時就不會遺漏任何排程。完整的外掛啟用與停用 Hook 使用方式,請參考「WordPress 外掛基礎架構教學」。

實務建議

作者實務經驗分享:

1. 高流量網站務必停用 WP-Cron,改用系統 Cron:在 wp-config.php 中加入以下設定停用 WordPress 內建的 Cron 觸發機制,然後透過伺服器的 crontab 定時呼叫 WordPress 的 Cron 處理程式:

// wp-config.php 中加入
define( 'DISABLE_WP_CRON', true );
# 在伺服器的 crontab 中加入(每分鐘執行一次)
* * * * * cd /path/to/wordpress && php wp-cron.php > /dev/null 2>&1

# 或使用 WP-CLI(推薦)
* * * * * cd /path/to/wordpress && wp cron event run --due-now --allow-root > /dev/null 2>&1

這樣做的好處是:排程任務不會受到網站流量的影響,能夠精確地按時執行;同時頁面載入時不再需要檢查 Cron 排程,提升前台效能。

2. 善用 WP-CLI 進行排程管理與除錯:WP-CLI 提供了強大的 Cron 管理命令,在開發和營運時非常實用:

# 列出所有已排程的 Cron 事件
wp cron event list --allow-root

# 手動立即執行特定 Hook 的排程任務(用於測試)
wp cron event run myplugin_cleanup_expired_data --allow-root

# 列出所有可用的排程間隔
wp cron schedule list --allow-root

# 刪除特定的排程事件
wp cron event delete myplugin_health_check --allow-root

3. 排程任務應該具備冪等性:所謂冪等性(Idempotency),是指同一個任務即使被多次執行,結果也應該一致。由於 WP-Cron 在某些情況下(如多個使用者同時訪問觸發 Cron)可能導致同一任務被重複執行,你的回呼函式應該能優雅地處理這種情況。例如使用鎖定機制(Transient Lock):

function myplugin_do_heavy_task() {
    // 使用 Transient 作為鎖定機制,防止同時重複執行
    if ( get_transient( 'myplugin_task_lock' ) ) {
        return; // 上一次執行尚未完成
    }

    // 設定鎖定,有效期 5 分鐘
    set_transient( 'myplugin_task_lock', true, 5 * MINUTE_IN_SECONDS );

    // 執行實際的耗時任務...
    myplugin_process_batch_data();

    // 任務完成,釋放鎖定
    delete_transient( 'myplugin_task_lock' );
}

4. 排程任務中使用合理的錯誤處理:Cron 任務在背景執行,沒有使用者看到結果。因此完善的 error logging 非常重要,使用 error_log() 或自訂的日誌機制記錄任務執行結果。同時要設定合理的 timeout,避免任務卡住影響後續排程。

5. 避免在 initplugins_loaded 中註冊排程:這是最常見的錯誤之一。如果在每次頁面載入都會執行的 Hook 中呼叫 wp_schedule_event(),雖然 WordPress 有內建防重複機制,但每次呼叫仍會進行不必要的資料庫查詢。正確做法是只在外掛啟用時透過 register_activation_hook() 註冊排程。

6. 搭配 Hook 系統使用:WP-Cron 的排程本質上就是在特定時間觸發一個 Action Hook,因此它與 WordPress 的 Hook 系統密切相關。如果你還不熟悉 Action 和 Filter 的運作方式,建議先閱讀「WordPress Hook 教學」。


本文是「WordPress 外掛開發完整指南」系列的第 12 篇。

上一篇:[WordPress] HTTP API 教學 - 外掛串接外部資源的正確方式

下一篇:即將推出 - 國際化(i18n)與在地化


Share:

作者: Chun

WordPress 社群貢獻者、開源社群推廣者。專注於 WordPress 外掛開發、網站效能最佳化、伺服器管理,以及 iDempiere 開源 ERP 導入與客製開發。曾參與 WordCamp Taipei 等社群活動,GitHub Arctic Code Vault Contributor。提供資訊顧問、WordPress 開發教學、主機最佳化與企業 ERP 整合服務。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *


文章
Filter
Apply Filters
Mastodon