329 lines
15 KiB
PHP
329 lines
15 KiB
PHP
<?php
|
||
/**
|
||
* 前端:产品页订阅选项渲染、加入购物车的校验与价格计算。
|
||
*/
|
||
defined('ABSPATH') || exit;
|
||
|
||
class Yoone_Subscriptions_Frontend {
|
||
protected static $instance = null;
|
||
|
||
public static function instance() {
|
||
if (null === self::$instance) self::$instance = new self();
|
||
return self::$instance;
|
||
}
|
||
|
||
private function __construct() {
|
||
// 注册“我的账户 → 订阅”端点与菜单
|
||
add_action('init', array($this, 'register_account_endpoint'));
|
||
add_filter('query_vars', array($this, 'add_query_var'));
|
||
add_filter('woocommerce_get_query_vars', array($this, 'register_wc_query_vars'));
|
||
add_filter('woocommerce_endpoint_subscriptions_title', array($this, 'endpoint_title'));
|
||
add_filter('woocommerce_account_menu_items', array($this, 'add_account_menu_item'));
|
||
add_action('woocommerce_account_subscriptions_endpoint', array($this, 'render_account_subscriptions'));
|
||
|
||
// 在简单产品的 add-to-cart 区域前渲染订阅选项
|
||
add_action('woocommerce_before_add_to_cart_button', array($this, 'render_subscription_options'));
|
||
|
||
// 校验与存储购物车项数据(Woo 默认传递 3 个参数:$passed, $product_id, $quantity)
|
||
add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_add_to_cart'), 10, 3);
|
||
add_filter('woocommerce_add_cart_item_data', array($this, 'add_cart_item_data'), 10, 3);
|
||
|
||
// 展示购物车/订单中的订阅摘要
|
||
add_filter('woocommerce_get_item_data', array($this, 'display_item_data'), 10, 2);
|
||
|
||
// 动态设置行项目价格
|
||
add_action('woocommerce_before_calculate_totals', array($this, 'adjust_subscription_price'), 20, 1);
|
||
}
|
||
|
||
/**
|
||
* 注册 My Account 的“subscriptions”端点。
|
||
* - 访问地址示例:/my-account/subscriptions/
|
||
*/
|
||
public function register_account_endpoint() {
|
||
// 为 WP 添加 rewrite 端点(仅需注册一次;在插件激活时会 flush rules)
|
||
add_rewrite_endpoint('subscriptions', EP_ROOT | EP_PAGES);
|
||
}
|
||
|
||
/**
|
||
* 将自定义端点加入 WP 的 query vars,避免部分站点未识别导致 404。
|
||
*/
|
||
public function add_query_var($vars) {
|
||
$vars[] = 'subscriptions';
|
||
return $vars;
|
||
}
|
||
|
||
/**
|
||
* 将端点注册到 WooCommerce 的 endpoint 映射中(用于标题与模板路由)。
|
||
*/
|
||
public function register_wc_query_vars($vars) {
|
||
$vars['subscriptions'] = 'subscriptions';
|
||
return $vars;
|
||
}
|
||
|
||
/**
|
||
* 设置端点页面标题。
|
||
*/
|
||
public function endpoint_title($title) {
|
||
return __('我的订阅', 'yoone-subscriptions');
|
||
}
|
||
|
||
/**
|
||
* 在 My Account 菜单中新增“我的订阅”。
|
||
*/
|
||
public function add_account_menu_item($items) {
|
||
// 在“订单”后插入“订阅”菜单
|
||
$new = array();
|
||
foreach ($items as $key => $label) {
|
||
$new[$key] = $label;
|
||
if ('orders' === $key) {
|
||
$new['subscriptions'] = __('我的订阅', 'yoone-subscriptions');
|
||
}
|
||
}
|
||
// 如果没有“订单”菜单,则直接追加
|
||
if (! isset($new['subscriptions'])) {
|
||
$new['subscriptions'] = __('我的订阅', 'yoone-subscriptions');
|
||
}
|
||
return $new;
|
||
}
|
||
|
||
/**
|
||
* 渲染 My Account → 订阅 列表。
|
||
*/
|
||
public function render_account_subscriptions() {
|
||
if (! is_user_logged_in()) {
|
||
echo '<p>' . esc_html__('请先登录以查看您的订阅。', 'yoone-subscriptions') . '</p>';
|
||
return;
|
||
}
|
||
$uid = get_current_user_id();
|
||
if (! class_exists('Yoone_Subscriptions_DB')) {
|
||
echo '<p>' . esc_html__('订阅数据模块未加载。', 'yoone-subscriptions') . '</p>';
|
||
return;
|
||
}
|
||
$subs = Yoone_Subscriptions_DB::get_by_user($uid, array('limit' => 50, 'offset' => 0));
|
||
|
||
echo '<h2>' . esc_html__('我的订阅', 'yoone-subscriptions') . '</h2>';
|
||
if (empty($subs)) {
|
||
echo '<p>' . esc_html__('您目前还没有订阅。', 'yoone-subscriptions') . '</p>';
|
||
return;
|
||
}
|
||
|
||
echo '<table class="shop_table shop_table_responsive my_account_subscriptions">
|
||
<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>
|
||
<th>' . esc_html__('开始时间', 'yoone-subscriptions') . '</th>
|
||
<th>' . esc_html__('下次续费', 'yoone-subscriptions') . '</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>';
|
||
foreach ($subs as $row) {
|
||
$product = wc_get_product(intval($row['product_id']));
|
||
$product_name = $product ? $product->get_name() : ('#' . intval($row['product_id']));
|
||
$period_label = $row['period'] === 'year' ? __('年', 'yoone-subscriptions') : __('月', 'yoone-subscriptions');
|
||
echo '<tr>
|
||
<td>' . esc_html($product_name) . '</td>
|
||
<td>' . esc_html($period_label) . '</td>
|
||
<td>' . esc_html(intval($row['qty'])) . '</td>
|
||
<td>' . wc_price(floatval($row['price_per_cycle'])) . '</td>
|
||
<td>' . esc_html($row['status']) . '</td>
|
||
<td>' . esc_html($row['start_date']) . '</td>
|
||
<td>' . esc_html($row['next_renewal_date']) . '</td>
|
||
</tr>';
|
||
}
|
||
echo '</tbody></table>';
|
||
}
|
||
|
||
/**
|
||
* 渲染订阅选择 UI(产品页)。
|
||
*/
|
||
public function render_subscription_options() {
|
||
global $product;
|
||
if (! $product || ! is_a($product, 'WC_Product')) return;
|
||
|
||
$cfg = Yoone_Subscriptions::get_config($product);
|
||
if (! $cfg['enabled']) return; // 未开启订阅
|
||
|
||
// 前端样式/脚本
|
||
wp_enqueue_style('yoone-subs-frontend');
|
||
wp_enqueue_script('yoone-subs-frontend');
|
||
|
||
$regular_price = floatval($product->get_price());
|
||
$sub_price = $cfg['price'] > 0 ? $cfg['price'] : $regular_price;
|
||
$discount = ($regular_price > 0 && $sub_price < $regular_price) ? (1 - $sub_price / $regular_price) * 100.0 : 0.0;
|
||
|
||
// 简单模板输出(不使用专门模板文件,保持轻量)
|
||
echo '<div class="yoone-subs-block">';
|
||
echo '<h3>' . esc_html__('订阅选项', 'yoone-subscriptions') . '</h3>';
|
||
|
||
// 一次性 vs 订阅
|
||
if ($cfg['allow_onetime']) {
|
||
echo '<p><label><input type="radio" name="yoone_sub_purchase_mode" value="onetime" checked> ' . esc_html__('一次性购买', 'yoone-subscriptions') . '</label></p>';
|
||
echo '<p><label><input type="radio" name="yoone_sub_purchase_mode" value="subscribe"> ' . esc_html__('订阅购买', 'yoone-subscriptions') . '</label></p>';
|
||
} else {
|
||
echo '<input type="hidden" name="yoone_sub_purchase_mode" value="subscribe" />';
|
||
}
|
||
|
||
// 周期选择
|
||
echo '<p class="yoone-subs-row">'
|
||
. '<label>' . esc_html__('订阅周期', 'yoone-subscriptions') . '</label> '
|
||
. '<select name="yoone_sub_period">'
|
||
. '<option value="month"' . selected($cfg['period'], 'month', false) . '>' . esc_html__('月', 'yoone-subscriptions') . '</option>'
|
||
. '<option value="year"' . selected($cfg['period'], 'year', false) . '>' . esc_html__('年', 'yoone-subscriptions') . '</option>'
|
||
. '</select>'
|
||
. '</p>';
|
||
|
||
// 数量输入
|
||
echo '<p class="yoone-subs-row">'
|
||
. '<label>' . esc_html__('订阅数量', 'yoone-subscriptions') . '</label> '
|
||
. '<input type="number" name="yoone_sub_quantity" min="1" step="1" value="' . esc_attr($cfg['qty_default']) . '" />'
|
||
. '</p>';
|
||
|
||
// 价格与折扣展示(每周期价格)
|
||
echo '<p class="yoone-subs-price">'
|
||
. esc_html__('订阅价格(每周期):', 'yoone-subscriptions')
|
||
. wc_price($sub_price)
|
||
. ($discount > 0 ? ' <span class="yoone-subs-discount">' . sprintf(esc_html__('折扣约 %.1f%%', 'yoone-subscriptions'), $discount) . '</span>' : '')
|
||
. '</p>';
|
||
|
||
echo '</div>';
|
||
}
|
||
|
||
/**
|
||
* 校验加入购物车的订阅参数。
|
||
*/
|
||
public function validate_add_to_cart($passed, $product_id, $quantity) {
|
||
$product = wc_get_product($product_id);
|
||
if (! $product) return $passed;
|
||
|
||
$cfg = Yoone_Subscriptions::get_config($product);
|
||
if (! $cfg['enabled']) return $passed;
|
||
|
||
$mode = isset($_POST['yoone_sub_purchase_mode']) ? sanitize_text_field($_POST['yoone_sub_purchase_mode']) : '';
|
||
if ($cfg['allow_onetime'] && $mode === 'onetime') return $passed; // 一次性购买,无需校验订阅参数
|
||
|
||
$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'];
|
||
$period = in_array($period, array('month','year'), true) ? $period : $cfg['period'];
|
||
$qty = max(1, $qty);
|
||
|
||
if ($mode !== 'subscribe' && ! $cfg['allow_onetime']) {
|
||
wc_add_notice(__('该产品仅支持订阅购买。', 'yoone-subscriptions'), 'error');
|
||
return false;
|
||
}
|
||
|
||
// 可根据需要扩展更多校验(库存、最大值等)
|
||
return $passed;
|
||
}
|
||
|
||
/**
|
||
* 将订阅参数写入购物车项。
|
||
*/
|
||
public function add_cart_item_data($cart_item_data, $product_id, $variation_id) {
|
||
$product = wc_get_product($product_id);
|
||
if (! $product) return $cart_item_data;
|
||
$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'];
|
||
$period = in_array($period, array('month','year'), true) ? $period : $cfg['period'];
|
||
$qty = max(1, $qty);
|
||
|
||
// 如果选择一次性购买,则不写入订阅元数据,购物车中不显示任何订阅信息
|
||
if ($cfg['allow_onetime'] && $mode === 'onetime') {
|
||
return $cart_item_data;
|
||
}
|
||
|
||
$cart_item_data['yoone_subscriptions'] = array(
|
||
'mode' => 'subscribe',
|
||
'period' => $period,
|
||
'qty' => $qty,
|
||
'price' => ($cfg['price'] > 0 ? $cfg['price'] : floatval($product->get_price())), // 每周期价格;对于混装容器,将在后续根据子项目求和覆盖
|
||
);
|
||
return $cart_item_data;
|
||
}
|
||
|
||
/**
|
||
* 购物车/订单行项目中展示订阅摘要。
|
||
*/
|
||
public function display_item_data($item_data, $cart_item) {
|
||
if (empty($cart_item['yoone_subscriptions'])) return $item_data;
|
||
$data = $cart_item['yoone_subscriptions'];
|
||
// 仅在选择订阅购买时展示订阅信息;一次性购买不展示任何订阅相关信息
|
||
if (! isset($data['mode']) || $data['mode'] !== 'subscribe') {
|
||
return $item_data;
|
||
}
|
||
$period_label = $data['period'] === 'year' ? __('年', 'yoone-subscriptions') : __('月', 'yoone-subscriptions');
|
||
$item_data[] = array('key' => __('购买方式', 'yoone-subscriptions'), 'value' => __('订阅', 'yoone-subscriptions'));
|
||
$item_data[] = array('key' => __('周期', 'yoone-subscriptions'), 'value' => $period_label);
|
||
$item_data[] = array('key' => __('订阅数量', 'yoone-subscriptions'), 'value' => intval($data['qty']));
|
||
$item_data[] = array('key' => __('订阅价格(每周期)', 'yoone-subscriptions'), 'value' => wc_price(floatval($data['price'])));
|
||
return $item_data;
|
||
}
|
||
|
||
/**
|
||
* 根据订阅规则设置行项目价格:价格 = 每周期价格 × 周期系数 × 订阅数量 × 购物车数量。
|
||
*/
|
||
public function adjust_subscription_price($cart) {
|
||
if (is_admin() && ! defined('DOING_AJAX')) return;
|
||
if (empty($cart) || ! method_exists($cart, 'get_cart')) return;
|
||
|
||
foreach ($cart->get_cart() as $key => $item) {
|
||
if (empty($item['yoone_subscriptions'])) continue;
|
||
$data = $item['yoone_subscriptions'];
|
||
if ($data['mode'] !== 'subscribe') continue;
|
||
|
||
$product = isset($item['data']) ? $item['data'] : null;
|
||
if (! $product || ! is_a($product, 'WC_Product')) continue;
|
||
|
||
$price_per_cycle = floatval($data['price']);
|
||
$factor = Yoone_Subscriptions::period_factor($data['period']);
|
||
$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);
|
||
}
|
||
}
|
||
} |