{"id":6697,"date":"2025-09-14T09:52:53","date_gmt":"2025-09-14T01:52:53","guid":{"rendered":"https:\/\/mynotes.org\/tech\/?p=6697"},"modified":"2025-09-14T09:53:44","modified_gmt":"2025-09-14T01:53:44","slug":"%e6%89%b9%e6%ac%a1%e7%b7%a8%e8%bc%af%e5%95%86%e5%93%81%e5%b1%ac%e6%80%a7","status":"publish","type":"post","link":"https:\/\/mynotes.org\/tech\/2025\/09\/14\/6697.htm","title":{"rendered":"\u6279\u6b21\u7de8\u8f2f\u5546\u54c1\u5c6c\u6027"},"content":{"rendered":"<p>WooCommerce \u5f8c\u53f0\u6e05\u55ae\u9801\uff0c\u4e0a\u65b9\u53ef\u7528\u5206\u985e\/\u6a19\u7c64\u7be9\u9078\uff1b\u63d0\u4f9b\u6bcd\u5b50\u5f0f\u4e0b\u62c9\u9078\u55ae\uff0c\u6279\u6b21\u70ba\u52fe\u9078\u5546\u54c1\u52a0\u5165\u6307\u5b9a\u5c6c\u6027\u8207\u5c6c\u6027\u9805\u76ee\uff08\u82e5\u7121\u8a72\u5c6c\u6027\u5247\u5148\u5efa\u7acb\u4e26\u6392\u5728\u6700\u5f8c\uff09\u3002\u5217\u8868\u79fb\u9664SKU\u6b04\u4f4d\uff1b\u6bcf\u500b\u5168\u57df\u5c6c\u6027\u5404\u81ea\u6210\u6b04\u986f\u793a\uff1b\u5c6c\u6027\u985e\u5225\u4ee5 slug \u5347\u51aa\u6392\u5217\uff1b\u6b04\u4f4d\u6a19\u984c\u53ef\u63db\u884c\uff1b\u5916\u5c64\u8996\u9700\u8981\u5141\u8a31\u6c34\u5e73\u6efe\u52d5\u3002<br \/>\n<!--more--><\/p>\n<pre escaped=\"true\" lang=\"php\">\r\n&lt;?php\r\n\/**\r\n * Plugin Name: WC Attribute Bulk Manager (Per-Attribute Columns, Wrapped Headers)\r\n * Description: WooCommerce \u5f8c\u53f0\u6e05\u55ae\u9801\uff0c\u4e0a\u65b9\u53ef\u7528\u5206\u985e\/\u6a19\u7c64\u7be9\u9078\uff1b\u63d0\u4f9b\u6bcd\u5b50\u5f0f\u4e0b\u62c9\u9078\u55ae\uff0c\u6279\u6b21\u70ba\u52fe\u9078\u5546\u54c1\u52a0\u5165\u6307\u5b9a\u5c6c\u6027\u8207\u5c6c\u6027\u9805\u76ee\uff08\u82e5\u7121\u8a72\u5c6c\u6027\u5247\u5148\u5efa\u7acb\u4e26\u6392\u5728\u6700\u5f8c\uff09\u3002\u5217\u8868\u79fb\u9664SKU\u6b04\u4f4d\uff1b\u6bcf\u500b\u5168\u57df\u5c6c\u6027\u5404\u81ea\u6210\u6b04\u986f\u793a\uff1b\u5c6c\u6027\u985e\u5225\u4ee5 slug \u5347\u51aa\u6392\u5217\uff1b\u6b04\u4f4d\u6a19\u984c\u53ef\u63db\u884c\uff1b\u5916\u5c64\u8996\u9700\u8981\u5141\u8a31\u6c34\u5e73\u6efe\u52d5\u3002\r\n * Version:     1.2.0\r\n * Author:      your-name\r\n * License:     GPLv2 or later\r\n *\/\r\n\r\nif ( ! defined( 'ABSPATH' ) ) exit;\r\n\r\nclass WC_Attribute_Bulk_Manager {\r\n    const SLUG = 'wc-attribute-bulk-manager';\r\n    const NONCE_ACTION = 'wc_abm_action';\r\n    const NONCE_FIELD  = 'wc_abm_nonce';\r\n\r\n    public function __construct() {\r\n        add_action( 'admin_menu', [ $this, 'add_menu' ], 99 );\r\n        add_action( 'admin_enqueue_scripts', [ $this, 'enqueue' ] );\r\n        add_action( 'wp_ajax_wc_abm_get_terms', [ $this, 'ajax_get_terms' ] );\r\n        add_action( 'admin_post_wc_abm_apply', [ $this, 'handle_apply' ] );\r\n    }\r\n\r\n    public function add_menu() {\r\n        add_submenu_page(\r\n            'woocommerce',\r\n            '\u5c6c\u6027\u6279\u6b21\u7ba1\u7406',\r\n            '\u5c6c\u6027\u6279\u6b21\u7ba1\u7406',\r\n            'manage_woocommerce',\r\n            self::SLUG,\r\n            [ $this, 'render_page' ]\r\n        );\r\n    }\r\n\r\n    public function enqueue( $hook ) {\r\n        if ( $hook !== 'woocommerce_page_' . self::SLUG ) return;\r\n\r\n        wp_enqueue_style( 'wc-abm-admin', plugins_url( 'wc-abm.css', __FILE__ ), [], '1.2.0' );\r\n        wp_enqueue_script( 'wc-abm-admin', plugins_url( 'wc-abm.js', __FILE__ ), [ 'jquery' ], '1.2.0', true );\r\n        wp_localize_script( 'wc-abm-admin', 'WCABM', [\r\n            'ajaxurl' =&gt; admin_url( 'admin-ajax.php' ),\r\n            'nonce'   =&gt; wp_create_nonce( self::NONCE_ACTION ),\r\n        ] );\r\n\r\n        \/\/ \u6837\u5f0f\uff1a\u7dad\u6301\u6b04\u5bec\u884c\u70ba\u8207\u524d\u7248\u4e00\u81f4\uff1b\u8868\u982d\u53ef\u63db\u884c\uff1b\u5fc5\u8981\u6642\u53ef\u6c34\u5e73\u6efe\u52d5\r\n        $css = '\r\n        .wc-abm-wrap .filters, .wc-abm-wrap .bulk-actions { display:flex; gap:12px; align-items:center; margin: 12px 0; flex-wrap: wrap;}\r\n        .wc-abm-table-wrap { overflow-x: auto; } \/* \u5fc5\u8981\u6642\u6c34\u5e73\u6efe\u52d5 *\/\r\n        .wc-abm-table{width:100%; border-collapse: collapse; background:#fff; min-width: 960px;} \/* \u4fdd\u7559\u8f03\u8212\u9069\u7684\u6700\u5c0f\u5bec *\/\r\n        .wc-abm-table th, .wc-abm-table td{border:1px solid #ddd; padding:8px; vertical-align: top;}\r\n        .wc-abm-table th{background:#f6f7f7; white-space: normal; word-break: break-word; line-height:1.3;} \/* \u8868\u982d\u5141\u8a31\u63db\u884c *\/\r\n        .wc-abm-table td{ white-space: normal; } \/* \u5167\u5bb9\u7dad\u6301\u4e0d\u63db\u884c\uff08\u8207\u524d\u7248\u4e00\u81f4\uff09 *\/\r\n        .term-chip{display:inline-block; border:1px solid #ddd; padding:2px 6px; margin:2px 4px 2px 0; border-radius:4px; font-size:12px; background:#fbfbfb; white-space:nowrap;}\r\n        .wc-abm-pagination{margin-top:12px;}\r\n        ';\r\n        wp_add_inline_style( 'wc-abm-admin', $css );\r\n\r\n        $js = \"\r\n        jQuery(function($){\r\n          function loadTerms(){\r\n            var attr = $('#wc-abm-attr').val();\r\n            var \\$child = $('#wc-abm-term');\r\n            \\$child.html('&lt;option value=\\\"\\\"&gt;\u8f09\u5165\u4e2d...&lt;\/option&gt;');\r\n            $.post(WCABM.ajaxurl, {\r\n              action:'wc_abm_get_terms',\r\n              attr: attr,\r\n              _ajax_nonce: WCABM.nonce\r\n            }, function(resp){\r\n              if(resp &amp;&amp; resp.success){\r\n                var opts = '&lt;option value=\\\"\\\"&gt;\u8acb\u9078\u64c7\u5c6c\u6027\u9805\u76ee&lt;\/option&gt;';\r\n                resp.data.forEach(function(t){\r\n                  opts += '&lt;option value=\\\"'+t.term_id+'\\\"&gt;'+t.name+'&lt;\/option&gt;';\r\n                });\r\n                \\$child.html(opts);\r\n              }else{\r\n                \\$child.html('&lt;option value=\\\"\\\"&gt;\u6c92\u6709\u9805\u76ee&lt;\/option&gt;');\r\n              }\r\n            });\r\n          }\r\n          $('#wc-abm-attr').on('change', loadTerms);\r\n\r\n          $('#wc-abm-checkall').on('change', function(){\r\n            $('.wc-abm-rowcheck').prop('checked', this.checked);\r\n          });\r\n        });\r\n        \";\r\n        wp_add_inline_script( 'wc-abm-admin', $js );\r\n    }\r\n\r\n    \/\/ \u5b50\u9078\u55ae\uff1a\u7dad\u6301\u9810\u8a2d\u9806\u5e8f\uff08\u4e0d\u518d\u5f37\u5236\u4ee5\u9805\u76ee slug \u6392\u5e8f\uff09\r\n    public function ajax_get_terms() {\r\n        check_ajax_referer( self::NONCE_ACTION );\r\n        if ( ! current_user_can( 'manage_woocommerce' ) ) wp_send_json_error();\r\n\r\n        $attr = isset($_POST['attr']) ? sanitize_text_field($_POST['attr']) : '';\r\n        if ( empty($attr) ) wp_send_json_success( [] );\r\n\r\n        $taxonomy = wc_attribute_taxonomy_name( $attr ); \/\/ 'color' -&gt; 'pa_color'\r\n        if ( ! taxonomy_exists( $taxonomy ) ) {\r\n            if ( taxonomy_exists( $attr ) ) $taxonomy = $attr;\r\n            else wp_send_json_success( [] );\r\n        }\r\n\r\n        $terms = get_terms( [ 'taxonomy' =&gt; $taxonomy, 'hide_empty' =&gt; false ] );\r\n        if ( is_wp_error($terms) ) $terms = [];\r\n\r\n        $out = [];\r\n        foreach ( $terms as $t ) {\r\n            $out[] = [ 'term_id' =&gt; $t-&gt;term_id, 'name' =&gt; $t-&gt;name ];\r\n        }\r\n        wp_send_json_success( $out );\r\n    }\r\n\r\n    public function render_page() {\r\n        if ( ! class_exists( 'WooCommerce' ) ) {\r\n            echo '&lt;div class=\"notice notice-error\"&gt;&lt;p&gt;\u8acb\u5148\u555f\u7528 WooCommerce\u3002&lt;\/p&gt;&lt;\/div&gt;';\r\n            return;\r\n        }\r\n\r\n        \/\/ \u7be9\u9078\u53c3\u6578\r\n        $cat   = isset($_GET['product_cat']) ? sanitize_text_field($_GET['product_cat']) : '';\r\n        $tag   = isset($_GET['product_tag']) ? sanitize_text_field($_GET['product_tag']) : '';\r\n        $paged = max(1, isset($_GET['paged']) ? intval($_GET['paged']) : 1 );\r\n        $per_page = 20;\r\n\r\n        $tax_query = [];\r\n        if ( $cat ) $tax_query[] = [ 'taxonomy'=&gt;'product_cat','field'=&gt;'slug','terms'=&gt;$cat ];\r\n        if ( $tag ) $tax_query[] = [ 'taxonomy'=&gt;'product_tag','field'=&gt;'slug','terms'=&gt;$tag ];\r\n        if ( count($tax_query) &gt; 1 ) $tax_query['relation'] = 'AND';\r\n\r\n        $q = new WP_Query( [\r\n            'post_type'      =&gt; 'product',\r\n            'post_status'    =&gt; [ 'publish','private','draft' ],\r\n            'posts_per_page' =&gt; $per_page,\r\n            'paged'          =&gt; $paged,\r\n            'tax_query'      =&gt; $tax_query,\r\n            'orderby'        =&gt; 'ID',\r\n            'order'          =&gt; 'DESC',\r\n        ] );\r\n\r\n        $total_pages = max(1, $q-&gt;max_num_pages);\r\n\r\n        \/\/ \u5168\u57df\u5c6c\u6027\uff08\u505a\u70ba\u6b04\u4f4d\u8207\u6bcd\u9078\u55ae\u4f86\u6e90\uff09\u2014\u2014\u4ee5\u300c\u5c6c\u6027 slug\uff08attribute_name\uff09\u300d\u5347\u51aa\u6392\u5e8f\r\n        $attribute_taxonomies = wc_get_attribute_taxonomies(); \/\/ object[]: attribute_id, attribute_name, attribute_label\r\n        usort($attribute_taxonomies, function($a,$b){\r\n            return strnatcasecmp($a-&gt;attribute_name, $b-&gt;attribute_name);\r\n        });\r\n\r\n        \/\/ \u6bcd\u9078\u55ae options\uff08\u4f9d\u5c6c\u6027 slug \u5347\u51aa\uff09\r\n        $attr_options = '';\r\n        if ( $attribute_taxonomies ) {\r\n            foreach ( $attribute_taxonomies as $at ) {\r\n                $label = $at-&gt;attribute_label ? $at-&gt;attribute_label : $at-&gt;attribute_name;\r\n                $attr_options .= sprintf(\r\n                    '&lt;option value=\"%s\"&gt;%s (%s)&lt;\/option&gt;',\r\n                    esc_attr( $at-&gt;attribute_name ),\r\n                    esc_html( $label ),\r\n                    esc_html( 'pa_' . $at-&gt;attribute_name )\r\n                );\r\n            }\r\n        }\r\n\r\n        \/\/ \u7be9\u9078\u4e0b\u62c9\r\n        $cat_dd = wp_dropdown_categories( [\r\n            'taxonomy'        =&gt; 'product_cat',\r\n            'name'            =&gt; 'product_cat',\r\n            'orderby'         =&gt; 'name',\r\n            'hide_empty'      =&gt; false,\r\n            'hierarchical'    =&gt; true,\r\n            'show_option_all' =&gt; '\u5168\u90e8\u5206\u985e',\r\n            'value_field'     =&gt; 'slug',\r\n            'selected'        =&gt; $cat,\r\n            'echo'            =&gt; 0,\r\n        ] );\r\n        $tag_terms = get_terms( [ 'taxonomy' =&gt; 'product_tag', 'hide_empty' =&gt; false ] );\r\n        $tag_dd = '&lt;select name=\"product_tag\"&gt;&lt;option value=\"\"&gt;\u5168\u90e8\u6a19\u7c64&lt;\/option&gt;';\r\n        if ( ! is_wp_error( $tag_terms ) ) {\r\n            foreach ( $tag_terms as $t ) {\r\n                $tag_dd .= sprintf(\r\n                    '&lt;option value=\"%s\"%s&gt;%s&lt;\/option&gt;',\r\n                    esc_attr( $t-&gt;slug ),\r\n                    selected( $tag, $t-&gt;slug, false ),\r\n                    esc_html( $t-&gt;name )\r\n                );\r\n            }\r\n        }\r\n        $tag_dd .= '&lt;\/select&gt;';\r\n\r\n        echo '&lt;div class=\"wrap wc-abm-wrap\"&gt;';\r\n        echo '&lt;h1 class=\"wp-heading-inline\"&gt;\u5c6c\u6027\u6279\u6b21\u7ba1\u7406&lt;\/h1&gt;';\r\n\r\n        \/\/ \u7be9\u9078\u5217\uff08GET\uff09\r\n        echo '&lt;form method=\"get\" class=\"filters\"&gt;';\r\n        echo '&lt;input type=\"hidden\" name=\"page\" value=\"'.esc_attr(self::SLUG).'\"\/&gt;';\r\n        echo $cat_dd;\r\n        echo $tag_dd;\r\n        submit_button( '\u7be9\u9078', 'secondary', '', false );\r\n        echo '&lt;\/form&gt;';\r\n\r\n        \/\/ \u5217\u8868 + \u6279\u6b21\u64cd\u4f5c\uff08POST\uff09\r\n        echo '&lt;form method=\"post\" action=\"'. esc_url( admin_url('admin-post.php') ) .'\"&gt;';\r\n        wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD );\r\n        echo '&lt;input type=\"hidden\" name=\"action\" value=\"wc_abm_apply\"\/&gt;';\r\n\r\n        \/\/ \u4fdd\u7559\u539f\u904e\u6ffe\u8207\u9801\u78bc\uff08\u63d0\u4ea4\u5f8c\u5e36\u56de\uff09\r\n        echo '&lt;input type=\"hidden\" name=\"_abm_keep_cat\" value=\"'.esc_attr($cat).'\"\/&gt;';\r\n        echo '&lt;input type=\"hidden\" name=\"_abm_keep_tag\" value=\"'.esc_attr($tag).'\"\/&gt;';\r\n        echo '&lt;input type=\"hidden\" name=\"_abm_keep_paged\" value=\"'.esc_attr($paged).'\"\/&gt;';\r\n\r\n        \/\/ \u6279\u6b21\u64cd\u4f5c\uff08\u6bcd\u5b50\u5f0f\uff09\r\n        echo '&lt;div class=\"bulk-actions\"&gt;';\r\n        echo '&lt;label&gt;\u5c6c\u6027\uff08\u6bcd\u9078\u55ae\uff09\uff1a&lt;\/label&gt;';\r\n        echo '&lt;select id=\"wc-abm-attr\" name=\"attr\" required&gt;';\r\n        echo '&lt;option value=\"\"&gt;\u8acb\u9078\u64c7\u5c6c\u6027&lt;\/option&gt;';\r\n        echo $attr_options ?: '&lt;option value=\"\"&gt;\uff08\u5c1a\u672a\u5efa\u7acb\u4efb\u4f55\u5168\u57df\u5c6c\u6027\uff09&lt;\/option&gt;';\r\n        echo '&lt;\/select&gt;';\r\n\r\n        echo '&lt;label&gt;\u5c6c\u6027\u9805\u76ee\uff08\u5b50\u9078\u55ae\uff09\uff1a&lt;\/label&gt;';\r\n        echo '&lt;select id=\"wc-abm-term\" name=\"term_id\" required&gt;';\r\n        echo '&lt;option value=\"\"&gt;\u8acb\u5148\u9078\u64c7\u5c6c\u6027&lt;\/option&gt;';\r\n        echo '&lt;\/select&gt;';\r\n\r\n        submit_button( '\u5957\u7528\u81f3\u52fe\u9078\u5546\u54c1', 'primary', 'wc-abm-apply', false, [ 'style' =&gt; 'margin-left:8px;' ] );\r\n        echo '&lt;\/div&gt;';\r\n\r\n        \/\/ \u8868\u683c\u5916\u5c64\uff1a\u5fc5\u8981\u6642\u6c34\u5e73\u6efe\u52d5\r\n        echo '&lt;div class=\"wc-abm-table-wrap\"&gt;';\r\n\r\n        \/\/ \u8868\u982d\uff1a\u52fe\u9078\u3001ID\u3001\u5546\u54c1\u540d\u7a31 + \u300c\u6bcf\u500b\u5c6c\u6027\u4e00\u6b04\u300d(\u5c6c\u6027\u4ee5 slug \u5347\u51aa)\r\n        echo '&lt;table class=\"widefat fixed striped wc-abm-table\"&gt;';\r\n        echo '&lt;thead&gt;&lt;tr&gt;';\r\n        echo '&lt;th style=\"width:32px;\"&gt;&lt;input type=\"checkbox\" id=\"wc-abm-checkall\"\/&gt;&lt;\/th&gt;';\r\n        echo '&lt;th style=\"width:80px;\"&gt;ID&lt;\/th&gt;';\r\n        echo '&lt;th&gt;\u5546\u54c1\u540d\u7a31&lt;\/th&gt;';\r\n        if ( $attribute_taxonomies ) {\r\n            foreach ( $attribute_taxonomies as $at ) {\r\n                $label = $at-&gt;attribute_label ?: $at-&gt;attribute_name;\r\n                echo '&lt;th&gt;'. esc_html( $label ) .'&lt;\/th&gt;'; \/\/ \u8868\u982d\u5141\u8a31\u63db\u884c\r\n            }\r\n        }\r\n        echo '&lt;\/tr&gt;&lt;\/thead&gt;&lt;tbody&gt;';\r\n\r\n        if ( $q-&gt;have_posts() ) {\r\n            while ( $q-&gt;have_posts() ) { $q-&gt;the_post();\r\n                $product = wc_get_product( get_the_ID() );\r\n                if ( ! $product ) continue;\r\n\r\n                $id  = $product-&gt;get_id();\r\n\r\n                echo '&lt;tr&gt;';\r\n                echo '&lt;td&gt;&lt;input type=\"checkbox\" class=\"wc-abm-rowcheck\" name=\"product_ids[]\" value=\"'.esc_attr($id).'\"\/&gt;&lt;\/td&gt;';\r\n                echo '&lt;td&gt;'.esc_html($id).'&lt;\/td&gt;';\r\n                echo '&lt;td&gt;&lt;a href=\"'.esc_url( get_edit_post_link($id) ).'\"&gt;'.esc_html( get_the_title() ).'&lt;\/a&gt;&lt;\/td&gt;';\r\n\r\n                if ( $attribute_taxonomies ) {\r\n                    foreach ( $attribute_taxonomies as $at ) {\r\n                        $taxonomy = wc_attribute_taxonomy_name( $at-&gt;attribute_name ); \/\/ pa_x\r\n                        echo '&lt;td&gt;'.$this-&gt;render_terms_chips( $id, $taxonomy ).'&lt;\/td&gt;';\r\n                    }\r\n                }\r\n\r\n                echo '&lt;\/tr&gt;';\r\n            }\r\n        } else {\r\n            $colspan = 3 + ( $attribute_taxonomies ? count($attribute_taxonomies) : 0 );\r\n            echo '&lt;tr&gt;&lt;td colspan=\"'.intval($colspan).'\"&gt;\u67e5\u7121\u5546\u54c1\u3002&lt;\/td&gt;&lt;\/tr&gt;';\r\n        }\r\n        wp_reset_postdata();\r\n\r\n        echo '&lt;\/tbody&gt;&lt;\/table&gt;';\r\n        echo '&lt;\/div&gt;'; \/\/ .wc-abm-table-wrap\r\n\r\n        \/\/ \u5206\u9801\r\n        if ( $total_pages &gt; 1 ) {\r\n            $base_url = remove_query_arg( 'paged' );\r\n            $base_url = add_query_arg( [\r\n                'page'        =&gt; self::SLUG,\r\n                'product_cat' =&gt; $cat,\r\n                'product_tag' =&gt; $tag,\r\n            ], admin_url('admin.php') );\r\n            echo '&lt;div class=\"tablenav wc-abm-pagination\"&gt;&lt;div class=\"tablenav-pages\"&gt;';\r\n            echo paginate_links( [\r\n                'base'      =&gt; add_query_arg( 'paged', '%#%', $base_url ),\r\n                'format'    =&gt; '',\r\n                'prev_text' =&gt; '\u00ab',\r\n                'next_text' =&gt; '\u00bb',\r\n                'current'   =&gt; $paged,\r\n                'total'     =&gt; $total_pages,\r\n            ] );\r\n            echo '&lt;\/div&gt;&lt;\/div&gt;';\r\n        }\r\n\r\n        echo '&lt;\/form&gt;';\r\n        echo '&lt;\/div&gt;&lt;!-- .wrap --&gt;';\r\n    }\r\n\r\n    \/\/ \u5217\u8868\u6bcf\u683c\uff1a\u7dad\u6301\u4e0d\u63db\u884c\uff1b\u9805\u76ee\u9806\u5e8f\u7dad\u6301\u9810\u8a2d\uff08\u4e0d\u5f37\u5236\u4ee5\u9805\u76ee slug \u6392\u5e8f\uff09\r\n    private function render_terms_chips( int $product_id, string $taxonomy ) : string {\r\n        if ( ! taxonomy_exists( $taxonomy ) ) return '\u2014';\r\n        $terms = get_the_terms( $product_id, $taxonomy );\r\n        if ( is_wp_error($terms) || empty($terms) ) return '\u2014';\r\n\r\n        $html = '';\r\n        foreach ( $terms as $t ) {\r\n            $html .= '&lt;span class=\"term-chip\"&gt;'. esc_html($t-&gt;name) .'&lt;\/span&gt;';\r\n        }\r\n        return $html ?: '\u2014';\r\n    }\r\n\r\n    public function handle_apply() {\r\n        if ( ! current_user_can( 'manage_woocommerce' ) ) wp_die( '\u6b0a\u9650\u4e0d\u8db3' );\r\n        check_admin_referer( self::NONCE_ACTION, self::NONCE_FIELD );\r\n\r\n        $product_ids = isset($_POST['product_ids']) ? array_map('intval', (array)$_POST['product_ids']) : [];\r\n        $attr_key    = isset($_POST['attr']) ? sanitize_text_field($_POST['attr']) : '';   \/\/ \u53ef\u80fd 'color' \u6216 'pa_color'\r\n        $term_id     = isset($_POST['term_id']) ? intval($_POST['term_id']) : 0;\r\n\r\n        \/\/ \u4fdd\u7559\u539f\u904e\u6ffe\u8207\u9801\u78bc\r\n        $keep_cat   = isset($_POST['_abm_keep_cat']) ? sanitize_text_field($_POST['_abm_keep_cat']) : '';\r\n        $keep_tag   = isset($_POST['_abm_keep_tag']) ? sanitize_text_field($_POST['_abm_keep_tag']) : '';\r\n        $keep_paged = isset($_POST['_abm_keep_paged']) ? intval($_POST['_abm_keep_paged']) : 1;\r\n\r\n        if ( empty($product_ids) || empty($attr_key) || empty($term_id) ) {\r\n            return $this-&gt;redirect_with_msg( '\u7f3a\u5c11\u5fc5\u8981\u53c3\u6578\u6216\u672a\u52fe\u9078\u5546\u54c1\u3002', 'error', $keep_cat, $keep_tag, $keep_paged );\r\n        }\r\n\r\n        \/\/ \u6b63\u898f\u5316 taxonomy\r\n        $taxonomy = taxonomy_exists( 'pa_' . $attr_key ) ? ( 'pa_' . $attr_key ) : $attr_key;\r\n        if ( ! taxonomy_exists( $taxonomy ) ) {\r\n            return $this-&gt;redirect_with_msg( '\u6307\u5b9a\u7684\u5c6c\u6027 taxonomy \u4e0d\u5b58\u5728\uff1a' . esc_html( $taxonomy ), 'error', $keep_cat, $keep_tag, $keep_paged );\r\n        }\r\n\r\n        \/\/ \u5c0d\u61c9 attribute_id\uff08\u7528\u65bc WC_Product_Attribute\uff09\r\n        $attr_id = 0;\r\n        $all_attr = wc_get_attribute_taxonomies();\r\n        if ( $all_attr ) {\r\n            foreach ( $all_attr as $a ) {\r\n                $tax_name = wc_attribute_taxonomy_name( $a-&gt;attribute_name ); \/\/ pa_x\r\n                if ( $tax_name === $taxonomy ) { $attr_id = intval( $a-&gt;attribute_id ); break; }\r\n            }\r\n        }\r\n\r\n        $success = 0; $fail = 0;\r\n        foreach ( $product_ids as $pid ) {\r\n            $ok = $this-&gt;apply_to_product( $pid, $taxonomy, $attr_id, $term_id );\r\n            if ( $ok ) $success++; else $fail++;\r\n        }\r\n\r\n        $msg = sprintf( '\u5957\u7528\u5b8c\u6210\uff1a\u6210\u529f %d \u7b46\uff0c\u5931\u6557 %d \u7b46\u3002', $success, $fail );\r\n        return $this-&gt;redirect_with_msg( $msg, $fail ? 'warning' : 'updated', $keep_cat, $keep_tag, $keep_paged );\r\n    }\r\n\r\n    private function apply_to_product( int $product_id, string $taxonomy, int $attr_id, int $term_id ) : bool {\r\n        try {\r\n            $product = wc_get_product( $product_id );\r\n            if ( ! $product ) return false;\r\n\r\n            \/\/ 1) \u5408\u4f75\u6307\u6d3e term\r\n            $current_terms = wp_get_object_terms( $product_id, $taxonomy, [ 'fields' =&gt; 'ids' ] );\r\n            if ( is_wp_error( $current_terms ) ) $current_terms = [];\r\n            if ( ! in_array( $term_id, $current_terms, true ) ) {\r\n                $merged = array_unique( array_filter( array_merge( $current_terms, [ $term_id ] ) ) );\r\n                wp_set_object_terms( $product_id, $merged, $taxonomy, false );\r\n            }\r\n\r\n            \/\/ 2) \u78ba\u4fdd product attributes \u5167\u5b58\u5728\u8a72\u5c6c\u6027\uff1b\u82e5\u7121\u2192\u65b0\u589e\u4e14\u6392\u6700\u5f8c\r\n            $attributes = $product-&gt;get_attributes(); \/\/ WC_Product_Attribute[]\r\n            $found_key = null;\r\n            foreach ( $attributes as $key =&gt; $attr ) {\r\n                if ( $attr-&gt;get_name() === $taxonomy ) { $found_key = $key; break; }\r\n            }\r\n\r\n            if ( is_null( $found_key ) ) {\r\n                $new = new WC_Product_Attribute();\r\n                if ( $attr_id ) $new-&gt;set_id( $attr_id );\r\n                $new-&gt;set_name( $taxonomy );\r\n                $new-&gt;set_visible( true );\r\n                $new-&gt;set_variation( false );\r\n                $new-&gt;set_options( [ $term_id ] );\r\n\r\n                $max_position = 0;\r\n                foreach ( $attributes as $a ) {\r\n                    $max_position = max( $max_position, intval( $a-&gt;get_position() ) );\r\n                }\r\n                $new-&gt;set_position( $max_position + 1 );\r\n                $attributes[] = $new;\r\n            } else {\r\n                \/** @var WC_Product_Attribute $exist *\/\r\n                $exist = $attributes[ $found_key ];\r\n                $opts  = $exist-&gt;is_taxonomy()\r\n                    ? array_map('intval', (array)$exist-&gt;get_options())\r\n                    : (array)$exist-&gt;get_options();\r\n\r\n                if ( ! in_array( $term_id, $opts, true ) ) {\r\n                    $opts[] = $term_id;\r\n                    $exist-&gt;set_options( $opts );\r\n                }\r\n                $attributes[ $found_key ] = $exist;\r\n            }\r\n\r\n            $product-&gt;set_attributes( $attributes );\r\n            $product-&gt;save();\r\n            wc_delete_product_transients( $product_id );\r\n            clean_post_cache( $product_id );\r\n\r\n            return true;\r\n        } catch ( Throwable $e ) {\r\n            error_log( '[WC ABM] Failed on product '. $product_id .': '. $e-&gt;getMessage() );\r\n            return false;\r\n        }\r\n    }\r\n\r\n    private function redirect_with_msg( string $msg, string $type = 'updated', string $keep_cat = '', string $keep_tag = '', int $keep_paged = 1 ) {\r\n        $args = [\r\n            'page'        =&gt; self::SLUG,\r\n            'abmmsg'      =&gt; rawurlencode( $msg ),\r\n            'abmtype'     =&gt; $type,\r\n        ];\r\n        if ( $keep_cat )   $args['product_cat'] = $keep_cat;\r\n        if ( $keep_tag )   $args['product_tag'] = $keep_tag;\r\n        if ( $keep_paged &amp;&amp; $keep_paged &gt; 1 ) $args['paged'] = $keep_paged;\r\n\r\n        $url = add_query_arg( $args, admin_url( 'admin.php' ) );\r\n        wp_safe_redirect( $url );\r\n        exit;\r\n    }\r\n}\r\n\r\nnew WC_Attribute_Bulk_Manager();\r\n\r\nadd_action( 'admin_notices', function(){\r\n    if ( ! is_admin() ) return;\r\n    if ( ! isset($_GET['page']) || $_GET['page'] !== WC_Attribute_Bulk_Manager::SLUG ) return;\r\n    if ( empty($_GET['abmmsg']) ) return;\r\n\r\n    $type = isset($_GET['abmtype']) ? sanitize_text_field($_GET['abmtype']) : 'updated';\r\n    $msg  = wp_kses_post( wp_unslash( $_GET['abmmsg'] ) );\r\n    printf( '&lt;div class=\"notice notice-%s is-dismissible\"&gt;&lt;p&gt;%s&lt;\/p&gt;&lt;\/div&gt;', esc_attr($type), $msg );\r\n});\r\n<\/pre>\n<p><a href=\"http:\/\/mynotes.org\/tech\/wp-content\/uploads\/2025\/09\/wc-attribute-bulk-manager.zip\">wc-attribute-bulk-manager<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>WooCommerce \u5f8c\u53f0\u6e05\u55ae\u9801\uff0c\u4e0a\u65b9\u53ef\u7528\u5206\u985e\/\u6a19\u7c64\u7be9\u9078\uff1b\u63d0\u4f9b\u6bcd\u5b50\u5f0f\u4e0b\u62c9\u9078\u55ae\uff0c\u6279\u6b21\u70ba\u52fe\u9078\u5546\u54c1\u52a0\u5165\u6307\u5b9a\u5c6c\u6027\u8207\u5c6c [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":"","_links_to":"","_links_to_target":""},"categories":[129,43,44,127,42],"tags":[],"class_list":["post-6697","post","type-post","status-publish","format-standard","hentry","category-php-program","category-wordpress-website-built","category-plugins-wordpress-website-built","category-program","category-website-built"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/mynotes.org\/tech\/wp-json\/wp\/v2\/posts\/6697","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mynotes.org\/tech\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mynotes.org\/tech\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mynotes.org\/tech\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mynotes.org\/tech\/wp-json\/wp\/v2\/comments?post=6697"}],"version-history":[{"count":2,"href":"https:\/\/mynotes.org\/tech\/wp-json\/wp\/v2\/posts\/6697\/revisions"}],"predecessor-version":[{"id":6700,"href":"https:\/\/mynotes.org\/tech\/wp-json\/wp\/v2\/posts\/6697\/revisions\/6700"}],"wp:attachment":[{"href":"https:\/\/mynotes.org\/tech\/wp-json\/wp\/v2\/media?parent=6697"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mynotes.org\/tech\/wp-json\/wp\/v2\/categories?post=6697"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mynotes.org\/tech\/wp-json\/wp\/v2\/tags?post=6697"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}