From ddc762bfbd68c5454c68c6b0aba384e9d72452cd Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 11 Aug 2026 16:03:06 +0800 Subject: [PATCH] fix(vip): honor manual membership changes --- assets/js/routes/user-center.js | 2 + inc/helpers/aksrt-vip.php | 251 ++++++++---------- .../user-center-renderers/sidebar-vip.php | 9 +- inc/helpers/user-center-renderers/vip-tab.php | 15 +- src/Admin/Native/UsersMenusVip.php | 9 +- src/Content/VipServicePurchase.php | 10 +- 6 files changed, 145 insertions(+), 151 deletions(-) diff --git a/assets/js/routes/user-center.js b/assets/js/routes/user-center.js index 7857390a..0deb2aac 100644 --- a/assets/js/routes/user-center.js +++ b/assets/js/routes/user-center.js @@ -103,6 +103,8 @@ function activate(name, updateHistory, forceLoad, queryParams) { if (!panels[name]) name = defaultTab; + // VIP membership may change outside this page, so never reuse stale HTML. + if (name === 'vip' && !forceLoad) forceLoad = true; return loadPanel(name, forceLoad, queryParams).then(function() { switchTo(name); if (updateHistory) updateUrl(name); diff --git a/inc/helpers/aksrt-vip.php b/inc/helpers/aksrt-vip.php index 56233b9e..e9fce308 100644 --- a/inc/helpers/aksrt-vip.php +++ b/inc/helpers/aksrt-vip.php @@ -8,142 +8,130 @@ defined('ABSPATH') || exit; /** - * 解析用户 VIP 等级:0=非 VIP,1=VIP1,2=VIP2。 + * Resolve the complete VIP membership state for a user. * - * 判定顺序: - * 1. VIP 功能未启用 → 0 - * 2. 角色在 vip_exempt_roles 白名单 → 当前启用的最高档 - * 3. user meta aksrt_vip_level + aksrt_vip_expires(过期 lazy 清 0;expires=0 且 level>0 视为永久) - * 4. 有效 VIP 订单兜底 → 1(套餐无 level 列,订单无法区分 VIP2) - * 5. 按 pay_user_vip_1/2 开关钳制 + * Explicit membership meta is authoritative, including an explicit level 0. + * Order fallback is only for users without a canonical membership level. * - * @param int $user_id 用户 ID,0 时取当前用户。 - * @return int 0|1|2 + * @return array{level:int,expires:int,state:string,expired_level:int,source:string,has_explicit_state:bool,is_permanent:bool} */ -function aksrt_get_user_vip_level(int $user_id = 0): int { +function aksrt_get_user_vip_state(int $user_id = 0): array { + $empty = [ + 'level' => 0, + 'expires' => 0, + 'state' => 'unopened', + 'expired_level' => 0, + 'source' => 'none', + 'has_explicit_state' => false, + 'is_permanent' => false, + ]; if ($user_id <= 0) { $user_id = get_current_user_id(); } - if ($user_id <= 0) { - return 0; + if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) { + return $empty; } - - if (!class_exists('\AKSRT\Settings\Settings')) { - return 0; + $vip = \AKSRT\Settings\Settings::get_payment_vip_settings(); + if (empty($vip['enabled'])) { + return $empty; } - - $vip_settings = \AKSRT\Settings\Settings::get_payment_vip_settings(); - if (empty($vip_settings['enabled'])) { - return 0; - } - - $levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : []; + $levels = is_array($vip['levels'] ?? null) ? $vip['levels'] : []; $vip1_enabled = !empty($levels[1]['enabled']); $vip2_enabled = !empty($levels[2]['enabled']); + $has_explicit = metadata_exists('user', $user_id, 'aksrt_vip_level'); + $level = min(2, max(0, (int) get_user_meta($user_id, 'aksrt_vip_level', true))); + $expires = max(0, (int) get_user_meta($user_id, 'aksrt_vip_expires', true)); + $expired_level = min(2, max(0, (int) get_user_meta($user_id, 'aksrt_vip_level_expired', true))); - // 1. 角色豁免:特权角色取当前启用最高档。 - $paid_settings = \AKSRT\Settings\Settings::get_paid_content_settings(); - $exempt_roles = $paid_settings['vip_exempt_roles'] ?? []; - if (!empty($exempt_roles)) { - $user = get_userdata($user_id); - if ($user && !empty(array_intersect((array) $user->roles, $exempt_roles))) { - if ($vip2_enabled) { - return 2; - } - if ($vip1_enabled) { - return 1; - } - return 0; - } - } - - $candidate = 0; - - // 2. meta 主路径。 - $meta_level = (int) get_user_meta($user_id, 'aksrt_vip_level', true); - $expires = (int) get_user_meta($user_id, 'aksrt_vip_expires', true); - - if ($meta_level > 0) { - if ($expires > 0 && $expires < time()) { - $base_level = (int) get_user_meta($user_id, 'aksrt_vip_base_level', true); - $base_expires = (int) get_user_meta($user_id, 'aksrt_vip_base_expires', true); - if ($base_level > 0) { + if ($has_explicit) { + if ($level > 0 && $expires > 0 && $expires < time()) { + $base_level = min(2, max(0, (int) get_user_meta($user_id, 'aksrt_vip_base_level', true))); + $base_expires = max(0, (int) get_user_meta($user_id, 'aksrt_vip_base_expires', true)); + if ($base_level > 0 && ($base_expires === 0 || $base_expires >= time())) { update_user_meta($user_id, 'aksrt_vip_level', $base_level); update_user_meta($user_id, 'aksrt_vip_expires', $base_expires); delete_user_meta($user_id, 'aksrt_vip_base_level'); delete_user_meta($user_id, 'aksrt_vip_base_expires'); - $candidate = min(2, max(0, $base_level)); + $level = $base_level; + $expires = $base_expires; + $expired_level = 0; } else { - // 过期:lazy 清 level,记录上次等级(zibll vip_level_expired)。 - update_user_meta($user_id, 'aksrt_vip_level_expired', min(2, max(1, $meta_level))); + $expired_level = max($expired_level, $level); + update_user_meta($user_id, 'aksrt_vip_level_expired', $expired_level); update_user_meta($user_id, 'aksrt_vip_level', 0); - $candidate = 0; - } - } else { - // expires === 0 视为永久;expires >= now 视为有效。 - $candidate = min(2, max(0, $meta_level)); - } - } - - // 3. 订单兜底(meta 无有效等级时)。 - if ($candidate <= 0 && class_exists('\AKSRT\Payment\Order')) { - $order_mgr = \AKSRT\Payment\Order::get_instance(); - $valid_order = method_exists($order_mgr, 'get_valid_vip_order') - ? $order_mgr->get_valid_vip_order($user_id) - : null; - - if (is_array($valid_order) && !empty($valid_order)) { - // Order.expires_at is payment checkout deadline, not VIP membership expiry. - // Prefer note/meta_payload level + aksrt_vip_expires if present. - $meta = []; - if (!empty($valid_order['meta_payload']) && is_array($valid_order['meta_payload'])) { - $meta = $valid_order['meta_payload']; - } elseif (!empty($valid_order['note']) && is_string($valid_order['note'])) { - $decoded = json_decode($valid_order['note'], true); - if (is_array($decoded)) { - $meta = $decoded; - } - } - - $order_level = (int) ($meta['level'] ?? 0); - if ($order_level <= 0 && !empty($valid_order['product_id']) && class_exists('\AKSRT\Content\VipService')) { - // package_id encodes level: decode if possible via service helpers. - $pkg_id = (int) $valid_order['product_id']; - // Encode scheme: level * 1000 + index (see VipServicePackages). - if ($pkg_id >= 1000) { - $order_level = (int) floor($pkg_id / 1000); - } elseif ($pkg_id > 0) { - $order_level = 1; - } - } - $order_level = max(0, min(2, $order_level)); - - // Membership still valid if user meta expires is in future or permanent (0 with prior grant). - // Without restored meta, treat paid VIP order as at least the ordered level. - if ($order_level > 0) { - $candidate = $order_level; - } else { - $candidate = 1; + $level = 0; + $expires = 0; } } - } - - // 4. 按档位开关钳制。 - if ($candidate >= 2) { - if ($vip2_enabled) { - return 2; + if ($level > 0) { + $level = $level >= 2 && $vip2_enabled ? 2 : ($vip1_enabled ? 1 : 0); + return [ + 'level' => $level, + 'expires' => $expires, + 'state' => $expires > 0 ? 'active' : 'permanent', + 'expired_level' => 0, + 'source' => 'meta', + 'has_explicit_state' => true, + 'is_permanent' => $expires <= 0, + ]; } - if ($vip1_enabled) { - return 1; + return [ + 'level' => 0, + 'expires' => 0, + 'state' => $expired_level > 0 ? 'expired' : 'unopened', + 'expired_level' => $expired_level, + 'source' => 'meta', + 'has_explicit_state' => true, + 'is_permanent' => false, + ]; + } + + $paid = \AKSRT\Settings\Settings::get_paid_content_settings(); + $user = get_userdata($user_id); + if ($user && array_intersect((array) $user->roles, (array) ($paid['vip_exempt_roles'] ?? []))) { + $level = $vip2_enabled ? 2 : ($vip1_enabled ? 1 : 0); + return [ + 'level' => $level, 'expires' => 0, + 'state' => $level > 0 ? 'permanent' : 'unopened', + 'expired_level' => 0, 'source' => 'exempt', + 'has_explicit_state' => false, 'is_permanent' => $level > 0, + ]; + } + + if (class_exists('\AKSRT\Payment\Order')) { + $orders = \AKSRT\Payment\Order::get_instance(); + $order = method_exists($orders, 'get_valid_vip_order') ? $orders->get_valid_vip_order($user_id) : null; + if (is_array($order) && $order !== []) { + $order_meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : []; + if ($order_meta === [] && is_string($order['note'] ?? null)) { + $decoded = json_decode($order['note'], true); + $order_meta = is_array($decoded) ? $decoded : []; + } + $level = (int) ($order_meta['level'] ?? 0); + $product_id = (int) ($order['product_id'] ?? 0); + if ($level <= 0 && $product_id > 0) { + $level = $product_id >= 1000 ? (int) floor($product_id / 1000) : 1; + } + $level = min(2, max(1, $level)); + $level = $level >= 2 && $vip2_enabled ? 2 : ($vip1_enabled ? 1 : 0); + $order_expires = max(0, (int) ($order_meta['expires'] ?? $order_meta['vip_expires'] ?? 0)); + return [ + 'level' => $level, 'expires' => $order_expires, + 'state' => $level > 0 ? 'active' : 'unopened', + 'expired_level' => 0, 'source' => 'order', + 'has_explicit_state' => false, 'is_permanent' => false, + ]; } - return 0; } + return $empty; +} - if ($candidate === 1) { - return $vip1_enabled ? 1 : 0; - } - - return 0; +/** + * @param int $user_id User ID, or current user when zero. + * @return int 0|1|2 + */ +function aksrt_get_user_vip_level(int $user_id = 0): int { + return (int) aksrt_get_user_vip_state($user_id)['level']; } /** @@ -373,23 +361,18 @@ function aksrt_render_vip_badge(int $user_id = 0, string $size = 'sm'): string { * @return array{is_vip: bool, level: int, badge_text: string, badge_html: string, name: string, expires: int, expires_label: string} */ function aksrt_get_vip_info(int $user_id = 0): array { - $level = aksrt_get_user_vip_level($user_id); + $state = function_exists('aksrt_get_user_vip_state') ? aksrt_get_user_vip_state($user_id) : []; + $level = (int) ($state['level'] ?? 0); $is_vip = $level >= 1; $badge_text = aksrt_get_vip_badge_text($user_id); - $expires = $user_id > 0 ? (int) get_user_meta($user_id, 'aksrt_vip_expires', true) : 0; + $expires = max(0, (int) ($state['expires'] ?? 0)); $expires_label = __('未开通', 'aksrt'); - if ($level > 0) { - if ($expires <= 0) { - $expires_label = __('永久有效', 'aksrt'); - } elseif ($expires < time()) { - $expires_label = __('已过期', 'aksrt'); - } else { - $expires_label = sprintf( - /* translators: %s: date */ - __('到期时间:%s', 'aksrt'), - wp_date('Y-m-d H:i', $expires) - ); - } + if (($state['state'] ?? '') === 'expired') { + $expires_label = __('已过期', 'aksrt'); + } elseif ($is_vip && $expires <= 0 && ($state['source'] ?? 'none') !== 'order') { + $expires_label = __('永久有效', 'aksrt'); + } elseif ($is_vip) { + $expires_label = sprintf(__('到期时间:%s', 'aksrt'), wp_date('Y-m-d H:i', $expires)); } $name = __('普通用户', 'aksrt'); @@ -401,13 +384,15 @@ function aksrt_get_vip_info(int $user_id = 0): array { } return [ - 'is_vip' => $is_vip, - 'level' => $level, - 'badge_text' => $badge_text, - 'badge_html' => $is_vip ? aksrt_render_vip_badge($user_id) : '', - 'name' => $name, - 'expires' => $expires, + 'is_vip' => $is_vip, + 'level' => $level, + 'badge_text' => $badge_text, + 'badge_html' => $is_vip ? aksrt_render_vip_badge($user_id) : '', + 'name' => $name, + 'expires' => $expires, 'expires_label' => $expires_label, + 'state' => (string) ($state['state'] ?? 'unopened'), + 'source' => (string) ($state['source'] ?? 'none'), ]; } diff --git a/inc/helpers/user-center-renderers/sidebar-vip.php b/inc/helpers/user-center-renderers/sidebar-vip.php index e4b4e436..4b15329a 100644 --- a/inc/helpers/user-center-renderers/sidebar-vip.php +++ b/inc/helpers/user-center-renderers/sidebar-vip.php @@ -16,9 +16,10 @@ if (!$vip_enabled) { } $user_id = (int) ($user_id ?? get_current_user_id()); -$vip_level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; -$expired_level = (int) get_user_meta($user_id, 'aksrt_vip_level_expired', true); -$expires = (int) get_user_meta($user_id, 'aksrt_vip_expires', true); +$vip_state = function_exists('aksrt_get_user_vip_state') ? aksrt_get_user_vip_state($user_id) : []; +$vip_level = (int) ($vip_state['level'] ?? 0); +$expired_level = min(2, max(0, (int) ($vip_state['expired_level'] ?? 0))); +$expires = max(0, (int) ($vip_state['expires'] ?? 0)); $vip_url = aksrt_get_user_center_tab_url('vip'); $title = __('开通会员', 'aksrt'); @@ -29,7 +30,7 @@ if ($status === '') { if ($vip_level > 0) { $title = (string) ($vip_levels[$vip_level]['name'] ?? ('VIP' . $vip_level)); - if ($expires <= 0) { + if ($expires <= 0 && $vip_state_source !== 'order') { $status = __('永久会员', 'aksrt'); } else { $remaining_days = max(1, (int) ceil(($expires - time()) / DAY_IN_SECONDS)); diff --git a/inc/helpers/user-center-renderers/vip-tab.php b/inc/helpers/user-center-renderers/vip-tab.php index 3163169f..86b45124 100644 --- a/inc/helpers/user-center-renderers/vip-tab.php +++ b/inc/helpers/user-center-renderers/vip-tab.php @@ -19,7 +19,8 @@ if (empty($vip_settings['enabled'])) { } $user_id = (int) ($user_id ?? get_current_user_id()); -$level = function_exists('aksrt_get_user_vip_level') ? aksrt_get_user_vip_level($user_id) : 0; +$vip_state = function_exists('aksrt_get_user_vip_state') ? aksrt_get_user_vip_state($user_id) : []; +$level = (int) ($vip_state['level'] ?? 0); $points_exchange = []; $user_points = 0; $currency_name = __('积分', 'aksrt'); @@ -29,10 +30,12 @@ if (class_exists('\\AKSRT\\Content\\VipService')) { $user_points = (int) ($vip_catalog['user_points'] ?? 0); $currency_name = (string) ($vip_catalog['currency_name'] ?? $currency_name); } -$expires = (int) get_user_meta($user_id, 'aksrt_vip_expires', true); -$base_level = (int) get_user_meta($user_id, 'aksrt_vip_base_level', true); -$base_expires = (int) get_user_meta($user_id, 'aksrt_vip_base_expires', true); -$expired_level = (int) get_user_meta($user_id, 'aksrt_vip_level_expired', true); +$expires = max(0, (int) ($vip_state['expires'] ?? 0)); +$base_level = 0; +$base_expires = 0; +$expired_level = min(2, max(0, (int) ($vip_state['expired_level'] ?? 0))); +$vip_state_name = (string) ($vip_state['state'] ?? 'unopened'); +$vip_state_source = (string) ($vip_state['source'] ?? 'none'); $levels = is_array($vip_settings['levels'] ?? null) ? $vip_settings['levels'] : []; $desc_top = (string) ($vip_settings['desc'] ?? ''); @@ -52,7 +55,7 @@ if ($level > 0) { $level_desc = (string) ($levels[$level]['desc'] ?? ''); $vip_description = $level_desc !== '' ? $level_desc : __('尊享专属特权', 'aksrt'); - if ($expires <= 0) { + if ($expires <= 0 && $vip_state_source !== 'order') { $vip_state = 'permanent'; $vip_state_label = __('永久有效', 'aksrt'); $vip_expiry_text = __('到期时间:永久会员', 'aksrt'); diff --git a/src/Admin/Native/UsersMenusVip.php b/src/Admin/Native/UsersMenusVip.php index 1e3e70f8..4e8a9f61 100644 --- a/src/Admin/Native/UsersMenusVip.php +++ b/src/Admin/Native/UsersMenusVip.php @@ -225,10 +225,11 @@ trait UsersMenusVip { } private function render_vip_operation_fields(array $names): void { - echo ' '; - echo ' '; - echo ' '; - echo ''; + echo ' '; + echo ' '; + echo ' '; + echo ''; + echo ''; } /** @return array|\WP_Error */ diff --git a/src/Content/VipServicePurchase.php b/src/Content/VipServicePurchase.php index f832f2ed..037483a9 100644 --- a/src/Content/VipServicePurchase.php +++ b/src/Content/VipServicePurchase.php @@ -52,10 +52,11 @@ trait VipServicePurchase { return $this->vip_member_cache[$user_id]; } - $level = $user_id > 0 && function_exists('aksrt_get_user_vip_level') - ? (int) aksrt_get_user_vip_level($user_id) - : 0; - $expires = $user_id > 0 ? (int) get_user_meta($user_id, 'aksrt_vip_expires', true) : 0; + $vip_state = $user_id > 0 && function_exists('aksrt_get_user_vip_state') + ? aksrt_get_user_vip_state($user_id) + : ['level' => 0, 'expires' => 0, 'state' => 'unopened', 'source' => 'none']; + $level = (int) ($vip_state['level'] ?? 0); + $expires = max(0, (int) ($vip_state['expires'] ?? 0)); $member_state = 'unopened'; if ($level > 0) { $member_state = $expires <= 0 @@ -71,6 +72,7 @@ trait VipServicePurchase { 'expires' => $expires, 'is_permanent' => $level > 0 && $expires <= 0, 'state' => $member_state, + 'source' => (string) ($vip_state['source'] ?? 'none'), 'expires_label' => $member_state === 'expired' ? __('已过期', 'aksrt') : $this->format_expires_label($expires, $level),