From 678143e7b942bdac92e956f1ac5e7c8b5dcf4403 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 16 Sep 2026 22:04:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(payment):=20=E5=8D=A1=E5=AF=86=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=94=B9=E4=B8=BA=E7=8B=AC=E7=AB=8B=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=B9=B6=E5=AF=B9=E9=BD=90=E5=8F=82=E8=80=83=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除「余额充值」设置中的充值卡(card_pass_*)配置组,PHP schema 与 SPA mirror 双轨同步 - 重写后台卡密管理页:列表过滤/排序/搜索/批量删除/同款分页/点击复制、自动生成与导入、text/xls 导出 - ChargeCard 服务新增 generate_cards/rand_number/rand_password/delete_by_ids/get_export_rows,get_list 支持参考主题排序别名与标识过滤 - 删除无消费方的 /payment/charge-cards REST 路由及 batch_create/disable 死代码,数据层零迁移 --- .../src/schema/sections/payment/recharge.js | 51 +- src/Admin/Native/PayChargeCards.php | 1362 +++++++++++++---- src/Admin/PaymentController.php | 351 ----- src/Admin/Schema/SchemaPayment.php | 22 +- src/Payment/ChargeCard.php | 183 ++- src/Settings/CreditsPaymentGroups.php | 9 - src/Settings/DisplayPageGroups.php | 1 - src/Settings/HomeCms.php | 4 - src/Settings/PaymentRuntime.php | 32 +- 9 files changed, 1227 insertions(+), 788 deletions(-) diff --git a/assets/admin/src/schema/sections/payment/recharge.js b/assets/admin/src/schema/sections/payment/recharge.js index 3f3035dc..bac9c1c8 100644 --- a/assets/admin/src/schema/sections/payment/recharge.js +++ b/assets/admin/src/schema/sections/payment/recharge.js @@ -1,12 +1,13 @@ /** * 余额充值 — 参考主题结构:用户余额(含余额提现)、转账、充值商品选项、 - * 卡密充值、充值卡(原「充值卡」页并入)与余额兑换积分。 + * 卡密充值与余额兑换积分。卡密的生成/管理在后台「卡密管理」页完成, + * 此处不再保留「充值卡」配置项。 */ export const rechargeSchema = { key: 'payment.recharge', title: '余额充值', icon: 'fa fas fa-bolt', - description: '配置余额功能、充值商品选项、卡密充值、充值卡规则与余额兑换积分。', + description: '配置余额功能、充值商品选项、卡密充值与余额兑换积分。', defaults: { pay_balance_s: true, pay_balance_withdraw_s: false, @@ -24,20 +25,6 @@ export const rechargeSchema = { pay_balance_pass_charge_s: false, pay_balance_pass_charge_desc: '', pay_balance_pass_charge_only_password: false, - card_pass_enabled: false, - card_pass_desc: '', - card_pass_type: 'balance', - card_pass_prefix: 'AK', - card_pass_length: 16, - card_pass_batch_size: 50, - card_pass_max_redeem: 1, - card_pass_expiry: 0, - card_pass_amount: '10.00', - card_pass_vip_days: 30, - card_pass_credits: 0, - card_pass_redeem_page: '', - card_pass_success_msg: '兑换成功!', - card_pass_fail_msg: '卡号无效或已过期', pay_balance_points_exchange_s: false, pay_balance_points_exchange_rate: 100, pay_balance_points_exchange_min_points: 100, @@ -116,38 +103,6 @@ export const rechargeSchema = { { key: 'pay_balance_pass_charge_only_password', label: '单密码模式', type: 'switch', default: false, dependsOn: { key: 'pay_balance_pass_charge_s', value: true }, help: '仅需输入密码,不需要卡号' }, ], }, - { - key: 'charge-card', - title: '充值卡', - icon: 'fa fas fa-id-card', - fields: [ - { key: 'card_pass_enabled', label: '启用充值卡', type: 'switch', default: false, help: '充值卡规则与批量发卡配置;卡密兑换入口由上方「卡密充值」控制' }, - { key: 'card_pass_desc', label: '充值卡说明', type: 'textarea', default: '', dependsOn: { key: 'card_pass_enabled', value: true } }, - { - key: 'card_pass_type', - label: '充值卡类型', - type: 'select', - default: 'balance', - dependsOn: { key: 'card_pass_enabled', value: true }, - options: [ - { label: '余额卡', value: 'balance' }, - { label: 'VIP卡', value: 'vip' }, - { label: '积分卡', value: 'credits' }, - ], - }, - { key: 'card_pass_prefix', label: '卡号前缀', type: 'input', default: 'AK', dependsOn: { key: 'card_pass_enabled', value: true } }, - { key: 'card_pass_length', label: '卡号长度', type: 'number', default: 16, min: 8, max: 32, dependsOn: { key: 'card_pass_enabled', value: true } }, - { key: 'card_pass_batch_size', label: '批量生成数量', type: 'number', default: 50, min: 1, max: 500, dependsOn: { key: 'card_pass_enabled', value: true } }, - { key: 'card_pass_max_redeem', label: '单卡最大兑换次数', type: 'number', default: 1, min: 1, max: 100, dependsOn: { key: 'card_pass_enabled', value: true } }, - { key: 'card_pass_expiry', label: '有效期(天)', type: 'number', default: 0, min: 0, help: '0为永久有效', dependsOn: { key: 'card_pass_enabled', value: true } }, - { key: 'card_pass_amount', label: '面额(元)', type: 'input', sanitize_type: 'money_decimal', default: '10.00', min: 1, max: 9999, dependsOn: { key: 'card_pass_enabled', value: true } }, - { key: 'card_pass_vip_days', label: 'VIP天数', type: 'number', default: 30, min: 1, max: 3650, dependsOn: { key: 'card_pass_enabled', value: true } }, - { key: 'card_pass_credits', label: '赠送积分', type: 'number', default: 0, min: 0, max: 99999, dependsOn: { key: 'card_pass_enabled', value: true } }, - { key: 'card_pass_redeem_page', label: '兑换页面', type: 'input', default: '', dependsOn: { key: 'card_pass_enabled', value: true } }, - { key: 'card_pass_success_msg', label: '兑换成功提示', type: 'input', default: '兑换成功!', dependsOn: { key: 'card_pass_enabled', value: true } }, - { key: 'card_pass_fail_msg', label: '兑换失败提示', type: 'input', default: '卡号无效或已过期', dependsOn: { key: 'card_pass_enabled', value: true } }, - ], - }, { key: 'points-exchange', title: '余额兑换积分', diff --git a/src/Admin/Native/PayChargeCards.php b/src/Admin/Native/PayChargeCards.php index cb443d7e..1fb3c59e 100644 --- a/src/Admin/Native/PayChargeCards.php +++ b/src/Admin/Native/PayChargeCards.php @@ -1,6 +1,13 @@ ensure_cap(); - $this->render_header(__('卡密管理', 'aksrt')); + $this->page_url = admin_url('admin.php?page=' . self::PAGE_SLUG); if (!class_exists('\AKSRT\Payment\ChargeCard')) { - echo '

' . esc_html__('卡密模块未初始化。', 'aksrt') . '

'; - $this->render_footer(); + echo '

' . esc_html__('卡密管理', 'aksrt') . '

'; + echo '

' . esc_html__('卡密模块未初始化。', 'aksrt') . '

'; return; } - $status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : ''; - $type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : ''; - $search = isset($_GET['s']) ? sanitize_text_field((string) wp_unslash($_GET['s'])) : ''; - $page_num = max(1, (int) ($_GET['paged'] ?? 1)); - $per_page = 20; - $svc = \AKSRT\Payment\ChargeCard::get_instance(); - $stats = $svc->get_stats(); - $type_labels = [ - 'balance' => __('余额', 'aksrt'), - 'vip' => __('VIP', 'aksrt'), - 'credits' => __('积分', 'aksrt'), + $tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : ''; + $page_title = __('卡密管理', 'aksrt'); + $head_but = '' . esc_html__('添加卡密', 'aksrt') . ''; + + // 页面级动作(参考主题在渲染前处理 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(); + } + + switch ($tab) { + case 'add': + $page_title = __('添加卡密', 'aksrt'); + $head_but = '' . esc_html__('返回列表', 'aksrt') . ''; + break; + + case 'export': + $page_title = __('导出卡密', 'aksrt'); + $head_but = '' . esc_html__('返回列表', 'aksrt') . ''; + break; + } + + echo '
'; + $this->print_assets(); + echo '

' . esc_html($page_title) . '

'; + 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(); + } + + echo '
'; + } + + /* ------------------------------------------------------------------ + * 列表 + * ------------------------------------------------------------------ */ + + private function render_list(): void { + $svc = \AKSRT\Payment\ChargeCard::get_instance(); + + $s = isset($_REQUEST['s']) ? sanitize_text_field((string) wp_unslash($_REQUEST['s'])) : ''; + $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'] ?? 30))); + + // 参考主题: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 = ''; + } + + // 标题右侧按钮与过滤链接。 + $head_but = ''; + if ($s !== '') { + $head_but .= '
' . sprintf(esc_html__('搜索 “%s” 的内容', 'aksrt'), esc_html($s)) . '
'; + } else { + $head_but .= '' . esc_html__('添加卡密', 'aksrt') . ''; + $head_but .= '' . esc_html__('导出卡密', 'aksrt') . ''; + } + + $sub_but = [ + ['name' => __('全部', 'aksrt'), 'href' => $this->page_url], ]; - $status_labels = [ - 'active' => __('可用', 'aksrt'), - 'used' => __('已用', 'aksrt'), - 'disabled' => __('已禁用', 'aksrt'), - 'expired' => __('已过期', 'aksrt'), - ]; - - echo '

' . esc_html(sprintf( - /* translators: 1: total 2: active 3: used */ - __('共 %1$d 张 · 可用 %2$d · 已用 %3$d', 'aksrt'), - (int) ($stats['total'] ?? 0), - (int) ($stats['active'] ?? 0), - (int) ($stats['used'] ?? 0) - )) . '

'; - - echo '

' . esc_html__('批量生成卡密', 'aksrt') . '

'; - echo '
'; - echo ''; - wp_nonce_field('aksrt_pay_batch_charge_cards'); - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - echo '

'; - submit_button(__('生成', 'aksrt'), 'primary', '', false); - echo '
'; - - if (!empty($_GET['aksrt_generated'])) { - $generated = (int) $_GET['aksrt_generated']; - echo '

' . esc_html(sprintf( - /* translators: %d count */ - __('成功生成 %d 张卡密。', 'aksrt'), - $generated - )) . '

'; + 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 .= '
  • ' . esc_html($but['name']) . '
  • | '; + } + $but_html = substr($but_html, 0, -2); - $generated_codes = get_transient('aksrt_pay_card_codes_' . get_current_user_id()); - if (is_array($generated_codes) && $generated_codes) { - delete_transient('aksrt_pay_card_codes_' . get_current_user_id()); - echo '

    ' . esc_html__('本次生成卡密(请及时复制保存):', 'aksrt') . '

    '; - echo ''; - echo '
    '; - } + $search = '
    +
    + + +
    +
    '; - echo '
    '; - echo ''; - echo ' '; - echo ' '; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
    '; + echo $head_but; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。 + echo '
    ' . $search . '
    '; $result = $svc->get_list([ - 'status' => $status, - 'type' => $type, - 'search' => $search, - 'limit' => $per_page, - 'offset' => ($page_num - 1) * $per_page, + 'status' => $status, + 'type' => $type, + 'other' => $other, + 'search' => $s, + 'orderby' => $orderby, + 'order' => $order, + 'limit' => $per_page, + 'offset' => ($paged - 1) * $per_page, ]); - $items = $result['items'] ?? []; - $total = (int) ($result['total'] ?? 0); - $pages = max(1, (int) ($result['max_pages'] ?? 1)); - - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; + $items = (array) ($result['items'] ?? []); + $count_all = (int) ($result['total'] ?? 0); if (!$items) { - echo ''; + echo '
    '; + echo '
    ID' . esc_html__('类型', 'aksrt') . '' . esc_html__('密码', 'aksrt') . '' . esc_html__('面额', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('过期', 'aksrt') . '' . esc_html__('使用者', 'aksrt') . '' . esc_html__('创建时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
    ' . esc_html__('暂无卡密', 'aksrt') . '
    '; + echo ''; + echo '
    ' . esc_html__('未找到对应内容,或暂无内容', 'aksrt') . '
    '; + return; } - foreach ($items as $row) { - $id = (int) ($row['id'] ?? 0); - $st = (string) ($row['status'] ?? ''); - $row_type = (string) ($row['type'] ?? ''); - $user_id = (int) ($row['used_by'] ?? $row['user_id'] ?? 0); - $user = $user_id > 0 ? get_userdata($user_id) : null; + // 表头(与参考主题同列、同排序链接)。 + $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')], + ]; - echo ''; - echo '' . esc_html((string) $id) . ''; - echo '' . esc_html($type_labels[$row_type] ?? $row_type) . ''; - echo '' . esc_html((string) ($row['password'] ?? '')) . ''; - echo '' . esc_html((string) ($row['amount'] ?? '')) . ''; - echo '' . esc_html($status_labels[$st] ?? $st) . ''; - echo '' . esc_html((string) ($row['expires_at'] ?? '—')) . ''; - echo '' . ($user ? esc_html($user->display_name) : '—') . ''; - echo '' . esc_html((string) ($row['created_at'] ?? '')) . ''; - echo ''; - if ($st === 'active') { - echo '
    '; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_disable_charge_card_' . $id); - submit_button(__('禁用', 'aksrt'), 'small', '', false); - echo '
    '; + $thead_th = ''; + foreach ($theads as $thead) { + $orderby_links = ''; + if ($thead['orderby']) { + $orderby_url = add_query_arg('orderby', $thead['orderby']); + $orderby_links .= ''; + $orderby_links .= ''; + $orderby_links = '' . $orderby_links . ''; + } + $thead_th .= '' . esc_html($thead['name']) . $orderby_links . ''; + } + + $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') { + $type_html = '' . esc_html__('自定义卡密', 'aksrt') . ''; } else { - echo '—'; + $status_html = '' . esc_html__('未使用', 'aksrt') . ''; + if ($msg['status'] === 'used') { + $status_html = '' . esc_html__('已使用', 'aksrt') . ''; + if (!empty($msg['order_number'])) { + $order_link_url = admin_url('admin.php?page=aksrt_orders&s=' . urlencode((string) $msg['order_number'])); + $status_html .= ' [' . esc_html__('查看', 'aksrt') . ']'; + } + } elseif ($msg['status'] === 'expired') { + $status_html = '' . esc_html__('已过期', 'aksrt') . ''; + } elseif ($msg['status'] === 'disabled') { + $status_html = '' . esc_html__('已禁用', 'aksrt') . ''; + } } - echo ''; - echo ''; + + $other_a = ''; + if (!empty($msg['note'])) { + $other_a = '' . esc_html($msg['note']) . ''; + } + + if ($msg['type'] === 'vip') { + $type_html = '' . esc_html__('会员兑换', 'aksrt') . ''; + $level = (int) ($meta['level'] ?? 1); + $type_html .= '
    ' . esc_html($this->vip_level_name($level)) . ':' . esc_html($this->vip_time_text($meta)); + } elseif ($msg['type'] === 'credits') { + $type_html = '' . esc_html__('积分兑换', 'aksrt') . ''; + $type_html .= '
    ' . esc_html__('积分:', 'aksrt') . esc_html((string) (int) ($meta['points'] ?? 0)); + } elseif ($msg['type'] === 'balance') { + $type_html = '' . esc_html__('余额充值', 'aksrt') . ''; + $type_html .= '
    ' . esc_html__('金额:', 'aksrt') . esc_html($this->balance_amount_text($meta)); + } else { + $type_html = '' . esc_html__('自定义卡密', 'aksrt') . ''; + } + + $card_name = (string) ($meta['custom_name']['card_name'] ?? ''); + $password_name = (string) ($meta['custom_name']['password_name'] ?? ''); + + $tbody .= ''; + $tbody .= ' + + '; + $tbody .= "" . esc_html($card_name) . ' ' . esc_html((string) $msg['card']) . ''; + $tbody .= "" . esc_html($password_name) . ' ' . esc_html((string) $msg['password']) . ''; + $tbody .= "$type_html"; + $tbody .= '' . esc_html((string) $msg['created_at']) . ''; + $tbody .= '' . esc_html((string) ($msg['used_at'] ?: $msg['created_at'])) . ''; + $tbody .= "$status_html"; + $tbody .= "$other_a"; + $tbody .= ''; } - echo ''; + echo '
    '; + echo '
    '; + wp_nonce_field(self::NONCE); + echo '
    + + +
    '; - if ($pages > 1) { - echo '
    '; - echo paginate_links([ - 'base' => add_query_arg('paged', '%#%'), - 'format' => '', - 'prev_text' => '«', - 'next_text' => '»', - 'total' => $pages, - 'current' => $page_num, + echo '
    '; + echo ''; + echo '' . $thead_th . ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。 + echo '' . $tbody . ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。 + echo '
    '; + echo '
    '; + echo ''; + echo '
    '; + + $this->render_pagenavi($count_all, $per_page); + } + + /* ------------------------------------------------------------------ + * 添加卡密 + * ------------------------------------------------------------------ */ + + private function render_add_form(): void { + $type_labels = $this->type_labels(); + ?> +

    +
      +
    • +
    • +
    +
    + + + + 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'), + '', + [['key' => 'add_type', 'op' => '==', 'val' => 'auto']], + __('对生成的卡密做标记标识,方便后期查找管理', 'aksrt') . '
    ' . sprintf(esc_html__('注意:标识中不能有%1$s字符,系统已占用', 'aksrt'), 'shipped') + ); + $this->field_row( + __('面额', 'aksrt'), + '', + [ + ['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') . '
    ', + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], + ['key' => 'type', 'op' => '==', 'val' => 'vip'], + ], + __('填', 'aksrt') . 'Permanent' . __('为永久', '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') . '

    ' . + __('卡密显示名称', 'aksrt') . '
    ', + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], + ['key' => 'type', 'op' => '==', 'val' => 'custom'], + ] + ); + $this->field_row( + __('高级选项', 'aksrt'), + '', + [['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->submessage_row( + [ + ['key' => 'add_type', 'op' => '!=', 'val' => 'auto'], + ['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'], + ], + '

    ' . esc_html__('导入卡密', 'aksrt') . '

    +
  • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('卡号 密码 面额 标识', 'aksrt') . '
  • +
  • ' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
  • +
  • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
  • ' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '
  • +

    ' . esc_html__('数据示例', 'aksrt') . '

    +
    4849977843617 3w20V4Qe7dRhAIoPYum6KSA 50 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('50' . $this->currency_unit())) . '
    +
    6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq 100 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('100' . $this->currency_unit())) . '
    ' + ); + $this->submessage_row( + [ + ['key' => 'add_type', 'op' => '!=', 'val' => 'auto'], + ['key' => 'type', 'op' => '==', 'val' => 'vip'], + ], + '

    ' . esc_html__('导入会员兑换卡密', 'aksrt') . '

    +
  • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('卡号 密码 会员参数 标识', 'aksrt') . '
  • +
  • ' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
  • +
  • ' . esc_html__('会员参数格式为:', 'aksrt') . 'level=1&time=5&unit=day
  • +
  • ' . esc_html__('参数说明:level为等级,只能为1或2。time为时长,填Permanent或数字。unit为时长单位,只能填day(天)或者month(月)', 'aksrt') . '
  • +
  • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
  • ' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '
  • +

    ' . esc_html__('数据示例', 'aksrt') . '

    +
    4849977843617 3w20V4Qe7dRhAIoPYum6KSA level=1&time=5&unit=day ' . esc_html__('5天一级会员', 'aksrt') . '
    +
    6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq level=2&time=Permanent ' . esc_html__('永久2级会员', 'aksrt') . '
    + ' + ); + $this->field_row( + __('单密码模式', 'aksrt'), + '', + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'import'], + ['key' => 'type', 'op' => '==', 'val' => 'custom'], + ] + ); + $this->submessage_row( + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'import'], + ['key' => 'type', 'op' => '==', 'val' => 'custom'], + ['key' => 'only_pass_s', 'op' => 'unchecked'], + ], + '

    ' . esc_html__('自定义卡密导入', 'aksrt') . '

    +
  • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('卡号 密码 标识 自定义参数', 'aksrt') . '
  • +
  • ' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
  • +
  • ' . esc_html__('自定义参数格式为:', 'aksrt') . 'card_name=授权号&password_name=授权密码,' . esc_html__('card_name为卡号的显示名称,password_name为卡密的显示名称,可以留空', 'aksrt') . '
  • +
  • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
  • +

    ' . esc_html__('数据示例', 'aksrt') . '

    +
    4849977843617 3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '
    +
    6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '
    +
    111111@qq.com 11assssddsds ' . esc_html__('某某账号', 'aksrt') . ' card_name=' . esc_html__('账号', 'aksrt') . '&password_name=' . esc_html__('密码', 'aksrt') . '
    ' + ); + $this->submessage_row( + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'import'], + ['key' => 'type', 'op' => '==', 'val' => 'custom'], + ['key' => 'only_pass_s', 'op' => 'checked'], + ], + '

    ' . esc_html__('自定义卡密导入(单密码模式)', 'aksrt') . '

    +
  • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('密码 标识 自定义参数', 'aksrt') . '
  • +
  • ' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
  • +
  • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
  • +
  • ' . esc_html__('自定义参数格式为:', 'aksrt') . 'password_name=授权密码,' . esc_html__('password_name为卡密的显示名称,可以留空', 'aksrt') . '
  • +

    ' . esc_html__('数据示例', 'aksrt') . '

    +
    3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '
    +
    SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '
    ' + ); + $this->field_row( + __('卡密数据', 'aksrt'), + '', + [['key' => 'add_type', 'op' => '==', 'val' => 'import']] + ); + $this->field_row( + __('自定义分隔符号', 'aksrt'), + '', + [['key' => 'add_type', 'op' => '==', 'val' => 'import']], + __('卡号和密码之间分割符号(默认为空格分割)', 'aksrt') + ); + ?> + + +
    + +
    + + + 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') . '
    ', + [['key' => 'export_format', 'op' => '==', 'val' => 'text']], + __('卡号和密码之间分割符号(默认为空格分割)', 'aksrt') + ); + ?> + + +
    + 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> $data + * @param array $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 $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)) . '"' : ''; + echo '' . esc_html($title) . '' . $html; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。 + if ($desc !== '') { + echo '

    ' . wp_kses_post($desc) . '

    '; + } + echo ''; + } + + /** + * @param array $deps + */ + private function submessage_row(array $deps, string $content): void { + echo '
    ' . $content . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。 + } + + /** + * button_set:radio 按钮组(参考主题 CSF button_set 的原生替代)。 + * + * @param array $options + */ + private function button_set(string $name, array $options, string $current): string { + $html = '
    '; + foreach ($options as $value => $label) { + $html .= ''; + } + return $html . '
    '; + } + + private function spinner(string $name, int $value, int $min, int $max, int $step, string $unit): string { + return ' ' . esc_html($unit) . ''; + } + + private function page_notice(string $title, string $msg, string $type = 'success'): void { + $html = ''; + $html .= $title ? '

    ' . esc_html($title) . '

    ' : ''; + $html .= $msg ? '

    ' . esc_html($msg) . '

    ' : ''; + + if ($html) { + echo '
    ' . $html . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput + } + } + + /** + * 分页(参考主题 zibpay_admin_pagenavi 同款结构与禁用逻辑)。 + */ + private function render_pagenavi(int $total_count, int $number_per_page = 15): void { + if ($total_count <= 0 || $number_per_page <= 0) { + return; + } + + $current_page = max(1, (int) ($_GET['paged'] ?? 1)); + $total_pages = (int) ceil($total_count / $number_per_page); + + $first_page_url = add_query_arg('paged', 1); + $last_page_url = add_query_arg('paged', $total_pages); + $prev_page_url = '#'; + $next_page_url = '#'; + + if ($current_page > 1 && $current_page < $total_pages) { + $prev_page_url = add_query_arg('paged', $current_page - 1); + $next_page_url = add_query_arg('paged', $current_page + 1); + } elseif (1 == $current_page) { + $first_page_url = '#'; + if ($total_pages > 1) { + $next_page_url = add_query_arg('paged', $current_page + 1); + } else { + $next_page_url = '#'; + $last_page_url = '#'; + } + } elseif ($current_page == $total_pages) { + $prev_page_url = add_query_arg('paged', $current_page - 1); + $next_page_url = '#'; + $last_page_url = '#'; + } + ?> +
    +
    + + + « + ‹ + ' . esc_html((string) $total_pages) . ''); ?> + › + » + +
    +
    + + + + + */ + 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 $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 $vip + * @return array + */ + 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 $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', ]); - echo '
    '; } - - $this->render_footer(); + $days = (int) ($meta['days'] ?? 0); + return build_query([ + 'level' => (int) ($meta['level'] ?? 1), + 'time' => $days > 0 ? (string) $days : 'Permanent', + 'unit' => 'day', + ]); } - public function handle_disable_charge_card(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); + /** + * 余额卡面额展示(参考主题为数值文本,不追加单位)。 + * + * @param array $meta + */ + private function balance_amount_text(array $meta): string { + $amount = (string) ($meta['amount'] ?? ''); + if ($amount === '') { + return '0'; } - - $id = isset($_POST['card_id']) ? (int) $_POST['card_id'] : 0; - check_admin_referer('aksrt_pay_disable_charge_card_' . $id); - - $ok = false; - if ($id > 0 && class_exists('\AKSRT\Payment\ChargeCard')) { - $ok = \AKSRT\Payment\ChargeCard::get_instance()->disable($id); - } - - wp_safe_redirect($this->page_url('aksrt_charge_cards', [ - 'aksrt_pay_msg' => $ok ? 'card_disabled' : 'card_failed', - ])); - exit; + return (string) (float) $amount; } - public function handle_batch_charge_cards(): void { - if (!current_user_can(self::CAPABILITY)) { - wp_die(esc_html__('权限不足', 'aksrt')); - } - check_admin_referer('aksrt_pay_batch_charge_cards'); - - if (!class_exists('\AKSRT\Payment\ChargeCard')) { - wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed'])); - exit; - } - - $type = isset($_POST['type']) ? sanitize_key((string) wp_unslash($_POST['type'])) : 'balance'; - $count = isset($_POST['count']) ? (int) $_POST['count'] : 10; - $length = isset($_POST['length']) ? (int) $_POST['length'] : 16; - $prefix = isset($_POST['prefix']) ? sanitize_text_field((string) wp_unslash($_POST['prefix'])) : ''; - $amount = isset($_POST['amount']) ? trim(sanitize_text_field((string) wp_unslash($_POST['amount']))) : ''; - $note = isset($_POST['note']) ? sanitize_textarea_field((string) wp_unslash($_POST['note'])) : ''; - $level = isset($_POST['vip_level']) ? max(1, min(2, (int) $_POST['vip_level'])) : 1; - $days = isset($_POST['days']) ? max(1, (int) $_POST['days']) : 30; - $expires = isset($_POST['expires_at']) ? sanitize_text_field((string) wp_unslash($_POST['expires_at'])) : ''; - if ($expires !== '') { - $expires = str_replace('T', ' ', $expires); - if (strlen($expires) === 16) { - $expires .= ':00'; - } - } - - if (!in_array($type, ['balance', 'vip', 'credits'], true)) { - wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed'])); - exit; - } - - $meta = []; - if ($type === 'balance') { - try { - $balance_amount = \AKSRT\Ledger\AssetAmount::balance($amount); - } catch (\InvalidArgumentException $e) { - wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed'])); - exit; - } - if ($balance_amount->minor() <= 0) { - wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed'])); - exit; - } - $meta['amount'] = $balance_amount->decimal(); - } elseif ($type === 'credits') { - if (!preg_match('/^[1-9]\d*$/', $amount)) { - wp_safe_redirect($this->page_url('aksrt_charge_cards', ['aksrt_pay_msg' => 'card_batch_failed'])); - exit; - } - $meta['points'] = (int) $amount; - } elseif ($type === 'vip') { - $meta['level'] = $level; - $meta['days'] = $days; - } - - $codes = \AKSRT\Payment\ChargeCard::get_instance()->batch_create($type, $meta, $count, $length, $prefix, $expires, $note); - $ok = !empty($codes); - if ($ok) { - set_transient('aksrt_pay_card_codes_' . get_current_user_id(), $codes, 10 * MINUTE_IN_SECONDS); - } - - wp_safe_redirect($this->page_url('aksrt_charge_cards', [ - 'aksrt_pay_msg' => $ok ? 'card_batch_ok' : 'card_batch_failed', - 'aksrt_generated' => count($codes), - ])); - exit; + 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')); } } - - private function render_header(string $title): void { - echo '
    '; - echo '

    ' . esc_html($title) . '

    '; - $this->render_flash(); - } - - private function render_footer(): void { - echo '
    '; - } - - private function render_flash(): void { - if (empty($_GET['aksrt_pay_msg'])) { - return; - } - $code = sanitize_key((string) wp_unslash($_GET['aksrt_pay_msg'])); - $map = [ - 'card_disabled' => __('充值卡已禁用。', 'aksrt'), - 'card_failed' => __('充值卡禁用失败。', 'aksrt'), - 'card_batch_ok' => __('卡密批量生成完成。', 'aksrt'), - 'card_batch_failed' => __('卡密批量生成失败。', 'aksrt'), - ]; - if (!isset($map[$code])) { - return; - } - $class = str_contains($code, 'failed') ? 'notice-error' : 'notice-success'; - echo '

    ' . esc_html($map[$code]) . '

    '; - } - - private function page_url(string $slug, array $args = []): string { - return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); - } - - private function render_pagination(int $pages, int $page_num): void { - if ($pages <= 1) { - return; - } - echo '
    '; - echo paginate_links([ - 'base' => add_query_arg('paged', '%#%'), - 'format' => '', - 'prev_text' => '«', - 'next_text' => '»', - 'total' => $pages, - 'current' => $page_num, - ]); - echo '
    '; - } } diff --git a/src/Admin/PaymentController.php b/src/Admin/PaymentController.php index 5efc2813..03593518 100644 --- a/src/Admin/PaymentController.php +++ b/src/Admin/PaymentController.php @@ -41,74 +41,6 @@ class PaymentController extends ControllerBase { 'note' => ['type' => 'string', 'default' => '', 'maxLength' => 1000, 'sanitize_callback' => 'sanitize_textarea_field'], ], ]); - - // ── Charge Card ── - - // Charge card list - register_rest_route(self::API_NAMESPACE, '/payment/charge-cards', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_charge_cards'], - 'permission_callback' => [$this, 'check_permission'], - 'args' => [ - 'page' => ['type' => 'integer', 'minimum' => 1, 'default' => 1], - 'per_page' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'default' => 20], - 'status' => ['type' => 'string', 'enum' => ['', 'active', 'used', 'expired', 'disabled']], - 'keyword' => ['type' => 'string', 'maxLength' => 191, 'sanitize_callback' => 'sanitize_text_field'], - 'date_from' => ['type' => 'string', 'format' => 'date'], - 'date_to' => ['type' => 'string', 'format' => 'date'], - ], - ]); - register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P\d+)', [ - 'methods' => 'GET', - 'callback' => [$this, 'get_charge_card_detail'], - 'permission_callback' => [$this, 'check_permission'], - 'args' => ['id' => ['type' => 'integer', 'minimum' => 1, 'required' => true]], - ]); - - // Charge card create - register_rest_route(self::API_NAMESPACE, '/payment/charge-cards', [ - 'methods' => 'POST', - 'callback' => [$this, 'create_charge_card'], - 'permission_callback' => [$this, 'check_permission'], - 'args' => [ - 'type' => ['type' => 'string', 'required' => true, 'enum' => ['balance', 'vip', 'credits']], - 'password' => ['type' => 'string', 'required' => true, 'minLength' => 1, 'maxLength' => 80, 'sanitize_callback' => 'sanitize_text_field'], - 'amount' => ['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'], - 'points' => ['type' => 'integer', 'minimum' => 1], - 'level' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 2], - 'days' => ['type' => 'integer', 'minimum' => 0], - 'expires_at' => ['type' => 'string', 'maxLength' => 32, 'sanitize_callback' => 'sanitize_text_field'], - 'note' => ['type' => 'string', 'maxLength' => 1000, 'sanitize_callback' => 'sanitize_textarea_field'], - ], - ]); - - // Charge card batch create - register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/batch', [ - 'methods' => 'POST', - 'callback' => [$this, 'batch_create_charge_cards'], - 'permission_callback' => [$this, 'check_permission'], - 'args' => [ - 'type' => ['type' => 'string', 'required' => true, 'enum' => ['balance', 'vip', 'credits']], - 'count' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 100, 'required' => true], - 'amount' => ['type' => 'string', 'pattern' => '^(0|[1-9]\\d*)(?:\\.\\d{1,2})?$'], - 'points' => ['type' => 'integer', 'minimum' => 1], - 'level' => ['type' => 'integer', 'minimum' => 1, 'maximum' => 2], - 'days' => ['type' => 'integer', 'minimum' => 0], - 'prefix' => ['type' => 'string', 'maxLength' => 20, 'sanitize_callback' => 'sanitize_text_field'], - 'length' => ['type' => 'integer', 'minimum' => 8, 'maximum' => 32, 'default' => 16], - 'expires_at' => ['type' => 'string', 'maxLength' => 32, 'sanitize_callback' => 'sanitize_text_field'], - 'note' => ['type' => 'string', 'maxLength' => 1000, 'sanitize_callback' => 'sanitize_textarea_field'], - ], - ]); - - // Charge card disable - register_rest_route(self::API_NAMESPACE, '/payment/charge-cards/(?P\d+)/disable', [ - 'methods' => 'POST', - 'callback' => [$this, 'disable_charge_card'], - 'permission_callback' => [$this, 'check_permission'], - 'args' => ['id' => ['type' => 'integer', 'minimum' => 1, 'required' => true]], - ]); - } public function get_withdrawals(\WP_REST_Request $request): \WP_REST_Response { @@ -178,287 +110,4 @@ class PaymentController extends ControllerBase { return $this->error(__('更新失败', 'aksrt')); } - /* ------------------------------------------------------------------ - * Charge Card - * ----------------------------------------------------------------*/ - - /** - * GET /payment/charge-cards — 充值卡列表 - */ - public function get_charge_cards(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\ChargeCard')) { - return $this->success(['data' => ['items' => [], 'total' => 0, 'page' => 1, 'max_pages' => 1]]); - } - - global $wpdb; - - $table = \AKSRT\Core\DBMigrator::table('charge_card'); - $page = max(1, (int) ($request->get_param('page') ?: 1)); - $per_page = max(1, min(100, (int) ($request->get_param('per_page') ?: 20))); - $status = sanitize_key((string) $request->get_param('status')); - $keyword = trim(sanitize_text_field((string) $request->get_param('keyword'))); - $date_from = sanitize_text_field((string) $request->get_param('date_from')); - $date_to = sanitize_text_field((string) $request->get_param('date_to')); - - $where = []; - $params = []; - - if ($status !== '') { - $where[] = 'status = %s'; - $params[] = $status; - } - if ($date_from !== '') { - $where[] = 'created_at >= %s'; - $params[] = $date_from . ' 00:00:00'; - } - if ($date_to !== '') { - $where[] = 'created_at <= %s'; - $params[] = $date_to . ' 23:59:59'; - } - - $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; - $sql = "SELECT * FROM {$table} {$where_sql} ORDER BY created_at DESC LIMIT 500"; - $rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A); - - $items = []; - foreach ((array) $rows as $row) { - $item = $this->build_charge_card_admin_item($row); - if ($keyword !== '' && mb_stripos(implode(' ', array_filter([ - (string) ($item['password'] ?? ''), - (string) ($item['card'] ?? ''), - (string) ($item['type_label'] ?? ''), - (string) ($item['note'] ?? ''), - (string) ($item['used_by_user']['display_name'] ?? ''), - (string) ($item['used_by_user']['email'] ?? ''), - ])), $keyword) === false) { - continue; - } - $items[] = $item; - } - - $total = count($items); - $offset = ($page - 1) * $per_page; - $stats = \AKSRT\Payment\ChargeCard::get_instance()->get_stats(); - - return $this->success([ - 'data' => [ - 'items' => array_values(array_slice($items, $offset, $per_page)), - 'total' => $total, - 'page' => $page, - 'per_page' => $per_page, - 'max_pages' => max(1, (int) ceil(max(1, $total) / $per_page)), - 'stats' => $stats, - 'options' => [ - 'statuses' => [ - ['label' => __('全部状态', 'aksrt'), 'value' => ''], - ['label' => __('未使用', 'aksrt'), 'value' => 'active'], - ['label' => __('已使用', 'aksrt'), 'value' => 'used'], - ['label' => __('已过期', 'aksrt'), 'value' => 'expired'], - ['label' => __('已禁用', 'aksrt'), 'value' => 'disabled'], - ], - ], - ], - ]); - } - - /** - * POST /payment/charge-cards — 创建充值卡 - */ - public function create_charge_card(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\ChargeCard')) { - return $this->error('充值卡系统未启用', 500); - } - - $params = $this->get_json_params($request); - $type = sanitize_key((string) ($params['type'] ?? 'balance')); - if (!in_array($type, ['balance', 'vip', 'credits'], true)) { - $type = 'balance'; - } - $password = sanitize_text_field((string) ($params['password'] ?? '')); - if ($password === '') { - return $this->error('卡密不能为空'); - } - - $id = \AKSRT\Payment\ChargeCard::get_instance()->create(array_merge($params, [ - 'type' => $type, - 'password' => $password, - ])); - - if ($id > 0) { - return $this->success([ - 'message' => __('充值卡已创建', 'aksrt'), - 'data' => ['id' => $id], - ], 201); - } - - return $this->error(__('创建失败(可能卡密已存在或参数无效)', 'aksrt')); - } - - /** - * POST /payment/charge-cards/batch — 批量生成充值卡 - */ - public function batch_create_charge_cards(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\ChargeCard')) { - return $this->error('充值卡系统未启用', 500); - } - - $params = $this->get_json_params($request); - $type = sanitize_key((string) ($params['type'] ?? 'balance')); - if (!in_array($type, ['balance', 'vip', 'credits'], true)) { - $type = 'balance'; - } - $count = max(1, min(100, (int) ($params['count'] ?? 1))); - $length = max(8, min(32, (int) ($params['length'] ?? 16))); - $prefix = sanitize_text_field((string) ($params['prefix'] ?? '')); - $note = sanitize_textarea_field((string) ($params['note'] ?? '')); - $expires = sanitize_text_field((string) ($params['expires_at'] ?? '')); - - $raw_amount = $params['amount'] ?? ''; - $meta_fields = [ - 'amount' => is_string($raw_amount) ? $raw_amount : '', - 'level' => (int) ($params['level'] ?? 1), - 'days' => (int) ($params['days'] ?? 30), - 'points' => (int) ($params['points'] ?? 0), - ]; - - if ($type === 'balance') { - try { - $card_amount = \AKSRT\Ledger\AssetAmount::balance($meta_fields['amount']); - } catch (\InvalidArgumentException $e) { - return $this->error('余额卡金额格式无效'); - } - if ($card_amount->minor() <= 0) return $this->error('余额卡金额必须大于 0'); - $meta_fields['amount'] = $card_amount->decimal(); - } - if ($type === 'credits' && $meta_fields['points'] <= 0) { - return $this->error('积分卡积分必须大于 0'); - } - - $codes = \AKSRT\Payment\ChargeCard::get_instance()->batch_create( - $type, - $meta_fields, - $count, - $length, - $prefix, - $expires, - $note - ); - - return $this->success([ - 'message' => sprintf(__('已生成 %d 张充值卡', 'aksrt'), count($codes)), - 'data' => ['codes' => $codes, 'count' => count($codes), 'type' => $type], - ]); - } - - /** - * POST /payment/charge-cards/{id}/disable — 禁用充值卡 - */ - public function disable_charge_card(\WP_REST_Request $request): \WP_REST_Response { - if (!class_exists('\AKSRT\Payment\ChargeCard')) { - return $this->error('充值卡系统未启用', 500); - } - - $id = (int) $request->get_param('id'); - if ($id <= 0) { - return $this->error('参数无效'); - } - - $success = \AKSRT\Payment\ChargeCard::get_instance()->disable($id); - - if ($success) { - return $this->success(['message' => __('充值卡已禁用', 'aksrt')]); - } - return $this->error(__('操作失败', 'aksrt')); - } - - public function get_charge_card_detail(\WP_REST_Request $request): \WP_REST_Response { - global $wpdb; - - $id = (int) $request->get_param('id'); - $table = \AKSRT\Core\DBMigrator::table('charge_card'); - - if ($id <= 0 || !$table) { - return $this->error(__('充值卡不存在', 'aksrt'), 404); - } - - $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$table} WHERE id = %d", $id), ARRAY_A); - if (!$row) { - return $this->error(__('充值卡不存在', 'aksrt'), 404); - } - - return $this->success([ - 'data' => [ - 'card' => $this->build_charge_card_admin_item($row), - ], - ]); - } - - private function build_charge_card_admin_item(array $row): array { - $used_by = (int) ($row['used_by'] ?? 0); - $user = $used_by > 0 ? get_userdata($used_by) : null; - $status = sanitize_key((string) ($row['status'] ?? 'active')); - $type = sanitize_key((string) ($row['type'] ?? 'balance')); - $statusMap = [ - 'active' => __('未使用', 'aksrt'), - 'used' => __('已使用', 'aksrt'), - 'expired' => __('已过期', 'aksrt'), - 'disabled' => __('已禁用', 'aksrt'), - ]; - $typeMap = [ - 'balance' => __('余额卡', 'aksrt'), - 'vip' => __('会员卡', 'aksrt'), - 'credits' => __('积分卡', 'aksrt'), - ]; - - $meta = []; - if (!empty($row['meta'])) { - if (is_array($row['meta'])) { - $meta = $row['meta']; - } else { - $decoded = json_decode((string) $row['meta'], true); - if (is_array($decoded)) { - $meta = $decoded; - } - } - } - - $password = (string) ($row['password'] ?? ''); - $card_no = (string) ($row['card'] ?? ''); - - return [ - 'id' => (int) ($row['id'] ?? 0), - 'password' => $password, - 'card' => $card_no, - 'type' => $type, - 'type_label' => $typeMap[$type] ?? $type, - 'amount' => $this->cash_decimal((string) ($meta['amount'] ?? $row['amount'] ?? '0')), - 'balance' => $this->cash_decimal((string) ($row['balance'] ?? '0')), - 'meta' => $meta, - 'level' => (int) ($meta['level'] ?? 0), - 'days' => (int) ($meta['days'] ?? 0), - 'points' => (int) ($meta['points'] ?? 0), - 'order_number' => (string) ($row['order_number'] ?? ''), - 'status' => $status, - 'status_label' => $statusMap[$status] ?? $status, - 'used_by' => $used_by, - 'used_at' => (string) ($row['used_at'] ?? ''), - 'expires_at' => (string) ($row['expires_at'] ?? ''), - 'created_at' => (string) ($row['created_at'] ?? ''), - 'note' => (string) ($row['note'] ?? ''), - 'used_by_user' => $user instanceof \WP_User ? [ - 'id' => (int) $user->ID, - 'display_name' => $user->display_name, - 'email' => $user->user_email, - ] : null, - ]; - } - - private function cash_decimal(string $value): string { - try { - return \AKSRT\Ledger\AssetAmount::balance($value)->decimal(); - } catch (\InvalidArgumentException $e) { - return '0.00'; - } - } - } diff --git a/src/Admin/Schema/SchemaPayment.php b/src/Admin/Schema/SchemaPayment.php index 7d1504c4..41aa2750 100644 --- a/src/Admin/Schema/SchemaPayment.php +++ b/src/Admin/Schema/SchemaPayment.php @@ -124,7 +124,7 @@ class SchemaPayment { 'recharge' => [ 'key' => 'recharge', 'title' => '余额充值', - 'version' => '1.1.0', + 'version' => '1.2.0', 'option_key' => 'aksrt_settings', 'groups' => [ ['key' => 'notice', 'title' => '说明', 'fields' => [ @@ -159,26 +159,6 @@ class SchemaPayment { ['key' => 'pay_balance_pass_charge_desc', 'label' => '卡密充值说明', 'type' => 'textarea', 'default' => '', 'rows' => 2, 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true], 'help' => '用户使用卡密充值时显示的内容,建议为提醒事项、充值协议等,支持HTML代码'], ['key' => 'pay_balance_pass_charge_only_password', 'label' => '单密码模式', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true], 'help' => '仅需输入密码,不需要卡号'], ]], - ['key' => 'charge-card', 'title' => '充值卡', 'fields' => [ - ['key' => 'card_pass_enabled', 'label' => '启用充值卡', 'type' => 'switch', 'default' => false, 'help' => '充值卡规则与批量发卡配置;卡密兑换入口由上方「卡密充值」控制'], - ['key' => 'card_pass_desc', 'label' => '充值卡说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_type', 'label' => '充值卡类型', 'type' => 'select', 'default' => 'balance', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true], 'options' => [ - ['label' => '余额卡', 'value' => 'balance'], - ['label' => 'VIP卡', 'value' => 'vip'], - ['label' => '积分卡', 'value' => 'credits'], - ]], - ['key' => 'card_pass_prefix', 'label' => '卡号前缀', 'type' => 'input', 'default' => 'AK', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_length', 'label' => '卡号长度', 'type' => 'number', 'default' => 16, 'min' => 8, 'max' => 32, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_batch_size', 'label' => '批量生成数量', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 500, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_max_redeem', 'label' => '单卡最大兑换次数', 'type' => 'number', 'default' => 1, 'min' => 1, 'max' => 100, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_expiry', 'label' => '有效期(天)', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为永久有效', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_amount', 'label' => '面额(元)', 'type' => 'input', 'sanitize_type' => 'money_decimal', 'default' => '10.00', 'min' => 1, 'max' => 9999, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_vip_days', 'label' => 'VIP天数', 'type' => 'number', 'default' => 30, 'min' => 1, 'max' => 3650, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_credits', 'label' => '赠送积分', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999, 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_redeem_page', 'label' => '兑换页面', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_success_msg', 'label' => '兑换成功提示', 'type' => 'input', 'default' => '兑换成功!', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ['key' => 'card_pass_fail_msg', 'label' => '兑换失败提示', 'type' => 'input', 'default' => '卡号无效或已过期', 'dependsOn' => ['key' => 'card_pass_enabled', 'value' => true]], - ]], ['key' => 'points-exchange', 'title' => '余额兑换积分', 'fields' => [ ['key' => 'pay_balance_points_exchange_s', 'label' => '启用余额兑换积分', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_s', 'value' => true]], ['key' => 'pay_balance_points_exchange_rate', 'label' => '兑换比例', 'type' => 'number', 'default' => 100, 'min' => 1, 'max' => 10000, 'help' => '1 余额 = N 积分', 'dependsOn' => ['key' => 'pay_balance_points_exchange_s', 'value' => true]], diff --git a/src/Payment/ChargeCard.php b/src/Payment/ChargeCard.php index 9417cd23..e6b90357 100644 --- a/src/Payment/ChargeCard.php +++ b/src/Payment/ChargeCard.php @@ -14,6 +14,12 @@ class ChargeCard { public const TYPE_BALANCE = 'balance'; public const TYPE_VIP = 'vip'; public const TYPE_CREDITS = 'credits'; + /** 自定义卡密:仅用于后台生成/导入/导出管理,不参与前台兑换流程。 */ + public const TYPE_CUSTOM = 'custom'; + + public const TYPE_KEYS = [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS, self::TYPE_CUSTOM]; + /** 可被前台兑换的类型(custom 仅后台管理)。 */ + public const REDEEMABLE_TYPES = [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS]; public const STATUS_ACTIVE = 'active'; public const STATUS_USED = 'used'; @@ -208,7 +214,7 @@ class ChargeCard { global $wpdb; $type = sanitize_key((string) ($data['type'] ?? self::TYPE_BALANCE)); - if (!in_array($type, [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS], true)) { + if (!in_array($type, self::TYPE_KEYS, true)) { $type = self::TYPE_BALANCE; } @@ -256,32 +262,63 @@ class ChargeCard { } /** - * @param array $meta_fields - * @return array passwords/codes generated + * 批量自动生成卡密(参考主题 zibpay_generate_pass_card 同款行为)。 + * + * 每张卡同时生成随机卡号与随机密码;$meta 由 normalize_meta 校验, + * 与「卡密管理」页面的自动生成表单一一对应。 + * + * @return int 成功生成的张数 */ - public function batch_create(string $type, array $meta_fields, int $count, int $length = 16, string $prefix = '', string $expires_at = '', string $note = ''): array { + public function generate_cards(string $type, int $num, array $meta, int $rand_number = 20, int $rand_password = 35, string $note = ''): int { $type = sanitize_key($type); - if (!in_array($type, [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS], true)) { - $type = self::TYPE_BALANCE; + if (!in_array($type, self::TYPE_KEYS, true)) { + return 0; } - $count = max(1, min(500, $count)); - $length = max(8, min(32, $length)); - $prefix = sanitize_text_field($prefix); - $codes = []; + $num = max(1, min(1000, $num)); + $rand_number = max(0, min(50, $rand_number)); + $rand_password = max(1, min(50, $rand_password)); - for ($i = 0; $i < $count; $i++) { - $password = $prefix . strtoupper(wp_generate_password($length, false)); - $id = $this->create(array_merge($meta_fields, [ - 'type' => $type, - 'password' => $password, - 'expires_at' => $expires_at, - 'note' => $note, - ])); + $created = 0; + for ($i = 1; $i <= $num; $i++) { + $id = $this->create([ + 'card' => self::rand_number($rand_number), + 'password' => self::rand_password($rand_password), + 'type' => $type, + 'meta' => $meta, + 'note' => $note, + ]); if ($id > 0) { - $codes[] = $password; + $created++; } } - return $codes; + return $created; + } + + /** + * 随机卡号(纯数字,参考主题同款算法)。 + */ + public static function rand_number(int $limit = 20): string { + if ($limit <= 0) { + return ''; + } + $chars = (string) mt_rand(100, 999) . (string) mt_rand(100, 999) . (string) mt_rand(100, 999); + if ($limit > 27) { + $chars = $chars . $chars; + } + $chars = str_shuffle($chars . $chars . $chars); + return substr($chars, 0, $limit); + } + + /** + * 随机密码(字母+数字混合,参考主题同款算法)。 + */ + public static function rand_password(int $limit = 35): string { + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890'; + if ($limit > 60) { + $chars = $chars . $chars; + } + $chars = str_shuffle($chars); + return substr($chars, 0, $limit); } /** @@ -794,7 +831,16 @@ class ChargeCard { if ($type === self::TYPE_VIP) { $level = max(1, min(2, (int) ($raw['level'] ?? $data['level'] ?? 1))); $days = max(0, (int) ($raw['days'] ?? $data['days'] ?? 0)); - return ['level' => $level, 'days' => $days]; + $meta = ['level' => $level, 'days' => $days]; + // 卡密管理页按参考主题保存 time/unit(Permanent|天数 + day|month), + // 仅作展示与导出使用;兑换仍以 days 为准。 + if (isset($raw['time'])) { + $meta['time'] = sanitize_text_field((string) $raw['time']); + } + if (isset($raw['unit'])) { + $meta['unit'] = sanitize_key((string) $raw['unit']) === 'month' ? 'month' : 'day'; + } + return $meta; } if ($type === self::TYPE_CREDITS) { $points = max(0, (int) ($raw['points'] ?? $data['points'] ?? 0)); @@ -803,6 +849,21 @@ class ChargeCard { } return ['points' => $points]; } + if ($type === self::TYPE_CUSTOM) { + $meta = []; + $custom_name = $raw['custom_name'] ?? $data['custom_name'] ?? []; + if (is_array($custom_name)) { + $card_name = trim(sanitize_text_field((string) ($custom_name['card_name'] ?? ''))); + $password_name = trim(sanitize_text_field((string) ($custom_name['password_name'] ?? ''))); + if ($card_name !== '') { + $meta['custom_name']['card_name'] = $card_name; + } + if ($password_name !== '') { + $meta['custom_name']['password_name'] = $password_name; + } + } + return $meta; + } return null; } @@ -828,6 +889,9 @@ class ChargeCard { /** * Admin list helper. * + * Supported args: limit, offset, status, type, search/s, other(标识), + * orderby(白名单,参考主题别名自动映射), order(ASC|DESC). + * * @return array{items:array,total:int,page:int,max_pages:int} */ public function get_list(array $args = []): array { @@ -838,6 +902,7 @@ class ChargeCard { $status = sanitize_key((string) ($args['status'] ?? '')); $type = sanitize_key((string) ($args['type'] ?? '')); $search = sanitize_text_field((string) ($args['search'] ?? $args['s'] ?? '')); + $other = sanitize_text_field((string) ($args['other'] ?? '')); $where = []; $params = []; @@ -849,9 +914,14 @@ class ChargeCard { $where[] = 'type = %s'; $params[] = $type; } + if ($other !== '') { + $where[] = 'note = %s'; + $params[] = $other; + } if ($search !== '') { $like = '%' . $wpdb->esc_like($search) . '%'; - $where[] = '(password LIKE %s OR card LIKE %s OR note LIKE %s OR CAST(id AS CHAR) = %s)'; + $where[] = '(password LIKE %s OR card LIKE %s OR note LIKE %s OR meta LIKE %s OR CAST(id AS CHAR) = %s)'; + $params[] = $like; $params[] = $like; $params[] = $like; $params[] = $like; @@ -864,7 +934,10 @@ class ChargeCard { ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql); - $sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY created_at DESC LIMIT %d OFFSET %d"; + $orderby = $this->resolve_orderby((string) ($args['orderby'] ?? '')); + $order = strtoupper((string) ($args['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; + + $sql = "SELECT * FROM {$this->table()} {$where_sql} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d"; $params[] = $limit; $params[] = $offset; $items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A); @@ -877,11 +950,67 @@ class ChargeCard { ]; } - public function disable(int $id): bool { + /** + * 排序字段白名单;兼容参考主题的列名别名。 + * modified_time 以 used_at 兜底(未使用卡等于创建时间)。 + */ + private function resolve_orderby(string $orderby): string { + $map = [ + 'id' => 'id', + 'card' => 'card', + 'password' => 'password', + 'type' => 'type', + 'status' => 'status', + 'note' => 'note', + 'other' => 'note', + 'create_time' => 'created_at', + 'created_at' => 'created_at', + 'modified_time' => 'COALESCE(used_at, created_at)', + 'modified' => 'COALESCE(used_at, created_at)', + ]; + $key = sanitize_key(trim($orderby)); + return $map[$key] ?? 'COALESCE(used_at, created_at)'; + } + + /** + * 按主键批量删除。 + * + * @param array $ids + */ + public function delete_by_ids(array $ids): int { global $wpdb; - return (bool) $wpdb->update($this->table(), [ - 'status' => self::STATUS_DISABLED, - ], ['id' => $id], ['%s'], ['%d']); + $ids = array_values(array_unique(array_filter(array_map('absint', $ids)))); + if (!$ids) { + return 0; + } + $placeholders = implode(',', array_fill(0, count($ids), '%d')); + return (int) $wpdb->query($wpdb->prepare("DELETE FROM {$this->table()} WHERE id IN ({$placeholders})", $ids)); + } + + /** + * 导出用全量行(不限分页)。 + * + * @return array> + */ + public function get_export_rows(string $type = '', string $status = ''): array { + global $wpdb; + $type = sanitize_key($type); + $status = sanitize_key($status); + + $where = []; + $params = []; + if ($type !== '' && in_array($type, self::TYPE_KEYS, true)) { + $where[] = 'type = %s'; + $params[] = $type; + } + if ($status !== '') { + $where[] = 'status = %s'; + $params[] = $status; + } + $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; + $sql = "SELECT card, password, type, meta, note, status FROM {$this->table()} {$where_sql} ORDER BY id ASC"; + $rows = $params ? $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A) : $wpdb->get_results($sql, ARRAY_A); + return is_array($rows) ? $rows : []; } /** diff --git a/src/Settings/CreditsPaymentGroups.php b/src/Settings/CreditsPaymentGroups.php index f555f4c8..369247d2 100644 --- a/src/Settings/CreditsPaymentGroups.php +++ b/src/Settings/CreditsPaymentGroups.php @@ -175,15 +175,6 @@ trait CreditsPaymentGroups { return PaymentRuntime::get_order_settings(); } - /** - * 获取充值卡设置 - * - * @return array{enabled:bool,desc:string,type:string,prefix:string,length:int,batch_size:int,max_redeem:int,expiry_days:int,amount:string,vip_days:int,credits:int,redeem_page:string,success_msg:string,fail_msg:string} - */ - public static function get_card_pass_settings(): array { - return PaymentRuntime::get_card_pass_settings(); - } - /** * 获取提现管理设置 * diff --git a/src/Settings/DisplayPageGroups.php b/src/Settings/DisplayPageGroups.php index 525423a0..4a301c3b 100644 --- a/src/Settings/DisplayPageGroups.php +++ b/src/Settings/DisplayPageGroups.php @@ -272,7 +272,6 @@ trait DisplayPageGroups { 'affiliate' => self::get_payment_affiliate_settings(), 'gateway' => self::get_payment_gateway_settings(), 'order' => self::get_order_settings(), - 'card_pass' => self::get_card_pass_settings(), 'withdrawal' => self::get_withdrawal_settings(), ]; } diff --git a/src/Settings/HomeCms.php b/src/Settings/HomeCms.php index bbe4fca0..61d8e903 100644 --- a/src/Settings/HomeCms.php +++ b/src/Settings/HomeCms.php @@ -242,10 +242,6 @@ class HomeCms { return PaymentRuntime::get_order_settings(); } - public static function get_card_pass_settings(): array { - return PaymentRuntime::get_card_pass_settings(); - } - public static function get_withdrawal_settings(): array { return PaymentRuntime::get_withdrawal_settings(); } diff --git a/src/Settings/PaymentRuntime.php b/src/Settings/PaymentRuntime.php index 1cc3fb42..154167a1 100644 --- a/src/Settings/PaymentRuntime.php +++ b/src/Settings/PaymentRuntime.php @@ -504,38 +504,8 @@ class PaymentRuntime { } /** - * @return array + * @return array{min_amount:string,service_charge_rate:int,daily_count:int,daily_limit:string,reset_time:string,payout_channels:string[]} */ - public static function get_card_pass_settings(): array { - $s = Settings::get_all(); - - $type = sanitize_key((string) ($s['card_pass_type'] ?? 'balance')); - if (!in_array($type, ['balance', 'vip', 'credits'], true)) { - $type = 'balance'; - } - - $success_msg = trim((string) ($s['card_pass_success_msg'] ?? '')); - $fail_msg = trim((string) ($s['card_pass_fail_msg'] ?? '')); - - return [ - 'enabled' => !empty($s['card_pass_enabled']), - 'desc' => sanitize_textarea_field((string) ($s['card_pass_desc'] ?? '')), - 'type' => $type, - 'prefix' => sanitize_text_field((string) ($s['card_pass_prefix'] ?? 'AK')), - 'length' => max(8, min(32, (int) ($s['card_pass_length'] ?? 16))), - 'batch_size' => max(1, min(500, (int) ($s['card_pass_batch_size'] ?? 50))), - 'max_redeem' => max(1, min(100, (int) ($s['card_pass_max_redeem'] ?? 1))), - 'expiry_days' => max(0, (int) ($s['card_pass_expiry'] ?? 0)), - 'amount' => self::money_decimal($s['card_pass_amount'] ?? '10.00', '10.00', 999900), - 'vip_days' => max(1, min(3650, (int) ($s['card_pass_vip_days'] ?? 30))), - 'credits' => max(0, min(99999, (int) ($s['card_pass_credits'] ?? 0))), - 'redeem_page' => esc_url_raw((string) ($s['card_pass_redeem_page'] ?? '')), - 'success_msg' => $success_msg !== '' ? $success_msg : __('兑换成功!', 'aksrt'), - 'fail_msg' => $fail_msg !== '' ? $fail_msg : __('卡号无效或已过期', 'aksrt'), - ]; - } - - /** @return array{min_amount:string,service_charge_rate:int,daily_count:int,daily_limit:string,reset_time:string,payout_channels:string[]} */ public static function get_withdrawal_settings(): array { $s = Settings::get_all(); $channels = array_values(array_intersect(['alipay', 'wechat'], array_map('sanitize_key', (array) ($s['affiliate_payout_channels'] ?? ['alipay', 'wechat']))));