feat: dashboard 初版
This commit is contained in:
@@ -2,46 +2,164 @@ package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
app_errors "gpt-load/internal/errors"
|
||||
"gpt-load/internal/models"
|
||||
"gpt-load/internal/response"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetDashboardStats godoc
|
||||
// Stats godoc
|
||||
// @Summary Get dashboard statistics
|
||||
// @Description Get statistics for the dashboard, including key counts and request metrics.
|
||||
// @Description Get statistics for the dashboard cards
|
||||
// @Tags Dashboard
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Router /api/dashboard/stats [get]
|
||||
// @Success 200 {object} response.Response{data=models.DashboardStatsResponse}
|
||||
// @Router /dashboard/stats [get]
|
||||
func (s *Server) Stats(c *gin.Context) {
|
||||
var totalRequests, successRequests int64
|
||||
var groupStats []models.GroupRequestStat
|
||||
var activeKeys, invalidKeys, groupCount int64
|
||||
s.DB.Model(&models.APIKey{}).Where("status = ?", models.KeyStatusActive).Count(&activeKeys)
|
||||
s.DB.Model(&models.APIKey{}).Where("status = ?", models.KeyStatusInvalid).Count(&invalidKeys)
|
||||
s.DB.Model(&models.Group{}).Count(&groupCount)
|
||||
|
||||
// 1. Get total and successful requests from the api_keys table
|
||||
s.DB.Model(&models.APIKey{}).Select("SUM(request_count)").Row().Scan(&totalRequests)
|
||||
s.DB.Model(&models.APIKey{}).Select("SUM(request_count) - SUM(failure_count)").Row().Scan(&successRequests)
|
||||
now := time.Now()
|
||||
twentyFourHoursAgo := now.Add(-24 * time.Hour)
|
||||
fortyEightHoursAgo := now.Add(-48 * time.Hour)
|
||||
|
||||
// 2. Get request counts per group
|
||||
s.DB.Table("api_keys").
|
||||
Select("groups.display_name as display_name, SUM(api_keys.request_count) as request_count").
|
||||
Joins("join groups on groups.id = api_keys.group_id").
|
||||
Group("groups.id, groups.display_name").
|
||||
Order("request_count DESC").
|
||||
Scan(&groupStats)
|
||||
|
||||
// 3. Calculate success rate
|
||||
var successRate float64
|
||||
if totalRequests > 0 {
|
||||
successRate = float64(successRequests) / float64(totalRequests) * 100
|
||||
currentPeriod, err := s.getHourlyStats(twentyFourHoursAgo, now)
|
||||
if err != nil {
|
||||
response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get current period stats"))
|
||||
return
|
||||
}
|
||||
previousPeriod, err := s.getHourlyStats(fortyEightHoursAgo, twentyFourHoursAgo)
|
||||
if err != nil {
|
||||
response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get previous period stats"))
|
||||
return
|
||||
}
|
||||
|
||||
stats := models.DashboardStats{
|
||||
TotalRequests: totalRequests,
|
||||
SuccessRequests: successRequests,
|
||||
SuccessRate: successRate,
|
||||
GroupStats: groupStats,
|
||||
reqTrend := 0.0
|
||||
if previousPeriod.TotalRequests > 0 {
|
||||
reqTrend = (float64(currentPeriod.TotalRequests-previousPeriod.TotalRequests) / float64(previousPeriod.TotalRequests)) * 100
|
||||
}
|
||||
|
||||
currentErrorRate := 0.0
|
||||
if currentPeriod.TotalRequests > 0 {
|
||||
currentErrorRate = (float64(currentPeriod.TotalFailures) / float64(currentPeriod.TotalRequests)) * 100
|
||||
}
|
||||
|
||||
previousErrorRate := 0.0
|
||||
if previousPeriod.TotalRequests > 0 {
|
||||
previousErrorRate = (float64(previousPeriod.TotalFailures) / float64(previousPeriod.TotalRequests)) * 100
|
||||
}
|
||||
|
||||
errorRateTrend := currentErrorRate - previousErrorRate
|
||||
|
||||
stats := models.DashboardStatsResponse{
|
||||
KeyCount: models.StatCard{
|
||||
Value: float64(activeKeys),
|
||||
SubValue: invalidKeys,
|
||||
SubValueTip: "无效秘钥数量",
|
||||
},
|
||||
GroupCount: models.StatCard{
|
||||
Value: float64(groupCount),
|
||||
},
|
||||
RequestCount: models.StatCard{
|
||||
Value: float64(currentPeriod.TotalRequests),
|
||||
Trend: reqTrend,
|
||||
TrendIsGrowth: reqTrend >= 0,
|
||||
},
|
||||
ErrorRate: models.StatCard{
|
||||
Value: currentErrorRate,
|
||||
Trend: errorRateTrend,
|
||||
TrendIsGrowth: errorRateTrend < 0, // 错误率下降是好事
|
||||
},
|
||||
}
|
||||
|
||||
response.Success(c, stats)
|
||||
}
|
||||
|
||||
|
||||
// Chart godoc
|
||||
// @Summary Get dashboard chart data
|
||||
// @Description Get chart data for the last 24 hours
|
||||
// @Tags Dashboard
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param groupId query int false "Group ID"
|
||||
// @Success 200 {object} response.Response{data=models.ChartData}
|
||||
// @Router /dashboard/chart [get]
|
||||
func (s *Server) Chart(c *gin.Context) {
|
||||
groupID := c.Query("groupId")
|
||||
|
||||
now := time.Now()
|
||||
twentyFourHoursAgo := now.Add(-24 * time.Hour)
|
||||
|
||||
var hourlyStats []models.GroupHourlyStat
|
||||
query := s.DB.Where("time >= ?", twentyFourHoursAgo)
|
||||
if groupID != "" {
|
||||
query = query.Where("group_id = ?", groupID)
|
||||
}
|
||||
if err := query.Order("time asc").Find(&hourlyStats).Error; err != nil {
|
||||
response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get chart data"))
|
||||
return
|
||||
}
|
||||
|
||||
statsByHour := make(map[time.Time]map[string]int64)
|
||||
for _, stat := range hourlyStats {
|
||||
hour := stat.Time.Truncate(time.Hour)
|
||||
if _, ok := statsByHour[hour]; !ok {
|
||||
statsByHour[hour] = make(map[string]int64)
|
||||
}
|
||||
statsByHour[hour]["success"] += stat.SuccessCount
|
||||
statsByHour[hour]["failure"] += stat.FailureCount
|
||||
}
|
||||
|
||||
var labels []string
|
||||
var successData, failureData []int64
|
||||
|
||||
for i := 0; i < 24; i++ {
|
||||
hour := twentyFourHoursAgo.Add(time.Duration(i) * time.Hour).Truncate(time.Hour)
|
||||
labels = append(labels, hour.Format("15:04"))
|
||||
|
||||
if data, ok := statsByHour[hour]; ok {
|
||||
successData = append(successData, data["success"])
|
||||
failureData = append(failureData, data["failure"])
|
||||
} else {
|
||||
successData = append(successData, 0)
|
||||
failureData = append(failureData, 0)
|
||||
}
|
||||
}
|
||||
|
||||
chartData := models.ChartData{
|
||||
Labels: labels,
|
||||
Datasets: []models.ChartDataset{
|
||||
{
|
||||
Label: "成功请求",
|
||||
Data: successData,
|
||||
Color: "rgba(10, 200, 110, 1)",
|
||||
},
|
||||
{
|
||||
Label: "失败请求",
|
||||
Data: failureData,
|
||||
Color: "rgba(255, 70, 70, 1)",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
response.Success(c, chartData)
|
||||
}
|
||||
|
||||
|
||||
type hourlyStatResult struct {
|
||||
TotalRequests int64
|
||||
TotalFailures int64
|
||||
}
|
||||
|
||||
func (s *Server) getHourlyStats(startTime, endTime time.Time) (hourlyStatResult, error) {
|
||||
var result hourlyStatResult
|
||||
err := s.DB.Model(&models.GroupHourlyStat{}).
|
||||
Select("sum(success_count) + sum(failure_count) as total_requests, sum(failure_count) as total_failures").
|
||||
Where("time >= ? AND time < ?", startTime, endTime).
|
||||
Scan(&result).Error
|
||||
return result, err
|
||||
}
|
||||
|
@@ -711,3 +711,21 @@ func (s *Server) GetGroupStats(c *gin.Context) {
|
||||
|
||||
response.Success(c, resp)
|
||||
}
|
||||
|
||||
|
||||
// List godoc
|
||||
// @Summary List all groups for selection
|
||||
// @Description Get a list of all groups with their ID and display name
|
||||
// @Tags Groups
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.Response{data=[]models.Group}
|
||||
// @Router /groups/list [get]
|
||||
func (s *Server) List(c *gin.Context) {
|
||||
var groups []models.Group
|
||||
if err := s.DB.Select("id, display_name").Find(&groups).Error; err != nil {
|
||||
response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "无法获取分组列表"))
|
||||
return
|
||||
}
|
||||
response.Success(c, groups)
|
||||
}
|
||||
|
Reference in New Issue
Block a user