132 lines
6.5 KiB
PHP
132 lines
6.5 KiB
PHP
<?php
|
||
/**
|
||
* Core: Registers the product type, constants, and utility methods.
|
||
*/
|
||
defined('ABSPATH') || exit;
|
||
|
||
class Yoone_Product_Bundles {
|
||
const TYPE = 'yoone_bundle';
|
||
|
||
// Post meta keys for configuration
|
||
const META_ALLOWED_PRODUCTS = '_yoone_bundle_allowed_products'; // array<int>
|
||
const META_MIN_QTY = '_yoone_bundle_min_quantity'; // int
|
||
// 是否允许在购物车中编辑混装(移除子项、调整数量等)
|
||
const META_EDIT_IN_CART = '_yoone_bundle_edit_in_cart'; // 'yes' | 'no'
|
||
// Bundle 折扣设置
|
||
const META_DISCOUNT_TYPE = '_yoone_bundle_discount_type'; // 'none' | 'percent' | 'fixed'
|
||
const META_DISCOUNT_AMOUNT = '_yoone_bundle_discount_amount'; // float
|
||
// 选择模式:include(仅包含列表)、exclude(排除列表,其余全部允许)、all(全部 simple 产品)
|
||
const META_SELECT_MODE = '_yoone_bundle_select_mode'; // 'include' | 'exclude' | 'all'
|
||
// 旧版:仅支持分类(product_cat)。
|
||
const META_CATEGORIES = '_yoone_bundle_categories'; // array<int> product_cat term_ids(兼容旧数据)
|
||
// 新版:支持按 taxonomy(product_cat 或 product_tag)进行分组显示
|
||
const META_GROUP_TAXONOMY = '_yoone_bundle_group_taxonomy'; // 'product_cat' | 'product_tag'
|
||
const META_GROUP_TERMS = '_yoone_bundle_group_terms'; // array<int> 选中的术语ID(来自所选taxonomy)
|
||
|
||
protected static $instance = null;
|
||
|
||
public static function instance() {
|
||
if (null === self::$instance) {
|
||
self::$instance = new self();
|
||
}
|
||
return self::$instance;
|
||
}
|
||
|
||
private function __construct() {
|
||
// Add "Mix and Match" to the "Product Type" dropdown in the admin.
|
||
add_filter('product_type_selector', array($this, 'register_product_type_in_selector'));
|
||
// Map the type to our class name, so WooCommerce instantiates the correct product object.
|
||
add_filter('woocommerce_product_class', array($this, 'map_product_class'), 10, 2);
|
||
}
|
||
|
||
/**
|
||
* Add "Mix and Match" to the "Product Type" dropdown in the admin.
|
||
*/
|
||
public function register_product_type_in_selector($types) {
|
||
$types[self::TYPE] = __('Product Bundle (Yoone Bundle)', 'yoone-product-bundles');
|
||
return $types;
|
||
}
|
||
|
||
/**
|
||
* Map the type to our class name, so WooCommerce instantiates the correct product object.
|
||
*/
|
||
public function map_product_class($classname, $product_type) {
|
||
if ($product_type === self::TYPE) {
|
||
$classname = 'WC_Product_Yoone_Bundle';
|
||
}
|
||
return $classname;
|
||
}
|
||
|
||
/**
|
||
* Read and normalize the bundle configuration.
|
||
* @param int|WC_Product $product
|
||
* @return array{allowed_products:int[],min_qty:int,categories:int[]}
|
||
*/
|
||
public static function get_bundle_config($product) {
|
||
$product = is_numeric($product) ? wc_get_product($product) : $product;
|
||
if (! $product) return array('allowed_products' => array(), 'min_qty' => 0, 'group_taxonomy' => 'product_cat', 'group_terms' => array());
|
||
$pid = $product->get_id();
|
||
$allowed = get_post_meta($pid, self::META_ALLOWED_PRODUCTS, true);
|
||
$min = absint(get_post_meta($pid, self::META_MIN_QTY, true));
|
||
$edit_in_cart = get_post_meta($pid, self::META_EDIT_IN_CART, true);
|
||
$edit_in_cart = ($edit_in_cart === 'yes') ? 'yes' : 'no';
|
||
$discount_type = get_post_meta($pid, self::META_DISCOUNT_TYPE, true);
|
||
$discount_type = in_array($discount_type, array('none','percent','fixed'), true) ? $discount_type : 'none';
|
||
$discount_amount = floatval(get_post_meta($pid, self::META_DISCOUNT_AMOUNT, true));
|
||
if ($discount_amount < 0) $discount_amount = 0.0;
|
||
$select_mode = get_post_meta($pid, self::META_SELECT_MODE, true);
|
||
$select_mode = in_array($select_mode, array('include','exclude','all'), true) ? $select_mode : 'include';
|
||
// 读取新的分组设置
|
||
$group_tax = get_post_meta($pid, self::META_GROUP_TAXONOMY, true);
|
||
$group_terms = get_post_meta($pid, self::META_GROUP_TERMS, true);
|
||
$group_tax = in_array($group_tax, array('product_cat','product_tag'), true) ? $group_tax : 'product_cat';
|
||
// Keep only simple products (to avoid issues if variations or other types were selected in the backend)
|
||
$allowed = is_array($allowed) ? array_values(array_map('absint', $allowed)) : array();
|
||
if (! empty($allowed)) {
|
||
$simple_only = array();
|
||
foreach ($allowed as $aid) {
|
||
$p = wc_get_product($aid);
|
||
if ($p && $p->is_type('simple')) {
|
||
$simple_only[] = $aid;
|
||
}
|
||
}
|
||
$allowed = $simple_only;
|
||
}
|
||
// 根据选择模式计算最终 allowed 列表
|
||
if ($select_mode === 'all' || $select_mode === 'exclude') {
|
||
// 获取所有 simple 产品ID(包含任何目录可见性)
|
||
$all_ids = wc_get_products(array(
|
||
'type' => array('simple'),
|
||
'status' => array('publish'),
|
||
'catalog_visibility' => 'any',
|
||
'limit' => -1,
|
||
'return' => 'ids',
|
||
));
|
||
$all_ids = is_array($all_ids) ? array_values(array_map('absint', $all_ids)) : array();
|
||
if ($select_mode === 'all') {
|
||
$allowed = $all_ids; // 全部 simple 产品
|
||
} else {
|
||
// exclude 模式:从全部 simple 产品中排除配置列表
|
||
$allowed = array_values(array_diff($all_ids, $allowed));
|
||
}
|
||
} // include 模式:保持 allowed 原样
|
||
// 兼容旧版:如果未配置新术语,但存在旧的分类设置,则沿用分类
|
||
if (empty($group_terms)) {
|
||
$cats = get_post_meta($pid, self::META_CATEGORIES, true);
|
||
$group_terms = is_array($cats) ? array_values(array_map('absint', $cats)) : array();
|
||
$group_tax = 'product_cat';
|
||
} else {
|
||
$group_terms = is_array($group_terms) ? array_values(array_map('absint', $group_terms)) : array();
|
||
}
|
||
return array(
|
||
'allowed_products' => $allowed,
|
||
'min_qty' => max(0, $min),
|
||
'edit_in_cart' => $edit_in_cart,
|
||
'discount_type' => $discount_type,
|
||
'discount_amount' => $discount_amount,
|
||
'select_mode' => $select_mode,
|
||
'group_taxonomy' => $group_tax,
|
||
'group_terms' => $group_terms,
|
||
);
|
||
}
|
||
} |