yoone-wc-subscriptions/includes/admin/class-yoone-subscriptions-a...

408 lines
22 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_Admin {
protected static $instance = null;
public static function instance() {
if (null === self::$instance) self::$instance = new self();
return self::$instance;
}
private function __construct() {
add_filter('woocommerce_product_data_tabs', array($this, 'add_tab'));
add_action('woocommerce_product_data_panels', array($this, 'render_panel'));
add_action('woocommerce_admin_process_product_meta', array($this, 'save_meta'));
// 在 WooCommerce 菜单下添加“Subscriptions”子菜单
add_action('admin_menu', array($this, 'register_submenu'));
}
/**
* 在产品数据区域添加“订阅计划”标签
*/
public function add_tab($tabs) {
$tabs['yoone_subscriptions'] = array(
'label' => __('订阅计划', 'yoone-subscriptions'),
'target' => 'yoone_subscriptions_data',
// 在 simple、variable 以及 yoone_bundle混装产品类型上显示
'class' => array('show_if_simple', 'show_if_variable', 'show_if_yoone_bundle'),
'priority' => 80,
);
return $tabs;
}
/**
* 渲染面板内容
*/
public function render_panel() {
global $post;
$product = wc_get_product($post->ID);
$cfg = Yoone_Subscriptions::get_config($product);
wp_nonce_field('yoone_subscriptions_save', 'yoone_subscriptions_nonce');
// 面板在 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">';
// 启用订阅
woocommerce_wp_checkbox(array(
'id' => 'yoone_sub_enabled',
'label' => __('启用订阅', 'yoone-subscriptions'),
'description' => __('开启后,前端产品页将显示订阅选项。', 'yoone-subscriptions'),
'desc_tip' => true,
'value' => $cfg['enabled'] ? 'yes' : 'no',
));
// 周期(月/年)
woocommerce_wp_select(array(
'id' => 'yoone_sub_period',
'label' => __('订阅周期', 'yoone-subscriptions'),
'options' => array('month' => __('月', 'yoone-subscriptions'), 'year' => __('年', 'yoone-subscriptions')),
'value' => $cfg['period'],
'desc_tip' => true,
'description' => __('订阅价格以该周期计费。年=12×月。', 'yoone-subscriptions'),
));
// 默认订阅数量
woocommerce_wp_text_input(array(
'id' => 'yoone_sub_qty_default',
'label' => __('默认订阅数量', 'yoone-subscriptions'),
'type' => 'number',
'value' => $cfg['qty_default'],
'custom_attributes' => array('min' => '1', 'step' => '1'),
'desc_tip' => true,
'description' => __('用于前端默认数量,可在产品页调整。', 'yoone-subscriptions'),
));
// 最小起订量(仅订阅模式下生效)
woocommerce_wp_text_input(array(
'id' => 'yoone_sub_min_qty',
'label' => __('最小起订量', 'yoone-subscriptions'),
'type' => 'number',
'value' => isset($cfg['min_qty']) ? intval($cfg['min_qty']) : 1,
'custom_attributes' => array('min' => '1', 'step' => '1'),
'desc_tip' => true,
'description' => __('仅在订阅购买模式下生效,购物车与订单将进行数量校验与提示。', 'yoone-subscriptions'),
));
// 订阅价格(可选,留空则使用产品常规价)
woocommerce_wp_text_input(array(
'id' => 'yoone_sub_price',
'label' => __('订阅价格(每周期)', 'yoone-subscriptions'),
'type' => 'text',
'value' => $cfg['price'] > 0 ? wc_format_decimal($cfg['price'], 2) : '',
'desc_tip' => true,
'description' => __('留空表示使用产品常规价。', 'yoone-subscriptions'),
));
// 分级折扣配置(逗号分隔的“数量:折扣%”对)
woocommerce_wp_text_input(array(
'id' => 'yoone_sub_tier_rules',
'label' => __('分级折扣规则', 'yoone-subscriptions'),
'type' => 'text',
'placeholder' => '10:5,20:10',
'value' => isset($cfg['tier_rules']) ? esc_attr($cfg['tier_rules']) : '',
'desc_tip' => true,
'description' => __('格式:数量:折扣%用逗号分隔多级。如“10:5,20:10”表示≥10件打95折≥20件打9折。留空则不启用。', 'yoone-subscriptions'),
));
// 是否允许一次性购买
woocommerce_wp_checkbox(array(
'id' => 'yoone_sub_allow_onetime',
'label' => __('允许一次性购买', 'yoone-subscriptions'),
'description' => __('开启后,前端产品页可选择一次性购买或订阅购买。', 'yoone-subscriptions'),
'desc_tip' => true,
'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>';
}
/**
* 保存订阅配置
*/
public function save_meta($post_id) {
if (! isset($_POST['yoone_subscriptions_nonce']) || ! wp_verify_nonce($_POST['yoone_subscriptions_nonce'], 'yoone_subscriptions_save')) {
return; // 安全验证失败
}
if (! current_user_can('edit_post', $post_id)) return;
$enabled = isset($_POST['yoone_sub_enabled']) && 'yes' === $_POST['yoone_sub_enabled'];
$period = isset($_POST['yoone_sub_period']) ? sanitize_text_field($_POST['yoone_sub_period']) : 'month';
$qty = isset($_POST['yoone_sub_qty_default']) ? absint($_POST['yoone_sub_qty_default']) : 1;
$min_qty = isset($_POST['yoone_sub_min_qty']) ? absint($_POST['yoone_sub_min_qty']) : 1;
$price = isset($_POST['yoone_sub_price']) ? wc_clean($_POST['yoone_sub_price']) : '';
$tiers = isset($_POST['yoone_sub_tier_rules']) ? sanitize_text_field($_POST['yoone_sub_tier_rules']) : '';
$onetime = isset($_POST['yoone_sub_allow_onetime']) && 'yes' === $_POST['yoone_sub_allow_onetime'];
$period = in_array($period, array('month','year'), true) ? $period : 'month';
$qty = max(1, $qty);
$price_v = ($price === '' ? '' : wc_format_decimal($price, 2));
update_post_meta($post_id, Yoone_Subscriptions::META_ENABLED, $enabled ? '1' : '');
update_post_meta($post_id, Yoone_Subscriptions::META_PERIOD, $period);
update_post_meta($post_id, Yoone_Subscriptions::META_QTY_DEFAULT, $qty);
update_post_meta($post_id, Yoone_Subscriptions::META_MIN_QTY, max(1, $min_qty));
if ($price_v === '') {
delete_post_meta($post_id, Yoone_Subscriptions::META_PRICE);
} else {
update_post_meta($post_id, Yoone_Subscriptions::META_PRICE, $price_v);
}
if ($tiers === '') {
delete_post_meta($post_id, Yoone_Subscriptions::META_TIER_RULES);
} else {
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);
}
/**
* 注册 WooCommerce → Subscriptions 子菜单。
*/
public function register_submenu() {
// 仅限具有管理 WooCommerce 权限的用户可访问
add_submenu_page(
'woocommerce',
__('Subscriptions', 'yoone-subscriptions'),
__('Subscriptions', 'yoone-subscriptions'),
'manage_woocommerce',
'yoone-subscriptions',
array($this, 'render_subscriptions_page'),
56 // 显示位置(可选)
);
}
/**
* 渲染订阅列表管理页面。
* 支持按用户、产品、状态筛选与分页。
*/
public function render_subscriptions_page() {
if (! current_user_can('manage_woocommerce')) {
wp_die(__('您没有权限查看该页面。', 'yoone-subscriptions'));
}
// 处理状态更新操作pause/resume/cancel带 nonce
if (isset($_POST['yoone_sub_action']) && isset($_POST['_wpnonce']) && wp_verify_nonce($_POST['_wpnonce'], 'yoone_subscriptions_action')) {
$action = sanitize_key($_POST['yoone_sub_action']);
$sub_id = isset($_POST['yoone_sub_id']) ? absint($_POST['yoone_sub_id']) : 0;
$allowed = array('pause' => 'paused', 'resume' => 'active', 'cancel' => 'canceled');
if ($sub_id > 0 && isset($allowed[$action])) {
if (class_exists('Yoone_Subscriptions_DB')) {
Yoone_Subscriptions_DB::update($sub_id, array('status' => $allowed[$action]));
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__('状态已更新。', 'yoone-subscriptions') . '</p></div>';
}
}
}
// 读取筛选参数
$status = isset($_GET['status']) ? sanitize_key($_GET['status']) : '';
$product_id = isset($_GET['product_id']) ? absint($_GET['product_id']) : 0;
$user_query = isset($_GET['user']) ? sanitize_text_field($_GET['user']) : '';
$paged = isset($_GET['paged']) ? max(1, absint($_GET['paged'])) : 1;
$per_page = 20;
$offset = ($paged - 1) * $per_page;
global $wpdb;
$table = method_exists('Yoone_Subscriptions_DB', 'table_name') ? Yoone_Subscriptions_DB::table_name() : $wpdb->prefix . 'yoone_subscriptions';
// 构造 WHERE 条件
$where = array('1=1');
$params = array();
if (! empty($status)) { $where[] = 'status = %s'; $params[] = $status; }
if ($product_id > 0) { $where[] = 'product_id = %d'; $params[] = $product_id; }
$user_clause = '';
if (! empty($user_query)) {
// 支持用用户ID或邮箱搜索
if (is_numeric($user_query)) {
$where[] = 'user_id = %d'; $params[] = absint($user_query);
} else {
// 通过邮箱查询用户ID
$user = get_user_by('email', $user_query);
if ($user) { $where[] = 'user_id = %d'; $params[] = $user->ID; }
else { $where[] = '0=1'; } // 无匹配,返回空
}
}
// 查询总数用于分页
$sql_count = "SELECT COUNT(*) FROM {$table} WHERE " . implode(' AND ', $where);
$total = $wpdb->get_var($wpdb->prepare($sql_count, $params));
// 查询当前页数据
$sql = "SELECT * FROM {$table} WHERE " . implode(' AND ', $where) . " ORDER BY id DESC LIMIT %d OFFSET %d";
$params_page = array_merge($params, array($per_page, $offset));
$rows = $wpdb->get_results($wpdb->prepare($sql, $params_page), ARRAY_A);
// 渲染页面
echo '<div class="wrap">';
echo '<h1 class="wp-heading-inline">' . esc_html__('Subscriptions', 'yoone-subscriptions') . '</h1>';
echo '<hr class="wp-header-end">';
// 筛选表单
echo '<form method="get">';
echo '<input type="hidden" name="page" value="yoone-subscriptions" />';
echo '<div class="tablenav top">';
echo '<div class="alignleft actions">';
echo '<select name="status">';
$statuses = array('' => __('全部状态', 'yoone-subscriptions'), 'active' => __('Active', 'yoone-subscriptions'), 'paused' => __('Paused', 'yoone-subscriptions'), 'canceled' => __('Canceled', 'yoone-subscriptions'));
foreach ($statuses as $key => $label) {
echo '<option value="' . esc_attr($key) . '"' . selected($status, $key, false) . '>' . esc_html($label) . '</option>';
}
echo '</select> ';
echo '<input type="number" name="product_id" placeholder="' . esc_attr__('产品ID', 'yoone-subscriptions') . '" value="' . esc_attr($product_id) . '" style="width:120px" /> ';
echo '<input type="text" name="user" placeholder="' . esc_attr__('用户ID或邮箱', 'yoone-subscriptions') . '" value="' . esc_attr($user_query) . '" style="width:200px" /> ';
submit_button(__('筛选'), 'secondary', null, false);
echo '</div>';
echo '</div>';
echo '</form>';
// 列表表格
echo '<table class="wp-list-table widefat fixed striped">';
echo '<thead><tr>';
$cols = array(
__('ID', 'yoone-subscriptions'),
__('用户', 'yoone-subscriptions'),
__('产品', 'yoone-subscriptions'),
__('周期', 'yoone-subscriptions'),
__('数量', 'yoone-subscriptions'),
__('每周期金额', 'yoone-subscriptions'),
__('状态', 'yoone-subscriptions'),
__('开始时间', 'yoone-subscriptions'),
__('下次续费', 'yoone-subscriptions'),
__('操作', 'yoone-subscriptions'),
);
foreach ($cols as $c) echo '<th>' . esc_html($c) . '</th>';
echo '</tr></thead><tbody>';
if (empty($rows)) {
echo '<tr><td colspan="10">' . esc_html__('暂无订阅记录。', 'yoone-subscriptions') . '</td></tr>';
} else {
foreach ($rows as $r) {
$user = get_user_by('id', intval($r['user_id']));
$product = wc_get_product(intval($r['product_id']));
echo '<tr>';
echo '<td>#' . intval($r['id']) . '</td>';
echo '<td>' . ($user ? esc_html($user->user_email) . ' (ID ' . intval($user->ID) . ')' : '-') . '</td>';
echo '<td>' . ($product ? esc_html($product->get_name()) . ' (ID ' . intval($product->get_id()) . ')' : 'ID ' . intval($r['product_id'])) . '</td>';
echo '<td>' . esc_html($r['period']) . '</td>';
echo '<td>' . intval($r['qty']) . '</td>';
echo '<td>' . wc_price(floatval($r['price_per_cycle'])) . '</td>';
echo '<td>' . esc_html($r['status']) . '</td>';
echo '<td>' . esc_html($r['start_date']) . '</td>';
echo '<td>' . esc_html($r['next_renewal_date']) . '</td>';
echo '<td>';
echo '<form method="post" style="display:inline-block;margin-right:6px;">' . wp_nonce_field('yoone_subscriptions_action', '_wpnonce', true, false) . '<input type="hidden" name="yoone_sub_id" value="' . intval($r['id']) . '" />';
if ($r['status'] !== 'paused') {
echo '<input type="hidden" name="yoone_sub_action" value="pause" />';
submit_button(__('暂停', 'yoone-subscriptions'), 'small', null, false);
}
echo '</form>';
echo '<form method="post" style="display:inline-block;margin-right:6px;">' . wp_nonce_field('yoone_subscriptions_action', '_wpnonce', true, false) . '<input type="hidden" name="yoone_sub_id" value="' . intval($r['id']) . '" />';
if ($r['status'] !== 'active') {
echo '<input type="hidden" name="yoone_sub_action" value="resume" />';
submit_button(__('恢复', 'yoone-subscriptions'), 'small', null, false);
}
echo '</form>';
echo '<form method="post" style="display:inline-block;">' . wp_nonce_field('yoone_subscriptions_action', '_wpnonce', true, false) . '<input type="hidden" name="yoone_sub_id" value="' . intval($r['id']) . '" />';
if ($r['status'] !== 'canceled') {
echo '<input type="hidden" name="yoone_sub_action" value="cancel" />';
submit_button(__('取消', 'yoone-subscriptions'), 'small', null, false);
}
echo '</form>';
echo '</td>';
echo '</tr>';
}
}
echo '</tbody></table>';
// 分页导航
if ($total > $per_page) {
$total_pages = ceil($total / $per_page);
echo '<div class="tablenav bottom"><div class="tablenav-pages">';
echo paginate_links(array(
'base' => add_query_arg(array('paged' => '%#%')),
'format' => '',
'prev_text' => __('« 上一页', 'yoone-subscriptions'),
'next_text' => __('下一页 »', 'yoone-subscriptions'),
'total' => $total_pages,
'current' => $paged,
));
echo '</div></div>';
}
echo '</div>';
}
}