重构数据总览部分代码并调整数据库索引加快查询

This commit is contained in:
Zhai Yiming 2017-07-13 18:39:15 +08:00
parent 599bdd3977
commit aaa2e5ad8f
4 changed files with 138 additions and 252 deletions

View File

@ -6,20 +6,23 @@ if (!defined('__ACCESS_PLUGIN_ROOT__')) {
class Access_Core
{
protected $db;
protected $prefix;
protected $table;
public $config;
protected $request;
protected $response;
protected $pageSize;
protected $isDrop;
public $ua;
public $config;
public $action;
public $title;
public $logs = array();
public $overview = array();
public $referer = array();
/**
* 构造函数,根据不同类型的请求,计算不同的数据并渲染输出
*
* @access public
* @return void
*/
public function __construct()
{
# Load language pack
@ -30,13 +33,10 @@ class Access_Core
}
# Init variables
$this->db = Typecho_Db::get();
$this->table = $this->db->getPrefix() . 'access_log';
$this->config = Typecho_Widget::widget('Widget_Options')->plugin('Access');
$this->request = Typecho_Request::getInstance();
$this->response = Typecho_Response::getInstance();
$this->pageSize = $this->config->pageSize;
$this->isDrop = $this->config->isDrop;
if ($this->pageSize == null || $this->isDrop == null) {
if ($this->config->pageSize == null || $this->config->isDrop == null) {
throw new Typecho_Plugin_Exception(_t('请先设置插件!'));
}
$this->ua = new Access_UA($this->request->getAgent());
@ -56,14 +56,20 @@ class Access_Core
}
}
/**
* 生成详细访问日志数据,提供给页面渲染使用
*
* @access public
* @return void
*/
protected function parseLogs()
{
$type = $this->request->get('type', 1);
$pagenum = $this->request->get('page', 1);
$offset = (max(intval($pagenum), 1) - 1) * $this->pageSize;
$offset = (max(intval($pagenum), 1) - 1) * $this->config->pageSize;
$query = $this->db->select()->from('table.access_log')
->order('time', Typecho_Db::SORT_DESC)
->offset($offset)->limit($this->pageSize);
->offset($offset)->limit($this->config->pageSize);
$qcount = $this->db->select('count(1) AS count')->from('table.access_log');
switch ($type) {
case 1:
@ -79,11 +85,11 @@ class Access_Core
}
$this->logs['list'] = $this->db->fetchAll($query);
$this->cleanArray($this->logs['list']);
$this->htmlEncode($this->logs['list']);
$this->logs['rows'] = $this->db->fetchAll($qcount)[0]['count'];
$page = new Access_Page($this->pageSize, $this->logs['rows'], $pagenum, 10, array(
$page = new Access_Page($this->config->pageSize, $this->logs['rows'], $pagenum, 10, array(
'panel' => Access_Plugin::$panel,
'action' => 'logs',
'type' => $type,
@ -91,85 +97,105 @@ class Access_Core
$this->logs['page'] = $page->show();
}
/**
* 生成来源统计数据,提供给页面渲染使用
*
* @access public
* @return void
*/
protected function parseReferer()
{
$this->referer['url'] = $this->db->fetchAll("SELECT DISTINCT referer, COUNT(*) as count FROM {$this->table} WHERE referer <> '' GROUP BY referer ORDER BY count DESC LIMIT {$this->pageSize}");
$this->referer['domain'] = $this->db->fetchAll("SELECT DISTINCT referer_domain, COUNT(*) as count FROM {$this->table} WHERE referer_domain <> '' GROUP BY referer_domain ORDER BY count DESC LIMIT {$this->pageSize}");
$this->cleanArray($this->referer);
$this->referer['url'] = $this->db->fetchAll($this->db->select('DISTINCT entrypoint AS value, COUNT(1) as count')
->from('table.access_log')->where("entrypoint <> ''")->group('entrypoint')
->order('count', Typecho_Db::SORT_DESC)->limit($this->config->pageSize));
$this->referer['domain'] = $this->db->fetchAll($this->db->select('DISTINCT entrypoint_domain AS value, COUNT(1) as count')
->from('table.access_log')->where("entrypoint_domain <> ''")->group('entrypoint_domain')
->order('count', Typecho_Db::SORT_DESC)->limit($this->config->pageSize));
$this->htmlEncode($this->referer);
}
/**
* 生成总览数据,提供给页面渲染使用
*
* @access public
* @return void
*/
protected function parseOverview()
{
$where = 'WHERE 1=1';
$this->overview['ip']['today']['total'] = 0;
$this->overview['uv']['today']['total'] = 0;
$this->overview['pv']['today']['total'] = 0;
$this->overview['ip']['yesterday']['total'] = 0;
$this->overview['uv']['yesterday']['total'] = 0;
$this->overview['pv']['yesterday']['total'] = 0;
for ($i = 0; $i < 24; $i++) {
$today = date("Y-m-d");
$start = strtotime(date("{$today} {$i}:00:00"));
$end = strtotime(date("{$today} {$i}:59:59"));
$this->overview['ip']['today']['hours'][] = count($this->db->fetchAll("SELECT DISTINCT ip FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}"));
$this->overview['ip']['today']['total'] += $this->overview['ip']['today']['hours'][$i];
$this->overview['uv']['today']['hours'][] = count($this->db->fetchAll("SELECT DISTINCT ip,ua FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}"));
$this->overview['uv']['today']['total'] += $this->overview['uv']['today']['hours'][$i];
$this->overview['pv']['today']['hours'][] = count($this->db->fetchAll("SELECT ip FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}"));
$this->overview['pv']['today']['total'] += $this->overview['pv']['today']['hours'][$i];
}
for ($i = 0; $i < 24; $i++) {
$yesterday = date("Y-m-d", time() - 24 * 60 * 60);
$start = strtotime(date("{$yesterday} {$i}:00:00"));
$end = strtotime(date("{$yesterday} {$i}:59:59"));
$this->overview['ip']['yesterday']['hours'][] = count($this->db->fetchAll("SELECT DISTINCT ip FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}"));
$this->overview['ip']['yesterday']['total'] += $this->overview['ip']['yesterday']['hours'][$i];
$this->overview['uv']['yesterday']['hours'][] = count($this->db->fetchAll("SELECT DISTINCT ip,ua FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}"));
$this->overview['uv']['yesterday']['total'] += $this->overview['uv']['yesterday']['hours'][$i];
$this->overview['pv']['yesterday']['hours'][] = count($this->db->fetchAll("SELECT ip FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}"));
$this->overview['pv']['yesterday']['total'] += $this->overview['pv']['yesterday']['hours'][$i];
}
$this->overview['ip']['all']['total'] = count($this->db->fetchAll("SELECT DISTINCT ip FROM {$this->table} {$where}"));
$this->overview['uv']['all']['total'] = count($this->db->fetchAll("SELECT DISTINCT ip,ua FROM {$this->table} {$where}"));
$this->overview['pv']['all']['total'] = count($this->db->fetchAll("SELECT ip FROM {$this->table} {$where}"));
$this->overview['chart']['title']['text'] = date("Y-m-d 统计");
$this->overview['chart']['xAxis']['categories'] = $this->buildObject(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23), true);
$this->overview['chart']['series']['pv'] = $this->buildObject($this->overview['pv']['today']['hours'], false);
$this->overview['chart']['series']['uv'] = $this->buildObject($this->overview['uv']['today']['hours'], false);
$this->overview['chart']['series']['ip'] = $this->buildObject($this->overview['ip']['today']['hours'], false);
}
protected function cleanArray(&$array)
{
if (is_array($array)) {
foreach ($array as &$value) {
if (!is_array($value)) {
$value = htmlspecialchars(urldecode($value));
} else {
$this->cleanArray($value);
}
# 初始化统计数组
foreach (['ip', 'uv', 'pv'] as $type) {
foreach (['today', 'yesterday'] as $day) {
$this->overview[$type][$day]['total'] = 0;
}
}
# 分类分时段统计数据
foreach (['today' => date("Y-m-d"), 'yesterday'=> date("Y-m-d", time() - 24 * 60 * 60)] as $day => $time) {
for ($i = 0; $i < 24; $i++) {
$time = date("Y-m-d");
$start = strtotime(date("{$time} {$i}:00:00"));
$end = strtotime(date("{$time} {$i}:59:59"));
// "SELECT DISTINCT ip FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}"));
$this->overview['ip'][$day]['hours'][$i] = intval($this->db->fetchAll($this->db->select('COUNT(1) AS count')
->from('(' . $this->db->select('DISTINCT ip')->from('table.access_log')
->where('time >= ? AND time <= ?', $start, $end) . ') AS tmp'))[0]['count']);
$this->overview['ip'][$day]['total'] += $this->overview['ip'][$day]['hours'][$i];
// "SELECT DISTINCT ip,ua FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}"));
$this->overview['uv'][$day]['hours'][$i] = intval($this->db->fetchAll($this->db->select('COUNT(1) AS count')
->from('(' . $this->db->select('DISTINCT ip,ua')->from('table.access_log')
->where('time >= ? AND time <= ?', $start, $end) . ') AS tmp'))[0]['count']);
$this->overview['uv'][$day]['total'] += $this->overview['uv'][$day]['hours'][$i];
// "SELECT ip FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}"));
$this->overview['pv'][$day]['hours'][$i] = intval($this->db->fetchAll($this->db->select('COUNT(1) AS count')
->from('table.access_log')->where('time >= ? AND time <= ?', $start, $end))[0]['count']);
$this->overview['pv'][$day]['total'] += $this->overview['pv'][$day]['hours'][$i];
}
}
# 总统计数据
// "SELECT DISTINCT ip FROM {$this->table} {$where}"));
$this->overview['ip']['all']['total'] = $this->db->fetchAll($this->db->select('COUNT(1) AS count')
->from('(' . $this->db->select('DISTINCT ip')->from('table.access_log') . ') AS tmp'))[0]['count'];
// "SELECT DISTINCT ip,ua FROM {$this->table} {$where}"));
$this->overview['uv']['all']['total'] = $this->db->fetchAll($this->db->select('COUNT(1) AS count')
->from('(' . $this->db->select('DISTINCT ip,ua')->from('table.access_log') . ') AS tmp'))[0]['count'];
// "SELECT ip FROM {$this->table} {$where}"));
$this->overview['pv']['all']['total'] = $this->db->fetchAll($this->db->select('COUNT(1) AS count')
->from('table.access_log'))[0]['count'];
# 分类型绘制24小时访问图
$this->overview['chart']['xAxis']['categories'] = json_encode([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23
]);
foreach (['ip', 'uv', 'pv'] as $type) {
$this->overview['chart']['series'][$type] = json_encode($this->overview[$type]['today']['hours']);
}
$this->overview['chart']['title']['text'] = _t('%s 统计', date("Y-m-d"));
}
protected function buildObject($array, $quote)
/**
* 转义特殊字符防止XSS等攻击
*
* @access public
* @return void
*/
protected function htmlEncode(&$variable)
{
$obj = Json::encode($array);
$obj = str_replace("\"", "'", $obj);
if ($quote) {
return $obj;
} else {
return str_replace("'", '', $obj);
if (is_array($variable)) {
foreach ($variable as &$value) {
$this->htmlEncode($value);
}
} elseif (is_string($variable)) {
$variable = htmlspecialchars(urldecode($variable));
}
}
/**
* 判断是否是管理员登录状态
*
* @access public
* @return bool
*/
public function isAdmin()
{
$hasLogin = Typecho_Widget::widget('Widget_User')->hasLogin();
@ -180,15 +206,27 @@ class Access_Core
return $isAdmin;
}
/**
* 删除记录
*
* @access public
* @return void
*/
public function deleteLogs($ids)
{
foreach ($ids as $id) {
$this->db->query($this->db->delete($this->table)
$this->db->query($this->db->delete('table.access_log')
->where('id = ?', $id)
);
}
}
/**
* 获取首次进入网站时的来源
*
* @access public
* @return string
*/
public function getEntryPoint()
{
$entrypoint = Typecho_Cookie::get('__typecho_access_entrypoint');
@ -204,6 +242,12 @@ class Access_Core
return $entrypoint;
}
/**
* 记录当前访问(管理员登录不会记录)
*
* @access public
* @return void
*/
public function writeLogs($url = null)
{
if ($this->isAdmin()) {

View File

@ -1,163 +0,0 @@
<?php
if (!defined('__ACCESS_PLUGIN_ROOT__')) {
throw new Exception('Boostrap file not found');
}
class Access_Parser
{
public $bots = array(
'TencentTraveler',
'Baiduspider',
'BaiduGame',
'Googlebot',
'msnbot',
'Sosospider+',
'Sogou web spider',
'ia_archiver',
'Yahoo! Slurp',
'YoudaoBot',
'Yahoo Slurp',
'MSNBot',
'Java (Often spam bot)',
'BaiDuSpider',
'Voila',
'Yandex bot',
'BSpider',
'twiceler',
'Sogou Spider',
'Speedy Spider',
'Google AdSense',
'Heritrix',
'Python-urllib',
'Alexa (IA Archiver)',
'Ask',
'Exabot',
'Custo',
'OutfoxBot/YodaoBot',
'yacy',
'SurveyBot',
'legs',
'lwp-trivial',
'Nutch',
'StackRambler',
'The web archive (IA Archiver)',
'Perl tool',
'MJ12bot',
'Netcraft',
'MSIECrawler',
'WGet tools',
'larbin',
'Fish search',
'crawler',
'bingbot',
'YisouSpider',
'Bot',
'Spider',
);
protected $currentBot = null;
public function getBrowser($ua)
{
$os = null;
if ($this->isBot($ua)) {
return $this->currentBot;
} elseif (preg_match('/Windows NT 6.0/i', $ua)) {
$os = 'Windows Vista';
} elseif (preg_match('/Windows NT 6.1/i', $ua)) {
$os = 'Windows 7';
} elseif (preg_match('/Windows NT 6.2/i', $ua)) {
$os = 'Windows 8';
} elseif (preg_match('/Windows NT 6.3/i', $ua)) {
$os = 'Windows 8.1';
} elseif (preg_match('/Windows NT 10.0/i', $ua)) {
$os = 'Windows 10';
} elseif (preg_match('/Windows NT 5.1/i', $ua)) {
$os = 'Windows XP';
} elseif (preg_match('/Windows NT 5.2/i', $ua) && preg_match('/Win64/i', $ua)) {
$os = 'Windows XP 64 bit';
} elseif (preg_match('/Android ([0-9.]+)/i', $ua, $matches)) {
$os = 'Android ' . $matches[1];
} elseif (preg_match('/iPhone OS ([_0-9]+)/i', $ua, $matches)) {
$os = 'iPhone ' . $matches[1];
} elseif (preg_match('/Ubuntu/i', $ua, $matches)) {
$os = 'Ubuntu ';
} elseif (preg_match('/Mac OS X ([0-9_]+)/i', $ua, $matches)) {
$os = 'Mac OS X ' . $matches[1];
} elseif (preg_match('/Linux/i', $ua, $matches)) {
$os = 'Linux';
} else {
$os = '未知';
}
if ($this->isBot($ua)) {
return $this->currentBot;
} elseif (preg_match('#(Camino|Chimera)[ /]([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = 'Camino ' . $matches[2];
} elseif (preg_match('#SE 2([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = '搜狗浏览器 2' . $matches[1];
} elseif (preg_match('#360([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = '360浏览器 ' . $matches[1];
} elseif (preg_match('#Maxthon( |\/)([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = 'Maxthon ' . $matches[2];
} elseif (preg_match('#Edge/([a-zA-Z0-9.]+)#i', $ua, $matches)) {
//Win10中Microsoft Edge浏览器
$browser = 'Edge ' . $matches[1];
} elseif (preg_match('#Chrome/([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = 'Chrome ' . $matches[1];
} elseif (preg_match('#XiaoMi/MiuiBrowser/([0-9.]+)#i', $ua, $matches)) {
$browser = '小米浏览器 ' . $matches[1];
} elseif (preg_match('#Safari/([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = 'Safari ' . $matches[1];
} elseif (preg_match('#opera mini#i', $ua)) {
preg_match('#Opera/([a-zA-Z0-9.]+)#i', $ua, $matches);
$browser = 'Opera Mini ' . $matches[1];
} elseif (preg_match('#Opera.([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = 'Opera ' . $matches[1];
} elseif (preg_match('#TencentTraveler ([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = '腾讯TT浏览器 ' . $matches[1];
} elseif (preg_match('#UCWEB([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = 'UCWEB ' . $matches[1];
} elseif (preg_match('#MSIE ([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = 'Internet Explorer ' . $matches[1];
} elseif (preg_match('#Trident#', $ua, $matches)) {
$browser = 'Internet Explorer 11';
} elseif (preg_match('#(Firefox|Phoenix|Firebird|BonEcho|GranParadiso|Minefield|Iceweasel)/([a-zA-Z0-9.]+)#i', $ua, $matches)) {
$browser = 'Firefox ' . $matches[2];
} else {
$browser = '未知';
}
return $os . ' / ' . $browser;
}
public function isBot($ua)
{
$ua = $this->filter($ua);
if (!empty($ua)) {
foreach ($this->bots as $val) {
if (($val == 'Bot' || $val == 'Spider')
&& (preg_match('#([a-zA-Z0-9]+(bot|spider))[ /]*([0-9.]*)#i', $ua, $matches))) {
$this->currentBot = $matches[1] . ' ' . $matches[3];
return true;
}
$str = $this->filter($val);
if (strpos($ua, $str) !== false) {
$this->currentBot = $str;
return true;
}
}
} else {
return false;
}
}
public function filter($str)
{
return $this->removeSpace(strtolower($str));
}
protected function removeSpace($str)
{
return preg_replace('/\s+/', '', $str);
}
}

View File

@ -184,7 +184,7 @@ $access = new Access_Core();
<tr>
<td><?php echo $key +1 ?></td>
<td><?php echo $value['count']?></td>
<td><?php echo $value['referer_domain']?></td>
<td><?php echo $value['value']?></td>
</tr>
<?php endforeach;?>
</tbody>
@ -212,7 +212,7 @@ $access = new Access_Core();
<tr>
<td><?php echo $key +1 ?></td>
<td><?php echo $value['count']?></td>
<td><?php echo $value['referer']?></td>
<td><?php echo $value['value']?></td>
</tr>
<?php endforeach;?>
</tbody>

View File

@ -20,14 +20,19 @@ CREATE TABLE `typecho_access_log` (
`robot_id` varchar(32) default '' ,
`robot_version` varchar(32) default '' ,
PRIMARY KEY (`id`),
KEY `idx_time` (`time` ),
KEY `idx_path` (`path` ),
KEY `idx_robot` (`robot`, `time`),
KEY `idx_os_id` (`os_id` ),
KEY `idx_robot_id` (`robot_id` ),
KEY `idx_browser_id` (`browser_id` ),
KEY `idx_content_id` (`content_id` ),
KEY `idx_meta_id` (`meta_id` ),
KEY `idx_time` (`time` ),
KEY `idx_path` (`path` ),
KEY `idx_ip_ua` (`ip`,`ua` ),
KEY `idx_robot` (`robot`, `time` ),
KEY `idx_os_id` (`os_id` ),
KEY `idx_robot_id` (`robot_id` ),
KEY `idx_browser_id` (`browser_id` ),
KEY `idx_content_id` (`content_id` ),
KEY `idx_meta_id` (`meta_id` ),
KEY `idx_entrypoint` (`entrypoint` ),
KEY `idx_entrypoint_domain` (`entrypoint_domain`),
KEY `idx_referer` (`referer` ),
KEY `idx_referer_domain` (`referer_domain` ),
CONSTRAINT `typecho_access_log_ibfk_cid` FOREIGN KEY (`content_id`) REFERENCES `typecho_contents` (`cid`) ON DELETE SET NULL ON UPDATE CASCADE,
CONSTRAINT `typecho_access_log_ibfk_mid` FOREIGN KEY (`meta_id` ) REFERENCES `typecho_metas` (`mid`) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=%charset%;