feat(pay): VIP upgrade jump/unit/permanent billing like zibll

Add jump permanent upgrade SKU, permanent-only product line, keep expiry on unit upgrade, and store upgrade_mode for paid activation.
This commit is contained in:
2026-07-14 20:29:08 +08:00
parent d34c954b83
commit 1039b26efd
3 changed files with 138 additions and 18 deletions
@@ -84,7 +84,14 @@ export const vipSchema = {
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: 'textarea',
default: '',
dependsOn: { key: 'vip_upgrade', value: true },
help: '每行一条。永久VIP1升级:permanent|价格|标题;按剩余天:unit|每日单价|标题;限时跳转永久:jump|价格|标题;兼容旧格式:价格|天数|标题',
},
],
},
],
@@ -143,7 +143,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], 'help' => '固定价:价格|天数|标题;按剩余天数:unit|每日单价|标题'],
['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '每行一条。永久VIP1:permanent|价格|标题;剩余天:unit|每日单价|标题;跳转永久:jump|价格|标题;兼容:价格|天数|标题'],
]],
],
],
+129 -16
View File
@@ -317,8 +317,10 @@ class AKSRT_Vip_Service {
$action = 'upgrade';
}
// Dedicated upgrade product (index 901 fixed, 902 unit remaining days).
if ($index === 901 || $index === 902) {
// Dedicated upgrade product:
// 901 permanent/fixed, 902 unit remaining days, 903 jump permanent.
$upgrade_mode = '';
if (in_array($index, [901, 902, 903], true)) {
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? ''));
$match = null;
foreach ($upgrade_items as $ui) {
@@ -339,6 +341,7 @@ class AKSRT_Vip_Service {
$days = (int) $match['days'];
$base_price = (float) ($match['base_price'] ?? $amount);
$discount_tag = (string) ($match['discount_tag'] ?? __('升级专享', 'aksrt'));
$upgrade_mode = sanitize_key((string) ($match['upgrade_mode'] ?? ''));
$action = 'upgrade';
} else {
$packages = $this->parse_packages((string) ($vip['packages_raw'] ?? ''));
@@ -377,6 +380,7 @@ class AKSRT_Vip_Service {
'package_title' => (string) ($pkg['title'] ?? ''),
'package_id' => $package_id,
'action' => $action,
'upgrade_mode' => $upgrade_mode,
'base_price' => $base_price,
'discount_tag' => $discount_tag,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
@@ -512,6 +516,7 @@ class AKSRT_Vip_Service {
$level = (int) ($meta['level'] ?? 0);
$days = (int) ($meta['days'] ?? 0);
$upgrade_mode = sanitize_key((string) ($meta['upgrade_mode'] ?? ''));
if ($level <= 0) {
$decoded = $this->decode_package_id((int) ($paid_order['product_id'] ?? 0));
if ($decoded) {
@@ -525,13 +530,21 @@ class AKSRT_Vip_Service {
$level = 2;
}
$this->activate_vip($user_id, $level, $days);
// unit upgrade: raise level only, keep current expiry (zibll).
if ($upgrade_mode === 'unit') {
$this->activate_vip($user_id, $level, 0, true);
} else {
$this->activate_vip($user_id, $level, $days, false);
}
}
/**
* Apply membership meta.
*
* @param bool $keep_expiry When true (unit upgrade), only raise level and do not change expiry.
* When days<=0 and keep_expiry=false, set permanent (expires=0).
*/
public function activate_vip(int $user_id, int $level, int $days = 0): void {
public function activate_vip(int $user_id, int $level, int $days = 0, bool $keep_expiry = false): void {
if ($user_id <= 0) {
return;
}
@@ -540,6 +553,11 @@ class AKSRT_Vip_Service {
$new_level = max($current_level, $level);
update_user_meta($user_id, 'aksrt_vip_level', $new_level);
// Unit upgrade (zibll): keep vip_exp_date, only raise level.
if ($keep_expiry) {
return;
}
if ($days <= 0) {
// Permanent.
update_user_meta($user_id, 'aksrt_vip_expires', 0);
@@ -723,11 +741,19 @@ class AKSRT_Vip_Service {
}
/**
* Build upgrade package items (fixed and/or remaining-days unit).
* Build upgrade package items (zibll vip_upgrade_product parity).
*
* upgrade.product formats:
* - fixed: price|days|title
* - unit: unit|daily_price|title (price = remaining_days * daily_price, days = remaining)
* upgrade.product multi-line formats:
* - permanent VIP1: permanent|price|title[|show_price]
* - fixed (alias): price|days|title
* - timed unit: unit|daily_price|title
* - timed jump permanent: jump|price|title[|show_price]
*
* Rules:
* - permanent user: only permanent/fixed (days=0 preferred)
* - timed user: unit (keep expiry) + optional jump (to permanent)
*
* package index: 901 permanent/fixed, 902 unit, 903 jump
*
* @return array<int, array<string,mixed>>
*/
@@ -738,7 +764,10 @@ class AKSRT_Vip_Service {
}
$items = [];
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
$fixed_i = 0;
$has_permanent = false;
$has_unit = false;
$has_jump = false;
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '') {
@@ -747,8 +776,9 @@ class AKSRT_Vip_Service {
$parts = array_map('trim', explode('|', $line));
$mode = strtolower((string) ($parts[0] ?? ''));
// unit|daily|title — timed VIP1 only.
if ($mode === 'unit') {
if ($is_permanent || $expires <= time()) {
if ($is_permanent || $expires <= time() || $has_unit) {
continue;
}
$daily = isset($parts[1]) ? (float) $parts[1] : 0.0;
@@ -779,16 +809,89 @@ class AKSRT_Vip_Service {
'is_upgrade_product' => true,
'upgrade_mode' => 'unit',
];
$has_unit = true;
continue;
}
$parsed = $this->parse_packages($line);
if (empty($parsed[0])) {
// jump|price|title[|show_price] — timed VIP1 only → permanent VIP2.
if ($mode === 'jump') {
if ($is_permanent || $has_jump) {
continue;
}
$price = isset($parts[1]) ? (float) $parts[1] : 0.0;
if ($price <= 0) {
continue;
}
$title = sanitize_text_field((string) ($parts[2] ?? ''));
if ($title === '') {
$title = __('升级为永久会员', 'aksrt');
}
$show = isset($parts[3]) ? (float) $parts[3] : 0.0;
$items[] = [
'package_id' => $this->encode_package_id(2, 903),
'level' => 2,
'index' => 903,
'action' => 'upgrade',
'price' => round($price, 2),
'base_price' => round($price, 2),
'show_price' => max(0.0, $show),
'discount_tag' => __('永久升级', 'aksrt'),
'days' => 0,
'title' => $title,
'price_label' => $this->format_cash_label($price),
'show_price_label' => $show > 0 ? $this->format_cash_label($show) : '',
'days_label' => __('永久', 'aksrt'),
'cta_label' => __('立即升级', 'aksrt'),
'is_upgrade_product' => true,
'upgrade_mode' => 'jump',
];
$has_jump = true;
continue;
}
$fixed_i++;
// Only one fixed upgrade product id 901.
if ($fixed_i > 1) {
// permanent|price|title[|show_price]
if ($mode === 'permanent') {
if ($has_permanent) {
continue;
}
$price = isset($parts[1]) ? (float) $parts[1] : 0.0;
if ($price <= 0) {
continue;
}
$title = sanitize_text_field((string) ($parts[2] ?? ''));
if ($title === '') {
$title = __('永久会员升级', 'aksrt');
}
$show = isset($parts[3]) ? (float) $parts[3] : 0.0;
$items[] = [
'package_id' => $this->encode_package_id(2, 901),
'level' => 2,
'index' => 901,
'action' => 'upgrade',
'price' => round($price, 2),
'base_price' => round($price, 2),
'show_price' => max(0.0, $show),
'discount_tag' => __('升级专享', 'aksrt'),
'days' => 0,
'title' => $title,
'price_label' => $this->format_cash_label($price),
'show_price_label' => $show > 0 ? $this->format_cash_label($show) : '',
'days_label' => __('永久', 'aksrt'),
'cta_label' => __('立即升级', 'aksrt'),
'is_upgrade_product' => true,
'upgrade_mode' => 'permanent',
];
$has_permanent = true;
continue;
}
// fixed: price|days|title
$parsed = $this->parse_packages($line);
if (empty($parsed[0]) || $has_permanent) {
continue;
}
// For permanent VIP1, only accept days=0 fixed lines as permanent upgrade.
if ($is_permanent && (int) $parsed[0]['days'] > 0) {
continue;
}
$pkg = $parsed[0];
@@ -810,9 +913,19 @@ class AKSRT_Vip_Service {
: sprintf(__('%d 天', 'aksrt'), (int) $pkg['days']),
'cta_label' => __('立即升级', 'aksrt'),
'is_upgrade_product' => true,
'upgrade_mode' => 'fixed',
'upgrade_mode' => ((int) $pkg['days'] <= 0) ? 'permanent' : 'fixed',
];
$has_permanent = true;
}
// Permanent VIP1 should not see unit/jump.
if ($is_permanent) {
$items = array_values(array_filter($items, static function ($it) {
$m = (string) ($it['upgrade_mode'] ?? '');
return in_array($m, ['permanent', 'fixed'], true);
}));
}
return $items;
}