不要使用TRANSIENT的常规方式来缓存WP_QUERY

缓存WP_Query最佳实践
本文探讨了在WordPress中缓存复杂查询(尤其是涉及元数据的查询)时可能遇到的问题,并提出了解决方案。介绍了为什么不应直接缓存WP_Query对象,而是推荐缓存HTML输出或仅缓存查询得到的ID列表。

WP_Query is one of the most complex classes in the WordPress codebase. It’s extremely powerful and flexible, but that flexibility often results in slower database queries, especially when working with metadata. To speed things up, WordPress developers tend to cache the results of such queries, but there are a few pitfalls you should be aware of.

Caching Queries

The Transients API is likely the top choice for caching WP_Query results, but what we often see is developers storing the whole WP_Query object in a transient, and we believe that’s a mistake. Translating that into code, it looks something like this:

$cache_key = 'my-expensive-query';
if ( ! $query = get_transient( $cache_key ) ) {
    $query = new WP_Query( ... ); // some expensive query;
    set_transient( $cache_key, $query, 24 * HOUR_IN_SECONDS );
}

while ( $query->have_posts() ) {
    $query->the_post();
    // ...
}

Here you can see that we’re passing the $query object directly to set_transient(), so whenever we get a cache hit, we’ll have our query object available, along with all the useful WP_Query properties and methods.

This is a bad idea, and while this works (or at least seems to work), you’ll want to know what’s happening behind the scenes when you call set_transient() in this particular case.

Serialize/Unseriazile

By default, transients in WordPress translate into the Options API. If you’re familiar with how options work internally, you’ll know that the values are serialized before hitting the database, and unseriaziled when retrieved. This is also true for most persistent object caching dropins, including Memcached and Redis.

As an example, just look at what happens when we serialize a small object in PHP:

$object = new stdClass;
$object->foo = 1;
$object->bar = 2;

var_dump( maybe_serialize( $object ) );
// string(47) "O:8:"stdClass":2:{s:3:"foo";i:1;s:3:"bar";i:2;}"

This allows us to store the object, along with all its properties, as a string, which works well in a MySQL table, in a Redis database, etc. When deserializing (or unserializing) such a string, the result is an identical copy of the object we previously had. This is great, but let’s consider a more complex object:

class A {}
class B {
    public $baz = 3;
}
class C {
    public $qux = 4;
}

$object = new A;
$object->foo = new B;
$object->bar = new C;

var_dump( maybe_serialize( $object ) );
// string(84) "O:1:"A":2:{s:3:"foo";O:1:"B":1:{s:3:"baz";i:3;}s:3:"bar";O:1:"C":1:{s:3:"qux";i:4;}}"

This illustrates that PHP’s serialize() function will recursively serialize any object referenced by a property of another object.

Serializing WP_Query

Let’s try and put this in a WP_Query context by running a simple query and serializing it:

$query = new WP_Query( array(
    'post_type' => 'post',
    'post_status' => 'publish',
    'posts_per_page' => 10,
) );

var_dump( maybe_serialize( $query ) );
// string(22183) "O:8:"WP_Query":50:{s:5:"query";a:3:{s:9:"post_type";s:4:"post";s:11:"post_status";s:7:"publish";s:14:"posts_per_page";i:10;}s:10:"query_vars";a:65:{s:9:"post_type";s:4:"post";s:11:"post_status";s:7:"publish";s:14:"posts_per_page"; ... (about 22000 more characters)

The first thing you’ll notice is that the output is extremely long. Indeed, we’re serializing every property of our WP_Query object, including all query variables, parsed query variables, the loop status and current position, all conditional states, a bunch of WP_Post objects we retrieved, as well as any additional referenced objects.

Referenced objects? Let’s take a look at the WP_Query constructor:

public function __construct( $query = '' ) {
    $this->db = $GLOBALS['wpdb'];
    // ...

Now let’s take a closer look at our gigantic serialized string:

... s:5:"*db";O:4:"wpdb":62:{s:11:"show_errors";b:1;s:15:"suppress_errors";b:0; ...

Whoops! But that’s not all. That wpdb object we’re storing as a string in our database will contain our database credentials, all other database settings, as well as the full list of SQL queries along with their timing and stacktraces if SAVEQUERIES was turned on.

The same is true for other referenced objects, such as WP_Meta_Query, WP_Tax_Query, WP_Date_Query, etc. Our goal was to speed that query up, and while we did, we introduced a lot of unnecessary overhead serializing and deserializing complex objects, as well as leaked potentially sensitive information.

But the overhead does not stop there.

Metadata, Terms, Posts & the Object Cache

Okay so now we have a huge serialized string containing the posts that we wanted to cache, along with a bunch of unnecessary data. What happens when we deserialize that string back to a WP_Query object? Well, nothing really…

When deserializing strings into objects, PHP does not run the constructor method (thankfully), but instead runs __wakeup() if it exists. It doesn’t exist in WP_Query, so that’s what happens — nothing, except of course populating all our properties with all those values from the serialized string, restoring nested objects, and objects nested inside those objects. It should be pretty fast, hopefully much faster than running our initial SQL query.

And after we’re done deserializing, even though at that point the WP_Query object is a bit crippled (serialize can’t store resource types, such as mysqli objects), we can still use it:

while ( $query->have_posts() ) {
    $query->the_post();
    the_title();
}

Which doesn’t cause any additional queries against the wp_posts table, since we already have all the necessary data in the $query->posts array. Until we do something like this:

while ( $query->have_posts() ) {
    $query->the_post();
    the_title();

    get_post_meta( get_the_ID(), 'key', true );
}

And this is where things go south.

The Object Cache

When running a regular WP_Query, the whole process (by default) takes care of retrieving the metadata and terms data for all the posts that match our query, and storing all that in the object cache for the request. That happens in the get_posts() method of our object (_prime_post_caches()). But when re-creating the WP_Query object from a string, the method never runs, and so our term and meta caches are never primed.

For that reason, when running get_post_meta() inside our loop, we’ll see a separate SQL query to fetch the metadata for that particular post. And this happens for every post. Separately. Which means that for 10 “cached” posts, we’re looking at 10 additional queries. Sure, they’re pretty fast, but still.

Now let’s add something like the_tags() to the same loop, and voila! We have another ten SQL queries to grab the terms now.

And finally… This is the best part. Let’s add something often done by a typical plugin that alters the post content or title in any way:

add_filter( 'the_title', function( $title ) {
    $post = get_post( get_the_ID() );
    // do something with $post->post_title and $title
    return $title;
} );

Now we’ll see an additional ten database queries for the posts. How did that happen? Didn’t we have those posts cached?

Yes we did, but we had them in our $query->posts array, and get_post() doesn’t know or care about any queries, it simply fetches data from the WordPress object cache, and it was WP_Query‘s job to prime those caches with the data, which it failed to do upon deserializing. Tough luck.

So ultimately, by caching our WP_Query object in a transient, we went from four database queries (found rows, posts, metadata and terms) to only two (transient timeout and transient value) and an additional thirty queries (posts_per_page * 3) if we want to use metadata, terms or anything that calls get_post().

To be fair, those thirty queries are likely much faster than our initial posts query because they’re lookups by primary key, but each one is still a round-trip to the (possibly remote) MySQL server. Sure, you can probably hack your way around it with _prime_post_caches(), but we don’t recommend that.

The Alternatives

Now that we have covered why you shouldn’t cache WP_Query objects, let’s look at a couple of better ways to cache those slow lookups.

The first, easiest and probably best method is to cache the complete HTML output, and PHP’s output buffering functions will help us implement that without moving too much code around:

$cache_key = 'my-expensive-query';
if ( ! $html = get_transient( $cache_key ) ) {
    $query = new WP_Query( ... );
    ob_start();
    while ( $query->have_posts() ) {
        $query->the_post();
        // output all the things
    }
    $html = ob_get_clean();
    set_transient( $cache_key, $html, 24 * HOUR_IN_SECONDS );
}

echo $html;

This way we’re only storing the actual output in our transient, no posts, no metadata, no terms, and most importantly no database passwords. Just the HTML.

If your HTML string is very (very!) long, you may also consider compressing it with gzcompress() and storing it as a base64 encoded string in your database, which is especially efficient if you’re working with memory-based storage, such as Redis or Memcached. The compute overhead to compress/uncompress is very close to zero.

The second method is to cache post IDs from the expensive query, and later perform lookups by those cached IDs which will be extremely fast. Here’s a simple snippet to illustrate the point:

$cache_key = 'my-expensive-query';
if ( ! $ids = get_transient( $cache_key ) ) {
    $query = new WP_Query( array(
        'fields' => 'ids',
        // ...
    ) );

    $ids = $query->posts;
    set_transient( $cache_key, $ids, 24 * HOUR_IN_SECONDS );
}

$query = new WP_Query( array(
    'post__in' => $ids,
) );

// while ( $query->have_posts() ) ...

Here we have two queries. The first query is the slow one, where we can fetch posts by meta values, etc. Note that we ask WP_Query to retrieve IDs only for that query, and later do a very fast lookup using the post__in argument. The expensive query runs only if we don’t already have an array of IDs in our transient.

This method is a bit less efficient than caching the entire HTML output, since we’re (probably) still querying the database. But the flexibility is sometimes necessary, especially when you’d like to cache the query for much longer, but have other unrelated things that may impact your output, such as a shortcode inside the post content.

Profile

Caching is a great way to speed things up, but you have to know exactly what you’re caching, when, where and how, otherwise you risk facing unexpected consequences. If you’re uncertain whether something is working as intended, always turn to profiling — look at each query against the database, look at all PHP function calls, watch for timing and memory usage.

When i click 结算, evrything from cart is sent to checkout. Thats not i want. I want to achieve cart like taobao that can do selective checkout. Please check whats wrong and regenrate the full code for cart.php and functions.php. Also, the 共计 i dont know how to fix. Please make necessary corrections, then regenerate the full code for me to fully replace with the current code in cart.php & functions.php These are the current cart.php & functions.php code Cart.php: <?php /** * Custom Cart Page for WooCommerce with Selective Checkout */ if (!defined('ABSPATH')) { exit; } do_action('woocommerce_before_cart'); // Provide context for JS (no layout change) $pc_cart_is_empty = WC()->cart->is_empty(); function pc_uid_for_view() { if (is_user_logged_in()) return 'user_' . get_current_user_id(); if (empty($_COOKIE['pc_cart_uid'])) { $token = wp_generate_uuid4(); setcookie('pc_cart_uid', $token, time() + YEAR_IN_SECONDS, COOKIEPATH ?: '/', '', is_ssl(), false); $_COOKIE['pc_cart_uid'] = $token; } return 'guest_' . sanitize_text_field(wp_unslash($_COOKIE['pc_cart_uid'])); } $pc_uid = pc_uid_for_view(); ?> <div class="cart-page-section container" style="max-width: 1200px; margin: 0 auto;"> <form class="woocommerce-cart-form" action="<?php echo esc_url( wc_get_cart_url() ); ?>" method="post"> <?php do_action('woocommerce_before_cart_table'); ?> <?php wp_nonce_field('woocommerce-cart', 'woocommerce-cart-nonce'); ?> <table class="shop_table shop_table_responsive cart woocommerce-cart-form__contents"> <thead> <tr> <th class="product-select" style="width: 8%;"> <input type="checkbox" id="select-all-items" /> <label for="select-all-items" style="display: inline-block; margin-left: 5px; cursor: pointer;">全选</label> </th> <th class="product-info"><?php esc_html_e('Product', 'woocommerce'); ?></th> <th class="product-price"><?php esc_html_e('Price', 'woocommerce'); ?></th> <th class="product-quantity"><?php esc_html_e('Quantity', 'woocommerce'); ?></th> <th class="product-subtotal"><?php esc_html_e('Subtotal', 'woocommerce'); ?></th> <th class="product-remove"><?php esc_html_e('操作', 'woocommerce'); ?></th> </tr> </thead> <tbody> <?php do_action('woocommerce_before_cart_contents'); ?> <?php foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) : ?> <?php $_product = apply_filters('woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key); $product_id = apply_filters('woocommerce_cart_item_product_id', $cart_item['product_id'], $cart_item, $cart_item_key); if ( $_product && $_product->exists() && $cart_item['quantity'] > 0 && apply_filters('woocommerce_cart_item_visible', true, $cart_item, $cart_item_key) ) : $product_permalink = apply_filters('woocommerce_cart_item_permalink', $_product->is_visible() ? $_product->get_permalink($cart_item) : '', $cart_item, $cart_item_key); $line_total_value = (float) ($cart_item['line_total'] + $cart_item['line_tax']); $variation_id = isset($cart_item['variation_id']) ? (int)$cart_item['variation_id'] : 0; $variation_data = isset($cart_item['variation']) ? $cart_item['variation'] : array(); $variation_json = wp_json_encode($variation_data); ?> <tr class="woocommerce-cart-form__cart-item <?php echo esc_attr( apply_filters('woocommerce_cart_item_class', 'cart_item', $cart_item, $cart_item_key) ); ?>" data-cart_item_key="<?php echo esc_attr($cart_item_key); ?>" data-product_id="<?php echo esc_attr($product_id); ?>" data-variation_id="<?php echo esc_attr($variation_id); ?>" data-variation="<?php echo esc_attr($variation_json); ?>"> <!-- Checkbox Column --> <td class="product-select" data-title="<?php esc_attr_e('Select', 'woocommerce'); ?>"> <input type="checkbox" class="item-checkbox" name="selected_items[]" value="<?php echo esc_attr($cart_item_key); ?>" data-price="<?php echo esc_attr($line_total_value); ?>" /> </td> <!-- Product Info Column --> <td class="product-info" data-title="<?php esc_attr_e('Product', 'woocommerce'); ?>"> <div class="product-image"> <?php $thumbnail = apply_filters('woocommerce_cart_item_thumbnail', $_product->get_image(), $cart_item, $cart_item_key); if ( ! $product_permalink ) : echo $thumbnail; // PHPCS: XSS ok. else : printf('<a href="%s">%s</a>', esc_url($product_permalink), $thumbnail); // PHPCS: XSS ok. endif; ?> </div> <div class="product-name"> <?php if ( ! $product_permalink ) : echo wp_kses_post( apply_filters('woocommerce_cart_item_name', $_product->get_name(), $cart_item, $cart_item_key) . ' ' ); else : echo wp_kses_post( apply_filters('woocommerce_cart_item_name', sprintf('<a href="%s">%s</a>', esc_url($product_permalink), $_product->get_name()), $cart_item, $cart_item_key) ); endif; do_action('woocommerce_after_cart_item_name', $cart_item, $cart_item_key); echo wc_get_formatted_cart_item_data($cart_item); // PHPCS: XSS ok. ?> </div> </td> <!-- Price Column --> <td class="product-price" data-title="<?php esc_attr_e('Price', 'woocommerce'); ?>"> <?php echo apply_filters('woocommerce_cart_item_price', WC()->cart->get_product_price($_product), $cart_item, $cart_item_key); // PHPCS: XSS ok. ?> </td> <!-- Quantity Column --> <td class="product-quantity" data-title="<?php esc_attr_e('Quantity', 'woocommerce'); ?>"> <?php if ( $_product->is_sold_individually() ) : $product_quantity = sprintf('1 <input type="hidden" name="cart[%s][qty]" value="1" />', $cart_item_key); else : $product_quantity = woocommerce_quantity_input( array( 'input_name' => "cart[{$cart_item_key}][qty]", 'input_value' => $cart_item['quantity'], 'max_value' => $_product->get_max_purchase_quantity(), 'min_value' => '0', 'product_name' => $_product->get_name(), ), $_product, false ); endif; echo apply_filters('woocommerce_cart_item_quantity', $product_quantity, $cart_item_key, $cart_item); // PHPCS: XSS ok. ?> <small class="qty-status" style="display:none;margin-left:8px;color:#666;">保存中…</small> </td> <!-- Subtotal Column --> <td class="product-subtotal" data-title="<?php esc_attr_e('Subtotal', 'woocommerce'); ?>"> <?php echo apply_filters('woocommerce_cart_item_subtotal', WC()->cart->get_product_subtotal($_product, $cart_item['quantity']), $cart_item, $cart_item_key); // PHPCS: XSS ok. ?> </td> <!-- Remove Item Column --> <td class="product-remove" data-title="<?php esc_attr_e('操作', 'woocommerce'); ?>"> <?php echo apply_filters('woocommerce_cart_item_remove_link', sprintf( '<a href="%s" class="remove" aria-label="%s" data-product_id="%s" data-product_sku="%s">×</a>', esc_url( wc_get_cart_remove_url($cart_item_key) ), esc_attr__('Remove this item', 'woocommerce'), esc_attr($product_id), esc_attr($_product->get_sku()) ), $cart_item_key); ?> </td> </tr> <?php endif; ?> <?php endforeach; ?> <?php do_action('woocommerce_after_cart_contents'); ?> </tbody> </table> <?php do_action('woocommerce_after_cart_table'); ?> </form> </div> <!-- Sticky Footer --> <div class="cart-footer-actions sticky-footer" style="position: sticky; bottom: 0; background: white; padding: 15px; border-top: 1px solid #ddd; max-width: 1200px; margin: 0 auto;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> <div class="footer-left"> <input type="checkbox" id="footer-select-all"> <label for="footer-select-all" style="display: inline-block; margin-left: 5px; cursor: pointer;">全选</label> <button type="button" class="button" id="remove-selected-items">刪除選中的商品</button> <button type="button" class="button" id="clear-cart">清空購物車</button> </div> <div class="coupon-section"> <input type="text" name="coupon_code" class="input-text" id="coupon_code" value="" placeholder="输入优惠券代码" style="padding: 8px; width: 200px; border: 1px solid #ddd; border-radius: 4px; margin-right: 5px;" /> <button type="button" class="button" id="apply-coupon">应用优惠券</button> </div> </div> <div style="display: flex; justify-content: space-between; align-items: center;"> <div class="selected-summary" style="font-size: 16px; font-weight: bold;"> 已选商品: <span id="selected-count">0</span> 件,共计: <span id="selected-total">RM0.00</span> </div> <a href="<?php echo esc_url( wc_get_checkout_url() ); ?>" class="checkout-button button alt wc-forward" id="partial-checkout">结算</a> </div> </div> <?php do_action('woocommerce_after_cart'); ?> <style> /* Layout styles (unchanged) */ .cart-page-section { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); margin-bottom: 30px; } .cart-page-section table.cart { width: 100%; border-collapse: collapse; margin-bottom: 20px; } .cart-page-section table.cart th, .cart-page-section table.cart td { padding: 15px; text-align: left; border-bottom: 1px solid #eee; } .cart-page-section table.cart th { background-color: #f8f8f8; font-weight: bold; } .cart-page-section table.cart th.product-select, .cart-page-section table.cart td.product-select { width: 8%; text-align: center; vertical-align: middle; white-space: nowrap; } .cart-page-section table.cart td.product-info { width: 37%; vertical-align: top; } .cart-page-section table.cart .product-info .product-image { float: left; margin-right: 15px; width: 100px; height: 100px; } .cart-page-section table.cart .product-info .product-image img { max-width: 100%; height: auto; display: block; } .cart-page-section table.cart .product-info .product-name { overflow: hidden; } .cart-page-section table.cart td.product-price, .cart-page-section table.cart td.product-quantity, .cart-page-section table.cart td.product-subtotal { width: 15%; vertical-align: middle; } .cart-page-section table.cart td.product-remove { width: 5%; text-align: center; vertical-align: middle; } .cart-footer-actions { position: sticky; bottom: 0; background: #fff; padding: 15px; border-top: 1px solid #ddd; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); z-index: 1000; display: flex; flex-direction: column; gap: 15px; } .footer-left { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; } .coupon-section { display: flex; align-items: center; gap: 5px; } .selected-summary { font-size: 16px; font-weight: bold; flex: 1; } .cart-footer-actions .button { padding: 10px 20px; background-color: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.3s; white-space: nowrap; } .cart-footer-actions .button:hover { background-color: #d32f2f; } #partial-checkout { padding: 12px 24px; background-color: #2196F3; color: white; text-decoration: none; border-radius: 4px; display: inline-block; transition: background-color 0.3s; white-space: nowrap; text-align: center; font-weight: bold; } #partial-checkout:hover { background-color: #0b7dda; } #coupon_code { padding: 8px; width: 200px; border: 1px solid #ddd; border-radius: 4px; transition: border-color 0.3s; } #coupon_code:focus { border-color: #2196F3; outline: none; } /* Responsive (unchanged) */ @media (max-width: 768px) { .cart-page-section table.cart thead { display: none; } .cart-page-section table.cart td { display: block; width: 100% !important; text-align: right; padding: 10px; position: relative; padding-left: 50%; } .cart-page-section table.cart td::before { content: attr(data-title); position: absolute; left: 15px; font-weight: bold; text-align: left; } .cart-page-section table.cart .product-info .product-image { float: none; margin: 0 auto 10px; } .cart-page-section table.cart td.product-select::before { content: "选择"; } .cart-footer-actions { flex-direction: column; align-items: flex-start; } .footer-left { width: 100%; justify-content: space-between; } .coupon-section { width: 100%; margin-top: 10px; } .coupon-section input { flex: 1; } .cart-footer-actions .footer-bottom-row { flex-direction: column; align-items: stretch; } .selected-summary { text-align: center; margin-bottom: 10px; } #partial-checkout { width: 100%; padding: 15px; } .cart-footer-actions .button { padding: 12px 15px; margin: 5px 0; width: 100%; text-align: center; } } @media (max-width: 480px) { .cart-page-section { padding: 15px; } .cart-page-section table.cart td { padding-left: 45%; } .cart-page-section table.cart td::before { font-size: 14px; } .cart-footer-actions { padding: 10px; } #coupon_code { width: 100%; } } /* Loader overlay above quantity input */ .product-quantity .quantity { position: relative; /* anchor for overlay */ } .quantity-saving-overlay { position: absolute; top: 50%; /* 垂直居中 */ right: -25px; /* 距离右侧8px */ transform: translateY(-50%); /* 垂直居中偏移 */ display: none; background: rgba(255,255,255,0.0); z-index: 10; pointer-events: none; } .quantity-saving-loader { width: 20px; height: 20px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { 0% { transform: translateY(-50%) rotate(0deg); } 100% { transform: translateY(-50%) rotate(360deg); } } </style> <script> jQuery(function($){ // Context var PC = { ajax_url: (window.wc_cart_params && window.wc_cart_params.ajax_url) || '<?php echo esc_url( admin_url("admin-ajax.php") ); ?>', security: (function(){ var n = $('input[name="woocommerce-cart-nonce"]').val(); if (n) return n; if (window.wc_cart_params && window.wc_cart_params.cart_nonce) return window.wc_cart_params.cart_nonce; return '<?php echo wp_create_nonce("woocommerce-cart"); ?>'; })(), cart_is_empty: <?php echo $pc_cart_is_empty ? 'true' : 'false'; ?>, cart_uid: '<?php echo esc_js($pc_uid); ?>' }; // Keys function lsKey(name){ return 'pc_' + name + '_' + PC.cart_uid; } var LS_SELECTION = lsKey('selected_items'); var LS_MASTER = lsKey('cart_master'); // Utilities function getSelectedKeys(){ return $('.item-checkbox:checked').map(function(){ return this.value; }).get(); } function fmtRM(n){ n = isNaN(n) ? 0 : n; return 'RM' + Number(n).toFixed(2); } // Selection persistence function readSelection(){ try { return JSON.parse(localStorage.getItem(LS_SELECTION) || '[]'); } catch(e){ return []; } } function writeSelection(keys){ try { localStorage.setItem(LS_SELECTION, JSON.stringify(keys||[])); } catch(e){} } function restoreSelectionFromLS(){ var saved = readSelection(); if (!Array.isArray(saved)) saved = []; $('.item-checkbox').each(function(){ $(this).prop('checked', saved.indexOf(this.value) !== -1); }); } window.addEventListener('storage', function(ev){ if (ev.key === LS_SELECTION) { restoreSelectionFromLS(); updateSelectedSummary(); } }); // Selected summary function updateSelectedSummary(){ var total = 0, count = 0; $('.item-checkbox:checked').each(function(){ var price = parseFloat($(this).data('price')); if (!isNaN(price)) { total += price; count++; } }); $('#selected-count').text(count); $('#selected-total').text(fmtRM(total)); var totalCbs = $('.item-checkbox').length; var checkedCbs = $('.item-checkbox:checked').length; var allChecked = totalCbs > 0 && checkedCbs === totalCbs; $('#select-all-items, #footer-select-all').prop('checked', allChecked); writeSelection(getSelectedKeys()); } // Select all $('#select-all-items, #footer-select-all').off('change.sc').on('change.sc', function(){ var checked = $(this).prop('checked'); $('.item-checkbox').prop('checked', checked); updateSelectedSummary(); }); // 单个勾选项变化时,更新统计与“全选”状态 $(document).on('change', '.item-checkbox', updateSelectedSummary); // Snapshot cart DOM -> localStorage (guest resilience) function snapshotCartFromDOM() { var items = []; $('tr.cart_item').each(function(){ var $row = $(this); var pid = parseInt($row.attr('data-product_id'),10)||0; var vid = parseInt($row.attr('data-variation_id'),10)||0; var qty = parseFloat($row.find('input.qty').val())||0; var variation = {}; try { variation = JSON.parse($row.attr('data-variation')||'{}'); } catch(e){ variation = {}; } if (pid && qty > 0) { items.push({ product_id: pid, variation_id: vid, variation: variation, quantity: qty }); } }); try { localStorage.setItem(LS_MASTER, JSON.stringify({ ts: Date.now(), items: items })); } catch(e){} } function maybeRehydrateFromLocal() { if (!PC.cart_is_empty) return; var raw = localStorage.getItem(LS_MASTER); if (!raw) return; var data; try { data = JSON.parse(raw); } catch(e){ return; } var items = (data && data.items) ? data.items : []; if (!items.length) return; $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'pc_rehydrate_cart', security: PC.security, items: JSON.stringify(items) } }).done(function(res){ if (res && res.success) { location.reload(); // display rehydrated items } }); } // 统一的购物车数量更新函数 function updateHeaderCartCount() { $.ajax({ url: PC.ajax_url, method: 'POST', data: { action: 'get_cart_count', security: PC.security } }).done(function(res) { if (res && res.success) { $('.header-cart-count, .cart-count').text(res.data.count); } }).fail(function() { console.error('Failed to update cart count'); }); } // AFTER $('#remove-selected-items').off('click.sc').on('click.sc', function(){ var keys = getSelectedKeys(); if (!keys.length) { alert('请选择要删除的商品'); return; } if (!confirm('确定要删除选中的商品吗?')) return; $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'remove_selected_cart_items', selected_items: keys, security: PC.security } }).done(function(res){ if (res && res.success) { keys.forEach(function(k){ var $row = $('tr.cart_item[data-cart_item_key="'+k+'"]'); $row.fadeOut(250, function(){ $(this).remove(); updateSelectedSummary(); updateHeaderCartCount(); // 更新购物车数量 }); }); snapshotCartFromDOM(); // 从本地选择集合中剔除已删除项 var saved = readSelection().filter(function(k0){ return keys.indexOf(k0) === -1; }); writeSelection(saved); } else { alert((res && res.data && (res.data.message || res.data)) || '删除失败,请重试'); } }).fail(function(){ alert('删除失败,请重试'); }); }); // AFTER $('#clear-cart').off('click.sc').on('click.sc', function(){ if (!confirm('确定要清空购物车吗?')) return; $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'empty_cart', security: PC.security } }).done(function(res){ if (res && res.success) { writeSelection([]); localStorage.removeItem(LS_MASTER); location.reload(); } else { alert((res && res.data && (res.data.message || res.data)) || '清空购物车失败,请重试'); } }).fail(function(){ alert('清空购物车失败,请重试'); }); }); // Apply coupon // AFTER $('#apply-coupon').off('click.sc').on('click.sc', function(){ var code = ($.trim($('#coupon_code').val()) || ''); if (!code) { alert('请输入优惠券代码'); return; } var $btn = $(this).prop('disabled', true).text('处理中...'); $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'apply_coupon', coupon_code: code, security: PC.security } }).done(function(res){ if (res && res.success) { location.reload(); } else { alert((res && res.data && (res.data.message || res.data)) || '优惠券应用失败,请重试'); $btn.prop('disabled', false).text('应用优惠券'); } }).fail(function(){ alert('发生错误,请重试'); $btn.prop('disabled', false).text('应用优惠券'); }); }); // ---- Quantity auto-save (fixed regex + loader above input) ---- // 已修正:正确的正则表达式 function parseCartKeyFromInputName(n) { var m = (n || '').match(/^cart\[([a-zA-Z0-9_]+)\]\[qty\]$/); return m ? m[1] : null; } function ensureLoader($quantityContainer){ var $overlay = $quantityContainer.find('.quantity-saving-overlay'); if (!$overlay.length) { $overlay = $('<div class="quantity-saving-overlay"><div class="quantity-saving-loader"></div></div>'); $quantityContainer.append($overlay); } return $overlay; } var qtyTimers = {}; $(document).on('input change', 'input.qty', function(){ var $input = $(this); var key = parseCartKeyFromInputName($input.attr('name')); if (!key) return; var $row = $input.closest('tr.cart_item'); var $quantityContainer = $input.closest('.quantity'); var val = parseInt($input.val(), 10); if (isNaN(val) || val < 0) val = 0; // debounce if (qtyTimers[key]) clearTimeout(qtyTimers[key]); var $overlay = ensureLoader($quantityContainer); $overlay.hide(); qtyTimers[key] = setTimeout(function(){ $overlay.show(); $input.prop('disabled', true); $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'update_cart_item_qty', cart_item_key: key, qty: val, security: PC.security } }).done(function(res){ if (res && res.success && res.data){ // 更新小计显示 if (res.data.subtotal_html) { $row.find('td.product-subtotal').html(res.data.subtotal_html); } // 关键修复 1:立即更新复选框价格属性(含折扣逻辑) var $cb = $row.find('.item-checkbox'); if ($cb.length) { // 使用API返回的含折扣价格 const finalPrice = res.data.line_total_incl_tax || res.data.line_total * (1 + res.data.tax_rate); $cb.attr('data-price', finalPrice.toFixed(2)); // 关键修复 2:立即更新已选商品总计 updateSelectedSummary(); } // 处理商品移除逻辑 if (val === 0 || res.data.removed) { $row.fadeOut(300, function(){ $(this).remove(); snapshotCartFromDOM(); updateSelectedSummary(); // 再次确保更新 updateHeaderCartCount(); }); } else { snapshotCartFromDOM(); updateSelectedSummary(); // 立即更新 updateHeaderCartCount(); } } else { var msg = (res && res.data && (res.data.message || res.data)) || '数量更新失败'; alert(msg); } }).fail(function(){ alert('数量更新失败,请重试'); }).always(function(){ $overlay.hide(); $input.prop('disabled', false); }); }, 500); // 0.5s debounce }); // Partial checkout -> regular checkout page $('#partial-checkout').off('click.sc').on('click.sc', function(e){ e.preventDefault(); var keys = getSelectedKeys(); if (!keys.length) { alert('请至少选择一件商品结算'); return; } var $btn = $(this), t = $btn.text(); $btn.prop('disabled', true).text('创建订单中...'); snapshotCartFromDOM(); $.ajax({ url: PC.ajax_url, method: 'POST', dataType: 'json', data: { action: 'create_direct_order', selected_items: keys, security: PC.security } }).done(function(res){ if (res && res.success && res.data && res.data.checkout_url) { window.location.href = res.data.checkout_url; // /checkout/?pc_token=... } else { alert((res && res.data && (res.data.message || res.data)) || '创建订单失败,请重试'); $btn.prop('disabled', false).text(t); } }).fail(function(xhr){ var msg = '创建订单失败'; if (xhr && xhr.responseJSON && xhr.responseJSON.data) { msg += ':' + (xhr.responseJSON.data.message || xhr.responseJSON.data); } alert(msg); $btn.prop('disabled', false).text(t); }); }); // Keep LS selection after clicking "x" remove; also snapshot $(document).on('click', 'a.remove', function(){ var key = $(this).closest('tr.cart_item').attr('data-cart_item_key'); if (key) { var saved = readSelection().filter(function(k){ return k !== key; }); writeSelection(saved); snapshotCartFromDOM(); updateHeaderCartCount(); // 更新购物车数量 } }); // Init restoreSelectionFromLS(); updateSelectedSummary(); snapshotCartFromDOM(); maybeRehydrateFromLocal(); updateHeaderCartCount(); // 初始加载时更新购物车数量 }); </script> functions.php: <?php defined('ABSPATH') || exit; /** * 健壮的部分结算系统 - 带持久化购物车快照 * - 结算前创建完整购物车快照 * - 订单创建成功后才移除商品 * - 感谢页面重建购物车(快照 - 已购买商品) * - 取消/返回时恢复完整快照 * - 访客支持:localStorage + AJAX 重新水合 */ /* ------------------------------------------------- * 辅助函数 * ------------------------------------------------- */ /** * 购物车数量AJAX端点 */ add_action('wp_ajax_get_cart_count', 'pc_get_cart_count'); add_action('wp_ajax_nopriv_get_cart_count', 'pc_get_cart_count'); function pc_get_cart_count() { check_ajax_referer('woocommerce-cart', 'security'); $count = WC()->cart->get_cart_contents_count(); wp_send_json_success(array('count' => $count)); } function pc_get_cart_uid() { if (is_user_logged_in()) { return 'user_' . get_current_user_id(); } if (empty($_COOKIE['pc_cart_uid'])) { $token = wp_generate_uuid4(); setcookie('pc_cart_uid', $token, time() + YEAR_IN_SECONDS, COOKIEPATH ?: '/', '', is_ssl(), false); $_COOKIE['pc_cart_uid'] = $token; } return 'guest_' . sanitize_text_field(wp_unslash($_COOKIE['pc_cart_uid'])); } function pc_build_item_key($product_id, $variation_id = 0) { return (int)$product_id . '|' . (int)$variation_id; } function pc_snapshot_current_cart() { if (!isset(WC()->cart)) { wc_load_cart(); } $items = array(); foreach (WC()->cart->get_cart() as $ci_key => $ci) { $pid = isset($ci['product_id']) ? (int)$ci['product_id'] : 0; $vid = isset($ci['variation_id']) ? (int)$ci['variation_id'] : 0; $qty = isset($ci['quantity']) ? wc_stock_amount($ci['quantity']) : 0; $var = isset($ci['variation']) && is_array($ci['variation']) ? $ci['variation'] : array(); if ($pid && $qty > 0) { $items[] = array( 'product_id' => $pid, 'variation_id' => $vid, 'variation' => array_map('wc_clean', $var), 'quantity' => $qty, ); } } return [ 'items' => $items, 'coupons' => WC()->cart->get_applied_coupons() ]; } /** * 恢复购物车优惠券 */ function pc_restore_cart_coupons($coupons) { if (!isset(WC()->cart)) { wc_load_cart(); } // 先移除所有现有优惠券 foreach (WC()->cart->get_applied_coupons() as $coupon_code) { WC()->cart->remove_coupon($coupon_code); } // 应用快照中的优惠券 foreach ($coupons as $coupon_code) { if (WC()->cart->is_valid_coupon($coupon_code)) { WC()->cart->apply_coupon($coupon_code); } } WC()->cart->calculate_totals(); } function pc_restore_cart_from_items($items) { if (!isset(WC()->cart)) { wc_load_cart(); } WC()->cart->empty_cart(); foreach ((array)$items as $it) { $pid = isset($it['product_id']) ? (int)$it['product_id'] : 0; $vid = isset($it['variation_id']) ? (int)$it['variation_id'] : 0; $qty = isset($it['quantity']) ? wc_stock_amount($it['quantity']) : 0; $var = isset($it['variation']) && is_array($it['variation']) ? array_map('wc_clean', $it['variation']) : array(); if ($pid && $qty > 0) { $product = wc_get_product($pid); // 检查商品是否有效且库存可用 if ($product && $product->exists() && $product->is_in_stock()) { // 获取库存数量 $stock_qty = $product->get_stock_quantity(); if ($stock_qty !== null) { $actual_qty = min($qty, $stock_qty); } else { $actual_qty = $qty; } WC()->cart->add_to_cart( $pid, $actual_qty, $vid, $var ); } } } WC()->cart->calculate_totals(); } // 完全重构的购物车恢复逻辑 - 确保WC会话存在 add_action('wp_loaded', function() { // 严格检查WC会话可用性 $wc = function_exists('WC') ? WC() : null; if (!$wc) return; $session = property_exists($wc, 'session') ? $wc->session : null; if (!$session) return; $token = method_exists($session, 'get') ? $session->get('pc_partial_token') : null; if (!$token) return; // 检查购物车是否为空 $cart = property_exists($wc, 'cart') ? $wc->cart : null; if (!$cart) return; if ($cart->is_empty()) { $payload = get_transient(pc_transient_key($token)); if (!empty($payload['snapshot'])) { pc_restore_cart_from_items($payload['snapshot']['items']); pc_restore_cart_coupons($payload['snapshot']['coupons']); $cart->calculate_totals(); } } }, 20); function pc_transient_key($token) { return 'pc_partial_payload_' . sanitize_key($token); } /* ------------------------------------------------- * AJAX: 当Woo购物车为空时本地重新水合 * ------------------------------------------------- */ add_action('wp_ajax_pc_rehydrate_cart', 'pc_rehydrate_cart'); add_action('wp_ajax_nopriv_pc_rehydrate_cart', 'pc_rehydrate_cart'); function pc_rehydrate_cart() { check_ajax_referer('woocommerce-cart', 'security'); $raw = isset($_POST['items']) ? wp_unslash($_POST['items']) : ''; $items = is_string($raw) ? json_decode($raw, true) : (array)$raw; if (!is_array($items)) { wp_send_json_error(array('message' => '无效的商品数据'), 400); } if (!isset(WC()->cart)) { wc_load_cart(); } if (!WC()->cart->is_empty()) { wp_send_json_success(array('message' => '购物车非空')); } foreach ($items as $it) { $pid = isset($it['product_id']) ? (int)$it['product_id'] : 0; $vid = isset($it['variation_id']) ? (int)$it['variation_id'] : 0; $qty = isset($it['quantity']) ? wc_stock_amount($it['quantity']) : 0; $var = isset($it['variation']) && is_array($it['variation']) ? array_map('wc_clean', $it['variation']) : array(); if ($pid && $qty > 0) { $product = wc_get_product($pid); if ($product && $product->exists() && $product->is_in_stock()) { // 检查库存 $stock_qty = $product->get_stock_quantity(); if ($stock_qty !== null) { $qty = min($qty, $stock_qty); } WC()->cart->add_to_cart($pid, $qty, $vid, $var); } } } WC()->cart->calculate_totals(); wp_send_json_success(array('rehydrated' => true)); } /* ------------------------------------------------- * AJAX: 更新购物车商品数量(无需刷新页面) * ------------------------------------------------- */ add_action('wp_ajax_update_cart_item_qty', 'pc_update_cart_item_qty'); add_action('wp_ajax_nopriv_update_cart_item_qty', 'pc_update_cart_item_qty'); function pc_update_cart_item_qty() { check_ajax_referer('woocommerce-cart', 'security'); $key = isset($_POST['cart_item_key']) ? wc_clean(wp_unslash($_POST['cart_item_key'])) : ''; $qty = isset($_POST['qty']) ? wc_stock_amount($_POST['qty']) : null; if (!$key || $qty === null) { wp_send_json_error(array('message' => '参数缺失'), 400); } if (!isset(WC()->cart)) { wc_load_cart(); } if ($qty <= 0) { $removed = WC()->cart->remove_cart_item($key); WC()->cart->calculate_totals(); wp_send_json_success(array('removed' => (bool)$removed)); } else { $set = WC()->cart->set_quantity($key, $qty, true); WC()->cart->calculate_totals(); $cart_item = WC()->cart->get_cart_item($key); if (!$cart_item) { wp_send_json_error(array('message' => '更新后未找到购物车商品'), 404); } $_product = $cart_item['data']; $subtotal_html = apply_filters( 'woocommerce_cart_item_subtotal', WC()->cart->get_product_subtotal($_product, $cart_item['quantity']), $cart_item, $key ); // 计算含税总价(考虑折扣) $line_total_incl_tax = $cart_item['line_total'] + $cart_item['line_tax']; wp_send_json_success(array( 'subtotal_html' => wc_price($product->get_price() * $qty), 'line_total' => $cart_item['line_total'], 'line_tax' => $cart_item['line_tax'], 'tax_rate' => WC_Tax::get_rates($product->get_tax_class()), 'line_total_incl_tax' => $line_total_incl_tax, // 包含折扣的价格 'removed' => ($qty === 0) )); } } /* ------------------------------------------------- * AJAX: 删除选中商品 * ------------------------------------------------- */ add_action('wp_ajax_remove_selected_cart_items', 'pc_remove_selected_cart_items'); add_action('wp_ajax_nopriv_remove_selected_cart_items', 'pc_remove_selected_cart_items'); function pc_remove_selected_cart_items() { check_ajax_referer('woocommerce-cart', 'security'); $keys = isset($_POST['selected_items']) ? (array) $_POST['selected_items'] : array(); if (!isset(WC()->cart)) { wc_load_cart(); } foreach ($keys as $k) { $k = wc_clean(wp_unslash($k)); WC()->cart->remove_cart_item($k); } WC()->cart->calculate_totals(); wp_send_json_success(true); } /* ------------------------------------------------- * AJAX: 清空购物车 * ------------------------------------------------- */ add_action('wp_ajax_empty_cart', 'pc_empty_cart'); add_action('wp_ajax_nopriv_empty_cart', 'pc_empty_cart'); function pc_empty_cart() { check_ajax_referer('woocommerce-cart', 'security'); if (!isset(WC()->cart)) { wc_load_cart(); } WC()->cart->empty_cart(); wp_send_json_success(true); } /* ------------------------------------------------- * AJAX: 应用优惠券 * ------------------------------------------------- */ add_action('wp_ajax_apply_coupon', 'pc_apply_coupon'); add_action('wp_ajax_nopriv_apply_coupon', 'pc_apply_coupon'); function pc_apply_coupon() { check_ajax_referer('woocommerce-cart', 'security'); $code = isset($_POST['coupon_code']) ? wc_format_coupon_code(wp_unslash($_POST['coupon_code'])) : ''; if (!$code) { wp_send_json_error(array('message' => __('请输入优惠券代码', 'woocommerce')), 400); } if (!isset(WC()->cart)) { wc_load_cart(); } $applied = WC()->cart->apply_coupon($code); WC()->cart->calculate_totals(); if (is_wp_error($applied)) { wp_send_json_error(array('message' => $applied->get_error_message()), 400); } if (!$applied) { wp_send_json_error(array('message' => __('优惠券应用失败', 'woocommerce')), 400); } wp_send_json_success(true); } /* ------------------------------------------------- * AJAX: 启动部分结算到常规结算页面 * ------------------------------------------------- */ add_action('wp_ajax_create_direct_order', 'pc_create_direct_order'); add_action('wp_ajax_nopriv_create_direct_order', 'pc_create_direct_order'); function pc_create_direct_order() { check_ajax_referer('woocommerce-cart', 'security'); $selected_keys = isset($_POST['selected_items']) ? (array) $_POST['selected_items'] : array(); if (empty($selected_keys)) { wp_send_json_error(array('message' => __('请选择要结算的商品', 'woocommerce')), 400); } if (!isset(WC()->cart)) { wc_load_cart(); } // 创建完整购物车快照 $snapshot = pc_snapshot_current_cart(); // 从当前购物车构建选中商品 $selected = array(); foreach (WC()->cart->get_cart() as $ci_key => $ci) { if (!in_array($ci_key, $selected_keys, true)) { continue; } $pid = (int)$ci['product_id']; $vid = (int)$ci['variation_id']; $qty = wc_stock_amount($ci['quantity']); $var = isset($ci['variation']) && is_array($ci['variation']) ? array_map('wc_clean', $ci['variation']) : array(); if ($pid && $qty > 0) { $selected[] = array( 'product_id' => $pid, 'variation_id' => $vid, 'variation' => $var, 'quantity' => $qty, ); } } if (empty($selected)) { wp_send_json_error(array('message' => __('没有可结算的商品', 'woocommerce')), 400); } $token = wp_generate_uuid4(); $payload = array( 'uid' => pc_get_cart_uid(), 'snapshot' => $snapshot, // 现在包含 items + coupons 'selected' => $selected, 'created' => time(), ); set_transient(pc_transient_key($token), $payload, 2 * DAY_IN_SECONDS); // 将会话令牌存入WooCommerce会话 if (isset(WC()->session) && method_exists(WC()->session, 'set')) { WC()->session->set('pc_partial_token', $token); } $checkout_url = add_query_arg('pc_token', rawurlencode($token), wc_get_checkout_url()); wp_send_json_success(array('checkout_url' => $checkout_url)); } /* ------------------------------------------------- * 结账时虚拟化购物车并在购买后重建 * ------------------------------------------------- */ // 加载结账页面时虚拟化购物车 add_action('woocommerce_before_checkout_form', function() { if (!isset($_GET['pc_token'])) return; $token = sanitize_text_field(wp_unslash($_GET['pc_token'])); $payload = get_transient(pc_transient_key($token)); if (empty($payload) || empty($payload['selected'])) return; if (!isset(WC()->cart)) { wc_load_cart(); } // 仅加载选中的商品 pc_restore_cart_from_items($payload['selected']); // 持久化令牌到会话 if (isset(WC()->session) && method_exists(WC()->session, 'set')) { WC()->session->set('pc_partial_token', $token); } }, 1); // 订单处理前确保虚拟化 add_action('woocommerce_before_checkout_process', function() { if (!isset(WC()->session) || !method_exists(WC()->session, 'get')) return; $token = WC()->session->get('pc_partial_token'); if (!$token) return; $payload = get_transient(pc_transient_key($token)); if (empty($payload) || empty($payload['selected'])) return; // 确保购物车仅包含选中商品 pc_restore_cart_from_items($payload['selected']); }, 1); // 为订单添加令牌标记 add_action('woocommerce_checkout_create_order', function($order) { $token = null; if (isset($_GET['pc_token'])) { $token = sanitize_text_field(wp_unslash($_GET['pc_token'])); } elseif (isset(WC()->session) && method_exists(WC()->session, 'get')) { $token = WC()->session->get('pc_partial_token'); } if ($token) { $order->update_meta_data('_pc_partial_token', $token); $order->update_meta_data('_pc_cart_snapshot', $token); } }, 10, 1); // 成功结账后仅移除已购买商品 add_action('woocommerce_thankyou', function($order_id) { $order = wc_get_order($order_id); if (!$order) return; $token = $order->get_meta('_pc_partial_token'); if (!$token) return; $payload = get_transient(pc_transient_key($token)); if (empty($payload) || empty($payload['snapshot'])) { if (isset(WC()->session) && method_exists(WC()->session, 'set')) { WC()->session->set('pc_partial_token', null); } delete_transient(pc_transient_key($token)); return; } // 1. 恢复完整快照 pc_restore_cart_from_items($payload['snapshot']['items']); pc_restore_cart_coupons($payload['snapshot']['coupons']); WC()->cart->calculate_totals(); // 2. 只移除已购买商品 $removed_count = 0; foreach ($payload['selected'] as $selected_item) { $cart_item_key = pc_find_cart_item($selected_item['product_id'], $selected_item['variation_id']); if ($cart_item_key) { WC()->cart->remove_cart_item($cart_item_key); $removed_count++; } } // 3. 如果有商品被移除才重新计算 if ($removed_count > 0) { WC()->cart->calculate_totals(); } // 清理令牌 if (isset(WC()->session) && method_exists(WC()->session, 'set')) { WC()->session->set('pc_partial_token', null); } delete_transient(pc_transient_key($token)); }, 20); // 辅助函数:通过产品和变体ID查找购物车项 function pc_find_cart_item($product_id, $variation_id = 0) { if (!isset(WC()->cart)) { wc_load_cart(); } foreach (WC()->cart->get_cart() as $cart_item_key => $cart_item) { $cart_pid = isset($cart_item['product_id']) ? (int)$cart_item['product_id'] : 0; $cart_vid = isset($cart_item['variation_id']) ? (int)$cart_item['variation_id'] : 0; if ($cart_pid == $product_id && $cart_vid == $variation_id) { return $cart_item_key; } } return false; } // 访问购物车页面时恢复完整快照(返回/取消操作) add_action('woocommerce_before_cart', function() { // 严格检查会话可用性 if (!isset(WC()->session) || !method_exists(WC()->session, 'get')) return; $token = WC()->session->get('pc_partial_token'); if (!$token) return; $payload = get_transient(pc_transient_key($token)); if (empty($payload) || empty($payload['snapshot'])) return; // 恢复完整快照 pc_restore_cart_from_items($payload['snapshot']['items']); pc_restore_cart_coupons($payload['snapshot']['coupons']); WC()->cart->calculate_totals(); }); /* ------------------------------------------------- * 保持购物车数量在结算过程中准确 * ------------------------------------------------- */ add_filter('woocommerce_cart_contents_count', function($count) { // 检查是否存在部分结算令牌 if (!isset(WC()->session) || !method_exists(WC()->session, 'get')) return $count; $token = WC()->session->get('pc_partial_token'); if (!$token) return $count; $payload = get_transient(pc_transient_key($token)); // 始终显示完整购物车数量 if (!empty($payload['snapshot']) && is_array($payload['snapshot']['items'])) { $snapshot_count = 0; foreach ($payload['snapshot']['items'] as $item) { $snapshot_count += (int)($item['quantity'] ?? 0); } return $snapshot_count; } return $count; }); // 确保购物车一致性 add_action('woocommerce_before_cart', 'pc_maintain_cart_consistency'); add_action('woocommerce_before_checkout_form', 'pc_maintain_cart_consistency'); function pc_maintain_cart_consistency() { if (!isset(WC()->session) || !method_exists(WC()->session, 'get')) return; $token = WC()->session->get('pc_partial_token'); if (!$token) return; $payload = get_transient(pc_transient_key($token)); if (empty($payload) || empty($payload['snapshot'])) return; // 立即恢复完整购物车以确保UI一致性 pc_restore_cart_from_items($payload['snapshot']['items']); pc_restore_cart_coupons($payload['snapshot']['coupons']); WC()->cart->calculate_totals(); }
09-06
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值