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.
This commit is contained in:
kate522 committed 2026-07-14 18:26:04 +08:00
1 parent d0b8f7805f
commit ba6ae854f8
3 files changed
+346 -40

No files matched your search

+274 -34
View File
@@ -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<string,mixed> $renew
* @param array<string,mixed> $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);
}