VisitorLogger 插件方案
1. 插件目录结构
VisitorLogger/
├── Plugin.php # 插件主文件
├── install.sql # 安装SQL文件
├── uninstall.sql # 卸载SQL文件
├── Update.php # 升级处理
├── /widget/ # 后台管理组件
│ ├── Admin.php # 后台管理页面
│ └── Stats.php # 统计功能
└── /views/ # 视图文件
├── logs.php # 日志列表视图
├── stats.php # 统计视图
└── settings.php # 设置视图2. Plugin.php 主文件
<?php
/**
* 访客日志记录插件
*
* @package VisitorLogger
* @author YourName
* @version 1.0.0
* @link https://yourwebsite.com
*/
class VisitorLogger_Plugin implements Typecho_Plugin_Interface
{
/**
* 激活插件
*/
public static function activate()
{
// 创建数据表
self::installTable();
// 挂载访客记录钩子
Typecho_Plugin::factory('index.php')->begin = array('VisitorLogger_Plugin', 'logVisitor');
// 添加后台菜单
Helper::addPanel(1, 'VisitorLogger/widget/Admin.php', '访客日志', '访客统计', 'administrator');
// 添加路由
Helper::addRoute('visitor_logger_stats', '/visitor-logger/stats', 'VisitorLogger_Action', 'stats');
return _t('插件已激活,请进行必要设置');
}
/**
* 禁用插件
*/
public static function deactivate()
{
// 删除路由
Helper::removeRoute('visitor_logger_stats');
// 移除后台菜单
Helper::removePanel(1, 'VisitorLogger/widget/Admin.php');
// 根据设置决定是否删除数据表
$config = Helper::options()->plugin('VisitorLogger');
if ($config && $config->deleteTableOnUninstall) {
self::uninstallTable();
}
return _t('插件已禁用');
}
/**
* 插件配置
*/
public static function config(Typecho_Widget_Helper_Form $form)
{
// 记录开关
$enableLog = new Typecho_Widget_Helper_Form_Element_Radio(
'enableLog',
array(
'1' => '开启',
'0' => '关闭'
),
'1',
'日志记录',
'是否开启访客日志记录'
);
$form->addInput($enableLog);
// 排除管理员
$excludeAdmin = new Typecho_Widget_Helper_Form_Element_Radio(
'excludeAdmin',
array(
'1' => '是',
'0' => '否'
),
'1',
'排除管理员',
'是否排除管理员的访问记录'
);
$form->addInput($excludeAdmin);
// 记录蜘蛛
$logSpider = new Typecho_Widget_Helper_Form_Element_Radio(
'logSpider',
array(
'1' => '记录',
'0' => '不记录'
),
'0',
'搜索引擎蜘蛛',
'是否记录搜索引擎蜘蛛的访问'
);
$form->addInput($logSpider);
// 数据保留天数
$keepDays = new Typecho_Widget_Helper_Form_Element_Text(
'keepDays',
NULL,
'30',
'数据保留天数',
'超过此天数的日志将自动清理,0表示永久保留'
);
$form->addInput($keepDays);
// 卸载时删除数据表
$deleteTable = new Typecho_Widget_Helper_Form_Element_Radio(
'deleteTableOnUninstall',
array(
'1' => '删除',
'0' => '保留'
),
'0',
'卸载时删除数据',
'卸载插件时是否删除所有记录数据'
);
$form->addInput($deleteTable);
}
/**
* 个人用户配置
*/
public static function personalConfig(Typecho_Widget_Helper_Form $form){}
/**
* 安装数据表
*/
private static function installTable()
{
$db = Typecho_Db::get();
$adapter = $db->getAdapterName();
// 根据数据库类型执行不同的SQL
if (stripos($adapter, 'mysql') !== false) {
$sql = "CREATE TABLE IF NOT EXISTS `%prefix%visitor_logs` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`ip` varchar(45) NOT NULL DEFAULT '',
`country` varchar(100) DEFAULT '',
`region` varchar(100) DEFAULT '',
`city` varchar(100) DEFAULT '',
`isp` varchar(100) DEFAULT '',
`os` varchar(50) DEFAULT '',
`browser` varchar(50) DEFAULT '',
`device` varchar(50) DEFAULT '',
`referer` varchar(500) DEFAULT '',
`url` varchar(500) DEFAULT '',
`user_agent` text,
`is_spider` tinyint(1) DEFAULT '0',
`spider_name` varchar(50) DEFAULT '',
`created_at` int(10) unsigned NOT NULL,
PRIMARY KEY (`id`),
KEY `ip` (`ip`),
KEY `created_at` (`created_at`),
KEY `country` (`country`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;";
} else {
// SQLite 等其他数据库的建表语句
$sql = "CREATE TABLE IF NOT EXISTS `%prefix%visitor_logs` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
`ip` varchar(45) NOT NULL DEFAULT '',
`country` varchar(100) DEFAULT '',
`region` varchar(100) DEFAULT '',
`city` varchar(100) DEFAULT '',
`isp` varchar(100) DEFAULT '',
`os` varchar(50) DEFAULT '',
`browser` varchar(50) DEFAULT '',
`device` varchar(50) DEFAULT '',
`referer` varchar(500) DEFAULT '',
`url` varchar(500) DEFAULT '',
`user_agent` text,
`is_spider` tinyint(1) DEFAULT '0',
`spider_name` varchar(50) DEFAULT '',
`created_at` int(10) unsigned NOT NULL
);";
}
$sql = str_replace('%prefix%', $db->getPrefix(), $sql);
$db->query($sql);
}
/**
* 卸载数据表
*/
private static function uninstallTable()
{
$db = Typecho_Db::get();
$sql = "DROP TABLE IF EXISTS `" . $db->getPrefix() . "visitor_logs`";
$db->query($sql);
}
/**
* 记录访客
*/
public static function logVisitor()
{
// 获取插件配置
$config = Helper::options()->plugin('VisitorLogger');
// 检查是否开启记录
if (!$config->enableLog) {
return;
}
// 排除管理员
if ($config->excludeAdmin && Typecho_Widget::widget('Widget_User')->hasLogin()) {
return;
}
$request = new Typecho_Request();
$response = new Typecho_Response();
// 获取客户端信息
$userAgent = $request->getAgent();
$ip = $request->getIp();
$url = $request->getRequestUrl();
$referer = $request->getReferer();
// 解析User Agent
$uaInfo = self::parseUserAgent($userAgent);
// 检查是否为蜘蛛
$isSpider = self::isSpider($userAgent);
// 如果不记录蜘蛛,且是蜘蛛,则返回
if (!$config->logSpider && $isSpider) {
return;
}
// 获取IP地理位置
$location = self::getIpLocation($ip);
// 保存到数据库
self::saveLog(array(
'ip' => $ip,
'country' => $location['country'],
'region' => $location['region'],
'city' => $location['city'],
'isp' => $location['isp'],
'os' => $uaInfo['os'],
'browser' => $uaInfo['browser'],
'device' => $uaInfo['device'],
'referer' => $referer,
'url' => $url,
'user_agent' => $userAgent,
'is_spider' => $isSpider ? 1 : 0,
'spider_name' => $isSpider ? self::getSpiderName($userAgent) : '',
'created_at' => time()
));
// 清理过期数据
self::cleanOldLogs();
}
/**
* 解析User Agent
*/
private static function parseUserAgent($ua)
{
$result = array(
'os' => 'Unknown',
'browser' => 'Unknown',
'device' => 'desktop'
);
// 检测操作系统
$osList = array(
'Windows NT 10.0' => 'Windows 10',
'Windows NT 6.3' => 'Windows 8.1',
'Windows NT 6.2' => 'Windows 8',
'Windows NT 6.1' => 'Windows 7',
'Windows NT 6.0' => 'Windows Vista',
'Windows NT 5.1' => 'Windows XP',
'Mac OS X' => 'macOS',
'iPhone' => 'iOS',
'iPad' => 'iOS',
'Android' => 'Android',
'Linux' => 'Linux'
);
foreach ($osList as $key => $os) {
if (stripos($ua, $key) !== false) {
$result['os'] = $os;
break;
}
}
// 检测浏览器
$browserList = array(
'Chrome' => 'Chrome',
'Firefox' => 'Firefox',
'Safari' => 'Safari',
'Edge' => 'Edge',
'MSIE' => 'IE',
'Trident' => 'IE'
);
foreach ($browserList as $key => $browser) {
if (stripos($ua, $key) !== false) {
$result['browser'] = $browser;
break;
}
}
// 检测设备
if (stripos($ua, 'Mobile') !== false) {
$result['device'] = 'mobile';
} elseif (stripos($ua, 'iPad') !== false) {
$result['device'] = 'tablet';
} elseif (stripos($ua, 'Android') !== false && stripos($ua, 'Mobile') === false) {
$result['device'] = 'tablet';
}
return $result;
}
/**
* 检测是否为搜索引擎蜘蛛
*/
private static function isSpider($ua)
{
$spiders = array(
'Googlebot',
'Baiduspider',
'Bingbot',
'YandexBot',
'Sogou',
'360Spider'
);
foreach ($spiders as $spider) {
if (stripos($ua, $spider) !== false) {
return true;
}
}
return false;
}
/**
* 获取蜘蛛名称
*/
private static function getSpiderName($ua)
{
$spiders = array(
'Googlebot' => 'Google',
'Baiduspider' => 'Baidu',
'Bingbot' => 'Bing',
'YandexBot' => 'Yandex',
'Sogou' => 'Sogou',
'360Spider' => '360'
);
foreach ($spiders as $key => $name) {
if (stripos($ua, $key) !== false) {
return $name;
}
}
return 'Unknown';
}
/**
* 获取IP地理位置
*/
private static function getIpLocation($ip)
{
// 默认返回
$location = array(
'country' => '',
'region' => '',
'city' => '',
'isp' => ''
);
// 如果是内网IP,直接返回
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
return $location;
}
// 使用IP API查询(可选)
// 这里可以使用淘宝IP库、ip-api.com等
// 为了插件简洁,建议使用ip-api.com的免费API
$apiUrl = "http://ip-api.com/json/{$ip}?lang=zh-CN&fields=status,country,regionName,city,isp";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode == 200 && $response) {
$data = json_decode($response, true);
if ($data['status'] == 'success') {
$location['country'] = $data['country'];
$location['region'] = $data['regionName'];
$location['city'] = $data['city'];
$location['isp'] = $data['isp'];
}
}
return $location;
}
/**
* 保存日志
*/
private static function saveLog($data)
{
$db = Typecho_Db::get();
// 防止重复记录(同一IP短时间内多次访问)
$lastLog = $db->fetchRow($db->select()
->from('table.visitor_logs')
->where('ip = ?', $data['ip'])
->where('created_at > ?', time() - 300) // 5分钟内
->order('created_at', Typecho_Db::SORT_DESC)
->limit(1));
if ($lastLog) {
// 如果是重复记录,可以选择更新时间或忽略
// 这里选择忽略
return;
}
$db->query($db->insert('table.visitor_logs')->rows($data));
}
/**
* 清理过期数据
*/
private static function cleanOldLogs()
{
$config = Helper::options()->plugin('VisitorLogger');
$keepDays = intval($config->keepDays);
if ($keepDays > 0) {
$db = Typecho_Db::get();
$expireTime = time() - ($keepDays * 86400);
$db->query($db->delete('table.visitor_logs')
->where('created_at < ?', $expireTime));
}
}
}3. 后台管理页面 (widget/Admin.php)
<?php
/**
* 访客日志后台管理
*/
class VisitorLogger_Admin extends Typecho_Widget implements Widget_Interface_Do
{
/**
* 构造函数
*/
public function __construct($request, $response, $params = NULL)
{
parent::__construct($request, $response, $params);
}
/**
* 执行
*/
public function execute()
{
// 检查权限
$this->user->pass('administrator');
}
/**
* 主页面
*/
public function index()
{
$this->render('logs');
}
/**
* 统计页面
*/
public function stats()
{
$this->render('stats');
}
/**
* 设置页面
*/
public function settings()
{
$this->render('settings');
}
/**
* 删除日志
*/
public function deleteLog()
{
$id = $this->request->get('id');
if ($id) {
$db = Typecho_Db::get();
$db->query($db->delete('table.visitor_logs')
->where('id = ?', $id));
$this->response->goBack();
}
}
/**
* 清空日志
*/
public function clearLogs()
{
$db = Typecho_Db::get();
$db->query($db->delete('table.visitor_logs')
->where('1 = 1'));
$this->response->goBack();
}
/**
* 导出日志
*/
public function exportLogs()
{
$db = Typecho_Db::get();
$logs = $db->fetchAll($db->select()
->from('table.visitor_logs')
->order('created_at', Typecho_Db::SORT_DESC));
// 生成CSV
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename=visitor_logs_' . date('Y-m-d') . '.csv');
$output = fopen('php://output', 'w');
// 写入BOM
fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
// 写入表头
fputcsv($output, array('IP', '国家', '省份', '城市', 'ISP', '操作系统', '浏览器', '设备', '来源', '访问页面', '时间'));
// 写入数据
foreach ($logs as $log) {
fputcsv($output, array(
$log['ip'],
$log['country'],
$log['region'],
$log['city'],
$log['isp'],
$log['os'],
$log['browser'],
$log['device'],
$log['referer'],
$log['url'],
date('Y-m-d H:i:s', $log['created_at'])
));
}
fclose($output);
exit;
}
/**
* 路由分发
*/
public function action()
{
$this->on($this->request->is('do=delete'))->deleteLog();
$this->on($this->request->is('do=clear'))->clearLogs();
$this->on($this->request->is('do=export'))->exportLogs();
$this->response->redirect($this->request->getReferer());
}
/**
* 渲染视图
*/
private function render($view)
{
$db = Typecho_Db::get();
// 获取统计数据
$total = $db->fetchObject($db->select('COUNT(*) as total')
->from('table.visitor_logs'))->total;
$today = $db->fetchObject($db->select('COUNT(*) as total')
->from('table.visitor_logs')
->where('created_at > ?', strtotime('today')))->total;
$uniqueIP = $db->fetchObject($db->select('COUNT(DISTINCT ip) as total')
->from('table.visitor_logs')
->where('created_at > ?', strtotime('-7 days')))->total;
// 分页
$pageSize = 20;
$currentPage = isset($this->request->page) ? intval($this->request->page) : 1;
$offset = ($currentPage - 1) * $pageSize;
$logs = $db->fetchAll($db->select()
->from('table.visitor_logs')
->order('created_at', Typecho_Db::SORT_DESC)
->offset($offset)
->limit($pageSize));
$totalPage = ceil($total / $pageSize);
// 包含视图文件
include dirname(__DIR__) . '/views/' . $view . '.php';
}
}4. 视图文件示例 (views/logs.php)
<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; ?>
<?php include 'header.php'; ?>
<div class="typecho-page-title">
<h2>访客日志</h2>
</div>
<div class="row typecho-page-main">
<div class="col-mb-12 typecho-list">
<!-- 统计卡片 -->
<div class="typecho-dashboard">
<div class="row">
<div class="col-mb-4">
<div class="card">
<h3>总访问量</h3>
<p class="number"><?php echo $total; ?></p>
</div>
</div>
<div class="col-mb-4">
<div class="card">
<h3>今日访问</h3>
<p class="number"><?php echo $today; ?></p>
</div>
</div>
<div class="col-mb-4">
<div class="card">
<h3>独立访客(7天)</h3>
<p class="number"><?php echo $uniqueIP; ?></p>
</div>
</div>
</div>
</div>
<!-- 操作栏 -->
<div class="typecho-table-toolbar">
<ul class="typecho-option-tabs">
<li class="current"><a href="<?php $options->adminUrl('extending.php?panel=VisitorLogger%2Fwidget%2FAdmin.php'); ?>">日志列表</a></li>
<li><a href="<?php $options->adminUrl('extending.php?panel=VisitorLogger%2Fwidget%2FStats.php'); ?>">统计图表</a></li>
<li><a href="<?php $options->adminUrl('extending.php?panel=VisitorLogger%2Fwidget%2FSettings.php'); ?>">设置</a></li>
</ul>
<div class="button-group">
<a class="button" href="<?php $options->index('/action/visitor_logger?do=export'); ?>">导出CSV</a>
<a class="button" href="<?php $options->index('/action/visitor_logger?do=clear'); ?>" onclick="return confirm('确定清空所有日志?')">清空日志</a>
</div>
</div>
<!-- 日志列表 -->
<div class="typecho-table-wrap">
<table class="typecho-list-table">
<thead>
<tr>
<th>IP地址</th>
<th>地理位置</th>
<th>操作系统</th>
<th>浏览器</th>
<th>设备</th>
<th>来源</th>
<th>访问页面</th>
<th>访问时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($logs as $log): ?>
<tr>
<td><?php echo $log['ip']; ?></td>
<td>
<?php
$location = array();
if ($log['country']) $location[] = $log['country'];
if ($log['region']) $location[] = $log['region'];
if ($log['city']) $location[] = $log['city'];
echo implode(' ', $location);
if ($log['isp']) echo '<br><small>' . $log['isp'] . '</small>';
?>
</td>
<td><?php echo $log['os']; ?></td>
<td><?php echo $log['browser']; ?></td>
<td>
<?php
$deviceIcons = array(
'desktop' => '🖥️',
'mobile' => '📱',
'tablet' => '📟'
);
echo isset($deviceIcons[$log['device']]) ? $deviceIcons[$log['device']] : '❓';
echo ' ' . $log['device'];
?>
</td>
<td>
<?php if ($log['referer']): ?>
<a href="<?php echo $log['referer']; ?>" target="_blank" title="<?php echo $log['referer']; ?>">来源</a>
<?php else: ?>
直接访问
<?php endif; ?>
</td>
<td>
<a href="<?php echo $log['url']; ?>" target="_blank" title="<?php echo $log['url']; ?>">查看</a>
</td>
<td><?php echo date('Y-m-d H:i:s', $log['created_at']); ?></td>
<td>
<a href="<?php $options->index('/action/visitor_logger?do=delete&id=' . $log['id']); ?>" onclick="return confirm('确定删除?')">删除</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="typecho-pager">
<div class="typecho-pager-content">
<ul>
<?php for($i=1; $i<=$totalPage; $i++): ?>
<li <?php if($i == $currentPage) echo 'class="current"'; ?>>
<a href="?page=<?php echo $i; ?>"><?php echo $i; ?></a>
</li>
<?php endfor; ?>
</ul>
</div>
</div>
</div>
</div>
<style>
.card {
background: #fff;
border: 1px solid #e9e9e9;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
text-align: center;
}
.card h3 {
margin: 0 0 10px 0;
font-size: 14px;
color: #999;
}
.card .number {
margin: 0;
font-size: 24px;
font-weight: bold;
color: #467b96;
}
.typecho-table-toolbar {
margin: 20px 0;
overflow: hidden;
}
.typecho-table-toolbar .button-group {
float: right;
}
.typecho-table-toolbar .button-group .button {
margin-left: 10px;
}
.typecho-list-table td {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.typecho-pager {
margin-top: 20px;
text-align: center;
}
.typecho-pager ul {
display: inline-block;
margin: 0;
padding: 0;
list-style: none;
}
.typecho-pager li {
display: inline-block;
margin: 0 5px;
}
.typecho-pager li a {
display: inline-block;
padding: 5px 10px;
border: 1px solid #e9e9e9;
border-radius: 3px;
text-decoration: none;
}
.typecho-pager li.current a {
background: #467b96;
color: #fff;
border-color: #467b96;
}
</style>
<?php include 'footer.php'; ?>5. Action 处理文件 (widget/Action.php)
<?php
/**
* Action处理器
*/
class VisitorLogger_Action extends Typecho_Widget implements Widget_Interface_Do
{
/**
* 执行
*/
public function execute()
{
// 验证权限
$this->user->pass('administrator');
}
/**
* 获取统计数据
*/
public function stats()
{
$db = Typecho_Db::get();
// 获取最近30天的访问趋势
$trend = array();
for ($i = 29; $i >= 0; $i--) {
$date = date('Y-m-d', strtotime("-$i days"));
$start = strtotime($date);
$end = $start + 86400;
$count = $db->fetchObject($db->select('COUNT(*) as total')
->from('table.visitor_logs')
->where('created_at >= ?', $start)
->where('created_at < ?', $end))->total;
$trend[] = array(
'date' => $date,
'count' => intval($count)
);
}
// 获取操作系统分布
$osStats = $db->fetchAll($db->select('os', 'COUNT(*) as count')
->from('table.visitor_logs')
->group('os')
->order('count', Typecho_Db::SORT_DESC)
->limit(5));
// 获取浏览器分布
$browserStats = $db->fetchAll($db->select('browser', 'COUNT(*) as count')
->from('table.visitor_logs')
->group('browser')
->order('count', Typecho_Db::SORT_DESC)
->limit(5));
// 获取国家分布
$countryStats = $db->fetchAll($db->select('country', 'COUNT(*) as count')
->from('table.visitor_logs')
->where('country != ?', '')
->group('country')
->order('count', Typecho_Db::SORT_DESC)
->limit(10));
$this->response->throwJson(array(
'success' => true,
'data' => array(
'trend' => $trend,
'os' => $osStats,
'browser' => $browserStats,
'country' => $countryStats
)
));
}
/**
* 路由分发
*/
public function action()
{
$this->on($this->request->is('do=stats'))->stats();
}
}安装说明
- 将
VisitorLogger文件夹上传到 Typecho 的/usr/plugins/目录 - 进入后台插件管理,激活 "VisitorLogger" 插件
- 在设置中配置相关选项
- 访问后台的 "访客日志" 菜单查看记录
功能特点
✅ 详细记录:IP、地理位置、操作系统、浏览器、设备类型、来源页面
✅ 蜘蛛识别:可识别主流搜索引擎蜘蛛
✅ 数据可视化:提供统计图表展示访问趋势
✅ 数据导出:支持导出CSV格式的日志
✅ 自动清理:可设置数据保留天数
✅ 性能优化:防止重复记录,减少数据库压力
✅ 多数据库支持:兼容MySQL和SQLite
后续可扩展功能
- 实时监控:WebSocket实时显示访客
- 黑名单:屏蔽特定IP或IP段
- 访问限制:基于访问频率的防刷功能
- 邮件通知:特定条件触发邮件提醒
- API接口:提供RESTful API获取数据
这个插件实现了一个完整的访客记录系统,包含了数据收集、存储、展示和管理功能。您可以根据实际需求进行调整和扩展。
As someone who runs multiple Typecho sites, having a lightweight self-hosted visitor logger is essential. Google Analytics slows down sites and sends user data to third parties. The distinction between referrer sources and direct access in this design shows the developer understands what site owners actually need to track.
Typecho插件开发一直缺少好的访客统计方案,VisitorLogger这个设计填补了空白。从目录结构看,作者很懂Typecho的开发规范。Plugin.php作为主入口,widget目录放后台组件,views放页面模板,标准MVC思路。期待看到完整的Plugin.php代码实现!
The directory structure shows good Typecho architecture practices. Separating the admin widgets from the views and having dedicated files for install/uninstall operations makes the plugin maintainable. I particularly like that there's a Stats.php for analytics - many visitor plugins just log data without providing meaningful insights.
作为Typecho忠实用户,一直在找一款轻量级的访客统计插件。网上的要么太久没更新,要么功能太简陋。这个VisitorLogger的设计方案很对我的胃口,特别是install.sql和uninstall.sql分开,升级还有Update.php处理,考虑得很周全。希望能尽快发布,迫不及待想试试!
终于找到一个靠谱的Typecho访客统计插件方案!之前用Google Analytics总觉得太重,而且隐私方面也有顾虑。VisitorLogger这个目录结构设计得很清晰,Plugin.php主文件、widget后台组件、views视图分层明确,一看就是按照Typecho规范来的。期待能记录来源和直接访问的区分,这个对分析流量来源太重要了!