Merge pull request 'feat(pay): points-to-VIP exchange and remaining-days upgrade pricing' (#125) from worktree-hero-banner-chain into main

Reviewed-on: CYL/wordpress-theme-aksrt#125
This commit is contained in:
2026-07-14 18:54:55 +08:00
6 changed files with 504 additions and 66 deletions
+19
View File
@@ -541,3 +541,22 @@
color: #9ca3af;
text-decoration: line-through;
}
.uc-vip-shop--points {
border-color: rgba(16, 185, 129, 0.25);
}
.uc-vip-shop__balance {
font-size: 13px;
font-weight: 600;
color: #047857;
margin: 0 0 10px;
}
.uc-vip-shop__item.is-disabled {
opacity: 0.55;
cursor: not-allowed;
}
.uc-vip-shop__item.is-disabled:hover {
transform: none;
box-shadow: none;
border-color: var(--color-line, #e5e7eb);
}
+45 -1
View File
@@ -318,7 +318,49 @@
}
// ===== VIP 开通 =====
// ===== VIP 积分兑换 =====
function initVipPointsExchange() {
var root = document.querySelector('[data-aksrt-vip-points]');
if (!root) return;
var tip = root.querySelector('[data-aksrt-vip-points-tip]');
var busy = false;
function setTip(text, isError) {
if (!tip) return;
tip.textContent = text || '';
tip.classList.toggle('is-error', !!isError);
}
root.addEventListener('click', function (e) {
var btn = e.target.closest('[data-aksrt-vip-points-buy]');
if (!btn || busy || btn.disabled) return;
e.preventDefault();
var packageId = parseInt(btn.getAttribute('data-package-id') || '0', 10);
if (!packageId) return;
busy = true;
btn.classList.add('is-loading');
setTip('正在兑换…');
api('/pay/vip/points-exchange', {
method: 'POST',
body: JSON.stringify({ package_id: packageId })
}).then(function (r) {
busy = false;
btn.classList.remove('is-loading');
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
setTip((r.json && r.json.message) || '兑换失败', true);
return;
}
var data = (r.json && r.json.data) ? r.json.data : r.json;
setTip((data && data.message) || '兑换成功,正在刷新…');
window.location.reload();
}).catch(function () {
busy = false;
btn.classList.remove('is-loading');
setTip('网络错误,请重试', true);
});
});
}
// ===== VIP 开通 =====
function initVipShop() {
var root = document.querySelector('[data-aksrt-vip-shop]');
if (!root) return;
@@ -405,10 +447,12 @@
initSignin();
initInvite();
initVipShop();
initVipPointsExchange();
});
} else {
initSignin();
initInvite();
initVipShop();
initVipPointsExchange();
}
})();
@@ -107,7 +107,12 @@ class AKSRT_Admin_Schema_Payment {
['key' => 'pay_user_vip_desc', 'label' => '会员说明', 'type' => 'textarea', 'default' => ''],
]],
['key' => 'packages', 'title' => '会员套餐', 'fields' => [
['key' => 'payment_vip_packages', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|月卡\n365|365|年卡"],
['key' => 'payment_vip_packages', 'label' => '套餐列表', 'type' => 'textarea', 'default' => "30|30|月卡\n365|365|年卡", 'help' => '每行:现金价|天数|标题(天数为 0 表示永久)'],
]],
['key' => 'points_exchange', 'title' => '积分兑换会员', 'fields' => [
['key' => 'pay_vip_points_exchange_s', 'label' => '启用积分兑换', 'type' => 'switch', 'default' => false],
['key' => 'payment_vip_points_packages', 'label' => '兑换套餐', 'type' => 'textarea', 'default' => "1|500|30|积分月卡\n1|5000|0|积分永久", 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true], 'help' => '每行:等级|积分|天数|标题(天数 0=永久;仅未开通会员可兑)'],
['key' => 'pay_vip_points_exchange_desc', 'label' => '兑换说明', 'type' => 'textarea', 'default' => '', 'dependsOn' => ['key' => 'pay_vip_points_exchange_s', 'value' => true]],
]],
['key' => 'benefit', 'title' => '下载权益', 'fields' => [
['key' => 'pay_download_limit', 'label' => '普通用户下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 9999],
@@ -133,7 +138,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]],
['key' => 'vip_upgrade_product', 'label' => '升级产品', 'type' => 'input', 'default' => '', 'dependsOn' => ['key' => 'vip_upgrade', 'value' => true], 'help' => '固定价:价格|天数|标题;按剩余天数:unit|每日单价|标题'],
]],
],
],
+371 -61
View File
@@ -41,6 +41,12 @@ class AKSRT_Vip_Service {
'permission_callback' => [$this, 'check_rest_nonce'],
],
]);
register_rest_route('aksrt/v1', '/pay/vip/points-exchange', [
'methods' => 'POST',
'callback' => [$this, 'rest_points_exchange'],
'permission_callback' => [$this, 'check_rest_nonce'],
]);
}
/**
@@ -103,13 +109,34 @@ class AKSRT_Vip_Service {
return new WP_REST_Response($result, 200);
}
/**
* POST /pay/vip/points-exchange
* body: { package_id: int } // points package id = 5000 + index
*/
public function rest_points_exchange(WP_REST_Request $request) {
$uid = get_current_user_id();
if ($uid <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录后再兑换。', 'aksrt'), ['status' => 401]);
}
$params = $request->get_json_params();
if (!is_array($params)) {
$params = $request->get_params();
}
$package_id = (int) ($params['package_id'] ?? 0);
$result = $this->exchange_with_points($uid, $package_id);
if (is_wp_error($result)) {
return $result;
}
return new WP_REST_Response($result, 200);
}
/**
* @return array<string,mixed>
*/
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' => [], 'renew' => [], 'upgrade' => []];
: ['enabled' => false, 'packages_raw' => '', 'levels' => [], 'renew' => [], 'upgrade' => [], 'points_exchange' => []];
$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;
@@ -118,23 +145,23 @@ class AKSRT_Vip_Service {
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
$renew = is_array($vip['renew'] ?? null) ? $vip['renew'] : [];
$upgrade = is_array($vip['upgrade'] ?? null) ? $vip['upgrade'] : [];
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
$renew_enabled = !isset($renew['enabled']) || !empty($renew['enabled']);
$upgrade_enabled = !isset($upgrade['enabled']) || !empty($upgrade['enabled']);
$points_enabled = !empty($points_ex['enabled'])
&& (!class_exists('AKSRT_Settings') || AKSRT_Settings::credits_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;
}
@@ -144,8 +171,6 @@ class AKSRT_Vip_Service {
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';
@@ -159,57 +184,34 @@ class AKSRT_Vip_Service {
}
$items[] = [
'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']),
'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)
'days_label' => ($days <= 0)
? __('永久', 'aksrt')
: sprintf(
/* translators: %d: days */
__('%d 天', 'aksrt'),
$days
),
'cta_label' => $action === 'renew'
: sprintf(__('%d 天', 'aksrt'), $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).
// Dedicated upgrade product: fixed or remaining-days unit price.
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,
];
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? ''));
foreach ($upgrade_items as $ui) {
$items[] = $ui;
}
}
@@ -224,7 +226,14 @@ class AKSRT_Vip_Service {
];
}
$points_packages = [];
if ($points_enabled && $level <= 0) {
$points_packages = $this->get_points_exchange_packages($vip, $levels);
}
$balance = ($user_id > 0 && class_exists('AKSRT_Credits')) ? AKSRT_Credits::balance($user_id) : 0;
$methods = $this->available_payment_methods();
$currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
return [
'enabled' => !empty($vip['enabled']),
@@ -234,11 +243,18 @@ class AKSRT_Vip_Service {
'user_expires' => $expires,
'user_is_permanent' => $is_permanent,
'user_expires_label' => $this->format_expires_label($expires, $level),
'user_points' => $balance,
'currency_name' => $currency_name,
'renew_enabled' => $renew_enabled,
'upgrade_enabled' => $upgrade_enabled,
'renew_desc' => (string) ($renew['desc'] ?? ''),
'upgrade_desc' => (string) ($upgrade['desc'] ?? ''),
'level_packages' => $level_packages,
'points_exchange' => [
'enabled' => $points_enabled && $level <= 0,
'desc' => (string) ($points_ex['desc'] ?? ''),
'packages' => $points_packages,
],
'payment_methods' => $methods,
'vip_url' => function_exists('aksrt_get_user_center_tab_url')
? aksrt_get_user_center_tab_url('credits')
@@ -301,17 +317,29 @@ class AKSRT_Vip_Service {
$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) {
// Dedicated upgrade product (index 901 fixed, 902 unit remaining days).
if ($index === 901 || $index === 902) {
$upgrade_items = $this->build_upgrade_items($user_id, $expires, $is_permanent, (string) ($upgrade['product'] ?? ''));
$match = null;
foreach ($upgrade_items as $ui) {
if ((int) ($ui['package_id'] ?? 0) === $package_id) {
$match = $ui;
break;
}
}
if (!$match || $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');
$pkg = [
'price' => (float) $match['price'],
'days' => (int) $match['days'],
'title' => (string) $match['title'],
];
$amount = round((float) $match['price'], 2);
$days = (int) $match['days'];
$base_price = (float) ($match['base_price'] ?? $amount);
$discount_tag = (string) ($match['discount_tag'] ?? __('升级专享', 'aksrt'));
$action = 'upgrade';
} else {
$packages = $this->parse_packages((string) ($vip['packages_raw'] ?? ''));
$pkg = $packages[$index - 1] ?? null;
@@ -561,6 +589,288 @@ class AKSRT_Vip_Service {
return $out;
}
/**
* Points exchange (non-VIP only, zibll pay_vip_points_exchange).
*
* @return array|WP_Error
*/
public function exchange_with_points(int $user_id, int $package_id) {
if ($user_id <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录后再兑换。', 'aksrt'), ['status' => 401]);
}
if (!class_exists('AKSRT_Credits')) {
return new WP_Error('aksrt_credits_unavailable', __('积分系统不可用', 'aksrt'), ['status' => 500]);
}
if (class_exists('AKSRT_Settings') && !AKSRT_Settings::credits_enabled()) {
return new WP_Error('aksrt_credits_disabled', __('积分功能未启用', 'aksrt'), ['status' => 400]);
}
$vip = class_exists('AKSRT_Settings')
? AKSRT_Settings::get_payment_vip_settings()
: [];
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
if (empty($points_ex['enabled'])) {
return new WP_Error('aksrt_points_exchange_disabled', __('积分兑换会员未启用', 'aksrt'), ['status' => 400]);
}
$user_level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0;
if ($user_level > 0) {
return new WP_Error('aksrt_already_vip', __('您已是会员,无法使用积分兑换。', 'aksrt'), ['status' => 400]);
}
$levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : [];
$packages = $this->get_points_exchange_packages($vip, $levels);
$match = null;
foreach ($packages as $pkg) {
if ((int) ($pkg['package_id'] ?? 0) === $package_id) {
$match = $pkg;
break;
}
}
if (!$match) {
return new WP_Error('aksrt_invalid_package', __('兑换套餐不存在', 'aksrt'), ['status' => 400]);
}
$cost = (int) ($match['points'] ?? 0);
$level = (int) ($match['level'] ?? 1);
$days = (int) ($match['days'] ?? 0);
if ($cost <= 0) {
return new WP_Error('aksrt_invalid_price', __('兑换积分无效', 'aksrt'), ['status' => 400]);
}
$balance = AKSRT_Credits::balance($user_id);
if ($balance < $cost) {
return new WP_Error('aksrt_credits_insufficient', __('积分不足,暂时无法兑换', 'aksrt'), [
'status' => 402,
'balance' => $balance,
'cost' => $cost,
]);
}
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
return new WP_Error('aksrt_pay_unavailable', __('订单系统不可用', 'aksrt'), ['status' => 500]);
}
$note = wp_json_encode([
'vip_pay' => 1,
'vip_pay_type' => 'points_exchange',
'level' => $level,
'days' => $days,
'points' => $cost,
'package_id' => $package_id,
'package_title' => (string) ($match['title'] ?? ''),
'action' => 'points_exchange',
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$order_number = AKSRT_Order_Repository::get_instance()->create([
'user_id' => $user_id,
'product_type' => AKSRT_Domain_Model::ORDER_PRODUCT_VIP,
'product_id' => $package_id,
'amount' => (float) $cost,
'currency' => 'points',
'status' => AKSRT_Order::STATUS_PENDING,
'payment_method' => 'points',
'note' => is_string($note) ? $note : '',
'expires_at' => gmdate('Y-m-d H:i:s', time() + 30 * MINUTE_IN_SECONDS),
]);
if ($order_number === '') {
return new WP_Error('aksrt_order_create_failed', __('创建订单失败', 'aksrt'), ['status' => 500]);
}
$deducted = AKSRT_Credits::deduct(
$user_id,
$cost,
'vip_points_exchange',
['order' => $order_number, 'level' => $level, 'days' => $days],
sprintf(__('积分兑换会员 · %s', 'aksrt'), (string) ($match['title'] ?? ''))
);
if (is_wp_error($deducted) || $deducted === false) {
AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_CANCELLED, [
'note' => __('积分扣除失败,自动取消', 'aksrt'),
]);
$msg = is_wp_error($deducted) ? $deducted->get_error_message() : __('积分扣除失败', 'aksrt');
return new WP_Error('aksrt_credits_deduct_failed', $msg ?: __('积分扣除失败', 'aksrt'), ['status' => 400]);
}
$paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => 'points',
'payment_transaction_id' => 'points_' . $order_number,
'note' => sprintf(__('积分兑换会员 %d 积分。', 'aksrt'), $cost),
]);
if (!$paid) {
AKSRT_Credits::add(
$user_id,
$cost,
'vip_points_exchange_refund',
['order' => $order_number],
__('会员兑换失败退款', 'aksrt')
);
return new WP_Error('aksrt_order_confirm_failed', __('兑换确认失败,已尝试退回积分', 'aksrt'), ['status' => 500]);
}
return [
'success' => true,
'data' => [
'order_number' => $order_number,
'points' => $cost,
'level' => $level,
'days' => $days,
'balance' => AKSRT_Credits::balance($user_id),
'message' => __('兑换成功', 'aksrt'),
'reload' => true,
],
];
}
/**
* Build upgrade package items (fixed and/or remaining-days unit).
*
* upgrade.product formats:
* - fixed: price|days|title
* - unit: unit|daily_price|title (price = remaining_days * daily_price, days = remaining)
*
* @return array<int, array<string,mixed>>
*/
private function build_upgrade_items(int $user_id, int $expires, bool $is_permanent, string $raw): array {
$raw = trim($raw);
if ($raw === '') {
return [];
}
$items = [];
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
$fixed_i = 0;
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '') {
continue;
}
$parts = array_map('trim', explode('|', $line));
$mode = strtolower((string) ($parts[0] ?? ''));
if ($mode === 'unit') {
if ($is_permanent || $expires <= time()) {
continue;
}
$daily = isset($parts[1]) ? (float) $parts[1] : 0.0;
if ($daily <= 0) {
continue;
}
$remaining = (int) ceil(max(1, ($expires - time()) / DAY_IN_SECONDS));
$price = round(max(0.01, $remaining * $daily), 2);
$title = sanitize_text_field((string) ($parts[2] ?? ''));
if ($title === '') {
$title = sprintf(__('按剩余 %d 天升级', 'aksrt'), $remaining);
}
$items[] = [
'package_id' => $this->encode_package_id(2, 902),
'level' => 2,
'index' => 902,
'action' => 'upgrade',
'price' => $price,
'base_price' => $price,
'show_price' => 0.0,
'discount_tag' => __('剩余天数', 'aksrt'),
'days' => $remaining,
'title' => $title,
'price_label' => $this->format_cash_label($price),
'show_price_label' => '',
'days_label' => sprintf(__('%d 天', 'aksrt'), $remaining),
'cta_label' => __('立即升级', 'aksrt'),
'is_upgrade_product' => true,
'upgrade_mode' => 'unit',
];
continue;
}
$parsed = $this->parse_packages($line);
if (empty($parsed[0])) {
continue;
}
$fixed_i++;
// Only one fixed upgrade product id 901.
if ($fixed_i > 1) {
continue;
}
$pkg = $parsed[0];
$items[] = [
'package_id' => $this->encode_package_id(2, 901),
'level' => 2,
'index' => 901,
'action' => 'upgrade',
'price' => (float) $pkg['price'],
'base_price' => (float) $pkg['price'],
'show_price' => 0.0,
'discount_tag' => __('升级专享', 'aksrt'),
'days' => (int) $pkg['days'],
'title' => (string) $pkg['title'],
'price_label' => $this->format_cash_label((float) $pkg['price']),
'show_price_label' => '',
'days_label' => ((int) $pkg['days'] <= 0)
? __('永久', 'aksrt')
: sprintf(__('%d 天', 'aksrt'), (int) $pkg['days']),
'cta_label' => __('立即升级', 'aksrt'),
'is_upgrade_product' => true,
'upgrade_mode' => 'fixed',
];
}
return $items;
}
/**
* Points exchange packages: level|points|days|title
*
* @param array<string,mixed> $vip
* @param array<int,mixed> $levels
* @return array<int, array<string,mixed>>
*/
public function get_points_exchange_packages(array $vip, array $levels): array {
$points_ex = is_array($vip['points_exchange'] ?? null) ? $vip['points_exchange'] : [];
$raw = (string) ($points_ex['packages_raw'] ?? '');
$currency = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
$out = [];
$lines = preg_split('/\r\n|\r|\n/', $raw) ?: [];
$i = 0;
foreach ($lines as $line) {
$line = trim((string) $line);
if ($line === '') {
continue;
}
$parts = array_map('trim', explode('|', $line));
if (count($parts) < 3) {
continue;
}
$lv = (int) ($parts[0] ?? 0);
$pts = (int) ($parts[1] ?? 0);
$days = (int) ($parts[2] ?? 0);
$title = sanitize_text_field((string) ($parts[3] ?? ''));
if ($lv < 1 || $lv > 2 || $pts <= 0) {
continue;
}
if (empty($levels[$lv]['enabled'])) {
continue;
}
$i++;
if ($title === '') {
$lv_name = (string) ($levels[$lv]['name'] ?? ('VIP' . $lv));
$title = $days > 0
? sprintf(__('%1$s · %2$d天', 'aksrt'), $lv_name, $days)
: sprintf(__('%s · 永久', 'aksrt'), $lv_name);
}
$out[] = [
'package_id' => 5000 + $i,
'level' => $lv,
'index' => $i,
'points' => $pts,
'days' => max(0, $days),
'title' => $title,
'price_label' => $pts . ' ' . $currency,
'days_label' => $days > 0 ? sprintf(__('%d 天', 'aksrt'), $days) : __('永久', 'aksrt'),
'cta_label' => __('立即兑换', 'aksrt'),
];
}
return $out;
}
/**
* Single upgrade product line: price|days|title
*
@@ -571,10 +881,13 @@ class AKSRT_Vip_Service {
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);
$line = trim((string) $line);
if ($line === '' || str_starts_with(strtolower($line), 'unit|')) {
continue;
}
$parsed = $this->parse_packages($line);
if (!empty($parsed[0])) {
return $parsed[0];
}
@@ -604,7 +917,6 @@ class AKSRT_Vip_Service {
return null;
}
// Permanent same-level packages never make sense for renew.
if ($action === 'renew' && $is_permanent) {
return null;
}
@@ -623,7 +935,6 @@ class AKSRT_Vip_Service {
$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;
@@ -648,7 +959,6 @@ class AKSRT_Vip_Service {
$this->format_cash_label($reduce)
);
} else {
// same
$show = 0.0;
$tag = __('续费同价', 'aksrt');
}
@@ -193,6 +193,11 @@ class AKSRT_Settings_Payment_Runtime {
'desc' => sanitize_textarea_field((string) ($settings['vip_upgrade_desc'] ?? '')),
'product' => sanitize_text_field((string) ($settings['vip_upgrade_product'] ?? '')),
],
'points_exchange' => [
'enabled' => !empty($settings['pay_vip_points_exchange_s']),
'desc' => sanitize_textarea_field((string) ($settings['pay_vip_points_exchange_desc'] ?? '')),
'packages_raw' => (string) ($settings['payment_vip_points_packages'] ?? "1|500|30|积分月卡\n1|5000|0|积分永久"),
],
];
}
@@ -1,6 +1,6 @@
<?php
/**
* User center credits: VIP membership card + purchase packages.
* User center credits: VIP membership card + purchase packages + points exchange.
*
* Expects extracted $context vars.
*
@@ -41,12 +41,20 @@ if ($vip_level >= 1 && !empty($vip_settings['levels'][$vip_level]['name'])) {
$packages = [];
$renew_desc = '';
$upgrade_desc = '';
$points_exchange = ['enabled' => false, 'packages' => [], 'desc' => ''];
$user_points = 0;
$currency_name = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_currency_name() : __('积分', 'aksrt');
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'] ?? '');
$points_exchange = is_array($catalog['points_exchange'] ?? null) ? $catalog['points_exchange'] : $points_exchange;
$user_points = (int) ($catalog['user_points'] ?? 0);
if (!empty($catalog['currency_name'])) {
$currency_name = (string) $catalog['currency_name'];
}
} else {
$methods = [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]];
}
@@ -61,6 +69,7 @@ $shop_title = __('开通会员', 'aksrt');
if ($vip_level > 0) {
$shop_title = __('续费 / 升级会员', 'aksrt');
}
$points_pkgs = is_array($points_exchange['packages'] ?? null) ? $points_exchange['packages'] : [];
?>
<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>
@@ -183,6 +192,52 @@ if ($vip_level > 0) {
<p class="uc-vip-shop__tip" data-aksrt-vip-tip></p>
</div>
<?php elseif (!empty($vip_settings['help_text'])) : ?>
<?php endif; ?>
<?php if (!empty($points_exchange['enabled']) && $points_pkgs !== []) : ?>
<div class="uc-vip-shop uc-vip-shop--points" data-aksrt-vip-points>
<div class="uc-vip-shop__title">
<i class="fas fa-coins" aria-hidden="true"></i>
<span><?php esc_html_e('积分兑换会员', 'aksrt'); ?></span>
</div>
<div class="uc-vip-shop__balance">
<?php
printf(
/* translators: 1: points balance 2: currency name */
esc_html__('我的积分:%1$s %2$s', 'aksrt'),
esc_html((string) $user_points),
esc_html($currency_name)
);
?>
</div>
<?php if (!empty($points_exchange['desc'])) : ?>
<p class="uc-vip-shop__note"><?php echo esc_html((string) $points_exchange['desc']); ?></p>
<?php endif; ?>
<div class="uc-vip-shop__grid">
<?php foreach ($points_pkgs as $item) :
$pid = (int) ($item['package_id'] ?? 0);
$cost = (int) ($item['points'] ?? 0);
$enough = $user_points >= $cost;
?>
<button
type="button"
class="uc-vip-shop__item<?php echo $enough ? '' : ' is-disabled'; ?>"
data-aksrt-vip-points-buy
data-package-id="<?php echo esc_attr((string) $pid); ?>"
data-points="<?php echo esc_attr((string) $cost); ?>"
<?php disabled(!$enough); ?>
>
<span class="uc-vip-shop__item-title"><?php echo esc_html((string) ($item['title'] ?? '')); ?></span>
<span class="uc-vip-shop__item-days"><?php echo esc_html((string) ($item['days_label'] ?? '')); ?></span>
<span class="uc-vip-shop__item-price"><?php echo esc_html((string) ($item['price_label'] ?? '')); ?></span>
<span class="uc-vip-shop__item-cta">
<?php echo $enough ? esc_html__('立即兑换', 'aksrt') : esc_html__('积分不足', 'aksrt'); ?>
</span>
</button>
<?php endforeach; ?>
</div>
<p class="uc-vip-shop__tip" data-aksrt-vip-points-tip></p>
</div>
<?php elseif (empty($packages) && !empty($vip_settings['help_text'])) : ?>
<p class="uc-vip-shop__empty"><?php echo esc_html((string) $vip_settings['help_text']); ?></p>
<?php endif; ?>