'POST', 'callback' => [$this, 'get_products_lessDetail'], 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], 'args' => [], ]); register_rest_route($namespace, '/products_moreDetail', [ 'methods' => 'POST', 'callback' => [$this, 'get_products_moreDetail'], 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], 'args' => [], ]); register_rest_route($namespace, '/product-reviews', [ 'methods' => 'POST', 'callback' => [$this, 'get_product_reviews'], 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], 'args' => [], ]); register_rest_route($namespace, '/plugin/settings', [ 'methods' => 'GET', 'callback' => [$this, 'get_agent_settings'], 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ]); } /** * 获取商品列表(详细版) * 支持任意维度搜索:按 IDs、关键词、价格区间查询 * 支持排序:上架时间(new)、销量(hot) * 特殊关键词 "random" 随机返回商品 * * @param WP_REST_Request $request * @return WP_REST_Response */ public function get_products_moreDetail(WP_REST_Request $request) { $body = $request->get_json_params(); if (!is_array($body)) { return new WP_REST_Response( ['success' => false, 'message' => 'Invalid JSON body'], 200 ); } // 参数解析 $search = []; if (isset($body['search'])) { if (is_array($body['search'])) { $search = array_values(array_unique(array_filter( array_map('sanitize_text_field', $body['search']) ))); } else { $single = sanitize_text_field($body['search']); if ($single !== '') { $search = [$single]; } } } $ids = isset($body['ids']) && is_array($body['ids']) ? array_values(array_unique(array_filter(array_map('absint', $body['ids'])))) : []; // 价格区间参数 $min_price = isset($body['min_price']) ? floatval($body['min_price']) : null; $max_price = isset($body['max_price']) ? floatval($body['max_price']) : null; // size 优先,pageSize 向后兼容 $size_raw = $body['size'] ?? $body['pageSize'] ?? 16; $per_page = max(1, min(50, absint($size_raw))); // sort 排序方式:hot(销量)、new(上架时间),默认不排序 $sort = isset($body['sort']) ? sanitize_text_field($body['sort']) : ''; if ($sort !== '' && !in_array($sort, ['hot', 'new'], true)) { $sort = ''; } // 判断是否为 random 特殊关键词 $is_random = (count($search) === 1 && strtolower($search[0]) === 'random'); if (empty($ids) && empty($search) && $min_price === null && $max_price === null) { return new WP_REST_Response( ['success' => false, 'message' => 'Invalid Param. Need ids, search or price range (min_price/max_price)'], 200 ); } $result = []; // 场景一:按 IDs 查询 if (!empty($ids)) { $seen_ids = []; foreach ($ids as $id) { $product = wc_get_product($id); if (!$product || $product->get_status() !== 'publish') { continue; } if ($product->is_type('variation')) { $product = wc_get_product($product->get_parent_id()); if (!$product || $product->get_status() !== 'publish') { continue; } } $pid = $product->get_id(); if (isset($seen_ids[$pid])) { continue; } $seen_ids[$pid] = true; if (!$this->match_price_range($product, $min_price, $max_price)) { continue; } $result[] = $this->build_full_product_data($product); if (count($result) >= $per_page) { break; } } } elseif ($is_random) { // 特殊场景:关键词为 "random",随机返回商品 $args = ['status' => 'publish', 'return' => 'ids', 'limit' => self::RANDOM_SAMPLE_LIMIT]; $this->apply_price_filter($args, $min_price, $max_price); $all_ids = wc_get_products($args); if (!empty($all_ids)) { shuffle($all_ids); $picked_ids = array_slice($all_ids, 0, $per_page); foreach ($picked_ids as $id) { $product = wc_get_product($id); if ($product instanceof WC_Product) { $result[] = $this->build_full_product_data($product); } } } } elseif (!empty($search)) { // 场景二:按关键词搜索,跨关键词按 id 去重(sequential 策略) $seen_ids = []; foreach ($search as $keyword) { if (count($result) >= $per_page) { break; } $query_args = ['status' => 'publish', 's' => $keyword]; $this->apply_price_filter($query_args, $min_price, $max_price); $products = $this->search_by_sort($query_args, $per_page, $sort); foreach ($products as $product) { if (!($product instanceof WC_Product)) { continue; } $pid = $product->get_id(); if (isset($seen_ids[$pid])) { continue; } $seen_ids[$pid] = true; $result[] = $this->build_full_product_data($product); if (count($result) >= $per_page) { break 2; } } } // 分类兜底(结果不足时补货) if (count($result) < $per_page) { foreach ($search as $kw) { $term = get_term_by('slug', sanitize_title($kw), 'product_cat') ?: get_term_by('name', $kw, 'product_cat'); if (!$term) { continue; } $query_args = ['status' => 'publish', 'category' => [$term->slug]]; $this->apply_price_filter($query_args, $min_price, $max_price); $products = $this->search_by_sort($query_args, $per_page, $sort); foreach ($products as $product) { if (!($product instanceof WC_Product)) { continue; } $pid = $product->get_id(); if (isset($seen_ids[$pid])) { continue; } $seen_ids[$pid] = true; $result[] = $this->build_full_product_data($product); if (count($result) >= $per_page) { break 2; } } } } } elseif ($min_price !== null || $max_price !== null) { // 场景:仅按价格区间搜索(无关键词、无 IDs) $query_args = ['status' => 'publish']; $this->apply_price_filter($query_args, $min_price, $max_price); $products = $this->search_by_sort($query_args, $per_page, $sort); foreach ($products as $product) { if ($product instanceof WC_Product) { $result[] = $this->build_full_product_data($product); } } } return new WP_REST_Response( ['success' => true, 'data' => ['products' => $result]], 200 ); } /** * 判断商品是否在指定价格区间内 * * @param WC_Product $product * @param float|null $min_price * @param float|null $max_price * @return bool */ private function match_price_range(WC_Product $product, ?float $min_price, ?float $max_price): bool { if ($min_price === null && $max_price === null) { return true; } $price = floatval($product->get_price()); if ($min_price !== null && $price < $min_price) { return false; } if ($max_price !== null && $price > $max_price) { return false; } return true; } /** * 将价格区间条件追加到 wc_get_products 查询参数中 * * @param array &$args * @param float|null $min_price * @param float|null $max_price */ private function apply_price_filter(array &$args, ?float $min_price, ?float $max_price): void { if ($min_price !== null) { $args['min_price'] = $min_price; } if ($max_price !== null) { $args['max_price'] = $max_price; } } private const RANDOM_SAMPLE_LIMIT = 500; /** * 按排序策略查询商品列表 * - hot:按 total_sales 降序(销量排序) * - new:按发布日期降序(上架时间排序) * - 空字符串(默认):从最多 RANDOM_SAMPLE_LIMIT 个 ID 中 shuffle 随机抽取 * * @param array $args 基础查询参数(status/s/category/min_price/max_price 等) * @param int $per_page 返回数量上限 * @param string $sort 排序策略:hot | new | ''(默认随机) * @return array WC_Product[] */ private function search_by_sort(array $args, int $per_page, string $sort): array { switch ($sort) { case 'hot': $args['orderby'] = 'meta_value_num'; $args['meta_key'] = 'total_sales'; $args['order'] = 'DESC'; $args['limit'] = $per_page; $args['return'] = 'ids'; $picked_ids = wc_get_products($args); break; case 'new': $args['orderby'] = 'date'; $args['order'] = 'DESC'; $args['limit'] = $per_page; $args['return'] = 'ids'; $picked_ids = wc_get_products($args); break; default: // 随机 $id_args = $args; $id_args['return'] = 'ids'; $id_args['limit'] = self::RANDOM_SAMPLE_LIMIT; $sampled_ids = wc_get_products($id_args); if (empty($sampled_ids)) { return []; } shuffle($sampled_ids); $picked_ids = array_slice($sampled_ids, 0, $per_page); break; } if (empty($picked_ids)) { return []; } $products = []; foreach ($picked_ids as $id) { $product = wc_get_product($id); if ($product instanceof WC_Product) { $products[] = $product; } } return $products; } /** * 构建完整商品数据(基础信息 + 类型适配) * * @param WC_Product $product * @return array */ private function build_full_product_data(WC_Product $product): array { $data = $this->build_base_product_data_moreDetail($product); $data['type'] = $product->get_type(); if ($product->is_type('variable')) { $data['variations'] = $this->get_formatted_variations($product); } elseif ($product->is_type('external')) { $data['product_url'] = $product->get_product_url(); $data['button_text'] = $product->get_button_text(); $data['variations'] = []; } elseif ($product->is_type('grouped')) { $data['grouped_products'] = $product->get_children(); $data['variations'] = []; } else { $data['variations'] = []; } return $data; } /** * 获取商品列表(简洁版) * * @param WP_REST_Request $request * @return WP_REST_Response */ public function get_products_lessDetail(WP_REST_Request $request) { $body = $request->get_json_params(); if (!is_array($body)) { return new WP_REST_Response( ['success' => false, 'message' => 'Invalid JSON body'], 200 ); } $search = isset($body['search']) ? sanitize_text_field($body['search']) : ''; $ids = isset($body['ids']) && is_array($body['ids']) ? array_map('absint', $body['ids']) : []; $per_page = isset($body['pageSize']) ? absint($body['pageSize']) : 20; $page = isset($body['page']) ? absint($body['page']) : 1; // 参数校验 $per_page = max(1, min(50, $per_page)); $page = max(1, $page); // 清理掉 0/重复 id $ids = array_values(array_unique(array_filter($ids))); if (empty($ids) && $search === '') { return new WP_REST_Response( ['success' => false, 'message' => 'Invalid Param. Need ids or search'], 200 ); } $result = []; // 场景一:按 IDs 查询(支持主商品 + 变体混合) if (!empty($ids) && is_array($ids)) { $ids = array_filter(array_map('absint', $ids), fn($id) => $id > 0); $ids = array_unique($ids); foreach ($ids as $id) { $product = wc_get_product($id); // 跳过无效或非 publish 状态的商品 if (!$product || !in_array($product->get_status(), ['publish'], true)) { continue; } $result[] = $this->build_base_product_data_lessDetail($product); } } else { // 场景二:按关键词搜索(仅主商品,WooCommerce 原生支持) $args = [ 'status' => 'publish', 'limit' => $per_page, 'page' => $page, 'orderby' => 'date', 'order' => 'DESC', ]; if (!empty($search)) { $args['s'] = $search; } $products = wc_get_products($args); foreach ($products as $product) { if (!($product instanceof WC_Product)) { continue; } $result[] = $this->build_base_product_data_lessDetail($product); } } return new WP_REST_Response( [ 'success' => true, 'data' => [ 'products' => $result, ], ], 200 ); } /** * 构建基础商品数据(简洁版) * * @param WC_Product $product * @return array */ private function build_base_product_data_lessDetail($product) { $image_id = $product->get_image_id(); $image_url = $image_id ? wp_get_attachment_url($image_id) : ''; $gallery_urls = array_map('wp_get_attachment_url', array_filter($product->get_gallery_image_ids())); $categories = wp_get_object_terms($product->get_id(), 'product_cat', ['fields' => 'names']); $tags = wp_get_object_terms($product->get_id(), 'product_tag', ['fields' => 'names']); $meta_data = []; $custom_metas = ['sales_rank', 'recommended_reason', 'subsidy_reason', 'bundled_items', 'delivery_info']; foreach ($custom_metas as $key) { $value = get_post_meta($product->get_id(), $key, true); if ($value !== '') { $meta_data[$key] = $value; } } // 获取动态小数位数 $decimals = wc_get_price_decimals(); return [ 'id' => $product->get_id(), 'name' => $product->get_name(), 'price' => $this->format_price($product->get_price(), $decimals), 'regular_price' => $this->format_price($product->get_regular_price(), $decimals), 'sale_price' => $this->format_price($product->get_sale_price(), $decimals), 'image' => $image_url, // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 'description' => apply_filters('the_content', $product->get_description()), 'currency_symbol' => html_entity_decode(get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8'), 'currency' => get_woocommerce_currency(), 'type' => $product->get_type(), ]; } /** * 构建基础商品数据(详细版) * * @param WC_Product $product * @return array */ private function build_base_product_data_moreDetail($product) { $image_id = $product->get_image_id(); $image_url = $image_id ? wp_get_attachment_url($image_id) : ''; $gallery_urls = array_map('wp_get_attachment_url', array_filter($product->get_gallery_image_ids())); // 合并主图到图册数组中 $all_images = []; if ($image_url) { $all_images[] = $image_url; } // 合并并去重 $all_images = array_unique(array_merge($all_images, $gallery_urls)); $categories = wp_get_object_terms($product->get_id(), 'product_cat', ['fields' => 'names']); $categories = array_map('wp_specialchars_decode', $categories); $tags = wp_get_object_terms($product->get_id(), 'product_tag', ['fields' => 'names']); $meta_data = []; $custom_metas = ['sales_rank', 'recommended_reason', 'subsidy_reason', 'bundled_items', 'delivery_info']; foreach ($custom_metas as $key) { $value = get_post_meta($product->get_id(), $key, true); if ($value !== '') { $meta_data[$key] = $value; } } $decimals = wc_get_price_decimals(); return [ 'id' => $product->get_id(), 'name' => $product->get_name(), 'price' => $this->format_price($product->get_price(), $decimals), 'regular_price' => $this->format_price($product->get_regular_price(), $decimals), 'sale_price' => $this->format_price($product->get_sale_price(), $decimals), 'image' => $image_url, 'permalink' => $product->get_permalink(), 'sku' => $product->get_sku(), 'stock_quantity' => $product->get_stock_quantity(), 'in_stock' => $product->is_in_stock(), 'categories' => $categories, 'tags' => $tags, 'short_description' => wp_strip_all_tags($product->get_short_description()), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound 'description' => apply_filters('the_content', $product->get_description()), 'average_rating' => round($product->get_average_rating(), 1), 'review_count' => $product->get_rating_count(), 'meta' => $meta_data, 'gallery_images' => array_values($all_images), 'currency_symbol' => html_entity_decode(get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8'), 'currency' => get_woocommerce_currency(), 'type' => $product->get_type(), ]; } /** * 安全格式化价格函数:处理空值和补零 * * @param mixed $price * @param int $decimals * @return string */ private function format_price($price, int $decimals): string { if ('' === $price || null === $price || false === $price) { return ''; } return wc_format_decimal($price, $decimals); } /** * 获取商品属性 * * @param WC_Product $product * @return array */ private function get_product_attributes($product) { $attributes = []; foreach ($product->get_attributes() as $attr_key => $attribute) { $name = wc_attribute_label($attribute->get_name()); $slug = $attribute->get_name(); $visible = (bool) $attribute->get_visible(); if ($attribute->is_taxonomy()) { // 全局属性(如 pa_color) $taxonomy = $attribute->get_name(); // 获取该 taxonomy 下所有可用 terms $terms = get_terms([ 'taxonomy' => $taxonomy, 'hide_empty' => false, ]); $options = []; if (!is_wp_error($terms)) { foreach ($terms as $term) { if ($term instanceof WP_Term) { $options[] = $term->name; } } } // 获取当前商品实际用到的属性值 $used_values = $this->get_used_variation_attribute_values($product, $taxonomy); $options = array_unique($used_values); } else { // 自定义文本属性 $options = array_filter( array_map('trim', (array) $attribute->get_options()), fn($v) => !empty($v) ); } $attributes[] = [ 'name' => $name, 'slug' => $slug, 'visible' => $visible, 'options' => array_values($options), ]; } return $attributes; } /** * 获取可变商品实际使用的属性值 * * @param WC_Product $product * @param string $taxonomy * @return array */ private function get_used_variation_attribute_values($product, $taxonomy) { if (!$product->is_type('variable')) { return []; } $values = []; $variations = $product->get_children(); foreach ($variations as $variation_id) { $variation = wc_get_product($variation_id); if (!$variation) { continue; } $attr_name = 'attribute_' . sanitize_title(str_replace('pa_', '', $taxonomy)); $value = $variation->get_attribute($attr_name); if ($value) { // 如果是 taxonomy,$value 是 slug;需要转成 name if (taxonomy_exists($taxonomy)) { $term = get_term_by('slug', $value, $taxonomy); if ($term && !is_wp_error($term)) { $values[] = $term->name; } else { $values[] = $value; } } else { $values[] = $value; } } } return array_unique($values); } /** * 获取格式化后的变体列表 * * @param WC_Product $variable_product * @return array */ private function get_formatted_variations($variable_product) { $variations = []; $available_variations = $variable_product->get_available_variations(); $decimals = wc_get_price_decimals(); foreach ($available_variations as $variation_data) { $variation_id = $variation_data['variation_id']; $variation = wc_get_product($variation_id); if (!$variation) { continue; } // 拼接变体名称 $attr_parts = []; foreach ($variation_data['attributes'] as $attr_name => $attr_value) { $clean_name = str_replace('attribute_', '', $attr_name); if ($attr_value) { $taxonomy = 'pa_' . sanitize_title($clean_name); if (taxonomy_exists($taxonomy)) { $term = get_term_by('slug', $attr_value, $taxonomy); $attr_parts[] = $term ? $term->name : $attr_value; } else { $attr_parts[] = $attr_value; } } } $variation_name = (!empty($attr_parts) ? '【' . implode('】【', $attr_parts) . '】' : '') . $variable_product->get_name(); $variations[] = [ 'id' => $variation->get_id(), 'name' => $variation_name, 'price' => $this->format_price($variation->get_price(), $decimals), 'regular_price' => $this->format_price($variation->get_regular_price(), $decimals), 'sale_price' => $this->format_price($variation->get_sale_price(), $decimals), 'image' => !empty($variation_data['image']['url']) ? $variation_data['image']['url'] : '', 'sku' => $variation->get_sku(), 'stock_quantity' => $variation->get_stock_quantity(), 'in_stock' => $variation->is_in_stock(), 'attributes' => $variation_data['attributes'], ]; } return $variations; } /** * 获取商品评价 * * @param WP_REST_Request $request * @return WP_REST_Response */ public function get_product_reviews(WP_REST_Request $request) { $body = $request->get_json_params(); if (!is_array($body)) { return new WP_REST_Response( ['success' => false, 'message' => 'Invalid JSON body'], 200 ); } $ids = isset($body['ids']) && is_array($body['ids']) ? array_map('absint', $body['ids']) : []; $reviews_per_product = isset($body['reviewsPerProduct']) ? absint($body['reviewsPerProduct']) : 40; $reviews_per_product = max(1, min(100, $reviews_per_product)); $ids = array_values(array_unique(array_filter($ids))); if (empty($ids)) { return new WP_REST_Response( ['success' => false, 'message' => 'ids is required'], 200 ); } $result = []; $total_all = 0; // 为每个商品单独查询评价 foreach ($ids as $product_id) { // 查询该商品的评价总数 $count_args = [ 'status' => 'approve', 'type' => 'review', 'post_id' => $product_id, 'count' => true, ]; $product_total = get_comments($count_args); $total_all += $product_total; // 查询该商品的最新评价 $args = [ 'status' => 'approve', 'type' => 'review', 'post_id' => $product_id, 'number' => $reviews_per_product, 'orderby' => 'comment_date_gmt', 'order' => 'DESC', ]; $comments = get_comments($args); foreach ($comments as $comment) { $comment_rating = get_comment_meta($comment->comment_ID, 'rating', true); $verified = get_comment_meta($comment->comment_ID, 'verified', true); $title = get_comment_meta($comment->comment_ID, 'title', true); $result[] = [ 'id' => $comment->comment_ID, 'product_id' => $comment->comment_post_ID, 'rating' => (int) $comment_rating, 'title' => $title ?: '', 'content' => $comment->comment_content, 'author' => $comment->comment_author, 'author_email' => $comment->comment_author_email, 'date' => $comment->comment_date, 'date_gmt' => $comment->comment_date_gmt, 'verified' => (bool) $verified, ]; } } return new WP_REST_Response( [ 'success' => true, 'data' => [ 'productReviews' => $result, 'total' => $total_all, 'reviewsPerProduct' => $reviews_per_product, ], ], 200 ); } /** * 获取插件配置 * * @return WP_REST_Response */ public function get_agent_settings() { $settings = get_option('antom_shomi_settings', []); $response = [ 'icon' => !empty($settings['icon']) ? esc_url_raw($settings['icon']) : '', 'name' => !empty($settings['name']) ? sanitize_text_field($settings['name']) : '', 'contact' => !empty($settings['contact']) ? sanitize_text_field($settings['contact']) : '', 'question' => !empty($settings['question']) ? sanitize_textarea_field($settings['question']) : '', ]; return new WP_REST_Response( [ 'success' => true, 'data' => [ 'settings' => $response, ], ], 200 ); } }