From 5c01497ce0d72f8a9f54b3b34376de8ee4b2e984 Mon Sep 17 00:00:00 2001 From: zenfun Date: Fri, 2 Jan 2026 23:17:55 +0800 Subject: [PATCH] fix(api): handle zero-baseline edge cases in trend calculation Introduce `CalculateTrendFloatWithBaseline` to correctly handle scenarios where previous period metrics (Error Rate, Latency) are zero or missing. This prevents arithmetic errors and distinguishes between "new" data and actual increases ("up") when starting from zero. Also updates the admin panel dashboard documentation to reflect current project status. --- docs/feat/admin-panel-dashboard-feat.md | 5 +- internal/api/dashboard_handler.go | 34 ++++++++-- internal/api/dashboard_handler_test.go | 82 +++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 7 deletions(-) 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