web添加解析类型显示
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ func (s *Server) Start() error {
|
|||||||
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
|
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
|
||||||
mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
|
mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
|
||||||
mux.HandleFunc("/api/monthly-stats", s.handleMonthlyStats)
|
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)
|
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 处理屏蔽规则管理请求
|
// 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")
|
||||||
|
|||||||
@@ -368,6 +368,14 @@
|
|||||||
<canvas id="ratio-chart"></canvas>
|
<canvas id="ratio-chart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
<div class="h-80 flex items-center justify-center">
|
||||||
|
<canvas id="query-type-chart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 最近活动表格 -->
|
<!-- 最近活动表格 -->
|
||||||
|
|||||||
@@ -129,6 +129,9 @@ const api = {
|
|||||||
// 获取每月统计数据(30天)
|
// 获取每月统计数据(30天)
|
||||||
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
|
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
|
||||||
|
|
||||||
|
// 获取查询类型统计
|
||||||
|
getQueryTypeStats: () => apiRequest('/query/type?t=' + Date.now()),
|
||||||
|
|
||||||
// 获取屏蔽规则 - 已禁用
|
// 获取屏蔽规则 - 已禁用
|
||||||
getShieldRules: () => {
|
getShieldRules: () => {
|
||||||
console.log('屏蔽规则功能已禁用');
|
console.log('屏蔽规则功能已禁用');
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
// 全局变量
|
// 全局变量
|
||||||
let ratioChart = null;
|
let ratioChart = null;
|
||||||
let dnsRequestsChart = null;
|
let dnsRequestsChart = null;
|
||||||
|
let queryTypeChart = null; // 解析类型统计饼图
|
||||||
let intervalId = null;
|
let intervalId = null;
|
||||||
// 统计卡片折线图实例
|
// 存储统计卡片图表实例
|
||||||
let statCardCharts = {};
|
let statCardCharts = {};
|
||||||
// 统计卡片历史数据
|
// 存储统计卡片历史数据
|
||||||
let statCardHistoryData = {};
|
let statCardHistoryData = {};
|
||||||
|
|
||||||
// 引入颜色配置文件
|
// 引入颜色配置文件
|
||||||
@@ -43,24 +44,77 @@ async function loadDashboardData() {
|
|||||||
const stats = await api.getStats();
|
const stats = await api.getStats();
|
||||||
console.log('统计数据:', stats);
|
console.log('统计数据:', stats);
|
||||||
|
|
||||||
// 获取TOP被屏蔽域名
|
// 获取查询类型统计数据
|
||||||
const topBlockedDomains = await api.getTopBlockedDomains();
|
let queryTypeStats = null;
|
||||||
console.log('TOP被屏蔽域名:', topBlockedDomains);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取最近屏蔽域名
|
// 尝试获取TOP被屏蔽域名,如果失败则提供模拟数据
|
||||||
const recentBlockedDomains = await api.getRecentBlockedDomains();
|
let topBlockedDomains = [];
|
||||||
console.log('最近屏蔽域名:', recentBlockedDomains);
|
try {
|
||||||
|
topBlockedDomains = await api.getTopBlockedDomains();
|
||||||
|
console.log('TOP被屏蔽域名:', topBlockedDomains);
|
||||||
|
|
||||||
// 原并行请求方式(保留以备后续恢复)
|
// 确保返回的数据是数组
|
||||||
// const [stats, topBlockedDomains, recentBlockedDomains] = await Promise.all([
|
if (!Array.isArray(topBlockedDomains)) {
|
||||||
// api.getStats(),
|
console.warn('TOP被屏蔽域名不是预期的数组格式,使用模拟数据');
|
||||||
// api.getTopBlockedDomains(),
|
topBlockedDomains = [];
|
||||||
// api.getRecentBlockedDomains()
|
}
|
||||||
// ]);
|
} 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() }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试获取最近屏蔽域名,如果失败则提供模拟数据
|
||||||
|
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);
|
updateStatsCards(stats);
|
||||||
|
|
||||||
|
// 更新图表数据,传入查询类型统计
|
||||||
|
updateCharts(stats, queryTypeStats);
|
||||||
|
|
||||||
|
// 更新表格数据
|
||||||
|
updateTopBlockedTable(topBlockedDomains);
|
||||||
|
updateRecentBlockedTable(recentBlockedDomains);
|
||||||
|
|
||||||
|
// 更新卡片图表
|
||||||
|
updateStatCardCharts(stats);
|
||||||
|
|
||||||
// 尝试从stats中获取总查询数等信息
|
// 尝试从stats中获取总查询数等信息
|
||||||
if (stats.dns) {
|
if (stats.dns) {
|
||||||
totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0);
|
totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0);
|
||||||
@@ -247,40 +301,43 @@ function updateStatsCards(stats) {
|
|||||||
let activeIPs = 0, activeIPsPercentage = 0;
|
let activeIPs = 0, activeIPsPercentage = 0;
|
||||||
|
|
||||||
// 检查数据结构,兼容可能的不同格式
|
// 检查数据结构,兼容可能的不同格式
|
||||||
if (stats.dns) {
|
if (stats) {
|
||||||
// 可能的数据结构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等
|
|
||||||
totalQueries = stats.totalQueries || 0;
|
totalQueries = stats.totalQueries || 0;
|
||||||
blockedQueries = stats.blockedQueries || 0;
|
blockedQueries = stats.blockedQueries || 0;
|
||||||
allowedQueries = stats.allowedQueries || 0;
|
allowedQueries = stats.allowedQueries || 0;
|
||||||
errorQueries = stats.errorQueries || 0;
|
errorQueries = stats.errorQueries || 0;
|
||||||
// 获取最常查询类型
|
|
||||||
topQueryType = stats.topQueryType || 'A';
|
topQueryType = stats.topQueryType || 'A';
|
||||||
queryTypePercentage = stats.queryTypePercentage || 0;
|
queryTypePercentage = stats.queryTypePercentage || 0;
|
||||||
// 获取活跃来源IP
|
|
||||||
activeIPs = stats.activeIPs || 0;
|
activeIPs = stats.activeIPs || 0;
|
||||||
activeIPsPercentage = stats.activeIPsPercentage || 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) {
|
} else if (Array.isArray(stats) && stats.length > 0) {
|
||||||
// 可能的数据结构3: 数组形式
|
// 可能的数据结构3: 数组形式
|
||||||
totalQueries = stats[0].total || 0;
|
totalQueries = stats[0].total || 0;
|
||||||
blockedQueries = stats[0].blocked || 0;
|
blockedQueries = stats[0].blocked || 0;
|
||||||
allowedQueries = stats[0].allowed || 0;
|
allowedQueries = stats[0].allowed || 0;
|
||||||
errorQueries = stats[0].error || 0;
|
errorQueries = stats[0].error || 0;
|
||||||
// 获取最常查询类型
|
|
||||||
topQueryType = stats[0].topQueryType || 'A';
|
topQueryType = stats[0].topQueryType || 'A';
|
||||||
queryTypePercentage = stats[0].queryTypePercentage || 0;
|
queryTypePercentage = stats[0].queryTypePercentage || 0;
|
||||||
// 获取活跃来源IP
|
|
||||||
activeIPs = stats[0].activeIPs || 0;
|
activeIPs = stats[0].activeIPs || 0;
|
||||||
activeIPsPercentage = stats[0].activeIPsPercentage || 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请求统计图表
|
// 初始化DNS请求统计图表
|
||||||
drawDNSRequestsChart();
|
drawDNSRequestsChart();
|
||||||
}
|
}
|
||||||
@@ -536,8 +636,9 @@ function drawDNSRequestsChart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新图表数据
|
// 更新图表数据
|
||||||
function updateCharts(stats) {
|
function updateCharts(stats, queryTypeStats) {
|
||||||
console.log('更新图表,收到统计数据:', stats);
|
console.log('更新图表,收到统计数据:', stats);
|
||||||
|
console.log('查询类型统计数据:', queryTypeStats);
|
||||||
|
|
||||||
// 空值检查
|
// 空值检查
|
||||||
if (!stats) {
|
if (!stats) {
|
||||||
@@ -563,6 +664,35 @@ function updateCharts(stats) {
|
|||||||
ratioChart.data.datasets[0].data = [allowed, blocked, error];
|
ratioChart.data.datasets[0].data = [allowed, blocked, error];
|
||||||
ratioChart.update();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新统计卡片折线图
|
// 更新统计卡片折线图
|
||||||
|
|||||||
Reference in New Issue
Block a user