From 1c5266e6374605296dd74aa598e559c0347ac281 Mon Sep 17 00:00:00 2001 From: KimmyXYC <69386744+KimmyXYC@users.noreply.github.com> Date: Sun, 7 Jan 2024 18:57:29 +0800 Subject: [PATCH] feat: Initial support for PostgreSQL (#80) --- Access_Migration.php | 62 +++++++++++++++---- Access_Statistic.php | 140 ++++++++++++++++++++++++++++++------------- Plugin.php | 34 ++++++++++- sql/Pgsql.sql | 41 +++++++++++++ 4 files changed, 221 insertions(+), 56 deletions(-) create mode 100644 sql/Pgsql.sql diff --git a/Access_Migration.php b/Access_Migration.php index 8b37aaf..ae44c6a 100644 --- a/Access_Migration.php +++ b/Access_Migration.php @@ -33,11 +33,21 @@ class Access_Migration { public function exists() { $prefix = $this->db->getPrefix(); - if ($this->db->fetchRow($this->db->query("SHOW TABLES LIKE '{$prefix}access';", Typecho_Db::READ))) { - return true; - } - if ($this->db->fetchRow($this->db->query("SHOW TABLES LIKE '{$prefix}access_log';", Typecho_Db::READ))) { - return true; + $adapterName = $this->db->getAdapterName(); + if (strpos($adapterName, 'Mysql') !== false || strpos($adapterName, 'SQLite') !== false) { + if ($this->db->fetchRow($this->db->query("SHOW TABLES LIKE '{$prefix}access';", Typecho_Db::READ))) { + return true; + } + if ($this->db->fetchRow($this->db->query("SHOW TABLES LIKE '{$prefix}access_log';", Typecho_Db::READ))) { + return true; + } + } else if (strpos($adapterName, 'Pgsql') !== false) { + if ($this->db->fetchRow($this->db->query("SELECT to_regclass('public.{$prefix}access');", Typecho_Db::READ))) { + return true; + } + if ($this->db->fetchRow($this->db->query("SELECT to_regclass('public.{$prefix}access_log');", Typecho_Db::READ))) { + return true; + } } return false; } @@ -53,18 +63,33 @@ class Access_Migration { { $resp = [ 'total' => 0 ]; $prefix = $this->db->getPrefix(); + $adapterName = $this->db->getAdapterName(); + // 统计 v1 版本数据 - if ($this->db->fetchRow($this->db->query("SHOW TABLES LIKE '{$prefix}access';", Typecho_Db::READ))) { + if (strpos($adapterName, 'Mysql') !== false || strpos($adapterName, 'SQLite') !== false) { + $checkTableV1 = $this->db->fetchRow($this->db->query("SHOW TABLES LIKE '{$prefix}access';", Typecho_Db::READ)); + } else if (strpos($adapterName, 'Pgsql') !== false) { + $checkTableV1 = $this->db->fetchRow($this->db->query("SELECT tablename FROM pg_catalog.pg_tables WHERE tablename = '{$prefix}access';", Typecho_Db::READ)); + } + if ($checkTableV1) { $resp['v1'] = $this->db->fetchRow($this->db->select('COUNT(1) AS cnt')->from('table.access'))['cnt']; $resp['total'] += $resp['v1']; } + // 统计 v2 版本数据 - if ($this->db->fetchRow($this->db->query("SHOW TABLES LIKE '{$prefix}access_log';", Typecho_Db::READ))) { + if (strpos($adapterName, 'Mysql') !== false || strpos($adapterName, 'SQLite') !== false) { + $checkTableV2 = $this->db->fetchRow($this->db->query("SHOW TABLES LIKE '{$prefix}access_log';", Typecho_Db::READ)); + } else if (strpos($adapterName, 'Pgsql') !== false) { + $checkTableV2 = $this->db->fetchRow($this->db->query("SELECT tablename FROM pg_catalog.pg_tables WHERE tablename = '{$prefix}access_log';", Typecho_Db::READ)); + } + if ($checkTableV2) { $resp['v2'] = intval($this->db->fetchRow($this->db->select('COUNT(1) AS cnt')->from('table.access_log'))['cnt']); $resp['total'] += $resp['v2']; } + return $resp; } + /** * 迁移旧版本数据,单次1000条 @@ -78,8 +103,13 @@ class Access_Migration { $resp = [ 'count' => 0, 'remain' => 0 ]; $step = 1000; $prefix = $this->db->getPrefix(); + $adapterName = $this->db->getAdapterName(); + + $showTableQuery = strpos($adapterName, 'Pgsql') !== false ? + "SELECT * FROM information_schema.tables WHERE table_name LIKE '{$prefix}access';" : + "SHOW TABLES LIKE '{$prefix}access';"; // 迁移 v1 版本数据 - if ($this->db->fetchRow($this->db->query("SHOW TABLES LIKE '{$prefix}access';", Typecho_Db::READ))) { + if ($this->db->fetchRow($this->db->query($showTableQuery, Typecho_Db::READ))) { $remain = intval($this->db->fetchRow($this->db->select('COUNT(1) AS cnt')->from('table.access'))['cnt']); if ($resp['count'] === 0) { $rows = $this->db->fetchAll($this->db->select()->from('table.access')->limit($step)); @@ -108,12 +138,19 @@ class Access_Migration { } } if ($remain === 0) { - $this->db->query("DROP TABLE `{$prefix}access`;", Typecho_Db::WRITE); + $dropTableQuery = strpos($adapterName, 'Pgsql') !== false ? + "DROP TABLE IF EXISTS {$prefix}access;" : + "DROP TABLE `{$prefix}access`;"; + $this->db->query($dropTableQuery, Typecho_Db::WRITE); } $resp['remain'] += $remain; } + + $showTableQuery = strpos($adapterName, 'Pgsql') !== false ? + "SELECT * FROM information_schema.tables WHERE table_name LIKE '{$prefix}access_log';" : + "SHOW TABLES LIKE '{$prefix}access_log';"; // 迁移 v2 版本数据 - if ($this->db->fetchRow($this->db->query("SHOW TABLES LIKE '{$prefix}access_log';", Typecho_Db::READ))) { + if ($this->db->fetchRow($this->db->query($showTableQuery, Typecho_Db::READ))) { $remain = intval($this->db->fetchRow($this->db->select('COUNT(1) AS cnt')->from('table.access_log'))['cnt']); if ($resp['count'] === 0) { $rows = $this->db->fetchAll($this->db->select()->from('table.access_log')->limit($step)); @@ -128,7 +165,10 @@ class Access_Migration { } } if ($remain === 0) { - $this->db->query("DROP TABLE `{$prefix}access_log`;", Typecho_Db::WRITE); + $dropTableQuery = strpos($adapterName, 'Pgsql') !== false ? + "DROP TABLE IF EXISTS {$prefix}access_log;" : + "DROP TABLE `{$prefix}access_log`;"; + $this->db->query($dropTableQuery, Typecho_Db::WRITE); } $resp['remain'] += $remain; } diff --git a/Access_Statistic.php b/Access_Statistic.php index fa1586c..f0e78a0 100644 --- a/Access_Statistic.php +++ b/Access_Statistic.php @@ -100,23 +100,48 @@ class Access_Statistic { $resp = []; $ps = $this->request->get('ps', 10); # 页大小 # 统计文章浏览比例 - foreach( - $this->db->fetchAll( - $this->db - ->select('content_id AS cid, table.contents.title AS title, COUNT(1) AS cnt') - ->from('table.access_logs') - ->join('table.contents', 'content_id = table.contents.cid', Typecho_Db::INNER_JOIN) - ->where('IFNULL(content_id, 0)') - ->group('content_id') - ->order('cnt', Typecho_Db::SORT_DESC) - ->limit($ps) - ) as $i - ) { - $resp[] = [ - 'cid' => intval($i['cid']), - 'title' => $i['title'], - 'count' => intval($i['cnt']) - ]; + + $adapterName = $this->db->getAdapterName(); + if (strpos($adapterName, 'Mysql') !== false || strpos($adapterName, 'SQLite') !== false) { + foreach( + $this->db->fetchAll( + $this->db + ->select('content_id AS cid, table.contents.title AS title, COUNT(1) AS cnt') + ->from('table.access_logs') + ->join('table.contents', 'content_id = table.contents.cid', Typecho_Db::INNER_JOIN) + ->where('IFNULL(content_id, 0)') + ->group('content_id') + ->order('cnt', Typecho_Db::SORT_DESC) + ->limit($ps) + ) as $i + ) { + $resp[] = [ + 'cid' => intval($i['cid']), + 'title' => $i['title'], + 'count' => intval($i['cnt']) + ]; + } + } else if (strpos($adapterName, 'Pgsql') !== false) { + foreach( + $this->db->fetchAll( + $this->db + ->select('content_id AS cid, MAX(table.contents.title) AS title, COUNT(1) AS cnt') + ->from('table.access_logs') + ->join('table.contents', 'content_id = table.contents.cid', Typecho_Db::INNER_JOIN) + ->where('content_id <> ?', 0) + ->group('content_id') + ->order('cnt', Typecho_Db::SORT_DESC) + ->limit($ps) + ) as $i + ) { + $resp[] = [ + 'cid' => intval($i['cid']), + 'title' => $i['title'], + 'count' => intval($i['cnt']) + ]; + } + } else { + throw new Exception("Unsupported database adapter: " . $adapterName); } return $resp; } @@ -132,32 +157,61 @@ class Access_Statistic { $resp = []; $ps = $this->request->get('ps', 10); # 页大小 $cate = $this->request->get('cate'); # 类型 - switch($cate) { - case 'china': - # 国内 - $fetchData = $this->db->fetchAll( - $this->db - ->select("IF(ip_province = '中国', '国内未明确', ip_province) AS area, COUNT(1) AS cnt") - ->from('table.access_logs') - ->where("ip_country = '中国'") - ->group('area') - ->order('cnt', Typecho_Db::SORT_DESC) - ->limit($ps) - ); - break; - case 'inter': - # 国际 - $fetchData = $this->db->fetchAll( - $this->db - ->select("ip_country AS area, COUNT(1) AS cnt") - ->from('table.access_logs') - ->group('area') - ->order('cnt', Typecho_Db::SORT_DESC) - ->limit(15) - ); - break; - default: - throw new Exception('Bad Request', 400); + $adapterName = $this->db->getAdapterName(); + if (strpos($adapterName, 'Mysql') !== false || strpos($adapterName, 'SQLite') !== false) { + switch($cate) { + case 'china': + # 国内 + $fetchData = $this->db->fetchAll( + $this->db + ->select("IF(ip_province = '中国', '国内未明确', ip_province) AS area, COUNT(1) AS cnt") + ->from('table.access_logs') + ->where("ip_country = '中国'") + ->group('area') + ->order('cnt', Typecho_Db::SORT_DESC) + ->limit($ps) + ); + break; + case 'inter': + # 国际 + $fetchData = $this->db->fetchAll( + $this->db + ->select("ip_country AS area, COUNT(1) AS cnt") + ->from('table.access_logs') + ->group('area') + ->order('cnt', Typecho_Db::SORT_DESC) + ->limit(15) + ); + break; + default: + throw new Exception('Bad Request', 400); + } + } else if (strpos($adapterName, 'Pgsql') !== false) { + switch($cate) { + case 'china': + # 国内 + $sql = "SELECT CASE WHEN ip_province = '中国' THEN '国内未明确' ELSE ip_province END AS area, COUNT(1) AS cnt + FROM {$this->db->getPrefix()}access_logs + WHERE ip_country = '中国' + GROUP BY area + ORDER BY cnt DESC + LIMIT {$ps}"; + $fetchData = $this->db->query($sql)->fetchAll(); + break; + case 'inter': + # 国际 + $fetchData = $this->db->fetchAll( + $this->db + ->select("ip_country AS area, COUNT(1) AS cnt") + ->from('table.access_logs') + ->group('area') + ->order('cnt', Typecho_Db::SORT_DESC) + ->limit(15) + ); + break; + default: + throw new Exception('Bad Request', 400); + } } foreach($fetchData as $row) { $resp[] = [ diff --git a/Plugin.php b/Plugin.php index 9c87057..0b3710a 100644 --- a/Plugin.php +++ b/Plugin.php @@ -46,7 +46,12 @@ class Access_Plugin implements Typecho_Plugin_Interface $config = Typecho_Widget::widget('Widget_Options')->plugin('Access'); if ($config->isDrop == 0) { $db = Typecho_Db::get(); - $db->query("DROP TABLE `{$db->getPrefix()}access_logs`", Typecho_Db::WRITE); + $adapterName = $db->getAdapterName(); + if (strpos($adapterName, 'Mysql') !== false || strpos($adapterName, 'SQLite') !== false) { + $db->query("DROP TABLE `{$db->getPrefix()}access_logs`", Typecho_Db::WRITE); + } else if (strpos($adapterName, 'Pgsql') !== false) { + $db->query("DROP TABLE \"{$db->getPrefix()}access_logs\"", Typecho_Db::WRITE); + } } Helper::removePanel(1, self::$panel); Helper::removeRoute("access_track_gif"); @@ -161,8 +166,33 @@ class Access_Plugin implements Typecho_Plugin_Interface } catch (Exception $e) { throw new Typecho_Plugin_Exception($e->getMessage()); } + } else if (strpos($adapterName, 'Pgsql') !== false) { + $prefix = $db->getPrefix(); + $scripts = file_get_contents('usr/plugins/Access/sql/Pgsql.sql'); + $scripts = str_replace('typecho_', $prefix, $scripts); + $scripts = explode(';', $scripts); + try { + $configLink = '' . _t('前往设置') . ''; + # 初始化数据库如果不存在 + if (!$db->fetchRow($db->query("SELECT tablename FROM pg_tables WHERE schemaname='public' AND tablename='{$prefix}access_logs';", Typecho_Db::READ))) { + foreach ($scripts as $script) { + $script = trim($script); + if ($script) { + $db->query($script, Typecho_Db::WRITE); + } + } + $msg = _t('成功创建数据表,插件启用成功,') . $configLink; + } else { + $msg = _t('数据表已存在,插件启用成功,') . $configLink; + } + return $msg; + } catch (Typecho_Db_Exception $e) { + throw new Typecho_Plugin_Exception(_t('数据表建立失败,插件启用失败,错误信息:%s。', $e->getMessage())); + } catch (Exception $e) { + throw new Typecho_Plugin_Exception($e->getMessage()); + } } else { - throw new Typecho_Plugin_Exception(_t('你的适配器为%s,目前只支持Mysql和SQLite', $adapterName)); + throw new Typecho_Plugin_Exception(_t('你的适配器为%s,目前只支持Mysql、SQLite和Pgsql', $adapterName)); } } diff --git a/sql/Pgsql.sql b/sql/Pgsql.sql new file mode 100644 index 0000000..cb09ad7 --- /dev/null +++ b/sql/Pgsql.sql @@ -0,0 +1,41 @@ +CREATE TABLE typecho_access_logs ( + id serial PRIMARY KEY, + ua varchar(512) DEFAULT '' , + browser_id varchar(32) DEFAULT '' , + browser_version varchar(32) DEFAULT '' , + os_id varchar(32) DEFAULT '' , + os_version varchar(32) DEFAULT '' , + url varchar(255) DEFAULT '' , + path varchar(255) DEFAULT '' , + query_string varchar(255) DEFAULT '' , + ip varchar(64) DEFAULT '' , + ip_country varchar(255) DEFAULT '' , + ip_province varchar(255) DEFAULT '' , + ip_city varchar(255) DEFAULT '' , + entrypoint varchar(255) DEFAULT '' , + entrypoint_domain varchar(100) DEFAULT '' , + referer varchar(255) DEFAULT '' , + referer_domain varchar(100) DEFAULT '' , + time int DEFAULT '0' , + content_id int DEFAULT NULL, + meta_id int DEFAULT NULL, + robot boolean DEFAULT '0' , + robot_id varchar(32) DEFAULT '' , + robot_version varchar(32) DEFAULT '' +); + +CREATE INDEX idx_time ON typecho_access_logs (time); +CREATE INDEX idx_path ON typecho_access_logs (path); +CREATE INDEX idx_ua ON typecho_access_logs (ua); +CREATE INDEX idx_ip_ua ON typecho_access_logs (ip, ua); +CREATE INDEX idx_ip_province ON typecho_access_logs (ip_province); +CREATE INDEX idx_robot ON typecho_access_logs (robot, time); +CREATE INDEX idx_os_id ON typecho_access_logs (os_id); +CREATE INDEX idx_robot_id ON typecho_access_logs (robot_id); +CREATE INDEX idx_browser_id ON typecho_access_logs (browser_id); +CREATE INDEX idx_content_id ON typecho_access_logs (content_id); +CREATE INDEX idx_meta_id ON typecho_access_logs (meta_id); +CREATE INDEX idx_entrypoint ON typecho_access_logs (entrypoint); +CREATE INDEX idx_entrypoint_domain ON typecho_access_logs (entrypoint_domain); +CREATE INDEX idx_referer ON typecho_access_logs (referer); +CREATE INDEX idx_referer_domain ON typecho_access_logs (referer_domain);