feat: dashboard 初版

This commit is contained in:
tbphp
2025-07-12 20:24:21 +08:00
parent b0e273060b
commit e07ef88e2a
9 changed files with 1027 additions and 252 deletions

View File

@@ -66,7 +66,7 @@ func (f *Factory) GetChannel(group *models.Group) (ChannelProxy, error) {
}
}
logrus.Infof("Creating new channel for group %d with type '%s'", group.ID, group.ChannelType)
logrus.Debugf("Creating new channel for group %d with type '%s'", group.ID, group.ChannelType)
constructor, ok := channelRegistry[group.ChannelType]
if !ok {

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -85,18 +85,34 @@ type RequestLog struct {
Retries int `gorm:"not null" json:"retries"`
}
// GroupRequestStat 用于表示每个分组的请求统计
type GroupRequestStat struct {
DisplayName string `json:"display_name"`
RequestCount int64 `json:"request_count"`
// StatCard 用于仪表盘的单个统计卡片数据
type StatCard struct {
Value float64 `json:"value"`
SubValue int64 `json:"sub_value,omitempty"`
SubValueTip string `json:"sub_value_tip,omitempty"`
Trend float64 `json:"trend"`
TrendIsGrowth bool `json:"trend_is_growth"`
}
// DashboardStats 用于仪表盘的统计数据
type DashboardStats struct {
TotalRequests int64 `json:"total_requests"`
SuccessRequests int64 `json:"success_requests"`
SuccessRate float64 `json:"success_rate"`
GroupStats []GroupRequestStat `json:"group_stats"`
// DashboardStatsResponse 用于仪表盘基础统计的API响应
type DashboardStatsResponse struct {
KeyCount StatCard `json:"key_count"`
GroupCount StatCard `json:"group_count"`
RequestCount StatCard `json:"request_count"`
ErrorRate StatCard `json:"error_rate"`
}
// ChartDataset 用于图表的数据集
type ChartDataset struct {
Label string `json:"label"`
Data []int64 `json:"data"`
Color string `json:"color"`
}
// ChartData 用于图表的API响应
type ChartData struct {
Labels []string `json:"labels"`
Datasets []ChartDataset `json:"datasets"`
}
// GroupHourlyStat 对应 group_hourly_stats 表,用于存储每个分组每小时的请求统计

View File

@@ -110,6 +110,7 @@ func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Ser
{
groups.POST("", serverHandler.CreateGroup)
groups.GET("", serverHandler.ListGroups)
groups.GET("/list", serverHandler.List)
groups.GET("/config-options", serverHandler.GetGroupConfigOptions)
groups.PUT("/:id", serverHandler.UpdateGroup)
groups.DELETE("/:id", serverHandler.DeleteGroup)
@@ -136,6 +137,7 @@ func registerProtectedAPIRoutes(api *gin.RouterGroup, serverHandler *handler.Ser
dashboard := api.Group("/dashboard")
{
dashboard.GET("/stats", serverHandler.Stats)
dashboard.GET("/chart", serverHandler.Chart)
}
// 日志