设计一个Typecho访客统计插件,命名为"VisitorLogger"(访客日志)。这个插件会记录访客的详细信息,并提供后台管理功能。

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();
    }
}

安装说明

  1. VisitorLogger 文件夹上传到 Typecho 的 /usr/plugins/ 目录
  2. 进入后台插件管理,激活 "VisitorLogger" 插件
  3. 在设置中配置相关选项
  4. 访问后台的 "访客日志" 菜单查看记录

功能特点

详细记录:IP、地理位置、操作系统、浏览器、设备类型、来源页面
蜘蛛识别:可识别主流搜索引擎蜘蛛
数据可视化:提供统计图表展示访问趋势
数据导出:支持导出CSV格式的日志
自动清理:可设置数据保留天数
性能优化:防止重复记录,减少数据库压力
多数据库支持:兼容MySQL和SQLite

后续可扩展功能

  1. 实时监控:WebSocket实时显示访客
  2. 黑名单:屏蔽特定IP或IP段
  3. 访问限制:基于访问频率的防刷功能
  4. 邮件通知:特定条件触发邮件提醒
  5. API接口:提供RESTful API获取数据

这个插件实现了一个完整的访客记录系统,包含了数据收集、存储、展示和管理功能。您可以根据实际需求进行调整和扩展。

暂无评论