diff --git a/handlers/handlers.go b/handlers/handlers.go index 34d4bb2..5f72f2e 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -1,9 +1,18 @@ package handlers import ( + "encoding/json" + "fmt" + "log" "net/http" + "net/url" + "random-api-go/monitoring" "random-api-go/router" + "random-api-go/services" "random-api-go/stats" + "random-api-go/utils" + "strings" + "time" ) type Router interface { @@ -15,24 +24,146 @@ type Handlers struct { } func (h *Handlers) HandleAPIRequest(w http.ResponseWriter, r *http.Request) { - HandleAPIRequest(w, r) + start := time.Now() + realIP := utils.GetRealIP(r) + + // 获取并处理 referer + sourceInfo := "direct" + if referer := r.Referer(); referer != "" { + if parsedURL, err := url.Parse(referer); err == nil { + sourceInfo = parsedURL.Host + parsedURL.Path + if parsedURL.RawQuery != "" { + sourceInfo += "?" + parsedURL.RawQuery + } + } + } + + path := strings.TrimPrefix(r.URL.Path, "/") + pathSegments := strings.Split(path, "/") + + if len(pathSegments) < 2 { + monitoring.LogRequest(monitoring.RequestLog{ + Time: time.Now(), + Path: r.URL.Path, + Method: r.Method, + StatusCode: http.StatusNotFound, + Latency: float64(time.Since(start).Microseconds()) / 1000, + IP: realIP, + Referer: sourceInfo, + }) + http.NotFound(w, r) + return + } + + prefix := pathSegments[0] + suffix := pathSegments[1] + + services.Mu.RLock() + csvPath, ok := services.CSVPathsCache[prefix][suffix] + services.Mu.RUnlock() + + if !ok { + monitoring.LogRequest(monitoring.RequestLog{ + Time: time.Now(), + Path: r.URL.Path, + Method: r.Method, + StatusCode: http.StatusNotFound, + Latency: float64(time.Since(start).Microseconds()) / 1000, + IP: realIP, + Referer: sourceInfo, + }) + http.NotFound(w, r) + return + } + + selector, err := services.GetCSVContent(csvPath) + if err != nil { + log.Printf("Error fetching CSV content: %v", err) + monitoring.LogRequest(monitoring.RequestLog{ + Time: time.Now(), + Path: r.URL.Path, + Method: r.Method, + StatusCode: http.StatusInternalServerError, + Latency: float64(time.Since(start).Microseconds()) / 1000, + IP: realIP, + Referer: sourceInfo, + }) + http.Error(w, "Failed to fetch content", http.StatusInternalServerError) + return + } + + if len(selector.URLs) == 0 { + monitoring.LogRequest(monitoring.RequestLog{ + Time: time.Now(), + Path: r.URL.Path, + Method: r.Method, + StatusCode: http.StatusNotFound, + Latency: float64(time.Since(start).Microseconds()) / 1000, + IP: realIP, + Referer: sourceInfo, + }) + http.Error(w, "No content available", http.StatusNotFound) + return + } + + randomURL := selector.GetRandomURL() + endpoint := fmt.Sprintf("%s/%s", prefix, suffix) + h.Stats.IncrementCalls(endpoint) + + duration := time.Since(start) + monitoring.LogRequest(monitoring.RequestLog{ + Time: time.Now(), + Path: r.URL.Path, + Method: r.Method, + StatusCode: http.StatusFound, + Latency: float64(duration.Microseconds()) / 1000, + IP: realIP, + Referer: sourceInfo, + }) + + log.Printf(" %-12s | %-15s | %-6s | %-20s | %-20s | %-50s", + duration, + realIP, + r.Method, + r.URL.Path, + sourceInfo, + randomURL, + ) + + http.Redirect(w, r, randomURL, http.StatusFound) } func (h *Handlers) HandleStats(w http.ResponseWriter, r *http.Request) { - HandleStats(w, r) + w.Header().Set("Content-Type", "application/json") + stats := h.Stats.GetStats() + if err := json.NewEncoder(w).Encode(stats); err != nil { + http.Error(w, "Error encoding stats", http.StatusInternalServerError) + log.Printf("Error encoding stats: %v", err) + } } func (h *Handlers) HandleURLStats(w http.ResponseWriter, r *http.Request) { - HandleURLStats(w, r) + w.Header().Set("Content-Type", "application/json") + services.Mu.RLock() + response := map[string]interface{}{ + "paths": services.CSVPathsCache, + } + services.Mu.RUnlock() + json.NewEncoder(w).Encode(response) } func (h *Handlers) HandleMetrics(w http.ResponseWriter, r *http.Request) { - HandleMetrics(w, r) + metrics := monitoring.CollectMetrics() + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(metrics) } func (h *Handlers) Setup(r *router.Router) { + // 动态路由处理 r.HandleFunc("/pic/", h.HandleAPIRequest) r.HandleFunc("/video/", h.HandleAPIRequest) + + // API 统计和监控 r.HandleFunc("/stats", h.HandleStats) r.HandleFunc("/urlstats", h.HandleURLStats) r.HandleFunc("/metrics", h.HandleMetrics) diff --git a/monitoring/metrics.go b/monitoring/metrics.go index 1b1a6c5..0aed431 100644 --- a/monitoring/metrics.go +++ b/monitoring/metrics.go @@ -2,6 +2,7 @@ package monitoring import ( "runtime" + "strings" "sync" "time" ) @@ -95,16 +96,19 @@ func LogRequest(log RequestLog) { metrics.StatusCodes[log.StatusCode]++ metrics.TopReferers[log.Referer]++ - // 更新路径延迟 - if existing, ok := metrics.PathLatencies[log.Path]; ok { - metrics.PathLatencies[log.Path] = (existing + log.Latency) / 2 - } else { - metrics.PathLatencies[log.Path] = log.Latency - } + // 只记录 API 请求 + if strings.HasPrefix(log.Path, "/pic/") || strings.HasPrefix(log.Path, "/video/") { + // 更新路径延迟 + if existing, ok := metrics.PathLatencies[log.Path]; ok { + metrics.PathLatencies[log.Path] = (existing + log.Latency) / 2 + } else { + metrics.PathLatencies[log.Path] = log.Latency + } - // 保存最近请求记录 - metrics.RecentRequests = append(metrics.RecentRequests, log) - if len(metrics.RecentRequests) > 100 { - metrics.RecentRequests = metrics.RecentRequests[1:] + // 保存最近请求记录 + metrics.RecentRequests = append(metrics.RecentRequests, log) + if len(metrics.RecentRequests) > 100 { + metrics.RecentRequests = metrics.RecentRequests[1:] + } } } diff --git a/public/config/endpoint.json b/public/config/endpoint.json new file mode 100644 index 0000000..015571c --- /dev/null +++ b/public/config/endpoint.json @@ -0,0 +1,10 @@ +{ + "pic": { + "all": "随机图片", + "fj": "随机风景", + "loading": "随机加载图" + }, + "video": { + "all": "随机视频" + } +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 2bc02a7..23d0628 100644 --- a/public/index.html +++ b/public/index.html @@ -34,9 +34,9 @@ // 用于存储配置的全局变量 let cachedEndpointConfig = null; + // 加载配置的函数 async function loadEndpointConfig() { - // 如果已经有缓存的配置,直接返回 if (cachedEndpointConfig) { return cachedEndpointConfig; } @@ -46,54 +46,50 @@ if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - // 保存配置到缓存 cachedEndpointConfig = await response.json(); return cachedEndpointConfig; } catch (error) { console.error('加载endpoint配置失败:', error); - return {}; // 返回空对象作为默认值 + return {}; } } // 加载统计数据 async function loadStats() { try { - // 添加刷新动画 - const refreshIcon = document.querySelector('.refresh-icon'); - const summaryElement = document.getElementById('stats-summary'); - const detailElement = document.getElementById('stats-detail'); + const [statsResponse, urlStatsResponse, endpointConfig] = await Promise.all([ + fetch('/stats'), + fetch('/urlstats'), + loadEndpointConfig() + ]); - if (refreshIcon) { - refreshIcon.classList.add('spinning'); - } - if (summaryElement) summaryElement.classList.add('fade'); - if (detailElement) detailElement.classList.add('fade'); + const stats = await statsResponse.json(); + const urlStats = await urlStatsResponse.json(); - // 获取数据 - const response = await fetch('/stats'); - const stats = await response.json(); - - // 更新统计 - await updateStats(stats); - - // 移除动画 - setTimeout(() => { - if (refreshIcon) { - refreshIcon.classList.remove('spinning'); + // 只显示 endpoint.json 中配置的路径 + const filteredPaths = {}; + for (const [category, types] of Object.entries(endpointConfig)) { + if (urlStats.paths[category]) { + filteredPaths[category] = {}; + for (const [type, desc] of Object.entries(types)) { + if (urlStats.paths[category][type]) { + filteredPaths[category][type] = { + path: urlStats.paths[category][type], + description: desc + }; + } + } } - if (summaryElement) summaryElement.classList.remove('fade'); - if (detailElement) detailElement.classList.remove('fade'); - }, 300); + } + await updateStats(stats, filteredPaths); } catch (error) { console.error('Error loading stats:', error); } } - // 处理统计数据 - async function updateStats(stats) { - const endpointConfig = await loadEndpointConfig(); - + // 更新统计显示 + async function updateStats(stats, paths) { const startDate = new Date('2024-11-1'); const today = new Date(); const daysSinceStart = Math.ceil((today - startDate) / (1000 * 60 * 60 * 24)); @@ -101,80 +97,82 @@ let totalCalls = 0; let todayCalls = 0; + // 计算总调用次数 Object.entries(stats).forEach(([endpoint, stat]) => { - if (endpointConfig[endpoint]) { - totalCalls += stat.total_calls; - todayCalls += stat.today_calls; - } + totalCalls += stat.total_calls; + todayCalls += stat.today_calls; }); const avgCallsPerDay = Math.round(totalCalls / daysSinceStart); + // 更新总览统计 const summaryHtml = ` -
-
-

📊 接口调用次数 🔄

-
-
-
今日总调用:${todayCalls} 次
-
平均每天调用:${avgCallsPerDay} 次
-
总调用次数:${totalCalls} 次
-
统计开始日期:2024-11-1
-
-
- `; +
+
+

📊 接口调用次数 🔄

+
+
+
今日总调用:${todayCalls} 次
+
平均每天调用:${avgCallsPerDay} 次
+
总调用次数:${totalCalls} 次
+
统计开始日期:2024-11-1
+
+
+ `; - const sortedEndpoints = Object.entries(stats) - .filter(([endpoint]) => endpointConfig[endpoint]) - .sort(([endpointA], [endpointB]) => { - const orderA = endpointConfig[endpointA].order; - const orderB = endpointConfig[endpointB].order; - return orderA - orderB; - }); + // 获取 endpoint 配置 + const endpointConfig = await loadEndpointConfig(); + // 生成详细统计表格 let detailHtml = ` - - - - - - - - - - - - `; +
接口今日调用总调用URL数量查看
+ + + + + + + + + + + `; - // 同时加载URL统计数据 - const urlStatsResponse = await fetch('/urlstats'); - const urlStats = await urlStatsResponse.json(); + // 按 order 排序并生成表格行 + const endpoints = Object.entries(endpointConfig) + .sort(([, a], [, b]) => a.order - b.order); - sortedEndpoints.forEach(([endpoint, stat]) => { + for (const [endpoint, config] of endpoints) { + const stat = stats[endpoint] || { today_calls: 0, total_calls: 0 }; const urlCount = urlStats[endpoint]?.total_urls || 0; + detailHtml += ` - + `; - }); + } detailHtml += `
接口名称今日调用总调用URL数量操作
- ${getDisplayName(endpoint, endpointConfig)} + onclick="copyToClipboard('${endpoint}')" + class="endpoint-link" + title="点击复制链接"> + ${config.name} ${stat.today_calls} ${stat.total_calls} ${urlCount}👀 + 👀 + 📋 +
`; + // 更新 DOM const summaryElement = document.getElementById('stats-summary'); const detailElement = document.getElementById('stats-detail'); @@ -182,46 +180,15 @@ if (detailElement) detailElement.innerHTML = detailHtml; } - // 获取显示名称的函数 - function getDisplayName(endpoint, endpointConfig) { - return endpointConfig[endpoint]?.name || endpoint; - } - - // 修改事件处理函数来处理异步更新 - async function refreshStats() { - try { - // 如果配置还没有加载,先加载配置 - if (!cachedEndpointConfig) { - await loadEndpointConfig(); - } - - // 然后获取统计数据 - const response = await fetch('/stats'); - const stats = await response.json(); - await updateStats(stats); - } catch (error) { - console.error('Error:', error); - } - } - - // 初始加载 - document.addEventListener('DOMContentLoaded', refreshStats); - - - // 添加复制功能 + // 复制链接功能 function copyToClipboard(endpoint) { - const url = `https://random-api.czl.net/${endpoint}`; + const url = `${window.location.protocol}//${window.location.host}/${endpoint}`; navigator.clipboard.writeText(url).then(() => { - // 显示提示 const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = '链接已复制到剪贴板!'; document.body.appendChild(toast); - - // 2秒后移除提示 - setTimeout(() => { - toast.remove(); - }, 2000); + setTimeout(() => toast.remove(), 2000); }).catch(err => { console.error('复制失败:', err); });