pass('administrator', true)) { die('无权限访问'); } $options = \Widget\Options::alloc(); // 从数据库读取路由设置 $db = \Typecho\Db::get(); $settingQuery = $db->select()->from('table.options')->where('name = ?', 'routingTable'); $settingRow = $db->fetchRow($settingQuery); $routingTable = @unserialize($settingRow['value']); // post 路由的 key 可能是 'post' 或 'archives' $postUrlRule = null; if (isset($routingTable[0]['post']['url'])) { $postUrlRule = $routingTable[0]['post']['url']; } elseif (isset($routingTable['post']['url'])) { $postUrlRule = $routingTable['post']['url']; } elseif (isset($routingTable[0]['archives']['url'])) { $postUrlRule = $routingTable[0]['archives']['url']; } elseif (isset($routingTable['archives']['url'])) { $postUrlRule = $routingTable['archives']['url']; } if (empty($postUrlRule)) { $postUrlRule = '/archives/{cid}/'; } // 检测是否包含 .html 后缀(兼容 [cid:digital] 和 {cid} 两种格式) if (strpos($postUrlRule, '.html') !== false) { $postUrlFormat = $options->siteUrl . '%s.html'; } else { $postUrlFormat = $options->siteUrl . '%s/'; } // 标签地址格式 - 修正为 tag-slug.html $tagUrlFormat = $options->siteUrl . 'tag-%s.html'; try { $pluginConfig = $options->plugin('YearlyData'); $topLimit = isset($pluginConfig->topLimit) ? intval($pluginConfig->topLimit) : 10; $includeDraft = isset($pluginConfig->includeDraft) ? $pluginConfig->includeDraft : '0'; $chartColor = isset($pluginConfig->chartColor) ? $pluginConfig->chartColor : '#667eea'; // 从数据库读取当前年份的目标设置 // 使用独立的配置存储,键名为:yearly_target_2026 $currentYear = isset($_GET['year']) ? intval($_GET['year']) : intval(date('Y')); $targetsQuery = $db->select('value')->from('table.options')->where('name = ?', 'yearly_target_' . $currentYear); $targetsRow = $db->fetchRow($targetsQuery); if ($targetsRow && $targetsRow['value']) { $targets = @unserialize($targetsRow['value']); $targetPosts = isset($targets['posts']) ? intval($targets['posts']) : 0; $targetComments = isset($targets['comments']) ? intval($targets['comments']) : 0; $targetImages = isset($targets['images']) ? intval($targets['images']) : 0; $targetWords = isset($targets['words']) ? intval($targets['words']) : 0; } else { // 如果没有找到该年份的目标设置,检查是否是当前年份且插件配置中有设置 if ($currentYear == date('Y')) { $targetPosts = isset($pluginConfig->targetPosts) ? intval($pluginConfig->targetPosts) : 0; $targetComments = isset($pluginConfig->targetComments) ? intval($pluginConfig->targetComments) : 0; $targetImages = isset($pluginConfig->targetImages) ? intval($pluginConfig->targetImages) : 0; $targetWords = isset($pluginConfig->targetWords) ? intval($pluginConfig->targetWords) : 0; // 如果是当前年份且有设置目标,保存到数据库 if ($targetPosts > 0 || $targetComments > 0 || $targetImages > 0 || $targetWords > 0) { $targetsData = serialize([ 'posts' => $targetPosts, 'comments' => $targetComments, 'images' => $targetImages, 'words' => $targetWords ]); // 检查是否已存在 $checkQuery = $db->select('value')->from('table.options')->where('name = ?', 'yearly_target_' . $currentYear); $checkRow = $db->fetchRow($checkQuery); if ($checkRow) { // 更新 $db->query($db->update('table.options')->rows(['value' => $targetsData])->where('name = ?', 'yearly_target_' . $currentYear)); } else { // 插入 $db->query($db->insert('table.options')->rows([ 'name' => 'yearly_target_' . $currentYear, 'user' => 0, 'value' => $targetsData ])); } } } else { $targetPosts = 0; $targetComments = 0; $targetImages = 0; $targetWords = 0; } } } catch (Exception $e) { $topLimit = 10; $includeDraft = '0'; $chartColor = '#667eea'; $targetPosts = 0; $targetComments = 0; $targetImages = 0; $targetWords = 0; } $currentYear = isset($_GET['year']) ? intval($_GET['year']) : intval(date('Y')); // 获取可用年份 $yearsQuery = $db->select('DISTINCT FROM_UNIXTIME(created, "%Y") as year') ->from('table.contents') ->where('type = ?', 'post') ->order('year', \Typecho\Db::SORT_DESC); $yearsRows = $db->fetchAll($yearsQuery); $availableYears = []; foreach ($yearsRows as $row) { if (!empty($row['year'])) { $availableYears[] = $row['year']; } } if (empty($availableYears)) { $availableYears = [date('Y')]; } // 时间范围 $startTime = mktime(0, 0, 0, 1, 1, $currentYear); $endTime = mktime(23, 59, 59, 12, 31, $currentYear); $statusCondition = ($includeDraft === '1') ? "status IN ('publish', 'draft')" : "status = 'publish'"; // 文章总数 $totalPostsQuery = $db->select('COUNT(*) as total') ->from('table.contents') ->where('type = ?', 'post') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where($statusCondition); $totalPosts = intval($db->fetchRow($totalPostsQuery)['total']); // 评论总数 $commentsQuery = $db->select('COUNT(*) as total') ->from('table.comments') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where('status = ?', 'approved'); $totalComments = intval($db->fetchRow($commentsQuery)['total']); // 图片总数(新增) $imagesQuery = $db->select('COUNT(*) as total') ->from('table.contents') ->where('type = ?', 'attachment') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where('text LIKE ?', '%.jpg%'); $imagesResult = $db->fetchRow($imagesQuery); $totalImages = intval($imagesResult['total']); // 总字数(同时获取 cid, title, slug 供后续最长/最短文章使用) $textsQuery = $db->select('cid, title, slug, text') ->from('table.contents') ->where('type = ?', 'post') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where($statusCondition); $textsRows = $db->fetchAll($textsQuery); $totalWords = 0; foreach ($textsRows as $row) { $text = strip_tags($row['text']); $text = preg_replace('/\s+/', '', $text); $totalWords += mb_strlen($text, 'UTF-8'); } $averageWords = $totalPosts > 0 ? intval($totalWords / $totalPosts) : 0; $averageComments = $totalPosts > 0 ? round($totalComments / $totalPosts, 2) : 0; // 计算目标完成百分比 function calculatePercentage($current, $target) { if ($target <= 0) return 0; return min(100, round(($current / $target) * 100, 1)); } $postsPercentage = calculatePercentage($totalPosts, $targetPosts); $commentsPercentage = calculatePercentage($totalComments, $targetComments); $imagesPercentage = calculatePercentage($totalImages, $targetImages); $wordsPercentage = calculatePercentage($totalWords, $targetWords); // 浏览量 $totalViews = 0; try { $viewsQuery = $db->select('SUM(views) as total') ->from('table.contents') ->where('type = ?', 'post') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where($statusCondition); $viewsResult = $db->fetchRow($viewsQuery); $totalViews = intval($viewsResult['total']); } catch (Exception $e) {} // 按月统计文章 $monthlyQuery = $db->select('FROM_UNIXTIME(created, "%m") as month, COUNT(*) as count') ->from('table.contents') ->where('type = ?', 'post') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where($statusCondition) ->group('month') ->order('month', \Typecho\Db::SORT_ASC); $monthlyRows = $db->fetchAll($monthlyQuery); $monthlyData = []; for ($i = 1; $i <= 12; $i++) { $monthlyData[str_pad($i, 2, '0', STR_PAD_LEFT)] = 0; } foreach ($monthlyRows as $row) { $monthlyData[$row['month']] = intval($row['count']); } // 按月统计图片(新增) $monthlyImagesQuery = $db->select('FROM_UNIXTIME(created, "%m") as month, COUNT(*) as count') ->from('table.contents') ->where('type = ?', 'attachment') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where('text LIKE ?', '%.jpg%') ->group('month') ->order('month', \Typecho\Db::SORT_ASC); $monthlyImagesRows = $db->fetchAll($monthlyImagesQuery); $monthlyImagesData = []; for ($i = 1; $i <= 12; $i++) { $monthlyImagesData[str_pad($i, 2, '0', STR_PAD_LEFT)] = 0; } foreach ($monthlyImagesRows as $row) { $monthlyImagesData[$row['month']] = intval($row['count']); } // 按时段统计 $hourlyQuery = $db->select('FROM_UNIXTIME(created, "%H") as hour, COUNT(*) as count') ->from('table.contents') ->where('type = ?', 'post') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where($statusCondition) ->group('hour'); $hourlyRows = $db->fetchAll($hourlyQuery); $hourlyData = ['凌晨 (0-6点)' => 0, '上午 (6-12点)' => 0, '下午 (12-18点)' => 0, '晚上 (18-24点)' => 0]; foreach ($hourlyRows as $row) { $hour = intval($row['hour']); $count = intval($row['count']); if ($hour < 6) $hourlyData['凌晨 (0-6点)'] += $count; elseif ($hour < 12) $hourlyData['上午 (6-12点)'] += $count; elseif ($hour < 18) $hourlyData['下午 (12-18点)'] += $count; else $hourlyData['晚上 (18-24点)'] += $count; } // 评论排行 $commentRankQuery = $db->select('cid, title, commentsNum') ->from('table.contents') ->where('type = ?', 'post') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where($statusCondition) ->order('commentsNum', \Typecho\Db::SORT_DESC) ->limit($topLimit); $topByComments = $db->fetchAll($commentRankQuery); // 浏览排行 $topByViews = []; try { $viewRankQuery = $db->select('cid, title, views') ->from('table.contents') ->where('type = ?', 'post') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where($statusCondition) ->order('views', \Typecho\Db::SORT_DESC) ->limit($topLimit); $topByViews = $db->fetchAll($viewRankQuery); } catch (Exception $e) {} // 活跃评论者(同时获取评论者网址)- 修改这里:添加排除管理员的条件 $commenterQuery = $db->select('author, mail, url, COUNT(*) as count') ->from('table.comments') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where('status = ?', 'approved') ->where('authorId != ?', $user->uid) // 排除当前登录的管理员 ->group('mail') ->order('count', \Typecho\Db::SORT_DESC) ->limit($topLimit); $topCommenters = $db->fetchAll($commenterQuery); // 获取最近四年的数据(按年份升序排列,从左到右:最老 -> 最新) $fourYearData = []; $yearsToShow = []; for ($i = 3; $i >= 0; $i--) { $year = $currentYear - $i; if (in_array($year, $availableYears) || $i == 0) { $yearsToShow[] = $year; $yearStart = mktime(0, 0, 0, 1, 1, $year); $yearEnd = mktime(23, 59, 59, 12, 31, $year); // 文章数 $postsQuery = $db->select('COUNT(*) as total')->from('table.contents') ->where('type = ?', 'post')->where('created >= ?', $yearStart) ->where('created <= ?', $yearEnd)->where($statusCondition); $posts = intval($db->fetchRow($postsQuery)['total']); // 评论数 $commsQuery = $db->select('COUNT(*) as total')->from('table.comments') ->where('created >= ?', $yearStart)->where('created <= ?', $yearEnd) ->where('status = ?', 'approved'); $comments = intval($db->fetchRow($commsQuery)['total']); // 图片附件数(新增) $imagesQuery = $db->select('COUNT(*) as total')->from('table.contents') ->where('type = ?', 'attachment') ->where('created >= ?', $yearStart) ->where('created <= ?', $yearEnd) ->where('text LIKE ?', '%.jpg%'); $imagesResult = $db->fetchRow($imagesQuery); $images = intval($imagesResult['total']); // 总字数 $wordsQuery = $db->select('text')->from('table.contents') ->where('type = ?', 'post')->where('created >= ?', $yearStart) ->where('created <= ?', $yearEnd)->where($statusCondition); $wordsRows = $db->fetchAll($wordsQuery); $words = 0; foreach ($wordsRows as $row) { $text = strip_tags($row['text']); $text = preg_replace('/\s+/', '', $text); $words += mb_strlen($text, 'UTF-8'); } $fourYearData[$year] = [ 'posts' => $posts, 'comments' => $comments, 'images' => $images, // 新增图片数 'words' => $words ]; } } // 如果不够4年,补全显示最近4年 if (count($yearsToShow) < 4) { $yearsToShow = []; for ($i = 3; $i >= 0; $i--) { $year = $currentYear - $i; if (!in_array($year, $yearsToShow)) { $yearsToShow[] = $year; } } } // 确保数组包含所有年份的数据 foreach ($yearsToShow as $year) { if (!isset($fourYearData[$year])) { $fourYearData[$year] = [ 'posts' => 0, 'comments' => 0, 'images' => 0, // 新增图片数 'words' => 0 ]; } } // 排序确保显示顺序正确 sort($yearsToShow); // 按周统计 $weeklyQuery = $db->select('WEEK(FROM_UNIXTIME(created), 1) as week, COUNT(*) as count') ->from('table.contents') ->where('type = ?', 'post') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where($statusCondition) ->group('week') ->order('week', \Typecho\Db::SORT_ASC); $weeklyRows = $db->fetchAll($weeklyQuery); $weeklyData = []; foreach ($weeklyRows as $row) { $weeklyData['第' . intval($row['week']) . '周'] = intval($row['count']); } // 最长文章 $longestPost = null; $maxWords = 0; foreach ($textsRows as $row) { $text = strip_tags($row['text']); $text = preg_replace('/\s+/', '', $text); $words = mb_strlen($text, 'UTF-8'); if ($words > $maxWords) { $maxWords = $words; $longestPost = [ 'cid' => $row['cid'], 'title' => $row['title'], 'slug' => $row['slug'], 'words' => $words ]; } } // 最短文章 $shortestPost = null; $minWords = PHP_INT_MAX; foreach ($textsRows as $row) { $text = strip_tags($row['text']); $text = preg_replace('/\s+/', '', $text); $words = mb_strlen($text, 'UTF-8'); if ($words > 0 && $words < $minWords) { $minWords = $words; $shortestPost = [ 'cid' => $row['cid'], 'title' => $row['title'], 'slug' => $row['slug'], 'words' => $words ]; } } // 先获取符合条件的文章CID列表 $cidsQuery = $db->select('cid')->from('table.contents') ->where('type = ?', 'post') ->where('created >= ?', $startTime) ->where('created <= ?', $endTime) ->where($statusCondition); $cidsRows = $db->fetchAll($cidsQuery); $cids = array_column($cidsRows, 'cid'); if (empty($cids)) { $cids = [0]; } // 获取标签分布(用两步查询避免JOIN问题) // 第一步:获取本年度文章的所有标签mid $midQuery = $db->select(' DISTINCT mid')->from('table.relationships') ->where('cid IN ?', $cids); $midRows = $db->fetchAll($midQuery); $mids = array_column($midRows, 'mid'); if (empty($mids)) { $tagDistribution = []; } else { // 第二步:获取每个标签的文章数,只保留type=tag的 $tagDistribution = []; foreach ($mids as $mid) { $count = $db->fetchRow($db->select('COUNT(*) as count')->from('table.relationships') ->where('mid = ?', $mid)->where('cid IN ?', $cids)); $meta = $db->fetchRow($db->select('name', 'slug')->from('table.metas')->where('mid = ?', $mid)->where('type = ?', 'tag')); if ($meta && $count['count'] > 0) { $tagDistribution[] = [ 'name' => $meta['name'], 'slug' => $meta['slug'], 'count' => intval($count['count']) ]; } } // 按数量排序 usort($tagDistribution, function($a, $b) { return $b['count'] - $a['count']; }); $tagDistribution = array_slice($tagDistribution, 0, $topLimit); } // 分类分布(用两步查询避免JOIN问题) // 第一步:获取本年度文章的所有分类mid $catMidQuery = $db->select(' DISTINCT mid')->from('table.relationships') ->where('cid IN ?', $cids); $catMidRows = $db->fetchAll($catMidQuery); $catMids = array_column($catMidRows, 'mid'); if (empty($catMids)) { $categoryDistribution = []; } else { // 第二步:获取每个分类的文章数,只保留type=category的 $categoryDistribution = []; foreach ($catMids as $mid) { $count = $db->fetchRow($db->select('COUNT(*) as count')->from('table.relationships') ->where('mid = ?', $mid)->where('cid IN ?', $cids)); $meta = $db->fetchRow($db->select('name', 'slug')->from('table.metas')->where('mid = ?', $mid)->where('type = ?', 'category')); if ($meta && $count['count'] > 0) { $categoryDistribution[] = [ 'name' => $meta['name'], 'slug' => $meta['slug'], 'count' => intval($count['count']) ]; } } // 按数量排序 usort($categoryDistribution, function($a, $b) { return $b['count'] - $a['count']; }); } ?>