diff --git a/assets/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css index 4f95407c..5e552773 100644 --- a/assets/css/routes/user-center/credits.css +++ b/assets/css/routes/user-center/credits.css @@ -321,3 +321,194 @@ color: var(--color-ink-4); margin: 0; } + +/* ==================== VIP 会员开通 ==================== */ + +.uc-vip-card { + position: relative; + border-radius: var(--radius); + overflow: hidden; + margin-bottom: 16px; + box-shadow: 0 4px 20px rgba(245, 158, 11, 0.18); +} +.uc-vip-card__bg { + position: absolute; + inset: 0; + background: linear-gradient(135deg, #f59e0b 0%, #f97316 55%, #ef4444 100%); + z-index: 0; +} +.uc-vip-card--active .uc-vip-card__bg { + background: linear-gradient(135deg, #0ea5e9 0%, #6366f1 55%, #8b5cf6 100%); +} +.uc-vip-card__content { + position: relative; + z-index: 1; + padding: 22px; + color: #fff; +} +.uc-vip-card__header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} +.uc-vip-card__icon { + width: 42px; + height: 42px; + border-radius: 12px; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(255,255,255,0.18); + font-size: 18px; +} +.uc-vip-card__label { + display: block; + font-size: 16px; + font-weight: 700; +} +.uc-vip-card__status { + display: inline-block; + margin-top: 4px; + font-size: 12px; + opacity: 0.9; + padding: 2px 8px; + border-radius: 999px; + background: rgba(255,255,255,0.16); +} +.uc-vip-card__status--active { + background: rgba(16, 185, 129, 0.25); +} +.uc-vip-card__benefits { + display: flex; + flex-wrap: wrap; + gap: 8px 12px; + font-size: 12px; + opacity: 0.95; +} +.uc-vip-card__benefit i { margin-right: 4px; } +.uc-vip-card__desc { + margin: 12px 0 0; + font-size: 12px; + opacity: 0.85; + line-height: 1.5; +} + +.uc-vip-shop { + background: var(--surface-1, #fff); + border: 1px solid var(--color-line-soft, rgba(0,0,0,0.06)); + border-radius: var(--radius, 10px); + padding: 16px 18px 18px; + margin-bottom: 20px; +} +.uc-vip-shop__title { + display: flex; + align-items: center; + gap: 8px; + font-size: 15px; + font-weight: 700; + color: var(--color-ink, #111); + margin-bottom: 14px; +} +.uc-vip-shop__title i { color: #f59e0b; } +.uc-vip-shop__group { margin-bottom: 16px; } +.uc-vip-shop__group-title { + font-size: 13px; + font-weight: 600; + color: var(--color-ink-2, #374151); + margin-bottom: 10px; + display: flex; + align-items: center; + gap: 8px; +} +.uc-vip-shop__badge { + font-size: 11px; + font-weight: 600; + color: #047857; + background: #ecfdf5; + border-radius: 999px; + padding: 2px 8px; +} +.uc-vip-shop__grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 10px; +} +.uc-vip-shop__item { + appearance: none; + border: 1px solid var(--color-line, #e5e7eb); + background: var(--surface-0, #fafafa); + border-radius: 12px; + padding: 14px 12px; + text-align: left; + cursor: pointer; + display: flex; + flex-direction: column; + gap: 4px; + transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; + color: inherit; + font: inherit; +} +.uc-vip-shop__item:hover { + border-color: #f59e0b; + box-shadow: 0 8px 20px rgba(245, 158, 11, 0.12); + transform: translateY(-1px); +} +.uc-vip-shop__item.is-loading { + opacity: 0.65; + pointer-events: none; +} +.uc-vip-shop__item-title { + font-size: 14px; + font-weight: 700; + color: var(--color-ink, #111); +} +.uc-vip-shop__item-days { + font-size: 12px; + color: var(--color-ink-3, #6b7280); +} +.uc-vip-shop__item-price { + margin-top: 6px; + font-size: 18px; + font-weight: 800; + color: #dc2626; +} +.uc-vip-shop__item-cta { + margin-top: 8px; + font-size: 12px; + font-weight: 600; + color: #b45309; +} +.uc-vip-shop__methods { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; + margin-top: 8px; + padding-top: 12px; + border-top: 1px solid var(--color-line-soft, rgba(0,0,0,0.06)); +} +.uc-vip-shop__methods-label { + font-size: 12px; + font-weight: 600; + color: var(--color-ink-3, #6b7280); +} +.uc-vip-shop__method { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + cursor: pointer; +} +.uc-vip-shop__tip { + margin: 10px 0 0; + font-size: 12px; + color: var(--color-ink-3, #6b7280); + min-height: 1.2em; +} +.uc-vip-shop__tip.is-error { color: #dc2626; } +.uc-vip-shop__empty { + font-size: 13px; + color: var(--color-ink-3, #6b7280); + margin: 0 0 16px; +} diff --git a/assets/js/credits.js b/assets/js/credits.js index 6b2c5ef0..827336e1 100644 --- a/assets/js/credits.js +++ b/assets/js/credits.js @@ -317,13 +317,98 @@ }); } + + // ===== VIP 开通 ===== + function initVipShop() { + var root = document.querySelector('[data-aksrt-vip-shop]'); + if (!root) return; + var tip = root.querySelector('[data-aksrt-vip-tip]'); + var busy = false; + + function setTip(text, isError) { + if (!tip) return; + tip.textContent = text || ''; + tip.classList.toggle('is-error', !!isError); + } + + function selectedMethod() { + var fixed = root.querySelector('[data-aksrt-vip-method-fixed]'); + if (fixed && fixed.value) return fixed.value; + var checked = root.querySelector('input[name="aksrt_vip_pay_method"]:checked'); + if (checked) return checked.value; + var first = root.querySelector('input[name="aksrt_vip_pay_method"]'); + return first ? first.value : 'balance'; + } + + function pollOrder(orderNumber) { + if (!orderNumber) return; + var tries = 0; + var maxTries = 40; + var timer = setInterval(function () { + tries += 1; + api('/pay/content/order/' + encodeURIComponent(orderNumber)).then(function (r) { + if (r.ok && r.json && r.json.paid) { + clearInterval(timer); + setTip('支付成功,正在刷新…'); + window.location.reload(); + } + }).catch(function () { /* ignore */ }); + if (tries >= maxTries) clearInterval(timer); + }, 3000); + } + + root.addEventListener('click', function (e) { + var btn = e.target.closest('[data-aksrt-vip-buy]'); + if (!btn || busy) return; + e.preventDefault(); + var packageId = parseInt(btn.getAttribute('data-package-id') || '0', 10); + if (!packageId) return; + busy = true; + btn.classList.add('is-loading'); + setTip('正在创建订单…'); + + api('/pay/vip', { + method: 'POST', + body: JSON.stringify({ + package_id: packageId, + payment_method: selectedMethod() + }) + }).then(function (r) { + busy = false; + btn.classList.remove('is-loading'); + if (!r.ok || (r.json && r.json.code && !r.json.success)) { + var msg = (r.json && r.json.message) || '开通失败'; + setTip(msg, true); + return; + } + var data = (r.json && r.json.data) ? r.json.data : r.json; + if (data && data.redirect && (data.payment_url || data.qr_content)) { + if (data.payment_url) { + window.open(data.payment_url, '_blank'); + } + setTip(data.message || '请完成支付,支付成功后将自动刷新。'); + if (data.order_number) pollOrder(data.order_number); + return; + } + setTip((data && data.message) || '开通成功,正在刷新…'); + window.location.reload(); + }).catch(function () { + busy = false; + btn.classList.remove('is-loading'); + setTip('网络错误,请重试', true); + }); + }); + } + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { initSignin(); initInvite(); + initVipShop(); }); } else { initSignin(); initInvite(); + initVipShop(); } })(); diff --git a/functions.php b/functions.php index ef40e883..21d363f2 100644 --- a/functions.php +++ b/functions.php @@ -177,6 +177,9 @@ if (class_exists('AKSRT_Download')) { if (class_exists('AKSRT_Content_Pay')) { AKSRT_Content_Pay::get_instance()->init(); } +if (class_exists('AKSRT_Vip_Service')) { + AKSRT_Vip_Service::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/bootstrap.php b/inc/bootstrap.php index 94933f55..d07db532 100644 --- a/inc/bootstrap.php +++ b/inc/bootstrap.php @@ -220,6 +220,9 @@ require_once AKSRT_DIR . '/inc/classes/class-aksrt-download.php'; // 付费内容收银台(post/_aksrt_pay → resource 订单) require_once AKSRT_DIR . '/inc/classes/class-aksrt-content-pay.php'; +// VIP 开通服务(package → ORDER_PRODUCT_VIP) +require_once AKSRT_DIR . '/inc/classes/class-aksrt-vip-service.php'; + // 下载状态统一解析器 require_once AKSRT_DIR . '/inc/classes/class-aksrt-download-state.php'; diff --git a/inc/classes/class-aksrt-content-pay.php b/inc/classes/class-aksrt-content-pay.php index f72252e1..e78d7514 100644 --- a/inc/classes/class-aksrt-content-pay.php +++ b/inc/classes/class-aksrt-content-pay.php @@ -306,9 +306,11 @@ class AKSRT_Content_Pay { $base_label = $this->format_amount_label($base, $pay_modo, $currency); $over_label = $over_price > 0 ? $this->format_amount_label($over_price, $pay_modo, $currency) : ''; - $vip_page = function_exists('aksrt_get_user_center_url') - ? aksrt_get_user_center_url() - : home_url('/'); + $vip_page = function_exists('aksrt_get_user_center_tab_url') + ? aksrt_get_user_center_tab_url('credits') + : (function_exists('aksrt_get_user_center_url') + ? aksrt_get_user_center_url() + : home_url('/')); $free_limit = $uid > 0 && function_exists('aksrt_get_user_free_download_limit') ? aksrt_get_user_free_download_limit($uid) : 0; diff --git a/inc/classes/class-aksrt-vip-service.php b/inc/classes/class-aksrt-vip-service.php new file mode 100644 index 00000000..13c7ed63 --- /dev/null +++ b/inc/classes/class-aksrt-vip-service.php @@ -0,0 +1,504 @@ + 'GET', + 'callback' => [$this, 'rest_catalog'], + 'permission_callback' => [$this, 'check_rest_nonce'], + ], + [ + 'methods' => 'POST', + 'callback' => [$this, 'rest_purchase'], + 'permission_callback' => [$this, 'check_rest_nonce'], + ], + ]); + } + + /** + * @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; + } + + /** + * GET /pay/vip + */ + public function rest_catalog(WP_REST_Request $request) { + $uid = get_current_user_id(); + return new WP_REST_Response($this->get_catalog($uid), 200); + } + + /** + * POST /pay/vip + * body: { package_id: int, payment_method?: string } + */ + public function rest_purchase(WP_REST_Request $request) { + $uid = get_current_user_id(); + if ($uid <= 0) { + return new WP_Error('aksrt_need_login', __('请先登录后再开通会员。', 'aksrt'), [ + 'status' => 401, + 'login_url' => function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(), + ]); + } + + $params = $request->get_json_params(); + if (!is_array($params)) { + $params = $request->get_params(); + } + $package_id = (int) ($params['package_id'] ?? 0); + $payment_method = sanitize_key((string) ($params['payment_method'] ?? 'balance')); + if ($payment_method === '' || $payment_method === 'cash') { + $payment_method = 'balance'; + } + + $result = $this->purchase($uid, $package_id, $payment_method); + if (is_wp_error($result)) { + return $result; + } + if (empty($result['success'])) { + return new WP_Error( + sanitize_key((string) ($result['code'] ?? 'aksrt_vip_pay_failed')), + (string) ($result['message'] ?? __('开通失败', 'aksrt')), + ['status' => (int) ($result['status'] ?? 400)] + ); + } + return new WP_REST_Response($result, 200); + } + + /** + * @return array + */ + public function get_catalog(int $user_id = 0): array { + $vip = class_exists('AKSRT_Settings') + ? AKSRT_Settings::get_payment_vip_settings() + : ['enabled' => false, 'packages_raw' => '', 'levels' => []]; + + $level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; + $expires = $user_id > 0 ? (int) get_user_meta($user_id, 'aksrt_vip_expires', true) : 0; + $packages = $this->parse_packages((string) ($vip['packages_raw'] ?? '')); + $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; + + $level_packages = []; + foreach ([1, 2] as $lv) { + if (empty($levels[$lv]['enabled'])) { + continue; + } + $name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv)); + $items = []; + foreach ($packages as $idx => $pkg) { + $items[] = [ + 'package_id' => $this->encode_package_id($lv, $idx + 1), + 'level' => $lv, + 'index' => $idx + 1, + 'price' => (float) $pkg['price'], + 'days' => (int) $pkg['days'], + 'title' => (string) $pkg['title'], + 'price_label' => $this->format_cash_label((float) $pkg['price']), + 'days_label' => ((int) $pkg['days'] <= 0) + ? __('永久', 'aksrt') + : sprintf( + /* translators: %d: days */ + __('%d 天', 'aksrt'), + (int) $pkg['days'] + ), + ]; + } + if ($items === []) { + continue; + } + $level_packages[] = [ + 'level' => $lv, + 'name' => $name, + 'items' => $items, + ]; + } + + $methods = $this->available_payment_methods(); + + return [ + 'enabled' => !empty($vip['enabled']), + 'desc' => (string) ($vip['desc'] ?? ''), + 'help_text' => (string) ($vip['help_text'] ?? ''), + 'user_level' => $level, + 'user_expires' => $expires, + 'user_expires_label' => $this->format_expires_label($expires, $level), + 'level_packages' => $level_packages, + 'payment_methods' => $methods, + 'vip_url' => function_exists('aksrt_get_user_center_tab_url') + ? aksrt_get_user_center_tab_url('credits') + : (function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/')), + 'logged_in' => $user_id > 0, + ]; + } + + /** + * @return array|WP_Error + */ + public function purchase(int $user_id, int $package_id, string $payment_method = 'balance') { + if ($user_id <= 0) { + return new WP_Error('aksrt_need_login', __('请先登录后再开通会员。', 'aksrt'), ['status' => 401]); + } + + $decoded = $this->decode_package_id($package_id); + if (!$decoded) { + return new WP_Error('aksrt_invalid_package', __('套餐无效', 'aksrt'), ['status' => 400]); + } + $level = (int) $decoded['level']; + $index = (int) $decoded['index']; + + $vip = class_exists('AKSRT_Settings') + ? AKSRT_Settings::get_payment_vip_settings() + : ['enabled' => false, 'packages_raw' => '', 'levels' => []]; + if (empty($vip['enabled'])) { + return new WP_Error('aksrt_vip_disabled', __('会员功能未启用', 'aksrt'), ['status' => 400]); + } + $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; + if (empty($levels[$level]['enabled'])) { + return new WP_Error('aksrt_vip_level_disabled', __('该会员等级未启用', 'aksrt'), ['status' => 400]); + } + + $packages = $this->parse_packages((string) ($vip['packages_raw'] ?? '')); + $pkg = $packages[$index - 1] ?? null; + if (!$pkg) { + return new WP_Error('aksrt_invalid_package', __('套餐不存在', 'aksrt'), ['status' => 400]); + } + + $amount = round((float) $pkg['price'], 2); + if ($amount <= 0) { + return new WP_Error('aksrt_invalid_price', __('套餐价格无效', 'aksrt'), ['status' => 400]); + } + + if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) { + return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用', 'aksrt'), ['status' => 500]); + } + + $payment_method = sanitize_key($payment_method); + if ($payment_method === '') { + $payment_method = 'balance'; + } + + $days = (int) $pkg['days']; + $note = wp_json_encode([ + 'vip_pay' => 1, + 'level' => $level, + 'days' => $days, + 'package_index' => $index, + 'package_title' => (string) $pkg['title'], + 'package_id' => $package_id, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + $order_number = AKSRT_Order_Repository::get_instance()->create([ + 'user_id' => $user_id, + 'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_VIP, + 'product_id' => $package_id, + 'amount' => $amount, + 'currency' => 'CNY', + 'status' => AKSRT_Order::STATUS_PENDING, + 'payment_method' => $payment_method, + 'note' => is_string($note) ? $note : '', + 'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS), + ]); + if ($order_number === '') { + return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]); + } + + $order = AKSRT_Order_Repository::get_instance()->get($order_number); + if (!$order) { + return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]); + } + + $level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level)); + $order['subject'] = sprintf( + /* translators: 1: vip name 2: package title */ + __('开通%s · %s', 'aksrt'), + $level_name, + (string) $pkg['title'] + ); + $order['payment_method'] = $payment_method; + + if (!class_exists('AKSRT_Payment_Gateway')) { + return new WP_Error('aksrt_pay_unavailable', __('支付网关不可用', 'aksrt'), ['status' => 500]); + } + $gateway = AKSRT_Payment_Gateway::get_instance()->get_gateway($payment_method); + if (!$gateway) { + return new WP_Error('aksrt_gateway_unavailable', __('当前支付方式不可用', 'aksrt'), [ + 'status' => 400, + 'order_number' => $order_number, + ]); + } + if (method_exists($gateway, 'is_configured') && !$gateway->is_configured()) { + return new WP_Error('aksrt_gateway_unconfigured', __('该支付方式尚未配置,请联系站长。', 'aksrt'), [ + 'status' => 400, + 'order_number' => $order_number, + ]); + } + + $payment_result = $gateway->create_payment($order); + if (empty($payment_result['success'])) { + $msg = (string) ($payment_result['message'] ?? __('支付失败', 'aksrt')); + $code = ($payment_method === 'balance' && (str_contains($msg, '余额') || str_contains($msg, '不足'))) + ? 'aksrt_balance_insufficient' + : 'aksrt_pay_failed'; + return new WP_Error($code, $msg, [ + 'status' => 402, + 'order_number' => $order_number, + ]); + } + + $code_url = (string) ($payment_result['payment_data']['code_url'] ?? ''); + $payment_url = (string) ($payment_result['payment_url'] ?? ''); + if ($payment_url !== '' || $code_url !== '') { + return [ + 'success' => true, + 'data' => [ + 'redirect' => true, + 'payment_url' => $payment_url, + 'qr_content' => $code_url, + 'order_number' => $order_number, + 'amount' => $amount, + 'message' => $payment_result['message'] ?? __('请在新页面完成支付。', 'aksrt'), + 'reload' => false, + 'poll' => true, + ], + ]; + } + + $paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [ + 'payment_method' => $payment_method, + 'payment_transaction_id' => sanitize_text_field((string) ($payment_result['transaction_id'] ?? '')), + 'note' => __('用户完成会员开通支付。', 'aksrt'), + ]); + if (!$paid) { + return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败', 'aksrt'), ['status' => 500]); + } + + return [ + 'success' => true, + 'data' => [ + 'order_number' => $order_number, + 'amount' => $amount, + 'level' => $level, + 'days' => $days, + 'message' => __('会员开通成功', 'aksrt'), + 'reload' => true, + ], + ]; + } + + /** + * Activate VIP on paid order. + * + * @param string $order_number + * @param array $paid_order + */ + public function on_order_paid(string $order_number, array $paid_order): void { + if (sanitize_key((string) ($paid_order['product_type'] ?? '')) !== AKSRT_Domain_Model::ORDER_PRODUCT_VIP) { + return; + } + $user_id = (int) ($paid_order['user_id'] ?? 0); + if ($user_id <= 0) { + return; + } + + $meta = []; + if (!empty($paid_order['note']) && is_string($paid_order['note'])) { + $decoded = json_decode($paid_order['note'], true); + if (is_array($decoded)) { + $meta = $decoded; + } + } + if (empty($meta) && is_array($paid_order['meta_payload'] ?? null)) { + $meta = $paid_order['meta_payload']; + } + + $level = (int) ($meta['level'] ?? 0); + $days = (int) ($meta['days'] ?? 0); + if ($level <= 0) { + $decoded = $this->decode_package_id((int) ($paid_order['product_id'] ?? 0)); + if ($decoded) { + $level = (int) $decoded['level']; + } + } + if ($level < 1) { + $level = 1; + } + if ($level > 2) { + $level = 2; + } + + $this->activate_vip($user_id, $level, $days); + } + + /** + * Apply membership meta. + */ + public function activate_vip(int $user_id, int $level, int $days = 0): void { + if ($user_id <= 0) { + return; + } + $level = max(1, min(2, $level)); + $current_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true); + $new_level = max($current_level, $level); + update_user_meta($user_id, 'aksrt_vip_level', $new_level); + + if ($days <= 0) { + // Permanent. + update_user_meta($user_id, 'aksrt_vip_expires', 0); + return; + } + + $current_exp = (int) get_user_meta($user_id, 'aksrt_vip_expires', true); + if ($current_exp === 0 && $current_level > 0) { + // Already permanent — keep permanent. + return; + } + $base = max($current_exp, time()); + update_user_meta($user_id, 'aksrt_vip_expires', $base + ($days * DAY_IN_SECONDS)); + } + + /** + * Parse packages_raw lines: price|days|title + * + * @return array + */ + public function parse_packages(string $raw): array { + $lines = preg_split('/\r\n|\r|\n/', $raw) ?: []; + $out = []; + foreach ($lines as $line) { + $line = trim((string) $line); + if ($line === '') { + continue; + } + $parts = array_map('trim', explode('|', $line)); + $price = isset($parts[0]) ? (float) $parts[0] : 0.0; + $days = isset($parts[1]) ? (int) $parts[1] : 0; + $title = isset($parts[2]) ? sanitize_text_field($parts[2]) : ''; + if ($price <= 0) { + continue; + } + if ($title === '') { + $title = $days > 0 + ? sprintf(__('%d天会员', 'aksrt'), $days) + : __('永久会员', 'aksrt'); + } + $out[] = [ + 'price' => round($price, 2), + 'days' => max(0, $days), + 'title' => $title, + ]; + } + return $out; + } + + public function encode_package_id(int $level, int $index): int { + return (max(1, min(2, $level)) * 1000) + max(1, $index); + } + + /** + * @return array{level:int,index:int}|null + */ + public function decode_package_id(int $package_id): ?array { + if ($package_id < 1001) { + return null; + } + $level = intdiv($package_id, 1000); + $index = $package_id % 1000; + if ($level < 1 || $level > 2 || $index < 1) { + return null; + } + return ['level' => $level, 'index' => $index]; + } + + /** + * @return array + */ + private function available_payment_methods(): array { + $methods = []; + if (!class_exists('AKSRT_Payment_Gateway')) { + return [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]; + } + $gw = AKSRT_Payment_Gateway::get_instance(); + foreach (['balance', 'alipay', 'wechatpay', 'paypal', 'stripe'] as $id) { + $g = $gw->get_gateway($id); + if (!$g) { + continue; + } + if (method_exists($g, 'is_configured') && !$g->is_configured()) { + continue; + } + $methods[] = [ + 'id' => $id, + 'name' => method_exists($g, 'get_name') ? (string) $g->get_name() : $id, + ]; + } + if ($methods === []) { + $methods[] = ['id' => 'balance', 'name' => __('站内余额', 'aksrt')]; + } + return $methods; + } + + private function format_cash_label(float $amount): string { + $symbol = '¥'; + if (class_exists('AKSRT_Settings')) { + $paid = AKSRT_Settings::get_paid_content_settings(); + $symbol = (string) ($paid['display']['currency_symbol'] ?? '¥'); + } + $n = function_exists('aksrt_format_price') ? aksrt_format_price($amount) : (string) $amount; + return $symbol . $n; + } + + private function format_expires_label(int $expires, int $level): string { + if ($level <= 0) { + return __('未开通', 'aksrt'); + } + if ($expires <= 0) { + return __('永久有效', 'aksrt'); + } + if ($expires < time()) { + return __('已过期', 'aksrt'); + } + return sprintf( + /* translators: %s: date */ + __('到期时间:%s', 'aksrt'), + wp_date('Y-m-d H:i', $expires) + ); + } +} diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index 199dba38..12368b4d 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -958,7 +958,9 @@ if (!function_exists('aksrt_get_paywall_html')) { $has_original = $original_price > 0 && $original_price > $price; $pay_limit = (int) ($meta['pay_limit'] ?? 0); $member_blocked = $pay_limit > 0 && $vip_level < $pay_limit; - $vip_url = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/'); + $vip_url = function_exists('aksrt_get_user_center_tab_url') + ? aksrt_get_user_center_tab_url('credits') + : (function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/')); ob_start(); ?> diff --git a/inc/helpers/aksrt-vip.php b/inc/helpers/aksrt-vip.php index ace5c53e..5f23fd8b 100644 --- a/inc/helpers/aksrt-vip.php +++ b/inc/helpers/aksrt-vip.php @@ -333,18 +333,58 @@ function aksrt_render_vip_badge(int $user_id = 0, string $size = 'sm'): string { * 获取用户 VIP 等级信息(用于展示) * * @param int $user_id - * @return array{is_vip: bool, level: int, badge_text: string, badge_html: string, name: string} + * @return array{is_vip: bool, level: int, badge_text: string, badge_html: string, name: string, expires: int, expires_label: string} */ function aksrt_get_vip_info(int $user_id = 0): array { $level = aksrt_get_user_vip_level($user_id); $is_vip = $level >= 1; $badge_text = aksrt_get_vip_badge_text(); + $expires = $user_id > 0 ? (int) get_user_meta($user_id, 'aksrt_vip_expires', true) : 0; + $expires_label = __('未开通', 'aksrt'); + if ($level > 0) { + if ($expires <= 0) { + $expires_label = __('永久有效', 'aksrt'); + } elseif ($expires < time()) { + $expires_label = __('已过期', 'aksrt'); + } else { + $expires_label = sprintf( + /* translators: %s: date */ + __('到期时间:%s', 'aksrt'), + wp_date('Y-m-d H:i', $expires) + ); + } + } + + $name = __('普通用户', 'aksrt'); + if ($is_vip && class_exists('AKSRT_Settings')) { + $vip = AKSRT_Settings::get_payment_vip_settings(); + $name = (string) ($vip['levels'][$level]['name'] ?? ($badge_text . __('会员', 'aksrt'))); + } elseif ($is_vip) { + $name = $badge_text . __('会员', 'aksrt'); + } return [ - 'is_vip' => $is_vip, - 'level' => $level, - 'badge_text' => $badge_text, - 'badge_html' => $is_vip ? aksrt_render_vip_badge($user_id) : '', - 'name' => $is_vip ? ($badge_text . '会员') : '普通用户', + 'is_vip' => $is_vip, + 'level' => $level, + 'badge_text' => $badge_text, + 'badge_html' => $is_vip ? aksrt_render_vip_badge($user_id) : '', + 'name' => $name, + 'expires' => $expires, + 'expires_label' => $expires_label, ]; } + +/** + * VIP 开通页入口(用户中心 credits 面板)。 + */ +if (!function_exists('aksrt_get_vip_purchase_url')) { + function aksrt_get_vip_purchase_url(): string { + if (function_exists('aksrt_get_user_center_tab_url')) { + return aksrt_get_user_center_tab_url('credits'); + } + if (function_exists('aksrt_get_user_center_url')) { + return aksrt_get_user_center_url(); + } + return home_url('/'); + } +} diff --git a/inc/helpers/user-center-renderers/credits-vip.php b/inc/helpers/user-center-renderers/credits-vip.php index 53dc27c1..e364d876 100644 --- a/inc/helpers/user-center-renderers/credits-vip.php +++ b/inc/helpers/user-center-renderers/credits-vip.php @@ -1,6 +1,6 @@ 0) { + if ($expires <= 0) { + $expires_label = __('永久有效', 'aksrt'); + } elseif ($expires < time()) { + $expires_label = __('已过期', 'aksrt'); + } else { + $expires_label = sprintf( + /* translators: %s: date */ + __('到期时间:%s', 'aksrt'), + wp_date('Y-m-d', $expires) + ); + } +} + +$level_name = (string) ($vip_settings['name'] ?? __('VIP会员', 'aksrt')); +if ($vip_level >= 1 && !empty($vip_settings['levels'][$vip_level]['name'])) { + $level_name = (string) $vip_settings['levels'][$vip_level]['name']; +} + +$packages = []; +if (class_exists('AKSRT_Vip_Service')) { + $catalog = AKSRT_Vip_Service::get_instance()->get_catalog($user_id); + $packages = is_array($catalog['level_packages'] ?? null) ? $catalog['level_packages'] : []; + $methods = is_array($catalog['payment_methods'] ?? null) ? $catalog['payment_methods'] : []; +} else { + $methods = [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]; +} + +$cash_symbol = '¥'; +if (class_exists('AKSRT_Settings')) { + $paid_display = AKSRT_Settings::get_paid_content_settings(); + $cash_symbol = (string) ($paid_display['display']['currency_symbol'] ?? '¥'); +} ?> -
+
@@ -24,27 +61,91 @@ $is_vip_user = function_exists('aksrt_is_vip') && aksrt_is_vip($user_id);
- + - +
-
+
- -
- - -
- - - + +

+ + +
+
+ + +
+ + +
+
+ + = $g_level) : ?> + + +
+
+ + + +
+
+ + + 1) : ?> +
+ + $m) : + $mid = (string) ($m['id'] ?? ''); + $mname = (string) ($m['name'] ?? $mid); + ?> + + +
+ + + + +

+
+ +

+ diff --git a/templates/parts/resource-download-box.php b/templates/parts/resource-download-box.php index ab4fd717..274d2fd9 100644 --- a/templates/parts/resource-download-box.php +++ b/templates/parts/resource-download-box.php @@ -62,7 +62,9 @@ if ($captcha_active && function_exists('aksrt_is_captcha_enabled_for') && !aksrt } $balance = (int) ($gate_data['balance'] ?? (is_user_logged_in() && class_exists('AKSRT_Credits') ? AKSRT_Credits::balance(get_current_user_id()) : 0)); $login_url = (string) ($gate_data['login_url'] ?? wp_login_url(get_permalink($post_id))); -$vip_url = function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/'); +$vip_url = function_exists('aksrt_get_user_center_tab_url') + ? aksrt_get_user_center_tab_url('credits') + : (function_exists('aksrt_get_user_center_url') ? aksrt_get_user_center_url() : home_url('/')); $is_need_login = $gate_blocked && $gate_code === 'aksrt_need_login'; $is_need_vip = $gate_blocked && $gate_code === 'aksrt_need_vip';