Files
wordpress-theme-aksrt/src/Core/StatsTracker.php
T
kate522 cb76b95b01 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.
2026-10-18 13:44:01 +08:00

166 lines
3.9 KiB
PHP

<?php
/**
* @package AKSRT
*/
namespace AKSRT\Core;
defined('ABSPATH') || exit;
class StatsTracker {
private static $instance = null;
/**
* 单例模式
*/
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 初始化:挂钩到 wp_head
*/
public static function init(): void {
add_action('wp_head', [self::get_instance(), 'track_visit'], 1);
}
/**
* 追踪访问
*
* 访客判定与 setcookie 必须留在 wp_head(响应头尚未发送);
* 计数读写改挂 shutdown,避免每次前台请求都在渲染关键路径上写库。
*/
public function track_visit(): void {
// 排除管理员、爬虫、AJAX请求
if (is_admin() || wp_doing_ajax()) {
return;
}
if (is_user_logged_in() && current_user_can('manage_options')) {
return;
}
static $scheduled = false;
if ($scheduled) {
return;
}
$scheduled = true;
$today = current_time('Y-m-d');
$option_key = 'aksrt_daily_stats_' . $today;
// 检查是否已统计过该访客
$visitor_cookie = 'aksrt_visitor_' . $today;
$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]);
if (!is_array($stats)) {
$stats = ['visitors' => 0, 'pageviews' => 0];
}
// 更新统计
$stats['pageviews']++;
if ($is_unique) {
$stats['visitors']++;
}
update_option($option_key, $stats, false);
// 每月1日清理旧数据
if ((int) date('j') === 1 && (int) date('H') === 0 && (int) date('i') < 5) {
$this->cleanup_old_stats();
}
}
/**
* 清理30天前的统计数据
*/
private function cleanup_old_stats(): void {
global $wpdb;
$threshold = date('Y-m-d', strtotime('-30 days'));
$wpdb->query($wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s AND option_name < %s",
$wpdb->esc_like('aksrt_daily_stats_') . '%',
'aksrt_daily_stats_' . $threshold
));
}
/**
* 获取今日统计
*/
public static function get_today_stats(): array {
$today = current_time('Y-m-d');
$stats = get_option('aksrt_daily_stats_' . $today, ['visitors' => 0, 'pageviews' => 0]);
if (!is_array($stats)) {
return ['visitors' => 0, 'pageviews' => 0];
}
return [
'visitors' => absint($stats['visitors'] ?? 0),
'pageviews' => absint($stats['pageviews'] ?? 0),
];
}
/**
* 获取指定日期统计
*/
public static function get_daily_stats(string $date): array {
$stats = get_option('aksrt_daily_stats_' . $date, ['visitors' => 0, 'pageviews' => 0]);
if (!is_array($stats)) {
return ['visitors' => 0, 'pageviews' => 0];
}
return [
'visitors' => absint($stats['visitors'] ?? 0),
'pageviews' => absint($stats['pageviews'] ?? 0),
];
}
/**
* 获取最近N天统计
*/
public static function get_period_stats(int $days = 30): array {
$total = ['visitors' => 0, 'pageviews' => 0];
for ($i = 0; $i < $days; $i++) {
$date = date('Y-m-d', strtotime("-{$i} days"));
$stats = self::get_daily_stats($date);
$total['visitors'] += $stats['visitors'];
$total['pageviews'] += $stats['pageviews'];
}
return $total;
}
/**
* 获取总浏览量(所有文章)
*/
public static function get_total_views(): int {
global $wpdb;
$total = $wpdb->get_var(
"SELECT SUM(CAST(meta_value AS UNSIGNED)) FROM {$wpdb->postmeta} WHERE meta_key = 'aksrt_post_views'"
);
return max(0, (int) $total);
}
}