From 9eaddce457090121484e7c2283feebbcb22cf1ae Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Jul 2026 18:52:11 +0800 Subject: [PATCH] feat(pay): points-to-VIP exchange and remaining-days upgrade pricing Add pay_vip_points_exchange packages (level|points|days|title), REST redeem for non-VIP users, unit|daily upgrade product for residual VIP1 days, and UI. --- assets/css/routes/user-center/credits.css | 19 + assets/js/credits.js | 46 +- .../class-aksrt-admin-schema-payment.php | 9 +- inc/classes/class-aksrt-vip-service.php | 432 +++++++++++++++--- .../class-aksrt-settings-payment-runtime.php | 5 + .../user-center-renderers/credits-vip.php | 59 ++- 6 files changed, 504 insertions(+), 66 deletions(-) diff --git a/assets/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css index 28b06efe..d67914a3 100644 --- a/assets/css/routes/user-center/credits.css +++ b/assets/css/routes/user-center/credits.css @@ -541,3 +541,22 @@ color: #9ca3af; text-decoration: line-through; } + +.uc-vip-shop--points { + border-color: rgba(16, 185, 129, 0.25); +} +.uc-vip-shop__balance { + font-size: 13px; + font-weight: 600; + color: #047857; + margin: 0 0 10px; +} +.uc-vip-shop__item.is-disabled { + opacity: 0.55; + cursor: not-allowed; +} +.uc-vip-shop__item.is-disabled:hover { + transform: none; + box-shadow: none; + border-color: var(--color-line, #e5e7eb); +} diff --git a/assets/js/credits.js b/assets/js/credits.js index 827336e1..bb9b103d 100644 --- a/assets/js/credits.js +++ b/assets/js/credits.js @@ -318,7 +318,49 @@ } - // ===== VIP 开通 ===== + + // ===== VIP 积分兑换 ===== + function initVipPointsExchange() { + var root = document.querySelector('[data-aksrt-vip-points]'); + if (!root) return; + var tip = root.querySelector('[data-aksrt-vip-points-tip]'); + var busy = false; + function setTip(text, isError) { + if (!tip) return; + tip.textContent = text || ''; + tip.classList.toggle('is-error', !!isError); + } + root.addEventListener('click', function (e) { + var btn = e.target.closest('[data-aksrt-vip-points-buy]'); + if (!btn || busy || btn.disabled) 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/points-exchange', { + method: 'POST', + body: JSON.stringify({ package_id: packageId }) + }).then(function (r) { + busy = false; + btn.classList.remove('is-loading'); + 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.classList.remove('is-loading'); + setTip('网络错误,请重试', true); + }); + }); + } + +// ===== VIP 开通 ===== function initVipShop() { var root = document.querySelector('[data-aksrt-vip-shop]'); if (!root) return; @@ -405,10 +447,12 @@ initSignin(); initInvite(); initVipShop(); + initVipPointsExchange(); }); } else { initSignin(); initInvite(); initVipShop(); + initVipPointsExchange(); } })(); 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 9b652a40..3e853382 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-payment.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-payment.php @@ -107,7 +107,12 @@ class AKSRT_Admin_Schema_Payment { ['key' => 'pay_user_vip_desc', 'label' => '会员说明', 'type' => 'textarea', 'default' => ''], ]], ['key' => 'packages', 'title' => '会员套餐', 'fields' => [ - ['key' => 'payment_vip_packages', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|月卡\n365|365|年卡"], + ['key' => 'payment_vip_packages', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|月卡\n365|365|年卡", 'help' => '每行:现金价|天数|标题(天数为 0 表示永久)'], + ]], + ['key' => 'points_exchange', 'title' => '积分兑换会员', 'fields' => [ + ['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false], + ['key' => 'payment_vip_points_packages', 'label' => '兑换套餐', 'type' => 'textarea', 'default' => "1|500|30|积分月卡\n1|5000|0|积分永久", 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '每行:等级|积分|天数|标题(天数 0=永久;仅未开通会员可兑)'], + ['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true]], ]], ['key' => 'benefit', 'title' => '下载权益', 'fields' => [ ['key' => 'pay_download_limit', 'label' => '普通用户下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999], @@ -133,7 +138,7 @@ class AKSRT_Admin_Schema_Payment { ['key' => 'upgrade', 'title' => '升级设置', 'fields' => [ ['key' => 'vip_upgrade', 'label' => '允许升级', 'type' => 'switch', 'default' => true], ['key' => 'vip_upgrade_desc', 'label' => '升级说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true]], - ['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true]], + ['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '固定价:价格|天数|标题;按剩余天数:unit|每日单价|标题'], ]], ], ], diff --git a/inc/classes/class-aksrt-vip-service.php b/inc/classes/class-aksrt-vip-service.php index cc5d7429..cabeb8d7 100644 --- a/inc/classes/class-aksrt-vip-service.php +++ b/inc/classes/class-aksrt-vip-service.php @@ -41,6 +41,12 @@ class AKSRT_Vip_Service { 'permission_callback' => [$this, 'check_rest_nonce'], ], ]); + + register_rest_route('aksrt/v1', '/pay/vip/points-exchange', [ + 'methods' => 'POST', + 'callback' => [$this, 'rest_points_exchange'], + 'permission_callback' => [$this, 'check_rest_nonce'], + ]); } /** @@ -103,13 +109,34 @@ class AKSRT_Vip_Service { return new WP_REST_Response($result, 200); } + /** + * POST /pay/vip/points-exchange + * body: { package_id: int } // points package id = 5000 + index + */ + public function rest_points_exchange(WP_REST_Request $request) { + $uid = get_current_user_id(); + if ($uid <= 0) { + return new WP_Error('aksrt_need_login', __('请先登录后再兑换。', 'aksrt'), ['status' => 401]); + } + $params = $request->get_json_params(); + if (!is_array($params)) { + $params = $request->get_params(); + } + $package_id = (int) ($params['package_id'] ?? 0); + $result = $this->exchange_with_points($uid, $package_id); + if (is_wp_error($result)) { + return $result; + } + 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' => [], 'renew' => [], 'upgrade' => []]; + : ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => [], 'points_exchange' => []]; $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; @@ -118,23 +145,23 @@ class AKSRT_Vip_Service { $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; $renew = is_array($vip['renew'] ?? null) ? $vip['renew'] : []; $upgrade = is_array($vip['upgrade'] ?? null) ? $vip['upgrade'] : []; + $points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : []; $renew_enabled = !isset($renew['enabled']) || !empty($renew['enabled']); $upgrade_enabled = !isset($upgrade['enabled']) || !empty($upgrade['enabled']); + $points_enabled = !empty($points_ex['enabled']) + && (!class_exists('AKSRT_Settings') || AKSRT_Settings::credits_enabled()); $level_packages = []; foreach ([1, 2] as $lv) { if (empty($levels[$lv]['enabled'])) { continue; } - // Permanent VIP at same/higher level: only show higher-level upgrade packages. if ($is_permanent && $lv <= $level) { continue; } - // Existing VIP without renew: hide same-level packages. if ($level > 0 && $lv === $level && !$renew_enabled) { continue; } - // Existing VIP without upgrade: hide higher-level packages. if ($level > 0 && $lv > $level && !$upgrade_enabled) { continue; } @@ -144,8 +171,6 @@ class AKSRT_Vip_Service { foreach ($packages as $idx => $pkg) { $base_price = (float) $pkg['price']; $days = (int) $pkg['days']; - // Permanent members don't renew timed packages at same level (already filtered). - // Non-permanent same-level: renew pricing. $action = 'pay'; if ($level > 0 && $lv === $level) { $action = 'renew'; @@ -159,57 +184,34 @@ class AKSRT_Vip_Service { } $items[] = [ - 'package_id' => $this->encode_package_id($lv, $idx + 1), - 'level' => $lv, - 'index' => $idx + 1, - 'action' => $action, - 'price' => (float) $priced['price'], - 'base_price' => $base_price, - 'show_price' => (float) ($priced['show_price'] ?? 0), - 'discount_tag' => (string) ($priced['tag'] ?? ''), - 'days' => $days, - 'title' => (string) $pkg['title'], - 'price_label' => $this->format_cash_label((float) $priced['price']), + 'package_id' => $this->encode_package_id($lv, $idx + 1), + 'level' => $lv, + 'index' => $idx + 1, + 'action' => $action, + 'price' => (float) $priced['price'], + 'base_price' => $base_price, + 'show_price' => (float) ($priced['show_price'] ?? 0), + 'discount_tag' => (string) ($priced['tag'] ?? ''), + 'days' => $days, + 'title' => (string) $pkg['title'], + 'price_label' => $this->format_cash_label((float) $priced['price']), 'show_price_label' => !empty($priced['show_price']) ? $this->format_cash_label((float) $priced['show_price']) : '', - 'days_label' => ($days <= 0) + 'days_label' => ($days <= 0) ? __('永久', 'aksrt') - : sprintf( - /* translators: %d: days */ - __('%d 天', 'aksrt'), - $days - ), - 'cta_label' => $action === 'renew' + : sprintf(__('%d 天', 'aksrt'), $days), + 'cta_label' => $action === 'renew' ? __('立即续费', 'aksrt') : ($action === 'upgrade' ? __('立即升级', 'aksrt') : __('立即开通', 'aksrt')), ]; } - // Optional dedicated upgrade permanent product line from settings.upgrade.product - // Format: price|days|title (single package for VIP2 upgrade). + // Dedicated upgrade product: fixed or remaining-days unit price. if ($level === 1 && $lv === 2 && $upgrade_enabled) { - $upgrade_pkg = $this->parse_upgrade_product_line((string) ($upgrade['product'] ?? '')); - if ($upgrade_pkg) { - $items[] = [ - 'package_id' => $this->encode_package_id(2, 901), - 'level' => 2, - 'index' => 901, - 'action' => 'upgrade', - 'price' => (float) $upgrade_pkg['price'], - 'base_price' => (float) $upgrade_pkg['price'], - 'show_price' => 0.0, - 'discount_tag' => __('升级专享', 'aksrt'), - 'days' => (int) $upgrade_pkg['days'], - 'title' => (string) $upgrade_pkg['title'], - 'price_label' => $this->format_cash_label((float) $upgrade_pkg['price']), - 'show_price_label' => '', - 'days_label' => ((int) $upgrade_pkg['days'] <= 0) - ? __('永久', 'aksrt') - : sprintf(__('%d 天', 'aksrt'), (int) $upgrade_pkg['days']), - 'cta_label' => __('立即升级', 'aksrt'), - 'is_upgrade_product' => true, - ]; + $upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? '')); + foreach ($upgrade_items as $ui) { + $items[] = $ui; } } @@ -224,7 +226,14 @@ class AKSRT_Vip_Service { ]; } + $points_packages = []; + if ($points_enabled && $level <= 0) { + $points_packages = $this->get_points_exchange_packages($vip, $levels); + } + + $balance = ($user_id > 0 && class_exists('AKSRT_Credits')) ? AKSRT_Credits::balance($user_id) : 0; $methods = $this->available_payment_methods(); + $currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt'); return [ 'enabled' => !empty($vip['enabled']), @@ -234,11 +243,18 @@ class AKSRT_Vip_Service { 'user_expires' => $expires, 'user_is_permanent' => $is_permanent, 'user_expires_label' => $this->format_expires_label($expires, $level), + 'user_points' => $balance, + 'currency_name' => $currency_name, 'renew_enabled' => $renew_enabled, 'upgrade_enabled' => $upgrade_enabled, 'renew_desc' => (string) ($renew['desc'] ?? ''), 'upgrade_desc' => (string) ($upgrade['desc'] ?? ''), 'level_packages' => $level_packages, + 'points_exchange' => [ + 'enabled' => $points_enabled && $level <= 0, + 'desc' => (string) ($points_ex['desc'] ?? ''), + 'packages' => $points_packages, + ], 'payment_methods' => $methods, 'vip_url' => function_exists('aksrt_get_user_center_tab_url') ? aksrt_get_user_center_tab_url('credits') @@ -301,17 +317,29 @@ class AKSRT_Vip_Service { $action = 'upgrade'; } - // Dedicated upgrade product (index 901). - if ($index === 901) { - $upgrade_pkg = $this->parse_upgrade_product_line((string) ($upgrade['product'] ?? '')); - if (!$upgrade_pkg || $level !== 2) { + // Dedicated upgrade product (index 901 fixed, 902 unit remaining days). + if ($index === 901 || $index === 902) { + $upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? '')); + $match = null; + foreach ($upgrade_items as $ui) { + if ((int) ($ui['package_id'] ?? 0) === $package_id) { + $match = $ui; + break; + } + } + if (!$match || $level !== 2) { return new WP_Error('aksrt_invalid_package', __('升级套餐不存在', 'aksrt'), ['status' => 400]); } - $pkg = $upgrade_pkg; - $amount = round((float) $pkg['price'], 2); - $days = (int) $pkg['days']; - $base_price = $amount; - $discount_tag = __('升级专享', 'aksrt'); + $pkg = [ + 'price' => (float) $match['price'], + 'days' => (int) $match['days'], + 'title' => (string) $match['title'], + ]; + $amount = round((float) $match['price'], 2); + $days = (int) $match['days']; + $base_price = (float) ($match['base_price'] ?? $amount); + $discount_tag = (string) ($match['discount_tag'] ?? __('升级专享', 'aksrt')); + $action = 'upgrade'; } else { $packages = $this->parse_packages((string) ($vip['packages_raw'] ?? '')); $pkg = $packages[$index - 1] ?? null; @@ -561,6 +589,288 @@ class AKSRT_Vip_Service { return $out; } + /** + * Points exchange (non-VIP only, zibll pay_vip_points_exchange). + * + * @return array|WP_Error + */ + public function exchange_with_points(int $user_id, int $package_id) { + if ($user_id <= 0) { + return new WP_Error('aksrt_need_login', __('请先登录后再兑换。', 'aksrt'), ['status' => 401]); + } + if (!class_exists('AKSRT_Credits')) { + return new WP_Error('aksrt_credits_unavailable', __('积分系统不可用', 'aksrt'), ['status' => 500]); + } + if (class_exists('AKSRT_Settings') && !AKSRT_Settings::credits_enabled()) { + return new WP_Error('aksrt_credits_disabled', __('积分功能未启用', 'aksrt'), ['status' => 400]); + } + + $vip = class_exists('AKSRT_Settings') + ? AKSRT_Settings::get_payment_vip_settings() + : []; + $points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : []; + if (empty($points_ex['enabled'])) { + return new WP_Error('aksrt_points_exchange_disabled', __('积分兑换会员未启用', 'aksrt'), ['status' => 400]); + } + + $user_level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; + if ($user_level > 0) { + return new WP_Error('aksrt_already_vip', __('您已是会员,无法使用积分兑换。', 'aksrt'), ['status' => 400]); + } + + $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; + $packages = $this->get_points_exchange_packages($vip, $levels); + $match = null; + foreach ($packages as $pkg) { + if ((int) ($pkg['package_id'] ?? 0) === $package_id) { + $match = $pkg; + break; + } + } + if (!$match) { + return new WP_Error('aksrt_invalid_package', __('兑换套餐不存在', 'aksrt'), ['status' => 400]); + } + + $cost = (int) ($match['points'] ?? 0); + $level = (int) ($match['level'] ?? 1); + $days = (int) ($match['days'] ?? 0); + if ($cost <= 0) { + return new WP_Error('aksrt_invalid_price', __('兑换积分无效', 'aksrt'), ['status' => 400]); + } + + $balance = AKSRT_Credits::balance($user_id); + if ($balance < $cost) { + return new WP_Error('aksrt_credits_insufficient', __('积分不足,暂时无法兑换', 'aksrt'), [ + 'status' => 402, + 'balance' => $balance, + 'cost' => $cost, + ]); + } + + if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) { + return new WP_Error('aksrt_pay_unavailable', __('订单系统不可用', 'aksrt'), ['status' => 500]); + } + + $note = wp_json_encode([ + 'vip_pay' => 1, + 'vip_pay_type' => 'points_exchange', + 'level' => $level, + 'days' => $days, + 'points' => $cost, + 'package_id' => $package_id, + 'package_title' => (string) ($match['title'] ?? ''), + 'action' => 'points_exchange', + ], 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' => (float) $cost, + 'currency' => 'points', + 'status' => AKSRT_Order::STATUS_PENDING, + 'payment_method' => 'points', + '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]); + } + + $deducted = AKSRT_Credits::deduct( + $user_id, + $cost, + 'vip_points_exchange', + ['order' => $order_number, 'level' => $level, 'days' => $days], + sprintf(__('积分兑换会员 · %s', 'aksrt'), (string) ($match['title'] ?? '')) + ); + if (is_wp_error($deducted) || $deducted === false) { + AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [ + 'note' => __('积分扣除失败,自动取消', 'aksrt'), + ]); + $msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt'); + return new WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败', 'aksrt'), ['status' => 400]); + } + + $paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [ + 'payment_method' => 'points', + 'payment_transaction_id' => 'points_' . $order_number, + 'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost), + ]); + if (!$paid) { + AKSRT_Credits::add( + $user_id, + $cost, + 'vip_points_exchange_refund', + ['order' => $order_number], + __('会员兑换失败退款', 'aksrt') + ); + return new WP_Error('aksrt_order_confirm_failed', __('兑换确认失败,已尝试退回积分', 'aksrt'), ['status' => 500]); + } + + return [ + 'success' => true, + 'data' => [ + 'order_number' => $order_number, + 'points' => $cost, + 'level' => $level, + 'days' => $days, + 'balance' => AKSRT_Credits::balance($user_id), + 'message' => __('兑换成功', 'aksrt'), + 'reload' => true, + ], + ]; + } + + /** + * Build upgrade package items (fixed and/or remaining-days unit). + * + * upgrade.product formats: + * - fixed: price|days|title + * - unit: unit|daily_price|title (price = remaining_days * daily_price, days = remaining) + * + * @return array> + */ + private function build_upgrade_items(int $user_id, int $expires, bool $is_permanent, string $raw): array { + $raw = trim($raw); + if ($raw === '') { + return []; + } + $items = []; + $lines = preg_split('/\r\n|\r|\n/', $raw) ?: []; + $fixed_i = 0; + foreach ($lines as $line) { + $line = trim((string) $line); + if ($line === '') { + continue; + } + $parts = array_map('trim', explode('|', $line)); + $mode = strtolower((string) ($parts[0] ?? '')); + + if ($mode === 'unit') { + if ($is_permanent || $expires <= time()) { + continue; + } + $daily = isset($parts[1]) ? (float) $parts[1] : 0.0; + if ($daily <= 0) { + continue; + } + $remaining = (int) ceil(max(1, ($expires - time()) / DAY_IN_SECONDS)); + $price = round(max(0.01, $remaining * $daily), 2); + $title = sanitize_text_field((string) ($parts[2] ?? '')); + if ($title === '') { + $title = sprintf(__('按剩余 %d 天升级', 'aksrt'), $remaining); + } + $items[] = [ + 'package_id' => $this->encode_package_id(2, 902), + 'level' => 2, + 'index' => 902, + 'action' => 'upgrade', + 'price' => $price, + 'base_price' => $price, + 'show_price' => 0.0, + 'discount_tag' => __('剩余天数', 'aksrt'), + 'days' => $remaining, + 'title' => $title, + 'price_label' => $this->format_cash_label($price), + 'show_price_label' => '', + 'days_label' => sprintf(__('%d 天', 'aksrt'), $remaining), + 'cta_label' => __('立即升级', 'aksrt'), + 'is_upgrade_product' => true, + 'upgrade_mode' => 'unit', + ]; + continue; + } + + $parsed = $this->parse_packages($line); + if (empty($parsed[0])) { + continue; + } + $fixed_i++; + // Only one fixed upgrade product id 901. + if ($fixed_i > 1) { + continue; + } + $pkg = $parsed[0]; + $items[] = [ + 'package_id' => $this->encode_package_id(2, 901), + 'level' => 2, + 'index' => 901, + 'action' => 'upgrade', + 'price' => (float) $pkg['price'], + 'base_price' => (float) $pkg['price'], + 'show_price' => 0.0, + 'discount_tag' => __('升级专享', 'aksrt'), + 'days' => (int) $pkg['days'], + 'title' => (string) $pkg['title'], + 'price_label' => $this->format_cash_label((float) $pkg['price']), + 'show_price_label' => '', + 'days_label' => ((int) $pkg['days'] <= 0) + ? __('永久', 'aksrt') + : sprintf(__('%d 天', 'aksrt'), (int) $pkg['days']), + 'cta_label' => __('立即升级', 'aksrt'), + 'is_upgrade_product' => true, + 'upgrade_mode' => 'fixed', + ]; + } + return $items; + } + + /** + * Points exchange packages: level|points|days|title + * + * @param array $vip + * @param array $levels + * @return array> + */ + public function get_points_exchange_packages(array $vip, array $levels): array { + $points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : []; + $raw = (string) ($points_ex['packages_raw'] ?? ''); + $currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt'); + $out = []; + $lines = preg_split('/\r\n|\r|\n/', $raw) ?: []; + $i = 0; + foreach ($lines as $line) { + $line = trim((string) $line); + if ($line === '') { + continue; + } + $parts = array_map('trim', explode('|', $line)); + if (count($parts) < 3) { + continue; + } + $lv = (int) ($parts[0] ?? 0); + $pts = (int) ($parts[1] ?? 0); + $days = (int) ($parts[2] ?? 0); + $title = sanitize_text_field((string) ($parts[3] ?? '')); + if ($lv < 1 || $lv > 2 || $pts <= 0) { + continue; + } + if (empty($levels[$lv]['enabled'])) { + continue; + } + $i++; + if ($title === '') { + $lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv)); + $title = $days > 0 + ? sprintf(__('%1$s · %2$d天', 'aksrt'), $lv_name, $days) + : sprintf(__('%s · 永久', 'aksrt'), $lv_name); + } + $out[] = [ + 'package_id' => 5000 + $i, + 'level' => $lv, + 'index' => $i, + 'points' => $pts, + 'days' => max(0, $days), + 'title' => $title, + 'price_label' => $pts . ' ' . $currency, + 'days_label' => $days > 0 ? sprintf(__('%d 天', 'aksrt'), $days) : __('永久', 'aksrt'), + 'cta_label' => __('立即兑换', 'aksrt'), + ]; + } + return $out; + } + /** * Single upgrade product line: price|days|title * @@ -571,10 +881,13 @@ class AKSRT_Vip_Service { if ($raw === '') { return null; } - // Allow multiline; take first valid. $lines = preg_split('/\r\n|\r|\n/', $raw) ?: []; foreach ($lines as $line) { - $parsed = $this->parse_packages((string) $line); + $line = trim((string) $line); + if ($line === '' || str_starts_with(strtolower($line), 'unit|')) { + continue; + } + $parsed = $this->parse_packages($line); if (!empty($parsed[0])) { return $parsed[0]; } @@ -604,7 +917,6 @@ class AKSRT_Vip_Service { return null; } - // Permanent same-level packages never make sense for renew. if ($action === 'renew' && $is_permanent) { return null; } @@ -623,7 +935,6 @@ class AKSRT_Vip_Service { $tag = ''; if ($type === 'discount') { - // Settings store "8" meaning 8折 (80%) when <= 10; or percent when > 10. $discount = (float) ($renew['discount'] ?? 8); if ($discount <= 0) { $discount = 8; @@ -648,7 +959,6 @@ class AKSRT_Vip_Service { $this->format_cash_label($reduce) ); } else { - // same $show = 0.0; $tag = __('续费同价', 'aksrt'); } diff --git a/inc/classes/settings/class-aksrt-settings-payment-runtime.php b/inc/classes/settings/class-aksrt-settings-payment-runtime.php index bb25cd25..a400036b 100644 --- a/inc/classes/settings/class-aksrt-settings-payment-runtime.php +++ b/inc/classes/settings/class-aksrt-settings-payment-runtime.php @@ -193,6 +193,11 @@ class AKSRT_Settings_Payment_Runtime { 'desc' => sanitize_textarea_field((string) ($settings['vip_upgrade_desc'] ?? '')), 'product' => sanitize_text_field((string) ($settings['vip_upgrade_product'] ?? '')), ], + 'points_exchange' => [ + 'enabled' => !empty($settings['pay_vip_points_exchange_s']), + 'desc' => sanitize_textarea_field((string) ($settings['pay_vip_points_exchange_desc'] ?? '')), + 'packages_raw' => (string) ($settings['payment_vip_points_packages'] ?? "1|500|30|积分月卡\n1|5000|0|积分永久"), + ], ]; } diff --git a/inc/helpers/user-center-renderers/credits-vip.php b/inc/helpers/user-center-renderers/credits-vip.php index 0e1a3319..b1776b1e 100644 --- a/inc/helpers/user-center-renderers/credits-vip.php +++ b/inc/helpers/user-center-renderers/credits-vip.php @@ -1,6 +1,6 @@ = 1 && !empty($vip_settings['levels'][$vip_level]['name'])) { $packages = []; $renew_desc = ''; $upgrade_desc = ''; +$points_exchange = ['enabled' => false, 'packages' => [], 'desc' => '']; +$user_points = 0; +$currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt'); 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'] : []; $renew_desc = (string) ($catalog['renew_desc'] ?? ''); $upgrade_desc = (string) ($catalog['upgrade_desc'] ?? ''); + $points_exchange = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : $points_exchange; + $user_points = (int) ($catalog['user_points'] ?? 0); + if (!empty($catalog['currency_name'])) { + $currency_name = (string) $catalog['currency_name']; + } } else { $methods = [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]; } @@ -61,6 +69,7 @@ $shop_title = __('开通会员', 'aksrt'); if ($vip_level > 0) { $shop_title = __('续费 / 升级会员', 'aksrt'); } +$points_pkgs = is_array($points_exchange['packages'] ?? null) ? $points_exchange['packages'] : []; ?>
@@ -183,6 +192,52 @@ if ($vip_level > 0) {

- + + + +
+
+ + +
+
+ +
+ +

+ +
+ = $cost; + ?> + + +
+

+
+