这个插件将实现文章置顶、浏览量统计、自定义字段点击统计三大功能,并
📦 插件名称:Statistician(统计师)
一、插件功能概述
| 功能模块 | 具体实现 | 技术特点 |
|---|---|---|
| 文章置顶 | 后台文章编辑页添加置顶开关,支持多篇文章置顶 | 基于Sticky插件思路,扩展字段标记 |
| 文章浏览量 | 自动记录文章被浏览次数,支持防刷机制 | 独立数据表存储,Cookie防刷 |
| 自定义字段统计 | 统计任意自定义字段(如下载链接)的点击次数 | 动态路由+异步请求,可扩展性强 |
二、数据库设计
创建独立数据表 typecho_statistician_stats:
CREATE TABLE `typecho_statistician_stats` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cid` int(10) unsigned NOT NULL COMMENT '文章ID',
`type` varchar(50) NOT NULL COMMENT '统计类型:views(浏览量)/field_字段名(如field_download)',
`value` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '统计数值',
`last_ip` varchar(45) DEFAULT NULL COMMENT '最后访问IP',
`last_time` int(10) unsigned DEFAULT NULL COMMENT '最后访问时间',
PRIMARY KEY (`id`),
UNIQUE KEY `cid_type` (`cid`,`type`),
KEY `type` (`type`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;三、插件目录结构
usr/plugins/Statistician/
├── Plugin.php # 主插件文件
├── Action.php # 路由动作处理
├── Widget.php # 数据展示Widget
├── install.php # 安装脚本(建表)
├── uninstall.php # 卸载脚本(删表)
├── assets/
│ ├── css/
│ │ └── admin.css # 后台样式
│ └── js/
│ ├── admin.js # 后台交互
│ └── frontend.js # 前端统计脚本
├── languages/ # 国际化(可选)
└── README.md # 说明文档四、核心代码实现
1. 插件主文件 Plugin.php
<?php
namespace TypechoPlugin\Statistician;
use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Form\Element\Text;
use Typecho\Widget\Helper\Form\Element\Radio;
use Widget\Options;
if (!defined('__TYPECHO_ROOT_DIR__')) {
exit;
}
/**
* 全能统计插件 - 支持文章置顶、浏览量、自定义字段点击统计
*
* @package Statistician
* @author YourName
* @version 1.0.0
* @link https://yourblog.com
*/
class Plugin implements PluginInterface
{
/**
* 激活插件
*/
public static function activate()
{
// 1. 注册钩子:文章编辑页添加置顶选项
\Typecho\Plugin::factory('admin/write-post.php')->bottom = __CLASS__ . '::renderStickyField';
\Typecho\Plugin::factory('admin/write-page.php')->bottom = __CLASS__ . '::renderStickyField';
// 2. 注册钩子:保存文章时处理置顶标记
\Typecho\Plugin::factory('Widget_Contents_Post_Edit')->finishPublish = __CLASS__ . '::saveStickyFlag';
// 3. 注册钩子:文章展示时增加浏览量
\Typecho\Plugin::factory('Widget_Archive')->beforeRender = __CLASS__ . '::recordView';
// 4. 添加路由:处理点击统计请求
\Typecho\Router::addRoute(
'statistician-click',
'/statistician/click/[cid]/[field]',
'TypechoPlugin\Statistician\Action',
'recordClick'
);
// 5. 安装数据库
self::installDb();
return _t('插件已激活,请配置统计选项');
}
/**
* 禁用插件
*/
public static function deactivate()
{
// 移除路由
\Typecho\Router::removeRoute('statistician-click');
return _t('插件已禁用');
}
/**
* 插件配置面板
*/
public static function config(Form $form)
{
// 统计开关
$enableView = new Radio(
'enable_view',
['1' => '开启', '0' => '关闭'],
'1',
_t('浏览量统计'),
_t('是否开启文章浏览量统计')
);
$form->addInput($enableView);
// 防刷时间间隔(分钟)
$viewInterval = new Text(
'view_interval',
null,
'60',
_t('浏览量防刷间隔'),
_t('同一IP在多少分钟内多次访问只统计一次,0表示不限制')
);
$form->addInput($viewInterval->addRule('isInteger', _t('请填写整数')));
// 自定义字段统计配置
$statFields = new Text(
'stat_fields',
null,
'download,baidu_down,lanzoub_down,alist_down',
_t('要统计的自定义字段'),
_t('填写字段名,多个用英文逗号分隔,如:download,baidu_down,lanzoub_down')
);
$form->addInput($statFields);
}
/**
* 安装数据库
*/
private static function installDb()
{
$db = \Typecho\Db::get();
$scripts = file_get_contents(__DIR__ . '/install.php');
$scripts = str_replace('typecho_', $db->getPrefix(), $scripts);
try {
foreach (explode(';', $scripts) as $script) {
$script = trim($script);
if ($script) {
$db->query($script, \Typecho\Db::WRITE);
}
}
} catch (\Exception $e) {
// 表可能已存在,忽略错误
}
}
/**
* 文章编辑页添加置顶选项
*/
public static function renderStickyField()
{
$cid = $_REQUEST['cid'] ?? 0;
$isSticky = self::isSticky($cid);
?>
<section class="typecho-post-option">
<label class="typecho-label">置顶设置</label>
<ul>
<li>
<input id="sticky" name="sticky" type="checkbox" value="1" <?php echo $isSticky ? 'checked' : ''; ?> />
<label for="sticky">将这篇文章置顶</label>
</li>
</ul>
</section>
<script src="<?php echo \Typecho\Common::url('Statistician/assets/js/admin.js', Options::alloc()->pluginUrl); ?>"></script>
<?php
}
/**
* 保存文章时处理置顶标记
*/
public static function saveStickyFlag($post)
{
$cid = $post->cid;
$sticky = $_POST['sticky'] ?? 0;
$db = \Typecho\Db::get();
if ($sticky) {
// 写入置顶标记到自定义字段
$db->query($db->insert('table.fields')
->rows([
'cid' => $cid,
'name' => 'sticky',
'type' => 'str',
'str_value' => '1'
]));
} else {
// 删除置顶标记
$db->query($db->delete('table.fields')
->where('cid = ? AND name = ?', $cid, 'sticky'));
}
}
/**
* 判断文章是否置顶
*/
public static function isSticky($cid)
{
$db = \Typecho\Db::get();
$sticky = $db->fetchRow($db->select('str_value')
->from('table.fields')
->where('cid = ? AND name = ?', $cid, 'sticky'));
return !empty($sticky);
}
/**
* 记录文章浏览量
*/
public static function recordView($archive)
{
// 只统计文章页面
if ($archive->is('single')) {
$cid = $archive->cid;
$db = \Typecho\Db::get();
// 获取配置
$options = Options::alloc()->plugin('Statistician');
$enableView = $options->enable_view ?? '1';
if (!$enableView) {
return;
}
$interval = intval($options->view_interval ?? 60);
// 防刷检查
$ip = $_SERVER['REMOTE_ADDR'];
$cookieKey = 'statistician_view_' . $cid;
if ($interval > 0) {
// 检查Cookie
if (isset($_COOKIE[$cookieKey])) {
return;
}
// 检查数据库记录
$last = $db->fetchRow($db->select('last_time, last_ip')
->from('table.statistician_stats')
->where('cid = ? AND type = ?', $cid, 'views'));
if ($last && $last['last_ip'] == $ip) {
$lastTime = $last['last_time'];
if (time() - $lastTime < $interval * 60) {
return; // 间隔内不统计
}
}
}
// 更新统计
$db->query($db->insert('table.statistician_stats')
->rows([
'cid' => $cid,
'type' => 'views',
'value' => 1,
'last_ip' => $ip,
'last_time' => time()
]) . ' ON DUPLICATE KEY UPDATE
value = value + 1,
last_ip = VALUES(last_ip),
last_time = VALUES(last_time)');
// 设置Cookie
if ($interval > 0) {
setcookie($cookieKey, '1', time() + $interval * 60, '/');
}
}
}
/**
* 获取文章浏览量
*/
public static function getViews($cid)
{
$db = \Typecho\Db::get();
$row = $db->fetchRow($db->select('value')
->from('table.statistician_stats')
->where('cid = ? AND type = ?', $cid, 'views'));
return $row ? intval($row['value']) : 0;
}
/**
* 获取字段点击量
*/
public static function getFieldClicks($cid, $field)
{
$db = \Typecho\Db::get();
$row = $db->fetchRow($db->select('value')
->from('table.statistician_stats')
->where('cid = ? AND type = ?', $cid, 'field_' . $field));
return $row ? intval($row['value']) : 0;
}
}2. 路由动作处理 Action.php
<?php
namespace TypechoPlugin\Statistician;
use Typecho\Widget\Exception;
class Action extends \Typecho\Widget
{
/**
* 记录字段点击
*/
public function recordClick()
{
$cid = $this->request->get('cid');
$field = $this->request->get('field');
// 参数验证
if (!$cid || !$field) {
$this->response->throwJson(['code' => 400, 'msg' => '参数错误']);
return;
}
// 检查该字段是否配置为需要统计
$options = \Widget\Options::alloc()->plugin('Statistician');
$statFields = explode(',', $options->stat_fields ?? '');
$statFields = array_map('trim', $statFields);
if (!in_array($field, $statFields)) {
$this->response->throwJson(['code' => 403, 'msg' => '该字段未启用统计']);
return;
}
$db = \Typecho\Db::get();
$ip = $_SERVER['REMOTE_ADDR'];
$type = 'field_' . $field;
// 可选:添加防刷限制
$interval = 10; // 10秒内不重复统计同个字段
$last = $db->fetchRow($db->select('last_time, last_ip')
->from('table.statistician_stats')
->where('cid = ? AND type = ?', $cid, $type));
if ($last && $last['last_ip'] == $ip) {
if (time() - $last['last_time'] < $interval) {
// 防刷,但不返回错误
$this->response->throwJson(['code' => 200, 'msg' => '统计成功(防刷)']);
return;
}
}
// 更新统计
$db->query($db->insert('table.statistician_stats')
->rows([
'cid' => $cid,
'type' => $type,
'value' => 1,
'last_ip' => $ip,
'last_time' => time()
]) . ' ON DUPLICATE KEY UPDATE
value = value + 1,
last_ip = VALUES(last_ip),
last_time = VALUES(last_time)');
$this->response->throwJson(['code' => 200, 'msg' => '统计成功']);
}
}3. 数据展示 Widget.php
<?php
namespace TypechoPlugin\Statistician;
class Widget extends \Typecho\Widget
{
/**
* 显示文章浏览量
*/
public static function views($cid, $echo = true)
{
$views = Plugin::getViews($cid);
$text = $views . ' 次阅读';
if ($echo) {
echo $text;
} else {
return $text;
}
}
/**
* 显示字段点击量
*/
public static function fieldClicks($cid, $field, $echo = true)
{
$clicks = Plugin::getFieldClicks($cid, $field);
$text = $clicks . ' 次下载';
if ($echo) {
echo $text;
} else {
return $text;
}
}
/**
* 获取置顶文章列表
*/
public static function getStickyPosts($limit = 5)
{
$db = \Typecho\Db::get();
// 查询有置顶标记的文章
$sticky = $db->fetchAll($db->select('cid')
->from('table.fields')
->where('name = ? AND str_value = ?', 'sticky', '1'));
if (empty($sticky)) {
return [];
}
$cids = array_column($sticky, 'cid');
$select = $db->select('cid, title, slug, created')
->from('table.contents')
->where('status = ? AND type = ?', 'publish', 'post')
->where('cid IN (' . implode(',', $cids) . ')')
->order('created', \Typecho\Db::SORT_DESC);
if ($limit > 0) {
$select->limit($limit);
}
return $db->fetchAll($select);
}
}4. 前端统计脚本 assets/js/frontend.js
/**
* Statistician 前端统计脚本
* 自动绑定自定义字段的点击统计
*/
(function() {
// 获取插件配置(通过PHP在页面中输出)
var config = window.StatisticianConfig || {
apiBase: '/statistician/click/',
fields: [] // 需要统计的字段名数组
};
// 初始化
function init() {
if (!config.fields || config.fields.length === 0) return;
// 遍历所有需要统计的字段
config.fields.forEach(function(field) {
bindFieldClicks(field);
});
}
// 绑定字段点击事件
function bindFieldClicks(field) {
// 查找包含该字段数据的元素
// 支持两种方式:
// 1. 元素上有 data-field="{字段名}" 属性
// 2. 元素上有 data-field-{字段名} 属性
var selectors = [
'[data-field="' + field + '"]',
'[data-field-' + field + ']'
];
selectors.forEach(function(selector) {
var elements = document.querySelectorAll(selector);
elements.forEach(function(el) {
el.addEventListener('click', function(e) {
// 获取文章CID
var cid = el.getAttribute('data-cid') ||
getCidFromUrl() ||
document.querySelector('meta[name="cid"]')?.content;
if (!cid) {
console.warn('Statistician: 未找到文章CID');
return;
}
// 发送统计请求
recordClick(cid, field);
});
});
});
}
// 发送点击统计
function recordClick(cid, field) {
var url = config.apiBase + cid + '/' + field;
// 使用fetch发送请求
fetch(url, {
method: 'GET',
credentials: 'same-origin',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.code === 200) {
console.log('Statistician: 统计成功', field);
}
})
.catch(err => {
console.warn('Statistician: 统计请求失败', err);
});
}
// 从URL中获取文章CID(适用于单页文章)
function getCidFromUrl() {
var match = window.location.pathname.match(/\/(\d+)\.html$/);
return match ? match[1] : null;
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();5. 安装脚本 install.php
CREATE TABLE IF NOT EXISTS `typecho_statistician_stats` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`cid` int(10) unsigned NOT NULL COMMENT '文章ID',
`type` varchar(50) NOT NULL COMMENT '统计类型',
`value` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '统计数值',
`last_ip` varchar(45) DEFAULT NULL COMMENT '最后访问IP',
`last_time` int(10) unsigned DEFAULT NULL COMMENT '最后访问时间',
PRIMARY KEY (`id`),
UNIQUE KEY `cid_type` (`cid`,`type`),
KEY `type` (`type`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;五、前端调用示例
在你的主题模板中,可以这样使用:
1. 显示浏览量
<?php while($this->next()): ?>
<article>
<h2><a href="<?php $this->permalink() ?>"><?php $this->title() ?></a></h2>
<!-- 显示浏览量 -->
<span class="post-views">
<?php \TypechoPlugin\Statistician\Widget::views($this->cid); ?>
</span>
<!-- 显示下载统计 -->
<?php if ($this->fields->download): ?>
<a href="<?php $this->fields->download() ?>"
class="download-btn"
data-field="download"
data-cid="<?php $this->cid() ?>"
target="_blank">
下载文件
</a>
<span class="download-count">
(<?php \TypechoPlugin\Statistician\Widget::fieldClicks($this->cid, 'download'); ?>)
</span>
<?php endif; ?>
</article>
<?php endwhile; ?>2. 在页面头部引入配置
在主题的 header.php 中添加:
<?php
// 获取需要统计的字段配置
$statFields = explode(',', \Widget\Options::alloc()->plugin('Statistician')->stat_fields ?? '');
$statFields = array_map('trim', $statFields);
?>
<script>
window.StatisticianConfig = {
apiBase: '<?php echo \Typecho\Common::url('statistician/click/', \Helper::options()->index); ?>',
fields: <?php echo json_encode($statFields); ?>
};
</script>
<script src="<?php echo \Typecho\Common::url('Statistician/assets/js/frontend.js', \Helper::options()->pluginUrl); ?>"></script>六、插件特点总结
- 一体化设计:集成了你需要的所有统计功能,无需多个插件组合
- 扩展性强:支持任意自定义字段统计,只需在配置中添加字段名
- 防刷机制:内置IP和Cookie双重防刷,统计数据更真实
- 使用简单:前端通过
data-field属性自动绑定,无需额外JS代码 - 数据独立:使用独立数据表存储,不影响系统原有表结构
- 兼容性好:基于Typecho官方钩子机制开发,符合规范
七、后续可扩展功能
- 数据可视化:后台增加统计图表展示
- 导出功能:支持导出统计数据为CSV
- 排行榜:自动生成热门文章、热门下载排行榜
- 定时清零:支持按周期重置统计数据
用上了,非常稳定!提个小建议:置顶功能如果能支持按置顶时间排序就更好了。
I wonder if this could be extended to track outbound links as well? That would make it a complete analytics solution for Typecho.
建议增加一个按时间段(比如本周、本月)的统计功能,能看到文章的近期热度变化。
The installation and uninstallation scripts are a nice touch. Clean plugin management is always appreciated.
作为一个经常分享下载链接的博主,这个自定义字段统计简直就是救星,能清楚知道哪些资源受欢迎。