feat(订阅): 实现多订阅计划功能并支持混装产品
- 在后台添加多订阅计划管理界面,支持添加、删除和配置多个订阅计划 - 扩展订阅功能以支持混装产品类型,避免子项目重复计价 - 新增订阅计划数据存储和规范化处理逻辑 - 前端订阅逻辑适配混装容器价格计算
This commit is contained in:
parent
94d76c9c10
commit
c2067450cb
|
|
@ -1,3 +1,30 @@
|
|||
(function($){
|
||||
// 预留:后台联动逻辑(例如启用订阅后才显示其他字段)。
|
||||
$(function(){
|
||||
var $tbody = $('#yoone-sub-plans-body');
|
||||
var $addBtn = $('#yoone-sub-plan-add');
|
||||
|
||||
if ($tbody.length && $addBtn.length) {
|
||||
$addBtn.on('click', function(){
|
||||
var row = [
|
||||
'<tr class="yoone-sub-plan-row">',
|
||||
'<td><input type="text" name="yoone_sub_plans[label][]" placeholder="',
|
||||
(typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.placeholderLabel : '例如:标准计划'),
|
||||
'" /></td>',
|
||||
'<td><select name="yoone_sub_plans[period][]">',
|
||||
'<option value="month">', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.month : '月'), '</option>',
|
||||
'<option value="year">', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.year : '年'), '</option>',
|
||||
'</select></td>',
|
||||
'<td><input type="text" name="yoone_sub_plans[price][]" placeholder="', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.placeholderPrice : '留空表示用产品价'), '" /></td>',
|
||||
'<td><input type="number" min="0" step="0.1" name="yoone_sub_plans[discount_percent][]" placeholder="', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.placeholderDiscount : '如:10 表示9折'), '" /></td>',
|
||||
'<td><button type="button" class="button yoone-sub-plan-remove">', (typeof yooneSubsI18n !== 'undefined' ? yooneSubsI18n.remove : '删除'), '</button></td>',
|
||||
'</tr>'
|
||||
].join('');
|
||||
$tbody.append(row);
|
||||
});
|
||||
|
||||
$tbody.on('click', '.yoone-sub-plan-remove', function(){
|
||||
$(this).closest('tr').remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
})(jQuery);
|
||||
|
|
@ -28,7 +28,8 @@ class Yoone_Subscriptions_Admin {
|
|||
$tabs['yoone_subscriptions'] = array(
|
||||
'label' => __('订阅计划', 'yoone-subscriptions'),
|
||||
'target' => 'yoone_subscriptions_data',
|
||||
'class' => array('show_if_simple', 'show_if_variable'),
|
||||
// 在 simple、variable 以及 yoone_bundle(混装)产品类型上显示
|
||||
'class' => array('show_if_simple', 'show_if_variable', 'show_if_yoone_bundle'),
|
||||
'priority' => 80,
|
||||
);
|
||||
return $tabs;
|
||||
|
|
@ -43,7 +44,8 @@ class Yoone_Subscriptions_Admin {
|
|||
$cfg = Yoone_Subscriptions::get_config($product);
|
||||
wp_nonce_field('yoone_subscriptions_save', 'yoone_subscriptions_nonce');
|
||||
|
||||
echo '<div id="yoone_subscriptions_data" class="panel woocommerce_options_panel hidden">';
|
||||
// 面板在 simple/variable/yoone_bundle 上显示
|
||||
echo '<div id="yoone_subscriptions_data" class="panel woocommerce_options_panel hidden show_if_simple show_if_variable show_if_yoone_bundle">';
|
||||
echo '<div class="options_group">';
|
||||
|
||||
// 启用订阅
|
||||
|
|
@ -117,6 +119,49 @@ class Yoone_Subscriptions_Admin {
|
|||
'value' => $cfg['allow_onetime'] ? 'yes' : 'no',
|
||||
));
|
||||
|
||||
// 多订阅计划管理
|
||||
echo '<hr />';
|
||||
echo '<h4>' . esc_html__('订阅计划列表', 'yoone-subscriptions') . '</h4>';
|
||||
echo '<p class="description">' . esc_html__('可以为该产品配置多个订阅计划,前端用户可在购买时选择其一。每个计划可设置周期与每周期价格,或以折扣百分比表示相对产品价的优惠。', 'yoone-subscriptions') . '</p>';
|
||||
|
||||
$plans = isset($cfg['plans']) && is_array($cfg['plans']) ? $cfg['plans'] : array();
|
||||
echo '<table class="widefat fixed" style="margin-top:10px;">';
|
||||
echo '<thead><tr>'
|
||||
. '<th>' . esc_html__('计划名称', 'yoone-subscriptions') . '</th>'
|
||||
. '<th>' . esc_html__('周期', 'yoone-subscriptions') . '</th>'
|
||||
. '<th>' . esc_html__('每周期价格', 'yoone-subscriptions') . '</th>'
|
||||
. '<th>' . esc_html__('折扣百分比(可选)', 'yoone-subscriptions') . '</th>'
|
||||
. '<th>' . esc_html__('操作', 'yoone-subscriptions') . '</th>'
|
||||
. '</tr></thead><tbody id="yoone-sub-plans-body">';
|
||||
if (! empty($plans)) {
|
||||
foreach ($plans as $idx => $p) {
|
||||
echo '<tr class="yoone-sub-plan-row">';
|
||||
echo '<td><input type="text" name="yoone_sub_plans[label][]" value="' . esc_attr($p['label']) . '" placeholder="' . esc_attr__('例如:标准计划', 'yoone-subscriptions') . '" /></td>';
|
||||
echo '<td><select name="yoone_sub_plans[period][]">'
|
||||
. '<option value="month"' . selected($p['period'], 'month', false) . '>' . esc_html__('月', 'yoone-subscriptions') . '</option>'
|
||||
. '<option value="year"' . selected($p['period'], 'year', false) . '>' . esc_html__('年', 'yoone-subscriptions') . '</option>'
|
||||
. '</select></td>';
|
||||
echo '<td><input type="text" name="yoone_sub_plans[price][]" value="' . esc_attr($p['price']) . '" placeholder="' . esc_attr__('留空表示用产品价', 'yoone-subscriptions') . '" /></td>';
|
||||
echo '<td><input type="number" min="0" step="0.1" name="yoone_sub_plans[discount_percent][]" value="' . esc_attr($p['discount_percent']) . '" placeholder="' . esc_attr__('如:10 表示9折', 'yoone-subscriptions') . '" /></td>';
|
||||
echo '<td><button type="button" class="button yoone-sub-plan-remove">' . esc_html__('删除', 'yoone-subscriptions') . '</button></td>';
|
||||
echo '</tr>';
|
||||
}
|
||||
} else {
|
||||
// 初始空行
|
||||
echo '<tr class="yoone-sub-plan-row">'
|
||||
. '<td><input type="text" name="yoone_sub_plans[label][]" placeholder="' . esc_attr__('例如:标准计划', 'yoone-subscriptions') . '" /></td>'
|
||||
. '<td><select name="yoone_sub_plans[period][]">'
|
||||
. '<option value="month">' . esc_html__('月', 'yoone-subscriptions') . '</option>'
|
||||
. '<option value="year">' . esc_html__('年', 'yoone-subscriptions') . '</option>'
|
||||
. '</select></td>'
|
||||
. '<td><input type="text" name="yoone_sub_plans[price][]" placeholder="' . esc_attr__('留空表示用产品价', 'yoone-subscriptions') . '" /></td>'
|
||||
. '<td><input type="number" min="0" step="0.1" name="yoone_sub_plans[discount_percent][]" placeholder="' . esc_attr__('如:10 表示9折', 'yoone-subscriptions') . '" /></td>'
|
||||
. '<td><button type="button" class="button yoone-sub-plan-remove">' . esc_html__('删除', 'yoone-subscriptions') . '</button></td>'
|
||||
. '</tr>';
|
||||
}
|
||||
echo '</tbody></table>';
|
||||
echo '<p><button type="button" class="button button-primary" id="yoone-sub-plan-add">' . esc_html__('新增订阅计划', 'yoone-subscriptions') . '</button></p>';
|
||||
|
||||
echo '</div></div>';
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +201,33 @@ class Yoone_Subscriptions_Admin {
|
|||
update_post_meta($post_id, Yoone_Subscriptions::META_TIER_RULES, $tiers);
|
||||
}
|
||||
update_post_meta($post_id, Yoone_Subscriptions::META_ALLOW_ONETIME, $onetime ? '1' : '');
|
||||
|
||||
// 保存多订阅计划
|
||||
$plans = array();
|
||||
if (isset($_POST['yoone_sub_plans']) && is_array($_POST['yoone_sub_plans'])) {
|
||||
$labels = isset($_POST['yoone_sub_plans']['label']) ? (array) $_POST['yoone_sub_plans']['label'] : array();
|
||||
$periods = isset($_POST['yoone_sub_plans']['period']) ? (array) $_POST['yoone_sub_plans']['period'] : array();
|
||||
$prices = isset($_POST['yoone_sub_plans']['price']) ? (array) $_POST['yoone_sub_plans']['price'] : array();
|
||||
$discounts = isset($_POST['yoone_sub_plans']['discount_percent']) ? (array) $_POST['yoone_sub_plans']['discount_percent'] : array();
|
||||
$count = max(count($labels), count($periods), count($prices), count($discounts));
|
||||
for ($i=0; $i<$count; $i++) {
|
||||
$label = isset($labels[$i]) ? sanitize_text_field($labels[$i]) : '';
|
||||
$per = isset($periods[$i]) ? sanitize_text_field($periods[$i]) : 'month';
|
||||
$per = in_array($per, array('month','year'), true) ? $per : 'month';
|
||||
$price = isset($prices[$i]) ? wc_clean($prices[$i]) : '';
|
||||
$price = ($price === '' ? 0.0 : floatval(wc_format_decimal($price, 2)));
|
||||
$disc = isset($discounts[$i]) ? floatval($discounts[$i]) : 0.0;
|
||||
if ($label === '' && $price <= 0 && $disc <= 0) continue; // 全空行跳过
|
||||
$plans[] = array(
|
||||
'id' => uniqid('plan_'),
|
||||
'label' => $label ? $label : __('订阅计划', 'yoone-subscriptions'),
|
||||
'period' => $per,
|
||||
'price' => $price,
|
||||
'discount_percent' => max(0.0, $disc),
|
||||
);
|
||||
}
|
||||
}
|
||||
update_post_meta($post_id, Yoone_Subscriptions::META_PLANS, $plans);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ class Yoone_Subscriptions {
|
|||
const META_ALLOW_ONETIME = '_yoone_sub_allow_onetime'; // bool
|
||||
const META_MIN_QTY = '_yoone_sub_min_qty'; // int >=1(最小起订量,仅订阅模式下生效)
|
||||
const META_TIER_RULES = '_yoone_sub_tier_rules'; // string 逗号分隔“数量:折扣%”对,如 10:5,20:10
|
||||
// 新增:多订阅计划(每个计划包含:id、label、period、price、discount_percent 可选)
|
||||
const META_PLANS = '_yoone_sub_plans'; // serialized array
|
||||
|
||||
protected static $instance = null;
|
||||
|
||||
|
|
@ -47,9 +49,26 @@ class Yoone_Subscriptions {
|
|||
$onetime = (bool) get_post_meta($pid, self::META_ALLOW_ONETIME, true);
|
||||
$min_qty = absint(get_post_meta($pid, self::META_MIN_QTY, true));
|
||||
$tiers = get_post_meta($pid, self::META_TIER_RULES, true);
|
||||
$plans = get_post_meta($pid, self::META_PLANS, true);
|
||||
$period = in_array($period, array('month','year'), true) ? $period : 'month';
|
||||
$qty = max(1, $qty);
|
||||
$price = is_numeric($price) ? floatval($price) : 0.0; // 0 表示按产品原价
|
||||
// 规范化计划数组
|
||||
if (! is_array($plans)) {
|
||||
$plans = array();
|
||||
} else {
|
||||
$norm = array();
|
||||
foreach ($plans as $p) {
|
||||
$norm[] = array(
|
||||
'id' => isset($p['id']) ? sanitize_key($p['id']) : uniqid('plan_'),
|
||||
'label' => isset($p['label']) ? sanitize_text_field($p['label']) : __('订阅计划', 'yoone-subscriptions'),
|
||||
'period' => (isset($p['period']) && in_array($p['period'], array('month','year'), true)) ? $p['period'] : 'month',
|
||||
'price' => isset($p['price']) && is_numeric($p['price']) ? floatval($p['price']) : 0.0,
|
||||
'discount_percent' => isset($p['discount_percent']) && is_numeric($p['discount_percent']) ? floatval($p['discount_percent']) : 0.0,
|
||||
);
|
||||
}
|
||||
$plans = $norm;
|
||||
}
|
||||
return array(
|
||||
'enabled' => $enabled,
|
||||
'period' => $period,
|
||||
|
|
@ -58,6 +77,7 @@ class Yoone_Subscriptions {
|
|||
'allow_onetime' => $onetime,
|
||||
'min_qty' => max(1, $min_qty),
|
||||
'tier_rules' => is_string($tiers) ? $tiers : '',
|
||||
'plans' => $plans,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +93,7 @@ class Yoone_Subscriptions {
|
|||
'allow_onetime' => true,
|
||||
'min_qty' => 1,
|
||||
'tier_rules' => '',
|
||||
'plans' => array(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -118,6 +118,11 @@ class Yoone_Subscriptions_Frontend {
|
|||
$cfg = Yoone_Subscriptions::get_config($product);
|
||||
if (! $cfg['enabled']) return $cart_item_data;
|
||||
|
||||
// 若是混装子项目(存在父容器ID),不在子项目上应用订阅,避免重复计价
|
||||
if (isset($cart_item_data['yoone_bundle_parent_id'])) {
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
||||
$mode = isset($_POST['yoone_sub_purchase_mode']) ? sanitize_text_field($_POST['yoone_sub_purchase_mode']) : '';
|
||||
$period = isset($_POST['yoone_sub_period']) ? sanitize_text_field($_POST['yoone_sub_period']) : $cfg['period'];
|
||||
$qty = isset($_POST['yoone_sub_quantity']) ? absint($_POST['yoone_sub_quantity']) : $cfg['qty_default'];
|
||||
|
|
@ -133,7 +138,7 @@ class Yoone_Subscriptions_Frontend {
|
|||
'mode' => 'subscribe',
|
||||
'period' => $period,
|
||||
'qty' => $qty,
|
||||
'price' => ($cfg['price'] > 0 ? $cfg['price'] : floatval($product->get_price())), // 每周期价格
|
||||
'price' => ($cfg['price'] > 0 ? $cfg['price'] : floatval($product->get_price())), // 每周期价格;对于混装容器,将在后续根据子项目求和覆盖
|
||||
);
|
||||
return $cart_item_data;
|
||||
}
|
||||
|
|
@ -176,6 +181,37 @@ class Yoone_Subscriptions_Frontend {
|
|||
$sub_qty = absint($data['qty']);
|
||||
$cart_qty = absint(isset($item['quantity']) ? $item['quantity'] : 1);
|
||||
|
||||
// 若为混装容器,则以其子项目的价格汇总作为每周期基价(若有配置价则优先使用配置价)
|
||||
if (isset($item['yoone_bundle_container_id'])) {
|
||||
$container_id = $item['yoone_bundle_container_id'];
|
||||
// 使用配置价优先,否则求和子项目价
|
||||
$cfg = Yoone_Subscriptions::get_config($product);
|
||||
if (!($cfg['price'] > 0)) {
|
||||
$sum = 0.0;
|
||||
foreach ($cart->get_cart() as $k2 => $child) {
|
||||
if (isset($child['yoone_bundle_parent_id']) && $child['yoone_bundle_parent_id'] === $container_id) {
|
||||
$child_product = isset($child['data']) ? $child['data'] : null;
|
||||
if ($child_product && is_a($child_product, 'WC_Product')) {
|
||||
$child_qty = absint(isset($child['quantity']) ? $child['quantity'] : 1);
|
||||
$sum += floatval($child_product->get_price()) * $child_qty;
|
||||
}
|
||||
}
|
||||
}
|
||||
$price_per_cycle = $sum; // 以子项目合计为每周期基价
|
||||
} else {
|
||||
$price_per_cycle = floatval($cfg['price']);
|
||||
}
|
||||
// 将子项目的价格置为 0,避免重复计费
|
||||
foreach ($cart->get_cart() as $k2 => $child) {
|
||||
if (isset($child['yoone_bundle_parent_id']) && $child['yoone_bundle_parent_id'] === $container_id) {
|
||||
$child_product = isset($child['data']) ? $child['data'] : null;
|
||||
if ($child_product && is_a($child_product, 'WC_Product')) {
|
||||
$child_product->set_price(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$line_price = $price_per_cycle * $factor * $sub_qty * $cart_qty;
|
||||
$product->set_price($line_price);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue