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 5987540..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,35 +193,89 @@ 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") - // 返回屏蔽规则的基本配置信息 + // 返回屏蔽规则的基本配置信息和统计数据,不返回完整规则列表 switch r.Method { case http.MethodGet: + // 获取规则统计信息 + 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"], + "regexExceptionsCount": stats["regexExceptions"], + "hostsRulesCount": stats["hostsRules"], } json.NewEncoder(w).Encode(shieldInfo) - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } - - // 处理远程黑名单管理子路由 - if strings.HasPrefix(r.URL.Path, "/shield/blacklists") { - s.handleShieldBlacklists(w, r) return - } - - switch r.Method { - case http.MethodGet: - // 获取完整规则列表 - rules := s.shieldManager.GetRules() - json.NewEncoder(w).Encode(rules) - case http.MethodPost: // 添加屏蔽规则 var req struct { @@ -237,7 +293,7 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { } json.NewEncoder(w).Encode(map[string]string{"status": "success"}) - + return case http.MethodDelete: // 删除屏蔽规则 var req struct { @@ -255,7 +311,7 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { } json.NewEncoder(w).Encode(map[string]string{"status": "success"}) - + return case http.MethodPut: // 重新加载规则 if err := s.shieldManager.LoadRules(); err != nil { @@ -263,9 +319,10 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) { return } json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "规则重新加载成功"}) - + return default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return } } diff --git a/package.json b/package.json new file mode 100644 index 0000000..94d5432 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "dns-server-console", + "version": "1.0.0", + "description": "DNS服务器Web控制台", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "tailwindcss": "^3.3.3", + "font-awesome": "^4.7.0", + "chart.js": "^4.4.8" + }, + "devDependencies": {}, + "keywords": ["dns", "server", "console", "web"], + "author": "", + "license": "ISC" +} \ No newline at end of file diff --git a/shield/manager.go b/shield/manager.go index bbc464d..356cbd6 100644 --- a/shield/manager.go +++ b/shield/manager.go @@ -970,22 +970,83 @@ func (m *ShieldManager) GetStats() map[string]interface{} { // loadStatsData 从文件加载计数数据 func (m *ShieldManager) loadStatsData() { if m.config.StatsFile == "" { + logger.Info("Shield统计文件路径未配置,跳过加载") return } - // 检查文件是否存在 - data, err := ioutil.ReadFile(m.config.StatsFile) + // 获取绝对路径以避免工作目录问题 + statsFilePath, err := filepath.Abs(m.config.StatsFile) if err != nil { - if !os.IsNotExist(err) { - logger.Error("读取Shield计数数据文件失败", "error", err) + logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err) + return + } + logger.Debug("尝试加载Shield统计数据", "file", statsFilePath) + + // 检查文件是否存在 + fileInfo, err := os.Stat(statsFilePath) + if err != nil { + if os.IsNotExist(err) { + logger.Info("Shield统计文件不存在,将创建新文件", "file", statsFilePath) + // 初始化空的计数数据 + m.rulesMutex.Lock() + m.blockedDomainsCount = make(map[string]int) + m.resolvedDomainsCount = make(map[string]int) + m.rulesMutex.Unlock() + // 尝试立即保存一个有效的空文件 + m.saveStatsData() + } else { + logger.Error("检查Shield统计文件失败", "file", statsFilePath, "error", err) } return } + // 检查文件大小 + if fileInfo.Size() == 0 { + logger.Warn("Shield统计文件为空,将重新初始化", "file", statsFilePath) + m.rulesMutex.Lock() + m.blockedDomainsCount = make(map[string]int) + m.resolvedDomainsCount = make(map[string]int) + m.rulesMutex.Unlock() + m.saveStatsData() + return + } + + // 读取文件内容 + data, err := ioutil.ReadFile(statsFilePath) + if err != nil { + logger.Error("读取Shield计数数据文件失败", "file", statsFilePath, "error", err) + return + } + + // 检查数据长度 + if len(data) == 0 { + logger.Warn("读取到的Shield统计数据为空", "file", statsFilePath) + return + } + + // 尝试解析JSON var statsData ShieldStatsData err = json.Unmarshal(data, &statsData) if err != nil { - logger.Error("解析Shield计数数据失败", "error", err) + // 记录更详细的错误信息,包括数据前50个字符 + dataSample := string(data) + if len(dataSample) > 50 { + dataSample = dataSample[:50] + "..." + } + logger.Error("解析Shield计数数据失败", + "file", statsFilePath, + "error", err, + "data_length", len(data), + "data_sample", dataSample) + + // 重置为默认空数据 + m.rulesMutex.Lock() + m.blockedDomainsCount = make(map[string]int) + m.resolvedDomainsCount = make(map[string]int) + m.rulesMutex.Unlock() + + // 尝试保存一个有效的空文件 + m.saveStatsData() return } @@ -993,26 +1054,38 @@ func (m *ShieldManager) loadStatsData() { m.rulesMutex.Lock() if statsData.BlockedDomainsCount != nil { m.blockedDomainsCount = statsData.BlockedDomainsCount + } else { + m.blockedDomainsCount = make(map[string]int) } if statsData.ResolvedDomainsCount != nil { m.resolvedDomainsCount = statsData.ResolvedDomainsCount + } else { + m.resolvedDomainsCount = make(map[string]int) } m.rulesMutex.Unlock() - logger.Info("Shield计数数据加载成功") + logger.Info("Shield计数数据加载成功", "blocked_entries", len(m.blockedDomainsCount), "resolved_entries", len(m.resolvedDomainsCount)) } // saveStatsData 保存计数数据到文件 func (m *ShieldManager) saveStatsData() { if m.config.StatsFile == "" { + logger.Debug("Shield统计文件路径未配置,跳过保存") + return + } + + // 获取绝对路径以避免工作目录问题 + statsFilePath, err := filepath.Abs(m.config.StatsFile) + if err != nil { + logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err) return } // 创建数据目录 - statsDir := filepath.Dir(m.config.StatsFile) - err := os.MkdirAll(statsDir, 0755) + statsDir := filepath.Dir(statsFilePath) + err = os.MkdirAll(statsDir, 0755) if err != nil { - logger.Error("创建Shield统计数据目录失败", "error", err) + logger.Error("创建Shield统计数据目录失败", "dir", statsDir, "error", err) return } @@ -1040,14 +1113,24 @@ func (m *ShieldManager) saveStatsData() { return } - // 写入文件 - err = ioutil.WriteFile(m.config.StatsFile, jsonData, 0644) + // 使用临时文件先写入,然后重命名,避免文件损坏 + tempFilePath := statsFilePath + ".tmp" + err = ioutil.WriteFile(tempFilePath, jsonData, 0644) if err != nil { - logger.Error("保存Shield计数数据到文件失败", "error", err) + logger.Error("写入临时Shield统计文件失败", "file", tempFilePath, "error", err) return } - logger.Info("Shield计数数据保存成功") + // 原子操作重命名文件 + err = os.Rename(tempFilePath, statsFilePath) + if err != nil { + logger.Error("重命名Shield统计文件失败", "temp", tempFilePath, "dest", statsFilePath, "error", err) + // 尝试清理临时文件 + os.Remove(tempFilePath) + return + } + + logger.Info("Shield计数数据保存成功", "file", statsFilePath, "blocked_entries", len(statsData.BlockedDomainsCount), "resolved_entries", len(statsData.ResolvedDomainsCount)) } // startAutoSaveStats 启动计数数据自动保存功能 diff --git a/static/index.html b/static/index.html index 34153ed..7e53a9b 100644 --- a/static/index.html +++ b/static/index.html @@ -45,7 +45,9 @@ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); } .sidebar-item-active { - @apply bg-primary/10 text-primary border-r-4 border-primary; + background-color: rgba(22, 93, 255, 0.1); + color: #165DFF; + border-right: 4px solid #165DFF; } } @@ -138,93 +140,194 @@
0
+ + + 0% +0
- - - 0% -0
+ + + 0% +0
- - - 0% -0
+ + + 0% +0
- - - 0% -0
+ + + 0% +0
- - - 0% - +0ms
+ + + 0% + +A
+ + + 0% + +0
+ + + 0% + +0%
+ + + 正常 + +