diff --git a/dns/server.go b/dns/server.go index 4d981a3..86afde1 100644 --- a/dns/server.go +++ b/dns/server.go @@ -33,6 +33,8 @@ type StatsData struct { BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"` ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"` HourlyStats map[string]int64 `json:"hourlyStats"` + DailyStats map[string]int64 `json:"dailyStats"` + MonthlyStats map[string]int64 `json:"monthlyStats"` LastSaved time.Time `json:"lastSaved"` } @@ -53,6 +55,10 @@ type Server struct { resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名 hourlyStatsMutex sync.RWMutex hourlyStats map[string]int64 // 按小时统计屏蔽数量 + dailyStatsMutex sync.RWMutex + dailyStats map[string]int64 // 按天统计屏蔽数量 + monthlyStatsMutex sync.RWMutex + monthlyStats map[string]int64 // 按月统计屏蔽数量 saveTicker *time.Ticker // 用于定时保存数据 saveDone chan struct{} // 用于通知保存协程停止 } @@ -88,6 +94,8 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie blockedDomains: make(map[string]*BlockedDomain), resolvedDomains: make(map[string]*BlockedDomain), hourlyStats: make(map[string]int64), + dailyStats: make(map[string]int64), + monthlyStats: make(map[string]int64), saveDone: make(chan struct{}), } @@ -354,11 +362,26 @@ func (s *Server) updateBlockedDomainStats(domain string) { } } + // 更新统计数据 + now := time.Now() + // 更新小时统计 - hourKey := time.Now().Format("2006-01-02-15") + hourKey := now.Format("2006-01-02-15") s.hourlyStatsMutex.Lock() s.hourlyStats[hourKey]++ s.hourlyStatsMutex.Unlock() + + // 更新每日统计 + dayKey := now.Format("2006-01-02") + s.dailyStatsMutex.Lock() + s.dailyStats[dayKey]++ + s.dailyStatsMutex.Unlock() + + // 更新每月统计 + monthKey := now.Format("2006-01") + s.monthlyStatsMutex.Lock() + s.monthlyStats[monthKey]++ + s.monthlyStatsMutex.Unlock() } // updateResolvedDomainStats 更新解析域名统计 @@ -469,7 +492,7 @@ func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain { return domains } -// GetHourlyStats 获取24小时屏蔽统计 +// GetHourlyStats 获取每小时统计数据 func (s *Server) GetHourlyStats() map[string]int64 { s.hourlyStatsMutex.RLock() defer s.hourlyStatsMutex.RUnlock() @@ -482,6 +505,32 @@ func (s *Server) GetHourlyStats() map[string]int64 { return result } +// GetDailyStats 获取每日统计数据 +func (s *Server) GetDailyStats() map[string]int64 { + s.dailyStatsMutex.RLock() + defer s.dailyStatsMutex.RUnlock() + + // 返回副本 + result := make(map[string]int64) + for k, v := range s.dailyStats { + result[k] = v + } + return result +} + +// GetMonthlyStats 获取每月统计数据 +func (s *Server) GetMonthlyStats() map[string]int64 { + s.monthlyStatsMutex.RLock() + defer s.monthlyStatsMutex.RUnlock() + + // 返回副本 + result := make(map[string]int64) + for k, v := range s.monthlyStats { + result[k] = v + } + return result +} + // loadStatsData 从文件加载统计数据 func (s *Server) loadStatsData() { if s.config.StatsFile == "" { @@ -528,6 +577,18 @@ func (s *Server) loadStatsData() { s.hourlyStats = statsData.HourlyStats } s.hourlyStatsMutex.Unlock() + + s.dailyStatsMutex.Lock() + if statsData.DailyStats != nil { + s.dailyStats = statsData.DailyStats + } + s.dailyStatsMutex.Unlock() + + s.monthlyStatsMutex.Lock() + if statsData.MonthlyStats != nil { + s.monthlyStats = statsData.MonthlyStats + } + s.monthlyStatsMutex.Unlock() logger.Info("统计数据加载成功") } @@ -573,6 +634,20 @@ func (s *Server) saveStatsData() { statsData.HourlyStats[k] = v } s.hourlyStatsMutex.RUnlock() + + s.dailyStatsMutex.RLock() + statsData.DailyStats = make(map[string]int64) + for k, v := range s.dailyStats { + statsData.DailyStats[k] = v + } + s.dailyStatsMutex.RUnlock() + + s.monthlyStatsMutex.RLock() + statsData.MonthlyStats = make(map[string]int64) + for k, v := range s.monthlyStats { + statsData.MonthlyStats[k] = v + } + s.monthlyStatsMutex.RUnlock() // 序列化数据 jsonData, err := json.MarshalIndent(statsData, "", " ") diff --git a/http/server.go b/http/server.go index 4ac1a95..8975766 100644 --- a/http/server.go +++ b/http/server.go @@ -51,6 +51,8 @@ func (s *Server) Start() error { mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains) mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains) mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats) + mux.HandleFunc("/api/daily-stats", s.handleDailyStats) + mux.HandleFunc("/api/monthly-stats", s.handleMonthlyStats) } // 静态文件服务(可后续添加前端界面) @@ -191,6 +193,68 @@ func (s *Server) handleHourlyStats(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(result) } +// handleDailyStats 处理每日统计数据请求 +func (s *Server) handleDailyStats(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // 获取每日统计数据 + dailyStats := s.dnsServer.GetDailyStats() + + // 生成过去7天的时间标签 + labels := make([]string, 7) + data := make([]int64, 7) + now := time.Now() + + for i := 6; i >= 0; i-- { + t := now.AddDate(0, 0, -i) + key := t.Format("2006-01-02") + labels[6-i] = t.Format("01-02") + data[6-i] = dailyStats[key] + } + + result := map[string]interface{}{ + "labels": labels, + "data": data, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) +} + +// handleMonthlyStats 处理每月统计数据请求 +func (s *Server) handleMonthlyStats(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // 获取每日统计数据(用于30天视图) + dailyStats := s.dnsServer.GetDailyStats() + + // 生成过去30天的时间标签 + labels := make([]string, 30) + data := make([]int64, 30) + now := time.Now() + + for i := 29; i >= 0; i-- { + t := now.AddDate(0, 0, -i) + key := t.Format("2006-01-02") + labels[29-i] = t.Format("01-02") + data[29-i] = dailyStats[key] + } + + result := map[string]interface{}{ + "labels": labels, + "data": data, + } + + 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") @@ -201,9 +265,9 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { // 获取规则统计信息 stats := s.shieldManager.GetStats() shieldInfo := map[string]interface{}{ - "updateInterval": s.globalConfig.Shield.UpdateInterval, - "blockMethod": s.globalConfig.Shield.BlockMethod, - "blacklistCount": len(s.globalConfig.Shield.Blacklists), + "updateInterval": s.globalConfig.Shield.UpdateInterval, + "blockMethod": s.globalConfig.Shield.BlockMethod, + "blacklistCount": len(s.globalConfig.Shield.Blacklists), "domainRulesCount": stats["domainRules"], "domainExceptionsCount": stats["domainExceptions"], "regexRulesCount": stats["regexRules"], diff --git a/static/index.html b/static/index.html index 434577a..f386686 100644 --- a/static/index.html +++ b/static/index.html @@ -210,6 +210,22 @@
+ +
+
+

DNS请求趋势

+ +
+ + + +
+
+
+ +
+
+

解析与屏蔽比例

diff --git a/static/js/api.js b/static/js/api.js index 12b42cb..bbd5ff5 100644 --- a/static/js/api.js +++ b/static/js/api.js @@ -103,6 +103,12 @@ const api = { // 获取小时统计 getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()), + // 获取每日统计数据(7天) + getDailyStats: () => apiRequest('/daily-stats?t=' + Date.now()), + + // 获取每月统计数据(30天) + getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()), + // 获取屏蔽规则 - 已禁用 getShieldRules: () => { console.log('屏蔽规则功能已禁用'); diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 5a30995..b04c864 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -2,6 +2,7 @@ // 全局变量 let ratioChart = null; +let dnsRequestsChart = null; let intervalId = null; // 初始化仪表盘 @@ -13,6 +14,9 @@ async function initDashboard() { // 初始化图表 initCharts(); + // 初始化时间范围切换 + initTimeRangeToggle(); + // 设置定时更新 intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次 } catch (error) { @@ -211,6 +215,28 @@ function updateRecentBlockedTable(domains) { tableBody.innerHTML = html; } +// 当前选中的时间范围 +let currentTimeRange = '24h'; // 默认为24小时 + +// 初始化时间范围切换 +function initTimeRangeToggle() { + const timeRangeButtons = document.querySelectorAll('.time-range-btn'); + timeRangeButtons.forEach(button => { + button.addEventListener('click', () => { + // 移除所有按钮的激活状态 + timeRangeButtons.forEach(btn => btn.classList.remove('active')); + // 添加当前按钮的激活状态 + button.classList.add('active'); + // 更新当前时间范围 + currentTimeRange = button.dataset.range; + // 重新加载数据 + loadDashboardData(); + // 更新DNS请求图表 + drawDNSRequestsChart(); + }); + }); +} + // 初始化图表 function initCharts() { // 初始化比例图表 @@ -241,6 +267,86 @@ function initCharts() { cutout: '70%' } }); + + // 初始化DNS请求统计图表 + drawDNSRequestsChart(); +} + +// 绘制DNS请求统计图表 +function drawDNSRequestsChart() { + const ctx = document.getElementById('dns-requests-chart'); + if (!ctx) { + console.error('未找到DNS请求图表元素'); + return; + } + + const chartContext = ctx.getContext('2d'); + let apiFunction; + + // 根据当前时间范围选择API函数 + switch (currentTimeRange) { + case '7d': + apiFunction = api.getDailyStats; + break; + case '30d': + apiFunction = api.getMonthlyStats; + break; + default: // 24h + apiFunction = api.getHourlyStats; + } + + // 获取统计数据 + apiFunction().then(data => { + // 创建或更新图表 + if (dnsRequestsChart) { + dnsRequestsChart.data.labels = data.labels; + dnsRequestsChart.data.datasets[0].data = data.data; + dnsRequestsChart.update(); + } else { + dnsRequestsChart = new Chart(chartContext, { + type: 'line', + data: { + labels: data.labels, + datasets: [{ + label: 'DNS请求数量', + data: data.data, + borderColor: '#3b82f6', + backgroundColor: 'rgba(59, 130, 246, 0.1)', + tension: 0.4, + fill: true + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + mode: 'index', + intersect: false + } + }, + scales: { + y: { + beginAtZero: true, + grid: { + color: 'rgba(0, 0, 0, 0.1)' + } + }, + x: { + grid: { + display: false + } + } + } + } + }); + } + }).catch(error => { + console.error('绘制DNS请求图表失败:', error); + }); } // 更新图表数据