web增加恢复解析统计图表
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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('屏蔽规则功能已禁用');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新图表数据
|
// 更新图表数据
|
||||||
|
|||||||
Reference in New Issue
Block a user