Merge pull request #732 from xvhuan/perf/admin-dashboard-preagg
perf(admin): 优化 Dashboard 大数据量加载(预聚合趋势+异步用户趋势)
This commit is contained in:
@@ -1655,6 +1655,13 @@ func (r *usageLogRepository) GetBatchAPIKeyUsageStats(ctx context.Context, apiKe
|
|||||||
|
|
||||||
// GetUsageTrendWithFilters returns usage trend data with optional filters
|
// 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) {
|
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)
|
dateFormat := safeDateFormat(granularity)
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
@@ -1719,6 +1726,78 @@ func (r *usageLogRepository) GetUsageTrendWithFilters(ctx context.Context, start
|
|||||||
return results, nil
|
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
|
// 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) {
|
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"
|
actualCostExpr := "COALESCE(SUM(actual_cost), 0) as actual_cost"
|
||||||
|
|||||||
@@ -246,7 +246,10 @@
|
|||||||
{{ t('admin.dashboard.recentUsage') }} (Top 12)
|
{{ t('admin.dashboard.recentUsage') }} (Top 12)
|
||||||
</h3>
|
</h3>
|
||||||
<div class="h-64">
|
<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
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="flex h-full items-center justify-center text-sm text-gray-500 dark:text-gray-400"
|
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 stats = ref<DashboardStats | null>(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const chartsLoading = ref(false)
|
const chartsLoading = ref(false)
|
||||||
|
const userTrendLoading = ref(false)
|
||||||
|
|
||||||
// Chart data
|
// Chart data
|
||||||
const trendData = ref<TrendDataPoint[]>([])
|
const trendData = ref<TrendDataPoint[]>([])
|
||||||
const modelStats = ref<ModelStat[]>([])
|
const modelStats = ref<ModelStat[]>([])
|
||||||
const userTrend = ref<UserUsageTrendPoint[]>([])
|
const userTrend = ref<UserUsageTrendPoint[]>([])
|
||||||
|
let chartLoadSeq = 0
|
||||||
|
|
||||||
// Helper function to format date in local timezone
|
// Helper function to format date in local timezone
|
||||||
const formatLocalDate = (date: Date): string => {
|
const formatLocalDate = (date: Date): string => {
|
||||||
@@ -531,7 +536,9 @@ const loadDashboardStats = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadChartData = async () => {
|
const loadChartData = async () => {
|
||||||
|
const currentSeq = ++chartLoadSeq
|
||||||
chartsLoading.value = true
|
chartsLoading.value = true
|
||||||
|
userTrendLoading.value = true
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
start_date: startDate.value,
|
start_date: startDate.value,
|
||||||
@@ -539,20 +546,39 @@ const loadChartData = async () => {
|
|||||||
granularity: granularity.value
|
granularity: granularity.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const [trendResponse, modelResponse, userResponse] = await Promise.all([
|
const [trendResponse, modelResponse] = await Promise.all([
|
||||||
adminAPI.dashboard.getUsageTrend(params),
|
adminAPI.dashboard.getUsageTrend(params),
|
||||||
adminAPI.dashboard.getModelStats({ start_date: startDate.value, end_date: endDate.value }),
|
adminAPI.dashboard.getModelStats({ start_date: startDate.value, end_date: endDate.value })
|
||||||
adminAPI.dashboard.getUserUsageTrend({ ...params, limit: 12 })
|
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (currentSeq !== chartLoadSeq) return
|
||||||
trendData.value = trendResponse.trend || []
|
trendData.value = trendResponse.trend || []
|
||||||
modelStats.value = modelResponse.models || []
|
modelStats.value = modelResponse.models || []
|
||||||
userTrend.value = userResponse.trend || []
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (currentSeq !== chartLoadSeq) return
|
||||||
console.error('Error loading chart data:', error)
|
console.error('Error loading chart data:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
if (currentSeq !== chartLoadSeq) return
|
||||||
chartsLoading.value = false
|
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(() => {
|
onMounted(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user