- DEFAULT_PER_PAGE 由 30 改为 20。 - 删除 per_page_selector() 及其表单样式(.aksrt-perpage-form / .order-header-right), 工具栏恢复为「过滤链接 + 搜索表单」两段式。 ice_perpage 仍作为 URL 参数被尊重,只是不再提供界面入口。 - 保留上一提交的分页健壮性修复:页码夹取到 [1, total_pages]、翻页链接用显式基准 URL、 越界页码回退并重查、禁用按钮 pointer-events:none。 - 契约测试同步改写:断言默认值为 20,并显式禁止「每页显示」控件回归。 验证:默认态渲染为「每页 20 共10」,页面内不存在 aksrt-perpage-form; ice_perpage=5 仍能翻到第 2 页且链接携带该参数;paged=2 越界仍回退到第 1 页的 10 行; node --test 三套 18/18;php -l 通过;无头 Chrome 截图确认工具栏无多余控件。
1336 lines
70 KiB
PHP
1336 lines
70 KiB
PHP
<?php
|
||
/**
|
||
* @package AKSRT
|
||
*
|
||
* 后台「卡密管理」页面 — 1:1 复刻参考主题 zibll 的 zibpay/page/charge-card.php。
|
||
*
|
||
* 结构:列表(过滤/搜索/排序/批量删除/分页) + 添加卡密(自动生成 / 导入) + 导出卡密(text / xls)。
|
||
* 卡密类型与参考主题一致:余额充值 / 会员兑换 / 积分兑换 / 自定义卡密;
|
||
* 本主题内部类型映射:balance / vip / credits / custom。
|
||
* 差异点:AKSRT 无商城发货(shipped)子系统,故不含「已售」标记与链接;删除取代参考主题的批量操作语义一致。
|
||
*/
|
||
|
||
namespace AKSRT\Admin\Native;
|
||
|
||
defined('ABSPATH') || exit;
|
||
|
||
class PayChargeCards {
|
||
|
||
public const CAPABILITY = 'manage_options';
|
||
public const PAGE_SLUG = 'aksrt_charge_cards';
|
||
public const NONCE = 'aksrt_card_pass_page';
|
||
public const EXPORT_AJAX_ACTION = 'aksrt_card_pass_export';
|
||
|
||
/** 每页条数默认值;列表页可由 ice_perpage 覆盖(无界面控件,仅供 URL / 程序化调用)。 */
|
||
public const DEFAULT_PER_PAGE = 20;
|
||
|
||
private static $instance = null;
|
||
|
||
/** @var string 当前页面 URL */
|
||
private $page_url = '';
|
||
|
||
public static function get_instance(): self {
|
||
if (null === self::$instance) {
|
||
self::$instance = new self();
|
||
}
|
||
return self::$instance;
|
||
}
|
||
|
||
public function init(): void {
|
||
if (!is_admin()) {
|
||
return;
|
||
}
|
||
add_action('wp_ajax_' . self::EXPORT_AJAX_ACTION, [$this, 'handle_export']);
|
||
}
|
||
|
||
public function render_charge_cards_page(): void {
|
||
$this->ensure_cap();
|
||
$this->page_url = admin_url('admin.php?page=' . self::PAGE_SLUG);
|
||
|
||
if (!class_exists('\AKSRT\Payment\ChargeCard')) {
|
||
echo '<div class="wrap"><h1>' . esc_html__('卡密管理', 'aksrt') . '</h1>';
|
||
echo '<div class="notice notice-error"><p>' . esc_html__('卡密模块未初始化。', 'aksrt') . '</p></div></div>';
|
||
return;
|
||
}
|
||
|
||
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : '';
|
||
$s = isset($_REQUEST['s']) ? sanitize_text_field((string) wp_unslash($_REQUEST['s'])) : '';
|
||
$page_title = __('卡密管理', 'aksrt');
|
||
$head_but = '';
|
||
|
||
// 页面级动作(参考主题在渲染前处理 add / delete)。
|
||
$action = isset($_REQUEST['action']) ? sanitize_key((string) wp_unslash($_REQUEST['action'])) : '';
|
||
if ($action === 'add') {
|
||
$this->handle_add();
|
||
} elseif ($action === 'delete') {
|
||
$this->handle_delete();
|
||
}
|
||
|
||
// 标题右侧按钮是页面头部的一部分,只在此处输出一次(列表页同样如此,由 render_list 负责列表本体)。
|
||
switch ($tab) {
|
||
case 'add':
|
||
$page_title = __('添加卡密', 'aksrt');
|
||
$head_but = '<a href="' . esc_url($this->page_url) . '" class="page-title-action">' . esc_html__('返回列表', 'aksrt') . '</a>';
|
||
break;
|
||
|
||
case 'export':
|
||
$page_title = __('导出卡密', 'aksrt');
|
||
$head_but = '<a href="' . esc_url($this->page_url) . '" class="page-title-action">' . esc_html__('返回列表', 'aksrt') . '</a>';
|
||
break;
|
||
|
||
default:
|
||
$head_but = '<a href="' . esc_url(add_query_arg('tab', 'add', $this->page_url)) . '" class="page-title-action">' . esc_html__('添加卡密', 'aksrt') . '</a>';
|
||
$head_but .= '<a href="' . esc_url(add_query_arg('tab', 'export', $this->page_url)) . '" class="page-title-action">' . esc_html__('导出卡密', 'aksrt') . '</a>';
|
||
if ($s !== '') {
|
||
$head_but .= '<div><div class="update-nag" style="margin: 10px 0 0;">' . sprintf(esc_html__('搜索 “%s” 的内容', 'aksrt'), esc_html($s)) . ' </div></div>';
|
||
}
|
||
break;
|
||
}
|
||
|
||
echo '<div class="wrap">';
|
||
$this->print_assets();
|
||
echo '<h1 class="wp-heading-inline">' . esc_html($page_title) . '</h1>';
|
||
echo $head_but; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
|
||
|
||
if ($tab === 'add') {
|
||
$this->render_add_form();
|
||
} elseif ($tab === 'export') {
|
||
$this->render_export_form();
|
||
} else {
|
||
$this->render_list($s);
|
||
}
|
||
|
||
echo '</div>';
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
* 列表
|
||
* ------------------------------------------------------------------ */
|
||
|
||
/**
|
||
* @param string $s 搜索词(由页面头部统一解析后传入,用于提示、过滤链接与查询)。
|
||
*/
|
||
private function render_list(string $s): void {
|
||
$svc = \AKSRT\Payment\ChargeCard::get_instance();
|
||
|
||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||
$type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : '';
|
||
$other = isset($_GET['other']) ? sanitize_text_field((string) wp_unslash($_GET['other'])) : '';
|
||
$orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : 'modified_time';
|
||
$order = isset($_GET['desc']) ? (strtoupper((string) wp_unslash($_GET['desc'])) === 'ASC' ? 'ASC' : 'DESC') : 'DESC';
|
||
$paged = max(1, (int) ($_GET['paged'] ?? 1));
|
||
$per_page = max(1, min(100, (int) ($_REQUEST['ice_perpage'] ?? self::DEFAULT_PER_PAGE)));
|
||
|
||
// 参考主题:status=0 表示未使用;本主题规范状态为 active。
|
||
if ($status === '0' || $status === 'active') {
|
||
$status = 'active';
|
||
} elseif (!in_array($status, ['used', 'expired', 'disabled'], true)) {
|
||
$status = '';
|
||
}
|
||
if (!in_array($type, ['balance', 'vip', 'credits', 'custom'], true)) {
|
||
$type = '';
|
||
}
|
||
|
||
// 过滤链接(标题右侧按钮与搜索提示由页面头部统一输出)。
|
||
$sub_but = [
|
||
['name' => __('全部', 'aksrt'), 'href' => $this->page_url],
|
||
];
|
||
if ($s === '') {
|
||
$sub_but[] = ['name' => __('未使用', 'aksrt'), 'href' => add_query_arg('status', '0', $this->page_url)];
|
||
$sub_but[] = ['name' => __('已使用', 'aksrt'), 'href' => add_query_arg('status', 'used', $this->page_url)];
|
||
foreach ($this->type_labels() as $_k => $_v) {
|
||
$sub_but[] = ['name' => $_v, 'href' => add_query_arg('type', $_k, $this->page_url)];
|
||
}
|
||
}
|
||
$but_html = '';
|
||
foreach ($sub_but as $but) {
|
||
$but_html .= '<li><a href="' . esc_url($but['href']) . '">' . esc_html($but['name']) . '</a></li> | ';
|
||
}
|
||
$but_html = substr($but_html, 0, -2);
|
||
|
||
$search = '<form class="form-inline form-order" method="post">
|
||
<div class="form-group" style="float: right;">
|
||
<input type="text" class="form-control" name="s" placeholder="' . esc_attr__('搜索卡密', 'aksrt') . '">
|
||
<button type="submit" class="button">' . esc_html__('提交', 'aksrt') . '</button>
|
||
</div>
|
||
</form>';
|
||
|
||
// 当前筛选参数(不含 paged)。分页链接显式带上它,不再依赖 add_query_arg()
|
||
// 默认取 $_SERVER['REQUEST_URI'](链接会随环境漂移,且列条件容易丢)。
|
||
$query_args = $this->list_query_args($status, $type, $other, $s, $orderby, $order, $per_page);
|
||
|
||
echo '<div class="order-header"><ul class="subsubsub">' . $but_html . '</ul>' . $search . '</div>';
|
||
|
||
$filters = [
|
||
'status' => $status,
|
||
'type' => $type,
|
||
'other' => $other,
|
||
'search' => $s,
|
||
'orderby' => $orderby,
|
||
'order' => $order,
|
||
];
|
||
|
||
$result = $svc->get_list($filters + ['limit' => $per_page, 'offset' => ($paged - 1) * $per_page]);
|
||
$items = (array) ($result['items'] ?? []);
|
||
$count_all = (int) ($result['total'] ?? 0);
|
||
|
||
// 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外,
|
||
// 表现为「空白表格 + 一个分页控件都没有」的死胡同,只能靠手改 URL 退出。
|
||
// 这里回退到最后一页重查一次,保证越界页码也能拿到数据与分页条。
|
||
$max_page = max(1, (int) ($result['max_pages'] ?? 1));
|
||
if (!$items && $count_all > 0 && $paged > $max_page) {
|
||
$paged = $max_page;
|
||
$result = $svc->get_list($filters + ['limit' => $per_page, 'offset' => ($paged - 1) * $per_page]);
|
||
$items = (array) ($result['items'] ?? []);
|
||
$count_all = (int) ($result['total'] ?? $count_all);
|
||
}
|
||
|
||
if (!$items) {
|
||
echo '<div class="clear"></div>';
|
||
echo '<table class="widefat fixed striped posts table table-bordered" style="min-width: 1000px;">';
|
||
echo '<thead><tr><td style="color: #ff4a4a;text-align: center;">' . esc_html__('未找到对应内容,或暂无内容', 'aksrt') . '</td></tr></thead>';
|
||
echo '</table>';
|
||
return;
|
||
}
|
||
|
||
// 表头(与参考主题同列、同排序链接)。
|
||
$theads = [
|
||
['width' => '6%', 'orderby' => 'card', 'name' => __('卡号', 'aksrt')],
|
||
['width' => '10%', 'orderby' => 'password', 'name' => __('密码', 'aksrt')],
|
||
['width' => '5%', 'orderby' => 'type', 'name' => __('类型', 'aksrt')],
|
||
['width' => '5%', 'orderby' => 'create_time', 'name' => __('创建时间', 'aksrt')],
|
||
['width' => '5%', 'orderby' => 'modified_time', 'name' => __('更新时间', 'aksrt')],
|
||
['width' => '5%', 'orderby' => 'status', 'name' => __('状态', 'aksrt')],
|
||
['width' => '8%', 'orderby' => 'other', 'name' => __('标识', 'aksrt')],
|
||
];
|
||
|
||
$thead_th = '<td id="cb" class="manage-column column-cb check-column" style="width: 2%;"><label class="screen-reader-text" for="cb-select-all-1">' . esc_html__('全选', 'aksrt') . '</label><input id="cb-select-all-1" type="checkbox"></td>';
|
||
foreach ($theads as $thead) {
|
||
$orderby_links = '';
|
||
if ($thead['orderby']) {
|
||
$orderby_url = add_query_arg('orderby', $thead['orderby']);
|
||
$orderby_links .= '<a title="' . esc_attr__('降序', 'aksrt') . '" href="' . esc_url(add_query_arg('desc', 'ASC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-up"></span></a>';
|
||
$orderby_links .= '<a title="' . esc_attr__('升序', 'aksrt') . '" href="' . esc_url(add_query_arg('desc', 'DESC', $orderby_url)) . '"><span class="dashicons dashicons-arrow-down"></span></a>';
|
||
$orderby_links = '<span class="orderby-but">' . $orderby_links . '</span>';
|
||
}
|
||
$thead_th .= '<th class="" width="' . esc_attr($thead['width']) . '">' . esc_html($thead['name']) . $orderby_links . '</th>';
|
||
}
|
||
|
||
$tbody = '';
|
||
foreach ($items as $msg) {
|
||
$meta = [];
|
||
if (!empty($msg['meta'])) {
|
||
$decoded = json_decode((string) $msg['meta'], true);
|
||
$meta = is_array($decoded) ? $decoded : [];
|
||
}
|
||
|
||
// 自定义卡密不展示状态;其余类型按状态着色(类型链接统一在下方链表中生成)。
|
||
$status_html = '';
|
||
if ($msg['type'] !== 'custom') {
|
||
$status_html = '<span style="color: #3d7ffd;">' . esc_html__('未使用', 'aksrt') . '</span>';
|
||
if ($msg['status'] === 'used') {
|
||
$status_html = '<span style="color: #f93b3b;">' . esc_html__('已使用', 'aksrt') . '</span>';
|
||
if (!empty($msg['order_number'])) {
|
||
$order_link_url = admin_url('admin.php?page=aksrt_orders&s=' . urlencode((string) $msg['order_number']));
|
||
$status_html .= '<a style="color: #ff6215;" target="_blank" href="' . esc_url($order_link_url) . '"> [' . esc_html__('查看', 'aksrt') . ']</a>';
|
||
}
|
||
} elseif ($msg['status'] === 'expired') {
|
||
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已过期', 'aksrt') . '</span>';
|
||
} elseif ($msg['status'] === 'disabled') {
|
||
$status_html = '<span style="color: #8c8f94;">' . esc_html__('已禁用', 'aksrt') . '</span>';
|
||
}
|
||
}
|
||
|
||
$other_a = '';
|
||
if (!empty($msg['note'])) {
|
||
$other_a = '<a href="' . esc_url(add_query_arg('other', $msg['note'], $this->page_url)) . '">' . esc_html($msg['note']) . '</a>';
|
||
}
|
||
|
||
if ($msg['type'] === 'vip') {
|
||
$type_html = '<a style="color:#975106" href="' . esc_url(add_query_arg('type', 'vip', $this->page_url)) . '">' . esc_html__('会员兑换', 'aksrt') . '</a>';
|
||
$level = (int) ($meta['level'] ?? 1);
|
||
$type_html .= '<br>' . esc_html($this->vip_level_name($level)) . ':' . esc_html($this->vip_time_text($meta));
|
||
} elseif ($msg['type'] === 'credits') {
|
||
$type_html = '<a style="color:#289d0f" href="' . esc_url(add_query_arg('type', 'credits', $this->page_url)) . '">' . esc_html__('积分兑换', 'aksrt') . '</a>';
|
||
$type_html .= '<br>' . esc_html__('积分:', 'aksrt') . esc_html((string) (int) ($meta['points'] ?? 0));
|
||
} elseif ($msg['type'] === 'balance') {
|
||
$type_html = '<a style="color:#0c76e6" href="' . esc_url(add_query_arg('type', 'balance', $this->page_url)) . '">' . esc_html__('余额充值', 'aksrt') . '</a>';
|
||
$type_html .= '<br>' . esc_html__('金额:', 'aksrt') . esc_html($this->balance_amount_text($meta));
|
||
} else {
|
||
$type_html = '<a style="color:#a42ade" href="' . esc_url(add_query_arg('type', 'custom', $this->page_url)) . '">' . esc_html__('自定义卡密', 'aksrt') . '</a>';
|
||
}
|
||
|
||
$card_name = (string) ($meta['custom_name']['card_name'] ?? '');
|
||
$password_name = (string) ($meta['custom_name']['password_name'] ?? '');
|
||
|
||
$tbody .= '<tr>';
|
||
$tbody .= '<th scope="row" class="check-column"><label class="screen-reader-text" for="cb-select-' . (int) $msg['id'] . '">' . esc_html__('选择', 'aksrt') . '</label>
|
||
<input id="cb-select-' . (int) $msg['id'] . '" type="checkbox" name="action_id[]" value="' . (int) $msg['id'] . '">
|
||
</th>';
|
||
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['card']) . "' data-clipboard-tag=\"" . esc_attr($card_name !== '' ? $card_name : __('卡号', 'aksrt')) . "\">" . esc_html($card_name) . ' ' . esc_html((string) $msg['card']) . '</span></td>';
|
||
$tbody .= "<td><span data-clipboard-text='" . esc_attr((string) $msg['password']) . "' data-clipboard-tag=\"" . esc_attr($password_name !== '' ? $password_name : __('密码', 'aksrt')) . "\">" . esc_html($password_name) . ' ' . esc_html((string) $msg['password']) . '</span></td>';
|
||
$tbody .= "<td>$type_html</td>";
|
||
$tbody .= '<td>' . esc_html((string) $msg['created_at']) . '</td>';
|
||
$tbody .= '<td>' . esc_html((string) ($msg['used_at'] ?: $msg['created_at'])) . '</td>';
|
||
$tbody .= "<td>$status_html</td>";
|
||
$tbody .= "<td>$other_a</td>";
|
||
$tbody .= '</tr>';
|
||
}
|
||
|
||
echo '<div class="clear"></div>';
|
||
echo '<form class="" method="post">';
|
||
wp_nonce_field(self::NONCE);
|
||
echo '<div class="bulkactions" style="margin: 10px 0;">
|
||
<label for="bulk-action-selector-top" class="screen-reader-text">' . esc_html__('选择批量操作', 'aksrt') . '</label><select name="action" id="bulk-action-selector-top">
|
||
<option value="-1">' . esc_html__('批量操作', 'aksrt') . '</option>
|
||
<option value="delete">' . esc_html__('删除', 'aksrt') . '</option>
|
||
</select>
|
||
<input type="submit" class="button action" value="' . esc_attr__('应用', 'aksrt') . '">
|
||
</div>';
|
||
|
||
echo '<div style="overflow-y: auto;width: 100%;">';
|
||
echo '<table class="widefat fixed striped posts table table-bordered" style="min-width: 1000px;">';
|
||
echo '<thead><tr>' . $thead_th . '</tr></thead>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
|
||
echo '<tbody>' . $tbody . '</tbody>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
|
||
echo '</table>';
|
||
echo '</div>';
|
||
echo '</form>';
|
||
echo '<div class="clear"></div>';
|
||
|
||
$this->render_pagenavi($count_all, $per_page, $query_args);
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
* 添加卡密
|
||
* ------------------------------------------------------------------ */
|
||
|
||
private function render_add_form(): void {
|
||
$type_labels = $this->type_labels();
|
||
?>
|
||
<p><b><?php esc_html_e('在此添加卡密', 'aksrt'); ?></b></p>
|
||
<ul style="list-style:disc;margin:0 0 12px 18px;line-height:1.9">
|
||
<li><?php esc_html_e('如果您已经准备好了卡密资料,请选择导入的方式添加', 'aksrt'); ?></li>
|
||
<li><?php esc_html_e('您也可以采用系统生成的方式,自动批量添加卡密', 'aksrt'); ?></li>
|
||
</ul>
|
||
<form class="aksrt-cardpass-form" method="post">
|
||
<?php wp_nonce_field(self::NONCE); ?>
|
||
<input type="hidden" name="action" value="add" />
|
||
<table class="form-table" role="presentation">
|
||
<?php
|
||
$this->field_row(
|
||
__('卡密类型', 'aksrt'),
|
||
$this->button_set('type', $type_labels, 'balance'),
|
||
[]
|
||
);
|
||
$this->field_row(
|
||
__('添加方式', 'aksrt'),
|
||
$this->button_set('add_type', [
|
||
'auto' => __('系统自动生成', 'aksrt'),
|
||
'import' => __('导入卡密', 'aksrt'),
|
||
], 'auto'),
|
||
[]
|
||
);
|
||
|
||
// 自动生成
|
||
$this->field_row(
|
||
__('生成数量', 'aksrt'),
|
||
$this->spinner('auto_num', 20, 1, 1000, 10, __('张', 'aksrt')),
|
||
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']],
|
||
__('需要生成多少张卡密(单次生成数量太多可能会对服务器性能造成影响)', 'aksrt')
|
||
);
|
||
$this->field_row(
|
||
__('标识', 'aksrt'),
|
||
'<input type="text" class="regular-text" name="auto_remarks" value="' . esc_attr('cardpass_' . current_time('YmdHis')) . '" />',
|
||
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']],
|
||
__('对生成的卡密做标记标识,方便后期查找管理', 'aksrt') . '<br>' . sprintf(esc_html__('注意:标识中不能有%1$s字符,系统已占用', 'aksrt'), '<code>shipped</code>')
|
||
);
|
||
$this->field_row(
|
||
__('面额', 'aksrt'),
|
||
'<input type="number" step="0.01" min="0" name="auto_price" value="0" />',
|
||
[
|
||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||
['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'],
|
||
],
|
||
__('单张卡密的面额', 'aksrt')
|
||
);
|
||
$this->field_row(
|
||
__('兑换会员', 'aksrt'),
|
||
__('会员等级', 'aksrt') . ' ' . $this->button_set('vip_exchange[level]', [
|
||
'1' => $this->vip_level_name(1),
|
||
'2' => $this->vip_level_name(2),
|
||
], '1'),
|
||
[
|
||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||
['key' => 'type', 'op' => '==', 'val' => 'vip'],
|
||
]
|
||
);
|
||
$this->field_row(
|
||
' ',
|
||
__('兑换会员时长', 'aksrt') . '<br><input type="text" name="vip_exchange[time]" value="" />',
|
||
[
|
||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||
['key' => 'type', 'op' => '==', 'val' => 'vip'],
|
||
],
|
||
__('填', 'aksrt') . '<code>Permanent</code>' . __('为永久', 'aksrt')
|
||
);
|
||
$this->field_row(
|
||
' ',
|
||
__('时长单位', 'aksrt') . ' ' . $this->button_set('vip_exchange[unit]', [
|
||
'day' => __('天', 'aksrt'),
|
||
'month' => __('个月', 'aksrt'),
|
||
], 'month'),
|
||
[
|
||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||
['key' => 'type', 'op' => '==', 'val' => 'vip'],
|
||
['key' => 'vip_exchange[time]', 'op' => '!=', 'val' => 'Permanent'],
|
||
]
|
||
);
|
||
$this->field_row(
|
||
__('自定义显示名称', 'aksrt'),
|
||
__('卡号显示名称', 'aksrt') . '<br><input type="text" class="regular-text" name="custom_name[card_name]" value="" /><br>' .
|
||
__('卡密显示名称', 'aksrt') . '<br><input type="text" class="regular-text" name="custom_name[password_name]" value="" />',
|
||
[
|
||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||
['key' => 'type', 'op' => '==', 'val' => 'custom'],
|
||
]
|
||
);
|
||
$this->field_row(
|
||
__('高级选项', 'aksrt'),
|
||
'<input type="checkbox" name="auto_top_s" value="1" />',
|
||
[['key' => 'add_type', 'op' => '==', 'val' => 'auto']]
|
||
);
|
||
$this->field_row(
|
||
__('自定义卡号位数', 'aksrt'),
|
||
$this->spinner('auto_rand_number_limit', 20, 1, 50, 5, __('位数', 'aksrt')),
|
||
[
|
||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||
['key' => 'auto_top_s', 'op' => 'checked'],
|
||
]
|
||
);
|
||
$this->field_row(
|
||
__('自定义密码位数', 'aksrt'),
|
||
$this->spinner('auto_rand_password_limit', 35, 1, 50, 5, __('位数', 'aksrt')),
|
||
[
|
||
['key' => 'add_type', 'op' => '==', 'val' => 'auto'],
|
||
['key' => 'auto_top_s', 'op' => 'checked'],
|
||
],
|
||
__('自定义自动生成的长度(不能太短,太短可能会出现重复)如果启用了单密码模式,可以将卡号位数设置为0', 'aksrt')
|
||
);
|
||
|
||
// 导入提示统一放在「卡密数据」正上方:切换卡密类型/添加方式时,
|
||
// 命中的那一条紧邻示例数据与输入框,便于对照填写。
|
||
$this->field_row(
|
||
__('单密码模式', 'aksrt'),
|
||
'<input type="checkbox" name="only_pass_s" value="1" />',
|
||
[
|
||
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
|
||
['key' => 'type', 'op' => '==', 'val' => 'custom'],
|
||
]
|
||
);
|
||
$this->submessage_row(
|
||
[
|
||
['key' => 'add_type', 'op' => '!=', 'val' => 'auto'],
|
||
['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'],
|
||
],
|
||
'<p><b>' . esc_html__('导入卡密', 'aksrt') . '</b></p>
|
||
<ul class="aksrt-submsg-list">
|
||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 面额 标识', 'aksrt') . '</code></li>
|
||
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '</li>
|
||
</ul>
|
||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA 50 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('50' . $this->currency_unit())) . '</div>
|
||
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq 100 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('100' . $this->currency_unit())) . '</div>'
|
||
);
|
||
$this->submessage_row(
|
||
[
|
||
['key' => 'add_type', 'op' => '!=', 'val' => 'auto'],
|
||
['key' => 'type', 'op' => '==', 'val' => 'vip'],
|
||
],
|
||
'<p><b>' . esc_html__('导入会员兑换卡密', 'aksrt') . '</b></p>
|
||
<ul class="aksrt-submsg-list">
|
||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 会员参数 标识', 'aksrt') . '</code></li>
|
||
<li>' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||
<li>' . esc_html__('会员参数格式为:', 'aksrt') . '<code>level=1&time=5&unit=day</code></li>
|
||
<li>' . esc_html__('参数说明:level为等级,只能为1或2。time为时长,填Permanent或数字。unit为时长单位,只能填day(天)或者month(月)', 'aksrt') . '</li>
|
||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li><li>' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '</li>
|
||
</ul>
|
||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA level=1&time=5&unit=day ' . esc_html__('5天一级会员', 'aksrt') . '</div>
|
||
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq level=2&time=Permanent ' . esc_html__('永久2级会员', 'aksrt') . '</div>
|
||
'
|
||
);
|
||
$this->submessage_row(
|
||
[
|
||
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
|
||
['key' => 'type', 'op' => '==', 'val' => 'custom'],
|
||
['key' => 'only_pass_s', 'op' => 'unchecked'],
|
||
],
|
||
'<p><b>' . esc_html__('自定义卡密导入', 'aksrt') . '</b></p>
|
||
<ul class="aksrt-submsg-list">
|
||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('卡号 密码 标识 自定义参数', 'aksrt') . '</code></li>
|
||
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||
<li>' . esc_html__('自定义参数格式为:', 'aksrt') . '<code>card_name=授权号&password_name=授权密码</code>,' . esc_html__('card_name为卡号的显示名称,password_name为卡密的显示名称,可以留空', 'aksrt') . '</li>
|
||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li>
|
||
</ul>
|
||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||
<div>4849977843617 3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '</div>
|
||
<div>6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '</div>
|
||
<div>111111@qq.com 11assssddsds ' . esc_html__('某某账号', 'aksrt') . ' card_name=' . esc_html__('账号', 'aksrt') . '&password_name=' . esc_html__('密码', 'aksrt') . '</div>'
|
||
);
|
||
$this->submessage_row(
|
||
[
|
||
['key' => 'add_type', 'op' => '==', 'val' => 'import'],
|
||
['key' => 'type', 'op' => '==', 'val' => 'custom'],
|
||
['key' => 'only_pass_s', 'op' => 'checked'],
|
||
],
|
||
'<p><b>' . esc_html__('自定义卡密导入(单密码模式)', 'aksrt') . '</b></p>
|
||
<ul class="aksrt-submsg-list">
|
||
<li>' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '<code>' . esc_html__('密码 标识 自定义参数', 'aksrt') . '</code></li>
|
||
<li>' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '</li>
|
||
<li>' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '</li>
|
||
<li>' . esc_html__('自定义参数格式为:', 'aksrt') . '<code>password_name=授权密码</code>,' . esc_html__('password_name为卡密的显示名称,可以留空', 'aksrt') . '</li>
|
||
</ul>
|
||
<p><b>' . esc_html__('数据示例', 'aksrt') . '</b></p>
|
||
<div>3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '</div>
|
||
<div>SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '</div>'
|
||
);
|
||
$this->field_row(
|
||
__('卡密数据', 'aksrt'),
|
||
'<textarea name="import_data" rows="10" class="large-text code" style="resize: both;max-width: none;"></textarea>',
|
||
[['key' => 'add_type', 'op' => '==', 'val' => 'import']]
|
||
);
|
||
$this->field_row(
|
||
__('自定义分隔符号', 'aksrt'),
|
||
'<input type="text" class="mini-input" style="width:80px;" name="import_division" value=" " />',
|
||
[['key' => 'add_type', 'op' => '==', 'val' => 'import']],
|
||
__('卡号和密码之间分割符号(默认为空格分割)', 'aksrt')
|
||
);
|
||
?>
|
||
</table>
|
||
<?php submit_button(__('确认提交', 'aksrt'), 'primary', 'submit', true); ?>
|
||
</form>
|
||
<?php
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
* 导出卡密
|
||
* ------------------------------------------------------------------ */
|
||
|
||
private function render_export_form(): void {
|
||
?>
|
||
<form class="aksrt-cardpass-form" method="post" action="<?php echo esc_url(admin_url('admin-ajax.php')); ?>">
|
||
<input type="hidden" name="action" value="<?php echo esc_attr(self::EXPORT_AJAX_ACTION); ?>" />
|
||
<table class="form-table" role="presentation">
|
||
<?php
|
||
$this->field_row(
|
||
__('卡密类型', 'aksrt'),
|
||
$this->button_set('type', $this->type_labels(), 'balance'),
|
||
[]
|
||
);
|
||
$this->field_row(
|
||
__('选择状态', 'aksrt'),
|
||
$this->button_set('status', [
|
||
'all' => __('全部', 'aksrt'),
|
||
'0' => __('未使用', 'aksrt'),
|
||
'used' => __('已使用', 'aksrt'),
|
||
], 'all'),
|
||
[['key' => 'type', 'op' => '!=', 'val' => 'custom']]
|
||
);
|
||
$this->field_row(
|
||
__('导出格式', 'aksrt'),
|
||
$this->button_set('export_format', [
|
||
'text' => __('文本文档', 'aksrt'),
|
||
'xls' => __('Excel表格', 'aksrt'),
|
||
], 'xls'),
|
||
[]
|
||
);
|
||
$this->field_row(
|
||
' ',
|
||
__('分隔符号', 'aksrt') . '<br><input type="text" class="mini-input" style="width:80px;" name="text_division" value=" " />',
|
||
[['key' => 'export_format', 'op' => '==', 'val' => 'text']],
|
||
__('卡号和密码之间分割符号(默认为空格分割)', 'aksrt')
|
||
);
|
||
?>
|
||
</table>
|
||
<?php submit_button(__('确认提交', 'aksrt'), 'primary', 'submit', true); ?>
|
||
</form>
|
||
<?php
|
||
}
|
||
|
||
/**
|
||
* 导出处理(参考主题 zibpay_ajax_card_pass_export 同款行为)。
|
||
*/
|
||
public function handle_export(): void {
|
||
if (!current_user_can(self::CAPABILITY)) {
|
||
wp_die(esc_html__('暂无此权限', 'aksrt'));
|
||
}
|
||
|
||
@set_time_limit(0);
|
||
|
||
$export_format = isset($_REQUEST['export_format']) && sanitize_key((string) wp_unslash($_REQUEST['export_format'])) === 'text' ? 'text' : 'xls';
|
||
$type = isset($_REQUEST['type']) ? sanitize_key((string) wp_unslash($_REQUEST['type'])) : '';
|
||
$status_raw = isset($_REQUEST['status']) ? sanitize_key((string) wp_unslash($_REQUEST['status'])) : 'all';
|
||
|
||
$status = '';
|
||
if ($status_raw !== 'all' && $status_raw !== '') {
|
||
// 参考主题:0=未使用。
|
||
$status = $status_raw === '0' ? 'active' : $status_raw;
|
||
}
|
||
|
||
$rows = \AKSRT\Payment\ChargeCard::get_instance()->get_export_rows($type, $status);
|
||
if (!$rows) {
|
||
wp_die(esc_html__('暂无可导出的内容', 'aksrt'));
|
||
}
|
||
|
||
$filename = $type . '_' . gmdate('d_m_Y');
|
||
|
||
// 组装行数据(与参考主题字段一致)。
|
||
$title = [];
|
||
$data = [];
|
||
foreach ($rows as $row) {
|
||
$meta = [];
|
||
if (!empty($row['meta'])) {
|
||
$decoded = json_decode((string) $row['meta'], true);
|
||
$meta = is_array($decoded) ? $decoded : [];
|
||
}
|
||
$status_text = $row['status'] === 'used'
|
||
? __('已使用', 'aksrt')
|
||
: ($row['status'] === 'expired' ? __('已过期', 'aksrt') : ($row['status'] === 'disabled' ? __('已禁用', 'aksrt') : __('未使用', 'aksrt')));
|
||
|
||
switch ($type) {
|
||
case 'credits': // 积分兑换
|
||
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('面额', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
|
||
$data[] = [(string) $row['card'], (string) $row['password'], (string) (int) ($meta['points'] ?? 0), (string) $row['note'], $status_text];
|
||
break;
|
||
case 'vip': // 会员兑换
|
||
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('兑换会员', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
|
||
$data[] = [(string) $row['card'], (string) $row['password'], $this->vip_meta_query($meta), (string) $row['note'], $status_text];
|
||
break;
|
||
case 'custom': // 自定义
|
||
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
|
||
$data[] = [(string) $row['card'], (string) $row['password'], (string) $row['note'], ''];
|
||
break;
|
||
default: // 余额充值
|
||
$title = [__('卡号', 'aksrt'), __('密码', 'aksrt'), __('面额', 'aksrt'), __('备注', 'aksrt'), __('状态', 'aksrt')];
|
||
$data[] = [(string) $row['card'], (string) $row['password'], $this->balance_amount_text($meta), (string) $row['note'], $status_text];
|
||
}
|
||
}
|
||
|
||
if ($export_format === 'text') {
|
||
$division = isset($_REQUEST['text_division']) ? (string) wp_unslash($_REQUEST['text_division']) : ' ';
|
||
header('Content-type:application/octet-stream');
|
||
header('Accept-Ranges:bytes');
|
||
header('Content-Disposition:attachment;filename=' . $filename . '.txt');
|
||
header('Pragma: no-cache');
|
||
header('Pragma: public');
|
||
header('Expires: 0');
|
||
$_data = [];
|
||
foreach ($data as $val) {
|
||
$_data[] = implode($division, $val);
|
||
}
|
||
echo implode("\n", $_data); // phpcs:ignore WordPress.Security.EscapeOutput
|
||
} else {
|
||
$this->export_excel($data, $title, $filename);
|
||
}
|
||
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* 导出 xls(参考主题 zib_export_excel 同款行为,GB2312 编码保证 Excel 兼容)。
|
||
*
|
||
* @param array<int,array<int,string>> $data
|
||
* @param array<int,string> $title
|
||
*/
|
||
private function export_excel(array $data, array $title, string $filename): void {
|
||
header('Content-type:application/octet-stream');
|
||
header('Accept-Ranges:bytes');
|
||
header('Content-type:application/vnd.ms-excel');
|
||
header('Content-Disposition:attachment;filename=' . $filename . '.xls');
|
||
header('Pragma: no-cache');
|
||
header('Expires: 0');
|
||
|
||
$convert = static function (string $value): string {
|
||
return function_exists('mb_convert_encoding')
|
||
? (string) @mb_convert_encoding($value, 'GB2312', 'UTF-8')
|
||
: $value;
|
||
};
|
||
|
||
if (!empty($title)) {
|
||
$title = array_map($convert, $title);
|
||
echo implode("\t", $title) . "\n";
|
||
}
|
||
|
||
if (!empty($data)) {
|
||
$_data = [];
|
||
foreach ($data as $val) {
|
||
$val = array_map($convert, array_map('strval', $val));
|
||
$_data[] = implode("\t", $val);
|
||
}
|
||
echo implode("\n", $_data);
|
||
}
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
* 动作处理
|
||
* ------------------------------------------------------------------ */
|
||
|
||
private function handle_add(): void {
|
||
check_admin_referer(self::NONCE);
|
||
|
||
$add_type = isset($_REQUEST['add_type']) ? sanitize_key((string) wp_unslash($_REQUEST['add_type'])) : 'auto';
|
||
$pass_type = isset($_REQUEST['type']) ? sanitize_key((string) wp_unslash($_REQUEST['type'])) : '';
|
||
if (!in_array($pass_type, ['balance', 'vip', 'credits', 'custom'], true)) {
|
||
$this->page_notice(__('错误!', 'aksrt'), __('参数传入错误', 'aksrt'), 'error');
|
||
return;
|
||
}
|
||
|
||
if ($add_type === 'import') {
|
||
$this->handle_import($pass_type);
|
||
return;
|
||
}
|
||
|
||
// 自动生成。
|
||
$auto_num = isset($_REQUEST['auto_num']) ? (int) $_REQUEST['auto_num'] : 0;
|
||
if (!$auto_num) {
|
||
$this->page_notice(__('错误!', 'aksrt'), __('请输入需要生成的数量', 'aksrt'), 'error');
|
||
return;
|
||
}
|
||
|
||
$pass_meta = [];
|
||
switch ($pass_type) {
|
||
case 'balance':
|
||
case 'credits':
|
||
$auto_price = isset($_REQUEST['auto_price']) ? floatval(round((float) $_REQUEST['auto_price'], 2)) : 0;
|
||
if (!$auto_price) {
|
||
$this->page_notice(__('错误!', 'aksrt'), __('请输入卡密的面额', 'aksrt'), 'error');
|
||
return;
|
||
}
|
||
$pass_meta = $pass_type === 'credits'
|
||
? ['points' => (int) $auto_price]
|
||
: ['amount' => number_format($auto_price, 2, '.', '')];
|
||
break;
|
||
case 'vip':
|
||
$vip = isset($_REQUEST['vip_exchange']) && is_array($_REQUEST['vip_exchange']) ? wp_unslash($_REQUEST['vip_exchange']) : [];
|
||
if (empty($vip['time'])) {
|
||
$this->page_notice(__('错误!', 'aksrt'), __('请填写兑换会员参数', 'aksrt'), 'error');
|
||
return;
|
||
}
|
||
$pass_meta = $this->vip_meta($vip);
|
||
break;
|
||
}
|
||
|
||
// 自定义显示名称(参考主题:所有类型均可附加)。
|
||
$custom_name = isset($_REQUEST['custom_name']) && is_array($_REQUEST['custom_name']) ? wp_unslash($_REQUEST['custom_name']) : [];
|
||
if (!empty($custom_name['card_name'])) {
|
||
$pass_meta['custom_name']['card_name'] = trim(sanitize_text_field((string) $custom_name['card_name']));
|
||
}
|
||
if (!empty($custom_name['password_name'])) {
|
||
$pass_meta['custom_name']['password_name'] = trim(sanitize_text_field((string) $custom_name['password_name']));
|
||
}
|
||
|
||
$rand_number = 20;
|
||
$rand_password = 35;
|
||
if (!empty($_REQUEST['auto_top_s'])) {
|
||
$rand_number = isset($_REQUEST['auto_rand_number_limit']) ? (int) $_REQUEST['auto_rand_number_limit'] : 20;
|
||
$rand_password = isset($_REQUEST['auto_rand_password_limit']) ? (int) $_REQUEST['auto_rand_password_limit'] : 35;
|
||
}
|
||
|
||
$remarks = isset($_REQUEST['auto_remarks']) ? sanitize_text_field((string) wp_unslash($_REQUEST['auto_remarks'])) : '';
|
||
|
||
$created = \AKSRT\Payment\ChargeCard::get_instance()->generate_cards($pass_type, $auto_num, $pass_meta, $rand_number, $rand_password, $remarks);
|
||
|
||
$this->page_notice(__('完成!', 'aksrt'), sprintf(__('已自动生成%s个卡密', 'aksrt'), (string) $created));
|
||
}
|
||
|
||
private function handle_import(string $pass_type): void {
|
||
$import_data = isset($_REQUEST['import_data']) ? (string) wp_unslash($_REQUEST['import_data']) : '';
|
||
if (!$import_data) {
|
||
$this->page_notice(__('错误!', 'aksrt'), __('请粘贴您需要导入的数据', 'aksrt'), 'error');
|
||
return;
|
||
}
|
||
|
||
$import_data_array = explode("\r\n", $import_data);
|
||
$import_division = isset($_REQUEST['import_division']) ? (string) wp_unslash($_REQUEST['import_division']) : ' ';
|
||
if ($import_division === '') {
|
||
$import_division = ' ';
|
||
}
|
||
|
||
$success_i = 0;
|
||
$error_i = 0;
|
||
$svc = \AKSRT\Payment\ChargeCard::get_instance();
|
||
|
||
foreach ($import_data_array as $line) {
|
||
$v = explode($import_division, $line);
|
||
$pass_meta = [];
|
||
$_card = isset($v[0]) ? trim((string) $v[0]) : '';
|
||
$_pass = isset($v[1]) ? trim((string) $v[1]) : '';
|
||
$_other = isset($v[3]) ? trim((string) $v[3]) : '';
|
||
|
||
switch ($pass_type) {
|
||
case 'balance':
|
||
case 'credits':
|
||
$val = isset($v[2]) && $v[2] !== '' ? (float) $v[2] : 0;
|
||
if ($pass_type === 'credits') {
|
||
$pass_meta = $val ? ['points' => (int) $val] : [];
|
||
} else {
|
||
$pass_meta = $val ? ['amount' => number_format($val, 2, '.', '')] : [];
|
||
}
|
||
if (!$pass_meta) {
|
||
$error_i++;
|
||
continue 2;
|
||
}
|
||
break;
|
||
|
||
case 'vip':
|
||
$vip_pass_data = isset($v[2]) && $v[2] !== '' ? wp_parse_args(trim((string) $v[2])) : [];
|
||
if (!empty($vip_pass_data['level']) && !empty($vip_pass_data['time'])) {
|
||
$pass_meta = $this->vip_meta(is_array($vip_pass_data) ? $vip_pass_data : []);
|
||
}
|
||
if (!$pass_meta) {
|
||
$error_i++;
|
||
continue 2;
|
||
}
|
||
break;
|
||
|
||
case 'custom':
|
||
$pass_meta = [];
|
||
$only_pass_s = !empty($_REQUEST['only_pass_s']);
|
||
if ($only_pass_s) {
|
||
// 单密码模式:密码 标识 自定义参数
|
||
$_card = '';
|
||
$_pass = isset($v[0]) ? trim((string) $v[0]) : '';
|
||
$_other = isset($v[1]) ? trim((string) $v[1]) : '';
|
||
$custom_name = isset($v[2]) && $v[2] !== '' ? wp_parse_args(trim((string) $v[2])) : [];
|
||
} else {
|
||
$_other = isset($v[2]) ? trim((string) $v[2]) : '';
|
||
$custom_name = isset($v[3]) && $v[3] !== '' ? wp_parse_args(trim((string) $v[3])) : [];
|
||
}
|
||
if (!empty($custom_name['card_name'])) {
|
||
$pass_meta['custom_name']['card_name'] = trim(sanitize_text_field((string) $custom_name['card_name']));
|
||
}
|
||
if (!empty($custom_name['password_name'])) {
|
||
$pass_meta['custom_name']['password_name'] = trim(sanitize_text_field((string) $custom_name['password_name']));
|
||
}
|
||
break;
|
||
}
|
||
|
||
if ($_pass !== '') {
|
||
$id = $svc->create([
|
||
'card' => $_card,
|
||
'password' => $_pass,
|
||
'type' => $pass_type,
|
||
'meta' => $pass_meta,
|
||
'note' => $_other,
|
||
]);
|
||
if ($id > 0) {
|
||
$success_i++;
|
||
} else {
|
||
$error_i++;
|
||
}
|
||
} else {
|
||
$error_i++;
|
||
}
|
||
}
|
||
|
||
if ($success_i) {
|
||
$this->page_notice(
|
||
__('导入完成', 'aksrt'),
|
||
sprintf(__('成功导入%s个卡密%s', 'aksrt'), (string) $success_i, $error_i ? sprintf(__(',%s个导入失败', 'aksrt'), (string) $error_i) : '')
|
||
);
|
||
} else {
|
||
$this->page_notice(__('导入失败', 'aksrt'), sprintf(__('%s个导入失败,数据格式错误', 'aksrt'), (string) $error_i), 'error');
|
||
}
|
||
}
|
||
|
||
private function handle_delete(): void {
|
||
check_admin_referer(self::NONCE);
|
||
|
||
$delete_ids = isset($_REQUEST['action_id']) ? array_map('absint', (array) wp_unslash($_REQUEST['action_id'])) : [];
|
||
$delete_ids = array_filter($delete_ids);
|
||
if (!$delete_ids) {
|
||
$this->page_notice(__('错误!', 'aksrt'), __('未选择需要删除的内容', 'aksrt'), 'error');
|
||
return;
|
||
}
|
||
|
||
$delete_i = \AKSRT\Payment\ChargeCard::get_instance()->delete_by_ids($delete_ids);
|
||
|
||
$this->page_notice(__('删除完成', 'aksrt'), sprintf(__('已删除%s个卡密', 'aksrt'), (string) $delete_i));
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
* 渲染辅助
|
||
* ------------------------------------------------------------------ */
|
||
|
||
/**
|
||
* @param array<int,array{key:string,op:string,val:string}> $deps
|
||
*/
|
||
private function field_row(string $title, string $html, array $deps = [], string $desc = ''): void {
|
||
$dep_attr = $deps ? ' data-dep="' . esc_attr((string) wp_json_encode($deps)) . '"' : '';
|
||
$hidden = ($deps && !$this->deps_visible($deps)) ? ' style="display:none;"' : '';
|
||
echo '<tr' . $dep_attr . $hidden . '><th scope="row">' . esc_html($title) . '</th><td>' . $html; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。
|
||
if ($desc !== '') {
|
||
echo '<p class="description">' . wp_kses_post($desc) . '</p>';
|
||
}
|
||
echo '</td></tr>';
|
||
}
|
||
|
||
/**
|
||
* @param array<int,array{key:string,op:string,val:string}> $deps
|
||
*/
|
||
private function submessage_row(array $deps, string $content): void {
|
||
$dep_attr = ' data-dep="' . esc_attr((string) wp_json_encode($deps)) . '"';
|
||
$hidden = $this->deps_visible($deps) ? '' : ' style="display:none;"';
|
||
// 提示块**不要**挂 WP 的 .notice / .notice-warning:wp-admin/js/common.js 在 DOM
|
||
// 就绪时会执行
|
||
// $('div.updated, div.error, div.notice').not('.inline, .below-h2').insertAfter($headerEnd)
|
||
// 把页面上所有「非 inline」的 notice 整块搬到 .wp-header-end(不存在时取 .wrap 内首个
|
||
// h1/h2)之后。提示块一旦带上 notice,就会被搬到页面顶部、脱离「添加方式—卡密数据」
|
||
// 之间的字段区,而且搬迁发生在求值之后,行内的显隐逻辑无从纠正。
|
||
// 外观改由 .aksrt-submsg 自带(见 print_assets),与参考主题 CSF content 字段一致。
|
||
// data-dep 与初始隐藏状态同时挂在 <tr> 和内层提示块上:提示块若脱离所在行,仍能自行显隐。
|
||
echo '<tr' . $dep_attr . $hidden . '><td colspan="2"><div' . $dep_attr . ' class="aksrt-submsg"' . $hidden . '>' . $content . '</div></td></tr>'; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。
|
||
}
|
||
|
||
/**
|
||
* 服务端按表单默认值预判依赖可见性(求值规则与行内 JS 完全一致)。
|
||
* 仅在表单渲染默认值(添加方式=系统自动生成、类型=余额)下,把不满足依赖的行标记为初始隐藏,
|
||
* 避免四条导入提示首帧铺开。
|
||
*
|
||
* 注意:已知键必须覆盖 field_row/submessage_row 用到的所有依赖键,否则该键的判定会与 JS 不一致,
|
||
* 出现「服务端判可见、JS 判隐藏」之类的错配(例如自定义卡密的两条导入提示同时铺开)。
|
||
*
|
||
* @param array<int,array{key:string,op:string,val:string}> $deps
|
||
*/
|
||
private function deps_visible(array $deps): bool {
|
||
foreach ($deps as $dep) {
|
||
$key = (string) ($dep['key'] ?? '');
|
||
$val = (string) ($dep['val'] ?? '');
|
||
$op = (string) ($dep['op'] ?? '');
|
||
|
||
if (!$this->is_known_dep_key($key)) {
|
||
// 未登记的键:不做服务端判定,交 JS 处理。
|
||
continue;
|
||
}
|
||
|
||
$v = (string) $this->default_form_value($key);
|
||
|
||
switch ($op) {
|
||
case '==':
|
||
if ($v !== $val) {
|
||
return false;
|
||
}
|
||
break;
|
||
case '!=':
|
||
if ($v === $val) {
|
||
return false;
|
||
}
|
||
break;
|
||
case 'any':
|
||
if (!in_array($v, explode(',', $val), true)) {
|
||
return false;
|
||
}
|
||
break;
|
||
case 'checked':
|
||
if ($v === '') {
|
||
return false;
|
||
}
|
||
break;
|
||
case 'unchecked':
|
||
if ($v !== '') {
|
||
return false;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 可在服务端求值的字段键(须完整覆盖表单依赖中使用的键,与 default_form_value 保持一致)。
|
||
*/
|
||
private function is_known_dep_key(string $key): bool {
|
||
static $known = [
|
||
'add_type',
|
||
'type',
|
||
'auto_top_s',
|
||
'only_pass_s',
|
||
'vip_exchange[time]',
|
||
'export_format',
|
||
'status',
|
||
];
|
||
return in_array($key, $known, true);
|
||
}
|
||
|
||
/**
|
||
* 表单字段的初始默认值(须与 field_row/button_set/spinner 渲染的默认值保持同步)。
|
||
*/
|
||
private function default_form_value(string $key): string {
|
||
switch ($key) {
|
||
case 'add_type':
|
||
return 'auto';
|
||
case 'type':
|
||
return 'balance';
|
||
case 'export_format':
|
||
return 'xls';
|
||
case 'status':
|
||
return 'all';
|
||
// 未勾选的 checkbox/switcher 与空文本,在依赖求值中均等价于空字符串。
|
||
case 'auto_top_s':
|
||
case 'only_pass_s':
|
||
case 'vip_exchange[time]':
|
||
return '';
|
||
default:
|
||
return '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* button_set:radio 按钮组(参考主题 CSF button_set 的原生替代)。
|
||
*
|
||
* @param array<string,string> $options
|
||
*/
|
||
private function button_set(string $name, array $options, string $current): string {
|
||
$html = '<div class="aksrt-btnset">';
|
||
foreach ($options as $value => $label) {
|
||
$html .= '<label class="aksrt-btnset-item"><input type="radio" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '"' . checked((string) $current, (string) $value, false) . '><span>' . esc_html((string) $label) . '</span></label>';
|
||
}
|
||
return $html . '</div>';
|
||
}
|
||
|
||
private function spinner(string $name, int $value, int $min, int $max, int $step, string $unit): string {
|
||
return '<input type="number" class="small-text" name="' . esc_attr($name) . '" value="' . esc_attr((string) $value) . '" min="' . esc_attr((string) $min) . '" max="' . esc_attr((string) $max) . '" step="' . esc_attr((string) $step) . '" /> <span class="description">' . esc_html($unit) . '</span>';
|
||
}
|
||
|
||
private function page_notice(string $title, string $msg, string $type = 'success'): void {
|
||
$html = '';
|
||
$html .= $title ? '<h3>' . esc_html($title) . '</h3>' : '';
|
||
$html .= $msg ? '<p>' . esc_html($msg) . '</p>' : '';
|
||
|
||
if ($html) {
|
||
echo '<div class="notice notice-' . esc_attr($type) . '">' . $html . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 列表筛选参数(不含 paged / page),用于构造翻页链接与「每页显示」表单。
|
||
* 显式列条件而不是依赖 add_query_arg() 默认取 $_SERVER['REQUEST_URI']:
|
||
* 后者把链接绑到当前请求上,一旦请求形态变化(POST、代理改写)就会丢条件。
|
||
*
|
||
* @return array<string,string>
|
||
*/
|
||
private function list_query_args(string $status, string $type, string $other, string $s, string $orderby, string $order, int $per_page): array {
|
||
$args = [];
|
||
if ($status !== '') {
|
||
$args['status'] = $status;
|
||
}
|
||
if ($type !== '') {
|
||
$args['type'] = $type;
|
||
}
|
||
if ($other !== '') {
|
||
$args['other'] = $other;
|
||
}
|
||
if ($s !== '') {
|
||
$args['s'] = $s;
|
||
}
|
||
if ($orderby !== '' && $orderby !== 'modified_time') {
|
||
$args['orderby'] = $orderby;
|
||
}
|
||
if ($order === 'ASC') {
|
||
$args['desc'] = 'ASC';
|
||
}
|
||
if ($per_page !== self::DEFAULT_PER_PAGE) {
|
||
$args['ice_perpage'] = (string) $per_page;
|
||
}
|
||
return $args;
|
||
}
|
||
|
||
/**
|
||
* 分页(参考主题 zibpay_admin_pagenavi 同款结构与禁用逻辑)。
|
||
*
|
||
* 与参考实现的两处差异,均为缺陷修复:
|
||
* 1) 页码先夹到 [1, total_pages]。历史写法在此处会出现「第 2 页 / 共 1 页」这类越界状态,
|
||
* 四个分支一个都不命中,上一页/下一页同时停在 '#',按钮既不禁用也点不动。
|
||
* 2) 链接基于显式基准 URL(page_url + 当前筛选条件),不再依赖 REQUEST_URI。
|
||
*
|
||
* @param array<string,string> $query_args 当前筛选参数(不含 paged)。
|
||
*/
|
||
private function render_pagenavi(int $total_count, int $number_per_page, array $query_args = []): void {
|
||
if ($total_count <= 0 || $number_per_page <= 0) {
|
||
return;
|
||
}
|
||
|
||
$total_pages = max(1, (int) ceil($total_count / $number_per_page));
|
||
$current_page = min(max(1, (int) ($_GET['paged'] ?? 1)), $total_pages);
|
||
|
||
$base = $query_args ? add_query_arg($query_args, $this->page_url) : $this->page_url;
|
||
$page_url = static function (int $page) use ($base): string {
|
||
return add_query_arg('paged', $page, $base);
|
||
};
|
||
|
||
$on_first = $current_page <= 1;
|
||
$on_last = $current_page >= $total_pages;
|
||
|
||
$first_page_url = $on_first ? '#' : $page_url(1);
|
||
$prev_page_url = $on_first ? '#' : $page_url($current_page - 1);
|
||
$next_page_url = $on_last ? '#' : $page_url($current_page + 1);
|
||
$last_page_url = $on_last ? '#' : $page_url($total_pages);
|
||
?>
|
||
<div class="tablenav bottom">
|
||
<div class="tablenav-pages">
|
||
<span class="displaying-num"><?php printf(esc_html__('每页 %1$s 共%2$s', 'aksrt'), esc_html((string) $number_per_page), esc_html((string) $total_count)); ?></span>
|
||
<span class="pagination-links">
|
||
<a class="first-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往第一页', 'aksrt'); ?>" href="<?php echo esc_url($first_page_url); ?>">«</a>
|
||
<a class="prev-page button<?php echo $on_first ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往上一页', 'aksrt'); ?>" href="<?php echo esc_url($prev_page_url); ?>">‹</a>
|
||
<span class="paging-input"><?php printf(esc_html__('第 %1$s 页,共 %2$s 页', 'aksrt'), esc_html((string) $current_page), '<span class="total-pages">' . esc_html((string) $total_pages) . '</span>'); ?></span>
|
||
<a class="next-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往下一页', 'aksrt'); ?>" href="<?php echo esc_url($next_page_url); ?>">›</a>
|
||
<a class="last-page button<?php echo $on_last ? ' disabled' : ''; ?>" title="<?php esc_attr_e('前往最后一页', 'aksrt'); ?>" href="<?php echo esc_url($last_page_url); ?>">»</a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<?php
|
||
}
|
||
|
||
/** 行内样式与脚本:依赖显隐 + 点击复制。 */
|
||
private function print_assets(): void {
|
||
?>
|
||
<style>
|
||
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
|
||
.order-header .subsubsub { margin: 6px 0; }
|
||
/* 禁用的翻页按钮已经无法跳转,但 <a href="#"> 仍会把页面滚回顶部;
|
||
这里直接让它们不可点击,避免「点了没反应还跳回页首」的错觉。 */
|
||
.tablenav-pages a.disabled { pointer-events: none; }
|
||
/* 排序箭头对齐:宿主做成零尺寸 inline-block —— 空 inline-block 的基线即其下边缘,
|
||
会与文字基线对齐,于是箭头可用 bottom 精确锚定到「基线以上 N em」。
|
||
箭头盒中心等于 bottom 值(translateY(50%) 抵消自身高度,与盒子高度无关):
|
||
两枚中心分别落在基线以上 .725em / .075em,对称于文字视觉中心(约 .4em)。
|
||
不要改回「无尺寸 inline 宿主 + 依赖静态位置 + 固定 -3px/+6px」的写法:
|
||
静态位置随表头字体/字号变化,会让整组箭头相对文字偏下(历史缺陷,已量测确认偏下 2px)。 */
|
||
.orderby-but { display: inline-block; position: relative; width: 0; height: 0; }
|
||
.orderby-but > a { opacity: .4; position: absolute; left: 0; bottom: .725em; transform: translateY(50%); transition: opacity .3s; padding-right: 1em; }
|
||
.orderby-but > a + a { bottom: .075em; }
|
||
.orderby-but:hover a { opacity: .6; }
|
||
.orderby-but > a:hover { opacity: 1; }
|
||
.aksrt-btnset { display: flex; flex-wrap: wrap; gap: 6px; }
|
||
.aksrt-btnset-item { display: inline-block; cursor: pointer; }
|
||
.aksrt-btnset-item input { position: absolute; opacity: 0; pointer-events: none; }
|
||
.aksrt-btnset-item span { display: inline-block; padding: 3px 12px; border: 1px solid #c3c4c7; border-radius: 3px; background: #fff; }
|
||
.aksrt-btnset-item input:checked + span { background: #2271b1; border-color: #2271b1; color: #fff; }
|
||
/* 导入提示:复刻后台 notice-warning 的外观,但**不用** .notice 类 —— 见 submessage_row()
|
||
的说明,带 .notice 会被 wp-admin/js/common.js 搬到页面顶部。改这里请连带改那里。 */
|
||
.aksrt-submsg { margin: 6px 0; padding: 6px 12px; background: #fef8ee; border-left: 4px solid #f0b849; }
|
||
/* 提示内的条目列表必须有 <ul> 承载:list-style-position 默认 outside,
|
||
圆点落在内容盒左侧的 padding 区里;若 <li> 没有列表容器提供缩进,
|
||
圆点就会溢出到提示块的文字边界之外。这里显式给出缩进,避免依赖 UA/后台的 ul 默认值。 */
|
||
.aksrt-submsg-list { margin: 6px 0; padding-left: 20px; list-style: disc; }
|
||
.aksrt-copy-tip { margin-left: 6px; color: #00a32a; font-size: 12px; white-space: nowrap; }
|
||
</style>
|
||
<script>
|
||
(function () {
|
||
// 按 name 属性逐元素匹配,避免把字段名拼进 CSS 选择器:
|
||
// 含方括号的字段名(如 vip_exchange[time])会让选择器抛异常并被 catch 吞掉,
|
||
// 从而把依赖行永久留在初始状态。
|
||
function fieldsByName(key) {
|
||
var all = document.querySelectorAll('.aksrt-cardpass-form [name]');
|
||
var out = [];
|
||
for (var i = 0; i < all.length; i++) {
|
||
if (all[i].getAttribute('name') === key) out.push(all[i]);
|
||
}
|
||
return out;
|
||
}
|
||
function formVal(key) {
|
||
var els = fieldsByName(key);
|
||
if (!els.length) return '';
|
||
if (els[0].type === 'radio') {
|
||
for (var i = 0; i < els.length; i++) {
|
||
if (els[i].checked) return els[i].value;
|
||
}
|
||
return '';
|
||
}
|
||
if (els[0].type === 'checkbox') return els[0].checked ? '1' : '';
|
||
return els[0].value;
|
||
}
|
||
function evalDeps() {
|
||
// 全文档查找 data-dep 节点:行与提示块各自带状态、各自落 display。
|
||
// 刻意不做限域(不写 .aksrt-cardpass-form 前缀):提示块一旦被 HTML 解析
|
||
// 移出表格甚至移出表单子树,限域查找会让它停在初始隐藏状态,表现为「提示不见了」。
|
||
document.querySelectorAll('[data-dep]').forEach(function (el) {
|
||
try {
|
||
var deps = JSON.parse(el.getAttribute('data-dep'));
|
||
var show = deps.every(function (d) {
|
||
var v = formVal(d.key);
|
||
switch (d.op) {
|
||
case '==': return String(v) === String(d.val);
|
||
case '!=': return String(v) !== String(d.val);
|
||
case 'any': return String(d.val).split(',').indexOf(String(v)) !== -1;
|
||
case 'checked': return !!v;
|
||
case 'unchecked': return !v;
|
||
}
|
||
return true;
|
||
});
|
||
el.style.display = show ? '' : 'none';
|
||
} catch (e) { /* noop */ }
|
||
});
|
||
}
|
||
// 任何 change 都重新求值:不做「是否属于本表单」的限定,
|
||
// 因为提示块的祖先链在这种页面上并不保证还挂在表单内。
|
||
document.addEventListener('change', evalDeps);
|
||
// 唯一的初始求值入口:本脚本在表单之前输出,此刻解析尚未到达表单,
|
||
// 直接求值必然取不到任何行(空操作);因此必须等 DOM 就绪后按默认值校正一次。
|
||
// 服务端已按默认值预隐藏,这里只做兜底校正。
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', evalDeps);
|
||
} else {
|
||
evalDeps();
|
||
}
|
||
document.addEventListener('click', function (e) {
|
||
var el = e.target.closest('[data-clipboard-text]');
|
||
if (!el) return;
|
||
var text = el.getAttribute('data-clipboard-text') || '';
|
||
if (!text) return;
|
||
var done = function () {
|
||
var tip = document.createElement('span');
|
||
tip.className = 'aksrt-copy-tip';
|
||
var tag = el.getAttribute('data-clipboard-tag') || '';
|
||
tip.textContent = (tag ? tag + ' ' : '') + '<?php echo esc_js(__('已复制', 'aksrt')); ?>';
|
||
el.appendChild(tip);
|
||
setTimeout(function () { tip.remove(); }, 1200);
|
||
};
|
||
var legacy = function () {
|
||
var ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
try { document.execCommand('copy'); } catch (err) { /* noop */ }
|
||
document.body.removeChild(ta);
|
||
done();
|
||
};
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(text).then(done, legacy);
|
||
} else {
|
||
legacy();
|
||
}
|
||
});
|
||
})();
|
||
</script>
|
||
<?php
|
||
}
|
||
|
||
/* ------------------------------------------------------------------
|
||
* 数据辅助
|
||
* ------------------------------------------------------------------ */
|
||
|
||
/**
|
||
* 卡密类型(key 即本主题内部类型,与参考主题语义一一对应)。
|
||
*
|
||
* @return array<string,string>
|
||
*/
|
||
private function type_labels(): array {
|
||
return [
|
||
'balance' => __('余额充值', 'aksrt'),
|
||
'vip' => __('会员兑换', 'aksrt'),
|
||
'credits' => __('积分兑换', 'aksrt'),
|
||
'custom' => __('自定义卡密', 'aksrt'),
|
||
];
|
||
}
|
||
|
||
private function vip_level_name(int $level): string {
|
||
$s = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||
$name = trim((string) ($s['pay_user_vip_' . max(1, min(2, $level)) . '_name'] ?? ''));
|
||
if ($name !== '') {
|
||
return $name;
|
||
}
|
||
return $level === 2 ? __('二级会员', 'aksrt') : __('一级会员', 'aksrt');
|
||
}
|
||
|
||
/**
|
||
* VIP 卡展示文本:Permanent 为永久;day 为天;month 为个月。
|
||
*
|
||
* @param array<string,mixed> $meta
|
||
*/
|
||
private function vip_time_text(array $meta): string {
|
||
$time = array_key_exists('time', $meta) ? (string) $meta['time'] : null;
|
||
if ($time !== null && $time !== '') {
|
||
if ($time === 'Permanent') {
|
||
return __('永久', 'aksrt');
|
||
}
|
||
$unit = (isset($meta['unit']) && $meta['unit'] === 'month') ? __('个月', 'aksrt') : __('天', 'aksrt');
|
||
return (string) ((int) $time) . $unit;
|
||
}
|
||
$days = (int) ($meta['days'] ?? 0);
|
||
return $days > 0 ? $days . __('天', 'aksrt') : __('永久', 'aksrt');
|
||
}
|
||
|
||
/**
|
||
* VIP 卡 meta(内部统一保存 level/days + 参考主题的 time/unit)。
|
||
*
|
||
* @param array<string,mixed> $vip
|
||
* @return array<string,mixed>
|
||
*/
|
||
private function vip_meta(array $vip): array {
|
||
$level = max(1, min(2, (int) ($vip['level'] ?? 1)));
|
||
$time_raw = trim((string) ($vip['time'] ?? ''));
|
||
$unit = (isset($vip['unit']) && $vip['unit'] === 'month') ? 'month' : 'day';
|
||
|
||
if ($time_raw === 'Permanent') {
|
||
$days = 0;
|
||
} else {
|
||
$time = (int) $time_raw;
|
||
$days = $unit === 'month' ? $time * 30 : $time;
|
||
}
|
||
|
||
return [
|
||
'level' => $level,
|
||
'days' => max(0, $days),
|
||
'time' => $time_raw,
|
||
'unit' => $unit,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* VIP 卡导出参数串(与参考主题导出格式一致)。
|
||
*
|
||
* @param array<string,mixed> $meta
|
||
*/
|
||
private function vip_meta_query(array $meta): string {
|
||
if (isset($meta['time']) && (string) $meta['time'] !== '') {
|
||
return build_query([
|
||
'level' => (int) ($meta['level'] ?? 1),
|
||
'time' => (string) $meta['time'],
|
||
'unit' => (isset($meta['unit']) && $meta['unit'] === 'month') ? 'month' : 'day',
|
||
]);
|
||
}
|
||
$days = (int) ($meta['days'] ?? 0);
|
||
return build_query([
|
||
'level' => (int) ($meta['level'] ?? 1),
|
||
'time' => $days > 0 ? (string) $days : 'Permanent',
|
||
'unit' => 'day',
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* 余额卡面额展示(参考主题为数值文本,不追加单位)。
|
||
*
|
||
* @param array<string,mixed> $meta
|
||
*/
|
||
private function balance_amount_text(array $meta): string {
|
||
$amount = (string) ($meta['amount'] ?? '');
|
||
if ($amount === '') {
|
||
return '0';
|
||
}
|
||
return (string) (float) $amount;
|
||
}
|
||
|
||
private function currency_unit(): string {
|
||
$s = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
|
||
return trim((string) ($s['pay_currency_unit'] ?? '')) !== '' ? trim((string) $s['pay_currency_unit']) : __('元', 'aksrt');
|
||
}
|
||
|
||
private function ensure_cap(): void {
|
||
if (!current_user_can(self::CAPABILITY)) {
|
||
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
|
||
}
|
||
}
|
||
}
|