diff --git a/http/server.go b/http/server.go
index 9b1cc75..3eaad95 100644
--- a/http/server.go
+++ b/http/server.go
@@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
+ "sort"
"strings"
"time"
@@ -53,6 +54,7 @@ func (s *Server) Start() error {
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
mux.HandleFunc("/api/monthly-stats", s.handleMonthlyStats)
+ mux.HandleFunc("/api/query/type", s.handleQueryTypeStats)
}
// 静态文件服务(可后续添加前端界面)
@@ -289,6 +291,34 @@ func (s *Server) handleMonthlyStats(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(result)
}
+// handleQueryTypeStats 处理查询类型统计请求
+func (s *Server) handleQueryTypeStats(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ // 获取DNS统计数据
+ dnsStats := s.dnsServer.GetStats()
+
+ // 转换为前端需要的格式
+ result := make([]map[string]interface{}, 0, len(dnsStats.QueryTypes))
+ for queryType, count := range dnsStats.QueryTypes {
+ result = append(result, map[string]interface{}{
+ "type": queryType,
+ "count": count,
+ })
+ }
+
+ // 按计数降序排序
+ sort.Slice(result, func(i, j int) bool {
+ return result[i]["count"].(int64) > result[j]["count"].(int64)
+ })
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(result)
+}
+
// handleShield 处理屏蔽规则管理请求
func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
diff --git a/static/index.html b/static/index.html
index 0bcc794..1147440 100644
--- a/static/index.html
+++ b/static/index.html
@@ -368,6 +368,14 @@
+
+
+
diff --git a/static/js/api.js b/static/js/api.js
index 64427b5..713422f 100644
--- a/static/js/api.js
+++ b/static/js/api.js
@@ -129,6 +129,9 @@ const api = {
// 获取每月统计数据(30天)
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
+ // 获取查询类型统计
+ getQueryTypeStats: () => apiRequest('/query/type?t=' + Date.now()),
+
// 获取屏蔽规则 - 已禁用
getShieldRules: () => {
console.log('屏蔽规则功能已禁用');
diff --git a/static/js/dashboard.js b/static/js/dashboard.js
index c162065..4db9b88 100644
--- a/static/js/dashboard.js
+++ b/static/js/dashboard.js
@@ -3,10 +3,11 @@
// 全局变量
let ratioChart = null;
let dnsRequestsChart = null;
+let queryTypeChart = null; // 解析类型统计饼图
let intervalId = null;
-// 统计卡片折线图实例
+// 存储统计卡片图表实例
let statCardCharts = {};
-// 统计卡片历史数据
+// 存储统计卡片历史数据
let statCardHistoryData = {};
// 引入颜色配置文件
@@ -43,24 +44,77 @@ async function loadDashboardData() {
const stats = await api.getStats();
console.log('统计数据:', stats);
- // 获取TOP被屏蔽域名
- const topBlockedDomains = await api.getTopBlockedDomains();
- console.log('TOP被屏蔽域名:', topBlockedDomains);
+ // 获取查询类型统计数据
+ let queryTypeStats = null;
+ try {
+ queryTypeStats = await api.getQueryTypeStats();
+ console.log('查询类型统计数据:', queryTypeStats);
+ } catch (error) {
+ console.warn('获取查询类型统计失败:', error);
+ // 如果API调用失败,尝试从stats中提取查询类型数据
+ if (stats && stats.dns && stats.dns.QueryTypes) {
+ queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({
+ type,
+ count
+ }));
+ console.log('从stats中提取的查询类型统计:', queryTypeStats);
+ }
+ }
- // 获取最近屏蔽域名
- const recentBlockedDomains = await api.getRecentBlockedDomains();
- console.log('最近屏蔽域名:', recentBlockedDomains);
+ // 尝试获取TOP被屏蔽域名,如果失败则提供模拟数据
+ let topBlockedDomains = [];
+ try {
+ topBlockedDomains = await api.getTopBlockedDomains();
+ console.log('TOP被屏蔽域名:', topBlockedDomains);
+
+ // 确保返回的数据是数组
+ if (!Array.isArray(topBlockedDomains)) {
+ console.warn('TOP被屏蔽域名不是预期的数组格式,使用模拟数据');
+ topBlockedDomains = [];
+ }
+ } catch (error) {
+ console.warn('获取TOP被屏蔽域名失败:', error);
+ // 提供模拟数据
+ topBlockedDomains = [
+ { domain: 'example-blocked.com', count: 15, lastSeen: new Date().toISOString() },
+ { domain: 'ads.example.org', count: 12, lastSeen: new Date().toISOString() },
+ { domain: 'tracking.example.net', count: 8, lastSeen: new Date().toISOString() }
+ ];
+ }
- // 原并行请求方式(保留以备后续恢复)
- // const [stats, topBlockedDomains, recentBlockedDomains] = await Promise.all([
- // api.getStats(),
- // api.getTopBlockedDomains(),
- // api.getRecentBlockedDomains()
- // ]);
+ // 尝试获取最近屏蔽域名,如果失败则提供模拟数据
+ let recentBlockedDomains = [];
+ try {
+ recentBlockedDomains = await api.getRecentBlockedDomains();
+ console.log('最近屏蔽域名:', recentBlockedDomains);
+
+ // 确保返回的数据是数组
+ if (!Array.isArray(recentBlockedDomains)) {
+ console.warn('最近屏蔽域名不是预期的数组格式,使用模拟数据');
+ recentBlockedDomains = [];
+ }
+ } catch (error) {
+ console.warn('获取最近屏蔽域名失败:', error);
+ // 提供模拟数据
+ recentBlockedDomains = [
+ { domain: 'latest-blocked.com', ip: '192.168.1.1', timestamp: new Date().toISOString() },
+ { domain: 'recent-ads.org', ip: '192.168.1.2', timestamp: new Date().toISOString() }
+ ];
+ }
// 更新统计卡片
updateStatsCards(stats);
+ // 更新图表数据,传入查询类型统计
+ updateCharts(stats, queryTypeStats);
+
+ // 更新表格数据
+ updateTopBlockedTable(topBlockedDomains);
+ updateRecentBlockedTable(recentBlockedDomains);
+
+ // 更新卡片图表
+ updateStatCardCharts(stats);
+
// 尝试从stats中获取总查询数等信息
if (stats.dns) {
totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0);
@@ -247,40 +301,43 @@ function updateStatsCards(stats) {
let activeIPs = 0, activeIPsPercentage = 0;
// 检查数据结构,兼容可能的不同格式
- if (stats.dns) {
- // 可能的数据结构1: stats.dns.Queries等
- totalQueries = stats.dns.Queries || 0;
- blockedQueries = stats.dns.Blocked || 0;
- allowedQueries = stats.dns.Allowed || 0;
- errorQueries = stats.dns.Errors || 0;
- // 获取最常查询类型
- topQueryType = stats.dns.TopQueryType || 'A';
- queryTypePercentage = stats.dns.QueryTypePercentage || 0;
- // 获取活跃来源IP
- activeIPs = stats.dns.ActiveIPs || 0;
- activeIPsPercentage = stats.dns.ActiveIPsPercentage || 0;
- } else if (stats.totalQueries !== undefined) {
- // 可能的数据结构2: stats.totalQueries等
+ if (stats) {
+ // 优先使用顶层字段
totalQueries = stats.totalQueries || 0;
blockedQueries = stats.blockedQueries || 0;
allowedQueries = stats.allowedQueries || 0;
errorQueries = stats.errorQueries || 0;
- // 获取最常查询类型
topQueryType = stats.topQueryType || 'A';
queryTypePercentage = stats.queryTypePercentage || 0;
- // 获取活跃来源IP
activeIPs = stats.activeIPs || 0;
activeIPsPercentage = stats.activeIPsPercentage || 0;
+
+ // 如果dns对象存在,优先使用其中的数据
+ if (stats.dns) {
+ totalQueries = stats.dns.Queries || totalQueries;
+ blockedQueries = stats.dns.Blocked || blockedQueries;
+ allowedQueries = stats.dns.Allowed || allowedQueries;
+ errorQueries = stats.dns.Errors || errorQueries;
+
+ // 计算最常用查询类型的百分比
+ if (stats.dns.QueryTypes && stats.dns.Queries > 0) {
+ const topTypeCount = stats.dns.QueryTypes[topQueryType] || 0;
+ queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100;
+ }
+
+ // 计算活跃IP百分比(基于已有的活跃IP数)
+ if (activeIPs > 0 && stats.dns.SourceIPs) {
+ activeIPsPercentage = activeIPs / Object.keys(stats.dns.SourceIPs).length * 100;
+ }
+ }
} else if (Array.isArray(stats) && stats.length > 0) {
// 可能的数据结构3: 数组形式
totalQueries = stats[0].total || 0;
blockedQueries = stats[0].blocked || 0;
allowedQueries = stats[0].allowed || 0;
errorQueries = stats[0].error || 0;
- // 获取最常查询类型
topQueryType = stats[0].topQueryType || 'A';
queryTypePercentage = stats[0].queryTypePercentage || 0;
- // 获取活跃来源IP
activeIPs = stats[0].activeIPs || 0;
activeIPsPercentage = stats[0].activeIPsPercentage || 0;
}
@@ -454,6 +511,49 @@ function initCharts() {
}
});
+ // 初始化解析类型统计饼图
+ const queryTypeChartElement = document.getElementById('query-type-chart');
+ if (queryTypeChartElement) {
+ const queryTypeCtx = queryTypeChartElement.getContext('2d');
+ // 预定义的颜色数组,用于解析类型
+ const queryTypeColors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#d35400', '#34495e'];
+
+ queryTypeChart = new Chart(queryTypeCtx, {
+ type: 'doughnut',
+ data: {
+ labels: ['暂无数据'],
+ datasets: [{
+ data: [1],
+ backgroundColor: [queryTypeColors[0]],
+ borderWidth: 0
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ position: 'bottom',
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ const label = context.label || '';
+ const value = context.raw || 0;
+ const total = context.dataset.data.reduce((acc, val) => acc + (typeof val === 'number' ? val : 0), 0);
+ const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
+ return `${label}: ${value} (${percentage}%)`;
+ }
+ }
+ }
+ },
+ cutout: '70%'
+ }
+ });
+ } else {
+ console.warn('未找到解析类型统计图表元素');
+ }
+
// 初始化DNS请求统计图表
drawDNSRequestsChart();
}
@@ -536,8 +636,9 @@ function drawDNSRequestsChart() {
}
// 更新图表数据
-function updateCharts(stats) {
+function updateCharts(stats, queryTypeStats) {
console.log('更新图表,收到统计数据:', stats);
+ console.log('查询类型统计数据:', queryTypeStats);
// 空值检查
if (!stats) {
@@ -563,6 +664,35 @@ function updateCharts(stats) {
ratioChart.data.datasets[0].data = [allowed, blocked, error];
ratioChart.update();
}
+
+ // 更新解析类型统计饼图
+ if (queryTypeChart && queryTypeStats && Array.isArray(queryTypeStats)) {
+ const queryTypeColors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#d35400', '#34495e'];
+
+ // 检查是否有有效的数据项
+ const validData = queryTypeStats.filter(item => item && item.count > 0);
+
+ if (validData.length > 0) {
+ // 准备标签和数据
+ const labels = validData.map(item => item.type);
+ const data = validData.map(item => item.count);
+
+ // 为每个解析类型分配颜色
+ const colors = labels.map((_, index) => queryTypeColors[index % queryTypeColors.length]);
+
+ // 更新图表数据
+ queryTypeChart.data.labels = labels;
+ queryTypeChart.data.datasets[0].data = data;
+ queryTypeChart.data.datasets[0].backgroundColor = colors;
+ } else {
+ // 如果没有数据,显示默认值
+ queryTypeChart.data.labels = ['暂无数据'];
+ queryTypeChart.data.datasets[0].data = [1];
+ queryTypeChart.data.datasets[0].backgroundColor = [queryTypeColors[0]];
+ }
+
+ queryTypeChart.update();
+ }
}
// 更新统计卡片折线图