web增加恢复解析统计图表
This commit is contained in:
@@ -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, "", " ")
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -210,6 +210,22 @@
|
||||
|
||||
<!-- 图表和数据表格 -->
|
||||
<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">
|
||||
<h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3>
|
||||
|
||||
@@ -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('屏蔽规则功能已禁用');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
|
||||
Reference in New Issue
Block a user