web增加恢复解析统计图表

This commit is contained in:
Alex Yang
2025-11-25 15:35:53 +08:00
parent e86c3db45f
commit 2fd2c65d64
5 changed files with 272 additions and 5 deletions

View File

@@ -33,6 +33,8 @@ type StatsData struct {
BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"` BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"`
ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"` ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"`
HourlyStats map[string]int64 `json:"hourlyStats"` HourlyStats map[string]int64 `json:"hourlyStats"`
DailyStats map[string]int64 `json:"dailyStats"`
MonthlyStats map[string]int64 `json:"monthlyStats"`
LastSaved time.Time `json:"lastSaved"` LastSaved time.Time `json:"lastSaved"`
} }
@@ -53,6 +55,10 @@ type Server struct {
resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名 resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名
hourlyStatsMutex sync.RWMutex hourlyStatsMutex sync.RWMutex
hourlyStats map[string]int64 // 按小时统计屏蔽数量 hourlyStats map[string]int64 // 按小时统计屏蔽数量
dailyStatsMutex sync.RWMutex
dailyStats map[string]int64 // 按天统计屏蔽数量
monthlyStatsMutex sync.RWMutex
monthlyStats map[string]int64 // 按月统计屏蔽数量
saveTicker *time.Ticker // 用于定时保存数据 saveTicker *time.Ticker // 用于定时保存数据
saveDone chan struct{} // 用于通知保存协程停止 saveDone chan struct{} // 用于通知保存协程停止
} }
@@ -88,6 +94,8 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
blockedDomains: make(map[string]*BlockedDomain), blockedDomains: make(map[string]*BlockedDomain),
resolvedDomains: make(map[string]*BlockedDomain), resolvedDomains: make(map[string]*BlockedDomain),
hourlyStats: make(map[string]int64), hourlyStats: make(map[string]int64),
dailyStats: make(map[string]int64),
monthlyStats: make(map[string]int64),
saveDone: make(chan struct{}), 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.hourlyStatsMutex.Lock()
s.hourlyStats[hourKey]++ s.hourlyStats[hourKey]++
s.hourlyStatsMutex.Unlock() 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 更新解析域名统计 // updateResolvedDomainStats 更新解析域名统计
@@ -469,7 +492,7 @@ func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain {
return domains return domains
} }
// GetHourlyStats 获取24小时屏蔽统计 // GetHourlyStats 获取每小时统计数据
func (s *Server) GetHourlyStats() map[string]int64 { func (s *Server) GetHourlyStats() map[string]int64 {
s.hourlyStatsMutex.RLock() s.hourlyStatsMutex.RLock()
defer s.hourlyStatsMutex.RUnlock() defer s.hourlyStatsMutex.RUnlock()
@@ -482,6 +505,32 @@ func (s *Server) GetHourlyStats() map[string]int64 {
return result 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 从文件加载统计数据 // loadStatsData 从文件加载统计数据
func (s *Server) loadStatsData() { func (s *Server) loadStatsData() {
if s.config.StatsFile == "" { if s.config.StatsFile == "" {
@@ -529,6 +578,18 @@ func (s *Server) loadStatsData() {
} }
s.hourlyStatsMutex.Unlock() 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("统计数据加载成功") logger.Info("统计数据加载成功")
} }
@@ -574,6 +635,20 @@ func (s *Server) saveStatsData() {
} }
s.hourlyStatsMutex.RUnlock() 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, "", " ") jsonData, err := json.MarshalIndent(statsData, "", " ")
if err != nil { if err != nil {

View File

@@ -51,6 +51,8 @@ func (s *Server) Start() error {
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains) mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains) mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains)
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats) 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) 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 处理屏蔽规则管理请求 // handleShield 处理屏蔽规则管理请求
func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") 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() stats := s.shieldManager.GetStats()
shieldInfo := map[string]interface{}{ shieldInfo := map[string]interface{}{
"updateInterval": s.globalConfig.Shield.UpdateInterval, "updateInterval": s.globalConfig.Shield.UpdateInterval,
"blockMethod": s.globalConfig.Shield.BlockMethod, "blockMethod": s.globalConfig.Shield.BlockMethod,
"blacklistCount": len(s.globalConfig.Shield.Blacklists), "blacklistCount": len(s.globalConfig.Shield.Blacklists),
"domainRulesCount": stats["domainRules"], "domainRulesCount": stats["domainRules"],
"domainExceptionsCount": stats["domainExceptions"], "domainExceptionsCount": stats["domainExceptions"],
"regexRulesCount": stats["regexRules"], "regexRulesCount": stats["regexRules"],

View File

@@ -210,6 +210,22 @@
<!-- 图表和数据表格 --> <!-- 图表和数据表格 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- DNS请求趋势图表 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">DNS请求趋势</h3>
<!-- 时间范围切换按钮 -->
<div class="flex space-x-2">
<button class="time-range-btn px-4 py-2 rounded-md bg-primary text-white" data-range="24h">24小时</button>
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="7d">7天</button>
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="30d">30天</button>
</div>
</div>
<div class="h-80">
<canvas id="dns-requests-chart"></canvas>
</div>
</div>
<!-- 解析与屏蔽比例 --> <!-- 解析与屏蔽比例 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3"> <div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3">
<h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3> <h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3>

View File

@@ -103,6 +103,12 @@ const api = {
// 获取小时统计 // 获取小时统计
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()), getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
// 获取每日统计数据7天
getDailyStats: () => apiRequest('/daily-stats?t=' + Date.now()),
// 获取每月统计数据30天
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
// 获取屏蔽规则 - 已禁用 // 获取屏蔽规则 - 已禁用
getShieldRules: () => { getShieldRules: () => {
console.log('屏蔽规则功能已禁用'); console.log('屏蔽规则功能已禁用');

View File

@@ -2,6 +2,7 @@
// 全局变量 // 全局变量
let ratioChart = null; let ratioChart = null;
let dnsRequestsChart = null;
let intervalId = null; let intervalId = null;
// 初始化仪表盘 // 初始化仪表盘
@@ -13,6 +14,9 @@ async function initDashboard() {
// 初始化图表 // 初始化图表
initCharts(); initCharts();
// 初始化时间范围切换
initTimeRangeToggle();
// 设置定时更新 // 设置定时更新
intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次 intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次
} catch (error) { } catch (error) {
@@ -211,6 +215,28 @@ function updateRecentBlockedTable(domains) {
tableBody.innerHTML = html; 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() { function initCharts() {
// 初始化比例图表 // 初始化比例图表
@@ -241,6 +267,86 @@ function initCharts() {
cutout: '70%' 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);
});
} }
// 更新图表数据 // 更新图表数据