diff --git a/atsf_server/model/node_access_log.go b/atsf_server/model/node_access_log.go index 7aee1257..72ef51f0 100644 --- a/atsf_server/model/node_access_log.go +++ b/atsf_server/model/node_access_log.go @@ -31,3 +31,23 @@ func ListNodeAccessLogs(nodeID string, since time.Time, offset int, limit int) ( err = query.Find(&logs).Error return logs, err } + +func CountNodeAccessLogs(nodeID string, since time.Time) (totalRecords int64, totalIPs int64, err error) { + query := DB.Model(&NodeAccessLog{}) + if nodeID != "" { + query = query.Where("node_id = ?", nodeID) + } + if !since.IsZero() { + query = query.Where("logged_at >= ?", since) + } + if err = query.Count(&totalRecords).Error; err != nil { + return 0, 0, err + } + if err = query. + Where("remote_addr <> ''"). + Distinct("remote_addr"). + Count(&totalIPs).Error; err != nil { + return 0, 0, err + } + return totalRecords, totalIPs, nil +} diff --git a/atsf_server/service/access_log.go b/atsf_server/service/access_log.go index 78a3c175..498551b2 100644 --- a/atsf_server/service/access_log.go +++ b/atsf_server/service/access_log.go @@ -23,25 +23,33 @@ type AccessLogView struct { } type AccessLogList struct { - Items []AccessLogView `json:"items"` - Page int `json:"page"` - PageSize int `json:"page_size"` - HasMore bool `json:"has_more"` + Items []AccessLogView `json:"items"` + Page int `json:"page"` + PageSize int `json:"page_size"` + HasMore bool `json:"has_more"` + TotalRecord int64 `json:"total_record"` + TotalIP int64 `json:"total_ip"` } func ListAccessLogs(nodeID string, page int, pageSize int) (*AccessLogList, error) { normalizedPage := normalizeAccessLogPage(page) normalizedPageSize := normalizeAccessLogPageSize(pageSize) offset := normalizedPage * normalizedPageSize + trimmedNodeID := strings.TrimSpace(nodeID) + since := time.Now().Add(-nodeAccessLogRetentionWindow) logs, err := model.ListNodeAccessLogs( - strings.TrimSpace(nodeID), - time.Now().Add(-nodeAccessLogRetentionWindow), + trimmedNodeID, + since, offset, normalizedPageSize+1, ) if err != nil { return nil, err } + totalRecords, totalIPs, err := model.CountNodeAccessLogs(trimmedNodeID, since) + if err != nil { + return nil, err + } nodes, err := model.ListNodes() if err != nil { return nil, err @@ -74,10 +82,12 @@ func ListAccessLogs(nodeID string, page int, pageSize int) (*AccessLogList, erro }) } return &AccessLogList{ - Items: views, - Page: normalizedPage, - PageSize: normalizedPageSize, - HasMore: hasMore, + Items: views, + Page: normalizedPage, + PageSize: normalizedPageSize, + HasMore: hasMore, + TotalRecord: totalRecords, + TotalIP: totalIPs, }, nil } diff --git a/atsf_server/service/access_log_test.go b/atsf_server/service/access_log_test.go new file mode 100644 index 00000000..862b2848 --- /dev/null +++ b/atsf_server/service/access_log_test.go @@ -0,0 +1,94 @@ +package service + +import ( + "atsflare/model" + "testing" + "time" +) + +func TestListAccessLogsIncludesSummaryTotals(t *testing.T) { + setupServiceTestDB(t) + + now := time.Now() + if err := model.DB.Create(&model.Node{ + NodeID: "node-a", + Name: "edge-a", + }).Error; err != nil { + t.Fatalf("failed to seed node-a: %v", err) + } + if err := model.DB.Create(&model.Node{ + NodeID: "node-b", + Name: "edge-b", + }).Error; err != nil { + t.Fatalf("failed to seed node-b: %v", err) + } + + logs := []*model.NodeAccessLog{ + { + NodeID: "node-a", + LoggedAt: now.Add(-5 * time.Minute), + RemoteAddr: "1.1.1.1", + Host: "a.example.com", + Path: "/alpha", + StatusCode: 200, + }, + { + NodeID: "node-a", + LoggedAt: now.Add(-4 * time.Minute), + RemoteAddr: "2.2.2.2", + Host: "a.example.com", + Path: "/beta", + StatusCode: 404, + }, + { + NodeID: "node-b", + LoggedAt: now.Add(-3 * time.Minute), + RemoteAddr: "1.1.1.1", + Host: "b.example.com", + Path: "/gamma", + StatusCode: 502, + }, + { + NodeID: "node-b", + LoggedAt: now.Add(-2 * time.Minute), + RemoteAddr: "", + Host: "b.example.com", + Path: "/delta", + StatusCode: 200, + }, + } + if err := model.DB.Create(&logs).Error; err != nil { + t.Fatalf("failed to seed access logs: %v", err) + } + + result, err := ListAccessLogs("", 0, 2) + if err != nil { + t.Fatalf("ListAccessLogs failed: %v", err) + } + if result.TotalRecord != 4 { + t.Fatalf("expected total_record=4, got %d", result.TotalRecord) + } + if result.TotalIP != 2 { + t.Fatalf("expected total_ip=2, got %d", result.TotalIP) + } + if len(result.Items) != 2 { + t.Fatalf("expected current page items=2, got %d", len(result.Items)) + } + if !result.HasMore { + t.Fatal("expected has_more to be true") + } + + filtered, err := ListAccessLogs("node-a", 0, 50) + if err != nil { + t.Fatalf("ListAccessLogs filtered failed: %v", err) + } + if filtered.TotalRecord != 2 { + t.Fatalf("expected filtered total_record=2, got %d", filtered.TotalRecord) + } + if filtered.TotalIP != 2 { + t.Fatalf("expected filtered total_ip=2, got %d", filtered.TotalIP) + } + if len(filtered.Items) != 2 { + t.Fatalf("expected filtered items=2, got %d", len(filtered.Items)) + } +} diff --git a/atsf_server/web/features/access-logs/components/access-logs-page.tsx b/atsf_server/web/features/access-logs/components/access-logs-page.tsx index 150d844f..f1e6b7f9 100644 --- a/atsf_server/web/features/access-logs/components/access-logs-page.tsx +++ b/atsf_server/web/features/access-logs/components/access-logs-page.tsx @@ -25,18 +25,10 @@ function getErrorMessage(error: unknown) { return error instanceof Error ? error.message : '请求失败,请稍后重试。'; } -function buildSummary(logs: AccessLogItem[]) { - const uniqueIPs = new Set(logs.map((item) => item.remote_addr).filter(Boolean)); - const uniqueNodes = new Set(logs.map((item) => item.node_id).filter(Boolean)); - +function buildSummary(totalRecord = 0, totalIP = 0) { return [ - { label: '访问记录', value: logs.length }, - { label: '来源 IP', value: uniqueIPs.size }, - { label: '命中节点', value: uniqueNodes.size }, - { - label: '5xx 响应', - value: logs.filter((item) => item.status_code >= 500).length, - }, + { label: '访问记录', value: totalRecord }, + { label: '来源 IP', value: totalIP }, ]; } @@ -64,7 +56,14 @@ export function AccessLogsPage() { const logs = useMemo(() => logsQuery.data?.items ?? [], [logsQuery.data]); const hasMore = logsQuery.data?.has_more ?? false; const pageSize = logsQuery.data?.page_size ?? 50; - const summary = useMemo(() => buildSummary(logs), [logs]); + const summary = useMemo( + () => + buildSummary( + logsQuery.data?.total_record ?? 0, + logsQuery.data?.total_ip ?? 0, + ), + [logsQuery.data?.total_ip, logsQuery.data?.total_record], + ); return (