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:
1 parent
d0b8f7805f
commit
ba6ae854f8
3 files changed
+346
-40
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user