@@ -11,12 +11,16 @@ import (
|
|||||||
|
|
||||||
// Stats Get dashboard statistics
|
// Stats Get dashboard statistics
|
||||||
func (s *Server) Stats(c *gin.Context) {
|
func (s *Server) Stats(c *gin.Context) {
|
||||||
var activeKeys, invalidKeys, groupCount int64
|
var activeKeys, invalidKeys int64
|
||||||
s.DB.Model(&models.APIKey{}).Where("status = ?", models.KeyStatusActive).Count(&activeKeys)
|
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.APIKey{}).Where("status = ?", models.KeyStatusInvalid).Count(&invalidKeys)
|
||||||
s.DB.Model(&models.Group{}).Count(&groupCount)
|
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
rpmStats, err := s.getRPMStats(now)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, app_errors.NewAPIError(app_errors.ErrDatabase, "failed to get rpm stats"))
|
||||||
|
return
|
||||||
|
}
|
||||||
twentyFourHoursAgo := now.Add(-24 * time.Hour)
|
twentyFourHoursAgo := now.Add(-24 * time.Hour)
|
||||||
fortyEightHoursAgo := now.Add(-48 * time.Hour)
|
fortyEightHoursAgo := now.Add(-48 * time.Hour)
|
||||||
|
|
||||||
@@ -85,9 +89,7 @@ func (s *Server) Stats(c *gin.Context) {
|
|||||||
SubValue: invalidKeys,
|
SubValue: invalidKeys,
|
||||||
SubValueTip: "无效密钥数量",
|
SubValueTip: "无效密钥数量",
|
||||||
},
|
},
|
||||||
GroupCount: models.StatCard{
|
RPM: rpmStats,
|
||||||
Value: float64(groupCount),
|
|
||||||
},
|
|
||||||
RequestCount: models.StatCard{
|
RequestCount: models.StatCard{
|
||||||
Value: float64(currentPeriod.TotalRequests),
|
Value: float64(currentPeriod.TotalRequests),
|
||||||
Trend: reqTrend,
|
Trend: reqTrend,
|
||||||
@@ -179,3 +181,42 @@ func (s *Server) getHourlyStats(startTime, endTime time.Time) (hourlyStatResult,
|
|||||||
Scan(&result).Error
|
Scan(&result).Error
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type rpmStatResult struct {
|
||||||
|
CurrentRequests int64
|
||||||
|
PreviousRequests int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) getRPMStats(now time.Time) (models.StatCard, error) {
|
||||||
|
tenMinutesAgo := now.Add(-10 * time.Minute)
|
||||||
|
twentyMinutesAgo := now.Add(-20 * time.Minute)
|
||||||
|
|
||||||
|
var result rpmStatResult
|
||||||
|
err := s.DB.Model(&models.RequestLog{}).
|
||||||
|
Select("count(case when timestamp >= ? then 1 end) as current_requests, count(case when timestamp >= ? and timestamp < ? then 1 end) as previous_requests", tenMinutesAgo, twentyMinutesAgo, tenMinutesAgo).
|
||||||
|
Where("timestamp >= ?", twentyMinutesAgo).
|
||||||
|
Scan(&result).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return models.StatCard{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentRPM := float64(result.CurrentRequests) / 10.0
|
||||||
|
previousRPM := float64(result.PreviousRequests) / 10.0
|
||||||
|
|
||||||
|
rpmTrend := 0.0
|
||||||
|
rpmTrendIsGrowth := true
|
||||||
|
if previousRPM > 0 {
|
||||||
|
rpmTrend = (currentRPM - previousRPM) / previousRPM * 100
|
||||||
|
rpmTrendIsGrowth = rpmTrend >= 0
|
||||||
|
} else if currentRPM > 0 {
|
||||||
|
rpmTrend = 100.0
|
||||||
|
rpmTrendIsGrowth = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.StatCard{
|
||||||
|
Value: currentRPM,
|
||||||
|
Trend: rpmTrend,
|
||||||
|
TrendIsGrowth: rpmTrendIsGrowth,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
@@ -107,7 +107,7 @@ type StatCard struct {
|
|||||||
// DashboardStatsResponse 用于仪表盘基础统计的API响应
|
// DashboardStatsResponse 用于仪表盘基础统计的API响应
|
||||||
type DashboardStatsResponse struct {
|
type DashboardStatsResponse struct {
|
||||||
KeyCount StatCard `json:"key_count"`
|
KeyCount StatCard `json:"key_count"`
|
||||||
GroupCount StatCard `json:"group_count"`
|
RPM StatCard `json:"rpm"`
|
||||||
RequestCount StatCard `json:"request_count"`
|
RequestCount StatCard `json:"request_count"`
|
||||||
ErrorRate StatCard `json:"error_rate"`
|
ErrorRate StatCard `json:"error_rate"`
|
||||||
}
|
}
|
||||||
|
@@ -39,7 +39,7 @@ const fetchStats = async () => {
|
|||||||
key_count:
|
key_count:
|
||||||
(stats.value?.key_count?.value ?? 0) /
|
(stats.value?.key_count?.value ?? 0) /
|
||||||
((stats.value?.key_count?.value ?? 1) + (stats.value?.key_count?.sub_value ?? 1)),
|
((stats.value?.key_count?.value ?? 1) + (stats.value?.key_count?.sub_value ?? 1)),
|
||||||
group_count: 1,
|
rpm: Math.min(100 + (stats.value?.rpm?.trend ?? 0), 100) / 100,
|
||||||
request_count: Math.min(100 + (stats.value?.request_count?.trend ?? 0), 100) / 100,
|
request_count: Math.min(100 + (stats.value?.request_count?.trend ?? 0), 100) / 100,
|
||||||
error_rate: (100 - (stats.value?.error_rate?.value ?? 0)) / 100,
|
error_rate: (100 - (stats.value?.error_rate?.value ?? 0)) / 100,
|
||||||
};
|
};
|
||||||
@@ -93,25 +93,33 @@ onMounted(() => {
|
|||||||
</n-card>
|
</n-card>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
|
|
||||||
<!-- 分组数量 -->
|
<!-- RPM (10分钟) -->
|
||||||
<n-grid-item span="1">
|
<n-grid-item span="1">
|
||||||
<n-card :bordered="false" class="stat-card" style="animation-delay: 0.05s">
|
<n-card :bordered="false" class="stat-card" style="animation-delay: 0.05s">
|
||||||
<div class="stat-header">
|
<div class="stat-header">
|
||||||
<div class="stat-icon group-icon">📁</div>
|
<div class="stat-icon rpm-icon">⏱️</div>
|
||||||
|
<n-tag
|
||||||
|
v-if="stats?.rpm && stats.rpm.trend !== undefined"
|
||||||
|
:type="stats?.rpm.trend_is_growth ? 'success' : 'error'"
|
||||||
|
size="small"
|
||||||
|
class="stat-trend"
|
||||||
|
>
|
||||||
|
{{ stats ? formatTrend(stats.rpm.trend) : "--" }}
|
||||||
|
</n-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-content">
|
<div class="stat-content">
|
||||||
<div class="stat-value">
|
<div class="stat-value">
|
||||||
{{ stats?.group_count?.value ?? 0 }}
|
{{ stats?.rpm?.value.toFixed(1) ?? 0 }}
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-title">分组数量</div>
|
<div class="stat-title">10分钟RPM</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stat-bar">
|
<div class="stat-bar">
|
||||||
<div
|
<div
|
||||||
class="stat-bar-fill group-bar"
|
class="stat-bar-fill rpm-bar"
|
||||||
:style="{
|
:style="{
|
||||||
width: `${(animatedValues.group_count ?? 0) * 100}%`,
|
width: `${(animatedValues.rpm ?? 0) * 100}%`,
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -232,7 +240,7 @@ onMounted(() => {
|
|||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-icon {
|
.rpm-icon {
|
||||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +303,7 @@ onMounted(() => {
|
|||||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-bar {
|
.rpm-bar {
|
||||||
background: linear-gradient(90deg, #f093fb 0%, #f5576c 100%);
|
background: linear-gradient(90deg, #f093fb 0%, #f5576c 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -171,7 +171,7 @@ export interface StatCard {
|
|||||||
// 仪表盘基础统计响应
|
// 仪表盘基础统计响应
|
||||||
export interface DashboardStatsResponse {
|
export interface DashboardStatsResponse {
|
||||||
key_count: StatCard;
|
key_count: StatCard;
|
||||||
group_count: StatCard;
|
rpm: StatCard;
|
||||||
request_count: StatCard;
|
request_count: StatCard;
|
||||||
error_rate: StatCard;
|
error_rate: StatCard;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user