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获取数据
这个插件实现了一个完整的访客记录系统,包含了数据收集、存储、展示和管理功能。您可以根据实际需求进行调整和扩展。
暂无评论