'GET', 'callback' => array(__CLASS__, 'get_cart'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ) ); register_rest_route( $namespace, '/cart/add', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'add_to_cart'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], 'args' => array( 'items' => array( 'required' => true, 'type' => 'array', ), ), ) ); register_rest_route( $namespace, '/cart/update', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'update_cart_item'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], 'args' => array( 'items' => array( 'required' => true, 'type' => 'array', ), ), ) ); register_rest_route( $namespace, '/cart/remove', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'remove_cart_item'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], 'args' => array( 'keys' => array( 'required' => true, 'type' => 'array', ), ), ) ); // ====================== // 地址相关路由 // ====================== register_rest_route( $namespace, '/address/fields', array( 'methods' => 'GET', 'callback' => array(__CLASS__, 'get_address_fields'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ) ); register_rest_route( $namespace, '/address/save', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'save_address'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ) ); register_rest_route( $namespace, '/address/countries', array( 'methods' => 'GET', 'callback' => array(__CLASS__, 'get_countries'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ) ); register_rest_route( $namespace, '/address/states', array( 'methods' => 'GET', 'callback' => array(__CLASS__, 'get_states'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], 'args' => array( 'country' => array( 'required' => true, 'type' => 'string', 'validate_callback' => function ($param) { $countries = WC()->countries->get_countries(); return isset($countries[$param]); }, 'validate_callback_message' => 'Invalid country code.', ), ), ) ); // ====================== // 配送相关路由 // ====================== register_rest_route( $namespace, '/shipping/calculate', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'calculate_shipping'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ) ); register_rest_route( $namespace, '/shipping/shipping-methods', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'get_shipping_methods'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ) ); // ====================== // 订单相关路由 // ====================== register_rest_route( $namespace, '/orders/get', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'get_orders'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ) ); // ====================== // 支付相关路由 // ====================== register_rest_route( $namespace, '/payment/methods', array( 'methods' => 'GET', 'callback' => array(__CLASS__, 'get_payment_methods'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ) ); register_rest_route( $namespace, '/payment/process', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'process_checkout'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ) ); // ====================== // 优惠券相关路由 // ====================== register_rest_route( $namespace, '/coupon/available', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'get_available_coupons'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], 'args' => array( 'ids' => array( 'required' => false, 'type' => 'array', ), ), ) ); register_rest_route( $namespace, '/coupon/apply', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'apply_coupon'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], 'args' => array( 'code' => array( 'required' => true, ), ), ) ); register_rest_route( $namespace, '/coupon/applied', array( 'methods' => 'GET', 'callback' => array(__CLASS__, 'get_applied_coupons'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], ) ); register_rest_route( $namespace, '/coupon/remove', array( 'methods' => 'POST', 'callback' => array(__CLASS__, 'remove_coupon'), 'permission_callback' => ['Antom_Shomi_Setup_Wizard', 'check_api_permission'], 'args' => array( 'code' => array( 'required' => true, ), ), ) ); } /** * 确保 WooCommerce session 和 cart 已正确初始化 * * @return WP_REST_Response|void 初始化失败时返回错误响应,成功时无返回 */ protected static function ensure_woocommerce_session_and_cart() { // 检查 WooCommerce 是否已初始化 if (!did_action('woocommerce_init')) { return new WP_REST_Response( array('success' => false, 'message' => 'WooCommerce is not ready'), 200 ); } // REST API 请求中恢复用户身份(Cookie 认证需要 nonce 验证) if (!is_user_logged_in()) { // 通过 WordPress 核心过滤器从 cookie 中验证并设置当前用户 $user_id = apply_filters('determine_current_user', 0); if ($user_id && $user_id > 0) { wp_set_current_user($user_id); } } // 初始化购物车和会话 wc_load_cart(); WC()->cart->get_cart(); // 确保 customer 对象存在 if (!WC()->customer) { WC()->customer = new WC_Customer(get_current_user_id(), true); } // 启动 WooCommerce session if (WC()->session && !WC()->session->has_session()) { WC()->session->set_customer_session_cookie(true); } // 登录用户确保 customer 对象关联正确的用户 ID if (is_user_logged_in() && WC()->customer) { $current_user_id = get_current_user_id(); if (WC()->customer->get_id() !== $current_user_id) { // 重新创建 customer 对象以关联正确的用户 WC()->customer = new WC_Customer($current_user_id, true); } } // 最终校验 if (!WC()->cart || !WC()->customer) { return new WP_REST_Response( array('success' => false, 'message' => 'Failed to load cart or customer.'), 200 ); } } // ====================== // 购物车管理 // ====================== /** * 获取当前购物车内容 * * 返回购物车中所有商品、总价、折扣、运费等信息。 * 包含商品名称、价格、数量、小计、是否虚拟商品、商品图片等详细信息。 * 同时计算商品折扣和优惠券折扣。 * * @return WP_REST_Response 购物车数据响应 */ public static function get_cart() { self::ensure_woocommerce_session_and_cart(); try { WC()->cart->calculate_totals(); $items = array(); $decimals = wc_get_price_decimals(); $product_discount_total = 0.0; // 遍历购物车商品 foreach (WC()->cart->get_cart() as $item_key => $item) { $product = $item['data']; if (!$product) { continue; } // 获取显示价格(已应用促销、税费等) $display_price = wc_get_price_to_display($product); $formatted_price = wc_format_decimal($display_price, $decimals); // 使用 line_total(含折扣)计算小计 $subtotal = isset($item['line_total']) ? $item['line_total'] : ($item['line_subtotal'] ?? 0); $formatted_subtotal = wc_format_decimal($subtotal, $decimals); // 计算商品折扣(原价 - 现价)* 数量 $regular_price = (float) $product->get_regular_price(); $sale_price = (float) $product->get_price(); if ($regular_price > $sale_price) { $product_discount_total += ($regular_price - $sale_price) * (int) $item['quantity']; } $items[] = array( 'key' => $item_key, 'product_id' => $product->get_id(), 'name' => $product->get_name(), 'price' => $formatted_price, 'quantity' => (int) $item['quantity'], 'subtotal' => $formatted_subtotal, 'is_virtual' => (bool) $product->is_virtual(), 'type' => $product->get_type(), 'image' => wp_get_attachment_image_url($product->get_image_id(), 'woocommerce_thumbnail') ?: wc_placeholder_img_src('woocommerce_thumbnail'), ); } $coupon_discount = WC()->cart->get_discount_total(); $shipping_total = WC()->cart->get_shipping_total(); return new WP_REST_Response( array( 'success' => true, 'data' => array( 'items' => $items, 'total_raw' => (float) WC()->cart->get_total('edit'), 'total' => wc_price(WC()->cart->get_total('edit')), 'needs_shipping' => (bool) WC()->cart->needs_shipping(), 'coupon_discount' => wc_format_decimal($coupon_discount, $decimals), 'shipping_total' => wc_format_decimal($shipping_total, $decimals), 'product_discount' => wc_format_decimal($product_discount_total, $decimals), 'currency' => get_woocommerce_currency(), 'currency_symbol' => html_entity_decode(get_woocommerce_currency_symbol(), ENT_QUOTES, 'UTF-8'), ) ), 200 ); } catch (Exception $e) { return new WP_REST_Response( array('success' => false, 'message' => 'Failed to retrieve cart.'), 200 ); } } /** * 添加商品到购物车 * * @param WP_REST_Request $request 请求参数,包含 items 数 * @return WP_REST_Response 添加结果响应,成功时返回更新后的购物车数据 */ public static function add_to_cart($request) { self::ensure_woocommerce_session_and_cart(); $items = $request->get_param('items'); $errors = array(); // 参数校验 if (!is_array($items) || empty($items)) { return new WP_REST_Response( array('success' => false, 'message' => "Invalid items: expected a non-empty array."), 200 ); } // 逐个添加商品 foreach ($items as $item) { $product_id = isset($item['product_id']) ? absint($item['product_id']) : 0; $quantity = isset($item['quantity']) ? absint($item['quantity']) : 1; $product = wc_get_product($product_id); if (!$product || !$product->is_purchasable()) { $errors[] = sprintf('Invalid product ID: %d', $product_id); continue; } $result = WC()->cart->add_to_cart($product_id, $quantity); if (!$result) { if ($product->is_type('variable')) { // 可变商品需要 variation_id + attributes $errors[] = sprintf('Failed to add product %d to cart, expected SKU ID but received SPU ID.', $product_id); } else { $errors[] = sprintf('Failed to add product %d to cart.', $product_id); } } } WC()->cart->calculate_totals(); WC()->cart->set_session(); if (!empty($errors)) { return new WP_REST_Response( array('success' => false, 'message' => implode('; ', $errors)), 200 ); } return self::get_cart(); } /** * 更新购物车商品数量 * * @param WP_REST_Request $request 请求参数,包含 items 数组 * @return WP_REST_Response 更新结果响应,成功时返回更新后的购物车数据 */ public static function update_cart_item($request) { self::ensure_woocommerce_session_and_cart(); $items = $request->get_param('items'); $errors = array(); // 参数校验 if (!is_array($items) || empty($items)) { return new WP_REST_Response( array('success' => false, 'message' => "Invalid items: expected a non-empty array."), 200 ); } // 逐个更新商品数量 foreach ($items as $item) { $key = isset($item['key']) ? sanitize_text_field($item['key']) : ''; $quantity = isset($item['quantity']) ? absint($item['quantity']) : 0; if ('' === $key || !isset(WC()->cart->cart_contents[$key])) { $errors[] = sprintf('Cart item not found: %s', $key); continue; } WC()->cart->set_quantity($key, $quantity, true); } WC()->cart->calculate_totals(); WC()->cart->set_session(); if (!empty($errors)) { return new WP_REST_Response( array('success' => false, 'message' => implode('; ', $errors)), 200 ); } return self::get_cart(); } /** * 批量从购物车中移除指定的商品。 * * @param WP_REST_Request $request 请求参数,包含 keys 数组 * @return WP_REST_Response 移除结果响应,成功时返回更新后的购物车数据 */ public static function remove_cart_item($request) { self::ensure_woocommerce_session_and_cart(); $keys = $request->get_param('keys'); $errors = array(); // 参数校验 if (!is_array($keys) || empty($keys)) { return new WP_REST_Response( array('success' => false, 'message' => "Invalid keys: expected a non-empty array."), 200 ); } // 逐个移除商品 foreach ($keys as $key) { $key = sanitize_text_field($key); if (!isset(WC()->cart->cart_contents[$key])) { $errors[] = sprintf('Cart item not found: %s', $key); continue; } WC()->cart->remove_cart_item($key); } WC()->cart->calculate_totals(); WC()->cart->set_session(); if (!empty($errors)) { return new WP_REST_Response( array('success' => false, 'message' => implode('; ', $errors)), 200 ); } return self::get_cart(); } // ====================== // 优惠券管理 // ====================== /** * 获取可用优惠券列表 * * @param WP_REST_Request $request 请求参数,可选包含 products 数组 * @return WP_REST_Response 优惠券列表响应 */ public static function get_available_coupons($request) { self::ensure_woocommerce_session_and_cart(); $product_items = $request->get_param('products'); $has_products = is_array($product_items) && !empty($product_items); try { // 构造商品对象列表和数量映射 $products = array(); $quantities = array(); if ($has_products) { foreach ($product_items as $item) { $pid = isset($item['id']) ? absint($item['id']) : 0; $qty = isset($item['quantity']) ? max(1, absint($item['quantity'])) : 1; if (!$pid) { continue; } $product = wc_get_product($pid); if ($product) { $products[$pid] = $product; $quantities[$pid] = $qty; } } $has_products = !empty($products); } // 查询所有已发布的优惠券 $coupon_posts = get_posts(array( 'posts_per_page' => 200, 'orderby' => 'date', 'order' => 'DESC', 'post_type' => 'shop_coupon', 'post_status' => 'publish', )); $now = current_time('timestamp'); $user_id = get_current_user_id(); $user_email = ''; if ($user_id) { $user = get_userdata($user_id); $user_email = $user ? strtolower(trim($user->user_email)) : ''; } // 计算传入商品的总价(用于最低/最高消费校验) $products_total = 0; if ($has_products) { foreach ($products as $pid => $product) { $qty = isset($quantities[$pid]) ? $quantities[$pid] : 1; $products_total += (float) wc_get_price_to_display($product) * $qty; } } $coupons = array(); // 遍历筛选有效优惠券 foreach ($coupon_posts as $post) { $coupon = new WC_Coupon($post->ID); // 过滤已过期 $expiry = $coupon->get_date_expires(); if ($expiry && $expiry->getTimestamp() < $now) { continue; } // 过滤已达总使用上限 $usage_limit = $coupon->get_usage_limit(); if ($usage_limit > 0 && $coupon->get_usage_count() >= $usage_limit) { continue; } // 过滤已达单用户使用上限 $per_user_limit = $coupon->get_usage_limit_per_user(); if ($per_user_limit > 0 && $user_id) { $used_by = $coupon->get_used_by(); $user_usage = 0; foreach ($used_by as $id) { if ((string) $id === (string) $user_id || strtolower((string) $id) === $user_email) { $user_usage++; } } if ($user_usage >= $per_user_limit) { continue; } } // 检查商品/分类限制 $has_product_restriction = !empty($coupon->get_product_ids()) || !empty($coupon->get_excluded_product_ids()) || !empty($coupon->get_product_categories()) || !empty($coupon->get_excluded_product_categories()); if ($has_product_restriction) { // 没传商品列表则跳过有商品限制的优惠券 if (!$has_products) { continue; } // 检查是否有商品适用该优惠券 $applicable = false; foreach ($products as $product) { if ($coupon->is_valid_for_product($product)) { $applicable = true; break; } } if (!$applicable) { continue; } } // 检查最低/最高消费金额限制,标记状态而非直接过滤 $check_total = $has_products ? $products_total : 0; $min_amount = (float) $coupon->get_minimum_amount(); $max_amount = (float) $coupon->get_maximum_amount(); $coupon_status = 'available'; if ($min_amount > 0 && $check_total < $min_amount) { $coupon_status = 'not_available'; } if ($max_amount > 0 && $check_total > $max_amount) { continue; } $coupon_data = self::format_coupon_data($coupon, false); $coupon_data['status'] = $coupon_status; $coupon_data['minimum_cost'] = $min_amount > 0 ? wc_format_decimal($min_amount, wc_get_price_decimals()) : null; // 计算预估优惠金额(available 时为实际预估,not_available 时为达到门槛后的潜在优惠) if ($has_products) { if ($coupon_status === 'available') { $coupon_data['estimated_discount'] = self::calculate_coupon_discount($coupon, $products, $quantities); } else { // 按门槛金额模拟计算潜在优惠 $coupon_data['potential_discount'] = self::calculate_potential_discount($coupon, $min_amount); } } $coupons[] = $coupon_data; } // 排序:available 优先,同 status 内按优惠金额降序 usort($coupons, function ($a, $b) { $status_order = array('available' => 0, 'not_available' => 1); $sa = $status_order[$a['status']] ?? 1; $sb = $status_order[$b['status']] ?? 1; if ($sa !== $sb) { return $sa <=> $sb; } $da = isset($a['estimated_discount']) ? (float) $a['estimated_discount'] : (isset($a['potential_discount']) ? (float) $a['potential_discount'] : 0); $db = isset($b['estimated_discount']) ? (float) $b['estimated_discount'] : (isset($b['potential_discount']) ? (float) $b['potential_discount'] : 0); return $db <=> $da; }); return new WP_REST_Response( array( 'success' => true, 'data' => $coupons, ), 200 ); } catch (Exception $e) { return new WP_REST_Response( array('success' => false, 'message' => 'Failed to retrieve coupons: ' . $e->getMessage()), 200 ); } } /** * 应用优惠券到购物车 * * code 参数支持字符串(单个)或数组(批量)。 * * @param WP_REST_Request $request 请求参数,包含 code 优惠券代码(string|array) * @return WP_REST_Response 应用结果响应,成功时返回优惠券信息和折扣金额 */ public static function apply_coupon($request) { self::ensure_woocommerce_session_and_cart(); $code_param = $request->get_param('code'); $codes = is_array($code_param) ? $code_param : array($code_param); $codes = array_map('sanitize_text_field', $codes); $codes = array_filter($codes); // 参数校验 if (empty($codes)) { return new WP_REST_Response( array('success' => false, 'message' => 'Coupon code is required.'), 200 ); } $applied_coupons = array(); $errors = array(); foreach ($codes as $coupon_code) { // 检查优惠券是否存在 $coupon = new WC_Coupon($coupon_code); if (!$coupon->get_id()) { $errors[] = sprintf('Invalid coupon code: %s', $coupon_code); continue; } // 检查是否已应用 if (WC()->cart->has_discount($coupon_code)) { $errors[] = sprintf('Coupon has already been applied: %s', $coupon_code); continue; } // 应用优惠券(WC 内部完成验证:过期、使用次数、最低消费等) $applied = WC()->cart->apply_coupon($coupon_code); if (!$applied) { $error_notices = wc_get_notices('error'); wc_clear_notices(); $message = sprintf('Failed to apply coupon: %s', $coupon_code); if (!empty($error_notices)) { $messages = array(); foreach ($error_notices as $notice) { $messages[] = isset($notice['notice']) ? wp_strip_all_tags($notice['notice']) : wp_strip_all_tags($notice); } $message = implode(' ', $messages); } $errors[] = $message; continue; } $applied_coupons[] = self::format_coupon_data($coupon, true); } WC()->cart->calculate_totals(); WC()->cart->set_session(); $discount_total = WC()->cart->get_discount_total(); $discount_tax = WC()->cart->get_discount_tax(); if (!empty($errors) && empty($applied_coupons)) { return new WP_REST_Response( array('success' => false, 'message' => implode('; ', $errors)), 200 ); } return new WP_REST_Response( array( 'success' => true, 'data' => array( 'applied_coupons' => $applied_coupons, 'errors' => $errors, 'discount_total' => wc_format_decimal($discount_total, wc_get_price_decimals()), 'discount_tax' => wc_format_decimal($discount_tax, wc_get_price_decimals()), 'discount_total_formatted' => wc_price($discount_total + $discount_tax), 'cart_total' => wc_price(WC()->cart->get_total('edit')), ) ), 200 ); } /** * 获取购物车中已应用的优惠券列表 * * @return WP_REST_Response 已应用优惠券列表响应 */ public static function get_applied_coupons() { self::ensure_woocommerce_session_and_cart(); try { WC()->cart->calculate_totals(); $applied_codes = WC()->cart->get_applied_coupons(); $decimals = wc_get_price_decimals(); $coupons = array(); foreach ($applied_codes as $code) { $coupon = new WC_Coupon($code); if (!$coupon->get_id()) { continue; } $coupon_data = self::format_coupon_data($coupon, true); $coupon_data['discount_amount'] = wc_format_decimal( WC()->cart->get_coupon_discount_amount($code), $decimals ); $coupons[] = $coupon_data; } $discount_total = WC()->cart->get_discount_total(); $discount_tax = WC()->cart->get_discount_tax(); return new WP_REST_Response( array( 'success' => true, 'data' => array( 'coupons' => $coupons, 'discount_total' => wc_format_decimal($discount_total, $decimals), 'discount_tax' => wc_format_decimal($discount_tax, $decimals), 'discount_total_formatted' => wc_price($discount_total + $discount_tax), 'cart_total' => wc_price(WC()->cart->get_total('edit')), ), ), 200 ); } catch (Exception $e) { return new WP_REST_Response( array('success' => false, 'message' => 'Failed to retrieve applied coupons.'), 200 ); } } /** * 移除购物车中已应用的优惠券 * * code 参数支持字符串(单个)或数组(批量)。 * * @param WP_REST_Request $request 请求参数,包含 code 优惠券代码(string|array) * @return WP_REST_Response 移除结果响应,成功时返回更新后的折扣金额 */ public static function remove_coupon($request) { self::ensure_woocommerce_session_and_cart(); $code_param = $request->get_param('code'); $codes = is_array($code_param) ? $code_param : array($code_param); $codes = array_map('sanitize_text_field', $codes); $codes = array_filter($codes); // 参数校验 if (empty($codes)) { return new WP_REST_Response( array('success' => false, 'message' => 'Coupon code is required.'), 200 ); } $removed_coupons = array(); $errors = array(); foreach ($codes as $coupon_code) { if (!WC()->cart->has_discount($coupon_code)) { $errors[] = sprintf('Coupon is not applied to the cart: %s', $coupon_code); continue; } $removed = WC()->cart->remove_coupon($coupon_code); if (!$removed) { $errors[] = sprintf('Failed to remove coupon: %s', $coupon_code); continue; } $removed_coupons[] = $coupon_code; } WC()->cart->calculate_totals(); WC()->cart->set_session(); $discount_total = WC()->cart->get_discount_total(); $discount_tax = WC()->cart->get_discount_tax(); if (!empty($errors) && empty($removed_coupons)) { return new WP_REST_Response( array('success' => false, 'message' => implode('; ', $errors)), 200 ); } return new WP_REST_Response( array( 'success' => true, 'data' => array( 'removed_coupons' => $removed_coupons, 'errors' => $errors, 'discount_total' => wc_format_decimal($discount_total, wc_get_price_decimals()), 'discount_tax' => wc_format_decimal($discount_tax, wc_get_price_decimals()), 'discount_total_formatted' => wc_price($discount_total + $discount_tax), 'cart_total' => wc_price(WC()->cart->get_total('edit')), ), ), 200 ); } /** * 根据优惠券类型和商品列表计算预估优惠金额 * * @param WC_Coupon $coupon 优惠券对象 * @param array $products 商品列表(pid => WC_Product) * @param array $quantities 数量映射(pid => int) * @return float 预估优惠金额 */ private static function calculate_coupon_discount($coupon, $products, $quantities) { $discount_type = $coupon->get_discount_type(); $amount = (float) $coupon->get_amount(); $discount = 0.0; // 获取优惠券限制的商品 ID(空数组表示不限制) $coupon_product_ids = $coupon->get_product_ids(); $has_product_restriction = !empty($coupon_product_ids); switch ($discount_type) { case 'percent': // 百分比折扣:对每个适用商品按比例计算 foreach ($products as $pid => $product) { if ($has_product_restriction && !in_array($pid, $coupon_product_ids)) { continue; } if (!$coupon->is_valid_for_product($product)) { continue; } $qty = isset($quantities[$pid]) ? $quantities[$pid] : 1; $price = (float) wc_get_price_to_display($product); $discount += $price * $qty * ($amount / 100); } // 百分比优惠券可能有最大折扣上限 $max_discount = $coupon->get_maximum_amount(); if ($max_discount > 0 && $discount > $max_discount) { $discount = (float) $max_discount; } break; case 'fixed_product': // 每件商品固定减免 $limit = (int) $coupon->get_limit_usage_to_x_items(); // 0 表示不限制 $applied_items = 0; foreach ($products as $pid => $product) { if ($limit > 0 && $applied_items >= $limit) { break; } if ($has_product_restriction && !in_array($pid, $coupon_product_ids)) { continue; } if (!$coupon->is_valid_for_product($product)) { continue; } $qty = isset($quantities[$pid]) ? $quantities[$pid] : 1; if ($limit > 0) { $qty = min($qty, $limit - $applied_items); } $price = (float) wc_get_price_to_display($product); // 单件优惠不能超过商品价格 $per_item_discount = min($amount, $price); $discount += $per_item_discount * $qty; $applied_items += $qty; } break; case 'fixed_cart': // 整个购物车固定减免 $total = 0; foreach ($products as $pid => $product) { $qty = isset($quantities[$pid]) ? $quantities[$pid] : 1; $total += (float) wc_get_price_to_display($product) * $qty; } // 折扣不能超过商品总价 $discount = min($amount, $total); break; default: // 其他类型不计算 break; } return round($discount, wc_get_price_decimals()); } /** * 计算优惠券达到门槛后的潜在优惠金额 * * @param WC_Coupon $coupon 优惠券对象 * @param float $minimum_amount 最低消费门槛 * @return float 潜在优惠金额 */ private static function calculate_potential_discount($coupon, $minimum_amount) { $discount_type = $coupon->get_discount_type(); $amount = (float) $coupon->get_amount(); $discount = 0.0; switch ($discount_type) { case 'percent': $discount = $minimum_amount * ($amount / 100); $max_discount = (float) $coupon->get_maximum_amount(); if ($max_discount > 0 && $discount > $max_discount) { $discount = $max_discount; } break; case 'fixed_cart': $discount = min($amount, $minimum_amount); break; case 'fixed_product': $discount = $amount; break; default: break; } return round($discount, wc_get_price_decimals()); } /** * 格式化优惠券数据 * * @param WC_Coupon $coupon 优惠券对象 * @param bool $is_applied 是否已应用到购物车 * @return array 格式化后的优惠券数据,包含 code、id、amount、discount_type、is_applied、description */ private static function format_coupon_data($coupon, $is_applied = false) { $discount_type = $coupon->get_discount_type(); $amount = $coupon->get_amount(); return array( 'code' => $coupon->get_code(), 'id' => $coupon->get_id(), 'amount' => wc_format_decimal($amount, wc_get_price_decimals()), 'discount_type' => $discount_type, 'is_applied' => $is_applied, 'description' => $coupon->get_description(), ); } /** * 获取地址字段定义及当前用户地址值 * * @return WP_REST_Response 地址字段响应 */ public static function get_address_fields() { self::ensure_woocommerce_session_and_cart(); if (!WC()->customer) { WC()->customer = new WC_Customer(get_current_user_id(), true); } try { $country = ''; $raw_billing_fields = WC()->countries->get_address_fields($country, 'billing_'); $raw_shipping_fields = WC()->countries->get_address_fields($country, 'shipping_'); // 处理账单地址字段 $billing_fields = array(); $billing_values = array(); foreach ($raw_billing_fields as $key => $field) { $fixed_key = preg_replace('/^billing([a-z])/', 'billing_$1', $key); $fixed_key = preg_replace('/^shipping([a-z])/', 'shipping_$1', $fixed_key); $safe_field = array( 'type' => $field['type'] ?? 'text', 'label' => $field['label'] ?? '', 'placeholder' => $field['placeholder'] ?? '', 'required' => !empty($field['required']), 'class' => $field['class'] ?? array(), 'options' => $field['options'] ?? array(), 'default' => $field['default'] ?? '', ); $billing_fields[$fixed_key] = $safe_field; $billing_values[$fixed_key] = WC()->customer ? WC()->customer->{"get_{$fixed_key}"}() : ''; } // 处理配送地址字段 $shipping_fields = array(); $shipping_values = array(); foreach ($raw_shipping_fields as $key => $field) { $fixed_key = preg_replace('/^billing([a-z])/', 'billing_$1', $key); $fixed_key = preg_replace('/^shipping([a-z])/', 'shipping_$1', $fixed_key); $safe_field = array( 'type' => $field['type'] ?? 'text', 'label' => $field['label'] ?? '', 'placeholder' => $field['placeholder'] ?? '', 'required' => !empty($field['required']), 'class' => $field['class'] ?? array(), 'options' => $field['options'] ?? array(), 'default' => $field['default'] ?? '', ); $shipping_fields[$fixed_key] = $safe_field; $shipping_values[$fixed_key] = WC()->customer ? WC()->customer->{"get_{$fixed_key}"}() : ''; } return new WP_REST_Response( array( 'success' => true, 'data' => array( 'billing' => array( 'fields' => $billing_fields, 'values' => $billing_values, ), 'shipping' => array( 'fields' => $shipping_fields, 'values' => $shipping_values, ), ) ), 200 ); } catch (Exception $e) { return new WP_REST_Response( array('success' => false, 'message' => 'Could not load address fields.'), 200 ); } } /** * 保存用户地址信息 * * @param WP_REST_Request $request 请求参数,包含 billing 和 shipping 地址数据 * @return WP_REST_Response 保存结果响应 */ public static function save_address($request) { self::ensure_woocommerce_session_and_cart(); $data = $request->get_params(); $billing_data = isset($data['billing']) && is_array($data['billing']) ? $data['billing'] : array(); $shipping_data = isset($data['shipping']) && is_array($data['shipping']) ? $data['shipping'] : array(); // 保存账单地址 foreach ($billing_data as $key => $value) { if (!is_callable(array(WC()->customer, "set_billing_{$key}"))) { continue; } WC()->customer->{"set_billing_{$key}"}(sanitize_text_field($value)); } // 保存配送地址(仅当需要配送时) if (WC()->cart->needs_shipping() && !empty($shipping_data)) { foreach ($shipping_data as $key => $value) { if (!is_callable(array(WC()->customer, "set_shipping_{$key}"))) { continue; } WC()->customer->{"set_shipping_{$key}"}(sanitize_text_field($value)); } } WC()->customer->save(); return new WP_REST_Response( array('success' => true), 200 ); } /** * 获取国家列表 * * @return WP_REST_Response 国家列表响应 */ public static function get_countries() { try { $countries = WC()->countries->get_countries(); $formatted_countries = array(); foreach ($countries as $code => $name) { $formatted_countries[] = array( 'code' => $code, 'name' => $name, ); } return new WP_REST_Response( array('success' => true, 'data' => $formatted_countries), 200 ); } catch (Exception $e) { return new WP_REST_Response( array('success' => false, 'message' => 'Could not fetch countries.'), 200 ); } } /** * 获取指定国家的省/州列表 * * @param WP_REST_Request $request 请求参数,包含 country 国家代码(必填) * @return WP_REST_Response 省/州列表响应 */ public static function get_states($request) { $country = $request->get_param('country'); $country = is_string($country) ? sanitize_text_field($country) : ''; // 参数校验 if ('' === $country) { return new WP_REST_Response( array('success' => false, 'message' => 'Country code is required.'), 200 ); } try { $states = WC()->countries->get_states($country); if (!$states || !is_array($states)) { return new WP_REST_Response( array('success' => true, 'data' => array()), 200 ); } $formatted_states = array(); foreach ($states as $code => $name) { $formatted_states[] = array( 'code' => $code, 'name' => $name, ); } return new WP_REST_Response( array('success' => true, 'data' => $formatted_states), 200 ); } catch (Exception $e) { return new WP_REST_Response( array('success' => false, 'message' => 'Could not fetch state'), 200 ); } } // ====================== // 配送管理 // ====================== /** * 计算当前购物车的配送方式和费用 * * @return WP_REST_Response 配送方式列表响应 */ public static function calculate_shipping() { self::ensure_woocommerce_session_and_cart(); try { WC()->cart->calculate_shipping(); WC()->cart->calculate_totals(); $packages = WC()->shipping->get_packages(); $methods = array(); foreach ($packages as $package) { if (empty($package['rates'])) { continue; } foreach ($package['rates'] as $rate) { $methods[] = array( 'id' => $rate->id, 'label' => $rate->label, 'cost' => (float) $rate->cost, 'cost_formatted' => wc_price($rate->cost), ); } } return new WP_REST_Response( array('success' => true, 'data' => $methods), 200 ); } catch (Exception $e) { return new WP_REST_Response( array('success' => false, 'message' => 'Shipping calculation failed.'), 200 ); } } /** * 根据地址获取可用配送方式 * * @param WP_REST_Request $request 请求参数,包含 address 地址对象 * @return WP_REST_Response 配送方式列表响应 */ public static function get_shipping_methods(WP_REST_Request $request) { if (!function_exists('WC')) { return new WP_REST_Response( array('success' => false, 'message' => 'WooCommerce not active'), 200 ); } // 确保 session/cart/customer 已初始化 if (null === WC()->session) { WC()->initialize_session(); } if (null === WC()->customer) { WC()->initialize_customer(); } if (null === WC()->cart) { WC()->initialize_cart(); } $params = $request->get_json_params(); $address = isset($params['address']) && is_array($params['address']) ? $params['address'] : array(); // 解析地址参数 $country = isset($address['country']) ? wc_strtoupper(wc_clean($address['country'])) : ''; $state = isset($address['state']) ? wc_strtoupper(wc_clean($address['state'])) : ''; $postcode = isset($address['postcode']) ? wc_clean($address['postcode']) : ''; $city = isset($address['city']) ? wc_clean($address['city']) : ''; $address_1 = isset($address['address_1']) ? wc_clean($address['address_1']) : ''; $address_2 = isset($address['address_2']) ? wc_clean($address['address_2']) : ''; // 国家代码必填 if ('' === $country) { return new WP_REST_Response(array('message' => 'country is required'), 400); } // 全虚拟购物车不需要配送 if (!WC()->cart->needs_shipping()) { return new WP_REST_Response( array( 'success' => true, 'data' => array( 'has_shipping_methods' => false, 'needs_shipping' => false, 'message' => '', 'packages' => array(), ) ), 200 ); } // 写入配送地址到 customer WC()->customer->set_shipping_country($country); WC()->customer->set_shipping_state($state); WC()->customer->set_shipping_postcode($postcode); WC()->customer->set_shipping_city($city); WC()->customer->set_shipping_address($address_1); WC()->customer->set_shipping_address_2($address_2); // 同时写入账单地址 WC()->customer->set_billing_country($country); WC()->customer->set_billing_state($state); WC()->customer->set_billing_postcode($postcode); WC()->customer->set_billing_city($city); WC()->customer->set_billing_address($address_1); WC()->customer->set_billing_address_2($address_2); WC()->customer->save(); // 计算运费 WC()->cart->calculate_totals(); $packages = WC()->shipping()->get_packages(); // 如果 packages 为空,尝试重新计算 if (empty($packages)) { WC()->shipping()->calculate_shipping(WC()->cart->get_shipping_packages()); $packages = WC()->shipping()->get_packages(); } $result = array(); // 遍历包裹获取配送方式 foreach ($packages as $package_index => $package) { $rates = isset($package['rates']) ? $package['rates'] : array(); $methods = array(); foreach ($rates as $rate_id => $rate) { /** @var WC_Shipping_Rate $rate */ $methods[] = array( 'id' => $rate->get_id(), // 如 flat_rate:3 'method_id' => $rate->get_method_id(), // 如 flat_rate 'instance_id' => $rate->get_instance_id(), 'label' => $rate->get_label(), 'cost' => wc_format_decimal($rate->get_cost(), wc_get_price_decimals()), 'taxes' => array_values($rate->get_taxes()), 'total' => wc_format_decimal((float) $rate->get_cost() + array_sum($rate->get_taxes()), wc_get_price_decimals()), 'meta_data' => $rate->get_meta_data(), ); } $result[] = array( 'package_index' => $package_index, 'destination' => isset($package['destination']) ? $package['destination'] : array(), 'shipping_methods' => $methods, ); } // 检查是否有可用配送方式 $has_methods = false; if (!empty($result)) { foreach ($result as $pkg) { if (!empty($pkg['shipping_methods'])) { $has_methods = true; break; } } } $message = ''; // 无可用配送方式时构造提示信息 if (!$has_methods) { $destination = array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, 'address' => $address_1, 'address_2' => $address_2, ); $result = array( array( 'package_index' => 0, 'destination' => $destination, 'shipping_methods' => array(), ), ); $formatted_destination = WC()->countries->get_formatted_address(array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, 'address_1' => $address_1, 'address_2' => $address_2, )); $message = sprintf( 'No shipping options were found for %s.', '' . esc_html(wp_strip_all_tags($formatted_destination)) . '' ); $message .= ' Enter a different address.'; } return new WP_REST_Response( array( 'success' => true, 'data' => array( 'address_used' => array( 'country' => $country, 'state' => $state, 'postcode' => $postcode, 'city' => $city, ), 'has_shipping_methods' => $has_methods, 'message' => $message, 'packages' => $result, ) ), 200 ); } /** * 获取订单信息 * * 支持两种模式: * 1. 通过 order_id + order_key 查询指定订单(无需登录) * 2. 登录用户返回近 1 个月的订单列表(最多 50 条) * * @param WP_REST_Request $request 请求参数 * @return WP_REST_Response 订单数据响应 */ public static function get_orders(WP_REST_Request $request) { if (!function_exists('wc_get_order')) { return new WP_REST_Response( array('success' => false, 'message' => 'WooCommerce not active'), 200 ); } $order_id = absint($request->get_param('order_id')); $order_key = (string) $request->get_param('order_key'); // 模式 A:通过 order_id + order_key 查询指定订单 if ($order_id && $order_key !== '') { $order = wc_get_order($order_id); if (!$order) { return new WP_REST_Response( array('success' => false, 'message' => 'Order not found'), 200 ); } // 校验 order_key if (!hash_equals((string) $order->get_order_key(), $order_key)) { return new WP_REST_Response( array('success' => false, 'message' => 'Invalid order key'), 200 ); } return new WP_REST_Response( array('success' => true, 'data' => self::format_order_detail($order)), 200 ); } // 模式 B:登录用户返回近 1 个月的订单列表 if (!is_user_logged_in()) { return new WP_REST_Response( array('success' => false, 'message' => "order_id is required or user must be logged in"), 200 ); } $user_id = get_current_user_id(); $after = (new DateTime('now', wp_timezone()))->modify('-1 month'); $query = new WC_Order_Query(array( 'customer_id' => $user_id, 'limit' => 50, 'orderby' => 'date', 'order' => 'DESC', 'date_created' => '>' . $after->format('Y-m-d H:i:s'), 'return' => 'ids', )); $ids = $query->get_orders(); $list = array(); foreach ($ids as $id) { $o = wc_get_order($id); if (!$o) { continue; } $list[] = self::format_order_detail($o); } return new WP_REST_Response( array('success' => true, 'data' => $list), 200 ); } /** * 格式化订单详情数据 * * @param WC_Order $order 订单对象 * @return array 格式化后的订单数据 */ private static function format_order_detail(WC_Order $order) { // 配送地址 $shipping = array( 'country' => (string) $order->get_shipping_country(), 'city' => (string) $order->get_shipping_city(), 'address_1' => (string) $order->get_shipping_address_1(), 'postcode' => (string) $order->get_shipping_postcode(), 'last_name' => (string) $order->get_shipping_last_name(), 'state' => (string) $order->get_shipping_state(), 'first_name' => (string) $order->get_shipping_first_name(), ); // 商品列表 $goods = array(); foreach ($order->get_items('line_item') as $item) { if (!$item instanceof WC_Order_Item_Product) { continue; } $product = $item->get_product(); if (!$product) { continue; } $product_id = $product->get_id(); $image_id = $product->get_image_id(); $image_url = $image_id ? wp_get_attachment_image_url($image_id, 'full') : ''; $qty = max(1, (int) $item->get_quantity()); $line_total = (float) $item->get_total(); // 不含税 $unit_price = $line_total / $qty; $goods[] = array( 'id' => $product_id, 'name' => (string) $item->get_name(), 'image' => (string) $image_url, 'price' => wc_format_decimal($unit_price, 2), 'quantity' => $qty, 'regular_price' => wc_format_decimal((float) $product->get_regular_price(), 2), 'sale_price' => wc_format_decimal((float) $product->get_sale_price(), 2), 'short_description' => (string) wp_strip_all_tags($product->get_short_description()), 'in_stock' => (bool) $product->is_in_stock(), 'permalink' => (string) get_permalink($product_id), ); } $discount_total = (float) $order->get_discount_total() + (float) $order->get_discount_tax(); $shipping_total = (float) $order->get_shipping_total(); $created = $order->get_date_created(); $updated = $order->get_date_modified(); return array( 'total' => wc_format_decimal((float) $order->get_total(), 2), 'shippingAddress' => $shipping, 'orderId' => $order->get_id(), 'orderKey' => (string) $order->get_order_key(), 'buyerEmail' => (string) $order->get_billing_email(), 'gmtUpdate' => $updated ? $updated->date('Y/m/d H:i') : null, 'goods' => $goods, 'discount' => wc_format_decimal($discount_total, 2), 'shipping_total' => wc_format_decimal($shipping_total, 2), 'currency' => (string) $order->get_currency(), 'gmtCreate' => $created ? $created->date('Y/m/d H:i') : null, 'status' => (string) $order->get_status(), ); } // ====================== // 支付管理 // ====================== /** * 获取可用支付方式列表 * * @return WP_REST_Response 支付方式列表响应 */ public static function get_payment_methods() { self::ensure_woocommerce_session_and_cart(); try { $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); $methods = array(); foreach ($available_gateways as $gateway) { // 获取支付表单 HTML ob_start(); $gateway->payment_fields(); $payment_html = ob_get_clean(); $payment_html = preg_replace('/\s+/', ' ', trim($payment_html)); $methods[] = array( 'id' => $gateway->id, 'title' => $gateway->title, 'description' => $gateway->description ?? '', 'tag' => $payment_html, ); } return new WP_REST_Response( array('success' => true, 'data' => $methods), 200 ); } catch (Exception $e) { return new WP_REST_Response( array('success' => false, 'message' => 'Could not load payment methods.'), 200 ); } } /** * 处理结账流程 * * @param WP_REST_Request $request 请求参数 * @return WP_REST_Response 结账结果响应,成功时返回订单信息和支付跳转 URL */ public static function process_checkout(WP_REST_Request $request) { self::ensure_woocommerce_session_and_cart(); // 1. 解析入参 $params = $request->get_json_params(); $address_data = isset($params['addressData']) && is_array($params['addressData']) ? $params['addressData'] : array(); $billing_data = isset($address_data['billing']) && is_array($address_data['billing']) ? $address_data['billing'] : array(); $shipping_data = isset($address_data['shipping']) && is_array($address_data['shipping']) ? $address_data['shipping'] : array(); // 2. 配送方式(虚拟商品无需配送,否则必填) $method = isset($address_data['shipping_method']) ? sanitize_text_field($address_data['shipping_method']) : ''; if ('' === $method && WC()->cart->needs_shipping()) { return new WP_REST_Response( array('success' => false, 'message' => 'Shipping method is required.'), 200 ); } if ('' !== $method) { WC()->session->set('chosen_shipping_methods', array($method)); } // 3. 支付网关(默认 antom) $gateway_id = sanitize_text_field((string) $request->get_param('payment_method_gateWay')); if ('' === $gateway_id) { $gateway_id = 'antom'; } // 4. 购物车校验 if (WC()->cart->is_empty()) { return new WP_REST_Response( array('success' => false, 'message' => 'Your cart is empty.'), 200 ); } // 5. 写入地址到 WC Customer,触发运费重算 if (WC()->customer) { foreach ($billing_data as $key => $value) { $setter = "set_billing_{$key}"; if (is_callable(array(WC()->customer, $setter))) { WC()->customer->{$setter}(sanitize_text_field($value)); } } foreach ($shipping_data as $key => $value) { $setter = "set_shipping_{$key}"; if (is_callable(array(WC()->customer, $setter))) { WC()->customer->{$setter}(sanitize_text_field($value)); } } WC()->customer->save(); } // 确保 shipping packages 已生成 WC()->cart->calculate_shipping(); WC()->cart->calculate_totals(); // 6. 校验支付网关是否可用 WC()->payment_gateways(); $gateways = WC()->payment_gateways->payment_gateways(); if (!isset($gateways[$gateway_id])) { return new WP_REST_Response( array('success' => false, 'message' => 'Selected payment gateway is not active.'), 200 ); } // 7. 为 Stripe 写入 $_POST(Stripe 插件内部读取) if ('stripe' === $gateway_id) { $stripe_payment_method_id = sanitize_text_field((string) $request->get_param('payment_method_id')); $stripe_payment_method_type = sanitize_text_field((string) $request->get_param('payment_method_type')); if (empty($stripe_payment_method_id) && empty($stripe_payment_method_type)) { return new WP_REST_Response( array('success' => false, 'message' => 'Missing Stripe payment_method_id or payment_method_type.'), 200 ); } $_POST['payment_method'] = $stripe_payment_method_type; $_POST['wc-stripe-payment-method'] = $stripe_payment_method_id; $gateway_is_deferred_intent = $request->get_param('stripe_is_deferred_intent'); $_POST['wc-stripe-is-deferred-intent'] = $gateway_is_deferred_intent ? sanitize_text_field($gateway_is_deferred_intent) : '1'; } else { $_POST['payment_method'] = $gateway_id; } // 8. 创建订单 $checkout_data = array('payment_method' => $gateway_id); foreach ($billing_data as $key => $value) { $checkout_data["billing_{$key}"] = sanitize_text_field($value); } foreach ($shipping_data as $key => $value) { $checkout_data["shipping_{$key}"] = sanitize_text_field($value); } if (!empty($shipping_data)) { $checkout_data['ship_to_different_address'] = 1; } try { $checkout = WC()->checkout(); $order_id = $checkout->create_order($checkout_data); if (is_wp_error($order_id)) { return new WP_REST_Response( array('success' => false, 'message' => $order_id->get_error_message()), 200 ); } if (!$order_id) { return new WP_REST_Response( array('success' => false, 'message' => 'Unable to create order.'), 200 ); } // 设置支付方式 $order = wc_get_order($order_id); $order->set_payment_method($gateway_id); $order->save(); } catch (Exception $e) { return new WP_REST_Response( array('success' => false, 'message' => $e->getMessage()), 200 ); } // 9. 调用网关 process_payment() $gateway = $gateways[$gateway_id]; $result = $gateway->process_payment($order_id); // 10. 处理支付结果 if (is_array($result) && isset($result['result']) && 'success' === $result['result']) { $order = wc_get_order($order_id); WC()->cart->empty_cart(); return new WP_REST_Response( array( 'success' => true, 'data' => array( 'success' => true, 'redirect' => isset($result['redirect']) ? $result['redirect'] : '', 'order_id' => $order->get_id(), 'order_key' => $order->get_order_key(), 'is_paid' => $order->is_paid(), 'payment_method' => $order->get_payment_method(), 'payment_method_title' => $order->get_payment_method_title(), ) ), 200 ); } // 支付失败:标记订单为 failed $failed_order = wc_get_order($order_id); if ($failed_order) { $failed_order->update_status('failed', 'Payment failed via API.'); } $msg = isset($result['messages']) ? wp_strip_all_tags($result['messages']) : 'Payment failed.'; return new WP_REST_Response( array('success' => false, 'message' => $msg), 200 ); } }