yoone-wc-subscriptions/includes/frontend/class-yoone-subscriptions-f...

329 lines
15 KiB
PHP
Raw 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_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);
}
}
}