*/ private const WC_CORE_TABLES = array( 'wc_admin_note_actions' => array( 'primary_key' => 'action_id', 'post_type_link' => null, 'description' => 'WC admin notes — action buttons', 'since' => 'WC 4.0', ), 'wc_admin_notes' => array( 'primary_key' => 'note_id', 'post_type_link' => null, 'description' => 'WC admin notes', 'since' => 'WC 4.0', ), 'wc_category_lookup' => array( 'primary_key' => 'category_tree_id', 'post_type_link' => null, 'description' => 'Product category hierarchy lookup', 'since' => 'WC 4.6', ), 'wc_customer_lookup' => array( 'primary_key' => 'customer_id', 'post_type_link' => null, 'description' => 'Customer aggregate (anti-EAV by WC core)', 'since' => 'WC 4.0', ), 'wc_download_log' => array( 'primary_key' => 'download_log_id', 'post_type_link' => null, 'description' => 'Downloadable product access log', 'since' => 'WC 3.3', ), 'wc_order_addresses' => array( 'primary_key' => 'id', 'post_type_link' => 'shop_order', 'description' => 'Order billing/shipping addresses (HPOS)', 'since' => 'WC 8.2', ), 'wc_order_coupon_lookup' => array( 'primary_key' => 'order_id', 'post_type_link' => 'shop_order', 'description' => 'Order coupon analytics lookup', 'since' => 'WC 4.0', ), 'wc_order_operational_data' => array( 'primary_key' => 'id', 'post_type_link' => 'shop_order', 'description' => 'Order operational data (HPOS)', 'since' => 'WC 8.2', ), 'wc_order_product_lookup' => array( 'primary_key' => 'order_item_id', 'post_type_link' => 'shop_order', 'description' => 'Order product analytics lookup', 'since' => 'WC 4.0', ), 'wc_order_stats' => array( 'primary_key' => 'order_id', 'post_type_link' => 'shop_order', 'description' => 'Order statistics (anti-EAV by WC core)', 'since' => 'WC 4.0', ), 'wc_order_tax_lookup' => array( 'primary_key' => null, 'post_type_link' => 'shop_order', 'description' => 'Order tax analytics lookup', 'since' => 'WC 4.0', ), 'wc_orders' => array( 'primary_key' => 'id', 'post_type_link' => 'shop_order', 'description' => 'Orders main table (HPOS)', 'since' => 'WC 8.2', ), 'wc_orders_meta' => array( 'primary_key' => 'id', 'post_type_link' => 'shop_order', 'description' => 'Order meta (HPOS replacement for postmeta)', 'since' => 'WC 8.2', ), 'wc_product_attributes_lookup' => array( 'primary_key' => null, 'post_type_link' => 'product', 'description' => 'Product attributes lookup (faceted search)', 'since' => 'WC 4.9', ), 'wc_product_download_directories' => array( 'primary_key' => 'url_id', 'post_type_link' => null, 'description' => 'Approved download directories', 'since' => 'WC 6.5', ), 'wc_product_meta_lookup' => array( 'primary_key' => 'product_id', 'post_type_link' => 'product', 'description' => 'Product meta lookup (anti-EAV by WC core)', 'since' => 'WC 3.6', ), 'wc_rate_limits' => array( 'primary_key' => 'rate_limit_id', 'post_type_link' => null, 'description' => 'API rate limits', 'since' => 'WC 5.6', ), 'wc_reserved_stock' => array( 'primary_key' => null, 'post_type_link' => 'product', 'description' => 'Reserved stock (cart hold)', 'since' => 'WC 4.5', ), 'wc_tax_rate_classes' => array( 'primary_key' => 'tax_rate_class_id', 'post_type_link' => null, 'description' => 'Tax rate classes', 'since' => 'WC 3.7', ), 'wc_webhooks' => array( 'primary_key' => 'webhook_id', 'post_type_link' => null, 'description' => 'Webhooks subscriptions', 'since' => 'WC 2.6', ), ); /** * Wire integration. No-op when WC not detected. * * @return void */ public static function register(): void { if ( ! self::is_active() ) { return; } add_action( 'wpdo_register_custom_tables', array( __CLASS__, 'register_custom_tables' ) ); add_action( 'wpdo_register_fields', array( __CLASS__, 'register_schema_fields' ) ); // v2.1.6: customer hot fields (_money_spent / _order_count / _last_order) move // to Entity_Registry so Hook Bus can route them to wp_wpdo_user_hot once // `entity_user` mode advances past `disabled`. See PLAN-entity-user-cutover.md. add_action( 'wpdo_register_entity_fields', array( __CLASS__, 'register_user_entity_fields' ) ); // HPOS recommendation notice — only on WC admin pages, only when: // (a) HPOS is OFF, (b) the threshold of legacy orders has been crossed. if ( is_admin() ) { add_action( 'admin_notices', array( __CLASS__, 'maybe_render_hpos_notice' ) ); } } /** * Threshold of legacy `shop_order` posts above which we recommend HPOS. * Below this, the HPOS migration overhead isn't justified. */ private const HPOS_RECOMMEND_THRESHOLD = 100; /** * Option name used to permanently dismiss the notice. */ private const HPOS_NOTICE_DISMISSED_OPT = 'wpdo_hpos_notice_dismissed'; /** * Conditionally render the HPOS recommendation admin notice. * * @return void */ public static function maybe_render_hpos_notice(): void { // Already enabled or already dismissed. if ( self::is_hpos_enabled() ) { return; } if ( '1' === (string) get_option( self::HPOS_NOTICE_DISMISSED_OPT, '' ) ) { return; } if ( ! current_user_can( 'manage_woocommerce' ) ) { return; } // Handle dismiss action (lightweight, no separate AJAX endpoint). // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only nonce check below. if ( ! empty( $_GET['wpdo_hpos_dismiss'] ) ) { $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ) : ''; if ( wp_verify_nonce( $nonce, 'wpdo_hpos_dismiss' ) ) { update_option( self::HPOS_NOTICE_DISMISSED_OPT, '1', false ); return; } } $legacy_count = self::count_legacy_orders(); if ( $legacy_count < self::HPOS_RECOMMEND_THRESHOLD ) { return; } $dismiss_url = wp_nonce_url( add_query_arg( 'wpdo_hpos_dismiss', '1' ), 'wpdo_hpos_dismiss' ); $settings_url = admin_url( 'admin.php?page=wc-settings&tab=advanced§ion=features' ); ?>

WP Data Optimizer: WooCommerce → 進階 → 功能', '關閉' ); ?>

get_var( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'shop_order'" ); } /** * Active when WC class loaded OR WC plugin file exists (loose detect for early bootstrap). * * @return bool */ public static function is_active(): bool { return class_exists( 'WooCommerce' ) || ( defined( 'WC_VERSION' ) && WC_VERSION ) || file_exists( WP_PLUGIN_DIR . '/woocommerce/woocommerce.php' ); } /** * HPOS (custom orders table) detection. * * @return bool True when HPOS is enabled. */ public static function is_hpos_enabled(): bool { // WC OrderUtil class — modern WC API. if ( class_exists( 'Automattic\\WooCommerce\\Utilities\\OrderUtil' ) && method_exists( 'Automattic\\WooCommerce\\Utilities\\OrderUtil', 'custom_orders_table_usage_is_enabled' ) ) { return (bool) Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled(); } // Fallback: option check. return 'yes' === get_option( 'woocommerce_custom_orders_table_enabled' ); } /** * Detected WC version (or 'unknown'). * * @return string */ public static function wc_version(): string { if ( defined( 'WC_VERSION' ) ) { return WC_VERSION; } return 'unknown'; } /** * Register all 20 wp_wc_* tables. * * @param mixed $registry TMDO_Custom_Table_Registry (passed by action). * @return void */ public static function register_custom_tables( $registry = null ): void { if ( ! class_exists( 'TMDO_Custom_Table_Registry' ) ) { return; } $registry = $registry ?: TMDO_Custom_Table_Registry::instance(); foreach ( self::WC_CORE_TABLES as $name => $meta ) { $registry->register( self::PROVIDER, array( 'table_name' => $name, 'primary_key' => $meta['primary_key'] ?: 'id', 'post_type_link' => $meta['post_type_link'], 'doctor_callback' => array( __CLASS__, 'doctor_check' ), 'description' => $meta['description'] . ' (' . $meta['since'] . ')', ) ); } // Our own commission tracking table (v2.1.0). $registry->register( self::PROVIDER, array( 'table_name' => 'wpdo_wc_commissions', 'primary_key' => 'id', 'post_type_link' => null, 'doctor_callback' => array( __CLASS__, 'doctor_check' ), 'description' => 'Vendor commission tracking (replaces hpct_wc_orders, HPOS-aware) (WPDO 2.1.0)', ) ); } /** * Register hot-zone meta fields for products + orders + customers. * * Delegates to dedicated registrars to keep this method readable. * * @param mixed $schema TMDO_Schema_Registry (passed by action). * @return void */ public static function register_schema_fields( $schema = null ): void { if ( ! class_exists( 'TMDO_Schema_Registry' ) ) { return; } $schema = $schema ?: TMDO_Schema_Registry::instance(); self::register_product_fields( $schema ); // Order fields only matter when HPOS is OFF (otherwise WC stores them in wc_orders_meta). if ( ! self::is_hpos_enabled() ) { self::register_order_fields( $schema ); } // v2.1.6: Customer usermeta hot fields moved out of Schema_Registry — they // are now registered via `register_user_entity_fields()` on the // `wpdo_register_entity_fields` action so Hook Bus can intercept them. // Schema_Registry never had a user-meta interceptor; the previous // registration was inert. See PLAN-entity-user-cutover.md. // Subscription fields if WC Subscriptions OR wc-linepay-subscription is active. if ( self::has_subscriptions() ) { self::register_subscription_fields( $schema ); } } /** * Product hot fields (search/filter shop pages). * * @param TMDO_Schema_Registry $schema Registry. * @return void */ private static function register_product_fields( $schema ): void { $schema->register_many( self::PROVIDER, array( array( 'post_type' => 'product', 'meta_key' => '_price', 'zone' => 'hot', 'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0', 'column' => '_price', 'indexed' => true, ), array( 'post_type' => 'product', 'meta_key' => '_regular_price', 'zone' => 'hot', 'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0', 'column' => '_regular_price', ), array( 'post_type' => 'product', 'meta_key' => '_sale_price', 'zone' => 'hot', 'data_type' => 'decimal(15,4) NULL', 'column' => '_sale_price', ), array( 'post_type' => 'product', 'meta_key' => '_sku', 'zone' => 'hot', 'data_type' => "varchar(100) NOT NULL DEFAULT ''", 'column' => '_sku', 'indexed' => true, ), array( 'post_type' => 'product', 'meta_key' => '_stock', 'zone' => 'hot', 'data_type' => 'decimal(15,4) NULL', 'column' => '_stock', ), array( 'post_type' => 'product', 'meta_key' => '_stock_status', 'zone' => 'hot', 'data_type' => "varchar(20) NOT NULL DEFAULT 'instock'", 'column' => '_stock_status', 'indexed' => true, ), array( 'post_type' => 'product', 'meta_key' => '_visibility', 'zone' => 'hot', 'data_type' => "varchar(20) NOT NULL DEFAULT 'visible'", 'column' => '_visibility', ), array( 'post_type' => 'product', 'meta_key' => '_featured', 'zone' => 'hot', 'data_type' => "varchar(3) NOT NULL DEFAULT 'no'", 'column' => '_featured', 'indexed' => true, ), array( 'post_type' => 'product', 'meta_key' => '_tax_status', 'zone' => 'hot', 'data_type' => "varchar(20) NOT NULL DEFAULT 'taxable'", 'column' => '_tax_status', ), array( 'post_type' => 'product', 'meta_key' => '_virtual', 'zone' => 'hot', 'data_type' => "varchar(3) NOT NULL DEFAULT 'no'", 'column' => '_virtual', ), array( 'post_type' => 'product', 'meta_key' => '_downloadable', 'zone' => 'hot', 'data_type' => "varchar(3) NOT NULL DEFAULT 'no'", 'column' => '_downloadable', ), ) ); // Cold zone — large display fields rarely searched but always shown. $schema->register_many( self::PROVIDER, array( array( 'post_type' => 'product', 'meta_key' => '_product_attributes', 'zone' => 'cold', 'cache_group' => 'wpdo_cold_product', 'cache_ttl' => HOUR_IN_SECONDS, ), array( 'post_type' => 'product', 'meta_key' => '_purchase_note', 'zone' => 'cold', 'cache_group' => 'wpdo_cold_product', 'cache_ttl' => HOUR_IN_SECONDS, ), ) ); } /** * Order hot fields (legacy postmeta path; only registered when HPOS off). * * @param TMDO_Schema_Registry $schema Registry. * @return void */ private static function register_order_fields( $schema ): void { $schema->register_many( self::PROVIDER, array( array( 'post_type' => 'shop_order', 'meta_key' => '_order_total', 'zone' => 'hot', 'data_type' => 'decimal(15,4) NOT NULL DEFAULT 0', 'column' => '_order_total', 'indexed' => true, ), array( 'post_type' => 'shop_order', 'meta_key' => '_order_currency', 'zone' => 'hot', 'data_type' => "varchar(8) NOT NULL DEFAULT ''", 'column' => '_order_currency', ), array( 'post_type' => 'shop_order', 'meta_key' => '_billing_email', 'zone' => 'hot', 'data_type' => "varchar(254) NOT NULL DEFAULT ''", 'column' => '_billing_email', 'indexed' => true, ), array( 'post_type' => 'shop_order', 'meta_key' => '_payment_method', 'zone' => 'hot', 'data_type' => "varchar(60) NOT NULL DEFAULT ''", 'column' => '_payment_method', 'indexed' => true, ), array( 'post_type' => 'shop_order', 'meta_key' => '_customer_user', 'zone' => 'hot', 'data_type' => 'bigint(20) NOT NULL DEFAULT 0', 'column' => '_customer_user', 'indexed' => true, ), ) ); } /** * Customer usermeta hot fields — registered to Entity_Registry so Hook Bus can * route them to `wp_wpdo_user_hot` once `entity_user` mode advances past `disabled`. * * Replaces the v2.1.5 `register_customer_fields()` which targeted Schema_Registry * (zone-based, no user interceptor) — that path was inert. See * PLAN-entity-user-cutover.md for the migration playbook. * * Type mapping (Entity_Registry uses fixed-precision DECIMAL(18,6) / BIGINT(20), * which is a superset of the prior `decimal(15,4)` / `bigint(20)` declarations — * no precision loss): * - _money_spent → decimal (DECIMAL(18,6)) * - _order_count → integer (BIGINT(20)) * - _last_order → integer (BIGINT(20)) * * @return void */ public static function register_user_entity_fields(): void { if ( ! class_exists( 'TMDO_Entity_Registry' ) ) { return; } TMDO_Entity_Registry::register_group( 'user', 'hot', array( array( 'key' => '_money_spent', 'type' => 'decimal', 'searchable' => true, 'label' => 'WC customer cumulative spend', ), array( 'key' => '_order_count', 'type' => 'integer', 'searchable' => true, 'label' => 'WC customer order count', ), array( 'key' => '_last_order', 'type' => 'integer', 'label' => 'WC customer last order ID', ), // WC runtime stats — written on order/session events, high-frequency // on busy stores. Unregistered they land back in wp_usermeta. array( 'key' => 'wc_last_active', 'type' => 'integer', 'label' => 'WC last customer activity timestamp (Unix)', ), array( // WooCommerce stores this per-site via Users::update_site_user_meta(), // which appends the de-suffixed blog prefix ('wc_order_count' + '_wp' // on the main site, '_wp_2' on a subsite). Compute it the same way // rather than hardcoding the main-site form. 'key' => 'wc_order_count_' . rtrim( $GLOBALS['wpdb']->get_blog_prefix(), '_' ), 'type' => 'integer', 'label' => 'WC order count for the current site (blog-aware key)', ), array( 'key' => 'last_update', 'type' => 'integer', 'label' => 'WC customer last profile update timestamp (Unix)', ), ) ); } /** * Subscription product fields (wc-subscriptions or wc-linepay-subscription). * * @param TMDO_Schema_Registry $schema Registry. * @return void */ private static function register_subscription_fields( $schema ): void { $schema->register_many( self::PROVIDER, array( array( 'post_type' => 'product', 'meta_key' => '_subscription_price', 'zone' => 'hot', 'data_type' => 'decimal(15,4) NULL', 'column' => '_subscription_price', ), array( 'post_type' => 'product', 'meta_key' => '_subscription_period', 'zone' => 'hot', 'data_type' => 'varchar(20) NULL', 'column' => '_subscription_period', ), array( 'post_type' => 'product', 'meta_key' => '_subscription_period_interval', 'zone' => 'hot', 'data_type' => 'int(11) NULL', 'column' => '_subscription_period_interval', ), array( 'post_type' => 'product', 'meta_key' => '_subscription_length', 'zone' => 'hot', 'data_type' => 'int(11) NULL', 'column' => '_subscription_length', ), ) ); } /** * Subscription extension detection — WC native or wc-linepay-subscription. * * @return bool */ private static function has_subscriptions(): bool { return class_exists( 'WC_Subscriptions' ) || defined( 'WCS_INIT_TIMESTAMP' ) || file_exists( WP_PLUGIN_DIR . '/wc-linepay-subscription/wc-linepay-subscription.php' ); } /** * WPDO doctor probe for any wc_* table — verifies existence + reports row count. * * @param string $table_suffix Suffix without prefix. * @return array{ok:bool, message:string} */ public static function doctor_check( string $table_suffix ): array { global $wpdb; $full = $wpdb->prefix . $table_suffix; $exists = (bool) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( 'SHOW TABLES LIKE %s', $full ) ); if ( ! $exists ) { $is_hpos_table = in_array( $table_suffix, array( 'wc_orders', 'wc_orders_meta', 'wc_order_addresses', 'wc_order_operational_data' ), true ); $msg = $is_hpos_table ? "{$full} 不存在 — HPOS 尚未啟用或 WC 版本 < 8.2。" : "{$full} 不存在 — 請確認 WooCommerce 已啟用。"; return array( 'ok' => false, 'message' => $msg, ); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared $count = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$full}`" ); return array( 'ok' => true, 'message' => "{$full}: {$count} rows", ); } }