yoone-wc-product-bundles/includes/frontend/class-yoone-product-bundles...

462 lines
22 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* 前端:为混装产品渲染自定义的“添加到购物车”表单,并处理验证和加入购物车的逻辑。
*/
defined('ABSPATH') || exit;
class Yoone_Product_Bundles_Frontend {
private static $_instance = null;
public static function instance() {
if (is_null(self::$_instance)) {
self::$_instance = new self();
}
return self::$_instance;
}
private function __construct() {
$this->setup_hooks();
}
/**
* 设置所有与前端相关的钩子。
*/
public function setup_hooks() {
// 处理混装产品的自定义“添加到购物车”流程
add_filter('woocommerce_add_to_cart_validation', array($this, 'process_bundle_add_to_cart'), 10, 3);
// 当混装容器被移除时,移除其子项目
add_action('woocommerce_cart_item_removed', array($this, 'remove_bundle_children'), 10, 2);
// 隐藏购物车中子项目的“移除”链接
add_filter('woocommerce_cart_item_remove_link', array($this, 'hide_child_remove_link'), 10, 2);
// 在购物车中,显示子项目所属的混装产品
add_filter('woocommerce_get_item_data', array($this, 'display_child_bundle_link'), 10, 2);
// 为购物车行添加分组样式类:容器与子项
add_filter('woocommerce_cart_item_class', array($this, 'add_cart_item_group_classes'), 10, 3);
// 在购物车中为混装容器显示聚合价格(不再显示 0 元),并展示折扣节省信息
add_filter('woocommerce_cart_item_price', array($this, 'render_container_cart_price'), 20, 3);
add_filter('woocommerce_cart_item_subtotal', array($this, 'render_container_cart_subtotal'), 20, 3);
// 为混装产品页面添加 body class以便于样式化
add_filter('body_class', array($this, 'filter_body_class'));
// 统一覆盖 Woo 的价格 HTML当订阅组件或其它部件尝试通过 get_price_html() 渲染价格时,避免显示 $0.00
add_filter('woocommerce_get_price_html', array($this, 'filter_yoone_bundle_price_html'), 10, 2);
// 根据当前购物车中的混装子项动态添加“Bundle Discount”费用负费用
add_action('woocommerce_cart_calculate_fees', array($this, 'apply_bundle_discounts'), 20, 1);
// 在单品页尽早移除 APFSAll Products for Subscriptions对按钮文案的有问题的过滤器避免致命错误。
// 该插件在某些 Woo 版本上将 'woocommerce_product_single_add_to_cart_text' 过滤器声明为接收2个参数
// 但实际只传递1个参数导致 ArgumentCountError。
add_action('woocommerce_before_single_product', array($this, 'remove_conflicting_apfs_add_to_cart_text_filter'), 1);
// 隐藏默认价格,改为说明语;用我们的自定义表单替换默认的“添加到购物车”按钮
add_action('woocommerce_single_product_summary', array($this, 'remove_default_add_to_cart_for_bundle'), 29);
add_action('woocommerce_single_product_summary', array($this, 'replace_single_price_for_bundle'), 8);
add_action('woocommerce_single_product_summary', array($this, 'render_bundle_add_to_cart_form'), 30);
}
/**
* 劫持混装产品的“添加到购物车”流程。
* 验证提交,然后将容器和所有子产品添加到购物车。
*
* @return bool False 以阻止默认的“添加到购物车”操作。
*/
public function process_bundle_add_to_cart($passed, $product_id, $quantity) {
$product = wc_get_product($product_id);
if (!$product || $product->get_type() !== Yoone_Product_Bundles::TYPE) {
return $passed; // 不是我们的产品,直接通过。
}
// 1. 验证
$config = Yoone_Product_Bundles::get_bundle_config($product);
$min_qty = max(1, absint($config['min_qty']));
$config_edit_in_cart = isset($config['edit_in_cart']) && $config['edit_in_cart'] === 'yes';
// 前端表单可显式传递是否允许在购物车编辑;否则以产品配置为准
$posted_edit_in_cart = isset($_POST['yoone_bundle_edit_in_cart']) ? (bool) $_POST['yoone_bundle_edit_in_cart'] : null;
$edit_in_cart = is_null($posted_edit_in_cart) ? $config_edit_in_cart : (bool) $posted_edit_in_cart;
$components = !empty($_POST['yoone_bundle_components']) ? (array) $_POST['yoone_bundle_components'] : array();
$total_qty = 0;
$clean_components = array();
foreach ($components as $comp_id => $qty) {
$qty = absint($qty);
if ($qty > 0) {
$total_qty += $qty;
$clean_components[absint($comp_id)] = $qty;
}
}
if ($total_qty < $min_qty) {
wc_add_notice(sprintf(__('You need to select at least %d items to add this bundle to your cart.', 'yoone-product-bundles'), $min_qty), 'error');
return false;
}
// 2. 添加商品到购物车
try {
$bundle_container_id = uniqid('bundle_');
// 如果 APFS 可用,从请求中读取选中的订阅方案,并在容器及子项目上附加 wcsatt_data。
// 注意:部分环境可能存在类名,但方法签名不同;为防止致命错误,需检测方法是否存在。
$wcsatt_data = array();
if (class_exists('WCS_ATT_Product_Schemes')) {
$posted_scheme_key = null;
if (method_exists('WCS_ATT_Product_Schemes', 'get_posted_subscription_scheme')) {
// APFS 正常方法:根据当前产品读取提交的订阅方案键。
$posted_scheme_key = WCS_ATT_Product_Schemes::get_posted_subscription_scheme($product_id);
} else {
// 兼容性回退:尝试从请求参数中获取。
// APFS 默认使用 name=wcsatt_subscription_scheme 的字段提交所选方案;此处作为保护性回退。
$posted_scheme_key = isset($_REQUEST['wcsatt_subscription_scheme']) ? wc_clean(wp_unslash($_REQUEST['wcsatt_subscription_scheme'])) : null;
// 若无法识别,则保持为 null/false表示一次性购买。
}
// 允许显式地传递 false表示一次性购买。APFS 会在后续根据该键处理。
$wcsatt_data = array('wcsatt_data' => array('active_subscription_scheme' => $posted_scheme_key));
}
// 添加主混装产品作为价格为0的容器
WC()->cart->add_to_cart($product_id, 1, 0, array(), array_merge($wcsatt_data, array(
'yoone_bundle_container_id' => $bundle_container_id,
'yoone_bundle_edit_in_cart' => $edit_in_cart ? 'yes' : 'no',
)));
// 添加子组件
foreach ($clean_components as $comp_id => $qty) {
WC()->cart->add_to_cart($comp_id, $qty, 0, array(), array_merge($wcsatt_data, array(
'yoone_bundle_parent_id' => $bundle_container_id,
'yoone_bundle_parent_product_id' => $product_id,
'yoone_bundle_edit_in_cart' => $edit_in_cart ? 'yes' : 'no',
)));
}
// 设置成功消息并重定向
wc_add_to_cart_message(array($product_id => 1), true);
add_filter('woocommerce_add_to_cart_redirect', function() { return wc_get_cart_url(); });
} catch (Exception $e) {
wc_add_notice($e->getMessage(), 'error');
return false;
}
// 阻止主产品的默认“添加到购物车”行为
return false;
}
/**
* 当其它插件(如订阅插件)在单品页通过 $product->get_price_html() 生成价格片段时,
* 我们为 yoone_bundle 返回友好提示,避免显示 $0.00。
*/
public function filter_yoone_bundle_price_html($html, $product) {
try {
if ($product && is_a($product, 'WC_Product') && $product->get_type() === Yoone_Product_Bundles::TYPE) {
if (is_product()) {
return '<span class="yoone-bundle-option-price-hint">' . esc_html__('Price depends on selected items.', 'yoone-product-bundles') . '</span>';
}
}
} catch (\Throwable $e) {
// swallow errors
}
return $html;
}
/**
* 防护:在“单品页”早期移除 APFS 对按钮文案的过滤器,以避免其函数签名与当前 Woo 版本不匹配导致的 500 错误。
*/
public function remove_conflicting_apfs_add_to_cart_text_filter() {
try {
global $product;
if (!$product || !is_a($product, 'WC_Product')) {
return;
}
if ($product->get_type() !== Yoone_Product_Bundles::TYPE) {
return; // 仅在我们的混装产品上做防护,不影响其它产品。
}
if (class_exists('WCS_ATT_Display_Product')) {
// APFS 在 includes/display/class-wcs-att-display-product.php 中以优先级 10 添加了该过滤器。
remove_filter('woocommerce_product_single_add_to_cart_text', array('WCS_ATT_Display_Product', 'single_add_to_cart_text'), 10);
}
} catch (\Throwable $e) {
// 忽略错误,尽可能不中断页面。
}
}
/**
* 当一个购物车项目被移除时,检查它是否是一个混装容器。
* 如果是,则找到并移除其所有的子项目。
*/
public function remove_bundle_children($removed_cart_item_key, $cart) {
// 此钩子在项目已从购物车会话中移除后触发,所以我们在 `removed_cart_contents` 中查找它。
if (!isset($cart->removed_cart_contents[$removed_cart_item_key])) {
return;
}
$removed_item = $cart->removed_cart_contents[$removed_cart_item_key];
// 检查被移除的商品是否是我们的混装容器。
if (isset($removed_item['yoone_bundle_container_id'])) {
$bundle_container_id = $removed_item['yoone_bundle_container_id'];
$child_item_keys_to_remove = [];
// 遍历购物车,找到所有属于此容器的子项目。
// 我们不能在遍历时直接修改购物车所以我们先收集要移除的项目的key。
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
if (isset($cart_item['yoone_bundle_parent_id']) && $cart_item['yoone_bundle_parent_id'] === $bundle_container_id) {
$child_item_keys_to_remove[] = $cart_item_key;
}
}
// 现在遍历收集到的key并从购物车中移除子项目。
foreach ($child_item_keys_to_remove as $key_to_remove) {
$cart->remove_cart_item($key_to_remove);
}
}
}
/**
* 隐藏混装产品子项目的移除链接。
*/
public function hide_child_remove_link($link, $cart_item_key) {
$cart_item = WC()->cart->get_cart_item($cart_item_key);
if (isset($cart_item['yoone_bundle_parent_id'])) {
// 当允许在购物车编辑时,不隐藏移除链接
$editable = isset($cart_item['yoone_bundle_edit_in_cart']) && $cart_item['yoone_bundle_edit_in_cart'] === 'yes';
if (! $editable) {
return '';
}
}
return $link;
}
/**
* 对于子项目,在购物车中添加一个元信息行,以链接回父混装产品。
*/
public function display_child_bundle_link($item_data, $cart_item) {
if (isset($cart_item['yoone_bundle_parent_product_id'])) {
$parent_product = wc_get_product($cart_item['yoone_bundle_parent_product_id']);
if ($parent_product) {
$item_data[] = array(
'key' => __('Part of', 'yoone-product-bundles'),
'value' => $parent_product->get_name(),
);
}
}
return $item_data;
}
/**
* 为购物车行添加样式类,便于将子项目缩小并缩进显示。
*/
public function add_cart_item_group_classes($class, $cart_item, $cart_item_key) {
if (isset($cart_item['yoone_bundle_container_id'])) {
$class .= ' yoone-bundle-container';
} elseif (isset($cart_item['yoone_bundle_parent_id'])) {
$class .= ' yoone-bundle-child';
}
return $class;
}
/**
* 计算混装容器的聚合价格与折扣后价格。
*/
private function compute_container_totals($cart, $container_cart_item) {
$result = array('raw_subtotal' => 0.0, 'discounted_subtotal' => 0.0, 'discount' => 0.0);
if (!isset($container_cart_item['yoone_bundle_container_id'])) {
return $result;
}
$bundle_id = $container_cart_item['yoone_bundle_container_id'];
// 找出对应的父产品(用于读取折扣配置)
$parent_product_id = 0;
if (isset($container_cart_item['product_id'])) {
$parent_product_id = absint($container_cart_item['product_id']);
}
$parent_product = $parent_product_id ? wc_get_product($parent_product_id) : null;
$config = $parent_product ? Yoone_Product_Bundles::get_bundle_config($parent_product) : array('discount_type' => 'none', 'discount_amount' => 0);
$type = isset($config['discount_type']) ? $config['discount_type'] : 'none';
$amount = isset($config['discount_amount']) ? floatval($config['discount_amount']) : 0.0;
foreach ($cart->get_cart() as $key => $item) {
if (isset($item['yoone_bundle_parent_id']) && $item['yoone_bundle_parent_id'] === $bundle_id) {
$qty = isset($item['quantity']) ? absint($item['quantity']) : 1;
$price = 0.0;
if (isset($item['data']) && is_object($item['data']) && method_exists($item['data'], 'get_price')) {
$price = floatval($item['data']->get_price());
}
$result['raw_subtotal'] += ($price * $qty);
}
}
$discount = 0.0;
if ($type !== 'none' && $amount > 0 && $result['raw_subtotal'] > 0) {
if ($type === 'percent') {
$discount = $result['raw_subtotal'] * ($amount / 100.0);
} else if ($type === 'fixed') {
$discount = min($amount, $result['raw_subtotal']);
}
}
$result['discount'] = $discount;
$result['discounted_subtotal'] = max(0, $result['raw_subtotal'] - $discount);
return $result;
}
/**
* 在购物车“单价”栏为容器显示聚合价格与折扣节省。
*/
public function render_container_cart_price($price_html, $cart_item, $cart_item_key) {
if (!isset($cart_item['yoone_bundle_container_id'])) {
return $price_html;
}
$cart = WC()->cart;
$totals = $this->compute_container_totals($cart, $cart_item);
$currency = get_woocommerce_currency_symbol();
$format = function($n) use ($currency) { return wc_price($n); };
if ($totals['raw_subtotal'] <= 0) {
return $price_html;
}
$html = '';
if ($totals['discount'] > 0) {
// 显示原价与折后价,以及节省标签
$html .= '<span class="yoone-bundle-original" style="text-decoration:line-through;opacity:.7;">' . $format($totals['raw_subtotal']) . '</span> ';
$html .= '<span class="yoone-bundle-discounted" style="font-weight:600;">' . $format($totals['discounted_subtotal']) . '</span>';
$html .= '<div class="yoone-bundle-savings" style="margin-top:4px;">' . sprintf(esc_html__('SAVE %s', 'yoone-product-bundles'), $format($totals['discount'])) . '</div>';
} else {
// 无折扣时仅显示聚合总价
$html .= '<span class="yoone-bundle-aggregated" style="font-weight:600;">' . $format($totals['raw_subtotal']) . '</span>';
}
return $html;
}
/**
* 在购物车“小计”栏为容器显示“应付”金额(折后总价)。
*/
public function render_container_cart_subtotal($subtotal_html, $cart_item, $cart_item_key) {
if (!isset($cart_item['yoone_bundle_container_id'])) {
return $subtotal_html;
}
$cart = WC()->cart;
$totals = $this->compute_container_totals($cart, $cart_item);
if ($totals['raw_subtotal'] <= 0) {
return $subtotal_html;
}
$format = function($n) { return wc_price($n); };
$html = '<span class="yoone-bundle-due-today" style="font-weight:600;">' . $format($totals['discounted_subtotal']) . '</span>';
return $html;
}
/**
* 仅为混装产品移除默认的“添加到购物车”按钮。
*/
public function remove_default_add_to_cart_for_bundle() {
global $product;
if ($product && $product->get_type() === Yoone_Product_Bundles::TYPE) {
remove_action('woocommerce_single_product_summary', 'woocommerce_template_single_add_to_cart', 30);
}
}
/**
* 在单品页隐藏默认价格输出,并显示“价格由所选子商品决定”的说明。
*/
public function replace_single_price_for_bundle() {
global $product;
if ($product && $product->get_type() === Yoone_Product_Bundles::TYPE) {
// 移除默认价格输出
remove_action('woocommerce_single_product_summary', 'woocommerce_template_single_price', 10);
// 输出说明文本(可根据需要美化)
echo '<p class="price yoone-bundle-price-hint">' . esc_html__('Price depends on selected items in the bundle.', 'yoone-product-bundles') . '</p>';
}
}
/**
* 为混装产品渲染自定义的“添加到购物车”表单。
*/
public function render_bundle_add_to_cart_form() {
global $product;
if ($product && $product->get_type() === Yoone_Product_Bundles::TYPE) {
wc_get_template('global/yoone-bundle-form.php', array(), '', YOONE_PB_PATH . 'templates/');
}
}
/**
* 为混装产品页面添加一个 body class。
*/
public function filter_body_class($classes) {
global $post;
if (is_singular('product') && $post) {
$product = wc_get_product($post->ID);
if ($product && $product->get_type() === Yoone_Product_Bundles::TYPE) {
$classes[] = 'yoone-bundle-product';
}
}
return $classes;
}
/**
* 遍历购物车,按容器分组子项目,计算各容器对应的折扣,并以负费用的形式添加到购物车。
* 折扣类型支持percent百分比或 fixed固定金额
*/
public function apply_bundle_discounts($cart) {
if (is_admin() && !defined('DOING_AJAX')) {
return;
}
if (empty($cart)) return;
// 收集所有容器及其子项的金额
$bundles = array(); // bundle_container_id => [ 'parent_product_id' => int, 'subtotal' => float ]
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
if (isset($cart_item['yoone_bundle_parent_id'])) {
$bundle_id = $cart_item['yoone_bundle_parent_id'];
$parent_pid = isset($cart_item['yoone_bundle_parent_product_id']) ? absint($cart_item['yoone_bundle_parent_product_id']) : 0;
if (!isset($bundles[$bundle_id])) {
$bundles[$bundle_id] = array('parent_product_id' => $parent_pid, 'subtotal' => 0.0);
}
// 使用当前商品价格 * 数量 计算小计(不考虑税)
$price = 0.0;
if (isset($cart_item['data']) && is_object($cart_item['data']) && method_exists($cart_item['data'], 'get_price')) {
$price = floatval($cart_item['data']->get_price());
}
$qty = isset($cart_item['quantity']) ? absint($cart_item['quantity']) : 1;
$bundles[$bundle_id]['subtotal'] += ($price * $qty);
}
}
// 为每个 bundle 容器应用折扣
foreach ($bundles as $bundle_id => $info) {
$parent_product = $info['parent_product_id'] ? wc_get_product($info['parent_product_id']) : null;
if (!$parent_product || $parent_product->get_type() !== Yoone_Product_Bundles::TYPE) {
continue;
}
$config = Yoone_Product_Bundles::get_bundle_config($parent_product);
$type = isset($config['discount_type']) ? $config['discount_type'] : 'none';
$amount = isset($config['discount_amount']) ? floatval($config['discount_amount']) : 0.0;
if ($type === 'none' || $amount <= 0) {
continue;
}
$subtotal = floatval($info['subtotal']);
if ($subtotal <= 0) continue;
$discount = 0.0;
if ($type === 'percent') {
$discount = $subtotal * ($amount / 100.0);
} else if ($type === 'fixed') {
// 固定金额:每个 bundle 容器减固定金额
$discount = min($amount, $subtotal);
}
if ($discount > 0) {
$title = sprintf(__('Bundle Discount: %s', 'yoone-product-bundles'), $parent_product->get_name());
// 添加负费用以抵扣;不含税
$cart->add_fee($title, -1 * $discount, false);
}
}
}
}