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

336 lines
16 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',
'class' => array('show_if_simple', 'show_if_variable'),
'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');
echo '<div id="yoone_subscriptions_data" class="panel woocommerce_options_panel hidden">';
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 '</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' : '');
}
/**
* 注册 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>';
}
}