feat(vip): skeleton loading state and faster modal/site startup

The VIP purchase/renew modal opened with a single "loading..." line, which
reads as a hang while /pay/vip/modal round-trips. Every frontend pageview
also paid an option write inside wp_head, and the header user-card CSS was
enqueued even for guests who never see the card.

pay-vip.js now builds a full-layout skeleton (user box, level tabs, the
two-column package panel) from vipSkeleton(), reusing the global
aksrt-modal-runtime__skeleton line/block classes; the VIP-specific shapes
live in the conditionally loaded vip-modal.css, and the 640px two-column
threshold matches .pay-vip-form-row. The modal cache TTL goes 15s -> 60s
(purchase, card-pass redeem and points exchange already invalidate the
vip tag), and hover/focus on a .pay-vip trigger now prefetches the payload
through the same cacheKey and flight openRemote uses, so a warm click
opens instantly.

The bottom note and the whole help_text pipeline behind it are removed; the
admin-editable pay_user_vip_desc still renders at the top of the modal, and
the recharge/gateway help_text keys are untouched. get_catalog() reads
points and wallet balance through one new LedgerService::points_and_balance()
call. The StatsTracker counter write moves to shutdown so wp_head only does
the visitor check and setcookie, and header-userbox.css is enqueued for
logged-in users only.

Verification: node --test across the suite (628 tests, including the 7 new
contract assertions in tests/widget-system/vip-modal-skeleton.test.cjs) and
php -l on every changed PHP file. No visual acceptance performed, per request.
This commit is contained in:
kate522 committed 2026-10-18 13:44:01 +08:00
1 parent 319ec2f47f
commit cb76b95b01
9 files changed
+376 -13

No files matched your search

+83
View File
@@ -101,3 +101,86 @@
opacity: 0.7; opacity: 0.7;
cursor: wait; cursor: wait;
} }
/* 会员弹窗骨架屏(pay-vip.js vipSkeleton()):仿真实弹窗布局——
用户框 + 等级卡行 + 双列套餐面板。线条/色块与 shimmer 动画复用全局
.aksrt-modal-runtime__skeleton-line/-block 与 @keyframes
aksrt-modal-skeleton(build/public.min.css 已全局入队);
这里只定义 VIP 专属形状(头像圆、tab 卡、双列 grid、按钮条),
与 .pay-vip-form-row 的 640px 双列阈值保持一致。 */
.aksrt-vip-modal-skeleton {
display: flex;
flex-direction: column;
gap: 14px;
padding: 15px;
}
.aksrt-vip-modal-skeleton__user {
display: flex;
align-items: center;
gap: 10px;
}
.aksrt-vip-modal-skeleton__avatar {
width: 42px;
height: 42px;
border-radius: 50%;
flex: none;
}
.aksrt-vip-modal-skeleton__lines {
display: flex;
flex-direction: column;
gap: 6px;
flex: 1;
min-width: 0;
}
.aksrt-vip-modal-skeleton__tabs {
display: flex;
gap: 10px;
}
.aksrt-vip-modal-skeleton__tab {
flex: 1;
height: 56px;
}
.aksrt-vip-modal-skeleton__pane {
display: grid;
gap: 12px;
}
.aksrt-vip-modal-skeleton__info {
display: flex;
flex-direction: column;
gap: 10px;
padding-top: 4px;
}
.aksrt-vip-modal-skeleton__products {
display: flex;
flex-direction: column;
gap: 10px;
}
.aksrt-vip-modal-skeleton__button {
height: 38px;
}
/* 专属形状共用骨架底色与 shimmer(值与 modal-runtime.css 的线条/块一致)。 */
.aksrt-vip-modal-skeleton__avatar,
.aksrt-vip-modal-skeleton__tab,
.aksrt-vip-modal-skeleton__button {
display: block;
border-radius: 8px;
background: linear-gradient(90deg, rgba(148, 163, 184, 0.16), rgba(148, 163, 184, 0.3), rgba(148, 163, 184, 0.16));
background-size: 200% 100%;
animation: aksrt-modal-skeleton 1.2s ease-in-out infinite;
}
@media (min-width: 640px) {
.aksrt-vip-modal-skeleton__pane {
grid-template-columns: 1fr 1.2fr;
}
}
+90 -4
View File
@@ -519,8 +519,6 @@
append(modal, tabs); append(modal, tabs);
append(modal, content); append(modal, content);
if (catalog.help_text) append(modal, node("div", "muted-2-color em09 mb6", catalog.help_text));
// 卡密渠道初始显隐(选中 card_pass 时隐藏套餐、显示卡密输入盒)。 // 卡密渠道初始显隐(选中 card_pass 时隐藏套餐、显示卡密输入盒)。
modal.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) { modal.querySelectorAll(".aksrt-vip-pane").forEach(function (pane) {
applyVipMethodVisibility(pane); applyVipMethodVisibility(pane);
@@ -698,6 +696,46 @@
}); });
} }
// 骨架屏:仿弹窗真实布局(用户框 + 等级卡 + 双列套餐面板)。
// 线条/色块复用全局骨架类(build/public.min.css),VIP 专属形状见 vip-modal.css。
function vipSkeleton() {
var skeleton = node("div", "aksrt-vip-modal-skeleton");
skeleton.setAttribute("aria-live", "polite");
skeleton.setAttribute("aria-busy", "true");
// 用户框:头像圆 + 两行文字。
var user = node("div", "aksrt-vip-modal-skeleton__user");
append(user, node("span", "aksrt-vip-modal-skeleton__avatar"));
var userLines = node("span", "aksrt-vip-modal-skeleton__lines");
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
append(userLines, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
append(user, userLines);
append(skeleton, user);
// 等级卡行(vip-cardminis):两张 tab 占位。
var tabs = node("div", "aksrt-vip-modal-skeleton__tabs");
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
append(tabs, node("span", "aksrt-vip-modal-skeleton__tab"));
append(skeleton, tabs);
// 双列面板:左权益信息 + 右套餐区(套餐卡、支付方式行、提交按钮条)。
var pane = node("div", "aksrt-vip-modal-skeleton__pane");
var info = node("div", "aksrt-vip-modal-skeleton__info");
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-wide"));
append(info, node("span", "aksrt-modal-runtime__skeleton-line"));
append(info, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
append(pane, info);
var products = node("div", "aksrt-vip-modal-skeleton__products");
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
append(products, node("span", "aksrt-modal-runtime__skeleton-block"));
append(products, node("span", "aksrt-modal-runtime__skeleton-line is-short"));
append(products, node("span", "aksrt-vip-modal-skeleton__button"));
append(pane, products);
append(skeleton, pane);
return skeleton.outerHTML;
}
function vipPay(trigger) { function vipPay(trigger) {
var level = trigger var level = trigger
? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1 ? parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1
@@ -706,11 +744,11 @@
url: "/pay/vip/modal", url: "/pay/vip/modal",
query: { vip_level: level }, query: { vip_level: level },
className: "aksrt-vip-modal-shell", className: "aksrt-vip-modal-shell",
skeleton: '<div class="aksrt-vip-modal-panel"><div class="aksrt-vip-modal-content" data-aksrt-vip-modal-body><div class="text-center padding-20 muted-2-color">加载中…</div></div></div>', skeleton: vipSkeleton(),
telemetry: "vip", telemetry: "vip",
kind: "vip", kind: "vip",
responseType: "json", responseType: "json",
cacheTtl: 15000, cacheTtl: 60000,
cacheTags: ["vip"], cacheTags: ["vip"],
renderData: function (data, session) { renderData: function (data, session) {
renderModalView(session.content, data); renderModalView(session.content, data);
@@ -728,6 +766,53 @@
}); });
} }
var prefetchedLevels = Object.create(null);
// 与 openRemote 同一 cacheKey(method + AksrtRequest.url),命中同一份缓存/flight。
function vipModalCacheKey(level) {
return "GET:" + window.AksrtRequest.url("/pay/vip/modal", { vip_level: level });
}
// 意图预热:悬停/聚焦开通入口时按等级预热弹窗数据(不打开弹窗,失败静默)。
// 点击打开时若预热请求在途,cache.acquire 的 flight 去重会自动合并。
function prefetchVipModal(trigger) {
if (!trigger || !window.AksrtModalCache || !window.AksrtRequest) return;
var level = parseInt(trigger.getAttribute("vip-level") || trigger.getAttribute("data-vip-level") || "1", 10) || 1;
if (level < 1 || level > 2) level = 1;
if (prefetchedLevels[level]) return;
prefetchedLevels[level] = true;
var cache = window.AksrtModalCache;
if (cache.get && cache.get(vipModalCacheKey(level)).hit) return;
try {
cache.acquire(vipModalCacheKey(level), function () {
return window.AksrtRequest.request("/pay/vip/modal", {
method: "GET",
query: { vip_level: level },
});
}, {
ttl: 60000,
tags: ["vip"],
});
} catch (error) {
// 预热失败不影响点击后的正常打开路径。
}
}
function vipTriggerFrom(event) {
var target = event.target;
if (!target || !target.closest) return null;
return target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
}
function bindPrefetch() {
// pointerenter 不冒泡但走捕获阶段;focusin 冒泡,两者互补覆盖鼠标与键盘。
["pointerenter", "focusin"].forEach(function (type) {
document.addEventListener(type, function (event) {
prefetchVipModal(vipTriggerFrom(event));
}, true);
});
}
function onDocClick(event) { function onDocClick(event) {
var trigger = event.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]"); var trigger = event.target.closest(".pay-vip, a.pay-vip, .float-btn.pay-vip, [data-aksrt-pay-vip]");
if (!trigger) return; if (!trigger) return;
@@ -768,6 +853,7 @@
if (window.__aksrtPayVipBound) return; if (window.__aksrtPayVipBound) return;
window.__aksrtPayVipBound = true; window.__aksrtPayVipBound = true;
document.addEventListener("click", onDocClick, true); document.addEventListener("click", onDocClick, true);
bindPrefetch();
bindPointsModal(); bindPointsModal();
bindPointsEntry(); bindPointsEntry();
} }
-1
View File
@@ -115,7 +115,6 @@ trait VipServiceModal {
'prefer_level' => $prefer_level, 'prefer_level' => $prefer_level,
'catalog' => [ 'catalog' => [
'desc' => (string) ($catalog['desc'] ?? ''), 'desc' => (string) ($catalog['desc'] ?? ''),
'help_text' => (string) ($catalog['help_text'] ?? ''),
'more' => (string) ($vip['more'] ?? ''), 'more' => (string) ($vip['more'] ?? ''),
'currency_name' => (string) ($catalog['currency_name'] ?? ''), 'currency_name' => (string) ($catalog['currency_name'] ?? ''),
'level_packages' => $groups, 'level_packages' => $groups,
+3 -5
View File
@@ -232,21 +232,19 @@ trait VipServicePurchase {
} }
} }
$points_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS) : 0; $balances = \AKSRT\Ledger\LedgerService::get_instance()->points_and_balance($user_id);
$wallet_balance = $user_id > 0 ? \AKSRT\Ledger\LedgerService::get_instance()->balance_decimal($user_id) : '0.00';
$methods = $this->available_payment_methods(); $methods = $this->available_payment_methods();
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt'); $currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
$catalog = [ $catalog = [
'enabled' => !empty($vip['enabled']), 'enabled' => !empty($vip['enabled']),
'desc' => (string) ($vip['desc'] ?? ''), 'desc' => (string) ($vip['desc'] ?? ''),
'help_text' => (string) ($vip['help_text'] ?? ''),
'user_level' => $level, 'user_level' => $level,
'user_expires' => $expires, 'user_expires' => $expires,
'user_is_permanent' => $is_permanent, 'user_is_permanent' => $is_permanent,
'user_expires_label' => (string) ($member['expires_label'] ?? ''), 'user_expires_label' => (string) ($member['expires_label'] ?? ''),
'user_points' => $points_balance, 'user_points' => $balances['points'],
'wallet_balance' => $wallet_balance, 'wallet_balance' => $balances['balance'],
'currency_name' => $currency_name, 'currency_name' => $currency_name,
'renew_enabled' => $renew_enabled, 'renew_enabled' => $renew_enabled,
'upgrade_enabled' => $upgrade_enabled, 'upgrade_enabled' => $upgrade_enabled,
+4 -1
View File
@@ -63,7 +63,10 @@ trait ScriptsPart1b {
}; };
// 顶部导航用户卡片(legacy header-userbox 模块样式)。 // 顶部导航用户卡片(legacy header-userbox 模块样式)。
$enqueue_module('header-userbox'); // 仅登录用户渲染用户卡(aksrt-header-userbox.php 的渲染条件),访客页面不加载。
if (is_user_logged_in()) {
$enqueue_module('header-userbox');
}
if (!is_user_logged_in() || (bool) get_query_var('aksrt_sign')) { if (!is_user_logged_in() || (bool) get_query_var('aksrt_sign')) {
$auth_css = get_template_directory() . '/assets/css/routes/sign.css'; $auth_css = get_template_directory() . '/assets/css/routes/sign.css';
+22 -1
View File
@@ -30,6 +30,9 @@ class StatsTracker {
/** /**
* 追踪访问 * 追踪访问
*
* 访客判定与 setcookie 必须留在 wp_head(响应头尚未发送);
* 计数读写改挂 shutdown,避免每次前台请求都在渲染关键路径上写库。
*/ */
public function track_visit(): void { public function track_visit(): void {
// 排除管理员、爬虫、AJAX请求 // 排除管理员、爬虫、AJAX请求
@@ -41,6 +44,12 @@ class StatsTracker {
return; return;
} }
static $scheduled = false;
if ($scheduled) {
return;
}
$scheduled = true;
$today = current_time('Y-m-d'); $today = current_time('Y-m-d');
$option_key = 'aksrt_daily_stats_' . $today; $option_key = 'aksrt_daily_stats_' . $today;
@@ -48,6 +57,19 @@ class StatsTracker {
$visitor_cookie = 'aksrt_visitor_' . $today; $visitor_cookie = 'aksrt_visitor_' . $today;
$is_unique = empty($_COOKIE[$visitor_cookie]); $is_unique = empty($_COOKIE[$visitor_cookie]);
if ($is_unique) {
setcookie($visitor_cookie, '1', time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
}
add_action('shutdown', static function () use ($option_key, $is_unique): void {
self::get_instance()->record_stats($option_key, $is_unique);
});
}
/**
* 计数落库(shutdown 阶段执行;请求中断时本次统计丢弃)。
*/
public function record_stats(string $option_key, bool $is_unique): void {
// 获取或初始化今日统计 // 获取或初始化今日统计
$stats = get_option($option_key, ['visitors' => 0, 'pageviews' => 0]); $stats = get_option($option_key, ['visitors' => 0, 'pageviews' => 0]);
@@ -59,7 +81,6 @@ class StatsTracker {
$stats['pageviews']++; $stats['pageviews']++;
if ($is_unique) { if ($is_unique) {
$stats['visitors']++; $stats['visitors']++;
setcookie($visitor_cookie, '1', time() + DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN);
} }
update_option($option_key, $stats, false); update_option($option_key, $stats, false);
+40
View File
@@ -63,6 +63,46 @@ final class LedgerService {
return AssetAmount::from_minor(AssetType::BALANCE, $this->balance($user_id, AssetType::BALANCE))->decimal(); return AssetAmount::from_minor(AssetType::BALANCE, $this->balance($user_id, AssetType::BALANCE))->decimal();
} }
/**
* Read points and cash balance together (VIP modal catalog).
*
* The meta adapter serves both assets from user meta; the asset-account
* fallback answers both rows with a single query.
*
* @return array{points:int,balance:string}
*/
public function points_and_balance(int $user_id): array {
if ($user_id <= 0) {
return ['points' => 0, 'balance' => '0.00'];
}
if (class_exists('\AKSRT\Credits\MetaAssetService')) {
$meta = \AKSRT\Credits\MetaAssetService::get_instance();
return [
'points' => $meta->balance_points($user_id),
'balance' => $meta->balance_decimal($user_id),
];
}
global $wpdb;
$table = $this->table('asset_account');
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT asset, balance_minor FROM {$table} WHERE user_id = %d AND asset IN (%s, %s)",
$user_id,
AssetType::POINTS->value,
AssetType::BALANCE->value
), ARRAY_A);
$minor = ['points' => 0, 'balance' => 0];
foreach ((array) $rows as $row) {
$asset = (string) ($row['asset'] ?? '');
if (isset($minor[$asset])) {
$minor[$asset] = max(0, (int) ($row['balance_minor'] ?? 0));
}
}
return [
'points' => $minor['points'],
'balance' => AssetAmount::from_minor(AssetType::BALANCE, $minor['balance'])->decimal(),
];
}
public function format_balance(int $minor): string { public function format_balance(int $minor): string {
return AssetAmount::from_minor(AssetType::BALANCE, max(0, $minor))->formatted(); return AssetAmount::from_minor(AssetType::BALANCE, max(0, $minor))->formatted();
} }
-1
View File
@@ -219,7 +219,6 @@ class PaymentRuntime {
'badge_text' => $badge, 'badge_text' => $badge,
'desc' => $desc, 'desc' => $desc,
'more' => wp_kses($more, $allowed_equity + ['div' => ['class' => true], 'p' => ['class' => true]]), 'more' => wp_kses($more, $allowed_equity + ['div' => ['class' => true], 'p' => ['class' => true]]),
'help_text' => $desc !== '' ? $desc : __('会员权益以免扣下载和身份展示为主,开通后立即生效。', 'aksrt'),
'levels' => [ 'levels' => [
1 => [ 1 => [
'enabled' => $vip1_enabled, 'enabled' => $vip1_enabled,
@@ -0,0 +1,134 @@
/**
* 会员弹窗骨架屏 / 打开提速 / help_text 清理 / 启动优化契约。
*
* 硬约束:
* 1) 会员弹窗加载占位为完整布局仿形骨架(vipSkeleton()),复用全局
* aksrt-modal-runtime__skeleton-line/-block 与 shimmer 动画,
* VIP 专属形状样式落在 assets/css/frontend/vip-modal.css;
* 2) 弹窗数据缓存 TTL 60s,悬停/聚焦入口按等级意图预热,预热与
* openRemote 共用 cacheKey 与 flight;
* 3) 弹窗底部提示句与整条 help_text 管线已移除,顶部说明 desc 保留;
* 4) 积分/钱包余额合并为一次 LedgerService::points_and_balance();
* 5) 访问统计写库移到 shutdown,wp_head 只做访客判定与 setcookie;
* 6) header-userbox 样式仅登录用户入队。
*/
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const root = path.join(__dirname, '..', '..');
const read = (...seg) => fs.readFileSync(path.join(root, ...seg), 'utf8');
// 取某个方法体(到下一个同类方法声明为止),避免断言误伤其它功能的同名键。
function sliceMethod(source, signature, nextSignature) {
const start = source.indexOf(signature);
assert.notStrictEqual(start, -1, `missing ${signature}`);
const end = source.indexOf(nextSignature, start + signature.length);
return source.slice(start, end === -1 ? undefined : end);
}
const vipJs = read('assets', 'js', 'pay-vip.js');
const vipCss = read('assets', 'css', 'frontend', 'vip-modal.css');
const paymentRuntime = read('src', 'Settings', 'PaymentRuntime.php');
const modal = read('src', 'Content', 'VipServiceModal.php');
const catalogSource = read('src', 'Content', 'VipServicePurchase.php');
const ledger = read('src', 'Ledger', 'LedgerService.php');
const stats = read('src', 'Core', 'StatsTracker.php');
const scripts = read('src', 'Core', 'ScriptsPart1b.php');
test('会员弹窗使用完整布局仿形骨架屏', () => {
assert.match(vipJs, /function vipSkeleton\(\) \{/);
assert.match(vipJs, /node\("div", "aksrt-vip-modal-skeleton"\)/);
assert.match(vipJs, /skeleton\.setAttribute\("aria-busy", "true"\)/);
// 结构:用户框(头像+两行)、等级卡行、双列面板(权益+套餐区)。
assert.match(vipJs, /aksrt-vip-modal-skeleton__user/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__avatar/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__lines/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__tabs/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__tab/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__pane/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__info/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__products/);
assert.match(vipJs, /aksrt-vip-modal-skeleton__button/);
// 线条/色块复用全局骨架类。
assert.match(vipJs, /aksrt-modal-runtime__skeleton-line is-wide/);
assert.match(vipJs, /aksrt-modal-runtime__skeleton-line is-short/);
assert.match(vipJs, /aksrt-modal-runtime__skeleton-block/);
// openRemote 传入自定义骨架,不再使用纯文本占位。
assert.match(vipJs, /skeleton: vipSkeleton\(\),/);
assert.doesNotMatch(vipJs, /加载中/);
});
test('骨架样式落在 vip-modal.css 并对齐 640px 双列阈值', () => {
assert.match(vipCss, /\.aksrt-vip-modal-skeleton \{/);
assert.match(vipCss, /\.aksrt-vip-modal-skeleton__avatar \{[\s\S]*?border-radius: 50%/);
assert.match(vipCss, /\.aksrt-vip-modal-skeleton__tab \{[\s\S]*?flex: 1/);
assert.match(vipCss, /\.aksrt-vip-modal-skeleton__button \{[\s\S]*?height: 38px/);
assert.match(vipCss, /animation: aksrt-modal-skeleton/);
assert.match(vipCss, /@media \(min-width: 640px\)[\s\S]*?\.aksrt-vip-modal-skeleton__pane \{[\s\S]*?grid-template-columns: 1fr 1\.2fr/);
});
test('弹窗打开提速:缓存 TTL 提升与意图预热', () => {
assert.match(vipJs, /cacheTtl: 60000,/);
// 预热与 openRemote 同一 cacheKey(method + AksrtRequest.url)。
assert.match(vipJs, /function vipModalCacheKey\(level\) \{/);
assert.match(vipJs, /"GET:" \+ window\.AksrtRequest\.url\("\/pay\/vip\/modal", \{ vip_level: level \}\)/);
assert.match(vipJs, /cache\.acquire\(vipModalCacheKey\(level\)/);
assert.match(vipJs, /ttl: 60000,/);
assert.match(vipJs, /tags: \["vip"\]/);
// 悬停/聚焦触发,绑定进 bindOnce。
assert.match(vipJs, /\["pointerenter", "focusin"\]/);
assert.match(vipJs, /function bindPrefetch\(\) \{/);
assert.match(vipJs, /document\.addEventListener\("click", onDocClick, true\);\s*bindPrefetch\(\);/);
// 每等级每页只预热一次。
assert.match(vipJs, /prefetchedLevels\[level\] = true;/);
});
test('弹窗底部提示与 help_text 管线已彻底移除', () => {
assert.doesNotMatch(vipJs, /help_text/);
// 仅收敛 VIP 设置函数:充值/网关设置的 help_text 与本文无关,保持原样。
const vipSettings = sliceMethod(
paymentRuntime,
'public static function get_payment_vip_settings',
'public static function get_payment_revenue_settings'
);
assert.doesNotMatch(vipSettings, /help_text/);
assert.doesNotMatch(vipSettings, /开通后立即生效/);
assert.doesNotMatch(modal, /help_text/);
const viewModel = sliceMethod(modal, 'public function get_modal_view_model', 'public function get_points_exchange_modal_html');
assert.doesNotMatch(viewModel, /help_text/);
assert.doesNotMatch(catalogSource, /help_text/);
// 顶部说明 desc 仍保留(后台 pay_user_vip_desc 继续生效)。
assert.match(vipJs, /catalog\.desc/);
assert.match(paymentRuntime, /'desc'\s+=> \$desc,/);
});
test('积分与钱包余额合并为一次 LedgerService 读取', () => {
assert.match(ledger, /public function points_and_balance\(int \$user_id\): array/);
// 仅收敛 get_catalog:购买/履约路径的余额读取与本文无关,保持原样。
const getCatalog = sliceMethod(catalogSource, 'public function get_catalog', 'private function find_catalog_package');
assert.match(getCatalog, /points_and_balance\(\$user_id\)/);
assert.match(getCatalog, /'user_points'\s+=> \$balances\['points'\]/);
assert.match(getCatalog, /'wallet_balance'\s+=> \$balances\['balance'\]/);
assert.doesNotMatch(getCatalog, /LedgerService::get_instance\(\)->balance\(/);
assert.doesNotMatch(getCatalog, /balance_decimal/);
});
test('访问统计写库移出渲染关键路径', () => {
assert.match(stats, /add_action\('wp_head', \[self::get_instance\(\), 'track_visit'\], 1\)/);
assert.match(stats, /add_action\('shutdown'/);
assert.match(stats, /public function record_stats\(string \$option_key, bool \$is_unique\): void/);
// wp_head 阶段只做判定与 setcookie,不再读写 option。
const trackVisit = stats.slice(stats.indexOf('public function track_visit'), stats.indexOf('public function record_stats'));
assert.doesNotMatch(trackVisit, /update_option|get_option/);
assert.match(trackVisit, /setcookie\(\$visitor_cookie/);
// 落库与月初清理收敛到 record_stats。
const recordStats = stats.slice(stats.indexOf('public function record_stats'));
assert.match(recordStats, /update_option\(\$option_key, \$stats, false\)/);
assert.match(recordStats, /cleanup_old_stats\(\)/);
});
test('header-userbox 样式仅登录用户入队', () => {
assert.match(scripts, /if \(is_user_logged_in\(\)\) \{\s*\$enqueue_module\('header-userbox'\);/);
});