diff --git a/docs/feat/admin-panel-dashboard-feat.md b/docs/feat/admin-panel-dashboard-feat.md index 1afd8d2..ae9951d 100644 --- a/docs/feat/admin-panel-dashboard-feat.md +++ b/docs/feat/admin-panel-dashboard-feat.md @@ -1,4 +1,4 @@ -# EZ-API 控制平面仪表盘 (Dashboard) 前后端对接功能文档 +# EZ-API admin-panel Dashboard 功能文档 ## 1. 文档概述 @@ -253,9 +253,8 @@ |--------|------| | Mock 数据 | 前端可利用 Mock 数据先行开发 UI | | Swagger 验证 | 复杂聚合接口建议先在 Swagger 中验证 | -| 自动化测试 | 利用 Mock Server 进行前端组件测试 | | 性能监控 | 关注 traffic-chart 接口在大数据量下的响应时间 | --- -*注:本对接文档基于现有端点整理,Billing 模块后端尚未实现。* +*注:本对接文档基于现有端点整理* diff --git a/internal/api/dashboard_handler.go b/internal/api/dashboard_handler.go index 3284afc..da8ac6e 100644 --- a/internal/api/dashboard_handler.go +++ b/internal/api/dashboard_handler.go @@ -89,6 +89,31 @@ func CalculateTrendFloat(current, previous float64) TrendInfo { return TrendInfo{Delta: &delta, Direction: direction} } +// CalculateTrendFloatWithBaseline calculates trend when baseline existence is explicit. +func CalculateTrendFloatWithBaseline(current, previous float64, hasBaseline bool) TrendInfo { + if !hasBaseline { + if current == 0 { + return TrendInfo{Direction: "stable"} + } + return TrendInfo{Direction: "new"} + } + if previous == 0 { + if current == 0 { + return TrendInfo{Direction: "stable"} + } + return TrendInfo{Direction: "up"} + } + delta := (current - previous) / previous * 100 + delta = math.Round(delta*10) / 10 + direction := "stable" + if delta > 0.5 { + direction = "up" + } else if delta < -0.5 { + direction = "down" + } + return TrendInfo{Delta: &delta, Direction: direction} +} + // RequestStats contains request-related statistics type RequestStats struct { Total int64 `json:"total"` @@ -302,20 +327,21 @@ func (h *DashboardHandler) GetSummary(c *gin.Context) { if !prevStart.IsZero() && !prevEnd.IsZero() { prevStats, err := h.aggregateFromLogRecords(prevStart, prevEnd) if err == nil { + hasBaseline := prevStats.Requests > 0 prevErrorRate := 0.0 - if prevStats.Requests > 0 { + if hasBaseline { prevErrorRate = float64(prevStats.Failed) / float64(prevStats.Requests) } prevAvgLatency := 0.0 - if prevStats.Requests > 0 { + if hasBaseline { prevAvgLatency = float64(prevStats.LatencySumMs) / float64(prevStats.Requests) } trends = &DashboardTrends{ Requests: CalculateTrend(totalRequests, prevStats.Requests), Tokens: CalculateTrend(ts.TokensIn+ts.TokensOut, prevStats.TokensIn+prevStats.TokensOut), - ErrorRate: CalculateTrendFloat(errorRate, prevErrorRate), - Latency: CalculateTrendFloat(ts.AvgLatency, prevAvgLatency), + ErrorRate: CalculateTrendFloatWithBaseline(errorRate, prevErrorRate, hasBaseline), + Latency: CalculateTrendFloatWithBaseline(ts.AvgLatency, prevAvgLatency, hasBaseline), } } } diff --git a/internal/api/dashboard_handler_test.go b/internal/api/dashboard_handler_test.go index ee8dd93..52baf12 100644 --- a/internal/api/dashboard_handler_test.go +++ b/internal/api/dashboard_handler_test.go @@ -162,6 +162,88 @@ func TestCalculateTrendFloat(t *testing.T) { } } +func TestCalculateTrendFloatWithBaseline(t *testing.T) { + tests := []struct { + name string + current float64 + previous float64 + hasBaseline bool + wantDelta *float64 + wantDirection string + }{ + { + name: "no baseline, zero current", + current: 0, + previous: 0, + hasBaseline: false, + wantDelta: nil, + wantDirection: "stable", + }, + { + name: "no baseline, positive current", + current: 0.2, + previous: 0, + hasBaseline: false, + wantDelta: nil, + wantDirection: "new", + }, + { + name: "baseline present, zero previous and current", + current: 0, + previous: 0, + hasBaseline: true, + wantDelta: nil, + wantDirection: "stable", + }, + { + name: "baseline present, zero previous to positive", + current: 0.2, + previous: 0, + hasBaseline: true, + wantDelta: nil, + wantDirection: "up", + }, + { + name: "baseline present, increase", + current: 0.15, + previous: 0.10, + hasBaseline: true, + wantDelta: ptr(50.0), + wantDirection: "up", + }, + { + name: "baseline present, no change", + current: 0.10, + previous: 0.10, + hasBaseline: true, + wantDelta: ptr(0.0), + wantDirection: "stable", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CalculateTrendFloatWithBaseline(tt.current, tt.previous, tt.hasBaseline) + + if tt.wantDelta == nil { + if got.Delta != nil { + t.Errorf("expected nil delta, got %v", *got.Delta) + } + } else { + if got.Delta == nil { + t.Errorf("expected delta %v, got nil", *tt.wantDelta) + } else if *got.Delta != *tt.wantDelta { + t.Errorf("expected delta %v, got %v", *tt.wantDelta, *got.Delta) + } + } + + if got.Direction != tt.wantDirection { + t.Errorf("expected direction %q, got %q", tt.wantDirection, got.Direction) + } + }) + } +} + func TestPreviousPeriodWindow(t *testing.T) { // These tests verify the structure of the period calculation // Not testing exact timestamps since they depend on current time