Merge pull request 'feat(pay): VIP renew discount tiers and upgrade package pricing' (#124) from worktree-hero-banner-chain into main

Reviewed-on: CYL/wordpress-theme-aksrt#124
This commit is contained in:
2026-07-14 18:31:05 +08:00
3 changed files with 346 additions and 40 deletions
+29
View File
@@ -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;
}
+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);
}
@@ -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');
}
?>
<div class="uc-vip-card<?php echo $is_vip_user ? ' uc-vip-card--active' : ''; ?>" data-aksrt-vip-panel>
<div class="uc-vip-card__bg"></div>
@@ -84,30 +93,49 @@ if (class_exists('AKSRT_Settings')) {
<div class="uc-vip-shop" data-aksrt-vip-shop>
<div class="uc-vip-shop__title">
<i class="fas fa-gem" aria-hidden="true"></i>
<span><?php echo $is_vip_user ? esc_html__('续费 / 升级会员', 'aksrt') : esc_html__('开通会员', 'aksrt'); ?></span>
<span><?php echo esc_html($shop_title); ?></span>
</div>
<?php if ($vip_level > 0 && $renew_desc !== '') : ?>
<p class="uc-vip-shop__note"><?php echo esc_html($renew_desc); ?></p>
<?php endif; ?>
<?php if ($vip_level === 1 && $upgrade_desc !== '') : ?>
<p class="uc-vip-shop__note"><?php echo esc_html($upgrade_desc); ?></p>
<?php endif; ?>
<?php foreach ($packages as $group) :
$g_level = (int) ($group['level'] ?? 0);
$g_name = (string) ($group['name'] ?? ('VIP' . $g_level));
$g_action = (string) ($group['action'] ?? 'pay');
$items = is_array($group['items'] ?? null) ? $group['items'] : [];
if ($items === []) {
continue;
}
$group_badge = '';
if ($g_action === 'renew') {
$group_badge = __('续费', 'aksrt');
} elseif ($g_action === 'upgrade') {
$group_badge = __('升级', 'aksrt');
} elseif ($vip_level >= $g_level) {
$group_badge = __('当前档', 'aksrt');
}
?>
<div class="uc-vip-shop__group" data-vip-level="<?php echo esc_attr((string) $g_level); ?>">
<div class="uc-vip-shop__group" data-vip-level="<?php echo esc_attr((string) $g_level); ?>" data-action="<?php echo esc_attr($g_action); ?>">
<div class="uc-vip-shop__group-title">
<?php echo esc_html($g_name); ?>
<?php if ($vip_level >= $g_level) : ?>
<span class="uc-vip-shop__badge"><?php esc_html_e('当前档', 'aksrt'); ?></span>
<?php if ($group_badge !== '') : ?>
<span class="uc-vip-shop__badge"><?php echo esc_html($group_badge); ?></span>
<?php endif; ?>
</div>
<div class="uc-vip-shop__grid">
<?php foreach ($items as $item) :
$pid = (int) ($item['package_id'] ?? 0);
$price_label = (string) ($item['price_label'] ?? ($cash_symbol . ($item['price'] ?? 0)));
$show_price_label = (string) ($item['show_price_label'] ?? '');
$discount_tag = (string) ($item['discount_tag'] ?? '');
$days_label = (string) ($item['days_label'] ?? '');
$title = (string) ($item['title'] ?? '');
$cta = (string) ($item['cta_label'] ?? __('立即开通', 'aksrt'));
?>
<button
type="button"
@@ -116,11 +144,20 @@ if (class_exists('AKSRT_Settings')) {
data-package-id="<?php echo esc_attr((string) $pid); ?>"
data-price-label="<?php echo esc_attr($price_label); ?>"
data-title="<?php echo esc_attr($title); ?>"
data-action="<?php echo esc_attr((string) ($item['action'] ?? 'pay')); ?>"
>
<?php if ($discount_tag !== '') : ?>
<span class="uc-vip-shop__item-tag"><?php echo esc_html($discount_tag); ?></span>
<?php endif; ?>
<span class="uc-vip-shop__item-title"><?php echo esc_html($title); ?></span>
<span class="uc-vip-shop__item-days"><?php echo esc_html($days_label); ?></span>
<span class="uc-vip-shop__item-price"><?php echo esc_html($price_label); ?></span>
<span class="uc-vip-shop__item-cta"><?php esc_html_e('立即开通', 'aksrt'); ?></span>
<span class="uc-vip-shop__item-price">
<?php echo esc_html($price_label); ?>
<?php if ($show_price_label !== '') : ?>
<small class="uc-vip-shop__item-show"><?php echo esc_html($show_price_label); ?></small>
<?php endif; ?>
</span>
<span class="uc-vip-shop__item-cta"><?php echo esc_html($cta); ?></span>
</button>
<?php endforeach; ?>
</div>