跳至正文

给博客加了个新功能,记录下过程

上周末闲得慌,翻自己博客后台,发现访问统计页面还是空的。之前用WordPress搭了个个人站,装了MonsterInsights的免费版,但Google Analytics的数据总感觉隔靴搔痒,看个UV都得等半天刷新。我就想,能不能自己写个简单的统计模块,直接存数据库,想看啥数据随时查,顺便练练手。

最开始的想法很粗暴:在主题的footer.php里插一段PHP,每次页面加载就往数据库写一条记录,记录IP、User-Agent、当前页面URL、时间戳。但写了两行代码我就意识到不对——这要是有人刷个几十次,我数据库不得炸了?而且同一个用户连续刷新,数据全是一堆重复IP,分析起来完全没用。

于是改成先查会话。用PHP的session_start(),给每个访客分配一个session_id,存到数据库时带上去。同一session在5分钟内重复访问同一页面,就不重复记录。这个逻辑其实挺糙的,但对付个人博客绰绰有余。代码写出来长这样:

“`php
function log_visit() {
if (is_admin()) return;
$session_id = session_id();
$page_url = $_SERVER[‘REQUEST_URI’];
$ip = $_SERVER[‘REMOTE_ADDR’];
$ua = $_SERVER[‘HTTP_USER_AGENT’];
$time = current_time(‘mysql’);

global $wpdb;
$table = $wpdb->prefix . ‘visit_logs’;
$last_visit = $wpdb->get_var($wpdb->prepare(
“SELECT visit_time FROM $table WHERE session_id = %s AND page_url = %s ORDER BY visit_time DESC LIMIT 1”,
$session_id, $page_url
));
if ($last_visit && (strtotime($time) – strtotime($last_visit) < 300)) {
return;
}
$wpdb->insert($table, [
‘session_id’ => $session_id,
‘page_url’ => $page_url,
‘ip’ => $ip,
‘user_agent’ => $ua,
‘visit_time’ => $time
]);
}
add_action(‘wp’, ‘log_visit’);
“`

写表结构的时候手贱加了个`ip`字段,后来想想其实没必要存完整IP,但已经写了就留着吧,万一以后要做地理定位呢。数据库建表SQL长这样:

“`sql
CREATE TABLE wp_visit_logs (
id bigint(20) NOT NULL AUTO_INCREMENT,
session_id varchar(64) NOT NULL,
page_url varchar(500) NOT NULL,
ip varchar(45) DEFAULT NULL,
user_agent text,
visit_time datetime NOT NULL,
PRIMARY KEY (id),
KEY session_id (session_id),
KEY page_url (page_url),
KEY visit_time (visit_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
“`

数据写进去了,接下来就是展示。我懒得搞复杂的图表库,直接拿Chart.js画两个简单的折线图:一个按天统计访问量,一个按小时统计。数据查询用`GROUP BY DATE(visit_time)`和`GROUP BY HOUR(visit_time)`,简单粗暴。

但问题来了——我的博客页面都是服务端渲染的,统计页面得放在WordPress后台的菜单里。于是注册了一个新的管理页面:

“`php
add_action(‘admin_menu’, function() {
add_menu_page(‘访问统计’, ‘访问统计’, ‘manage_options’, ‘my-visit-stats’, ‘render_stats_page’, ‘dashicons-chart-area’, 30);
});
“`

`render_stats_page`函数里,我调用了Chart.js的CDN版本3.9.1,然后用wp_localize_script把PHP查出来的数据传到JavaScript里。这一步折腾了我半小时,因为没注意wp_enqueue_script要在admin_enqueue_scripts钩子里调用,直接在render函数里写script标签,结果Chrome控制台报`Chart is not defined`,后来改成这样:

“`php
function enqueue_stats_scripts($hook) {
if ($hook != ‘toplevel_page_my-visit-stats’) return;
wp_enqueue_script(‘chart-js’, ‘https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js’, [], null, true);
wp_enqueue_script(‘stats-js’, get_template_directory_uri() . ‘/js/stats.js’, [‘chart-js’], ‘1.0’, true);
wp_localize_script(‘stats-js’, ‘statsData’, [
‘daily’ => get_daily_visits(),
‘hourly’ => get_hourly_visits()
]);
}
add_action(‘admin_enqueue_scripts’, ‘enqueue_stats_scripts’);
“`

数据查出来之后,我发现一个问题:当天访问量少的时候,折线图看起来像心电图,全是锯齿。因为如果某小时没有访问,数据库里就查不到那条记录,Chart.js默认不画点,直接跳过,导致线条断裂。解决办法是在PHP里补全缺失的时间段,如果没数据就填0。写了个循环把24小时都塞满:

“`php
function get_hourly_visits() {
global $wpdb;
$table = $wpdb->prefix . ‘visit_logs’;
$today = date(‘Y-m-d’);
$results = $wpdb->get_results($wpdb->prepare(
“SELECT HOUR(visit_time) as hour, COUNT(*) as count FROM $table WHERE DATE(visit_time) = %s GROUP BY HOUR(visit_time)”,
$today
));
$data = array_fill(0, 24, 0);
foreach ($results as $row) {
$data[$row->hour] = (int)$row->count;
}
return $data;
}
“`

搞完这些,我还加了两个小功能:一个是最热门文章排行(按访问次数排前10),一个是实时在线访客(5分钟内有活动的session数)。热门文章直接用`GROUP BY page_url ORDER BY count DESC`,实时在线用`SELECT COUNT(DISTINCT session_id) WHERE visit_time > NOW() – INTERVAL 5 MINUTE`。这两个查询都很轻,不用担心拖慢后台。

踩坑经验总结一下:

第一,session_start()必须在任何输出之前调用,WordPress里如果在插件或主题的init钩子里没处理好,会导致session冲突。我最后用了一个判断:如果session没启动就手动调用`if (!session_id()) session_start();`放在wp钩子之前。

第二,存User-Agent字段长度要够,有些爬虫的UA字符串巨长,我一开始设了varchar(255),结果插入时被截断了,而且没报错,数据丢了。后来改成text类型。

第三,Chart.js版本别乱用最新的,我试了4.x版本,结果发现它不再支持`new Chart(ctx, config)`那种旧写法,必须用`Chart.register(…)`注册组件。我嫌麻烦,直接锁3.9.1。

现在这个统计页面每天打开看看,虽然数据量不大(一天也就两三百次访问),但看着自己写的代码跑出来的数据,比GA那种黑盒舒服多了。唯一的问题是我没做缓存,每次打开后台页面都要查一次数据库,如果以后访问量大了,得加个Redis或者用WordPress的Transient API缓存一下。不过就我那个博客的访问量,估计得等到明年才需要考虑这个问题。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注