From ba6ae854f80cdeb03f767f3e428b8269fd9bce59 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 14 Jul 2026 18:26:04 +0800 Subject: [PATCH] feat(pay): VIP renew discount tiers and upgrade package pricing Apply vip_renew discount/reduce to same-level packages, gate renew/upgrade availability, support dedicated vip_upgrade_product line, and show tags/CTAs. --- assets/css/routes/user-center/credits.css | 29 ++ inc/classes/class-aksrt-vip-service.php | 308 ++++++++++++++++-- .../user-center-renderers/credits-vip.php | 49 ++- 3 files changed, 346 insertions(+), 40 deletions(-) diff --git a/assets/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css index 5e552773..28b06efe 100644 --- a/assets/css/routes/user-center/credits.css +++ b/assets/css/routes/user-center/credits.css @@ -512,3 +512,32 @@ color: var(--color-ink-3, #6b7280); margin: 0 0 16px; } + +.uc-vip-shop__note { + margin: 0 0 12px; + font-size: 12px; + color: var(--color-ink-3, #6b7280); + line-height: 1.5; +} +.uc-vip-shop__item { + position: relative; +} +.uc-vip-shop__item-tag { + position: absolute; + top: 8px; + right: 8px; + font-size: 10px; + font-weight: 700; + color: #92400e; + background: #fef3c7; + border-radius: 999px; + padding: 2px 6px; +} +.uc-vip-shop__item-show { + display: inline-block; + margin-left: 6px; + font-size: 12px; + font-weight: 500; + color: #9ca3af; + text-decoration: line-through; +} diff --git a/inc/classes/class-aksrt-vip-service.php b/inc/classes/class-aksrt-vip-service.php index 13c7ed63..cc5d7429 100644 --- a/inc/classes/class-aksrt-vip-service.php +++ b/inc/classes/class-aksrt-vip-service.php @@ -109,63 +109,141 @@ class AKSRT_Vip_Service { 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' => []]; + : ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => []]; $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; + $is_permanent = ($level > 0 && $expires <= 0); $packages = $this->parse_packages((string) ($vip['packages_raw'] ?? '')); $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; + $renew = is_array($vip['renew'] ?? null) ? $vip['renew'] : []; + $upgrade = is_array($vip['upgrade'] ?? null) ? $vip['upgrade'] : []; + $renew_enabled = !isset($renew['enabled']) || !empty($renew['enabled']); + $upgrade_enabled = !isset($upgrade['enabled']) || !empty($upgrade['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; + } + $name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv)); $items = []; 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'; + } elseif ($level > 0 && $lv > $level) { + $action = 'upgrade'; + } + + $priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $lv, $level, $expires, $is_permanent, $days); + if ($priced === null) { + continue; + } + $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) + '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) ? __('永久', 'aksrt') : sprintf( /* translators: %d: days */ __('%d 天', 'aksrt'), - (int) $pkg['days'] + $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). + 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, + ]; + } + } + if ($items === []) { continue; } $level_packages[] = [ - 'level' => $lv, - 'name' => $name, - 'items' => $items, + 'level' => $lv, + 'name' => $name, + 'action' => ($level > 0 && $lv === $level) ? 'renew' : (($level > 0 && $lv > $level) ? 'upgrade' : 'pay'), + '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, + 'enabled' => !empty($vip['enabled']), + 'desc' => (string) ($vip['desc'] ?? ''), + 'help_text' => (string) ($vip['help_text'] ?? ''), + 'user_level' => $level, + 'user_expires' => $expires, + 'user_is_permanent' => $is_permanent, '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') + 'renew_enabled' => $renew_enabled, + 'upgrade_enabled' => $upgrade_enabled, + 'renew_desc' => (string) ($renew['desc'] ?? ''), + 'upgrade_desc' => (string) ($upgrade['desc'] ?? ''), + '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, + 'logged_in' => $user_id > 0, ]; } @@ -186,7 +264,7 @@ class AKSRT_Vip_Service { $vip = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_payment_vip_settings() - : ['enabled' => false, 'packages_raw' => '', 'levels' => []]; + : ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => []]; if (empty($vip['enabled'])) { return new WP_Error('aksrt_vip_disabled', __('会员功能未启用', 'aksrt'), ['status' => 400]); } @@ -195,13 +273,61 @@ class AKSRT_Vip_Service { 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]); + $user_level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; + $expires = (int) get_user_meta($user_id, 'aksrt_vip_expires', true); + $is_permanent = ($user_level > 0 && $expires <= 0); + $renew = is_array($vip['renew'] ?? null) ? $vip['renew'] : []; + $upgrade = is_array($vip['upgrade'] ?? null) ? $vip['upgrade'] : []; + $renew_enabled = !isset($renew['enabled']) || !empty($renew['enabled']); + $upgrade_enabled = !isset($upgrade['enabled']) || !empty($upgrade['enabled']); + + if ($is_permanent && $level <= $user_level) { + return new WP_Error('aksrt_vip_permanent', __('您已是永久会员,无需续费。', 'aksrt'), ['status' => 400]); + } + if ($user_level > 0 && $level === $user_level && !$renew_enabled) { + return new WP_Error('aksrt_vip_renew_disabled', __('续费功能未开启。', 'aksrt'), ['status' => 400]); + } + if ($user_level > 0 && $level > $user_level && !$upgrade_enabled) { + return new WP_Error('aksrt_vip_upgrade_disabled', __('升级功能未开启。', 'aksrt'), ['status' => 400]); + } + if ($user_level > 0 && $level < $user_level) { + return new WP_Error('aksrt_vip_downgrade', __('不支持降级购买。', 'aksrt'), ['status' => 400]); + } + + $action = 'pay'; + if ($user_level > 0 && $level === $user_level) { + $action = 'renew'; + } elseif ($user_level > 0 && $level > $user_level) { + $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) { + 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'); + } else { + $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]); + } + $base_price = (float) $pkg['price']; + $days = (int) $pkg['days']; + $priced = $this->apply_action_price($base_price, $action, $renew, $upgrade, $level, $user_level, $expires, $is_permanent, $days); + if ($priced === null) { + return new WP_Error('aksrt_invalid_package', __('当前无法购买该套餐', 'aksrt'), ['status' => 400]); + } + $amount = round((float) $priced['price'], 2); + $discount_tag = (string) ($priced['tag'] ?? ''); } - $amount = round((float) $pkg['price'], 2); if ($amount <= 0) { return new WP_Error('aksrt_invalid_price', __('套餐价格无效', 'aksrt'), ['status' => 400]); } @@ -215,14 +341,16 @@ class AKSRT_Vip_Service { $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_title' => (string) ($pkg['title'] ?? ''), 'package_id' => $package_id, + 'action' => $action, + 'base_price' => $base_price, + 'discount_tag' => $discount_tag, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $order_number = AKSRT_Order_Repository::get_instance()->create([ @@ -246,11 +374,13 @@ class AKSRT_Vip_Service { } $level_name = (string) ($levels[$level]['name'] ?? ('VIP' . $level)); + $action_label = $action === 'renew' ? __('续费', 'aksrt') : ($action === 'upgrade' ? __('升级', 'aksrt') : __('开通', 'aksrt')); $order['subject'] = sprintf( - /* translators: 1: vip name 2: package title */ - __('开通%s · %s', 'aksrt'), + /* translators: 1: action 2: vip name 3: package title */ + __('%1$s%2$s · %3$s', 'aksrt'), + $action_label, $level_name, - (string) $pkg['title'] + (string) ($pkg['title'] ?? '') ); $order['payment_method'] = $payment_method; @@ -317,7 +447,10 @@ class AKSRT_Vip_Service { 'amount' => $amount, 'level' => $level, 'days' => $days, - 'message' => __('会员开通成功', 'aksrt'), + 'action' => $action, + 'message' => $action === 'renew' + ? __('会员续费成功', 'aksrt') + : ($action === 'upgrade' ? __('会员升级成功', 'aksrt') : __('会员开通成功', 'aksrt')), 'reload' => true, ], ]; @@ -428,6 +561,113 @@ class AKSRT_Vip_Service { return $out; } + /** + * Single upgrade product line: price|days|title + * + * @return array{price:float,days:int,title:string}|null + */ + public function parse_upgrade_product_line(string $raw): ?array { + $raw = trim($raw); + 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); + if (!empty($parsed[0])) { + return $parsed[0]; + } + } + return null; + } + + /** + * Apply renew discount/reduce to same-level packages. + * + * @param array $renew + * @param array $upgrade + * @return array{price:float,show_price:float,tag:string}|null + */ + private function apply_action_price( + float $base_price, + string $action, + array $renew, + array $upgrade, + int $target_level, + int $user_level, + int $expires, + bool $is_permanent, + int $days + ): ?array { + if ($base_price <= 0) { + return null; + } + + // Permanent same-level packages never make sense for renew. + if ($action === 'renew' && $is_permanent) { + return null; + } + + if ($action !== 'renew') { + return [ + 'price' => round($base_price, 2), + 'show_price' => 0.0, + 'tag' => '', + ]; + } + + $type = sanitize_key((string) ($renew['price_type'] ?? 'discount')); + $price = $base_price; + $show = $base_price; + $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; + } + if ($discount <= 10) { + $price = round($base_price * ($discount / 10), 2); + $tag = sprintf( + /* translators: %s: discount like 8 */ + __('%s折续费', 'aksrt'), + rtrim(rtrim(number_format($discount, 1, '.', ''), '0'), '.') + ); + } else { + $price = round($base_price * (min(100, $discount) / 100), 2); + $tag = sprintf(__('%s%% 续费价', 'aksrt'), (string) (int) $discount); + } + } elseif ($type === 'reduce') { + $reduce = max(0, (float) ($renew['reduce'] ?? 20)); + $price = round(max(0.01, $base_price - $reduce), 2); + $tag = sprintf( + /* translators: %s: amount */ + __('续费立减%s', 'aksrt'), + $this->format_cash_label($reduce) + ); + } else { + // same + $show = 0.0; + $tag = __('续费同价', 'aksrt'); + } + + if ($price < 0.01) { + $price = 0.01; + } + if ($show > 0 && $price >= $show) { + $show = 0.0; + $tag = __('续费', 'aksrt'); + } + + return [ + 'price' => $price, + 'show_price' => $show > $price ? $show : 0.0, + 'tag' => $tag, + ]; + } + public function encode_package_id(int $level, int $index): int { return (max(1, min(2, $level)) * 1000) + max(1, $index); } diff --git a/inc/helpers/user-center-renderers/credits-vip.php b/inc/helpers/user-center-renderers/credits-vip.php index e364d876..0e1a3319 100644 --- a/inc/helpers/user-center-renderers/credits-vip.php +++ b/inc/helpers/user-center-renderers/credits-vip.php @@ -39,10 +39,14 @@ if ($vip_level >= 1 && !empty($vip_settings['levels'][$vip_level]['name'])) { } $packages = []; +$renew_desc = ''; +$upgrade_desc = ''; 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'] ?? ''); } else { $methods = [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]]; } @@ -52,6 +56,11 @@ if (class_exists('AKSRT_Settings')) { $paid_display = AKSRT_Settings::get_paid_content_settings(); $cash_symbol = (string) ($paid_display['display']['currency_symbol'] ?? '¥'); } + +$shop_title = __('开通会员', 'aksrt'); +if ($vip_level > 0) { + $shop_title = __('续费 / 升级会员', 'aksrt'); +} ?>
@@ -84,30 +93,49 @@ if (class_exists('AKSRT_Settings')) {
- +
+ 0 && $renew_desc !== '') : ?> +

+ + +

+ + = $g_level) { + $group_badge = __('当前档', 'aksrt'); + } ?> -
+
- = $g_level) : ?> - + +