diff --git a/assets/admin/src/pages/payment/PaymentChargeCard.vue b/assets/admin/src/pages/payment/PaymentChargeCard.vue index 965df4f2..c9b0c19d 100644 --- a/assets/admin/src/pages/payment/PaymentChargeCard.vue +++ b/assets/admin/src/pages/payment/PaymentChargeCard.vue @@ -51,21 +51,33 @@ - - + + - + + + + + + + - - + + - - + + + + + + + + @@ -78,22 +90,42 @@ + + + - + + + + + + + + + + + + + + + + + + - + @@ -152,21 +184,40 @@ const filters = reactive({ }) const createForm = reactive({ + type: 'balance', code: '', amount: 10, + level: 1, + days: 30, + points: 100, expiresAt: null, note: '', }) const batchForm = reactive({ + type: 'balance', count: 20, amount: 10, - prefix: 'CARD', + level: 1, + days: 30, + points: 100, + prefix: 'AK', length: 12, expiresAt: null, note: '', }) +const typeOptions = [ + { label: '余额卡', value: 'balance' }, + { label: '会员卡', value: 'vip' }, + { label: '积分卡', value: 'credits' }, +] + +const levelOptions = [ + { label: 'VIP1', value: 1 }, + { label: 'VIP2', value: 2 }, +] + const pager = reactive({ page: 1, pageSize: 20, @@ -214,9 +265,26 @@ const pagination = computed(() => ({ const columns = [ { title: 'ID', key: 'id', width: 70 }, - { title: '卡号', key: 'code', minWidth: 190, ellipsis: { tooltip: true } }, - { title: '面额', key: 'amount', width: 90 }, - { title: '余额', key: 'balance', width: 90 }, + { title: '卡密', key: 'code', minWidth: 170, ellipsis: { tooltip: true } }, + { + title: '类型', + key: 'type_label', + width: 90, + render: (row) => row.type_label || row.type || '-', + }, + { + title: '面额/权益', + key: 'amount', + width: 120, + render: (row) => { + if (row.type === 'vip') { + const days = Number(row.days || 0) + return `VIP${row.level || 1} / ${days > 0 ? `${days}天` : '永久'}` + } + if (row.type === 'credits') return `${row.points || row.amount || 0} 积分` + return row.amount + }, + }, { title: '状态', key: 'status_label', @@ -339,8 +407,13 @@ async function handleCreate() { await api.request('/payment/charge-cards', { method: 'POST', data: { + type: createForm.type, code: createForm.code, + password: createForm.code, amount: createForm.amount, + level: createForm.level, + days: createForm.days, + points: createForm.points, expires_at: createForm.expiresAt ? formatDateValue(createForm.expiresAt) : '', note: createForm.note, }, @@ -363,8 +436,12 @@ async function handleBatchCreate() { await api.request('/payment/charge-cards/batch', { method: 'POST', data: { + type: batchForm.type, count: batchForm.count, amount: batchForm.amount, + level: batchForm.level, + days: batchForm.days, + points: batchForm.points, prefix: batchForm.prefix, length: batchForm.length, expires_at: batchForm.expiresAt ? formatDateValue(batchForm.expiresAt) : '', diff --git a/assets/admin/src/schema/sections/payment/recharge.js b/assets/admin/src/schema/sections/payment/recharge.js index 46274cd6..6c74626b 100644 --- a/assets/admin/src/schema/sections/payment/recharge.js +++ b/assets/admin/src/schema/sections/payment/recharge.js @@ -71,9 +71,9 @@ export const rechargeSchema = { title: '充值卡免扣', icon: 'fas fa-ticket-alt', fields: [ - { key: 'pay_balance_pass_charge_s', label: '启用充值卡免扣', type: 'switch', default: false }, - { key: 'pay_balance_pass_charge_desc', label: '免扣说明', type: 'textarea', default: '', dependsOn: { key: 'pay_balance_pass_charge_s', value: true } }, - { key: 'pay_balance_pass_charge_only_password', label: '仅密码免扣', type: 'switch', default: false, dependsOn: { key: 'pay_balance_pass_charge_s', value: true } }, + { key: 'pay_balance_pass_charge_s', label: '启用余额卡密兑换', type: 'switch', default: false }, + { key: 'pay_balance_pass_charge_desc', label: '卡密兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_balance_pass_charge_s', value: true } }, + { key: 'pay_balance_pass_charge_only_password', label: '仅密码兑换', type: 'switch', default: false, dependsOn: { key: 'pay_balance_pass_charge_s', value: true } }, ], }, { diff --git a/assets/admin/src/schema/sections/payment/vip.js b/assets/admin/src/schema/sections/payment/vip.js index ca4b7907..e9029e87 100644 --- a/assets/admin/src/schema/sections/payment/vip.js +++ b/assets/admin/src/schema/sections/payment/vip.js @@ -18,6 +18,7 @@ export const vipSchema = { pay_download_speed_vip_2: 0, pay_vip_pass_charge_s: false, pay_vip_pass_charge_desc: '', + pay_vip_pass_charge_only_password: true, vip_renew: true, vip_renew_desc: '', vip_renew_price_type: 'discount', @@ -61,6 +62,7 @@ export const vipSchema = { { key: 'pay_download_speed_vip_2', label: '二级会员下载速度(KB/s)', type: 'number', default: 0, min: 0, max: 99999, dependsOn: { key: 'pay_user_vip_2_s', value: true } }, { key: 'pay_vip_pass_charge_s', label: '会员卡密兑换', type: 'switch', default: false }, { key: 'pay_vip_pass_charge_desc', label: '卡密兑换说明', type: 'textarea', default: '', dependsOn: { key: 'pay_vip_pass_charge_s', value: true } }, + { key: 'pay_vip_pass_charge_only_password', label: '仅密码兑换', type: 'switch', default: true, dependsOn: { key: 'pay_vip_pass_charge_s', value: true }, help: '开启后只需输入卡密;关闭则需卡号+卡密。' }, ], }, { diff --git a/assets/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css index d67914a3..f4a56337 100644 --- a/assets/css/routes/user-center/credits.css +++ b/assets/css/routes/user-center/credits.css @@ -560,3 +560,62 @@ box-shadow: none; border-color: var(--color-line, #e5e7eb); } + +/* Card-pass redeem (balance / VIP) */ +.uc-card-pass { + margin: 16px 0 0; + padding: 14px 16px; + border: 1px solid var(--color-line, #e5e7eb); + border-radius: 12px; + background: #fff; +} +.uc-card-pass__title { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 700; + color: #111827; + margin: 0 0 8px; +} +.uc-card-pass__title i { color: #2563eb; } +.uc-card-pass__desc { + margin: 0 0 10px; + font-size: 12px; + color: #6b7280; + line-height: 1.5; +} +.uc-card-pass__input { + display: block; + width: 100%; + margin: 0 0 8px; + padding: 9px 12px; + border: 1px solid #e5e7eb; + border-radius: 8px; + font-size: 13px; + box-sizing: border-box; +} +.uc-card-pass__btn { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 36px; + padding: 0 14px; + border: 0; + border-radius: 8px; + background: #2563eb; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; +} +.uc-card-pass__btn:hover { background: #1d4ed8; } +.uc-card-pass__btn:disabled { opacity: .6; cursor: not-allowed; } +.uc-card-pass__tip { + margin: 8px 0 0; + font-size: 12px; + color: #059669; + min-height: 1.2em; +} +.uc-card-pass__tip.is-error { color: #b91c1c; } + diff --git a/assets/js/credits.js b/assets/js/credits.js index bb9b103d..959f5f99 100644 --- a/assets/js/credits.js +++ b/assets/js/credits.js @@ -442,17 +442,84 @@ }); } + // ===== 卡密兑换(余额 / 会员) ===== + function initCardPass() { + var roots = document.querySelectorAll('[data-aksrt-card-pass]'); + if (!roots || !roots.length) return; + + Array.prototype.forEach.call(roots, function (root) { + var tip = root.querySelector('[data-aksrt-card-tip]'); + var busy = false; + + function setTip(text, isError) { + if (!tip) return; + tip.textContent = text || ''; + tip.classList.toggle('is-error', !!isError); + } + + var btn = root.querySelector('[data-aksrt-card-redeem]'); + if (!btn) return; + + btn.addEventListener('click', function () { + if (busy || btn.disabled) return; + var type = btn.getAttribute('data-type') || root.getAttribute('data-aksrt-card-pass') || ''; + var pwInput = root.querySelector('[data-aksrt-card-password]'); + var cardInput = root.querySelector('[data-aksrt-card-no]'); + var password = pwInput ? String(pwInput.value || '').trim() : ''; + var card = cardInput ? String(cardInput.value || '').trim() : ''; + if (!password) { + setTip('请输入卡密', true); + if (pwInput) pwInput.focus(); + return; + } + if (cardInput && !card) { + setTip('请输入卡号', true); + cardInput.focus(); + return; + } + + busy = true; + btn.disabled = true; + setTip('正在兑换…'); + + var body = { password: password, type: type }; + if (card) body.card = card; + + api('/pay/card-pass/redeem', { + method: 'POST', + body: JSON.stringify(body) + }).then(function (r) { + busy = false; + btn.disabled = false; + if (!r.ok || (r.json && r.json.code && !r.json.success)) { + setTip((r.json && r.json.message) || '兑换失败', true); + return; + } + var data = (r.json && r.json.data) ? r.json.data : r.json; + setTip((data && data.message) || '兑换成功,正在刷新…'); + window.location.reload(); + }).catch(function () { + busy = false; + btn.disabled = false; + setTip('网络错误,请重试', true); + }); + }); + }); + } + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { initSignin(); initInvite(); initVipShop(); - initVipPointsExchange(); + initVipPointsExchange(); + initCardPass(); }); } else { initSignin(); initInvite(); initVipShop(); initVipPointsExchange(); + initCardPass(); } })(); diff --git a/functions.php b/functions.php index 21d363f2..e189a506 100644 --- a/functions.php +++ b/functions.php @@ -180,6 +180,9 @@ if (class_exists('AKSRT_Content_Pay')) { if (class_exists('AKSRT_Vip_Service')) { AKSRT_Vip_Service::get_instance()->init(); } +if (class_exists('AKSRT_Charge_Card')) { + AKSRT_Charge_Card::get_instance()->init(); +} // Download State & Docs Presenter are pure service singletons (no hooks) if (class_exists('AKSRT_Download_State')) { AKSRT_Download_State::get_instance(); diff --git a/inc/classes/admin/class-aksrt-admin-payment-controller.php b/inc/classes/admin/class-aksrt-admin-payment-controller.php index 0bd0352c..b8d1627d 100644 --- a/inc/classes/admin/class-aksrt-admin-payment-controller.php +++ b/inc/classes/admin/class-aksrt-admin-payment-controller.php @@ -334,6 +334,9 @@ class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base { $item = $this->build_charge_card_admin_item($row); if ($keyword !== '' && mb_stripos(implode(' ', array_filter([ (string) ($item['code'] ?? ''), + (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'] ?? ''), @@ -377,14 +380,19 @@ class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base { } $params = $this->get_json_params($request); - $code = sanitize_text_field((string) ($params['code'] ?? '')); - $amount = (float) ($params['amount'] ?? 0); - - if ($code === '' || $amount <= 0) { - return $this->error('卡号和金额不能为空'); + $type = sanitize_key((string) ($params['type'] ?? 'balance')); + if (!in_array($type, ['balance', 'vip', 'credits'], true)) { + $type = 'balance'; + } + $password = sanitize_text_field((string) ($params['password'] ?? $params['code'] ?? '')); + if ($password === '') { + return $this->error('卡密不能为空'); } - $id = AKSRT_Charge_Card::get_instance()->create($params); + $id = AKSRT_Charge_Card::get_instance()->create(array_merge($params, [ + 'type' => $type, + 'password' => $password, + ])); if ($id > 0) { return $this->success([ @@ -393,7 +401,7 @@ class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base { ], 201); } - return $this->error(__('创建失败(可能卡号已存在)', 'aksrt')); + return $this->error(__('创建失败(可能卡密已存在或参数无效)', 'aksrt')); } /** @@ -405,35 +413,43 @@ class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base { } $params = $this->get_json_params($request); - $amount = (float) ($params['amount'] ?? 0); + $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'] ?? '')); - if ($amount <= 0) { - return $this->error('金额必须大于 0'); + $meta_fields = [ + 'amount' => (float) ($params['amount'] ?? 0), + 'level' => (int) ($params['level'] ?? 1), + 'days' => (int) ($params['days'] ?? $params['vip_days'] ?? 30), + 'points' => (int) ($params['points'] ?? $params['credits'] ?? 0), + ]; + + if ($type === 'balance' && $meta_fields['amount'] <= 0) { + return $this->error('余额卡金额必须大于 0'); + } + if ($type === 'credits' && $meta_fields['points'] <= 0) { + return $this->error('积分卡积分必须大于 0'); } - $service = AKSRT_Charge_Card::get_instance(); - $codes = []; - for ($i = 0; $i < $count; $i++) { - $code = $prefix . strtoupper(wp_generate_password($length, false)); - $id = $service->create([ - 'code' => $code, - 'amount' => $amount, - 'expires_at' => $expires, - 'note' => $note, - ]); - if ($id > 0) { - $codes[] = $code; - } - } + $codes = AKSRT_Charge_Card::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)], + 'data' => ['codes' => $codes, 'count' => count($codes), 'type' => $type], ]); } @@ -650,18 +666,48 @@ class AKSRT_Admin_Payment_Controller extends AKSRT_Admin_Controller_Base { $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'] ?? $row['code'] ?? ''); + $card_no = (string) ($row['card'] ?? ''); return [ 'id' => (int) ($row['id'] ?? 0), - 'code' => (string) ($row['code'] ?? ''), + 'code' => $password, + 'password' => $password, + 'card' => $card_no, + 'type' => $type, + 'type_label' => $typeMap[$type] ?? $type, 'amount' => (float) ($row['amount'] ?? 0), 'balance' => (float) ($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, diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-payment.php b/inc/classes/admin/schema/class-aksrt-admin-schema-payment.php index cc5a4d07..a053f18a 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-payment.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-payment.php @@ -86,9 +86,9 @@ class AKSRT_Admin_Schema_Payment { ['key' => 'pay_balance_transfer_desc', 'label' => '转账说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_transfer_s', 'value' => true]], ]], ['key' => 'pass-charge', 'title' => '充值卡免扣', 'fields' => [ - ['key' => 'pay_balance_pass_charge_s', 'label' => '启用充值卡免扣', 'type' => 'switch', 'default' => false], - ['key' => 'pay_balance_pass_charge_desc', 'label' => '免扣说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]], - ['key' => 'pay_balance_pass_charge_only_password', 'label' => '仅密码免扣', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]], + ['key' => 'pay_balance_pass_charge_s', 'label' => '启用余额卡密兑换', 'type' => 'switch', 'default' => false], + ['key' => 'pay_balance_pass_charge_desc', 'label' => '卡密兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]], + ['key' => 'pay_balance_pass_charge_only_password', 'label' => '仅密码兑换', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'pay_balance_pass_charge_s', 'value' => true]], ]], ['key' => 'exchange', 'title' => '积分兑换', 'fields' => [ ['key' => 'pay_balance_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false], @@ -127,6 +127,7 @@ class AKSRT_Admin_Schema_Payment { ['key' => 'pay_download_speed_vip_2', 'label' => '二级会员下载速度(KB/s)', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 99999, 'dependsOn' => ['key' => 'pay_user_vip_2_s', 'value' => true]], ['key' => 'pay_vip_pass_charge_s', 'label' => '会员卡密兑换', 'type' => 'switch', 'default' => false], ['key' => 'pay_vip_pass_charge_desc', 'label' => '卡密兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true]], + ['key' => 'pay_vip_pass_charge_only_password', 'label' => '仅密码兑换', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'pay_vip_pass_charge_s', 'value' => true]], ]], ['key' => 'renew', 'title' => '续费设置', 'fields' => [ ['key' => 'vip_renew', 'label' => '允许续费', 'type' => 'switch', 'default' => true], diff --git a/inc/classes/class-aksrt-charge-card.php b/inc/classes/class-aksrt-charge-card.php index c5babef4..c59843c0 100644 --- a/inc/classes/class-aksrt-charge-card.php +++ b/inc/classes/class-aksrt-charge-card.php @@ -1,8 +1,9 @@ 'GET', + 'callback' => [$this, 'rest_options'], + 'permission_callback' => [$this, 'check_rest_nonce'], + ]); + register_rest_route('aksrt/v1', '/pay/card-pass/redeem', [ + 'methods' => 'POST', + 'callback' => [$this, 'rest_redeem'], + 'permission_callback' => [$this, 'check_logged_in_rest'], + ]); + } + /** - * 创建充值卡 + * @param WP_REST_Request $request + * @return true|WP_Error + */ + public function check_rest_nonce(WP_REST_Request $request) { + $nonce = $request->get_header('x-wp-nonce'); + if (!$nonce) { + return new WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]); + } + if (!wp_verify_nonce($nonce, 'wp_rest')) { + return new WP_Error('aksrt_rest_bad_nonce', __('安全令牌已过期,请刷新页面', 'aksrt'), ['status' => 403]); + } + return true; + } + + /** + * @param WP_REST_Request $request + * @return true|WP_Error + */ + public function check_logged_in_rest(WP_REST_Request $request) { + $nonce = $this->check_rest_nonce($request); + if (is_wp_error($nonce)) { + return $nonce; + } + if (!is_user_logged_in()) { + return new WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]); + } + return true; + } + + public function rest_options(WP_REST_Request $request) { + return new WP_REST_Response([ + 'success' => true, + 'data' => $this->get_public_options(), + ], 200); + } + + public function rest_redeem(WP_REST_Request $request) { + $params = $request->get_json_params(); + if (!is_array($params)) { + $params = $request->get_params(); + } + $password = sanitize_text_field((string) ($params['password'] ?? $params['code'] ?? '')); + $card = sanitize_text_field((string) ($params['card'] ?? '')); + $type = sanitize_key((string) ($params['type'] ?? '')); + if ($type !== '' && !in_array($type, [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS], true)) { + $type = ''; + } + + $result = $this->redeem_for_user(get_current_user_id(), $password, $card, $type); + if (is_wp_error($result)) { + return $result; + } + return new WP_REST_Response([ + 'success' => true, + 'data' => $result, + ], 200); + } + + /** + * @return array + */ + public function get_public_options(): array { + $balance = ['enabled' => false, 'desc' => '', 'only_password' => true]; + $vip = ['enabled' => false, 'desc' => '', 'only_password' => true]; + + if (class_exists('AKSRT_Settings')) { + if (method_exists('AKSRT_Settings', 'get_payment_recharge_settings')) { + $recharge = AKSRT_Settings::get_payment_recharge_settings(); + $pc = is_array($recharge['pass_charge'] ?? null) ? $recharge['pass_charge'] : []; + $balance = [ + 'enabled' => !empty($pc['enabled']), + 'desc' => (string) ($pc['desc'] ?? ''), + 'only_password' => !isset($pc['only_password']) || !empty($pc['only_password']), + ]; + } + if (method_exists('AKSRT_Settings', 'get_payment_vip_settings')) { + $vip_s = AKSRT_Settings::get_payment_vip_settings(); + $benefit = is_array($vip_s['benefit'] ?? null) ? $vip_s['benefit'] : []; + $vip = [ + 'enabled' => !empty($benefit['pass_charge']), + 'desc' => (string) ($benefit['pass_charge_desc'] ?? ''), + 'only_password' => !isset($benefit['pass_charge_only_password']) || !empty($benefit['pass_charge_only_password']), + ]; + } + } + + return [ + 'balance' => $balance, + 'vip' => $vip, + 'credits' => [ + 'enabled' => false, + 'desc' => '', + 'only_password' => true, + ], + 'labels' => [ + 'title' => __('卡密兑换', 'aksrt'), + 'password' => __('卡密', 'aksrt'), + 'card' => __('卡号', 'aksrt'), + 'submit_balance' => __('兑换余额', 'aksrt'), + 'submit_vip' => __('兑换会员', 'aksrt'), + 'placeholder_pw' => __('请输入卡密', 'aksrt'), + 'placeholder_card'=> __('请输入卡号', 'aksrt'), + ], + ]; + } + + /** + * Create one card. + * + * Accepted $data keys: + * - password|code (required unless auto) + * - card (optional) + * - type balance|vip|credits + * - amount (balance) + * - level, days (vip; days 0 = permanent) + * - points (credits) + * - meta (array override) + * - expires_at, note */ public function create(array $data): int { global $wpdb; - $code = sanitize_text_field((string) ($data['code'] ?? '')); - $amount = (float) ($data['amount'] ?? 0); - $expires_at = sanitize_text_field((string) ($data['expires_at'] ?? '')); - $note = sanitize_textarea_field((string) ($data['note'] ?? '')); + $type = sanitize_key((string) ($data['type'] ?? self::TYPE_BALANCE)); + if (!in_array($type, [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS], true)) { + $type = self::TYPE_BALANCE; + } - if ($code === '' || $amount <= 0) { + $password = sanitize_text_field((string) ($data['password'] ?? $data['code'] ?? '')); + if ($password === '') { + return 0; + } + $card = sanitize_text_field((string) ($data['card'] ?? '')); + + $meta = $this->normalize_meta($type, $data); + if ($meta === null) { return 0; } - // 检查 code 唯一性 + // Uniqueness: password alone (only_password mode common). $exists = $wpdb->get_var($wpdb->prepare( - "SELECT id FROM {$this->table()} WHERE code = %s", - $code + "SELECT id FROM {$this->table()} WHERE password = %s LIMIT 1", + $password )); - if ($exists) return 0; + if ($exists) { + return 0; + } + + $expires_at = sanitize_text_field((string) ($data['expires_at'] ?? '')); + $note = sanitize_textarea_field((string) ($data['note'] ?? '')); + $amount_col = (float) ($meta['amount'] ?? $meta['points'] ?? 0); $result = $wpdb->insert($this->table(), [ - 'code' => $code, - 'amount' => $amount, - 'balance' => $amount, - 'status' => self::STATUS_ACTIVE, - 'expires_at' => $expires_at !== '' ? $expires_at : null, - 'created_at' => current_time('mysql'), - 'note' => $note, + 'card' => $card, + 'password' => $password, + 'type' => $type, + 'amount' => $amount_col, + 'balance' => $amount_col, + 'status' => self::STATUS_ACTIVE, + 'meta' => wp_json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'order_number' => '', + 'used_by' => null, + 'used_at' => null, + 'expires_at' => $expires_at !== '' ? $expires_at : null, + 'created_at' => current_time('mysql'), + 'note' => $note, ]); return $result ? (int) $wpdb->insert_id : 0; } /** - * 批量生成充值卡 + * @param array $meta_fields + * @return array passwords/codes generated */ - public function batch_create(float $amount, int $count, int $length = 16, string $prefix = ''): array { - $codes = []; + public function batch_create(string $type, array $meta_fields, int $count, int $length = 16, string $prefix = '', string $expires_at = '', string $note = ''): array { + $type = sanitize_key($type); + if (!in_array($type, [self::TYPE_BALANCE, self::TYPE_VIP, self::TYPE_CREDITS], true)) { + $type = self::TYPE_BALANCE; + } + $count = max(1, min(500, $count)); + $length = max(8, min(32, $length)); + $prefix = sanitize_text_field($prefix); + $codes = []; + for ($i = 0; $i < $count; $i++) { - $code = $prefix . strtoupper(wp_generate_password($length, false)); - $id = $this->create([ - 'code' => $code, - 'amount' => $amount, - ]); + $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, + ])); if ($id > 0) { - $codes[] = $code; + $codes[] = $password; } } return $codes; } /** - * 使用充值卡 + * @return array{success:bool,message:string,type?:string,amount?:float,level?:int,days?:int,order_number?:string,balance?:int}|WP_Error */ - public function redeem(string $code, int $user_id): array { - global $wpdb; + public function redeem_for_user(int $user_id, string $password, string $card = '', string $expected_type = '') { + $user_id = max(0, $user_id); + $password = sanitize_text_field($password); + $card = sanitize_text_field($card); - $code = sanitize_text_field($code); - $card = $wpdb->get_row($wpdb->prepare( - "SELECT * FROM {$this->table()} WHERE code = %s", - $code - ), ARRAY_A); - - if (!$card) { - return ['success' => false, 'message' => '充值卡不存在']; + if ($user_id <= 0) { + return new WP_Error('aksrt_need_login', __('请先登录', 'aksrt'), ['status' => 401]); + } + if ($password === '') { + return new WP_Error('aksrt_card_empty', __('请输入卡密', 'aksrt'), ['status' => 400]); } - if ($card['status'] !== self::STATUS_ACTIVE) { - return ['success' => false, 'message' => '充值卡已使用或已失效']; + // Simple per-user throttle. + $throttle_key = 'aksrt_card_pass_rl_' . $user_id; + $hits = (int) get_transient($throttle_key); + if ($hits >= 20) { + return new WP_Error('aksrt_card_rate', __('操作过于频繁,请稍后再试', 'aksrt'), ['status' => 429]); + } + set_transient($throttle_key, $hits + 1, MINUTE_IN_SECONDS); + + $row = $this->find_active($password, $card, $expected_type); + if (!$row) { + return new WP_Error('aksrt_card_invalid', __('卡号或卡密错误', 'aksrt'), ['status' => 400]); } - if ($card['expires_at'] && strtotime($card['expires_at']) < time()) { - // 标记过期 - $wpdb->update($this->table(), ['status' => self::STATUS_EXPIRED], ['id' => (int) $card['id']]); - return ['success' => false, 'message' => '充值卡已过期']; + $type = sanitize_key((string) ($row['type'] ?? self::TYPE_BALANCE)); + if ($expected_type !== '' && $type !== $expected_type) { + return new WP_Error('aksrt_card_type', __('当前卡密不能用于该兑换类型', 'aksrt'), ['status' => 400]); } - $amount = (float) $card['balance']; + $gate = $this->assert_type_enabled($type); + if (is_wp_error($gate)) { + return $gate; + } - // 更新卡状态 - $wpdb->update($this->table(), [ - 'status' => self::STATUS_USED, - 'used_by' => $user_id, - 'used_at' => current_time('mysql'), - 'balance' => 0, - ], ['id' => (int) $card['id']]); + // only_password: if feature requires card number and empty, fail. + $opts = $this->get_public_options(); + $only_password = true; + if ($type === self::TYPE_BALANCE) { + $only_password = !empty($opts['balance']['only_password']); + } elseif ($type === self::TYPE_VIP) { + $only_password = !empty($opts['vip']['only_password']); + } + if (!$only_password && $card === '' && (string) ($row['card'] ?? '') !== '') { + return new WP_Error('aksrt_card_need_no', __('请输入卡号', 'aksrt'), ['status' => 400]); + } + if (!$only_password && $card !== '' && (string) ($row['card'] ?? '') !== '' && (string) $row['card'] !== $card) { + return new WP_Error('aksrt_card_invalid', __('卡号或卡密错误', 'aksrt'), ['status' => 400]); + } - // 增加用户余额 - if (class_exists('AKSRT_Credits')) { - AKSRT_Credits::add($user_id, (int) $amount, 'charge_card', [ - 'ref_type' => 'charge_card', - 'ref_id' => (int) $card['id'], + if (!empty($row['expires_at']) && strtotime((string) $row['expires_at']) < time()) { + global $wpdb; + $wpdb->update($this->table(), ['status' => self::STATUS_EXPIRED], ['id' => (int) $row['id']], ['%s'], ['%d']); + return new WP_Error('aksrt_card_expired', __('卡密已过期', 'aksrt'), ['status' => 400]); + } + + $meta = $this->decode_meta($row); + $card_id = (int) $row['id']; + + // Create audit order first (pending), then CAS mark used, then fulfill + mark paid. + $order_number = $this->create_redeem_order($user_id, $type, $meta, $card_id, $password); + if ($order_number === '') { + return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]); + } + + if (!$this->mark_used_cas($card_id, $user_id, $order_number)) { + // Race lost. + if (class_exists('AKSRT_Order')) { + AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [ + 'note' => __('卡密已被使用,订单取消', 'aksrt'), + ]); + } + return new WP_Error('aksrt_card_used', __('该卡密已使用', 'aksrt'), ['status' => 400]); + } + + $fulfill = $this->fulfill($user_id, $type, $meta, $order_number, $card_id); + if (is_wp_error($fulfill)) { + return $fulfill; + } + + if (class_exists('AKSRT_Order')) { + AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [ + 'payment_method' => 'card_pass', + 'payment_transaction_id' => 'card_pass_' . $card_id, + 'note' => (string) ($fulfill['message'] ?? __('卡密兑换成功', 'aksrt')), ]); } - return [ - 'success' => true, - 'message' => sprintf('充值成功,已到账 %s 元', number_format($amount, 2)), - 'amount' => $amount, - ]; + $out = array_merge([ + 'success' => true, + 'type' => $type, + 'order_number' => $order_number, + 'reload' => true, + ], $fulfill); + + if (class_exists('AKSRT_Credits')) { + $out['balance'] = AKSRT_Credits::balance($user_id); + } + + return $out; } /** - * 获取充值卡列表 + * @return array|null + */ + public function find_active(string $password, string $card = '', string $type = ''): ?array { + global $wpdb; + $password = sanitize_text_field($password); + if ($password === '') { + return null; + } + + $sql = "SELECT * FROM {$this->table()} WHERE password = %s"; + $params = [$password]; + if ($type !== '') { + $sql .= ' AND type = %s'; + $params[] = sanitize_key($type); + } + $sql .= ' LIMIT 1'; + $row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A); + if (!$row) { + return null; + } + if ((string) ($row['status'] ?? '') !== self::STATUS_ACTIVE) { + return null; + } + return $row; + } + + public function mark_used_cas(int $card_id, int $user_id, string $order_number): bool { + global $wpdb; + if ($card_id <= 0) { + return false; + } + $updated = $wpdb->query($wpdb->prepare( + "UPDATE {$this->table()} SET status = %s, used_by = %d, used_at = %s, order_number = %s, balance = 0 + WHERE id = %d AND status = %s", + self::STATUS_USED, + $user_id, + current_time('mysql'), + $order_number, + $card_id, + self::STATUS_ACTIVE + )); + return $updated === 1; + } + + /** + * @param array $meta + * @return array{message:string,amount?:float,level?:int,days?:int}|WP_Error + */ + private function fulfill(int $user_id, string $type, array $meta, string $order_number, int $card_id) { + if ($type === self::TYPE_BALANCE) { + $amount = (int) round((float) ($meta['amount'] ?? 0)); + if ($amount <= 0 || !class_exists('AKSRT_Credits')) { + return new WP_Error('aksrt_card_amount', __('当前卡密可充值金额为0', 'aksrt'), ['status' => 400]); + } + $new = AKSRT_Credits::add($user_id, $amount, 'card_pass_balance', [ + 'ref_type' => 'charge_card', + 'ref_id' => $card_id, + 'order' => $order_number, + ], sprintf(__('卡密充值 %d', 'aksrt'), $amount)); + if ($new === null || $new === false) { + return new WP_Error('aksrt_credit_fail', __('余额入账失败', 'aksrt'), ['status' => 500]); + } + return [ + 'message' => sprintf(__('卡密充值成功,充值金额:%s', 'aksrt'), (string) $amount), + 'amount' => (float) $amount, + ]; + } + + if ($type === self::TYPE_VIP) { + $level = max(1, min(2, (int) ($meta['level'] ?? 1))); + $days = max(0, (int) ($meta['days'] ?? 0)); + if (!class_exists('AKSRT_Vip_Service')) { + return new WP_Error('aksrt_vip_unavailable', __('会员系统不可用', 'aksrt'), ['status' => 500]); + } + AKSRT_Vip_Service::get_instance()->activate_vip($user_id, $level, $days); + $msg = $days > 0 + ? sprintf( + /* translators: 1: level 2: days */ + __('卡密兑换成功:VIP%s %d 天', 'aksrt'), + (string) $level, + $days + ) + : sprintf( + /* translators: %s: level */ + __('卡密兑换成功:VIP%s 永久', 'aksrt'), + (string) $level + ); + return [ + 'message' => $msg, + 'level' => $level, + 'days' => $days, + ]; + } + + if ($type === self::TYPE_CREDITS) { + $points = (int) ($meta['points'] ?? 0); + if ($points <= 0 || !class_exists('AKSRT_Credits')) { + return new WP_Error('aksrt_card_points', __('当前卡密可兑换积分为0', 'aksrt'), ['status' => 400]); + } + AKSRT_Credits::add($user_id, $points, 'card_pass_credits', [ + 'ref_type' => 'charge_card', + 'ref_id' => $card_id, + 'order' => $order_number, + ], sprintf(__('卡密兑换积分 %d', 'aksrt'), $points)); + return [ + 'message' => sprintf(__('卡密兑换成功,积分:%d', 'aksrt'), $points), + 'amount' => (float) $points, + ]; + } + + return new WP_Error('aksrt_card_type', __('不支持的卡密类型', 'aksrt'), ['status' => 400]); + } + + /** + * @param array $meta + */ + private function create_redeem_order(int $user_id, string $type, array $meta, int $card_id, string $password): string { + if (!class_exists('AKSRT_Order_Repository') || !class_exists('AKSRT_Domain_Model')) { + return 'CARD' . $card_id . '_' . time(); + } + + $product_type = AKSRT_Domain_Model::ORDER_PRODUCT_RECHARGE; + $product_id = $card_id; + $amount = 0.0; + $currency = 'CNY'; + + if ($type === self::TYPE_VIP) { + $product_type = AKSRT_Domain_Model::ORDER_PRODUCT_VIP; + $amount = 0.0; + } elseif ($type === self::TYPE_BALANCE) { + $amount = round((float) ($meta['amount'] ?? 0), 2); + $currency = 'CNY'; + } elseif ($type === self::TYPE_CREDITS) { + $amount = (float) ($meta['points'] ?? 0); + $currency = 'points'; + } + + $note = wp_json_encode([ + 'card_pass' => 1, + 'card_id' => $card_id, + 'card_type' => $type, + 'meta' => $meta, + 'password' => substr(md5($password), 0, 8), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + return AKSRT_Order_Repository::get_instance()->create([ + 'user_id' => $user_id, + 'product_type' => $product_type, + 'product_id' => $product_id, + 'amount' => $amount, + 'currency' => $currency, + 'status' => class_exists('AKSRT_Order') ? AKSRT_Order::STATUS_PENDING : 'pending', + 'payment_method' => 'card_pass', + 'note' => is_string($note) ? $note : '', + 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), + ]); + } + + /** + * @return true|WP_Error + */ + private function assert_type_enabled(string $type) { + $opts = $this->get_public_options(); + if ($type === self::TYPE_BALANCE && empty($opts['balance']['enabled'])) { + return new WP_Error('aksrt_card_disabled', __('余额卡密兑换未开启', 'aksrt'), ['status' => 403]); + } + if ($type === self::TYPE_VIP && empty($opts['vip']['enabled'])) { + return new WP_Error('aksrt_card_disabled', __('会员卡密兑换未开启', 'aksrt'), ['status' => 403]); + } + if ($type === self::TYPE_CREDITS && empty($opts['credits']['enabled'])) { + return new WP_Error('aksrt_card_disabled', __('积分卡密兑换未开启', 'aksrt'), ['status' => 403]); + } + return true; + } + + /** + * @param array $data + * @return array|null + */ + private function normalize_meta(string $type, array $data): ?array { + if (is_array($data['meta'] ?? null)) { + $raw = $data['meta']; + } else { + $raw = $data; + } + + if ($type === self::TYPE_BALANCE) { + $amount = round((float) ($raw['amount'] ?? $data['amount'] ?? 0), 2); + if ($amount <= 0) { + return null; + } + return ['amount' => $amount]; + } + if ($type === self::TYPE_VIP) { + $level = max(1, min(2, (int) ($raw['level'] ?? $data['level'] ?? 1))); + $days = max(0, (int) ($raw['days'] ?? $data['days'] ?? $data['vip_days'] ?? 0)); + return ['level' => $level, 'days' => $days]; + } + if ($type === self::TYPE_CREDITS) { + $points = max(0, (int) ($raw['points'] ?? $data['points'] ?? $data['credits'] ?? 0)); + if ($points <= 0) { + return null; + } + return ['points' => $points]; + } + return null; + } + + /** + * @param array $row + * @return array + */ + private function decode_meta(array $row): array { + $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; + } + } + } + // Legacy amount-only rows. + if ($meta === [] && isset($row['amount'])) { + $meta = ['amount' => (float) $row['amount']]; + } + return $meta; + } + + /** + * Admin list helper (supports new + legacy columns). + * + * @return array{items:array,total:int,page:int,max_pages:int} */ public function get_list(array $args = []): array { global $wpdb; @@ -142,46 +599,47 @@ class AKSRT_Charge_Card { $limit = max(1, min(100, (int) ($args['limit'] ?? 20))); $offset = max(0, (int) ($args['offset'] ?? 0)); $status = sanitize_key((string) ($args['status'] ?? '')); + $type = sanitize_key((string) ($args['type'] ?? '')); - $where = []; + $where = []; $params = []; - if ($status !== '') { - $where[] = 'status = %s'; + $where[] = 'status = %s'; $params[] = $status; } - + if ($type !== '') { + $where[] = 'type = %s'; + $params[] = $type; + } $where_sql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $count_sql = "SELECT COUNT(*) FROM {$this->table()} {$where_sql}"; - $total = $params ? (int) $wpdb->get_var($wpdb->prepare($count_sql, $params)) : (int) $wpdb->get_var($count_sql); + $total = $params + ? (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"; $params[] = $limit; $params[] = $offset; - $items = $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A); return [ 'items' => $items ?: [], 'total' => $total, - 'page' => floor($offset / $limit) + 1, - 'max_pages' => ceil($total / $limit), + 'page' => (int) floor($offset / $limit) + 1, + 'max_pages' => (int) ceil(max(1, $total) / $limit), ]; } - /** - * 禁用充值卡 - */ public function disable(int $id): bool { global $wpdb; return (bool) $wpdb->update($this->table(), [ 'status' => self::STATUS_DISABLED, - ], ['id' => $id]); + ], ['id' => $id], ['%s'], ['%d']); } /** - * 获取统计 + * @return array */ public function get_stats(): array { global $wpdb; @@ -206,4 +664,21 @@ class AKSRT_Charge_Card { 'used_amount' => (float) ($row['used_amount'] ?? 0), ]; } + + /** + * Legacy alias used by older admin code. + * + * @return array{success:bool,message:string,amount?:float} + */ + public function redeem(string $code, int $user_id): array { + $result = $this->redeem_for_user($user_id, $code, '', self::TYPE_BALANCE); + if (is_wp_error($result)) { + return ['success' => false, 'message' => $result->get_error_message()]; + } + return [ + 'success' => true, + 'message' => (string) ($result['message'] ?? __('兑换成功', 'aksrt')), + 'amount' => (float) ($result['amount'] ?? 0), + ]; + } } diff --git a/inc/classes/class-aksrt-db-migrator.php b/inc/classes/class-aksrt-db-migrator.php index 73e6dd19..f584f8ff 100644 --- a/inc/classes/class-aksrt-db-migrator.php +++ b/inc/classes/class-aksrt-db-migrator.php @@ -19,7 +19,7 @@ class AKSRT_DB_Migrator { use AKSRT_DB_Migrator_Part_3; /** Current schema version; changing it re-runs dbDelta. */ - const SCHEMA_VERSION = '1.13.0'; + const SCHEMA_VERSION = '1.14.0'; const OPT_KEY = 'aksrt_db_schema_version'; diff --git a/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php b/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php index 54d9e194..a7d80111 100644 --- a/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php +++ b/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php @@ -348,22 +348,29 @@ trait AKSRT_DB_Migrator_Part_2 { ) {$charset_collate};"; dbDelta($sql_shipping); - // ── charge_card ── + // ── charge_card (zibll-style typed card_pass) ── $sql_charge_card = "CREATE TABLE {$prefix}aksrt_charge_card ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, - code VARCHAR(60) NOT NULL DEFAULT '', + card VARCHAR(60) NOT NULL DEFAULT '', + password VARCHAR(80) NOT NULL DEFAULT '', + type VARCHAR(20) NOT NULL DEFAULT 'balance', amount DECIMAL(10,2) NOT NULL DEFAULT 0, balance DECIMAL(10,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'active', + meta LONGTEXT DEFAULT NULL, + order_number VARCHAR(32) NOT NULL DEFAULT '', used_by BIGINT UNSIGNED DEFAULT NULL, used_at DATETIME DEFAULT NULL, expires_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', note TEXT DEFAULT NULL, PRIMARY KEY (id), - UNIQUE KEY idx_code (code), + UNIQUE KEY idx_password (password), + KEY idx_card (card), + KEY idx_type (type), KEY idx_status (status), KEY idx_used_by (used_by), + KEY idx_order_number (order_number), KEY idx_expires (expires_at) ) {$charset_collate};"; dbDelta($sql_charge_card); diff --git a/inc/classes/settings/class-aksrt-settings-payment-runtime.php b/inc/classes/settings/class-aksrt-settings-payment-runtime.php index 0a8118b6..97909e77 100644 --- a/inc/classes/settings/class-aksrt-settings-payment-runtime.php +++ b/inc/classes/settings/class-aksrt-settings-payment-runtime.php @@ -196,6 +196,7 @@ class AKSRT_Settings_Payment_Runtime { 'download_speed' => max(0, min(99999, (int) ($settings['pay_download_speed'] ?? 0))), 'pass_charge' => !empty($settings['pay_vip_pass_charge_s']), 'pass_charge_desc' => sanitize_textarea_field((string) ($settings['pay_vip_pass_charge_desc'] ?? '')), + 'pass_charge_only_password' => !isset($settings['pay_vip_pass_charge_only_password']) || !empty($settings['pay_vip_pass_charge_only_password']), ], 'renew' => [ 'enabled' => !isset($settings['vip_renew']) || !empty($settings['vip_renew']), diff --git a/inc/helpers/user-center-renderers/credits-balance.php b/inc/helpers/user-center-renderers/credits-balance.php index 3bdf8561..7bd04c31 100644 --- a/inc/helpers/user-center-renderers/credits-balance.php +++ b/inc/helpers/user-center-renderers/credits-balance.php @@ -1,6 +1,6 @@ false, 'desc' => '', 'only_password' => true]; +if (class_exists('AKSRT_Charge_Card')) { + $opts = AKSRT_Charge_Card::get_instance()->get_public_options(); + $card_pass_balance = is_array($opts['balance'] ?? null) ? $opts['balance'] : $card_pass_balance; +} elseif (class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_payment_recharge_settings')) { + $recharge = AKSRT_Settings::get_payment_recharge_settings(); + $pc = is_array($recharge['pass_charge'] ?? null) ? $recharge['pass_charge'] : []; + $card_pass_balance = [ + 'enabled' => !empty($pc['enabled']), + 'desc' => (string) ($pc['desc'] ?? ''), + 'only_password' => !isset($pc['only_password']) || !empty($pc['only_password']), + ]; +} ?>
@@ -34,3 +48,23 @@ $credits_enabled = !empty($credits_enabled);
+ + +
+
+ + +
+ +

+ + + + + + +

+
+ diff --git a/inc/helpers/user-center-renderers/credits-vip.php b/inc/helpers/user-center-renderers/credits-vip.php index b1776b1e..450c1fb5 100644 --- a/inc/helpers/user-center-renderers/credits-vip.php +++ b/inc/helpers/user-center-renderers/credits-vip.php @@ -70,6 +70,18 @@ if ($vip_level > 0) { $shop_title = __('续费 / 升级会员', 'aksrt'); } $points_pkgs = is_array($points_exchange['packages'] ?? null) ? $points_exchange['packages'] : []; + +$card_pass_vip = ['enabled' => false, 'desc' => '', 'only_password' => true]; +if (class_exists('AKSRT_Charge_Card')) { + $card_opts = AKSRT_Charge_Card::get_instance()->get_public_options(); + $card_pass_vip = is_array($card_opts['vip'] ?? null) ? $card_opts['vip'] : $card_pass_vip; +} elseif (!empty($vip_settings['benefit']['pass_charge'])) { + $card_pass_vip = [ + 'enabled' => true, + 'desc' => (string) ($vip_settings['benefit']['pass_charge_desc'] ?? ''), + 'only_password' => !isset($vip_settings['benefit']['pass_charge_only_password']) || !empty($vip_settings['benefit']['pass_charge_only_password']), + ]; +} ?>
@@ -241,3 +253,23 @@ $points_pkgs = is_array($points_exchange['packages'] ?? null) ? $points_exchange

+ + +
+
+ + +
+ +

+ + + + + + +

+
+