增加显示IP地理位置的功能
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dns": {
|
"dns": {
|
||||||
"port": 5353,
|
"port": 53,
|
||||||
"upstreamDNS": [
|
"upstreamDNS": [
|
||||||
"223.5.5.5:53",
|
"223.5.5.5:53",
|
||||||
"223.6.6.6:53"
|
"223.6.6.6:53"
|
||||||
@@ -115,4 +115,4 @@
|
|||||||
"maxBackups": 10,
|
"maxBackups": 10,
|
||||||
"maxAge": 30
|
"maxAge": 30
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -37,10 +38,18 @@ type ClientStats struct {
|
|||||||
LastSeen time.Time
|
LastSeen time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPGeolocation IP地理位置信息
|
||||||
|
type IPGeolocation struct {
|
||||||
|
Country string `json:"country"` // 国家
|
||||||
|
City string `json:"city"` // 城市
|
||||||
|
Expiry time.Time `json:"expiry"` // 缓存过期时间
|
||||||
|
}
|
||||||
|
|
||||||
// QueryLog 查询日志记录
|
// QueryLog 查询日志记录
|
||||||
type QueryLog struct {
|
type QueryLog struct {
|
||||||
Timestamp time.Time // 查询时间
|
Timestamp time.Time // 查询时间
|
||||||
ClientIP string // 客户端IP
|
ClientIP string // 客户端IP
|
||||||
|
Location string // IP地理位置(国家 城市)
|
||||||
Domain string // 查询域名
|
Domain string // 查询域名
|
||||||
QueryType string // 查询类型
|
QueryType string // 查询类型
|
||||||
ResponseTime int64 // 响应时间(ms)
|
ResponseTime int64 // 响应时间(ms)
|
||||||
@@ -93,6 +102,11 @@ type Server struct {
|
|||||||
saveDone chan struct{} // 用于通知保存协程停止
|
saveDone chan struct{} // 用于通知保存协程停止
|
||||||
stopped bool // 服务器是否已经停止
|
stopped bool // 服务器是否已经停止
|
||||||
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
|
stoppedMutex sync.Mutex // 保护stopped标志的互斥锁
|
||||||
|
|
||||||
|
// IP地理位置缓存
|
||||||
|
ipGeolocationCache map[string]*IPGeolocation // IP地址到地理位置的映射
|
||||||
|
ipGeolocationCacheMutex sync.RWMutex // 保护IP地理位置缓存的互斥锁
|
||||||
|
ipGeolocationCacheTTL time.Duration // 缓存有效期
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stats DNS服务器统计信息
|
// Stats DNS服务器统计信息
|
||||||
@@ -144,6 +158,9 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
|
|||||||
maxQueryLogs: 10000, // 最大保存10000条日志
|
maxQueryLogs: 10000, // 最大保存10000条日志
|
||||||
saveDone: make(chan struct{}),
|
saveDone: make(chan struct{}),
|
||||||
stopped: false, // 初始化为未停止状态
|
stopped: false, // 初始化为未停止状态
|
||||||
|
// IP地理位置缓存初始化
|
||||||
|
ipGeolocationCache: make(map[string]*IPGeolocation),
|
||||||
|
ipGeolocationCacheTTL: 24 * time.Hour, // 缓存有效期24小时
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载已保存的统计数据
|
// 加载已保存的统计数据
|
||||||
@@ -575,10 +592,14 @@ func (s *Server) updateStats(update func(*Stats)) {
|
|||||||
|
|
||||||
// addQueryLog 添加查询日志
|
// addQueryLog 添加查询日志
|
||||||
func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string) {
|
func (s *Server) addQueryLog(clientIP, domain, queryType string, responseTime int64, result, blockRule, blockType string) {
|
||||||
|
// 获取IP地理位置
|
||||||
|
location := s.getIpGeolocation(clientIP)
|
||||||
|
|
||||||
// 创建日志记录
|
// 创建日志记录
|
||||||
log := QueryLog{
|
log := QueryLog{
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
ClientIP: clientIP,
|
ClientIP: clientIP,
|
||||||
|
Location: location,
|
||||||
Domain: domain,
|
Domain: domain,
|
||||||
QueryType: queryType,
|
QueryType: queryType,
|
||||||
ResponseTime: responseTime,
|
ResponseTime: responseTime,
|
||||||
@@ -907,6 +928,83 @@ func (s *Server) GetMonthlyStats() map[string]int64 {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getIpGeolocation 获取IP地址的地理位置信息
|
||||||
|
func (s *Server) getIpGeolocation(ip string) string {
|
||||||
|
// 检查IP是否为本地地址
|
||||||
|
if ip == "127.0.0.1" || ip == "::1" {
|
||||||
|
return "本地 本地"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先检查缓存
|
||||||
|
s.ipGeolocationCacheMutex.RLock()
|
||||||
|
geo, exists := s.ipGeolocationCache[ip]
|
||||||
|
s.ipGeolocationCacheMutex.RUnlock()
|
||||||
|
|
||||||
|
// 如果缓存存在且未过期,直接返回
|
||||||
|
if exists && time.Now().Before(geo.Expiry) {
|
||||||
|
return fmt.Sprintf("%s %s", geo.Country, geo.City)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存不存在或已过期,从API获取
|
||||||
|
geoInfo, err := s.fetchIpGeolocationFromAPI(ip)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("获取IP地理位置失败", "ip", ip, "error", err)
|
||||||
|
return "未知 未知"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到缓存
|
||||||
|
s.ipGeolocationCacheMutex.Lock()
|
||||||
|
s.ipGeolocationCache[ip] = &IPGeolocation{
|
||||||
|
Country: geoInfo["country"].(string),
|
||||||
|
City: geoInfo["city"].(string),
|
||||||
|
Expiry: time.Now().Add(s.ipGeolocationCacheTTL),
|
||||||
|
}
|
||||||
|
s.ipGeolocationCacheMutex.Unlock()
|
||||||
|
|
||||||
|
// 返回格式化的地理位置
|
||||||
|
return fmt.Sprintf("%s %s", geoInfo["country"].(string), geoInfo["city"].(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchIpGeolocationFromAPI 从第三方API获取IP地理位置信息
|
||||||
|
func (s *Server) fetchIpGeolocationFromAPI(ip string) (map[string]interface{}, error) {
|
||||||
|
// 使用ip-api.com获取IP地理位置信息
|
||||||
|
url := fmt.Sprintf("http://ip-api.com/json/%s?fields=country,city", ip)
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 读取响应内容
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析JSON响应
|
||||||
|
var result map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查API返回状态
|
||||||
|
status, ok := result["status"].(string)
|
||||||
|
if !ok || status != "success" {
|
||||||
|
return nil, fmt.Errorf("API返回错误状态: %v", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保国家和城市字段存在
|
||||||
|
if _, ok := result["country"]; !ok {
|
||||||
|
result["country"] = "未知"
|
||||||
|
}
|
||||||
|
if _, ok := result["city"]; !ok {
|
||||||
|
result["city"] = "未知"
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
// loadStatsData 从文件加载统计数据
|
// loadStatsData 从文件加载统计数据
|
||||||
func (s *Server) loadStatsData() {
|
func (s *Server) loadStatsData() {
|
||||||
if s.config.StatsFile == "" {
|
if s.config.StatsFile == "" {
|
||||||
|
|||||||
41
server.log
Normal file
41
server.log
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
2025/11/30 11:09:05 正在创建所需的文件和文件夹...
|
||||||
|
2025/11/30 11:09:05 所需文件和文件夹创建成功
|
||||||
|
time="2025-11-30T11:09:05+08:00" level=debug msg="尝试加载Shield统计数据" file=/root/dnsbak/data/shield_stats.json
|
||||||
|
time="2025-11-30T11:09:05+08:00" level=info msg="Shield计数数据加载成功" blocked_entries=0 resolved_entries=0
|
||||||
|
time="2025-11-30T11:09:05+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/filter.txt"
|
||||||
|
time="2025-11-30T11:09:05+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/adaway.txt"
|
||||||
|
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/list/easylist.txt"
|
||||||
|
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/costomize.txt"
|
||||||
|
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/dsjh.txt"
|
||||||
|
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hate-and-junk-extended.txt"
|
||||||
|
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/costomize.txt"
|
||||||
|
time="2025-11-30T11:09:06+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/hosts/anti-remoterequests.txt"
|
||||||
|
time="2025-11-30T11:09:07+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/url-based-adguard.txt"
|
||||||
|
time="2025-11-30T11:09:07+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/ads-and-trackers.txt"
|
||||||
|
time="2025-11-30T11:09:08+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/malware.txt"
|
||||||
|
time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/hosts/costomize.txt"
|
||||||
|
time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="http://gitea.amazehome.xyz/AMAZEHOME/hosts-and-Filters/raw/branch/main/rules/AWAvenue-Ads-Rule.txt"
|
||||||
|
time="2025-11-30T11:09:09+08:00" level=info msg="从缓存加载远程规则" url="https://gitea.amazehome.xyz/AMAZEHOME/hosts-and-filters/raw/branch/main/rules/cheat.txt"
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="规则加载完成,域名规则: 189895, 排除规则: 653, 正则规则: 24094, hosts规则: 0"
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="统计数据加载成功"
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="查询日志加载成功" count=8608
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="DNS服务器已启动,监听端口: 5353"
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="HTTP控制台已启动,监听端口: 8081"
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="DNS TCP服务器启动,监听端口: 5353"
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="启动Shield计数数据自动保存功能" file=./data/shield_stats.json interval=60
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="HTTP控制台服务器启动,监听地址: 0.0.0.0:8081"
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="规则自动更新已启动" interval=3600
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="DNS UDP服务器启动,监听端口: 5353"
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="启动统计数据自动保存功能" file=data/stats.json interval=300
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=error msg="DNS UDP服务器启动失败" error="listen udp :5353: bind: address already in use"
|
||||||
|
time="2025-11-30T11:09:10+08:00" level=info msg="Shield计数数据保存成功" blocked_entries=0 file=/root/dnsbak/data/shield_stats.json resolved_entries=0
|
||||||
|
2025/11/30 11:09:18 正在关闭服务...
|
||||||
|
time="2025-11-30T11:09:18+08:00" level=info msg="统计数据保存成功" file=/root/dnsbak/data/stats.json
|
||||||
|
time="2025-11-30T11:09:18+08:00" level=info msg="查询日志保存成功" file=/root/dnsbak/data/querylog.json
|
||||||
|
time="2025-11-30T11:09:18+08:00" level=info msg="DNS服务器已停止"
|
||||||
|
time="2025-11-30T11:09:18+08:00" level=error msg="HTTP控制台服务器启动失败" error="http: Server closed"
|
||||||
|
time="2025-11-30T11:09:18+08:00" level=info msg="HTTP控制台服务器已停止"
|
||||||
|
time="2025-11-30T11:09:18+08:00" level=info msg="Shield计数数据保存成功" blocked_entries=0 file=/root/dnsbak/data/shield_stats.json resolved_entries=0
|
||||||
|
time="2025-11-30T11:09:18+08:00" level=info msg="规则自动更新已停止"
|
||||||
|
2025/11/30 11:09:18 服务已关闭
|
||||||
|
time="2025-11-30T11:09:18+08:00" level=warning msg="日志系统已关闭"
|
||||||
@@ -942,14 +942,14 @@
|
|||||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">屏蔽规则</th>
|
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">屏蔽规则</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="logs-table-body">
|
<tbody id="logs-table-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
|
<td colspan="5" class="py-8 text-center text-gray-500 border-b border-gray-100">
|
||||||
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
||||||
<div>暂无查询日志</div>
|
<div>暂无查询日志</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ let dnsRequestsChart = null;
|
|||||||
let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗)
|
let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗)
|
||||||
let queryTypeChart = null; // 解析类型统计饼图
|
let queryTypeChart = null; // 解析类型统计饼图
|
||||||
let intervalId = null;
|
let intervalId = null;
|
||||||
let wsConnection = null;
|
let dashboardWsConnection = null;
|
||||||
let wsReconnectTimer = null;
|
let dashboardWsReconnectTimer = null;
|
||||||
// 存储统计卡片图表实例
|
// 存储统计卡片图表实例
|
||||||
let statCardCharts = {};
|
let statCardCharts = {};
|
||||||
// 存储统计卡片历史数据
|
// 存储统计卡片历史数据
|
||||||
@@ -53,22 +53,22 @@ function connectWebSocket() {
|
|||||||
console.log('正在连接WebSocket:', wsUrl);
|
console.log('正在连接WebSocket:', wsUrl);
|
||||||
|
|
||||||
// 创建WebSocket连接
|
// 创建WebSocket连接
|
||||||
wsConnection = new WebSocket(wsUrl);
|
dashboardWsConnection = new WebSocket(wsUrl);
|
||||||
|
|
||||||
// 连接打开事件
|
// 连接打开事件
|
||||||
wsConnection.onopen = function() {
|
dashboardWsConnection.onopen = function() {
|
||||||
console.log('WebSocket连接已建立');
|
console.log('WebSocket连接已建立');
|
||||||
showNotification('数据更新成功', 'success');
|
showNotification('数据更新成功', 'success');
|
||||||
|
|
||||||
// 清除重连计时器
|
// 清除重连计时器
|
||||||
if (wsReconnectTimer) {
|
if (dashboardWsReconnectTimer) {
|
||||||
clearTimeout(wsReconnectTimer);
|
clearTimeout(dashboardWsReconnectTimer);
|
||||||
wsReconnectTimer = null;
|
dashboardWsReconnectTimer = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 接收消息事件
|
// 接收消息事件
|
||||||
wsConnection.onmessage = function(event) {
|
dashboardWsConnection.onmessage = function(event) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
@@ -82,16 +82,16 @@ function connectWebSocket() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 连接关闭事件
|
// 连接关闭事件
|
||||||
wsConnection.onclose = function(event) {
|
dashboardWsConnection.onclose = function(event) {
|
||||||
console.warn('WebSocket连接已关闭,代码:', event.code);
|
console.warn('WebSocket连接已关闭,代码:', event.code);
|
||||||
wsConnection = null;
|
dashboardWsConnection = null;
|
||||||
|
|
||||||
// 设置重连
|
// 设置重连
|
||||||
setupReconnect();
|
setupReconnect();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 连接错误事件
|
// 连接错误事件
|
||||||
wsConnection.onerror = function(error) {
|
dashboardWsConnection.onerror = function(error) {
|
||||||
console.error('WebSocket连接错误:', error);
|
console.error('WebSocket连接错误:', error);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,14 +104,14 @@ function connectWebSocket() {
|
|||||||
|
|
||||||
// 设置重连逻辑
|
// 设置重连逻辑
|
||||||
function setupReconnect() {
|
function setupReconnect() {
|
||||||
if (wsReconnectTimer) {
|
if (dashboardWsReconnectTimer) {
|
||||||
return; // 已经有重连计时器在运行
|
return; // 已经有重连计时器在运行
|
||||||
}
|
}
|
||||||
|
|
||||||
const reconnectDelay = 5000; // 5秒后重连
|
const reconnectDelay = 5000; // 5秒后重连
|
||||||
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
|
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
|
||||||
|
|
||||||
wsReconnectTimer = setTimeout(() => {
|
dashboardWsReconnectTimer = setTimeout(() => {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
}, reconnectDelay);
|
}, reconnectDelay);
|
||||||
}
|
}
|
||||||
@@ -362,15 +362,15 @@ function fallbackToIntervalRefresh() {
|
|||||||
// 清理资源
|
// 清理资源
|
||||||
function cleanupResources() {
|
function cleanupResources() {
|
||||||
// 清除WebSocket连接
|
// 清除WebSocket连接
|
||||||
if (wsConnection) {
|
if (dashboardWsConnection) {
|
||||||
wsConnection.close();
|
dashboardWsConnection.close();
|
||||||
wsConnection = null;
|
dashboardWsConnection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除重连计时器
|
// 清除重连计时器
|
||||||
if (wsReconnectTimer) {
|
if (dashboardWsReconnectTimer) {
|
||||||
clearTimeout(wsReconnectTimer);
|
clearTimeout(dashboardWsReconnectTimer);
|
||||||
wsReconnectTimer = null;
|
dashboardWsReconnectTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除定时刷新
|
// 清除定时刷新
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ let logsChart = null;
|
|||||||
let currentSortField = '';
|
let currentSortField = '';
|
||||||
let currentSortDirection = 'desc'; // 默认降序
|
let currentSortDirection = 'desc'; // 默认降序
|
||||||
|
|
||||||
|
// IP地理位置缓存
|
||||||
|
let ipGeolocationCache = {};
|
||||||
|
const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时
|
||||||
|
|
||||||
|
// WebSocket连接和重连计时器
|
||||||
|
let logsWsConnection = null;
|
||||||
|
let logsWsReconnectTimer = null;
|
||||||
|
|
||||||
// 初始化查询日志页面
|
// 初始化查询日志页面
|
||||||
function initLogsPage() {
|
function initLogsPage() {
|
||||||
console.log('初始化查询日志页面');
|
console.log('初始化查询日志页面');
|
||||||
@@ -36,15 +44,15 @@ function initLogsPage() {
|
|||||||
// 清理资源
|
// 清理资源
|
||||||
function cleanupLogsResources() {
|
function cleanupLogsResources() {
|
||||||
// 清除WebSocket连接
|
// 清除WebSocket连接
|
||||||
if (wsConnection) {
|
if (logsWsConnection) {
|
||||||
wsConnection.close();
|
logsWsConnection.close();
|
||||||
wsConnection = null;
|
logsWsConnection = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除重连计时器
|
// 清除重连计时器
|
||||||
if (wsReconnectTimer) {
|
if (logsWsReconnectTimer) {
|
||||||
clearTimeout(wsReconnectTimer);
|
clearTimeout(logsWsReconnectTimer);
|
||||||
wsReconnectTimer = null;
|
logsWsReconnectTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,9 +198,14 @@ function updateSortIcons() {
|
|||||||
|
|
||||||
// 加载日志统计数据
|
// 加载日志统计数据
|
||||||
function loadLogsStats() {
|
function loadLogsStats() {
|
||||||
fetch('/api/logs/stats')
|
// 使用封装的apiRequest函数进行API调用
|
||||||
.then(response => response.json())
|
apiRequest('/logs/stats')
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
if (data && data.error) {
|
||||||
|
console.error('加载日志统计数据失败:', data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 更新统计卡片
|
// 更新统计卡片
|
||||||
document.getElementById('logs-total-queries').textContent = data.totalQueries;
|
document.getElementById('logs-total-queries').textContent = data.totalQueries;
|
||||||
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
|
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
|
||||||
@@ -216,32 +229,50 @@ function loadLogs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 构建请求URL
|
// 构建请求URL
|
||||||
let url = `/api/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
|
let endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
|
||||||
|
|
||||||
// 添加过滤条件
|
// 添加过滤条件
|
||||||
if (currentFilter) {
|
if (currentFilter) {
|
||||||
url += `&result=${currentFilter}`;
|
endpoint += `&result=${currentFilter}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加搜索条件
|
// 添加搜索条件
|
||||||
if (currentSearch) {
|
if (currentSearch) {
|
||||||
url += `&search=${encodeURIComponent(currentSearch)}`;
|
endpoint += `&search=${encodeURIComponent(currentSearch)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加排序条件
|
// 添加排序条件
|
||||||
if (currentSortField) {
|
if (currentSortField) {
|
||||||
url += `&sort=${currentSortField}&direction=${currentSortDirection}`;
|
endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(url)
|
// 使用封装的apiRequest函数进行API调用
|
||||||
.then(response => response.json())
|
apiRequest(endpoint)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
if (data && data.error) {
|
||||||
|
console.error('加载日志详情失败:', data.error);
|
||||||
|
// 隐藏加载状态
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 加载日志总数
|
// 加载日志总数
|
||||||
return fetch('/api/logs/count').then(response => response.json()).then(countData => {
|
return apiRequest('/logs/count').then(countData => {
|
||||||
return { logs: data, count: countData.count };
|
return { logs: data, count: countData.count };
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(result => {
|
.then(result => {
|
||||||
|
if (!result || !result.logs) {
|
||||||
|
console.error('加载日志详情失败: 无效的响应数据');
|
||||||
|
// 隐藏加载状态
|
||||||
|
if (loadingEl) {
|
||||||
|
loadingEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const logs = result.logs;
|
const logs = result.logs;
|
||||||
const totalLogs = result.count;
|
const totalLogs = result.count;
|
||||||
|
|
||||||
@@ -358,7 +389,10 @@ function updateLogsTable(logs) {
|
|||||||
<div class="text-sm font-medium">${formattedTime}</div>
|
<div class="text-sm font-medium">${formattedTime}</div>
|
||||||
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
<div class="text-xs text-gray-500 mt-1">${formattedDate}</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 px-4 text-sm">${log.ClientIP}</td>
|
<td class="py-3 px-4 text-sm">
|
||||||
|
<div class="font-medium">${log.ClientIP}</div>
|
||||||
|
<div class="text-xs text-gray-500 mt-1">${log.Location || '未知 未知'}</div>
|
||||||
|
</td>
|
||||||
<td class="py-3 px-4 text-sm">
|
<td class="py-3 px-4 text-sm">
|
||||||
<div class="font-medium">${log.Domain}</div>
|
<div class="font-medium">${log.Domain}</div>
|
||||||
<div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span></div>
|
<div class="text-xs text-gray-500 mt-1">类型: ${log.QueryType}, <span class="${statusClass}">${statusText}</span></div>
|
||||||
@@ -396,9 +430,13 @@ function initLogsChart() {
|
|||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
// 获取24小时统计数据
|
// 获取24小时统计数据
|
||||||
fetch('/api/hourly-stats')
|
apiRequest('/hourly-stats')
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
if (data && data.error) {
|
||||||
|
console.error('初始化日志图表失败:', data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 创建图表
|
// 创建图表
|
||||||
logsChart = new Chart(ctx, {
|
logsChart = new Chart(ctx, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
@@ -446,24 +484,29 @@ function initLogsChart() {
|
|||||||
function updateLogsChart(range) {
|
function updateLogsChart(range) {
|
||||||
if (!logsChart) return;
|
if (!logsChart) return;
|
||||||
|
|
||||||
let url = '';
|
let endpoint = '';
|
||||||
switch (range) {
|
switch (range) {
|
||||||
case '24h':
|
case '24h':
|
||||||
url = '/api/hourly-stats';
|
endpoint = '/hourly-stats';
|
||||||
break;
|
break;
|
||||||
case '7d':
|
case '7d':
|
||||||
url = '/api/daily-stats';
|
endpoint = '/daily-stats';
|
||||||
break;
|
break;
|
||||||
case '30d':
|
case '30d':
|
||||||
url = '/api/monthly-stats';
|
endpoint = '/monthly-stats';
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
url = '/api/hourly-stats';
|
endpoint = '/hourly-stats';
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(url)
|
// 使用封装的apiRequest函数进行API调用
|
||||||
.then(response => response.json())
|
apiRequest(endpoint)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
|
if (data && data.error) {
|
||||||
|
console.error('更新日志图表失败:', data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 更新图表数据
|
// 更新图表数据
|
||||||
logsChart.data.labels = data.labels;
|
logsChart.data.labels = data.labels;
|
||||||
logsChart.data.datasets[0].data = data.data;
|
logsChart.data.datasets[0].data = data.data;
|
||||||
@@ -484,15 +527,15 @@ function connectLogsWebSocket() {
|
|||||||
console.log('正在连接WebSocket:', wsUrl);
|
console.log('正在连接WebSocket:', wsUrl);
|
||||||
|
|
||||||
// 创建WebSocket连接
|
// 创建WebSocket连接
|
||||||
wsConnection = new WebSocket(wsUrl);
|
logsWsConnection = new WebSocket(wsUrl);
|
||||||
|
|
||||||
// 连接打开事件
|
// 连接打开事件
|
||||||
wsConnection.onopen = function() {
|
logsWsConnection.onopen = function() {
|
||||||
console.log('WebSocket连接已建立');
|
console.log('WebSocket连接已建立');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 接收消息事件
|
// 接收消息事件
|
||||||
wsConnection.onmessage = function(event) {
|
logsWsConnection.onmessage = function(event) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data);
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
@@ -507,16 +550,16 @@ function connectLogsWebSocket() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 连接关闭事件
|
// 连接关闭事件
|
||||||
wsConnection.onclose = function(event) {
|
logsWsConnection.onclose = function(event) {
|
||||||
console.warn('WebSocket连接已关闭,代码:', event.code);
|
console.warn('WebSocket连接已关闭,代码:', event.code);
|
||||||
wsConnection = null;
|
logsWsConnection = null;
|
||||||
|
|
||||||
// 设置重连
|
// 设置重连
|
||||||
setupLogsReconnect();
|
setupLogsReconnect();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 连接错误事件
|
// 连接错误事件
|
||||||
wsConnection.onerror = function(error) {
|
logsWsConnection.onerror = function(error) {
|
||||||
console.error('WebSocket连接错误:', error);
|
console.error('WebSocket连接错误:', error);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -527,14 +570,14 @@ function connectLogsWebSocket() {
|
|||||||
|
|
||||||
// 设置重连逻辑
|
// 设置重连逻辑
|
||||||
function setupLogsReconnect() {
|
function setupLogsReconnect() {
|
||||||
if (wsReconnectTimer) {
|
if (logsWsReconnectTimer) {
|
||||||
return; // 已经有重连计时器在运行
|
return; // 已经有重连计时器在运行
|
||||||
}
|
}
|
||||||
|
|
||||||
const reconnectDelay = 5000; // 5秒后重连
|
const reconnectDelay = 5000; // 5秒后重连
|
||||||
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
|
console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`);
|
||||||
|
|
||||||
wsReconnectTimer = setTimeout(() => {
|
logsWsReconnectTimer = setTimeout(() => {
|
||||||
connectLogsWebSocket();
|
connectLogsWebSocket();
|
||||||
}, reconnectDelay);
|
}, reconnectDelay);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,11 @@
|
|||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('登录失败');
|
if (response.status === 401) {
|
||||||
|
throw new Error('未知用户名或密码');
|
||||||
|
} else {
|
||||||
|
throw new Error('登录失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
@@ -170,7 +174,11 @@
|
|||||||
// 登录成功,重定向到主页
|
// 登录成功,重定向到主页
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.error || '登录失败');
|
if (data.error === '用户名或密码错误') {
|
||||||
|
throw new Error('未知用户名或密码');
|
||||||
|
} else {
|
||||||
|
throw new Error(data.error || '登录失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
|||||||
Reference in New Issue
Block a user