From 3494ce88a1e2d71708d4d220090dda6a883973d4 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Wed, 26 Nov 2025 00:06:14 +0800 Subject: [PATCH] =?UTF-8?q?web=E6=B7=BB=E5=8A=A0=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- http/server.go | 30 +++++++ static/index.html | 8 ++ static/js/api.js | 3 + static/js/dashboard.js | 196 ++++++++++++++++++++++++++++++++++------- 4 files changed, 204 insertions(+), 33 deletions(-) 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(); + } } // 更新统计卡片折线图