Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
陈曦
sub2api
Commits
1c6393b1
Unverified
Commit
1c6393b1
authored
Mar 03, 2026
by
Wesley Liddick
Committed by
GitHub
Mar 03, 2026
Browse files
Merge pull request #732 from xvhuan/perf/admin-dashboard-preagg
perf(admin): 优化 Dashboard 大数据量加载(预聚合趋势+异步用户趋势)
parents
22f04e72
7be8f4dc
Changes
2
Hide whitespace changes
Inline
Side-by-side
backend/internal/repository/usage_log_repo.go
View file @
1c6393b1
...
...
@@ -1655,6 +1655,13 @@ func (r *usageLogRepository) GetBatchAPIKeyUsageStats(ctx context.Context, apiKe
// GetUsageTrendWithFilters returns usage trend data with optional filters
func
(
r
*
usageLogRepository
)
GetUsageTrendWithFilters
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
,
userID
,
apiKeyID
,
accountID
,
groupID
int64
,
model
string
,
requestType
*
int16
,
stream
*
bool
,
billingType
*
int8
)
(
results
[]
TrendDataPoint
,
err
error
)
{
if
shouldUsePreaggregatedTrend
(
granularity
,
userID
,
apiKeyID
,
accountID
,
groupID
,
model
,
requestType
,
stream
,
billingType
)
{
aggregated
,
aggregatedErr
:=
r
.
getUsageTrendFromAggregates
(
ctx
,
startTime
,
endTime
,
granularity
)
if
aggregatedErr
==
nil
&&
len
(
aggregated
)
>
0
{
return
aggregated
,
nil
}
}
dateFormat
:=
safeDateFormat
(
granularity
)
query
:=
fmt
.
Sprintf
(
`
...
...
@@ -1719,6 +1726,78 @@ func (r *usageLogRepository) GetUsageTrendWithFilters(ctx context.Context, start
return
results
,
nil
}
func
shouldUsePreaggregatedTrend
(
granularity
string
,
userID
,
apiKeyID
,
accountID
,
groupID
int64
,
model
string
,
requestType
*
int16
,
stream
*
bool
,
billingType
*
int8
)
bool
{
if
granularity
!=
"day"
&&
granularity
!=
"hour"
{
return
false
}
return
userID
==
0
&&
apiKeyID
==
0
&&
accountID
==
0
&&
groupID
==
0
&&
model
==
""
&&
requestType
==
nil
&&
stream
==
nil
&&
billingType
==
nil
}
func
(
r
*
usageLogRepository
)
getUsageTrendFromAggregates
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
granularity
string
)
(
results
[]
TrendDataPoint
,
err
error
)
{
dateFormat
:=
safeDateFormat
(
granularity
)
query
:=
""
args
:=
[]
any
{
startTime
,
endTime
}
switch
granularity
{
case
"hour"
:
query
=
fmt
.
Sprintf
(
`
SELECT
TO_CHAR(bucket_start, '%s') as date,
total_requests as requests,
input_tokens,
output_tokens,
(cache_creation_tokens + cache_read_tokens) as cache_tokens,
(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) as total_tokens,
total_cost as cost,
actual_cost
FROM usage_dashboard_hourly
WHERE bucket_start >= $1 AND bucket_start < $2
ORDER BY bucket_start ASC
`
,
dateFormat
)
case
"day"
:
query
=
fmt
.
Sprintf
(
`
SELECT
TO_CHAR(bucket_date::timestamp, '%s') as date,
total_requests as requests,
input_tokens,
output_tokens,
(cache_creation_tokens + cache_read_tokens) as cache_tokens,
(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) as total_tokens,
total_cost as cost,
actual_cost
FROM usage_dashboard_daily
WHERE bucket_date >= $1::date AND bucket_date < $2::date
ORDER BY bucket_date ASC
`
,
dateFormat
)
default
:
return
nil
,
nil
}
rows
,
err
:=
r
.
sql
.
QueryContext
(
ctx
,
query
,
args
...
)
if
err
!=
nil
{
return
nil
,
err
}
defer
func
()
{
if
closeErr
:=
rows
.
Close
();
closeErr
!=
nil
&&
err
==
nil
{
err
=
closeErr
results
=
nil
}
}()
results
,
err
=
scanTrendRows
(
rows
)
if
err
!=
nil
{
return
nil
,
err
}
return
results
,
nil
}
// GetModelStatsWithFilters returns model statistics with optional filters
func
(
r
*
usageLogRepository
)
GetModelStatsWithFilters
(
ctx
context
.
Context
,
startTime
,
endTime
time
.
Time
,
userID
,
apiKeyID
,
accountID
,
groupID
int64
,
requestType
*
int16
,
stream
*
bool
,
billingType
*
int8
)
(
results
[]
ModelStat
,
err
error
)
{
actualCostExpr
:=
"COALESCE(SUM(actual_cost), 0) as actual_cost"
...
...
frontend/src/views/admin/DashboardView.vue
View file @
1c6393b1
...
...
@@ -246,7 +246,10 @@
{{
t
(
'
admin.dashboard.recentUsage
'
)
}}
(Top 12)
</h3>
<div
class=
"h-64"
>
<Line
v-if=
"userTrendChartData"
:data=
"userTrendChartData"
:options=
"lineOptions"
/>
<div
v-if=
"userTrendLoading"
class=
"flex h-full items-center justify-center"
>
<LoadingSpinner
size=
"md"
/>
</div>
<Line
v-else-if=
"userTrendChartData"
:data=
"userTrendChartData"
:options=
"lineOptions"
/>
<div
v-else
class=
"flex h-full items-center justify-center text-sm text-gray-500 dark:text-gray-400"
...
...
@@ -306,11 +309,13 @@ const appStore = useAppStore()
const
stats
=
ref
<
DashboardStats
|
null
>
(
null
)
const
loading
=
ref
(
false
)
const
chartsLoading
=
ref
(
false
)
const
userTrendLoading
=
ref
(
false
)
// Chart data
const
trendData
=
ref
<
TrendDataPoint
[]
>
([])
const
modelStats
=
ref
<
ModelStat
[]
>
([])
const
userTrend
=
ref
<
UserUsageTrendPoint
[]
>
([])
let
chartLoadSeq
=
0
// Helper function to format date in local timezone
const
formatLocalDate
=
(
date
:
Date
):
string
=>
{
...
...
@@ -531,7 +536,9 @@ const loadDashboardStats = async () => {
}
const
loadChartData
=
async
()
=>
{
const
currentSeq
=
++
chartLoadSeq
chartsLoading
.
value
=
true
userTrendLoading
.
value
=
true
try
{
const
params
=
{
start_date
:
startDate
.
value
,
...
...
@@ -539,20 +546,39 @@ const loadChartData = async () => {
granularity
:
granularity
.
value
}
const
[
trendResponse
,
modelResponse
,
userResponse
]
=
await
Promise
.
all
([
const
[
trendResponse
,
modelResponse
]
=
await
Promise
.
all
([
adminAPI
.
dashboard
.
getUsageTrend
(
params
),
adminAPI
.
dashboard
.
getModelStats
({
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
}),
adminAPI
.
dashboard
.
getUserUsageTrend
({
...
params
,
limit
:
12
})
adminAPI
.
dashboard
.
getModelStats
({
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
})
])
if
(
currentSeq
!==
chartLoadSeq
)
return
trendData
.
value
=
trendResponse
.
trend
||
[]
modelStats
.
value
=
modelResponse
.
models
||
[]
userTrend
.
value
=
userResponse
.
trend
||
[]
}
catch
(
error
)
{
if
(
currentSeq
!==
chartLoadSeq
)
return
console
.
error
(
'
Error loading chart data:
'
,
error
)
}
finally
{
if
(
currentSeq
!==
chartLoadSeq
)
return
chartsLoading
.
value
=
false
}
try
{
const
params
=
{
start_date
:
startDate
.
value
,
end_date
:
endDate
.
value
,
granularity
:
granularity
.
value
,
limit
:
12
}
const
userResponse
=
await
adminAPI
.
dashboard
.
getUserUsageTrend
(
params
)
if
(
currentSeq
!==
chartLoadSeq
)
return
userTrend
.
value
=
userResponse
.
trend
||
[]
}
catch
(
error
)
{
if
(
currentSeq
!==
chartLoadSeq
)
return
console
.
error
(
'
Error loading user trend:
'
,
error
)
}
finally
{
if
(
currentSeq
!==
chartLoadSeq
)
return
userTrendLoading
.
value
=
false
}
}
onMounted
(()
=>
{
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment